From 1545e58ea69ffad7480774eee250ee93e3b3898d Mon Sep 17 00:00:00 2001
From: Andrew Ozz a b a b a b a b a b
are added as separate exceptions to the rule
+ return !/<(?:\/?(?!(?:div|p|br|span)>)\w+|(?:(?!(?:span style="white-space:\s?pre;?">)|br\s?\/>))\w+\s[^>]+)>/i.test(text);
+ };
+
+
+ var toBRs = function (text) {
+ return text.replace(/\r?\n/g, '
');
+ };
+
+
+ var openContainer = function (rootTag, rootAttrs) {
+ var key, attrs = [];
+ var tag = '<' + rootTag;
+
+ if (typeof rootAttrs === 'object') {
+ for (key in rootAttrs) {
+ if (rootAttrs.hasOwnProperty(key)) {
+ attrs.push(key + '="' + Entities.encodeAllRaw(rootAttrs[key]) + '"');
+ }
+ }
+
+ if (attrs.length) {
+ tag += ' ' + attrs.join(' ');
+ }
+ }
+ return tag + '>';
+ };
+
+
+ var toBlockElements = function (text, rootTag, rootAttrs) {
+ var blocks = text.split(/\n\n/);
+ var tagOpen = openContainer(rootTag, rootAttrs);
+ var tagClose = '' + rootTag + '>';
+
+ var paragraphs = Tools.map(blocks, function (p) {
+ return p.split(/\n/).join('
');
+ });
+
+ var stitch = function (p) {
+ return tagOpen + p + tagClose;
+ };
+
+ return paragraphs.length === 1 ? paragraphs[0] : Tools.map(paragraphs, stitch).join('');
+ };
+
+
+ var convert = function (text, rootTag, rootAttrs) {
+ return rootTag ? toBlockElements(text, rootTag, rootAttrs) : toBRs(text);
+ };
+
+
+ return {
+ isPlainText: isPlainText,
+ convert: convert,
+ toBRs: toBRs,
+ toBlockElements: toBlockElements
+ };
+ }
+);
+/**
+ * PasteBin.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * @class tinymce.pasteplugin.PasteBin
+ * @private
+ */
+define(
+ 'tinymce.plugins.paste.core.PasteBin',
+ [
+ 'tinymce.core.util.Tools',
+ 'tinymce.core.Env'
+ ],
+ function (Tools, Env) {
+ return function (editor) {
+ var lastRng;
+ var pasteBinDefaultContent = '%MCEPASTEBIN%';
+
+ /**
+ * Creates a paste bin element as close as possible to the current caret location and places the focus inside that element
+ * so that when the real paste event occurs the contents gets inserted into this element
+ * instead of the current editor selection element.
+ */
+ var create = function () {
+ var dom = editor.dom, body = editor.getBody();
+ var viewport = editor.dom.getViewPort(editor.getWin()), scrollTop = viewport.y, top = 20;
+ var pasteBinElm;
+ var scrollContainer;
+
+ lastRng = editor.selection.getRng();
+
+ if (editor.inline) {
+ scrollContainer = editor.selection.getScrollContainer();
+
+ // Can't always rely on scrollTop returning a useful value.
+ // It returns 0 if the browser doesn't support scrollTop for the element or is non-scrollable
+ if (scrollContainer && scrollContainer.scrollTop > 0) {
+ scrollTop = scrollContainer.scrollTop;
+ }
+ }
+
+ /**
+ * Returns the rect of the current caret if the caret is in an empty block before a
+ * BR we insert a temporary invisible character that we get the rect this way we always get a proper rect.
+ *
+ * TODO: This might be useful in core.
+ */
+ function getCaretRect(rng) {
+ var rects, textNode, node, container = rng.startContainer;
+
+ rects = rng.getClientRects();
+ if (rects.length) {
+ return rects[0];
+ }
+
+ if (!rng.collapsed || container.nodeType != 1) {
+ return;
+ }
+
+ node = container.childNodes[lastRng.startOffset];
+
+ // Skip empty whitespace nodes
+ while (node && node.nodeType == 3 && !node.data.length) {
+ node = node.nextSibling;
+ }
+
+ if (!node) {
+ return;
+ }
+
+ // Check if the location is |
+ // TODO: Might need to expand this to say |
+ if (node.tagName == 'BR') {
+ textNode = dom.doc.createTextNode('\uFEFF');
+ node.parentNode.insertBefore(textNode, node);
+
+ rng = dom.createRng();
+ rng.setStartBefore(textNode);
+ rng.setEndAfter(textNode);
+
+ rects = rng.getClientRects();
+ dom.remove(textNode);
+ }
+
+ if (rects.length) {
+ return rects[0];
+ }
+ }
+
+ // Calculate top cordinate this is needed to avoid scrolling to top of document
+ // We want the paste bin to be as close to the caret as possible to avoid scrolling
+ if (lastRng.getClientRects) {
+ var rect = getCaretRect(lastRng);
+
+ if (rect) {
+ // Client rects gets us closes to the actual
+ // caret location in for example a wrapped paragraph block
+ top = scrollTop + (rect.top - dom.getPos(body).y);
+ } else {
+ top = scrollTop;
+
+ // Check if we can find a closer location by checking the range element
+ var container = lastRng.startContainer;
+ if (container) {
+ if (container.nodeType == 3 && container.parentNode != body) {
+ container = container.parentNode;
+ }
+
+ if (container.nodeType == 1) {
+ top = dom.getPos(container, scrollContainer || body).y;
+ }
+ }
+ }
+ }
+
+ // Create a pastebin
+ pasteBinElm = editor.dom.add(editor.getBody(), 'div', {
+ id: "mcepastebin",
+ contentEditable: true,
+ "data-mce-bogus": "all",
+ style: 'position: absolute; top: ' + top + 'px; width: 10px; height: 10px; overflow: hidden; opacity: 0'
+ }, pasteBinDefaultContent);
+
+ // Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
+ if (Env.ie || Env.gecko) {
+ dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) == 'rtl' ? 0xFFFF : -0xFFFF);
+ }
+
+ // Prevent focus events from bubbeling fixed FocusManager issues
+ dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', function (e) {
+ e.stopPropagation();
+ });
+
+ pasteBinElm.focus();
+ editor.selection.select(pasteBinElm, true);
+ };
+
+ /**
+ * Removes the paste bin if it exists.
+ */
+ var remove = function () {
+ if (getEl()) {
+ var pasteBinClone;
+
+ // WebKit/Blink might clone the div so
+ // lets make sure we remove all clones
+ // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
+ while ((pasteBinClone = editor.dom.get('mcepastebin'))) {
+ editor.dom.remove(pasteBinClone);
+ editor.dom.unbind(pasteBinClone);
+ }
+
+ if (lastRng) {
+ editor.selection.setRng(lastRng);
+ }
+ }
+
+ lastRng = null;
+ };
+
+
+ var getEl = function () {
+ return editor.dom.get('mcepastebin');
+ };
+
+ /**
+ * Returns the contents of the paste bin as a HTML string.
+ *
+ * @return {String} Get the contents of the paste bin.
+ */
+ var getHtml = function () {
+ var pasteBinElm, pasteBinClones, i, dirtyWrappers, cleanWrapper;
+
+ // Since WebKit/Chrome might clone the paste bin when pasting
+ // for example:
"},d=function(a){var b=a.dom.select("*[data-mce-id]");return b[0]},e=function(a,b,e){a.undoManager.transact(function(){var f,g;a.insertContent(c(b,e)),f=d(a),f.removeAttribute("data-mce-id"),g=a.dom.select("td,th",f),a.selection.setCursorLocation(g[0],0)})},f=function(a,b){a.execCommand("FormatBlock",!1,b)},g=function(b,c,d){var e,f;e=b.editorUpload.blobCache,f=e.create(a.uuid("mceu"),d,c),e.add(f),b.insertContent(b.dom.createHTML("img",{src:f.blobUri()}))},h=function(a){a.selection.collapse(!1)},i=function(a){a.focus(),b.unlinkSelection(a),h(a)},j=function(a,b,c){a.focus(),a.dom.setAttrib(b,"href",c),h(a)},k=function(a,b){a.execCommand("mceInsertLink",!1,{href:b}),h(a)},l=function(a,b){var c=a.dom.getParent(a.selection.getStart(),"a[href]");c?j(a,c,b):k(a,b)},m=function(a,b){0===b.trim().length?i(a):l(a,b)};return{insertTable:e,formatBlock:f,insertBlob:g,createLink:m,unlink:i}}),g("u",[],function(){var a=function(a){return/^www\.|\.(com|org|edu|gov|uk|net|ca|de|jp|fr|au|us|ru|ch|it|nl|se|no|es|mil)$/i.test(a.trim())},b=function(a){return/^https?:\/\//.test(a.trim())};return{isDomainLike:a,isAbsolute:b}}),g("l",["g","j","r","p","u"],function(a,b,c,d,e){var f=function(a){a.find("textbox").eq(0).each(function(a){a.focus()})},g=function(c,d){var e=b.create(a.extend({type:"form",layout:"flex",direction:"row",padding:5,name:c,spacing:3},d));return e.on("show",function(){f(e)}),e},h=function(a,b){return b?a.show():a.hide()},i=function(a,b){return new c(function(c){a.windowManager.confirm("The URL you entered seems to be an external link. Do you want to add the required http:// prefix?",function(a){var d=a===!0?"http://"+b:b;c(d)})})},j=function(a,b){return!e.isAbsolute(b)&&e.isDomainLike(b)?i(a,b):c.resolve(b)},k=function(a,b){var c={},e=function(){a.focus(),d.unlink(a),b()},f=function(a){var b=a.meta;b&&b.attach&&(c={href:this.value(),attach:b.attach})},i=function(b){if(b.control===this){var c,d="";c=a.dom.getParent(a.selection.getStart(),"a[href]"),c&&(d=a.dom.getAttrib(c,"href")),this.fromJSON({linkurl:d}),h(this.find("#unlink"),c),this.find("#linkurl")[0].focus()}};return g("quicklink",{items:[{type:"button",name:"unlink",icon:"unlink",onclick:e,tooltip:"Remove link"},{type:"filepicker",name:"linkurl",placeholder:"Paste or type a link",filetype:"file",onchange:f},{type:"button",icon:"checkmark",subtype:"primary",tooltip:"Ok",onclick:"submit"}],onshow:i,onsubmit:function(e){j(a,e.data.linkurl).then(function(e){a.undoManager.transact(function(){e===c.href&&(c.attach(),c={}),d.createLink(a,e)}),b()})}})};return{createQuickLinkForm:k}}),g("v",["d"],function(a){return a("tinymce.geom.Rect")}),g("m",["v","q"],function(a,b){var c=function(a,b){return{rect:a,position:b}},d=function(a,b){return{x:b.x,y:b.y,w:a.w,h:a.h}},e=function(b,e,f,g,h){var i,j,k,l={x:g.x,y:g.y,w:g.w+(g.w we need to check if any of them contains some useful html.
+ // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
+
+ var copyAndRemove = function (toElm, fromElm) {
+ toElm.appendChild(fromElm);
+ editor.dom.remove(fromElm, true); // remove, but keep children
+ };
+
+ // find only top level elements (there might be more nested inside them as well, see TINY-1162)
+ pasteBinClones = Tools.grep(editor.getBody().childNodes, function (elm) {
+ return elm.id === 'mcepastebin';
+ });
+ pasteBinElm = pasteBinClones.shift();
+
+ // if clones were found, move their content into the first bin
+ Tools.each(pasteBinClones, function (pasteBinClone) {
+ copyAndRemove(pasteBinElm, pasteBinClone);
+ });
+
+ // TINY-1162: when copying plain text (from notepad for example) WebKit clones
+ // paste bin (with styles and attributes) and uses it as a default wrapper for
+ // the chunks of the content, here we cycle over the whole paste bin and replace
+ // those wrappers with a basic div
+ dirtyWrappers = editor.dom.select('div[id=mcepastebin]', pasteBinElm);
+ for (i = dirtyWrappers.length - 1; i >= 0; i--) {
+ cleanWrapper = editor.dom.create('div');
+ pasteBinElm.insertBefore(cleanWrapper, dirtyWrappers[i]);
+ copyAndRemove(cleanWrapper, dirtyWrappers[i]);
+ }
+
+ return pasteBinElm ? pasteBinElm.innerHTML : '';
+ };
+
+
+ var getLastRng = function () {
+ return lastRng;
+ };
+
+
+ var isDefaultContent = function (content) {
+ return content === pasteBinDefaultContent;
+ };
+
+
+ var isPasteBin = function (elm) {
+ return elm && elm.id === 'mcepastebin';
+ };
+
+
+ var isDefault = function () {
+ var pasteBinElm = getEl();
+ return isPasteBin(pasteBinElm) && isDefaultContent(pasteBinElm.innerHTML);
+ };
+
+ return {
+ create: create,
+ remove: remove,
+ getEl: getEl,
+ getHtml: getHtml,
+ getLastRng: getLastRng,
+ isDefault: isDefault,
+ isDefaultContent: isDefaultContent
+ };
+ };
+ }
+);
+
/**
* ResolveGlobal.js
*
@@ -284,6 +673,46 @@ define(
}
);
+/**
+ * ResolveGlobal.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define(
+ 'tinymce.core.html.Serializer',
+ [
+ 'global!tinymce.util.Tools.resolve'
+ ],
+ function (resolve) {
+ return resolve('tinymce.html.Serializer');
+ }
+);
+
+/**
+ * ResolveGlobal.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define(
+ 'tinymce.core.html.Node',
+ [
+ 'global!tinymce.util.Tools.resolve'
+ ],
+ function (resolve) {
+ return resolve('tinymce.html.Node');
+ }
+);
+
/**
* Utils.js
*
@@ -435,143 +864,7 @@ define(
);
/**
- * CutCopy.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-define(
- 'tinymce.plugins.paste.core.CutCopy',
- [
- 'tinymce.core.Env',
- 'tinymce.plugins.paste.core.InternalHtml',
- 'tinymce.plugins.paste.core.Utils'
- ],
- function (Env, InternalHtml, Utils) {
- var noop = function () {
- };
-
- var hasWorkingClipboardApi = function (clipboardData) {
- // iOS supports the clipboardData API but it doesn't do anything for cut operations
- // Edge 15 has a broken HTML Clipboard API see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11780845/
- return Env.iOS === false && clipboardData !== undefined && typeof clipboardData.setData === 'function' && Utils.isMsEdge() !== true;
- };
-
- var setHtml5Clipboard = function (clipboardData, html, text) {
- if (hasWorkingClipboardApi(clipboardData)) {
- try {
- clipboardData.clearData();
- clipboardData.setData('text/html', html);
- clipboardData.setData('text/plain', text);
- clipboardData.setData(InternalHtml.internalHtmlMime(), html);
- return true;
- } catch (e) {
- return false;
- }
- } else {
- return false;
- }
- };
-
- var setClipboardData = function (evt, data, fallback, done) {
- if (setHtml5Clipboard(evt.clipboardData, data.html, data.text)) {
- evt.preventDefault();
- done();
- } else {
- fallback(data.html, done);
- }
- };
-
- var fallback = function (editor) {
- return function (html, done) {
- var markedHtml = InternalHtml.mark(html);
- var outer = editor.dom.create('div', { contenteditable: "false" });
- var inner = editor.dom.create('div', { contenteditable: "true" }, markedHtml);
- editor.dom.setStyles(outer, {
- position: 'fixed',
- left: '-3000px',
- width: '1000px',
- overflow: 'hidden'
- });
- outer.appendChild(inner);
- editor.dom.add(editor.getBody(), outer);
-
- var range = editor.selection.getRng();
- inner.focus();
-
- var offscreenRange = editor.dom.createRng();
- offscreenRange.selectNodeContents(inner);
- editor.selection.setRng(offscreenRange);
-
- setTimeout(function () {
- outer.parentNode.removeChild(outer);
- editor.selection.setRng(range);
- done();
- }, 0);
- };
- };
-
- var getData = function (editor) {
- return {
- html: editor.selection.getContent({ contextual: true }),
- text: editor.selection.getContent({ format: 'text' })
- };
- };
-
- var cut = function (editor) {
- return function (evt) {
- if (editor.selection.isCollapsed() === false) {
- setClipboardData(evt, getData(editor), fallback(editor), function () {
- editor.execCommand('Delete');
- });
- }
- };
- };
-
- var copy = function (editor) {
- return function (evt) {
- if (editor.selection.isCollapsed() === false) {
- setClipboardData(evt, getData(editor), fallback(editor), noop);
- }
- };
- };
-
- var register = function (editor) {
- editor.on('cut', cut(editor));
- editor.on('copy', copy(editor));
- };
-
- return {
- register: register
- };
- }
-);
-/**
- * ResolveGlobal.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-define(
- 'tinymce.core.html.Entities',
- [
- 'global!tinymce.util.Tools.resolve'
- ],
- function (resolve) {
- return resolve('tinymce.html.Entities');
- }
-);
-
-/**
- * Newlines.js
+ * WordFilter.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
@@ -581,97 +874,546 @@ define(
*/
/**
- * Newlines class contains utilities to convert newlines (\n or \r\n) tp BRs or to a combination of the specified block element and BRs
+ * This class parses word HTML into proper TinyMCE markup.
*
- * @class tinymce.Newlines
+ * @class tinymce.pasteplugin.WordFilter
* @private
*/
define(
- 'tinymce.plugins.paste.core.Newlines',
+ 'tinymce.plugins.paste.core.WordFilter',
[
- 'tinymce.core.html.Entities'
+ 'tinymce.core.util.Tools',
+ 'tinymce.core.html.DomParser',
+ 'tinymce.core.html.Schema',
+ 'tinymce.core.html.Serializer',
+ 'tinymce.core.html.Node',
+ 'tinymce.plugins.paste.core.Utils'
],
- function (Entities) {
+ function (Tools, DomParser, Schema, Serializer, Node, Utils) {
+ /**
+ * Checks if the specified content is from any of the following sources: MS Word/Office 365/Google docs.
+ */
+ function isWordContent(content) {
+ return (
+ (/]*|(?:div|p|br)\s+\w[^>]+)>/.test(text);
- };
+ /**
+ * Checks if the specified text starts with "1. " or "a. " etc.
+ */
+ function isNumericList(text) {
+ var found, patterns;
+ patterns = [
+ /^[IVXLMCD]{1,2}\.[ \u00a0]/, // Roman upper case
+ /^[ivxlmcd]{1,2}\.[ \u00a0]/, // Roman lower case
+ /^[a-z]{1,2}[\.\)][ \u00a0]/, // Alphabetical a-z
+ /^[A-Z]{1,2}[\.\)][ \u00a0]/, // Alphabetical A-Z
+ /^[0-9]+\.[ \u00a0]/, // Numeric lists
+ /^[\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d]+\.[ \u00a0]/, // Japanese
+ /^[\u58f1\u5f10\u53c2\u56db\u4f0d\u516d\u4e03\u516b\u4e5d\u62fe]+\.[ \u00a0]/ // Chinese
+ ];
- var toBRs = function (text) {
- return text.replace(/\r?\n/g, '
');
- };
+ text = text.replace(/^[\u00a0 ]+/, '');
+ Tools.each(patterns, function (pattern) {
+ if (pattern.test(text)) {
+ found = true;
+ return false;
+ }
+ });
- var openContainer = function (rootTag, rootAttrs) {
- var key, attrs = [];
- var tag = '<' + rootTag;
+ return found;
+ }
- if (typeof rootAttrs === 'object') {
- for (key in rootAttrs) {
- if (rootAttrs.hasOwnProperty(key)) {
- attrs.push(key + '="' + Entities.encodeAllRaw(rootAttrs[key]) + '"');
+ function isBulletList(text) {
+ return /^[\s\u00a0]*[\u2022\u00b7\u00a7\u25CF]\s*/.test(text);
+ }
+
+ /**
+ * Converts fake bullet and numbered lists to real semantic OL/UL.
+ *
+ * @param {tinymce.html.Node} node Root node to convert children of.
+ */
+ function convertFakeListsToProperLists(node) {
+ var currentListNode, prevListNode, lastLevel = 1;
+
+ function getText(node) {
+ var txt = '';
+
+ if (node.type === 3) {
+ return node.value;
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ txt += getText(node);
+ } while ((node = node.next));
+ }
+
+ return txt;
+ }
+
+ function trimListStart(node, regExp) {
+ if (node.type === 3) {
+ if (regExp.test(node.value)) {
+ node.value = node.value.replace(regExp, '');
+ return false;
}
}
- if (attrs.length) {
- tag += ' ' + attrs.join(' ');
- }
- }
- return tag + '>';
- };
-
-
- var toBlockElements = function (text, rootTag, rootAttrs) {
- var pieces = text.split(/\r?\n/);
- var i = 0, len = pieces.length;
- var stack = [];
- var blocks = [];
- var tagOpen = openContainer(rootTag, rootAttrs);
- var tagClose = '' + rootTag + '>';
- var isLast, newlineFollows, isSingleNewline;
-
- // if single-line text then nothing to do
- if (pieces.length === 1) {
- return text;
- }
-
- for (; i < len; i++) {
- isLast = i === len - 1;
- newlineFollows = !isLast && !pieces[i + 1];
- isSingleNewline = !pieces[i] && !stack.length;
-
- stack.push(pieces[i] ? pieces[i] : ' ');
-
- if (isLast || newlineFollows || isSingleNewline) {
- blocks.push(stack.join('
'));
- stack = [];
+ if ((node = node.firstChild)) {
+ do {
+ if (!trimListStart(node, regExp)) {
+ return false;
+ }
+ } while ((node = node.next));
}
- if (newlineFollows) {
- i++; // extra progress for extra newline
+ return true;
+ }
+
+ function removeIgnoredNodes(node) {
+ if (node._listIgnore) {
+ node.remove();
+ return;
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ removeIgnoredNodes(node);
+ } while ((node = node.next));
}
}
- return blocks.length === 1 ? blocks[0] : tagOpen + blocks.join(tagClose + tagOpen) + tagClose;
+ function convertParagraphToLi(paragraphNode, listName, start) {
+ var level = paragraphNode._listLevel || lastLevel;
+
+ // Handle list nesting
+ if (level != lastLevel) {
+ if (level < lastLevel) {
+ // Move to parent list
+ if (currentListNode) {
+ currentListNode = currentListNode.parent.parent;
+ }
+ } else {
+ // Create new list
+ prevListNode = currentListNode;
+ currentListNode = null;
+ }
+ }
+
+ if (!currentListNode || currentListNode.name != listName) {
+ prevListNode = prevListNode || currentListNode;
+ currentListNode = new Node(listName, 1);
+
+ if (start > 1) {
+ currentListNode.attr('start', '' + start);
+ }
+
+ paragraphNode.wrap(currentListNode);
+ } else {
+ currentListNode.append(paragraphNode);
+ }
+
+ paragraphNode.name = 'li';
+
+ // Append list to previous list if it exists
+ if (level > lastLevel && prevListNode) {
+ prevListNode.lastChild.append(currentListNode);
+ }
+
+ lastLevel = level;
+
+ // Remove start of list item "1. " or "· " etc
+ removeIgnoredNodes(paragraphNode);
+ trimListStart(paragraphNode, /^\u00a0+/);
+ trimListStart(paragraphNode, /^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/);
+ trimListStart(paragraphNode, /^\u00a0+/);
+ }
+
+ // Build a list of all root level elements before we start
+ // altering them in the loop below.
+ var elements = [], child = node.firstChild;
+ while (typeof child !== 'undefined' && child !== null) {
+ elements.push(child);
+
+ child = child.walk();
+ if (child !== null) {
+ while (typeof child !== 'undefined' && child.parent !== node) {
+ child = child.walk();
+ }
+ }
+ }
+
+ for (var i = 0; i < elements.length; i++) {
+ node = elements[i];
+
+ if (node.name == 'p' && node.firstChild) {
+ // Find first text node in paragraph
+ var nodeText = getText(node);
+
+ // Detect unordered lists look for bullets
+ if (isBulletList(nodeText)) {
+ convertParagraphToLi(node, 'ul');
+ continue;
+ }
+
+ // Detect ordered lists 1., a. or ixv.
+ if (isNumericList(nodeText)) {
+ // Parse OL start number
+ var matches = /([0-9]+)\./.exec(nodeText);
+ var start = 1;
+ if (matches) {
+ start = parseInt(matches[1], 10);
+ }
+
+ convertParagraphToLi(node, 'ol', start);
+ continue;
+ }
+
+ // Convert paragraphs marked as lists but doesn't look like anything
+ if (node._listLevel) {
+ convertParagraphToLi(node, 'ul', 1);
+ continue;
+ }
+
+ currentListNode = null;
+ } else {
+ // If the root level element isn't a p tag which can be
+ // processed by convertParagraphToLi, it interrupts the
+ // lists, causing a new list to start instead of having
+ // elements from the next list inserted above this tag.
+ prevListNode = currentListNode;
+ currentListNode = null;
+ }
+ }
+ }
+
+ function filterStyles(editor, validStyles, node, styleValue) {
+ var outputStyles = {}, matches, styles = editor.dom.parseStyle(styleValue);
+
+ Tools.each(styles, function (value, name) {
+ // Convert various MS styles to W3C styles
+ switch (name) {
+ case 'mso-list':
+ // Parse out list indent level for lists
+ matches = /\w+ \w+([0-9]+)/i.exec(styleValue);
+ if (matches) {
+ node._listLevel = parseInt(matches[1], 10);
+ }
+
+ // Remove these nodes o
+ // Since the span gets removed we mark the text node and the span
+ if (/Ignore/i.test(value) && node.firstChild) {
+ node._listIgnore = true;
+ node.firstChild._listIgnore = true;
+ }
+
+ break;
+
+ case "horiz-align":
+ name = "text-align";
+ break;
+
+ case "vert-align":
+ name = "vertical-align";
+ break;
+
+ case "font-color":
+ case "mso-foreground":
+ name = "color";
+ break;
+
+ case "mso-background":
+ case "mso-highlight":
+ name = "background";
+ break;
+
+ case "font-weight":
+ case "font-style":
+ if (value != "normal") {
+ outputStyles[name] = value;
+ }
+ return;
+
+ case "mso-element":
+ // Remove track changes code
+ if (/^(comment|comment-list)$/i.test(value)) {
+ node.remove();
+ return;
+ }
+
+ break;
+ }
+
+ if (name.indexOf('mso-comment') === 0) {
+ node.remove();
+ return;
+ }
+
+ // Never allow mso- prefixed names
+ if (name.indexOf('mso-') === 0) {
+ return;
+ }
+
+ // Output only valid styles
+ if (editor.settings.paste_retain_style_properties == "all" || (validStyles && validStyles[name])) {
+ outputStyles[name] = value;
+ }
+ });
+
+ // Convert bold style to "b" element
+ if (/(bold)/i.test(outputStyles["font-weight"])) {
+ delete outputStyles["font-weight"];
+ node.wrap(new Node("b", 1));
+ }
+
+ // Convert italic style to "i" element
+ if (/(italic)/i.test(outputStyles["font-style"])) {
+ delete outputStyles["font-style"];
+ node.wrap(new Node("i", 1));
+ }
+
+ // Serialize the styles and see if there is something left to keep
+ outputStyles = editor.dom.serializeStyle(outputStyles, node.name);
+ if (outputStyles) {
+ return outputStyles;
+ }
+
+ return null;
+ }
+
+ var filterWordContent = function (editor, content) {
+ var retainStyleProperties, validStyles;
+
+ retainStyleProperties = editor.settings.paste_retain_style_properties;
+ if (retainStyleProperties) {
+ validStyles = Tools.makeMap(retainStyleProperties.split(/[, ]/));
+ }
+
+ // Remove basic Word junk
+ content = Utils.filter(content, [
+ // Remove apple new line markers
+ /
/gi,
+
+ // Remove google docs internal guid markers
+ /]+id="?docs-internal-[^>]*>/gi,
+
+ // Word comments like conditional comments etc
+ //gi,
+
+ // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
+ // MS Office namespaced tags, and a few other tags
+ /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
+
+ // Convert into for line-though
+ [/<(\/?)s>/gi, "<$1strike>"],
+
+ // Replace nsbp entites to char since it's easier to handle
+ [/ /gi, "\u00a0"],
+
+ // Convert ___ to string of alternating
+ // breaking/non-breaking spaces of same length
+ [/([\s\u00a0]*)<\/span>/gi,
+ function (str, spaces) {
+ return (spaces.length > 0) ?
+ spaces.replace(/./, " ").slice(Math.floor(spaces.length / 2)).split("").join("\u00a0") : "";
+ }
+ ]
+ ]);
+
+ var validElements = editor.settings.paste_word_valid_elements;
+ if (!validElements) {
+ validElements = (
+ '-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,' +
+ '-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,' +
+ 'td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody'
+ );
+ }
+
+ // Setup strict schema
+ var schema = new Schema({
+ valid_elements: validElements,
+ valid_children: '-li[p]'
+ });
+
+ // Add style/class attribute to all element rules since the user might have removed them from
+ // paste_word_valid_elements config option and we need to check them for properties
+ Tools.each(schema.elements, function (rule) {
+ /*eslint dot-notation:0*/
+ if (!rule.attributes["class"]) {
+ rule.attributes["class"] = {};
+ rule.attributesOrder.push("class");
+ }
+
+ if (!rule.attributes.style) {
+ rule.attributes.style = {};
+ rule.attributesOrder.push("style");
+ }
+ });
+
+ // Parse HTML into DOM structure
+ var domParser = new DomParser({}, schema);
+
+ // Filter styles to remove "mso" specific styles and convert some of them
+ domParser.addAttributeFilter('style', function (nodes) {
+ var i = nodes.length, node;
+
+ while (i--) {
+ node = nodes[i];
+ node.attr('style', filterStyles(editor, validStyles, node, node.attr('style')));
+
+ // Remove pointess spans
+ if (node.name == 'span' && node.parent && !node.attributes.length) {
+ node.unwrap();
+ }
+ }
+ });
+
+ // Check the class attribute for comments or del items and remove those
+ domParser.addAttributeFilter('class', function (nodes) {
+ var i = nodes.length, node, className;
+
+ while (i--) {
+ node = nodes[i];
+
+ className = node.attr('class');
+ if (/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(className)) {
+ node.remove();
+ }
+
+ node.attr('class', null);
+ }
+ });
+
+ // Remove all del elements since we don't want the track changes code in the editor
+ domParser.addNodeFilter('del', function (nodes) {
+ var i = nodes.length;
+
+ while (i--) {
+ nodes[i].remove();
+ }
+ });
+
+ // Keep some of the links and anchors
+ domParser.addNodeFilter('a', function (nodes) {
+ var i = nodes.length, node, href, name;
+
+ while (i--) {
+ node = nodes[i];
+ href = node.attr('href');
+ name = node.attr('name');
+
+ if (href && href.indexOf('#_msocom_') != -1) {
+ node.remove();
+ continue;
+ }
+
+ if (href && href.indexOf('file://') === 0) {
+ href = href.split('#')[1];
+ if (href) {
+ href = '#' + href;
+ }
+ }
+
+ if (!href && !name) {
+ node.unwrap();
+ } else {
+ // Remove all named anchors that aren't specific to TOC, Footnotes or Endnotes
+ if (name && !/^_?(?:toc|edn|ftn)/i.test(name)) {
+ node.unwrap();
+ continue;
+ }
+
+ node.attr({
+ href: href,
+ name: name
+ });
+ }
+ }
+ });
+
+ // Parse into DOM structure
+ var rootNode = domParser.parse(content);
+
+ // Process DOM
+ if (editor.settings.paste_convert_word_fake_lists !== false) {
+ convertFakeListsToProperLists(rootNode);
+ }
+
+ // Serialize DOM back to HTML
+ content = new Serializer({
+ validate: editor.settings.validate
+ }, schema).serialize(rootNode);
+
+ return content;
};
-
- var convert = function (text, rootTag, rootAttrs) {
- return rootTag ? toBlockElements(text, rootTag, rootAttrs) : toBRs(text);
+ var preProcess = function (editor, content) {
+ return editor.settings.paste_enable_default_filters === false ? content : filterWordContent(editor, content);
};
-
return {
- isPlainText: isPlainText,
- convert: convert,
- toBRs: toBRs,
- toBlockElements: toBlockElements
+ preProcess: preProcess,
+ isWordContent: isWordContent
};
}
);
+
+/**
+ * ProcessFilters.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define(
+ 'tinymce.plugins.paste.core.ProcessFilters',
+ [
+ 'tinymce.plugins.paste.api.Events',
+ 'tinymce.plugins.paste.core.WordFilter'
+ ],
+ function (Events, WordFilter) {
+ var processResult = function (content, cancelled) {
+ return { content: content, cancelled: cancelled };
+ };
+
+ var postProcessFilter = function (editor, html, internal, isWordHtml) {
+ var tempBody = editor.dom.create('div', { style: 'display:none' }, html);
+ var postProcessArgs = Events.firePastePostProcess(editor, tempBody, internal, isWordHtml);
+ return processResult(postProcessArgs.node.innerHTML, postProcessArgs.isDefaultPrevented());
+ };
+
+ var filterContent = function (editor, content, internal, isWordHtml) {
+ var preProcessArgs = Events.firePastePreProcess(editor, content, internal, isWordHtml);
+
+ if (editor.hasEventListeners('PastePostProcess') && !preProcessArgs.isDefaultPrevented()) {
+ return postProcessFilter(editor, preProcessArgs.content, internal, isWordHtml);
+ } else {
+ return processResult(preProcessArgs.content, preProcessArgs.isDefaultPrevented());
+ }
+ };
+
+ var process = function (editor, html, internal) {
+ var isWordHtml = WordFilter.isWordContent(html);
+ var content = isWordHtml ? WordFilter.preProcess(editor, html) : html;
+
+ return filterContent(editor, content, internal, isWordHtml);
+ };
+
+ return {
+ process: process
+ };
+ }
+);
+
/**
* SmartPaste.js
*
@@ -804,16 +1546,18 @@ define(
'tinymce.core.util.Delay',
'tinymce.core.util.Tools',
'tinymce.core.util.VK',
- 'tinymce.plugins.paste.core.CutCopy',
'tinymce.plugins.paste.core.InternalHtml',
'tinymce.plugins.paste.core.Newlines',
+ 'tinymce.plugins.paste.core.PasteBin',
+ 'tinymce.plugins.paste.core.ProcessFilters',
'tinymce.plugins.paste.core.SmartPaste',
'tinymce.plugins.paste.core.Utils'
],
- function (RangeUtils, Env, Delay, Tools, VK, CutCopy, InternalHtml, Newlines, SmartPaste, Utils) {
+ function (RangeUtils, Env, Delay, Tools, VK, InternalHtml, Newlines, PasteBin, ProcessFilters, SmartPaste, Utils) {
return function (editor) {
- var self = this, pasteBinElm, lastRng, keyboardPasteTimeStamp = 0, draggingInternally = false;
- var pasteBinDefaultContent = '%MCEPASTEBIN%', keyboardPastePlainTextState;
+ var self = this, keyboardPasteTimeStamp = 0, draggingInternally = false;
+ var pasteBin = new PasteBin(editor);
+ var keyboardPastePlainTextState;
var mceInternalUrlPrefix = 'data:text/mce-internal,';
var uniqueId = Utils.createIdGenerator("mceclip");
@@ -826,30 +1570,11 @@ define(
* @param {Boolean?} internalFlag Optional true/false flag if the contents is internal or external.
*/
function pasteHtml(html, internalFlag) {
- var args, dom = editor.dom, internal;
+ var internal = internalFlag ? internalFlag : InternalHtml.isMarked(html);
+ var args = ProcessFilters.process(editor, InternalHtml.unmark(html), internal);
- internal = internalFlag || InternalHtml.isMarked(html);
- html = InternalHtml.unmark(html);
-
- args = editor.fire('BeforePastePreProcess', { content: html, internal: internal }); // Internal event used by Quirks
- args = editor.fire('PastePreProcess', args);
- html = args.content;
-
- if (!args.isDefaultPrevented()) {
- // User has bound PastePostProcess events then we need to pass it through a DOM node
- // This is not ideal but we don't want to let the browser mess up the HTML for example
- // some browsers add to P tags etc
- if (editor.hasEventListeners('PastePostProcess') && !args.isDefaultPrevented()) {
- // We need to attach the element to the DOM so Sizzle selectors work on the contents
- var tempBody = dom.add(editor.getBody(), 'div', { style: 'display:none' }, html);
- args = editor.fire('PastePostProcess', { node: tempBody, internal: internal });
- dom.remove(tempBody);
- html = args.node.innerHTML;
- }
-
- if (!args.isDefaultPrevented()) {
- SmartPaste.insertContent(editor, html);
- }
+ if (args.cancelled === false) {
+ SmartPaste.insertContent(editor, args.content);
}
}
@@ -866,176 +1591,6 @@ define(
pasteHtml(text, false);
}
- /**
- * Creates a paste bin element as close as possible to the current caret location and places the focus inside that element
- * so that when the real paste event occurs the contents gets inserted into this element
- * instead of the current editor selection element.
- */
- function createPasteBin() {
- var dom = editor.dom, body = editor.getBody();
- var viewport = editor.dom.getViewPort(editor.getWin()), scrollTop = viewport.y, top = 20;
- var scrollContainer;
-
- lastRng = editor.selection.getRng();
-
- if (editor.inline) {
- scrollContainer = editor.selection.getScrollContainer();
-
- // Can't always rely on scrollTop returning a useful value.
- // It returns 0 if the browser doesn't support scrollTop for the element or is non-scrollable
- if (scrollContainer && scrollContainer.scrollTop > 0) {
- scrollTop = scrollContainer.scrollTop;
- }
- }
-
- /**
- * Returns the rect of the current caret if the caret is in an empty block before a
- * BR we insert a temporary invisible character that we get the rect this way we always get a proper rect.
- *
- * TODO: This might be useful in core.
- */
- function getCaretRect(rng) {
- var rects, textNode, node, container = rng.startContainer;
-
- rects = rng.getClientRects();
- if (rects.length) {
- return rects[0];
- }
-
- if (!rng.collapsed || container.nodeType != 1) {
- return;
- }
-
- node = container.childNodes[lastRng.startOffset];
-
- // Skip empty whitespace nodes
- while (node && node.nodeType == 3 && !node.data.length) {
- node = node.nextSibling;
- }
-
- if (!node) {
- return;
- }
-
- // Check if the location is |
- // TODO: Might need to expand this to say |
- if (node.tagName == 'BR') {
- textNode = dom.doc.createTextNode('\uFEFF');
- node.parentNode.insertBefore(textNode, node);
-
- rng = dom.createRng();
- rng.setStartBefore(textNode);
- rng.setEndAfter(textNode);
-
- rects = rng.getClientRects();
- dom.remove(textNode);
- }
-
- if (rects.length) {
- return rects[0];
- }
- }
-
- // Calculate top cordinate this is needed to avoid scrolling to top of document
- // We want the paste bin to be as close to the caret as possible to avoid scrolling
- if (lastRng.getClientRects) {
- var rect = getCaretRect(lastRng);
-
- if (rect) {
- // Client rects gets us closes to the actual
- // caret location in for example a wrapped paragraph block
- top = scrollTop + (rect.top - dom.getPos(body).y);
- } else {
- top = scrollTop;
-
- // Check if we can find a closer location by checking the range element
- var container = lastRng.startContainer;
- if (container) {
- if (container.nodeType == 3 && container.parentNode != body) {
- container = container.parentNode;
- }
-
- if (container.nodeType == 1) {
- top = dom.getPos(container, scrollContainer || body).y;
- }
- }
- }
- }
-
- // Create a pastebin
- pasteBinElm = dom.add(editor.getBody(), 'div', {
- id: "mcepastebin",
- contentEditable: true,
- "data-mce-bogus": "all",
- style: 'position: absolute; top: ' + top + 'px;' +
- 'width: 10px; height: 10px; overflow: hidden; opacity: 0'
- }, pasteBinDefaultContent);
-
- // Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
- if (Env.ie || Env.gecko) {
- dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) == 'rtl' ? 0xFFFF : -0xFFFF);
- }
-
- // Prevent focus events from bubbeling fixed FocusManager issues
- dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', function (e) {
- e.stopPropagation();
- });
-
- pasteBinElm.focus();
- editor.selection.select(pasteBinElm, true);
- }
-
- /**
- * Removes the paste bin if it exists.
- */
- function removePasteBin() {
- if (pasteBinElm) {
- var pasteBinClone;
-
- // WebKit/Blink might clone the div so
- // lets make sure we remove all clones
- // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
- while ((pasteBinClone = editor.dom.get('mcepastebin'))) {
- editor.dom.remove(pasteBinClone);
- editor.dom.unbind(pasteBinClone);
- }
-
- if (lastRng) {
- editor.selection.setRng(lastRng);
- }
- }
-
- pasteBinElm = lastRng = null;
- }
-
- /**
- * Returns the contents of the paste bin as a HTML string.
- *
- * @return {String} Get the contents of the paste bin.
- */
- function getPasteBinHtml() {
- var html = '', pasteBinClones, i, clone, cloneHtml;
-
- // Since WebKit/Chrome might clone the paste bin when pasting
- // for example:
"},d=function(a){var b=a.dom.select("*[data-mce-id]");return b[0]},e=function(a,b,e){a.undoManager.transact(function(){var f,g;a.insertContent(c(b,e)),f=d(a),f.removeAttribute("data-mce-id"),g=a.dom.select("td,th",f),a.selection.setCursorLocation(g[0],0)})},f=function(a,b){a.execCommand("FormatBlock",!1,b)},g=function(b,c,d){var e,f;e=b.editorUpload.blobCache,f=e.create(a.uuid("mceu"),d,c),e.add(f),b.insertContent(b.dom.createHTML("img",{src:f.blobUri()}))},h=function(a){a.selection.collapse(!1)},i=function(a){a.focus(),b.unlinkSelection(a),h(a)},j=function(a,b,c){a.focus(),a.dom.setAttrib(b,"href",c),h(a)},k=function(a,b){a.execCommand("mceInsertLink",!1,{href:b}),h(a)},l=function(a,b){var c=a.dom.getParent(a.selection.getStart(),"a[href]");c?j(a,c,b):k(a,b)},m=function(a,b){0===b.trim().length?i(a):l(a,b)};return{insertTable:e,formatBlock:f,insertBlob:g,createLink:m,unlink:i}}),g("v",[],function(){var a=function(a){return/^www\.|\.(com|org|edu|gov|uk|net|ca|de|jp|fr|au|us|ru|ch|it|nl|se|no|es|mil)$/i.test(a.trim())},b=function(a){return/^https?:\/\//.test(a.trim())};return{isDomainLike:a,isAbsolute:b}}),g("l",["g","j","s","p","v"],function(a,b,c,d,e){var f=function(a){a.find("textbox").eq(0).each(function(a){a.focus()})},g=function(c,d){var e=b.create(a.extend({type:"form",layout:"flex",direction:"row",padding:5,name:c,spacing:3},d));return e.on("show",function(){f(e)}),e},h=function(a,b){return b?a.show():a.hide()},i=function(a,b){return new c(function(c){a.windowManager.confirm("The URL you entered seems to be an external link. Do you want to add the required http:// prefix?",function(a){var d=a===!0?"http://"+b:b;c(d)})})},j=function(a,b){return!e.isAbsolute(b)&&e.isDomainLike(b)?i(a,b):c.resolve(b)},k=function(a,b){var c={},e=function(){a.focus(),d.unlink(a),b()},f=function(a){var b=a.meta;b&&b.attach&&(c={href:this.value(),attach:b.attach})},i=function(b){if(b.control===this){var c,d="";c=a.dom.getParent(a.selection.getStart(),"a[href]"),c&&(d=a.dom.getAttrib(c,"href")),this.fromJSON({linkurl:d}),h(this.find("#unlink"),c),this.find("#linkurl")[0].focus()}};return g("quicklink",{items:[{type:"button",name:"unlink",icon:"unlink",onclick:e,tooltip:"Remove link"},{type:"filepicker",name:"linkurl",placeholder:"Paste or type a link",filetype:"file",onchange:f},{type:"button",icon:"checkmark",subtype:"primary",tooltip:"Ok",onclick:"submit"}],onshow:i,onsubmit:function(e){j(a,e.data.linkurl).then(function(e){a.undoManager.transact(function(){e===c.href&&(c.attach(),c={}),d.createLink(a,e)}),b()})}})};return{createQuickLinkForm:k}}),g("m",["q","r"],function(a,b){var c=function(a,b){return{rect:a,position:b}},d=function(a,b){return{x:b.x,y:b.y,w:a.w,h:a.h}},e=function(b,e,f,g,h){var i,j,k;return i=a.findBestRelativePosition(h,f,g,b),f=a.clamp(f,g),i?(j=a.relativePosition(h,f,i),k=d(h,j),c(k,i)):(f=a.intersect(g,f),f?(i=a.findBestRelativePosition(h,f,g,e))?(j=a.relativePosition(h,f,i),k=d(h,j),c(k,i)):(k=d(h,f),c(k,i)):null)},f=function(a,b,c){return e(["cr-cl","cl-cr"],["bc-tc","bl-tl","br-tr"],a,b,c)},g=function(a,b,c){return e(["tc-bc","bc-tc","tl-bl","bl-tl","tr-br","br-tr"],["bc-tc","bl-tl","br-tr"],a,b,c)},h=function(a,c,d,e){var f;return"function"==typeof a?(f=a({elementRect:b.toClientRect(c),contentAreaRect:b.toClientRect(d),panelRect:b.toClientRect(e)}),b.fromClientRect(f)):e},i=function(a){return a.panelRect};return{calcInsert:f,calc:g,userConstrain:h,defaultHandler:i}}),g("c",["g","j","i","k","l","f","m","5"],function(a,b,c,d,e,f,g,h){return function(){var i,j,k=["bold","italic","|","quicklink","h2","h3","blockquote"],l=["quickimage","quicktable"],m=function(b,c){return a.map(c,function(a){return d.create(b,a.id,a.items)})},n=function(a){return h.getToolbarItemsOr(a,"selection_toolbar",k)},o=function(a){return h.getToolbarItemsOr(a,"insert_toolbar",l)},p=function(a){return a.items().length>0},q=function(c,f){var g=m(c,f).concat([d.create(c,"text",n(c)),d.create(c,"insert",o(c)),e.createQuickLinkForm(c,B)]);return b.create({type:"floatpanel",role:"dialog",classes:"tinymce tinymce-inline arrow",ariaLabel:"Inline toolbar",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!0,border:1,items:a.grep(g,p),oncancel:function(){c.focus()}})},r=function(a){a&&a.show()},s=function(a,b){a.moveTo(b.x,b.y)},t=function(b,c){c=c?c.substr(0,2):"",a.each({t:"down",b:"up",c:"center"},function(a,d){b.classes.toggle("arrow-"+a,d===c.substr(0,1))}),"cr"===c?(b.classes.toggle("arrow-left",!0),b.classes.toggle("arrow-right",!1)):"cl"===c?(b.classes.toggle("arrow-left",!0),b.classes.toggle("arrow-right",!0)):a.each({l:"left",r:"right"},function(a,d){b.classes.toggle("arrow-"+a,d===c.substr(1,1))})},u=function(a,b){var c=a.items().filter("#"+b);return c.length>0&&(c[0].show(),a.reflow(),!0)},v=function(a,b,d,e){var i,k,l,m;return m=h.getHandlerOr(d,"inline_toolbar_position_handler",g.defaultHandler),i=f.getContentAreaRect(d),k=c.DOM.getRect(a.getEl()),l="insert"===b?g.calcInsert(e,i,k):g.calc(e,i,k),!!l&&(k=l.rect,j=e,s(a,g.userConstrain(m,e,i,k)),t(a,l.position),!0)},w=function(a,b,c,d){return r(a),a.items().hide(),u(a,b)?void(v(a,b,c,d)===!1&&B(a)):void B(a)},x=function(){return i.items().filter("form:visible").length>0},y=function(a,b){if(i){if(i.items().hide(),!u(i,b))return void B(i);var d,e,k,l;r(i),i.items().hide(),u(i,b),l=h.getHandlerOr(a,"inline_toolbar_position_handler",g.defaultHandler),d=f.getContentAreaRect(a),e=c.DOM.getRect(i.getEl()),k=g.calc(j,d,e),k&&(e=k.rect,s(i,g.userConstrain(l,j,d,e)),t(i,k.position))}},z=function(a,b,c,d){i||(i=q(a,d),i.renderTo(document.body).reflow().moveTo(c.x,c.y),a.nodeChanged()),w(i,b,a,c)},A=function(a,b,c){i&&v(i,b,a,c)},B=function(){i&&i.hide()},C=function(){i&&i.find("toolbar:visible").eq(0).each(function(a){a.focus(!0)})},D=function(){i&&(i.remove(),i=null)},E=function(){return i&&i.visible()&&x()};return{show:z,showForm:y,reposition:A,inForm:E,hide:B,focus:C,remove:D}}}),g("n",["s"],function(a){var b=function(b){return new a(function(a){var c=new FileReader;c.onloadend=function(){a(c.result.split(",")[1])},c.readAsDataURL(b)})};return{blobToBase64:b}}),g("o",["s"],function(a){var b=function(){return new a(function(a){var b;b=document.createElement("input"),b.type="file",b.style.position="fixed",b.style.left=0,b.style.top=0,b.style.opacity=.001,document.body.appendChild(b),b.onchange=function(b){a(Array.prototype.slice.call(b.target.files))},b.click(),b.parentNode.removeChild(b)})};return{pickFile:b}}),g("b",["c","n","o","p"],function(a,b,c,d){var e=function(a){for(var b=function(b){return function(){d.formatBlock(a,b)}},c=1;c<6;c++){var e="h"+c;a.addButton(e,{text:e.toUpperCase(),tooltip:"Heading "+c,stateSelector:e,onclick:b(e),onPostRender:function(){var a=this.getEl().firstChild.firstChild;a.style.fontWeight="bold"}})}},f=function(a,f){a.addButton("quicklink",{icon:"link",tooltip:"Insert/Edit link",stateSelector:"a[href]",onclick:function(){f.showForm(a,"quicklink")}}),a.addButton("quickimage",{icon:"image",tooltip:"Insert image",onclick:function(){c.pickFile().then(function(c){var e=c[0];b.blobToBase64(e).then(function(b){d.insertBlob(a,b,e)})})}}),a.addButton("quicktable",{icon:"table",tooltip:"Insert table",onclick:function(){f.hide(),d.insertTable(a,2,2)}}),e(a)};return{addToEditor:f}}),g("0",["1","2","3","4","5","6","7","8","9","a","b","c"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=function(a){var b=a.selection.getNode(),c=a.dom.getParents(b);return c},n=function(a,b,c,d){var e=function(c){return a.dom.is(c,b)};return{predicate:e,id:c,items:d}},o=function(a){var b=a.contextToolbars;return d.flatten([b?b:[],n(a,"img","image","alignleft aligncenter alignright")])},p=function(a,b){var c,d,e;return d=m(a),e=h.fromContextToolbars(b),c=g.match(a,[f.element(d[0],e),i.textSelection("text"),i.emptyTextBlock(d,"insert"),f.parent(d,e)]),c&&c.rect?c:null},q=function(a,b){var c=function(){var c=o(a),d=p(a,c);d?b.show(a,d.id,d.rect,c):b.hide()};return function(){a.removed||c()}},r=function(a,b){return function(){var c=o(a),d=p(a,c);d&&b.reposition(a,d.id,d.rect)}},s=function(a,b,c){return function(){a.removed||b.inForm()||c()}},t=function(a,b){var d=c.throttle(q(a,b),0),e=c.throttle(s(a,b,q(a,b)),0);a.on("blur hide ObjectResizeStart",b.hide),a.on("click",d),a.on("nodeChange mouseup",e),a.on("ResizeEditor keyup",d),a.on("ResizeWindow",r(a,b)),a.on("remove",b.remove),a.shortcuts.add("Alt+F10","",b.focus)},u=function(a,b){a.shortcuts.remove("meta+k"),a.shortcuts.add("meta+k","",function(){var c=o(a),d=d=g.match(a,[i.textSelection("quicklink")]);d&&b.show(a,d.id,d.rect,c)})},v=function(a,b){return j.load(a,function(){t(a,b),u(a,b)}),{}},w=function(a){throw new Error(a)};return a.add("inlite",function(a){var b=new l;k.addToEditor(a,b);var c=function(){return a.inline?v(a,b):w("inlite theme only supports inline mode.")};return{renderUI:c}}),b.appendTo(window.tinymce?window.tinymce:{}),function(){}}),d("0")()}();
\ No newline at end of file
+!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i we need to check if any of them contains some useful html.
- // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
- pasteBinClones = editor.dom.select('div[id=mcepastebin]');
- for (i = 0; i < pasteBinClones.length; i++) {
- clone = pasteBinClones[i];
-
- // Pasting plain text produces pastebins in pastebinds makes sence right!?
- if (clone.firstChild && clone.firstChild.id == 'mcepastebin') {
- clone = clone.firstChild;
- }
-
- cloneHtml = clone.innerHTML;
- if (html != pasteBinDefaultContent) {
- html += cloneHtml;
- }
- }
-
- return html;
- }
/**
* Gets various content types out of a datatransfer object.
@@ -1060,7 +1615,11 @@ define(
if (dataTransfer.types) {
for (var i = 0; i < dataTransfer.types.length; i++) {
var contentType = dataTransfer.types[i];
- items[contentType] = dataTransfer.getData(contentType);
+ try { // IE11 throws exception when contentType is Files (type is present but data cannot be retrieved via getData())
+ items[contentType] = dataTransfer.getData(contentType);
+ } catch (ex) {
+ items[contentType] = ""; // useless in general, but for consistency across browsers
+ }
}
}
}
@@ -1101,6 +1660,11 @@ define(
return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true;
}
+ function extractFilename(str) {
+ var m = str.match(/([\s\S]+?)\.(?:jpeg|jpg|png|gif)$/i);
+ return m ? editor.dom.encode(m[1]) : null;
+ }
+
function pasteImage(rng, reader, blob) {
if (rng) {
editor.selection.setRng(rng);
@@ -1109,8 +1673,10 @@ define(
var dataUri = reader.result;
var base64 = getBase64FromUri(dataUri);
-
+ var id = uniqueId();
+ var name = editor.settings.images_reuse_filename && blob.name ? extractFilename(blob.name) : id;
var img = new Image();
+
img.src = dataUri;
// TODO: Move the bulk of the cache logic to EditorUpload
@@ -1123,7 +1689,7 @@ define(
});
if (!existingBlobInfo) {
- blobInfo = blobCache.create(uniqueId(), blob, base64);
+ blobInfo = blobCache.create(id, blob, base64, name);
blobCache.add(blobInfo);
} else {
blobInfo = existingBlobInfo;
@@ -1203,7 +1769,7 @@ define(
function removePasteBinOnKeyUp(e) {
// Ctrl+V or Shift+Insert
if (isKeyboardPasteEvent(e) && !e.isDefaultPrevented()) {
- removePasteBin();
+ pasteBin.remove();
}
}
@@ -1230,8 +1796,8 @@ define(
return;
}
- removePasteBin();
- createPasteBin();
+ pasteBin.remove();
+ pasteBin.create();
// Remove pastebin if we get a keyup and no paste event
// For example pasting a file in IE 11 will not produce a paste event
@@ -1249,26 +1815,21 @@ define(
if (hasContentType(clipboardContent, 'text/html')) {
content = clipboardContent['text/html'];
} else {
- content = getPasteBinHtml();
+ content = pasteBin.getHtml();
+ internal = internal ? internal : InternalHtml.isMarked(content);
// If paste bin is empty try using plain text mode
// since that is better than nothing right
- if (content == pasteBinDefaultContent) {
+ if (pasteBin.isDefaultContent(content)) {
plainTextMode = true;
}
}
content = Utils.trimHtml(content);
- // WebKit has a nice bug where it clones the paste bin if you paste from for example notepad
- // so we need to force plain text mode in this case
- if (pasteBinElm && pasteBinElm.firstChild && pasteBinElm.firstChild.id === 'mcepastebin') {
- plainTextMode = true;
- }
+ pasteBin.remove();
- removePasteBin();
-
- isPlainTextHtml = internal === false && Newlines.isPlainText(content);
+ isPlainTextHtml = (internal === false && Newlines.isPlainText(content));
// If we got nothing from clipboard API and pastebin or the content is a plain text (with only
// some BRs, Ps or DIVs as newlines) then we fallback to plain/text
@@ -1276,8 +1837,6 @@ define(
plainTextMode = true;
}
-
-
// Grab plain text from Clipboard API or convert existing HTML to plain text
if (plainTextMode) {
// Use plain text contents from Clipboard API unless the HTML contains paragraphs then
@@ -1291,7 +1850,7 @@ define(
// If the content is the paste bin default HTML then it was
// impossible to get the cliboard data out.
- if (content == pasteBinDefaultContent) {
+ if (pasteBin.isDefaultContent(content)) {
if (!isKeyBoardPaste) {
editor.windowManager.alert('Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents.');
}
@@ -1307,7 +1866,7 @@ define(
}
var getLastRng = function () {
- return lastRng || editor.selection.getRng();
+ return pasteBin.getLastRng() || editor.selection.getRng();
};
editor.on('paste', function (e) {
@@ -1323,12 +1882,12 @@ define(
keyboardPastePlainTextState = false;
if (e.isDefaultPrevented() || isBrokenAndroidClipboardEvent(e)) {
- removePasteBin();
+ pasteBin.remove();
return;
}
if (!hasHtmlOrText(clipboardContent) && pasteImageData(e, getLastRng())) {
- removePasteBin();
+ pasteBin.remove();
return;
}
@@ -1339,19 +1898,25 @@ define(
// Try IE only method if paste isn't a keyboard paste
if (Env.ie && (!isKeyBoardPaste || e.ieFake) && !hasContentType(clipboardContent, 'text/html')) {
- createPasteBin();
+ pasteBin.create();
- editor.dom.bind(pasteBinElm, 'paste', function (e) {
+ editor.dom.bind(pasteBin.getEl(), 'paste', function (e) {
e.stopPropagation();
});
editor.getDoc().execCommand('Paste', false, null);
- clipboardContent["text/html"] = getPasteBinHtml();
+ clipboardContent["text/html"] = pasteBin.getHtml();
}
// If clipboard API has HTML then use that directly
if (hasContentType(clipboardContent, 'text/html')) {
e.preventDefault();
+
+ // if clipboard lacks internal mime type, inspect html for internal markings
+ if (!internal) {
+ internal = InternalHtml.isMarked(clipboardContent['text/html']);
+ }
+
insertClipboardContent(clipboardContent, isKeyBoardPaste, plainTextMode, internal);
} else {
Delay.setEditorTimeout(editor, function () {
@@ -1473,7 +2038,7 @@ define(
);
/**
- * ResolveGlobal.js
+ * CutCopy.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
@@ -1483,540 +2048,118 @@ define(
*/
define(
- 'tinymce.core.html.Serializer',
+ 'tinymce.plugins.paste.core.CutCopy',
[
- 'global!tinymce.util.Tools.resolve'
- ],
- function (resolve) {
- return resolve('tinymce.html.Serializer');
- }
-);
-
-/**
- * ResolveGlobal.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-define(
- 'tinymce.core.html.Node',
- [
- 'global!tinymce.util.Tools.resolve'
- ],
- function (resolve) {
- return resolve('tinymce.html.Node');
- }
-);
-
-/**
- * WordFilter.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-/**
- * This class parses word HTML into proper TinyMCE markup.
- *
- * @class tinymce.pasteplugin.WordFilter
- * @private
- */
-define(
- 'tinymce.plugins.paste.core.WordFilter',
- [
- 'tinymce.core.util.Tools',
- 'tinymce.core.html.DomParser',
- 'tinymce.core.html.Schema',
- 'tinymce.core.html.Serializer',
- 'tinymce.core.html.Node',
+ 'tinymce.core.Env',
+ 'tinymce.plugins.paste.core.InternalHtml',
'tinymce.plugins.paste.core.Utils'
],
- function (Tools, DomParser, Schema, Serializer, Node, Utils) {
- /**
- * Checks if the specified content is from any of the following sources: MS Word/Office 365/Google docs.
- */
- function isWordContent(content) {
- return (
- (/]+id="?docs-internal-[^>]*>/gi, '');
- content = content.replace(/
/gi, '');
+ setTimeout(function () {
+ outer.parentNode.removeChild(outer);
+ editor.selection.setRng(range);
+ done();
+ }, 0);
+ };
+ };
- retainStyleProperties = settings.paste_retain_style_properties;
- if (retainStyleProperties) {
- validStyles = Tools.makeMap(retainStyleProperties.split(/[, ]/));
+ var getData = function (editor) {
+ return {
+ html: editor.selection.getContent({ contextual: true }),
+ text: editor.selection.getContent({ format: 'text' })
+ };
+ };
+
+ var cut = function (editor) {
+ return function (evt) {
+ if (editor.selection.isCollapsed() === false) {
+ setClipboardData(evt, getData(editor), fallback(editor), function () {
+ // Chrome fails to execCommand from another execCommand with this message:
+ // "We don't execute document.execCommand() this time, because it is called recursively.""
+ setTimeout(function () { // detach
+ editor.execCommand('Delete');
+ }, 0);
+ });
}
+ };
+ };
- /**
- * Converts fake bullet and numbered lists to real semantic OL/UL.
- *
- * @param {tinymce.html.Node} node Root node to convert children of.
- */
- function convertFakeListsToProperLists(node) {
- var currentListNode, prevListNode, lastLevel = 1;
-
- function getText(node) {
- var txt = '';
-
- if (node.type === 3) {
- return node.value;
- }
-
- if ((node = node.firstChild)) {
- do {
- txt += getText(node);
- } while ((node = node.next));
- }
-
- return txt;
- }
-
- function trimListStart(node, regExp) {
- if (node.type === 3) {
- if (regExp.test(node.value)) {
- node.value = node.value.replace(regExp, '');
- return false;
- }
- }
-
- if ((node = node.firstChild)) {
- do {
- if (!trimListStart(node, regExp)) {
- return false;
- }
- } while ((node = node.next));
- }
-
- return true;
- }
-
- function removeIgnoredNodes(node) {
- if (node._listIgnore) {
- node.remove();
- return;
- }
-
- if ((node = node.firstChild)) {
- do {
- removeIgnoredNodes(node);
- } while ((node = node.next));
- }
- }
-
- function convertParagraphToLi(paragraphNode, listName, start) {
- var level = paragraphNode._listLevel || lastLevel;
-
- // Handle list nesting
- if (level != lastLevel) {
- if (level < lastLevel) {
- // Move to parent list
- if (currentListNode) {
- currentListNode = currentListNode.parent.parent;
- }
- } else {
- // Create new list
- prevListNode = currentListNode;
- currentListNode = null;
- }
- }
-
- if (!currentListNode || currentListNode.name != listName) {
- prevListNode = prevListNode || currentListNode;
- currentListNode = new Node(listName, 1);
-
- if (start > 1) {
- currentListNode.attr('start', '' + start);
- }
-
- paragraphNode.wrap(currentListNode);
- } else {
- currentListNode.append(paragraphNode);
- }
-
- paragraphNode.name = 'li';
-
- // Append list to previous list if it exists
- if (level > lastLevel && prevListNode) {
- prevListNode.lastChild.append(currentListNode);
- }
-
- lastLevel = level;
-
- // Remove start of list item "1. " or "· " etc
- removeIgnoredNodes(paragraphNode);
- trimListStart(paragraphNode, /^\u00a0+/);
- trimListStart(paragraphNode, /^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/);
- trimListStart(paragraphNode, /^\u00a0+/);
- }
-
- // Build a list of all root level elements before we start
- // altering them in the loop below.
- var elements = [], child = node.firstChild;
- while (typeof child !== 'undefined' && child !== null) {
- elements.push(child);
-
- child = child.walk();
- if (child !== null) {
- while (typeof child !== 'undefined' && child.parent !== node) {
- child = child.walk();
- }
- }
- }
-
- for (var i = 0; i < elements.length; i++) {
- node = elements[i];
-
- if (node.name == 'p' && node.firstChild) {
- // Find first text node in paragraph
- var nodeText = getText(node);
-
- // Detect unordered lists look for bullets
- if (isBulletList(nodeText)) {
- convertParagraphToLi(node, 'ul');
- continue;
- }
-
- // Detect ordered lists 1., a. or ixv.
- if (isNumericList(nodeText)) {
- // Parse OL start number
- var matches = /([0-9]+)\./.exec(nodeText);
- var start = 1;
- if (matches) {
- start = parseInt(matches[1], 10);
- }
-
- convertParagraphToLi(node, 'ol', start);
- continue;
- }
-
- // Convert paragraphs marked as lists but doesn't look like anything
- if (node._listLevel) {
- convertParagraphToLi(node, 'ul', 1);
- continue;
- }
-
- currentListNode = null;
- } else {
- // If the root level element isn't a p tag which can be
- // processed by convertParagraphToLi, it interrupts the
- // lists, causing a new list to start instead of having
- // elements from the next list inserted above this tag.
- prevListNode = currentListNode;
- currentListNode = null;
- }
- }
+ var copy = function (editor) {
+ return function (evt) {
+ if (editor.selection.isCollapsed() === false) {
+ setClipboardData(evt, getData(editor), fallback(editor), noop);
}
+ };
+ };
- function filterStyles(node, styleValue) {
- var outputStyles = {}, matches, styles = editor.dom.parseStyle(styleValue);
+ var register = function (editor) {
+ editor.on('cut', cut(editor));
+ editor.on('copy', copy(editor));
+ };
- Tools.each(styles, function (value, name) {
- // Convert various MS styles to W3C styles
- switch (name) {
- case 'mso-list':
- // Parse out list indent level for lists
- matches = /\w+ \w+([0-9]+)/i.exec(styleValue);
- if (matches) {
- node._listLevel = parseInt(matches[1], 10);
- }
-
- // Remove these nodes o
- // Since the span gets removed we mark the text node and the span
- if (/Ignore/i.test(value) && node.firstChild) {
- node._listIgnore = true;
- node.firstChild._listIgnore = true;
- }
-
- break;
-
- case "horiz-align":
- name = "text-align";
- break;
-
- case "vert-align":
- name = "vertical-align";
- break;
-
- case "font-color":
- case "mso-foreground":
- name = "color";
- break;
-
- case "mso-background":
- case "mso-highlight":
- name = "background";
- break;
-
- case "font-weight":
- case "font-style":
- if (value != "normal") {
- outputStyles[name] = value;
- }
- return;
-
- case "mso-element":
- // Remove track changes code
- if (/^(comment|comment-list)$/i.test(value)) {
- node.remove();
- return;
- }
-
- break;
- }
-
- if (name.indexOf('mso-comment') === 0) {
- node.remove();
- return;
- }
-
- // Never allow mso- prefixed names
- if (name.indexOf('mso-') === 0) {
- return;
- }
-
- // Output only valid styles
- if (retainStyleProperties == "all" || (validStyles && validStyles[name])) {
- outputStyles[name] = value;
- }
- });
-
- // Convert bold style to "b" element
- if (/(bold)/i.test(outputStyles["font-weight"])) {
- delete outputStyles["font-weight"];
- node.wrap(new Node("b", 1));
- }
-
- // Convert italic style to "i" element
- if (/(italic)/i.test(outputStyles["font-style"])) {
- delete outputStyles["font-style"];
- node.wrap(new Node("i", 1));
- }
-
- // Serialize the styles and see if there is something left to keep
- outputStyles = editor.dom.serializeStyle(outputStyles, node.name);
- if (outputStyles) {
- return outputStyles;
- }
-
- return null;
- }
-
- if (settings.paste_enable_default_filters === false) {
- return;
- }
-
- // Detect is the contents is Word junk HTML
- if (isWordContent(e.content)) {
- e.wordContent = true; // Mark it for other processors
-
- // Remove basic Word junk
- content = Utils.filter(content, [
- // Word comments like conditional comments etc
- //gi,
-
- // Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
- // MS Office namespaced tags, and a few other tags
- /<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,
-
- // Convert into for line-though
- [/<(\/?)s>/gi, "<$1strike>"],
-
- // Replace nsbp entites to char since it's easier to handle
- [/ /gi, "\u00a0"],
-
- // Convert ___ to string of alternating
- // breaking/non-breaking spaces of same length
- [/([\s\u00a0]*)<\/span>/gi,
- function (str, spaces) {
- return (spaces.length > 0) ?
- spaces.replace(/./, " ").slice(Math.floor(spaces.length / 2)).split("").join("\u00a0") : "";
- }
- ]
- ]);
-
- var validElements = settings.paste_word_valid_elements;
- if (!validElements) {
- validElements = (
- '-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,' +
- '-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,' +
- 'td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody'
- );
- }
-
- // Setup strict schema
- var schema = new Schema({
- valid_elements: validElements,
- valid_children: '-li[p]'
- });
-
- // Add style/class attribute to all element rules since the user might have removed them from
- // paste_word_valid_elements config option and we need to check them for properties
- Tools.each(schema.elements, function (rule) {
- /*eslint dot-notation:0*/
- if (!rule.attributes["class"]) {
- rule.attributes["class"] = {};
- rule.attributesOrder.push("class");
- }
-
- if (!rule.attributes.style) {
- rule.attributes.style = {};
- rule.attributesOrder.push("style");
- }
- });
-
- // Parse HTML into DOM structure
- var domParser = new DomParser({}, schema);
-
- // Filter styles to remove "mso" specific styles and convert some of them
- domParser.addAttributeFilter('style', function (nodes) {
- var i = nodes.length, node;
-
- while (i--) {
- node = nodes[i];
- node.attr('style', filterStyles(node, node.attr('style')));
-
- // Remove pointess spans
- if (node.name == 'span' && node.parent && !node.attributes.length) {
- node.unwrap();
- }
- }
- });
-
- // Check the class attribute for comments or del items and remove those
- domParser.addAttributeFilter('class', function (nodes) {
- var i = nodes.length, node, className;
-
- while (i--) {
- node = nodes[i];
-
- className = node.attr('class');
- if (/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(className)) {
- node.remove();
- }
-
- node.attr('class', null);
- }
- });
-
- // Remove all del elements since we don't want the track changes code in the editor
- domParser.addNodeFilter('del', function (nodes) {
- var i = nodes.length;
-
- while (i--) {
- nodes[i].remove();
- }
- });
-
- // Keep some of the links and anchors
- domParser.addNodeFilter('a', function (nodes) {
- var i = nodes.length, node, href, name;
-
- while (i--) {
- node = nodes[i];
- href = node.attr('href');
- name = node.attr('name');
-
- if (href && href.indexOf('#_msocom_') != -1) {
- node.remove();
- continue;
- }
-
- if (href && href.indexOf('file://') === 0) {
- href = href.split('#')[1];
- if (href) {
- href = '#' + href;
- }
- }
-
- if (!href && !name) {
- node.unwrap();
- } else {
- // Remove all named anchors that aren't specific to TOC, Footnotes or Endnotes
- if (name && !/^_?(?:toc|edn|ftn)/i.test(name)) {
- node.unwrap();
- continue;
- }
-
- node.attr({
- href: href,
- name: name
- });
- }
- }
- });
-
- // Parse into DOM structure
- var rootNode = domParser.parse(content);
-
- // Process DOM
- if (settings.paste_convert_word_fake_lists !== false) {
- convertFakeListsToProperLists(rootNode);
- }
-
- // Serialize DOM back to HTML
- e.content = new Serializer({
- validate: settings.validate
- }, schema).serialize(rootNode);
- }
- });
- }
-
- WordFilter.isWordContent = isWordContent;
-
- return WordFilter;
+ return {
+ register: register
+ };
}
);
-
/**
* Quirks.js
*
@@ -2044,154 +2187,152 @@ define(
'tinymce.plugins.paste.core.Utils'
],
function (Env, Tools, WordFilter, Utils) {
- "use strict";
+ function addPreProcessFilter(editor, filterFunc) {
+ editor.on('PastePreProcess', function (e) {
+ e.content = filterFunc(editor, e.content, e.internal, e.wordContent);
+ });
+ }
- return function (editor) {
- function addPreProcessFilter(filterFunc) {
- editor.on('BeforePastePreProcess', function (e) {
- e.content = filterFunc(e.content);
- });
- }
-
- function addPostProcessFilter(filterFunc) {
- editor.on('PastePostProcess', function (e) {
- filterFunc(e.node);
- });
- }
-
- /**
- * Removes BR elements after block elements. IE9 has a nasty bug where it puts a BR element after each
- * block element when pasting from word. This removes those elements.
- *
- * This:
- *
[\\s\\r\\n]+|
)*(<\\/?(' + blockElements.join('|') + ')[^>]*>)(?:
[\\s\\r\\n]+|
)*',
- 'g'
- );
-
- // Remove BR:s from:
- html = Utils.filter(html, [
- [explorerBlocksRegExp, '$1']
- ]);
-
- // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
- html = Utils.filter(html, [
- [/
/g, '
'], // Replace multiple BR elements with uppercase BR to keep them intact
- [/
/g, ' '], // Replace single br elements with space since they are word wrap BR:s
- [/
/g, '
'] // Replace back the double brs but into a single BR
- ]);
+ function addPostProcessFilter(editor, filterFunc) {
+ editor.on('PastePostProcess', function (e) {
+ filterFunc(editor, e.node);
+ });
+ }
+ /**
+ * Removes BR elements after block elements. IE9 has a nasty bug where it puts a BR element after each
+ * block element when pasting from word. This removes those elements.
+ *
+ * This:
+ *
[\\s\\r\\n]+|
)*(<\\/?(' + blockElements.join('|') + ')[^>]*>)(?:
[\\s\\r\\n]+|
)*',
+ 'g'
+ );
- if (webKitStyles) {
- webKitStyles = webKitStyles.split(/[, ]/);
- }
+ // Remove BR:s from:
+ html = Utils.filter(html, [
+ [explorerBlocksRegExp, '$1']
+ ]);
- // Keep specific styles that doesn't match the current node computed style
- if (webKitStyles) {
- var dom = editor.dom, node = editor.selection.getNode();
+ // IE9 also adds an extra BR element for each soft-linefeed and it also adds a BR for each word wrap break
+ html = Utils.filter(html, [
+ [/
/g, '
'], // Replace multiple BR elements with uppercase BR to keep them intact
+ [/
/g, ' '], // Replace single br elements with space since they are word wrap BR:s
+ [/
/g, '
'] // Replace back the double brs but into a single BR
+ ]);
- content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, function (all, before, value, after) {
- var inputStyles = dom.parseStyle(dom.decode(value), 'span');
- var outputStyles = {};
-
- if (webKitStyles === "none") {
- return before + after;
- }
-
- for (var i = 0; i < webKitStyles.length; i++) {
- var inputValue = inputStyles[webKitStyles[i]], currentValue = dom.getStyle(node, webKitStyles[i], true);
-
- if (/color/.test(webKitStyles[i])) {
- inputValue = dom.toHex(inputValue);
- currentValue = dom.toHex(currentValue);
- }
-
- if (currentValue != inputValue) {
- outputStyles[webKitStyles[i]] = inputValue;
- }
- }
-
- outputStyles = dom.serializeStyle(outputStyles, 'span');
- if (outputStyles) {
- return before + ' style="' + outputStyles + '"' + after;
- }
-
- return before + after;
- });
- } else {
- // Remove all external styles
- content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, '$1$3');
- }
-
- // Keep internal styles
- content = content.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi, function (all, before, value, after) {
- return before + ' style="' + value + '"' + after;
- });
+ return html;
+ }
+ /**
+ * WebKit has a nasty bug where the all computed styles gets added to style attributes when copy/pasting contents.
+ * This fix solves that by simply removing the whole style attribute.
+ *
+ * The paste_webkit_styles option can be set to specify what to keep:
+ * paste_webkit_styles: "none" // Keep no styles
+ * paste_webkit_styles: "all", // Keep all of them
+ * paste_webkit_styles: "font-weight color" // Keep specific ones
+ */
+ function removeWebKitStyles(editor, content, internal, isWordHtml) {
+ // WordFilter has already processed styles at this point and internal doesn't need any processing
+ if (isWordHtml || internal) {
return content;
}
- function removeUnderlineAndFontInAnchor(root) {
- editor.$('a', root).find('font,u').each(function (i, node) {
- editor.dom.remove(node, true);
- });
+ // Filter away styles that isn't matching the target node
+ var webKitStyles = editor.settings.paste_webkit_styles;
+
+ if (editor.settings.paste_remove_styles_if_webkit === false || webKitStyles == "all") {
+ return content;
}
- // Sniff browsers and apply fixes since we can't feature detect
+ if (webKitStyles) {
+ webKitStyles = webKitStyles.split(/[, ]/);
+ }
+
+ // Keep specific styles that doesn't match the current node computed style
+ if (webKitStyles) {
+ var dom = editor.dom, node = editor.selection.getNode();
+
+ content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, function (all, before, value, after) {
+ var inputStyles = dom.parseStyle(dom.decode(value), 'span');
+ var outputStyles = {};
+
+ if (webKitStyles === "none") {
+ return before + after;
+ }
+
+ for (var i = 0; i < webKitStyles.length; i++) {
+ var inputValue = inputStyles[webKitStyles[i]], currentValue = dom.getStyle(node, webKitStyles[i], true);
+
+ if (/color/.test(webKitStyles[i])) {
+ inputValue = dom.toHex(inputValue);
+ currentValue = dom.toHex(currentValue);
+ }
+
+ if (currentValue != inputValue) {
+ outputStyles[webKitStyles[i]] = inputValue;
+ }
+ }
+
+ outputStyles = dom.serializeStyle(outputStyles, 'span');
+ if (outputStyles) {
+ return before + ' style="' + outputStyles + '"' + after;
+ }
+
+ return before + after;
+ });
+ } else {
+ // Remove all external styles
+ content = content.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi, '$1$3');
+ }
+
+ // Keep internal styles
+ content = content.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi, function (all, before, value, after) {
+ return before + ' style="' + value + '"' + after;
+ });
+
+ return content;
+ }
+
+ function removeUnderlineAndFontInAnchor(editor, root) {
+ editor.$('a', root).find('font,u').each(function (i, node) {
+ editor.dom.remove(node, true);
+ });
+ }
+
+ var setup = function (editor) {
if (Env.webkit) {
- addPreProcessFilter(removeWebKitStyles);
+ addPreProcessFilter(editor, removeWebKitStyles);
}
if (Env.ie) {
- addPreProcessFilter(removeExplorerBrElementsAfterBlocks);
- addPostProcessFilter(removeUnderlineAndFontInAnchor);
+ addPreProcessFilter(editor, removeExplorerBrElementsAfterBlocks);
+ addPostProcessFilter(editor, removeUnderlineAndFontInAnchor);
}
};
+
+ return {
+ setup: setup
+ };
}
);
/**
@@ -2214,12 +2355,12 @@ define(
'tinymce.plugins.paste.Plugin',
[
'tinymce.core.PluginManager',
+ 'tinymce.plugins.paste.api.Events',
'tinymce.plugins.paste.core.Clipboard',
'tinymce.plugins.paste.core.CutCopy',
- 'tinymce.plugins.paste.core.Quirks',
- 'tinymce.plugins.paste.core.WordFilter'
+ 'tinymce.plugins.paste.core.Quirks'
],
- function (PluginManager, Clipboard, CutCopy, Quirks, WordFilter) {
+ function (PluginManager, Events, Clipboard, CutCopy, Quirks) {
var userIsInformed;
PluginManager.add('paste', function (editor) {
@@ -2232,10 +2373,10 @@ define(
function togglePlainTextPaste() {
if (clipboard.pasteFormat == "text") {
clipboard.pasteFormat = "html";
- editor.fire('PastePlainTextToggle', { state: false });
+ Events.firePastePlainTextToggle(editor, false);
} else {
clipboard.pasteFormat = "text";
- editor.fire('PastePlainTextToggle', { state: true });
+ Events.firePastePlainTextToggle(editor, true);
if (!isUserInformedAboutPlainText()) {
var message = editor.translate('Paste is now in plain text mode. Contents will now ' +
@@ -2273,8 +2414,7 @@ define(
}
self.clipboard = clipboard = new Clipboard(editor);
- self.quirks = new Quirks(editor);
- self.wordFilter = new WordFilter(editor);
+ self.quirks = Quirks.setup(editor);
if (editor.settings.paste_as_text) {
self.clipboard.pasteFormat = "text";
diff --git a/src/wp-includes/js/tinymce/plugins/paste/plugin.min.js b/src/wp-includes/js/tinymce/plugins/paste/plugin.min.js
index 5bbb85d7c0..425febd73e 100644
--- a/src/wp-includes/js/tinymce/plugins/paste/plugin.min.js
+++ b/src/wp-includes/js/tinymce/plugins/paste/plugin.min.js
@@ -1 +1 @@
-!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i
/g,/
$/i])}function g(a){var b=0;return function(){return a+b++}}var h=function(){return navigator.userAgent.indexOf(" Edge/")!==-1};return{filter:d,innerText:e,trimHtml:f,createIdGenerator:g,isMsEdge:h}}),g("3",["8","c","d"],function(a,b,c){var d=function(){},e=function(b){return a.iOS===!1&&void 0!==b&&"function"==typeof b.setData&&c.isMsEdge()!==!0},f=function(a,c,d){if(!e(a))return!1;try{return a.clearData(),a.setData("text/html",c),a.setData("text/plain",d),a.setData(b.internalHtmlMime(),c),!0}catch(a){return!1}},g=function(a,b,c,d){f(a.clipboardData,b.html,b.text)?(a.preventDefault(),d()):c(b.html,d)},h=function(a){return function(c,d){var e=b.mark(c),f=a.dom.create("div",{contenteditable:"false"}),g=a.dom.create("div",{contenteditable:"true"},e);a.dom.setStyles(f,{position:"fixed",left:"-3000px",width:"1000px",overflow:"hidden"}),f.appendChild(g),a.dom.add(a.getBody(),f);var h=a.selection.getRng();g.focus();var i=a.dom.createRng();i.selectNodeContents(g),a.selection.setRng(i),setTimeout(function(){f.parentNode.removeChild(f),a.selection.setRng(h),d()},0)}},i=function(a){return{html:a.selection.getContent({contextual:!0}),text:a.selection.getContent({format:"text"})}},j=function(a){return function(b){a.selection.isCollapsed()===!1&&g(b,i(a),h(a),function(){a.execCommand("Delete")})}},k=function(a){return function(b){a.selection.isCollapsed()===!1&&g(b,i(a),h(a),d)}},l=function(a){a.on("cut",j(a)),a.on("copy",k(a))};return{register:l}}),g("k",["6"],function(a){return a("tinymce.html.Entities")}),g("e",["k"],function(a){var b=function(a){return!/<(?:(?!\/?(?:div|p|br))[^>]*|(?:div|p|br)\s+\w[^>]+)>/.test(a)},c=function(a){return a.replace(/\r?\n/g,"
")},d=function(b,c){var d,e=[],f="<"+b;if("object"==typeof c){for(d in c)c.hasOwnProperty(d)&&e.push(d+'="'+a.encodeAllRaw(c[d])+'"');e.length&&(f+=" "+e.join(" "))}return f+">"},e=function(a,b,c){var e,f,g,h=a.split(/\r?\n/),i=0,j=h.length,k=[],l=[],m=d(b,c),n=""+b+">";if(1===h.length)return a;for(;i')}),!0},e=function(a,b,c){return a.undoManager.extra(function(){c(a,b)},function(){a.execCommand("mceInsertLink",!1,b)}),!0},f=function(a,c,d){return!(a.selection.isCollapsed()!==!1||!b(c))&&e(a,c,d)},g=function(a,b,e){return!!c(b)&&d(a,b,e)},h=function(a,b){return a.insertContent(b,{merge:a.settings.paste_merge_formats!==!1,paste:!0}),!0},i=function(b,c){a.each([f,g,h],function(a){return a(b,c,h)!==!0})},j=function(a,b){a.settings.smart_paste===!1?h(a,b):i(a,b)};return{isImageUrl:c,isAbsoluteUrl:b,insertContent:j}}),g("2",["7","8","9","a","b","3","c","e","f","d"],function(a,b,c,d,e,f,g,h,i,j){return function(f){function k(a,b){var c,d,e=f.dom;if(d=b||g.isMarked(a),a=g.unmark(a),c=f.fire("BeforePastePreProcess",{content:a,internal:d}),c=f.fire("PastePreProcess",c),a=c.content,!c.isDefaultPrevented()){if(f.hasEventListeners("PastePostProcess")&&!c.isDefaultPrevented()){var h=e.add(f.getBody(),"div",{style:"display:none"},a);c=f.fire("PastePostProcess",{node:h,internal:d}),e.remove(h),a=c.node.innerHTML}c.isDefaultPrevented()||i.insertContent(f,a)}}function l(a){a=f.dom.encode(a).replace(/\r\n/g,"\n"),a=h.convert(a,f.settings.forced_root_block,f.settings.forced_root_block_attrs),k(a,!1)}function m(){function a(a){var b,c,e,f=a.startContainer;if(b=a.getClientRects(),b.length)return b[0];if(a.collapsed&&1==f.nodeType){for(e=f.childNodes[C.startOffset];e&&3==e.nodeType&&!e.data.length;)e=e.nextSibling;if(e)return"BR"==e.tagName&&(c=d.doc.createTextNode("\ufeff"),e.parentNode.insertBefore(c,e),a=d.createRng(),a.setStartBefore(c),a.setEndAfter(c),b=a.getClientRects(),d.remove(c)),b.length?b[0]:void 0}}var c,d=f.dom,e=f.getBody(),g=f.dom.getViewPort(f.getWin()),h=g.y,i=20;if(C=f.selection.getRng(),f.inline&&(c=f.selection.getScrollContainer(),c&&c.scrollTop>0&&(h=c.scrollTop)),C.getClientRects){var j=a(C);if(j)i=h+(j.top-d.getPos(e).y);else{i=h;var k=C.startContainer;k&&(3==k.nodeType&&k.parentNode!=e&&(k=k.parentNode),1==k.nodeType&&(i=d.getPos(k,c||e).y))}}B=d.add(f.getBody(),"div",{id:"mcepastebin",contentEditable:!0,"data-mce-bogus":"all",style:"position: absolute; top: "+i+"px;width: 10px; height: 10px; overflow: hidden; opacity: 0"},H),(b.ie||b.gecko)&&d.setStyle(B,"left","rtl"==d.getStyle(e,"direction",!0)?65535:-65535),d.bind(B,"beforedeactivate focusin focusout",function(a){a.stopPropagation()}),B.focus(),f.selection.select(B,!0)}function n(){if(B){for(var a;a=f.dom.get("mcepastebin");)f.dom.remove(a),f.dom.unbind(a);C&&f.selection.setRng(C)}B=C=null}function o(){var a,b,c,d,e="";for(a=f.dom.select("div[id=mcepastebin]"),b=0;b
',!1)}function v(a,b){function c(c){var d,e,f,g=!1;if(c)for(d=0;d
/gi,""),o=k.paste_retain_style_properties,o&&(p=a.makeMap(o.split(/[, ]/))),k.paste_enable_default_filters!==!1&&g(l.content)){l.wordContent=!0,q=f.filter(q,[//gi,/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,[/<(\/?)s>/gi,"<$1strike>"],[/ /gi,"\xa0"],[/([\s\u00a0]*)<\/span>/gi,function(a,b){return b.length>0?b.replace(/./," ").slice(Math.floor(b.length/2)).split("").join("\xa0"):""}]]);var r=k.paste_word_valid_elements;r||(r="-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody");var s=new c({valid_elements:r,valid_children:"-li[p]"});a.each(s.elements,function(a){a.attributes["class"]||(a.attributes["class"]={},a.attributesOrder.push("class")),a.attributes.style||(a.attributes.style={},a.attributesOrder.push("style"))});var t=new b({},s);t.addAttributeFilter("style",function(a){for(var b,c=a.length;c--;)b=a[c],b.attr("style",n(b,b.attr("style"))),"span"==b.name&&b.parent&&!b.attributes.length&&b.unwrap()}),t.addAttributeFilter("class",function(a){for(var b,c,d=a.length;d--;)b=a[d],c=b.attr("class"),/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(c)&&b.remove(),b.attr("class",null)}),t.addNodeFilter("del",function(a){for(var b=a.length;b--;)a[b].remove()}),t.addNodeFilter("a",function(a){for(var b,c,d,e=a.length;e--;)if(b=a[e],c=b.attr("href"),d=b.attr("name"),c&&c.indexOf("#_msocom_")!=-1)b.remove();else if(c&&0===c.indexOf("file://")&&(c=c.split("#")[1],c&&(c="#"+c)),c||d){if(d&&!/^_?(?:toc|edn|ftn)/i.test(d)){b.unwrap();continue}b.attr({href:c,name:d})}else b.unwrap()});var u=t.parse(q);k.paste_convert_word_fake_lists!==!1&&m(u),l.content=new d({validate:k.validate},s).serialize(u)}})}return j.isWordContent=g,j}),g("4",["8","a","5","d"],function(a,b,c,d){"use strict";return function(e){function f(a){e.on("BeforePastePreProcess",function(b){b.content=a(b.content)})}function g(a){e.on("PastePostProcess",function(b){a(b.node)})}function h(a){if(!c.isWordContent(a))return a;var f=[];b.each(e.schema.getBlockElements(),function(a,b){f.push(b)});var g=new RegExp("(?:
[\\s\\r\\n]+|
)*(<\\/?("+f.join("|")+")[^>]*>)(?:
[\\s\\r\\n]+|
)*","g");return a=d.filter(a,[[g,"$1"]]),a=d.filter(a,[[/
/g,"
"],[/
/g," "],[/
/g,"
"]])}function i(a){if(c.isWordContent(a))return a;var b=e.settings.paste_webkit_styles;if(e.settings.paste_remove_styles_if_webkit===!1||"all"==b)return a;if(b&&(b=b.split(/[, ]/)),b){var d=e.dom,f=e.selection.getNode();a=a.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi,function(a,c,e,g){var h=d.parseStyle(d.decode(e),"span"),i={};if("none"===b)return c+g;for(var j=0;j
")},e=function(a,c){var d,e=[],f="<"+a;if("object"==typeof c){for(d in c)c.hasOwnProperty(d)&&e.push(d+'="'+b.encodeAllRaw(c[d])+'"');e.length&&(f+=" "+e.join(" "))}return f+">"},f=function(b,c,d){var f=b.split(/\n\n/),g=e(c,d),h=""+c+">",i=a.map(f,function(a){return a.split(/\n/).join("
")}),j=function(a){return g+a+h};return 1===i.length?i[0]:a.map(i,j).join("")},g=function(a,b,c){return b?f(a,b,c):d(a)};return{isPlainText:c,convert:g,toBRs:d,toBlockElements:f}}),g("e",["a","8"],function(a,b){return function(c){var d,e="%MCEPASTEBIN%",f=function(){function a(a){var b,c,e,f=a.startContainer;if(b=a.getClientRects(),b.length)return b[0];if(a.collapsed&&1==f.nodeType){for(e=f.childNodes[d.startOffset];e&&3==e.nodeType&&!e.data.length;)e=e.nextSibling;if(e)return"BR"==e.tagName&&(c=h.doc.createTextNode("\ufeff"),e.parentNode.insertBefore(c,e),a=h.createRng(),a.setStartBefore(c),a.setEndAfter(c),b=a.getClientRects(),h.remove(c)),b.length?b[0]:void 0}}var f,g,h=c.dom,i=c.getBody(),j=c.dom.getViewPort(c.getWin()),k=j.y,l=20;if(d=c.selection.getRng(),c.inline&&(g=c.selection.getScrollContainer(),g&&g.scrollTop>0&&(k=g.scrollTop)),d.getClientRects){var m=a(d);if(m)l=k+(m.top-h.getPos(i).y);else{l=k;var n=d.startContainer;n&&(3==n.nodeType&&n.parentNode!=i&&(n=n.parentNode),1==n.nodeType&&(l=h.getPos(n,g||i).y))}}f=c.dom.add(c.getBody(),"div",{id:"mcepastebin",contentEditable:!0,"data-mce-bogus":"all",style:"position: absolute; top: "+l+"px; width: 10px; height: 10px; overflow: hidden; opacity: 0"},e),(b.ie||b.gecko)&&h.setStyle(f,"left","rtl"==h.getStyle(i,"direction",!0)?65535:-65535),h.bind(f,"beforedeactivate focusin focusout",function(a){a.stopPropagation()}),f.focus(),c.selection.select(f,!0)},g=function(){if(h()){for(var a;a=c.dom.get("mcepastebin");)c.dom.remove(a),c.dom.unbind(a);d&&c.selection.setRng(d)}d=null},h=function(){return c.dom.get("mcepastebin")},i=function(){var b,d,e,f,g,h=function(a,b){a.appendChild(b),c.dom.remove(b,!0)};for(d=a.grep(c.getBody().childNodes,function(a){return"mcepastebin"===a.id}),b=d.shift(),a.each(d,function(a){h(b,a)}),f=c.dom.select("div[id=mcepastebin]",b),e=f.length-1;e>=0;e--)g=c.dom.create("div"),b.insertBefore(g,f[e]),h(g,f[e]);return b?b.innerHTML:""},j=function(){return d},k=function(a){return a===e},l=function(a){return a&&"mcepastebin"===a.id},m=function(){var a=h();return l(a)&&k(a.innerHTML)};return{create:f,remove:g,getEl:h,getHtml:i,getLastRng:j,isDefault:m,isDefaultContent:k}}}),g("k",["6"],function(a){return a("tinymce.html.DomParser")}),g("l",["6"],function(a){return a("tinymce.html.Schema")}),g("m",["6"],function(a){return a("tinymce.html.Serializer")}),g("n",["6"],function(a){return a("tinymce.html.Node")}),g("h",["a","k","l"],function(a,b,c){function d(b,c){return a.each(c,function(a){b=a.constructor==RegExp?b.replace(a,""):b.replace(a[0],a[1])}),b}function e(e){function f(a){var b=a.name,c=a;if("br"===b)return void(i+="\n");if(j[b]&&(i+=" "),k[b])return void(i+=" ");if(3==a.type&&(i+=a.value),!a.shortEnded&&(a=a.firstChild))do f(a);while(a=a.next);l[b]&&c.next&&(i+="\n","p"==b&&(i+="\n"))}var g=new c,h=new b({},g),i="",j=g.getShortEndedElements(),k=a.makeMap("script noscript style textarea video audio iframe object"," "),l=g.getBlockElements();return e=d(e,[//g]),f(h.parse(e)),i}function f(a){function b(a,b,c){return b||c?"\xa0":" "}return a=d(a,[/^[\s\S]*]*>\s*|\s*<\/body[^>]*>[\s\S]*$/gi,/|/g,[/( ?)\u00a0<\/span>( ?)/g,b],/
/g,/
$/i])}function g(a){var b=0;return function(){return a+b++}}var h=function(){return navigator.userAgent.indexOf(" Edge/")!==-1};return{filter:d,innerText:e,trimHtml:f,createIdGenerator:g,isMsEdge:h}}),g("i",["a","k","l","m","n","h"],function(a,b,c,d,e,f){function g(a){return/1&&g.attr("start",""+f),a.wrap(g)),a.name="li",h>k&&j&&j.lastChild.append(g),k=h,d(a),c(a,/^\u00a0+/),c(a,/^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/),c(a,/^\u00a0+/)}for(var g,j,k=1,l=[],m=a.firstChild;"undefined"!=typeof m&&null!==m;)if(l.push(m),m=m.walk(),null!==m)for(;"undefined"!=typeof m&&m.parent!==a;)m=m.walk();for(var n=0;n')}),!0},e=function(a,b,c){return a.undoManager.extra(function(){c(a,b)},function(){a.execCommand("mceInsertLink",!1,b)}),!0},f=function(a,c,d){return!(a.selection.isCollapsed()!==!1||!b(c))&&e(a,c,d)},g=function(a,b,e){return!!c(b)&&d(a,b,e)},h=function(a,b){return a.insertContent(b,{merge:a.settings.paste_merge_formats!==!1,paste:!0}),!0},i=function(b,c){a.each([f,g,h],function(a){return a(b,c,h)!==!0})},j=function(a,b){a.settings.smart_paste===!1?h(a,b):i(a,b)};return{isImageUrl:c,isAbsoluteUrl:b,insertContent:j}}),g("3",["7","8","9","a","b","c","d","e","f","g","h"],function(a,b,c,d,e,f,g,h,i,j,k){return function(l){function m(a,b){var c=b?b:f.isMarked(a),d=i.process(l,f.unmark(a),c);d.cancelled===!1&&j.insertContent(l,d.content)}function n(a){a=l.dom.encode(a).replace(/\r\n/g,"\n"),a=g.convert(a,l.settings.forced_root_block,l.settings.forced_root_block_attrs),m(a,!1)}function o(a){var b={};if(a){if(a.getData){var c=a.getData("Text");c&&c.length>0&&c.indexOf(G)==-1&&(b["text/plain"]=c)}if(a.types)for(var d=0;d
',!1)}function v(a,b){function c(c){var d,e,f,g=!1;if(c)for(d=0;d
[\\s\\r\\n]+|
)*(<\\/?("+f.join("|")+")[^>]*>)(?:
[\\s\\r\\n]+|
)*","g");return e=d.filter(e,[[g,"$1"]]),e=d.filter(e,[[/
/g,"
"],[/
/g," "],[/
/g,"
"]])}function h(a,b,c,d){if(d||c)return b;var e=a.settings.paste_webkit_styles;if(a.settings.paste_remove_styles_if_webkit===!1||"all"==e)return b;if(e&&(e=e.split(/[, ]/)),e){var f=a.dom,g=a.selection.getNode();b=b.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi,function(a,b,c,d){var h=f.parseStyle(f.decode(c),"span"),i={};if("none"===e)return b+d;for(var j=0;j
";e+=""}return e+="",e+="
";e+=""}return e+="",e+="
- if (elements[node.name]) {
- return false;
- }
-
- // Keep bookmark nodes and name attribute like
- i = node.attributes.length;
- while (i--) {
- name = node.attributes[i].name;
- if (name === "name" || name.indexOf('data-mce-bookmark') === 0) {
- return false;
- }
- }
- }
-
- // Keep comments
- if (node.type === 8) {
- return false;
- }
-
- // Keep non whitespace text nodes
- if (node.type === 3 && !whiteSpaceRegExp.test(node.value)) {
- return false;
- }
-
- // Keep whitespace preserve elements
- if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) {
- return false;
- }
- } while ((node = walk(node, self)));
- }
-
- return true;
- },
-
- /**
- * Walks to the next or previous node and returns that node or null if it wasn't found.
- *
- * @method walk
- * @param {Boolean} prev Optional previous node state defaults to false.
- * @return {tinymce.html.Node} Node that is next to or previous of the current node.
- */
- walk: function (prev) {
- return walk(this, null, prev);
- }
- };
-
- /**
- * Creates a node of a specific type.
- *
- * @static
- * @method create
- * @param {String} name Name of the node type to create for example "b" or "#text".
- * @param {Object} attrs Name/value collection of attributes that will be applied to elements.
- */
- Node.create = function (name, attrs) {
- var node, attrName;
-
- // Create node
- node = new Node(name, typeLookup[name] || 1);
-
- // Add attributes if needed
- if (attrs) {
- for (attrName in attrs) {
- node.attr(attrName, attrs[attrName]);
- }
- }
-
- return node;
- };
-
- return Node;
- }
-);
-
-/**
- * SaxParser.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-/*eslint max-depth:[2, 9] */
-
-/**
- * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will
- * always execute the events in the right order for tag soup code like . It will also remove elements
- * and attributes that doesn't fit the schema if the validate setting is enabled.
- *
- * @example
- * var parser = new tinymce.html.SaxParser({
- * validate: true,
- *
- * comment: function(text) {
- * console.log('Comment:', text);
- * },
- *
- * cdata: function(text) {
- * console.log('CDATA:', text);
- * },
- *
- * text: function(text, raw) {
- * console.log('Text:', text, 'Raw:', raw);
- * },
- *
- * start: function(name, attrs, empty) {
- * console.log('Start:', name, attrs, empty);
- * },
- *
- * end: function(name) {
- * console.log('End:', name);
- * },
- *
- * pi: function(name, text) {
- * console.log('PI:', name, text);
- * },
- *
- * doctype: function(text) {
- * console.log('DocType:', text);
- * }
- * }, schema);
- * @class tinymce.html.SaxParser
- * @version 3.4
- */
-define(
- 'tinymce.core.html.SaxParser',
- [
- "tinymce.core.html.Schema",
- "tinymce.core.html.Entities",
- "tinymce.core.util.Tools"
- ],
- function (Schema, Entities, Tools) {
- var each = Tools.each;
-
- var isValidPrefixAttrName = function (name) {
- return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0;
- };
-
- /**
- * 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.
- *
- * @private
- * @method findEndTag
- * @param {tinymce.html.Schema} schema Schema instance to use to match short ended elements.
- * @param {String} html HTML string to find the end tag in.
- * @param {Number} startIndex Indext to start searching at should be after the start tag.
- * @return {Number} Index of the end tag.
- */
- function findEndTag(schema, html, startIndex) {
- var count = 1, index, matches, tokenRegExp, shortEndedElements;
-
- shortEndedElements = schema.getShortEndedElements();
- tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g;
- tokenRegExp.lastIndex = index = startIndex;
-
- while ((matches = tokenRegExp.exec(html))) {
- index = tokenRegExp.lastIndex;
-
- if (matches[1] === '/') { // End element
- count--;
- } else if (!matches[1]) { // Start element
- if (matches[2] in shortEndedElements) {
- continue;
- }
-
- count++;
- }
-
- if (count === 0) {
- break;
- }
- }
-
- return index;
- }
-
- /**
- * Constructs a new SaxParser instance.
- *
- * @constructor
- * @method SaxParser
- * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks.
- * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing.
- */
- function SaxParser(settings, schema) {
- var self = this;
-
- function noop() { }
-
- settings = settings || {};
- self.schema = schema = schema || new Schema();
-
- if (settings.fix_self_closing !== false) {
- settings.fix_self_closing = true;
- }
-
- // Add handler functions from settings and setup default handlers
- each('comment cdata text start end pi doctype'.split(' '), function (name) {
- if (name) {
- self[name] = settings[name] || noop;
- }
- });
-
- /**
- * Parses the specified HTML string and executes the callbacks for each item it finds.
- *
- * @example
- * new SaxParser({...}).parse('text');
- * @method parse
- * @param {String} html Html string to sax parse.
- */
- self.parse = function (html) {
- var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name;
- var isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded;
- var validate, elementRule, isValidElement, attr, attribsValue, validAttributesMap, validAttributePatterns;
- var attributesRequired, attributesDefault, attributesForced, processHtml;
- var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
- var decode = Entities.decode, fixSelfClosing, filteredUrlAttrs = Tools.makeMap('src,href,data,background,formaction,poster');
- var scriptUriRegExp = /((java|vb)script|mhtml):/i, dataUriRegExp = /^data:/i;
-
- function processEndTag(name) {
- var pos, i;
-
- // Find position of parent of the same type
- pos = stack.length;
- while (pos--) {
- if (stack[pos].name === name) {
- break;
- }
- }
-
- // Found parent
- if (pos >= 0) {
- // Close all the open elements
- for (i = stack.length - 1; i >= pos; i--) {
- name = stack[i];
-
- if (name.valid) {
- self.end(name.name);
- }
- }
-
- // Remove the open elements from the stack
- stack.length = pos;
- }
- }
-
- function parseAttribute(match, name, value, val2, val3) {
- var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
-
- name = name.toLowerCase();
- value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
-
- // Validate name and value pass through all data- attributes
- if (validate && !isInternalElement && isValidPrefixAttrName(name) === false) {
- attrRule = validAttributesMap[name];
-
- // Find rule by pattern matching
- if (!attrRule && validAttributePatterns) {
- i = validAttributePatterns.length;
- while (i--) {
- attrRule = validAttributePatterns[i];
- if (attrRule.pattern.test(name)) {
- break;
- }
- }
-
- // No rule matched
- if (i === -1) {
- attrRule = null;
- }
- }
-
- // No attribute rule found
- if (!attrRule) {
- return;
- }
-
- // Validate value
- if (attrRule.validValues && !(value in attrRule.validValues)) {
- return;
- }
- }
-
- // Block any javascript: urls or non image data uris
- if (filteredUrlAttrs[name] && !settings.allow_script_urls) {
- var uri = value.replace(trimRegExp, '');
-
- try {
- // Might throw malformed URI sequence
- uri = decodeURIComponent(uri);
- } catch (ex) {
- // Fallback to non UTF-8 decoder
- uri = unescape(uri);
- }
-
- if (scriptUriRegExp.test(uri)) {
- return;
- }
-
- if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) {
- return;
- }
- }
-
- // Add attribute to list and map
- attrList.map[name] = value;
- attrList.push({
- name: name,
- value: value
- });
- }
-
- // Precompile RegExps and map objects
- tokenRegExp = new RegExp('<(?:' +
- '(?:!--([\\w\\W]*?)-->)|' + // Comment
- '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
- '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
- '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
- '(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|' + // End element
- '(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
- ')', 'g');
-
- attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
-
- // Setup lookup tables for empty elements and boolean attributes
- shortEndedElements = schema.getShortEndedElements();
- selfClosing = settings.self_closing_elements || schema.getSelfClosingElements();
- fillAttrsMap = schema.getBoolAttrs();
- validate = settings.validate;
- removeInternalElements = settings.remove_internals;
- fixSelfClosing = settings.fix_self_closing;
- specialElements = schema.getSpecialElements();
- processHtml = html + '>';
-
- while ((matches = tokenRegExp.exec(processHtml))) { // Adds and extra '>' to keep regexps from doing catastrofic backtracking on malformed html
- // Text
- if (index < matches.index) {
- self.text(decode(html.substr(index, matches.index - index)));
- }
-
- if ((value = matches[6])) { // End element
- value = value.toLowerCase();
-
- // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
- if (value.charAt(0) === ':') {
- value = value.substr(1);
- }
-
- processEndTag(value);
- } else if ((value = matches[7])) { // Start element
- // Did we consume the extra character then treat it as text
- // This handles the case with html like this: "text a html.length) {
- self.text(decode(html.substr(matches.index)));
- index = matches.index + matches[0].length;
- continue;
- }
-
- value = value.toLowerCase();
-
- // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
- if (value.charAt(0) === ':') {
- value = value.substr(1);
- }
-
- isShortEnded = value in shortEndedElements;
-
- // Is self closing tag for example an
a
b
c
- * - * @example - * var parser = new tinymce.html.DomParser({validate: true}, schema); - * var rootNode = parser.parse('x
->x
- function trim(rootBlockNode) { - if (rootBlockNode) { - node = rootBlockNode.firstChild; - if (node && node.type == 3) { - node.value = node.value.replace(startWhiteSpaceRegExp, ''); - } - - node = rootBlockNode.lastChild; - if (node && node.type == 3) { - node.value = node.value.replace(endWhiteSpaceRegExp, ''); - } - } - } - - // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root - if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { - return; - } - - while (node) { - next = node.next; - - if (node.type == 3 || (node.type == 1 && node.name !== 'p' && - !blockElements[node.name] && !node.attr('data-mce-type'))) { - if (!rootBlockNode) { - // Create a new root block element - rootBlockNode = createNode(rootBlockName, 1); - rootBlockNode.attr(settings.forced_root_block_attrs); - rootNode.insert(rootBlockNode, node); - rootBlockNode.append(node); - } else { - rootBlockNode.append(node); - } - } else { - trim(rootBlockNode); - rootBlockNode = null; - } - - node = next; - } - - trim(rootBlockNode); - } - - function createNode(name, type) { - var node = new Node(name, type), list; - - if (name in nodeFilters) { - list = matchedNodes[name]; - - if (list) { - list.push(node); - } else { - matchedNodes[name] = [node]; - } - } - - return node; - } - - function removeWhitespaceBefore(node) { - var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements(); - - for (textNode = node.prev; textNode && textNode.type === 3;) { - textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); - - // Found a text node with non whitespace then trim that and break - if (textVal.length > 0) { - textNode.value = textVal; - return; - } - - textNodeNext = textNode.next; - - // Fix for bug #7543 where bogus nodes would produce empty - // text nodes and these would be removed if a nested list was before it - if (textNodeNext) { - if (textNodeNext.type == 3 && textNodeNext.value.length) { - textNode = textNode.prev; - continue; - } - - if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') { - textNode = textNode.prev; - continue; - } - } - - sibling = textNode.prev; - textNode.remove(); - textNode = sibling; - } - } - - function cloneAndExcludeBlocks(input) { - var name, output = {}; - - for (name in input) { - if (name !== 'li' && name != 'p') { - output[name] = input[name]; - } - } - - return output; - } - - parser = new SaxParser({ - validate: validate, - allow_script_urls: settings.allow_script_urls, - allow_conditional_comments: settings.allow_conditional_comments, - - // Exclude P and LI from DOM parsing since it's treated better by the DOM parser - self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), - - cdata: function (text) { - node.append(createNode('#cdata', 4)).value = text; - }, - - text: function (text, raw) { - var textNode; - - // Trim all redundant whitespace on non white space elements - if (!isInWhiteSpacePreservedElement) { - text = text.replace(allWhiteSpaceRegExp, ' '); - - if (node.lastChild && blockElements[node.lastChild.name]) { - text = text.replace(startWhiteSpaceRegExp, ''); - } - } - - // Do we need to create the node - if (text.length !== 0) { - textNode = createNode('#text', 3); - textNode.raw = !!raw; - node.append(textNode).value = text; - } - }, - - comment: function (text) { - node.append(createNode('#comment', 8)).value = text; - }, - - pi: function (name, text) { - node.append(createNode(name, 7)).value = text; - removeWhitespaceBefore(node); - }, - - doctype: function (text) { - var newNode; - - newNode = node.append(createNode('#doctype', 10)); - newNode.value = text; - removeWhitespaceBefore(node); - }, - - start: function (name, attrs, empty) { - var newNode, attrFiltersLen, elementRule, attrName, parent; - - elementRule = validate ? schema.getElementRule(name) : {}; - if (elementRule) { - newNode = createNode(elementRule.outputName || name, 1); - newNode.attributes = attrs; - newNode.shortEnded = empty; - - node.append(newNode); - - // Check if node is valid child of the parent node is the child is - // unknown we don't collect it since it's probably a custom element - parent = children[node.name]; - if (parent && children[newNode.name] && !parent[newNode.name]) { - invalidChildren.push(newNode); - } - - attrFiltersLen = attributeFilters.length; - while (attrFiltersLen--) { - attrName = attributeFilters[attrFiltersLen].name; - - if (attrName in attrs.map) { - list = matchedAttributes[attrName]; - - if (list) { - list.push(newNode); - } else { - matchedAttributes[attrName] = [newNode]; - } - } - } - - // Trim whitespace before block - if (blockElements[name]) { - removeWhitespaceBefore(newNode); - } - - // Change current node if the element wasn't empty i.e nota
- lastParent = node; - while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { - lastParent = parent; - - if (blockElements[parent.name]) { - break; - } - - parent = parent.parent; - } - - if (lastParent === parent && settings.padd_empty_with_br !== true) { - textNode = new Node('#text', 3); - textNode.value = '\u00a0'; - node.replace(textNode); - } - } - } - }); - } - - if (!settings.allow_unsafe_link_target) { - self.addAttributeFilter('href', function (nodes) { - var i = nodes.length, node; - - var appendRel = function (rel) { - var parts = rel.split(' ').filter(function (p) { - return p.length > 0; - }); - return parts.concat(['noopener']).join(' '); - }; - - var addNoOpener = function (rel) { - var newRel = rel ? Tools.trim(rel) : ''; - if (!/\b(noopener)\b/g.test(newRel)) { - return appendRel(newRel); - } else { - return newRel; - } - }; - - while (i--) { - node = nodes[i]; - if (node.name === 'a' && node.attr('target') === '_blank') { - node.attr('rel', addNoOpener(node.attr('rel'))); - } - } - }); - } - - // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. - if (!settings.allow_html_in_named_anchor) { - self.addAttributeFilter('id,name', function (nodes) { - var i = nodes.length, sibling, prevSibling, parent, node; - - while (i--) { - node = nodes[i]; - if (node.name === 'a' && node.firstChild && !node.attr('href')) { - parent = node.parent; - - // Move children after current node - sibling = node.lastChild; - do { - prevSibling = sibling.prev; - parent.insert(sibling, node); - sibling = prevSibling; - } while (sibling); - } - } - }); - } - - if (settings.fix_list_elements) { - self.addNodeFilter('ul,ol', function (nodes) { - var i = nodes.length, node, parentNode; - - while (i--) { - node = nodes[i]; - parentNode = node.parent; - - if (parentNode.name === 'ul' || parentNode.name === 'ol') { - if (node.prev && node.prev.name === 'li') { - node.prev.append(node); - } else { - var li = new Node('li', 1); - li.attr('style', 'list-style-type: none'); - node.wrap(li); - } - } - } - }); - } - - 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); - } - }); - } - }; - } -); - -/** - * Writer.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. - * - * @class tinymce.html.Writer - * @example - * var writer = new tinymce.html.Writer({indent: true}); - * var parser = new tinymce.html.SaxParser(writer).parse('
.
- *
- * @method start
- * @param {String} name Name of the element.
- * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
- * @param {Boolean} empty Optional empty state if the tag should end like
.
- */
- start: function (name, attrs, empty) {
- var i, l, attr, value;
-
- if (indent && indentBefore[name] && html.length > 0) {
- value = html[html.length - 1];
-
- if (value.length > 0 && value !== '\n') {
- html.push('\n');
- }
- }
-
- html.push('<', name);
-
- if (attrs) {
- for (i = 0, l = attrs.length; i < l; i++) {
- attr = attrs[i];
- html.push(' ', attr.name, '="', encode(attr.value, true), '"');
- }
- }
-
- if (!empty || htmlOutput) {
- html[html.length] = '>';
- } else {
- html[html.length] = ' />';
- }
-
- if (empty && indent && indentAfter[name] && html.length > 0) {
- value = html[html.length - 1];
-
- if (value.length > 0 && value !== '\n') {
- html.push('\n');
- }
- }
- },
-
- /**
- * Writes the a end element such as
text
')); - * @class tinymce.html.Serializer - * @version 3.4 - */ -define( - 'tinymce.core.html.Serializer', - [ - "tinymce.core.html.Writer", - "tinymce.core.html.Schema" - ], - function (Writer, Schema) { - /** - * Constructs a new Serializer instance. - * - * @constructor - * @method Serializer - * @param {Object} settings Name/value settings object. - * @param {tinymce.html.Schema} schema Schema instance to use. - */ - return function (settings, schema) { - var self = this, writer = new Writer(settings); - - settings = settings || {}; - settings.validate = "validate" in settings ? settings.validate : true; - - self.schema = schema = schema || new Schema(); - self.writer = writer; - - /** - * Serializes the specified node into a string. - * - * @example - * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('text
')); - * @method serialize - * @param {tinymce.html.Node} node Node instance to serialize. - * @return {String} String with HTML based on DOM tree. - */ - self.serialize = function (node) { - var handlers, validate; - - validate = settings.validate; - - handlers = { - // #text - 3: function (node) { - writer.text(node.value, node.raw); - }, - - // #comment - 8: function (node) { - writer.comment(node.value); - }, - - // Processing instruction - 7: function (node) { - writer.pi(node.name, node.value); - }, - - // Doctype - 10: function (node) { - writer.doctype(node.value); - }, - - // CDATA - 4: function (node) { - writer.cdata(node.value); - }, - - // Document fragment - 11: function (node) { - if ((node = node.firstChild)) { - do { - walk(node); - } while ((node = node.next)); - } - } - }; - - writer.reset(); - - function walk(node) { - var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; - - if (!handler) { - name = node.name; - isEmpty = node.shortEnded; - attrs = node.attributes; - - // Sort attributes - if (validate && attrs && attrs.length > 1) { - sortedAttrs = []; - sortedAttrs.map = {}; - - elementRule = schema.getElementRule(node.name); - if (elementRule) { - for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { - attrName = elementRule.attributesOrder[i]; - - if (attrName in attrs.map) { - attrValue = attrs.map[attrName]; - sortedAttrs.map[attrName] = attrValue; - sortedAttrs.push({ name: attrName, value: attrValue }); - } - } - - for (i = 0, l = attrs.length; i < l; i++) { - attrName = attrs[i].name; - - if (!(attrName in sortedAttrs.map)) { - attrValue = attrs.map[attrName]; - sortedAttrs.map[attrName] = attrValue; - sortedAttrs.push({ name: attrName, value: attrValue }); - } - } - - attrs = sortedAttrs; - } - } - - writer.start(node.name, attrs, isEmpty); - - if (!isEmpty) { - if ((node = node.firstChild)) { - do { - walk(node); - } while ((node = node.next)); - } - - writer.end(name); - } - } else { - handler(node); - } - } - - // Serialize element and treat all non elements as fragments - if (node.type == 1 && !settings.inner) { - walk(node); - } else { - handlers[11](node); - } - - return writer.getContent(); - }; - }; - } -); - -/** - * Serializer.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for - * more details and examples on how to use this class. - * - * @class tinymce.dom.Serializer - */ -define( - 'tinymce.core.dom.Serializer', - [ - "tinymce.core.dom.DOMUtils", - "tinymce.core.html.DomParser", - "tinymce.core.html.SaxParser", - "tinymce.core.html.Entities", - "tinymce.core.html.Serializer", - "tinymce.core.html.Node", - "tinymce.core.html.Schema", - "tinymce.core.Env", - "tinymce.core.util.Tools", - "tinymce.core.text.Zwsp" - ], - function (DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) { - var each = Tools.each, trim = Tools.trim; - var DOM = DOMUtils.DOM; - - /** - * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when - * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync - * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML - * but not as the lastChild of the body. So this fix simply removes the last two - * BR elements at the end of the document. - * - * Example of what happens: text becomes text|
would become this:|
- sibling = startContainer.previousSibling; - if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { - sibling.innerHTML = ''; - } else { - sibling = null; - } - - startContainer.innerHTML = ''; - ieRng.moveToElementText(startContainer.lastChild); - ieRng.select(); - dom.doc.selection.clear(); - startContainer.innerHTML = ''; - - if (sibling) { - sibling.innerHTML = ''; - } - return; - } - - startOffset = dom.nodeIndex(startContainer); - startContainer = startContainer.parentNode; - } - - if (startOffset == endOffset - 1) { - try { - ctrlElm = startContainer.childNodes[startOffset]; - ctrlRng = body.createControlRange(); - ctrlRng.addElement(ctrlElm); - ctrlRng.select(); - - // Check if the range produced is on the correct element and is a control range - // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 - nativeRng = selection.getRng(); - if (nativeRng.item && ctrlElm === nativeRng.item(0)) { - return; - } - } catch (ex) { - // Ignore - } - } - } - - // Set start/end point of selection - setEndPoint(true); - setEndPoint(); - - // Select the new range and scroll it into view - ieRng.select(); - }; - - // Expose range method - this.getRangeAt = getRange; + return parents; } - return Selection; + function nodeAtIndex(container, offset) { + if (container.hasChildNodes() && offset < container.childNodes.length) { + return container.childNodes[offset]; + } + + return null; + } + + function getCaretCandidatePosition(direction, node) { + if (isForwards(direction)) { + if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { + return CaretPosition.before(node); + } + + if (isText(node)) { + return CaretPosition(node, 0); + } + } + + if (isBackwards(direction)) { + if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) { + return CaretPosition.after(node); + } + + if (isText(node)) { + return CaretPosition(node, node.data.length); + } + } + + if (isBackwards(direction)) { + if (isBr(node)) { + return CaretPosition.before(node); + } + + return CaretPosition.after(node); + } + + return CaretPosition.before(node); + } + + // Jumps over BR elements|
a
->|a
+ function isBrBeforeBlock(node, rootNode) { + var next; + + if (!NodeType.isBr(node)) { + return false; + } + + next = findCaretPosition(1, CaretPosition.after(node), rootNode); + if (!next) { + return false; + } + + return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode); + } + + function findCaretPosition(direction, startCaretPosition, rootNode) { + var container, offset, node, nextNode, innerNode, + rootContentEditableFalseElm, caretPosition; + + if (!isElement(rootNode) || !startCaretPosition) { + return null; + } + + if (startCaretPosition.isEqual(CaretPosition.after(rootNode)) && rootNode.lastChild) { + caretPosition = CaretPosition.after(rootNode.lastChild); + if (isBackwards(direction) && isCaretCandidate(rootNode.lastChild) && isElement(rootNode.lastChild)) { + return isBr(rootNode.lastChild) ? CaretPosition.before(rootNode.lastChild) : caretPosition; + } + } else { + caretPosition = startCaretPosition; + } + + container = caretPosition.container(); + offset = caretPosition.offset(); + + if (isText(container)) { + if (isBackwards(direction) && offset > 0) { + return CaretPosition(container, --offset); + } + + if (isForwards(direction) && offset < container.length) { + return CaretPosition(container, ++offset); + } + + node = container; + } else { + if (isBackwards(direction) && offset > 0) { + nextNode = nodeAtIndex(container, offset - 1); + if (isCaretCandidate(nextNode)) { + if (!isAtomic(nextNode)) { + innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); + if (innerNode) { + if (isText(innerNode)) { + return CaretPosition(innerNode, innerNode.data.length); + } + + return CaretPosition.after(innerNode); + } + } + + if (isText(nextNode)) { + return CaretPosition(nextNode, nextNode.data.length); + } + + return CaretPosition.before(nextNode); + } + } + + if (isForwards(direction) && offset < container.childNodes.length) { + nextNode = nodeAtIndex(container, offset); + if (isCaretCandidate(nextNode)) { + if (isBrBeforeBlock(nextNode, rootNode)) { + return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode); + } + + if (!isAtomic(nextNode)) { + innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); + if (innerNode) { + if (isText(innerNode)) { + return CaretPosition(innerNode, 0); + } + + return CaretPosition.before(innerNode); + } + } + + if (isText(nextNode)) { + return CaretPosition(nextNode, 0); + } + + return CaretPosition.after(nextNode); + } + } + + node = caretPosition.getNode(); + } + + if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) { + node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true); + if (isEditableCaretCandidate(node)) { + return getCaretCandidatePosition(direction, node); + } + } + + nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); + + rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse)); + if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { + if (isForwards(direction)) { + caretPosition = CaretPosition.after(rootContentEditableFalseElm); + } else { + caretPosition = CaretPosition.before(rootContentEditableFalseElm); + } + + return caretPosition; + } + + if (nextNode) { + return getCaretCandidatePosition(direction, nextNode); + } + + return null; + } + + return function (rootNode) { + return { + /** + * Returns the next logical caret position from the specificed input + * caretPoisiton or null if there isn't any more positions left for example + * at the end specified root element. + * + * @method next + * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. + * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. + */ + next: function (caretPosition) { + return findCaretPosition(1, caretPosition, rootNode); + }, + + /** + * Returns the previous logical caret position from the specificed input + * caretPoisiton or null if there isn't any more positions left for example + * at the end specified root element. + * + * @method prev + * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. + * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. + */ + prev: function (caretPosition) { + return findCaretPosition(-1, caretPosition, rootNode); + } + }; + }; + } +); +/** + * CaretFinder.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.caret.CaretFinder', + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'tinymce.core.caret.CaretCandidate', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.caret.CaretUtils', + 'tinymce.core.caret.CaretWalker', + 'tinymce.core.dom.NodeType' + ], + function (Fun, Option, CaretCandidate, CaretPosition, CaretUtils, CaretWalker, NodeType) { + var walkToPositionIn = function (forward, rootNode, startNode) { + var position = forward ? CaretPosition.before(startNode) : CaretPosition.after(startNode); + return fromPosition(forward, rootNode, position); + }; + + var afterElement = function (node) { + return NodeType.isBr(node) ? CaretPosition.before(node) : CaretPosition.after(node); + }; + + var isBeforeOrStart = function (position) { + if (CaretPosition.isTextPosition(position)) { + return position.offset() === 0; + } else { + return CaretCandidate.isCaretCandidate(position.getNode()); + } + }; + + var isAfterOrEnd = function (position) { + if (CaretPosition.isTextPosition(position)) { + return position.offset() === position.container().data.length; + } else { + return CaretCandidate.isCaretCandidate(position.getNode(true)); + } + }; + + var isBeforeAfterSameElement = function (from, to) { + return !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true); + }; + + var isAtBr = function (position) { + return !CaretPosition.isTextPosition(position) && NodeType.isBr(position.getNode()); + }; + + var shouldSkipPosition = function (forward, from, to) { + if (forward) { + return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to); + } else { + return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to); + } + }; + + // Finds:a|b
->a|b
+ var fromPosition = function (forward, rootNode, position) { + var walker = new CaretWalker(rootNode); + return Option.from(forward ? walker.next(position) : walker.prev(position)); + }; + + // Finds:a|b
->ab|
+ var navigate = function (forward, rootNode, from) { + return fromPosition(forward, rootNode, from).bind(function (to) { + if (CaretUtils.isInSameBlock(from, to, rootNode) && shouldSkipPosition(forward, from, to)) { + return fromPosition(forward, rootNode, to); + } else { + return Option.some(to); + } + }); + }; + + var positionIn = function (forward, element) { + var startNode = forward ? element.firstChild : element.lastChild; + if (NodeType.isText(startNode)) { + return Option.some(new CaretPosition(startNode, forward ? 0 : startNode.data.length)); + } else if (startNode) { + if (CaretCandidate.isCaretCandidate(startNode)) { + return Option.some(forward ? CaretPosition.before(startNode) : afterElement(startNode)); + } else { + return walkToPositionIn(forward, element, startNode); + } + } else { + return Option.none(); + } + }; + + return { + fromPosition: fromPosition, + nextPosition: Fun.curry(fromPosition, true), + prevPosition: Fun.curry(fromPosition, false), + navigate: navigate, + positionIn: positionIn, + firstPositionIn: Fun.curry(positionIn, true), + lastPositionIn: Fun.curry(positionIn, false) + }; + } +); + +/** + * RangeNormalizer.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.dom.RangeNormalizer', + [ + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.caret.CaretUtils' + ], + function (CaretFinder, CaretPosition, CaretUtils) { + var createRange = function (sc, so, ec, eo) { + var rng = document.createRange(); + rng.setStart(sc, so); + rng.setEnd(ec, eo); + return rng; + }; + + // If you triple click a paragraph in this case: + //a
b
+ // It would become this range in webkit: + //[a
]b
+ // We would want it to be: + //[a]
b
+ // Since it would otherwise produces spans out of thin air on insertContent for example. + var normalizeBlockSelectionRange = function (rng) { + var startPos = CaretPosition.fromRangeStart(rng); + var endPos = CaretPosition.fromRangeEnd(rng); + var rootNode = rng.commonAncestorContainer; + + return CaretFinder.fromPosition(false, rootNode, endPos) + .map(function (newEndPos) { + if (!CaretUtils.isInSameBlock(startPos, endPos, rootNode) && CaretUtils.isInSameBlock(startPos, newEndPos, rootNode)) { + return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset()); + } else { + return rng; + } + }).getOr(rng); + }; + + var normalizeBlockSelection = function (rng) { + return rng.collapsed ? rng : normalizeBlockSelectionRange(rng); + }; + + var normalize = function (rng) { + return normalizeBlockSelection(rng); + }; + + return { + normalize: normalize + }; + } +); +define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; }); +defineGlobal("global!document", document); +define( + 'ephox.sugar.api.node.Element', + + [ + 'ephox.katamari.api.Fun', + 'global!Error', + 'global!console', + 'global!document' + ], + + function (Fun, Error, console, document) { + var fromHtml = function (html, scope) { + var doc = scope || document; + var div = doc.createElement('div'); + div.innerHTML = html; + if (!div.hasChildNodes() || div.childNodes.length > 1) { + console.error('HTML does not have a single root node', html); + throw 'HTML must have a single root node'; + } + return fromDom(div.childNodes[0]); + }; + + var fromTag = function (tag, scope) { + var doc = scope || document; + var node = doc.createElement(tag); + return fromDom(node); + }; + + var fromText = function (text, scope) { + var doc = scope || document; + var node = doc.createTextNode(text); + return fromDom(node); + }; + + var fromDom = function (node) { + if (node === null || node === undefined) throw new Error('Node cannot be null or undefined'); + return { + dom: Fun.constant(node) + }; + }; + + return { + fromHtml: fromHtml, + fromTag: fromTag, + fromText: fromText, + fromDom: fromDom + }; } ); @@ -19787,58 +16349,6 @@ define( }; } ); -define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; }); -defineGlobal("global!document", document); -define( - 'ephox.sugar.api.node.Element', - - [ - 'ephox.katamari.api.Fun', - 'global!Error', - 'global!console', - 'global!document' - ], - - function (Fun, Error, console, document) { - var fromHtml = function (html, scope) { - var doc = scope || document; - var div = doc.createElement('div'); - div.innerHTML = html; - if (!div.hasChildNodes() || div.childNodes.length > 1) { - console.error('HTML does not have a single root node', html); - throw 'HTML must have a single root node'; - } - return fromDom(div.childNodes[0]); - }; - - var fromTag = function (tag, scope) { - var doc = scope || document; - var node = doc.createElement(tag); - return fromDom(node); - }; - - var fromText = function (text, scope) { - var doc = scope || document; - var node = doc.createTextNode(text); - return fromDom(node); - }; - - var fromDom = function (node) { - if (node === null || node === undefined) throw new Error('Node cannot be null or undefined'); - return { - dom: Fun.constant(node) - }; - }; - - return { - fromHtml: fromHtml, - fromTag: fromTag, - fromText: fromText, - fromDom: fromDom - }; - } -); - define( 'ephox.sugar.api.node.NodeTypes', @@ -20207,151 +16717,6 @@ define( } ); -define( - 'ephox.sugar.api.node.Node', - - [ - 'ephox.sugar.api.node.NodeTypes' - ], - - function (NodeTypes) { - var name = function (element) { - var r = element.dom().nodeName; - return r.toLowerCase(); - }; - - var type = function (element) { - return element.dom().nodeType; - }; - - var value = function (element) { - return element.dom().nodeValue; - }; - - var isType = function (t) { - return function (element) { - return type(element) === t; - }; - }; - - var isComment = function (element) { - return type(element) === NodeTypes.COMMENT || name(element) === '#comment'; - }; - - var isElement = isType(NodeTypes.ELEMENT); - var isText = isType(NodeTypes.TEXT); - var isDocument = isType(NodeTypes.DOCUMENT); - - return { - name: name, - type: type, - value: value, - isElement: isElement, - isText: isText, - isDocument: isDocument, - isComment: isComment - }; - } -); - -define( - 'ephox.sugar.api.properties.Attr', - - [ - 'ephox.katamari.api.Type', - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Obj', - 'ephox.sugar.api.node.Node', - 'global!Error', - 'global!console' - ], - - /* - * Direct attribute manipulation has been around since IE8, but - * was apparently unstable until IE10. - */ - function (Type, Arr, Obj, Node, Error, console) { - var rawSet = function (dom, key, value) { - /* - * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined. - * - * We fail on those invalid cases, only allowing numbers and booleans. - */ - if (Type.isString(value) || Type.isBoolean(value) || Type.isNumber(value)) { - dom.setAttribute(key, value + ''); - } else { - console.error('Invalid call to Attr.set. Key ', key, ':: Value ', value, ':: Element ', dom); - throw new Error('Attribute value was not simple'); - } - }; - - var set = function (element, key, value) { - rawSet(element.dom(), key, value); - }; - - var setAll = function (element, attrs) { - var dom = element.dom(); - Obj.each(attrs, function (v, k) { - rawSet(dom, k, v); - }); - }; - - var get = function (element, key) { - var v = element.dom().getAttribute(key); - - // undefined is the more appropriate value for JS, and this matches JQuery - return v === null ? undefined : v; - }; - - var has = function (element, key) { - var dom = element.dom(); - - // return false for non-element nodes, no point in throwing an error - return dom && dom.hasAttribute ? dom.hasAttribute(key) : false; - }; - - var remove = function (element, key) { - element.dom().removeAttribute(key); - }; - - var hasNone = function (element) { - var attrs = element.dom().attributes; - return attrs === undefined || attrs === null || attrs.length === 0; - }; - - var clone = function (element) { - return Arr.foldl(element.dom().attributes, function (acc, attr) { - acc[attr.name] = attr.value; - return acc; - }, {}); - }; - - var transferOne = function (source, destination, attr) { - // NOTE: We don't want to clobber any existing attributes - if (has(source, attr) && !has(destination, attr)) set(destination, attr, get(source, attr)); - }; - - // Transfer attributes(attrs) from source to destination, unless they are already present - var transfer = function (source, destination, attrs) { - if (!Node.isElement(source) || !Node.isElement(destination)) return; - Arr.each(attrs, function (attr) { - transferOne(source, destination, attr); - }); - }; - - return { - clone: clone, - set: set, - setAll: setAll, - get: get, - has: has, - remove: remove, - hasNone: hasNone, - transfer: transfer - }; - } -); - define( 'ephox.sugar.api.dom.InsertAll', @@ -20440,100 +16805,274 @@ define( ); define( - 'ephox.sugar.api.dom.Replication', + 'ephox.sugar.api.node.Node', [ - 'ephox.sugar.api.properties.Attr', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.dom.Insert', - 'ephox.sugar.api.dom.InsertAll', - 'ephox.sugar.api.dom.Remove', - 'ephox.sugar.api.search.Traverse' + 'ephox.sugar.api.node.NodeTypes' ], - function (Attr, Element, Insert, InsertAll, Remove, Traverse) { - var clone = function (original, deep) { - return Element.fromDom(original.dom().cloneNode(deep)); + function (NodeTypes) { + var name = function (element) { + var r = element.dom().nodeName; + return r.toLowerCase(); }; - /** Shallow clone - just the tag, no children */ - var shallow = function (original) { - return clone(original, false); + var type = function (element) { + return element.dom().nodeType; }; - /** Deep clone - everything copied including children */ - var deep = function (original) { - return clone(original, true); + var value = function (element) { + return element.dom().nodeValue; }; - /** Shallow clone, with a new tag */ - var shallowAs = function (original, tag) { - var nu = Element.fromTag(tag); - - var attributes = Attr.clone(original); - Attr.setAll(nu, attributes); - - return nu; + var isType = function (t) { + return function (element) { + return type(element) === t; + }; }; - /** Deep clone, with a new tag */ - var copy = function (original, tag) { - var nu = shallowAs(original, tag); - - // NOTE - // previously this used serialisation: - // nu.dom().innerHTML = original.dom().innerHTML; - // - // Clone should be equivalent (and faster), but if TD <-> TH toggle breaks, put it back. - - var cloneChildren = Traverse.children(deep(original)); - InsertAll.append(nu, cloneChildren); - - return nu; + var isComment = function (element) { + return type(element) === NodeTypes.COMMENT || name(element) === '#comment'; }; - /** Change the tag name, but keep all children */ - var mutate = function (original, tag) { - var nu = shallowAs(original, tag); - - Insert.before(original, nu); - var children = Traverse.children(original); - InsertAll.append(nu, children); - Remove.remove(original); - return nu; - }; + var isElement = isType(NodeTypes.ELEMENT); + var isText = isType(NodeTypes.TEXT); + var isDocument = isType(NodeTypes.DOCUMENT); return { - shallow: shallow, - shallowAs: shallowAs, - deep: deep, - copy: copy, - mutate: mutate + name: name, + type: type, + value: value, + isElement: isElement, + isText: isText, + isDocument: isDocument, + isComment: isComment }; } ); define( - 'ephox.sugar.api.node.Fragment', + 'ephox.sugar.impl.NodeValue', [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.node.Element', - 'global!document' + 'ephox.sand.api.PlatformDetection', + 'ephox.katamari.api.Option', + 'global!Error' ], - function (Arr, Element, document) { - var fromElements = function (elements, scope) { - var doc = scope || document; - var fragment = doc.createDocumentFragment(); - Arr.each(elements, function (element) { - fragment.appendChild(element.dom()); - }); - return Element.fromDom(fragment); + function (PlatformDetection, Option, Error) { + return function (is, name) { + var get = function (element) { + if (!is(element)) throw new Error('Can only get ' + name + ' value of a ' + name + ' node'); + return getOption(element).getOr(''); + }; + + var getOptionIE10 = function (element) { + // Prevent IE10 from throwing exception when setting parent innerHTML clobbers (TBIO-451). + try { + return getOptionSafe(element); + } catch (e) { + return Option.none(); + } + }; + + var getOptionSafe = function (element) { + return is(element) ? Option.from(element.dom().nodeValue) : Option.none(); + }; + + var browser = PlatformDetection.detect().browser; + var getOption = browser.isIE() && browser.version.major === 10 ? getOptionIE10 : getOptionSafe; + + var set = function (element, value) { + if (!is(element)) throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node'); + element.dom().nodeValue = value; + }; + + return { + get: get, + getOption: getOption, + set: set + }; + }; + } +); +define( + 'ephox.sugar.api.node.Text', + + [ + 'ephox.sugar.api.node.Node', + 'ephox.sugar.impl.NodeValue' + ], + + function (Node, NodeValue) { + var api = NodeValue(Node.isText, 'text'); + + var get = function (element) { + return api.get(element); + }; + + var getOption = function (element) { + return api.getOption(element); + }; + + var set = function (element, value) { + api.set(element, value); }; return { - fromElements: fromElements + get: get, + getOption: getOption, + set: set + }; + } +); + +define( + 'ephox.sugar.api.node.Body', + + [ + 'ephox.katamari.api.Thunk', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'global!document' + ], + + function (Thunk, Element, Node, document) { + + // Node.contains() is very, very, very good performance + // http://jsperf.com/closest-vs-contains/5 + var inBody = function (element) { + // Technically this is only required on IE, where contains() returns false for text nodes. + // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet). + var dom = Node.isText(element) ? element.dom().parentNode : element.dom(); + + // use ownerDocument.body to ensure this works inside iframes. + // Normally contains is bad because an element "contains" itself, but here we want that. + return dom !== undefined && dom !== null && dom.ownerDocument.body.contains(dom); + }; + + var body = Thunk.cached(function() { + return getBody(Element.fromDom(document)); + }); + + var getBody = function (doc) { + var body = doc.dom().body; + if (body === null || body === undefined) throw 'Body is not available yet'; + return Element.fromDom(body); + }; + + return { + body: body, + getBody: getBody, + inBody: inBody + }; + } +); + +define( + 'ephox.sugar.api.search.PredicateFilter', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.search.Traverse' + ], + + function (Arr, Body, Traverse) { + // maybe TraverseWith, similar to traverse but with a predicate? + + var all = function (predicate) { + return descendants(Body.body(), predicate); + }; + + var ancestors = function (scope, predicate, isRoot) { + return Arr.filter(Traverse.parents(scope, isRoot), predicate); + }; + + var siblings = function (scope, predicate) { + return Arr.filter(Traverse.siblings(scope), predicate); + }; + + var children = function (scope, predicate) { + return Arr.filter(Traverse.children(scope), predicate); + }; + + var descendants = function (scope, predicate) { + var result = []; + + // Recurse.toArray() might help here + Arr.each(Traverse.children(scope), function (x) { + if (predicate(x)) { + result = result.concat([ x ]); + } + result = result.concat(descendants(x, predicate)); + }); + return result; + }; + + return { + all: all, + ancestors: ancestors, + siblings: siblings, + children: children, + descendants: descendants + }; + } +); + +define( + 'ephox.sugar.api.search.SelectorFilter', + + [ + 'ephox.sugar.api.search.PredicateFilter', + 'ephox.sugar.api.search.Selectors' + ], + + function (PredicateFilter, Selectors) { + var all = function (selector) { + return Selectors.all(selector); + }; + + // For all of the following: + // + // jQuery does siblings of firstChild. IE9+ supports scope.dom().children (similar to Traverse.children but elements only). + // Traverse should also do this (but probably not by default). + // + + var ancestors = function (scope, selector, isRoot) { + // It may surprise you to learn this is exactly what JQuery does + // TODO: Avoid all this wrapping and unwrapping + return PredicateFilter.ancestors(scope, function (e) { + return Selectors.is(e, selector); + }, isRoot); + }; + + var siblings = function (scope, selector) { + // It may surprise you to learn this is exactly what JQuery does + // TODO: Avoid all the wrapping and unwrapping + return PredicateFilter.siblings(scope, function (e) { + return Selectors.is(e, selector); + }); + }; + + var children = function (scope, selector) { + // It may surprise you to learn this is exactly what JQuery does + // TODO: Avoid all the wrapping and unwrapping + return PredicateFilter.children(scope, function (e) { + return Selectors.is(e, selector); + }); + }; + + var descendants = function (scope, selector) { + return Selectors.all(selector, scope); + }; + + return { + all: all, + ancestors: ancestors, + siblings: siblings, + children: children, + descendants: descendants }; } ); @@ -20571,6 +17110,7 @@ define( ]; var tableCells = ['td', 'th']; + var tableSections = ['thead', 'tbody', 'tfoot']; var textBlocks = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', @@ -20579,6 +17119,8 @@ define( ]; var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + var listItems = ['li', 'dd', 'dt']; + var lists = ['ul', 'ol', 'dl']; var lazyLookup = function (items) { var lookup; @@ -20596,19 +17138,27 @@ define( return Node.isElement(node) && !isBlock(node); }; + var isBr = function (node) { + return Node.isElement(node) && Node.name(node) === 'br'; + }; + return { isBlock: isBlock, isInline: isInline, isHeading: isHeading, isTextBlock: lazyLookup(textBlocks), + isList: lazyLookup(lists), + isListItem: lazyLookup(listItems), isVoid: lazyLookup(voids), - isTableCell: lazyLookup(tableCells) + isTableSection: lazyLookup(tableSections), + isTableCell: lazyLookup(tableCells), + isBr: isBr }; } ); /** - * Parents.js + * PaddingBr.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -20618,259 +17168,71 @@ define( */ define( - 'tinymce.core.dom.Parents', - [ - 'ephox.katamari.api.Fun', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.search.Traverse' - ], - function (Fun, Compare, Traverse) { - var dropLast = function (xs) { - return xs.slice(0, -1); - }; - - var parentsUntil = function (startNode, rootElm, predicate) { - if (Compare.contains(rootElm, startNode)) { - return dropLast(Traverse.parents(startNode, function (elm) { - return predicate(elm) || Compare.eq(elm, rootElm); - })); - } else { - return []; - } - }; - - var parents = function (startNode, rootElm) { - return parentsUntil(startNode, rootElm, Fun.constant(false)); - }; - - var parentsAndSelf = function (startNode, rootElm) { - return [startNode].concat(parents(startNode, rootElm)); - }; - - return { - parentsUntil: parentsUntil, - parents: parents, - parentsAndSelf: parentsAndSelf - }; - } -); - -define( - 'ephox.katamari.api.Options', - - [ - 'ephox.katamari.api.Option' - ], - - function (Option) { - /** cat :: [Option a] -> [a] */ - var cat = function (arr) { - var r = []; - var push = function (x) { - r.push(x); - }; - for (var i = 0; i < arr.length; i++) { - arr[i].each(push); - } - return r; - }; - - /** findMap :: ([a], (a, Int -> Option b)) -> Option b */ - var findMap = function (arr, f) { - for (var i = 0; i < arr.length; i++) { - var r = f(arr[i], i); - if (r.isSome()) { - return r; - } - } - return Option.none(); - }; - - /** - * if all elements in arr are 'some', their inner values are passed as arguments to f - * f must have arity arr.length - */ - var liftN = function(arr, f) { - var r = []; - for (var i = 0; i < arr.length; i++) { - var x = arr[i]; - if (x.isSome()) { - r.push(x.getOrDie()); - } else { - return Option.none(); - } - } - return Option.some(f.apply(null, r)); - }; - - return { - cat: cat, - findMap: findMap, - liftN: liftN - }; - } -); - -/** - * SelectionUtils.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.selection.SelectionUtils', + 'tinymce.core.dom.PaddingBr', [ 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Options', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'ephox.sugar.api.search.Traverse', - 'tinymce.core.dom.NodeType' - ], - function (Arr, Fun, Option, Options, Compare, Element, Node, Traverse, NodeType) { - var getStartNode = function (rng) { - var sc = rng.startContainer, so = rng.startOffset; - if (NodeType.isText(sc)) { - return so === 0 ? Option.some(Element.fromDom(sc)) : Option.none(); - } else { - return Option.from(sc.childNodes[so]).map(Element.fromDom); - } - }; - - var getEndNode = function (rng) { - var ec = rng.endContainer, eo = rng.endOffset; - if (NodeType.isText(ec)) { - return eo === ec.data.length ? Option.some(Element.fromDom(ec)) : Option.none(); - } else { - return Option.from(ec.childNodes[eo - 1]).map(Element.fromDom); - } - }; - - var getFirstChildren = function (node) { - return Traverse.firstChild(node).fold( - Fun.constant([node]), - function (child) { - return [node].concat(getFirstChildren(child)); - } - ); - }; - - var getLastChildren = function (node) { - return Traverse.lastChild(node).fold( - Fun.constant([node]), - function (child) { - if (Node.name(child) === 'br') { - return Traverse.prevSibling(child).map(function (sibling) { - return [node].concat(getLastChildren(sibling)); - }).getOr([]); - } else { - return [node].concat(getLastChildren(child)); - } - } - ); - }; - - var hasAllContentsSelected = function (elm, rng) { - return Options.liftN([getStartNode(rng), getEndNode(rng)], function (startNode, endNode) { - var start = Arr.find(getFirstChildren(elm), Fun.curry(Compare.eq, startNode)); - var end = Arr.find(getLastChildren(elm), Fun.curry(Compare.eq, endNode)); - return start.isSome() && end.isSome(); - }).getOr(false); - }; - - return { - hasAllContentsSelected: hasAllContentsSelected - }; - } -); - -/** - * FragmentReader.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.selection.FragmentReader', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', 'ephox.sugar.api.dom.Insert', - 'ephox.sugar.api.dom.Replication', + 'ephox.sugar.api.dom.Remove', 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Fragment', 'ephox.sugar.api.node.Node', - 'tinymce.core.dom.ElementType', - 'tinymce.core.dom.Parents', - 'tinymce.core.selection.SelectionUtils' + 'ephox.sugar.api.node.Text', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.Traverse', + 'tinymce.core.dom.ElementType' ], - function (Arr, Fun, Insert, Replication, Element, Fragment, Node, ElementType, Parents, SelectionUtils) { - var findParentListContainer = function (parents) { - return Arr.find(parents, function (elm) { - return Node.name(elm) === 'ul' || Node.name(elm) === 'ol'; + function (Arr, Insert, Remove, Element, Node, Text, SelectorFilter, Traverse, ElementType) { + var getLastChildren = function (elm) { + var children = [], rawNode = elm.dom(); + + while (rawNode) { + children.push(Element.fromDom(rawNode)); + rawNode = rawNode.lastChild; + } + + return children; + }; + + var removeTrailingBr = function (elm) { + var allBrs = SelectorFilter.descendants(elm, 'br'); + var brs = Arr.filter(getLastChildren(elm).slice(-1), ElementType.isBr); + if (allBrs.length === brs.length) { + Arr.each(brs, Remove.remove); + } + }; + + var fillWithPaddingBr = function (elm) { + Remove.empty(elm); + Insert.append(elm, Element.fromHtml('*texttext*
! + // This will reduce the number of wrapper elements that needs to be created + // Move start point up the tree + if (format[0].inline || format[0].block_expand) { + if (!format[0].inline || (startContainer.nodeType !== 3 || startOffset === 0)) { + startContainer = findParentContainer(true); + } + + if (!format[0].inline || (endContainer.nodeType !== 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } + } + + // Expand start/end container to matching selector + if (format[0].selector && format[0].expand !== false && !format[0].inline) { + // Find new startContainer/endContainer if there is better one + startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); + endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); + } + + // Expand start/end container to matching block element or text node + if (format[0].block || format[0].selector) { + // Find new startContainer/endContainer if there is better one + startContainer = findBlockEndPoint(startContainer, 'previousSibling'); + endContainer = findBlockEndPoint(endContainer, 'nextSibling'); + + // Non block element then try to expand up the leaf + if (format[0].block) { + if (!dom.isBlock(startContainer)) { + startContainer = findParentContainer(true); + } + + if (!dom.isBlock(endContainer)) { + endContainer = findParentContainer(); + } + } + } + + // Setup index for startContainer + if (startContainer.nodeType === 1) { + startOffset = dom.nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } + + // Setup index for endContainer + if (endContainer.nodeType === 1) { + endOffset = dom.nodeIndex(endContainer) + 1; + endContainer = endContainer.parentNode; + } + + // Return new range like object + return { + startContainer: startContainer, + startOffset: startOffset, + endContainer: endContainer, + endOffset: endOffset + }; + }; + + return { + expandRng: expandRng + }; + } +); +/** + * MatchFormat.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.fmt.MatchFormat', + [ + 'tinymce.core.fmt.FormatUtils' + ], + function (FormatUtils) { + var isEq = FormatUtils.isEq; + + var matchesUnInheritedFormatSelector = function (ed, node, name) { + var formatList = ed.formatter.get(name); + + if (formatList) { + for (var i = 0; i < formatList.length; i++) { + if (formatList[i].inherit === false && ed.dom.is(node, formatList[i].selector)) { + return true; + } + } + } + + return false; + }; + + var matchParents = function (editor, node, name, vars) { + var root = editor.dom.getRoot(); + + if (node === root) { + return false; + } + + // Find first node with similar format settings + node = editor.dom.getParent(node, function (node) { + if (matchesUnInheritedFormatSelector(editor, node, name)) { + return true; + } + + return node.parentNode === root || !!matchNode(editor, node, name, vars, true); + }); + + // Do an exact check on the similar format element + return matchNode(editor, node, name, vars); + }; + + var matchName = function (dom, node, format) { + // Check for inline match + if (isEq(node, format.inline)) { + return true; + } + + // Check for block match + if (isEq(node, format.block)) { + return true; + } + + // Check for selector match + if (format.selector) { + return node.nodeType === 1 && dom.is(node, format.selector); } }; - return Selection; + var matchItems = function (dom, node, format, itemName, similar, vars) { + var key, value, items = format[itemName], i; + + // Custom match + if (format.onmatch) { + return format.onmatch(node, format, itemName); + } + + // Check all items + if (items) { + // Non indexed object + if (typeof items.length === 'undefined') { + for (key in items) { + if (items.hasOwnProperty(key)) { + if (itemName === 'attributes') { + value = dom.getAttrib(node, key); + } else { + value = FormatUtils.getStyle(dom, node, key); + } + + if (similar && !value && !format.exact) { + return; + } + + if ((!similar || format.exact) && !isEq(value, FormatUtils.normalizeStyleValue(dom, FormatUtils.replaceVars(items[key], vars), key))) { + return; + } + } + } + } else { + // Only one match needed for indexed arrays + for (i = 0; i < items.length; i++) { + if (itemName === 'attributes' ? dom.getAttrib(node, items[i]) : FormatUtils.getStyle(dom, node, items[i])) { + return format; + } + } + } + } + + return format; + }; + + var matchNode = function (ed, node, name, vars, similar) { + var formatList = ed.formatter.get(name), format, i, x, classes, dom = ed.dom; + + if (formatList && node) { + // Check each format in list + for (i = 0; i < formatList.length; i++) { + format = formatList[i]; + + // Name name, attributes, styles and classes + if (matchName(ed.dom, node, format) && matchItems(dom, node, format, 'attributes', similar, vars) && matchItems(dom, node, format, 'styles', similar, vars)) { + // Match classes + if ((classes = format.classes)) { + for (x = 0; x < classes.length; x++) { + if (!ed.dom.hasClass(node, classes[x])) { + return; + } + } + } + + return format; + } + } + } + }; + + var match = function (editor, name, vars, node) { + var startNode; + + // Check specified node + if (node) { + return matchParents(editor, node, name, vars); + } + + // Check selected node + node = editor.selection.getNode(); + if (matchParents(editor, node, name, vars)) { + return true; + } + + // Check start node if it's different + startNode = editor.selection.getStart(); + if (startNode !== node) { + if (matchParents(editor, startNode, name, vars)) { + return true; + } + } + + return false; + }; + + var matchAll = function (editor, names, vars) { + var startElement, matchedFormatNames = [], checkedMap = {}; + + // Check start of selection for formats + startElement = editor.selection.getStart(); + editor.dom.getParent(startElement, function (node) { + var i, name; + + for (i = 0; i < names.length; i++) { + name = names[i]; + + if (!checkedMap[name] && matchNode(editor, node, name, vars)) { + checkedMap[name] = true; + matchedFormatNames.push(name); + } + } + }, editor.dom.getRoot()); + + return matchedFormatNames; + }; + + var canApply = function (editor, name) { + var formatList = editor.formatter.get(name), startNode, parents, i, x, selector, dom = editor.dom; + + if (formatList) { + startNode = editor.selection.getStart(); + parents = FormatUtils.getParents(dom, startNode); + + for (x = formatList.length - 1; x >= 0; x--) { + selector = formatList[x].selector; + + // Format is not selector based then always return TRUE + // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line + if (!selector || formatList[x].defaultBlock) { + return true; + } + + for (i = parents.length - 1; i >= 0; i--) { + if (dom.is(parents[i], selector)) { + return true; + } + } + } + } + + return false; + }; + + return { + matchNode: matchNode, + matchName: matchName, + match: match, + matchAll: matchAll, + canApply: canApply, + matchesUnInheritedFormatSelector: matchesUnInheritedFormatSelector + }; + } +); +/** + * CaretFormat.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.fmt.CaretFormat', + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'tinymce.core.dom.PaddingBr', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.fmt.ExpandRange', + 'tinymce.core.fmt.FormatUtils', + 'tinymce.core.fmt.MatchFormat', + 'tinymce.core.text.Zwsp', + 'tinymce.core.util.Fun', + 'tinymce.core.util.Tools' + ], + function (Arr, Element, PaddingBr, RangeUtils, TreeWalker, ExpandRange, FormatUtils, MatchFormat, Zwsp, Fun, Tools) { + var ZWSP = Zwsp.ZWSP, CARET_ID = '_mce_caret', DEBUG = false; + + var isCaretNode = function (node) { + return node.nodeType === 1 && node.id === CARET_ID; + }; + + var isCaretContainerEmpty = function (node, nodes) { + while (node) { + if ((node.nodeType === 3 && node.nodeValue !== ZWSP) || node.childNodes.length > 1) { + return false; + } + + // Collect nodes + if (nodes && node.nodeType === 1) { + nodes.push(node); + } + + node = node.firstChild; + } + + return true; + }; + + var findFirstTextNode = function (node) { + var walker; + + if (node) { + walker = new TreeWalker(node, node); + + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3) { + return node; + } + } + } + + return null; + }; + + var createCaretContainer = function (dom, fill) { + var caretContainer = dom.create('span', { id: CARET_ID, 'data-mce-bogus': '1', style: DEBUG ? 'color:red' : '' }); + + if (fill) { + caretContainer.appendChild(dom.doc.createTextNode(ZWSP)); + } + + return caretContainer; + }; + + var getParentCaretContainer = function (node) { + while (node) { + if (node.id === CARET_ID) { + return node; + } + + node = node.parentNode; + } + }; + + // Checks if the parent caret container node isn't empty if that is the case it + // will remove the bogus state on all children that isn't empty + var unmarkBogusCaretParents = function (dom, selection) { + var caretContainer; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer && !dom.isEmpty(caretContainer)) { + Tools.walk(caretContainer, function (node) { + if (node.nodeType === 1 && node.id !== CARET_ID && !dom.isEmpty(node)) { + dom.setAttrib(node, 'data-mce-bogus', null); + } + }, 'childNodes'); + } + }; + + var trimZwspFromCaretContainer = function (caretContainerNode) { + var textNode = findFirstTextNode(caretContainerNode); + if (textNode && textNode.nodeValue.charAt(0) === ZWSP) { + textNode.deleteData(0, 1); + } + + return textNode; + }; + + var removeCaretContainerNode = function (dom, selection, node, moveCaret) { + var rng, block, textNode; + + rng = selection.getRng(true); + block = dom.getParent(node, dom.isBlock); + + if (isCaretContainerEmpty(node)) { + if (moveCaret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); + } + + dom.remove(node); + } else { + textNode = trimZwspFromCaretContainer(node); + if (rng.startContainer === textNode && rng.startOffset > 0) { + rng.setStart(textNode, rng.startOffset - 1); + } + + if (rng.endContainer === textNode && rng.endOffset > 0) { + rng.setEnd(textNode, rng.endOffset - 1); + } + + dom.remove(node, true); + } + + if (block && dom.isEmpty(block)) { + PaddingBr.fillWithPaddingBr(Element.fromDom(block)); + } + + selection.setRng(rng); + }; + + // Removes the caret container for the specified node or all on the current document + var removeCaretContainer = function (dom, selection, node, moveCaret) { + if (!node) { + node = getParentCaretContainer(selection.getStart()); + + if (!node) { + while ((node = dom.get(CARET_ID))) { + removeCaretContainerNode(dom, selection, node, false); + } + } + } else { + removeCaretContainerNode(dom, selection, node, moveCaret); + } + }; + + var insertCaretContainerNode = function (editor, caretContainer, formatNode) { + var dom = editor.dom, block = dom.getParent(formatNode, Fun.curry(FormatUtils.isTextBlock, editor)); + + if (block && dom.isEmpty(block)) { + // Replace formatNode with caretContainer when removing format from empty block like|
+ formatNode.parentNode.replaceChild(caretContainer, formatNode); + } else { + PaddingBr.removeTrailingBr(Element.fromDom(formatNode)); + if (dom.isEmpty(formatNode)) { + formatNode.parentNode.replaceChild(caretContainer, formatNode); + } else { + dom.insertAfter(caretContainer, formatNode); + } + } + }; + + var appendNode = function (parentNode, node) { + parentNode.appendChild(node); + return node; + }; + + var insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) { + var innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) { + return appendNode(parentNode, formatNode.cloneNode(false)); + }, caretContainer); + + return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP)); + }; + + var setupCaretEvents = function (editor) { + if (!editor._hasCaretEvents) { + bindEvents(editor); + editor._hasCaretEvents = true; + } + }; + + var applyCaretFormat = function (editor, name, vars) { + var rng, caretContainer, textNode, offset, bookmark, container, text; + var dom = editor.dom, selection = editor.selection; + + setupCaretEvents(editor); + + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } + + // Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character + var wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/; + if (text && offset > 0 && offset < text.length && + wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name)); + rng = new RangeUtils(dom).split(rng); + + // Apply the format to the range + editor.formatter.apply(name, vars, rng); + + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== ZWSP) { + caretContainer = createCaretContainer(dom, true); + textNode = caretContainer.firstChild; + + rng.insertNode(caretContainer); + offset = 1; + + editor.formatter.apply(name, vars, caretContainer); + } else { + editor.formatter.apply(name, vars, caretContainer); + } + + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } + }; + + var removeCaretFormat = function (editor, name, vars, similar) { + var dom = editor.dom, selection = editor.selection; + var rng = selection.getRng(true), container, offset, bookmark; + var hasContentAfter, node, formatNode, parents = [], caretContainer; + + setupCaretEvents(editor); + + container = rng.startContainer; + offset = rng.startOffset; + node = container; + + if (container.nodeType === 3) { + if (offset !== container.nodeValue.length) { + hasContentAfter = true; + } + + node = node.parentNode; + } + + while (node) { + if (MatchFormat.matchNode(editor, node, name, vars, similar)) { + formatNode = node; + break; + } + + if (node.nextSibling) { + hasContentAfter = true; + } + + parents.push(node); + node = node.parentNode; + } + + // Node doesn't have the specified format + if (!formatNode) { + return; + } + + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name), true); + rng = new RangeUtils(dom).split(rng); + + editor.formatter.remove(name, vars, rng); + selection.moveToBookmark(bookmark); + } else { + caretContainer = getParentCaretContainer(formatNode); + var newCaretContainer = createCaretContainer(dom, false); + var caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer); + + if (caretContainer) { + insertCaretContainerNode(editor, newCaretContainer, caretContainer); + } else { + insertCaretContainerNode(editor, newCaretContainer, formatNode); + } + + removeCaretContainerNode(dom, selection, caretContainer, false); + selection.setCursorLocation(caretNode, 1); + + if (dom.isEmpty(formatNode)) { + dom.remove(formatNode); + } + } + }; + + var bindEvents = function (editor) { + var dom = editor.dom, selection = editor.selection; + + if (!editor._hasCaretEvents) { + var markCaretContainersBogus, disableCaretContainer; + + editor.on('BeforeGetContent', function (e) { + if (markCaretContainersBogus && e.format !== 'raw') { + markCaretContainersBogus(); + } + }); + + editor.on('mouseup keydown', function (e) { + if (disableCaretContainer) { + disableCaretContainer(e); + } + }); + + // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements + markCaretContainersBogus = function () { + var nodes = [], i; + + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + } + } + }; + + disableCaretContainer = function (e) { + var keyCode = e.keyCode; + + removeCaretContainer(dom, selection, null, false); + + // Remove caret container if it's empty + if (keyCode === 8 && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) { + removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); + } + + // Remove caret container on keydown and it's left/right arrow keys + if (keyCode === 37 || keyCode === 39) { + removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); + } + + unmarkBogusCaretParents(dom, selection); + }; + + // Remove bogus state if they got filled by contents using editor.selection.setContent + editor.on('SetContent', function (e) { + if (e.selection) { + unmarkBogusCaretParents(dom, selection); + } + }); + editor._hasCaretEvents = true; + } + }; + + return { + applyCaretFormat: applyCaretFormat, + removeCaretFormat: removeCaretFormat, + isCaretNode: isCaretNode + }; + } +); +/** + * Hooks.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Internal class for overriding formatting. + * + * @private + * @class tinymce.fmt.Hooks + */ +define( + 'tinymce.core.fmt.Hooks', + [ + "tinymce.core.util.Arr", + "tinymce.core.dom.NodeType", + "tinymce.core.dom.DomQuery" + ], + function (Arr, NodeType, $) { + var postProcessHooks = {}, filter = Arr.filter, each = Arr.each; + + function addPostProcessHook(name, hook) { + var hooks = postProcessHooks[name]; + + if (!hooks) { + postProcessHooks[name] = hooks = []; + } + + postProcessHooks[name].push(hook); + } + + function postProcess(name, editor) { + each(postProcessHooks[name], function (hook) { + hook(editor); + }); + } + + addPostProcessHook("pre", function (editor) { + var rng = editor.selection.getRng(), isPre, blocks; + + function hasPreSibling(pre) { + return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) !== -1; + } + + function joinPre(pre1, pre2) { + $(pre2).remove(); + $(pre1).append('[a
]b
->[a]
b
- function adjustSelectionToVisibleSelection() { - function findSelectionEnd(start, end) { - var walker = new TreeWalker(end); - for (node = walker.prev2(); node; node = walker.prev2()) { - if (node.nodeType == 3 && node.data.length > 0) { - return node; - } - - if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { - return node; - } - } - } - - // Adjust selection so that a end container with a end offset of zero is not included in the selection - // as this isn't visible to the user. - var rng = ed.selection.getRng(); - var start = rng.startContainer; - var end = rng.endContainer; - - if (start != end && rng.endOffset === 0) { - var newEnd = findSelectionEnd(start, end); - var endOffset = newEnd.nodeType == 3 ? newEnd.data.length : newEnd.childNodes.length; - - rng.setEnd(newEnd, endOffset); - } - - return rng; - } - - function applyRngStyle(rng, bookmark, nodeSpecific) { - var newWrappers = [], wrapName, wrapElm, contentEditable = true; - - // Setup wrapper element - wrapName = format.inline || format.block; - wrapElm = dom.create(wrapName); - setElementFormat(wrapElm); - - rangeUtils.walk(rng, function (nodes) { - var currentWrapElm; - - /** - * Process a list of nodes wrap them. - */ - function process(node) { - var nodeName, parentName, hasContentEditableState, lastContentEditable; - - lastContentEditable = contentEditable; - nodeName = node.nodeName.toLowerCase(); - parentName = node.parentNode.nodeName.toLowerCase(); - - // Node has a contentEditable value - if (node.nodeType === 1 && getContentEditable(node)) { - lastContentEditable = contentEditable; - contentEditable = getContentEditable(node) === "true"; - hasContentEditableState = true; // We don't want to wrap the container only it's children - } - - // Stop wrapping on br elements - if (isEq(nodeName, 'br')) { - currentWrapElm = 0; - - // Remove any br elements when we wrap things - if (format.block) { - dom.remove(node); - } - - return; - } - - // If node is wrapper type - if (format.wrapper && matchNode(node, name, vars)) { - currentWrapElm = 0; - return; - } - - // Can we rename the block - // TODO: Break this if up, too complex - if (contentEditable && !hasContentEditableState && format.block && - !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { - node = dom.rename(node, wrapName); - setElementFormat(node); - newWrappers.push(node); - currentWrapElm = 0; - return; - } - - // Handle selector patterns - if (format.selector) { - var found = applyNodeStyle(formatList, node); - - // Continue processing if a selector match wasn't found and a inline element is defined - if (!format.inline || found) { - currentWrapElm = 0; - return; - } - } - - // Is it valid to wrap this item - // TODO: Break this if up, too complex - if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && - !(!nodeSpecific && node.nodeType === 3 && - node.nodeValue.length === 1 && - node.nodeValue.charCodeAt(0) === 65279) && - !isCaretNode(node) && - (!format.inline || !isBlock(node))) { - // Start wrapping - if (!currentWrapElm) { - // Wrap the node - currentWrapElm = dom.clone(wrapElm, FALSE); - node.parentNode.insertBefore(currentWrapElm, node); - newWrappers.push(currentWrapElm); - } - - currentWrapElm.appendChild(node); - } else { - // Start a new wrapper for possible children - currentWrapElm = 0; - - each(grep(node.childNodes), process); - - if (hasContentEditableState) { - contentEditable = lastContentEditable; // Restore last contentEditable state from stack - } - - // End the last wrapper - currentWrapElm = 0; - } - } - - // Process siblings from range - each(nodes, process); - }); - - // Apply formats to links as well to get the color of the underline to change as well - if (format.links === true) { - each(newWrappers, function (node) { - function process(node) { - if (node.nodeName === 'A') { - setElementFormat(node, format); - } - - each(grep(node.childNodes), process); - } - - process(node); - }); - } - - // Cleanup - each(newWrappers, function (node) { - var childCount; - - function getChildCount(node) { - var count = 0; - - each(node.childNodes, function (node) { - if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { - count++; - } - }); - - return count; - } - - function getChildElementNode(root) { - var child = false; - each(root.childNodes, function (node) { - if (isElementNode(node)) { - child = node; - return false; // break loop - } - }); - return child; - } - - function matchNestedWrapper(node, filter) { - do { - if (getChildCount(node) !== 1) { - break; - } - - node = getChildElementNode(node); - if (!node) { - break; - } else if (filter(node)) { - return node; - } - } while (node); - - return null; - } - - function mergeStyles(node) { - var child, clone; - - child = getChildElementNode(node); - - // If child was found and of the same type as the current node - if (child && !isBookmarkNode(child) && matchName(child, format)) { - clone = dom.clone(child, FALSE); - setElementFormat(clone); - - dom.replace(clone, node, TRUE); - dom.remove(child, 1); - } - - return clone || node; - } - - childCount = getChildCount(node); - - // Remove empty nodes but only if there is multiple wrappers and they are not block - // elements so never remove single since that would remove the - // current empty block element where the caret is at - if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { - dom.remove(node, 1); - return; - } - - if (format.inline || format.wrapper) { - // Merges the current node with it's children of similar type to reduce the number of elements - if (!format.exact && childCount === 1) { - node = mergeStyles(node); - } - - // Remove/merge children - each(formatList, function (format) { - // Merge all children of similar type will move styles from child to parent - // this: text - // will become: text - each(dom.select(format.inline, node), function (child) { - if (!isElementNode(child)) { - return; - } - - removeFormat(format, vars, child, format.exact ? child : null); - }); - - clearChildStyles(format, node); - }); - - // Remove format if direct parent already has the same format - if (matchNode(node.parentNode, name, vars)) { - if (removeFormat(format, vars, node)) { - node = 0; - } - } - - // Remove format if any ancestor already has the same format - if (format.merge_with_parents) { - dom.getParent(node.parentNode, function (parent) { - if (matchNode(parent, name, vars)) { - if (removeFormat(format, vars, node)) { - node = 0; - } - return TRUE; - } - }); - } - - // fontSize defines the line height for the whole branch of nested style wrappers, - // therefore it should be set on the outermost wrapper - if (node && !isBlock(node) && !getStyle(node, 'fontSize')) { - var styleNode = matchNestedWrapper(node, hasStyle('fontSize')); - if (styleNode) { - apply('fontsize', { value: getStyle(styleNode, 'fontSize') }, node); - } - } - - // Merge next and previous siblings if they are similar texttext becomes texttext - if (node && format.merge_siblings !== false) { - node = mergeSiblings(getNonWhiteSpaceSibling(node), node); - node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); - } - } - }); - } - - if (getContentEditable(selection.getNode()) === "false") { - node = selection.getNode(); - for (var i = 0, l = formatList.length; i < l; i++) { - if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) { - setElementFormat(node, formatList[i]); - return; - } - } - - return; - } - - if (format) { - if (node) { - if (node.nodeType) { - if (!applyNodeStyle(formatList, node)) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - applyRngStyle(expandRng(rng, formatList), null, true); - } - } else { - applyRngStyle(node, null, true); - } - } else { - if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { - // Obtain selection node before selection is unselected by applyRngStyle() - var curSelNode = ed.selection.getNode(); - - // If the formats have a default block and we can't find a parent block then - // start wrapping it with a DIV this is for forced_root_blocks: false - // It's kind of a hack but people should be using the default block type P since all desktop editors work that way - if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { - apply(formatList[0].defaultBlock); - } - - // Apply formatting to selection - ed.selection.setRng(adjustSelectionToVisibleSelection()); - bookmark = selection.getBookmark(); - applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); - - if (format.styles) { - // Colored nodes should be underlined so that the color of the underline matches the text color. - if (format.styles.color || format.styles.textDecoration) { - walk(curSelNode, processUnderlineAndColor, 'childNodes'); - processUnderlineAndColor(curSelNode); - } - - // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882) - if (format.styles.backgroundColor) { - processChildElements(curSelNode, - hasStyle('fontSize'), - applyStyle('backgroundColor', replaceVars(format.styles.backgroundColor, vars)) - ); - } - } - - selection.moveToBookmark(bookmark); - moveStart(selection.getRng(TRUE)); - ed.nodeChanged(); - } else { - performCaretAction('apply', name, vars); - } - } - - Hooks.postProcess(name, ed); - } - } - - /** - * Removes the specified format from the current selection or specified node. - * - * @method remove - * @param {String} name Name of format to remove. - * @param {Object} vars Optional list of variables to replace within format before removing it. - * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. - */ - function remove(name, vars, node, similar) { - var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; - - // Merges the styles for each node - function process(node) { - var children, i, l, lastContentEditable, hasContentEditableState; - - // Node has a contentEditable value - if (node.nodeType === 1 && getContentEditable(node)) { - lastContentEditable = contentEditable; - contentEditable = getContentEditable(node) === "true"; - hasContentEditableState = true; // We don't want to wrap the container only it's children - } - - // Grab the children first since the nodelist might be changed - children = grep(node.childNodes); - - // Process current node - if (contentEditable && !hasContentEditableState) { - for (i = 0, l = formatList.length; i < l; i++) { - if (removeFormat(formatList[i], vars, node, node)) { - break; - } - } - } - - // Process the children - if (format.deep) { - if (children.length) { - for (i = 0, l = children.length; i < l; i++) { - process(children[i]); - } - - if (hasContentEditableState) { - contentEditable = lastContentEditable; // Restore last contentEditable state from stack - } - } - } - } - - function findFormatRoot(container) { - var formatRoot; - - // Find format root - each(getParents(container.parentNode).reverse(), function (parent) { - var format; - - // Find format root element - if (!formatRoot && parent.id != '_start' && parent.id != '_end') { - // Is the node matching the format we are looking for - format = matchNode(parent, name, vars, similar); - if (format && format.split !== false) { - formatRoot = parent; - } - } - }); - - return formatRoot; - } - - function wrapAndSplit(formatRoot, container, target, split) { - var parent, clone, lastClone, firstClone, i, formatRootParent; - - // Format root found then clone formats and split it - 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++) { - if (removeFormat(formatList[i], vars, clone, clone)) { - clone = 0; - break; - } - } - - // Build wrapper node - if (clone) { - if (lastClone) { - clone.appendChild(lastClone); - } - - if (!firstClone) { - firstClone = clone; - } - - lastClone = clone; - } - } - - // Never split block elements if the format is mixed - if (split && (!format.mixed || !isBlock(formatRoot))) { - container = dom.split(formatRoot, container); - } - - // Wrap container in cloned formats - if (lastClone) { - target.parentNode.insertBefore(lastClone, target); - firstClone.appendChild(target); - } - } - - return container; - } - - function splitToFormatRoot(container) { - return wrapAndSplit(findFormatRoot(container), container, container, true); - } - - function unwrap(start) { - var node = dom.get(start ? '_start' : '_end'), - out = node[start ? 'firstChild' : 'lastChild']; - - // If the end is placed within the start the result will be removed - // So this checks if the out node is a bookmark node if it is it - // checks for another more suitable node - if (isBookmarkNode(out)) { - out = out[start ? 'firstChild' : 'lastChild']; - } - - // Since dom.remove removes empty text nodes then we need to try to find a better node - if (out.nodeType == 3 && out.data.length === 0) { - out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling; - } - - dom.remove(node, true); - - return out; - } - - function removeRngStyle(rng) { - var startContainer, endContainer; - var commonAncestorContainer = rng.commonAncestorContainer; - - rng = expandRng(rng, formatList, TRUE); - - if (format.split) { - startContainer = getContainer(rng, TRUE); - endContainer = getContainer(rng); - - if (startContainer != endContainer) { - // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN - // so let's see if we can use the first child instead - // This will happen if you triple click a table cell and use remove formatting - if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { - if (startContainer.nodeName == "TR") { - startContainer = startContainer.firstChild.firstChild || startContainer; - } else { - startContainer = startContainer.firstChild || startContainer; - } - } - - // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 - if (commonAncestorContainer && - /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && - isTableCell(endContainer) && endContainer.firstChild) { - endContainer = endContainer.firstChild || endContainer; - } - - if (dom.isChildOf(startContainer, endContainer) && !isBlock(endContainer) && - !isTableCell(startContainer) && !isTableCell(endContainer)) { - startContainer = wrap(startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); - splitToFormatRoot(startContainer); - startContainer = unwrap(TRUE); - return; - } - - // Wrap start/end nodes in span element since these might be cloned/moved - startContainer = wrap(startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); - endContainer = wrap(endContainer, 'span', { id: '_end', 'data-mce-type': 'bookmark' }); - - // Split start/end - splitToFormatRoot(startContainer); - splitToFormatRoot(endContainer); - - // Unwrap start/end to get real elements again - startContainer = unwrap(TRUE); - endContainer = unwrap(); - } else { - startContainer = endContainer = splitToFormatRoot(startContainer); - } - - // Update range positions since they might have changed after the split operations - rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; - rng.startOffset = nodeIndex(startContainer); - rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; - rng.endOffset = nodeIndex(endContainer) + 1; - } - - // Remove items between start/end - rangeUtils.walk(rng, function (nodes) { - each(nodes, function (node) { - process(node); - - // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. - if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && - node.parentNode && getTextDecoration(node.parentNode) === 'underline') { - removeFormat({ - 'deep': false, - 'exact': true, - 'inline': 'span', - 'styles': { - 'textDecoration': 'underline' - } - }, null, node); - } - }); - }); - } - - // Handle node - if (node) { - if (node.nodeType) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); - } else { - removeRngStyle(node); - } - - return; - } - - if (getContentEditable(selection.getNode()) === "false") { - node = selection.getNode(); - for (var i = 0, l = formatList.length; i < l; i++) { - if (formatList[i].ceFalseOverride) { - if (removeFormat(formatList[i], vars, node, node)) { - break; - } - } - } - - return; - } - - if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { - bookmark = selection.getBookmark(); - removeRngStyle(selection.getRng(TRUE)); - selection.moveToBookmark(bookmark); - - // Check if start element still has formatting then we are at: "text|text" - // and need to move the start into the next text node - if (format.inline && match(name, vars, selection.getStart())) { - moveStart(selection.getRng(true)); - } - - ed.nodeChanged(); - } else { - performCaretAction('remove', name, vars, similar); - } - } - - /** - * Toggles the specified format on/off. - * - * @method toggle - * @param {String} name Name of format to apply/remove. - * @param {Object} vars Optional list of variables to replace within format before applying/removing it. - * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. - */ - function toggle(name, vars, node) { - var fmt = get(name); - - if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { - remove(name, vars, node); - } else { - apply(name, vars, node); - } - } - - /** - * Return true/false if the specified node has the specified format. - * - * @method matchNode - * @param {Node} node Node to check the format on. - * @param {String} name Format name to check. - * @param {Object} vars Optional list of variables to replace before checking it. - * @param {Boolean} similar Match format that has similar properties. - * @return {Object} Returns the format object it matches or undefined if it doesn't match. - */ - function matchNode(node, name, vars, similar) { - var formatList = get(name), format, i, classes; - - function matchItems(node, format, itemName) { - var key, value, items = format[itemName], i; - - // Custom match - if (format.onmatch) { - return format.onmatch(node, format, itemName); - } - - // Check all items - if (items) { - // Non indexed object - if (items.length === undef) { - for (key in items) { - if (items.hasOwnProperty(key)) { - if (itemName === 'attributes') { - value = dom.getAttrib(node, key); - } else { - value = getStyle(node, key); - } - - if (similar && !value && !format.exact) { - return; - } - - if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { - return; - } - } - } - } else { - // Only one match needed for indexed arrays - for (i = 0; i < items.length; i++) { - if (itemName === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { - return format; - } - } - } - } - - return format; - } - - if (formatList && node) { - // Check each format in list - for (i = 0; i < formatList.length; i++) { - format = formatList[i]; - - // Name name, attributes, styles and classes - if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { - // Match classes - if ((classes = format.classes)) { - for (i = 0; i < classes.length; i++) { - if (!dom.hasClass(node, classes[i])) { - return; - } - } - } - - return format; - } - } - } - } - - /** - * Matches the current selection or specified node against the specified format name. - * - * @method match - * @param {String} name Name of format to match. - * @param {Object} vars Optional list of variables to replace before checking it. - * @param {Node} node Optional node to check. - * @return {boolean} true/false if the specified selection/node matches the format. - */ - function match(name, vars, node) { - var startNode; - - function matchParents(node) { - var root = dom.getRoot(); - - if (node === root) { - return false; - } - - // Find first node with similar format settings - node = dom.getParent(node, function (node) { - if (matchesUnInheritedFormatSelector(node, name)) { - return true; - } - - return node.parentNode === root || !!matchNode(node, name, vars, true); - }); - - // Do an exact check on the similar format element - return matchNode(node, name, vars); - } - - // Check specified node - if (node) { - return matchParents(node); - } - - // Check selected node - node = selection.getNode(); - if (matchParents(node)) { - return TRUE; - } - - // Check start node if it's different - startNode = selection.getStart(); - if (startNode != node) { - if (matchParents(startNode)) { - return TRUE; - } - } - - return FALSE; - } - - /** - * Matches the current selection against the array of formats and returns a new array with matching formats. - * - * @method matchAll - * @param {Array} names Name of format to match. - * @param {Object} vars Optional list of variables to replace before checking it. - * @return {Array} Array with matched formats. - */ - function matchAll(names, vars) { - var startElement, matchedFormatNames = [], checkedMap = {}; - - // Check start of selection for formats - startElement = selection.getStart(); - dom.getParent(startElement, function (node) { - var i, name; - - for (i = 0; i < names.length; i++) { - name = names[i]; - - if (!checkedMap[name] && matchNode(node, name, vars)) { - checkedMap[name] = true; - matchedFormatNames.push(name); - } - } - }, dom.getRoot()); - - return matchedFormatNames; - } - - /** - * Returns true/false if the specified format can be applied to the current selection or not. It - * will currently only check the state for selector formats, it returns true on all other format types. - * - * @method canApply - * @param {String} name Name of format to check. - * @return {boolean} true/false if the specified format can be applied to the current selection/node. - */ - function canApply(name) { - var formatList = get(name), startNode, parents, i, x, selector; - - if (formatList) { - startNode = selection.getStart(); - parents = getParents(startNode); - - for (x = formatList.length - 1; x >= 0; x--) { - selector = formatList[x].selector; - - // Format is not selector based then always return TRUE - // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line - if (!selector || formatList[x].defaultBlock) { - return TRUE; - } - - for (i = parents.length - 1; i >= 0; i--) { - if (dom.is(parents[i], selector)) { - return TRUE; - } - } - } - } - - return FALSE; - } - - /** - * Executes the specified callback when the current selection matches the formats or not. - * - * @method formatChanged - * @param {String} formats Comma separated list of formats to check for. - * @param {function} callback Callback with state and args when the format is changed/toggled on/off. - * @param {Boolean} similar True/false state if the match should handle similar or exact formats. - */ - function formatChanged(formats, callback, similar) { - var currentFormats; - - // Setup format node change logic - if (!formatChangeData) { - formatChangeData = {}; - currentFormats = {}; - - ed.on('NodeChange', function (e) { - var parents = getParents(e.element), matchedFormats = {}; - - // Ignore bogus nodes like the tag created by moveStart() - parents = Tools.grep(parents, function (node) { - return node.nodeType == 1 && !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]) { - // Execute callbacks - each(callbacks, function (callback) { - callback(true, { node: node, format: format, parents: parents }); - }); - - currentFormats[format] = callbacks; - } - - matchedFormats[format] = callbacks; - return false; - } - - if (matchesUnInheritedFormatSelector(node, format)) { - return false; - } - }); - }); - - // Check if current formats still match - each(currentFormats, function (callbacks, format) { - if (!matchedFormats[format]) { - delete currentFormats[format]; - - each(callbacks, function (callback) { - callback(false, { node: e.element, format: format, parents: parents }); - }); - } - }); - }); - } - - // Add format listeners - each(formats.split(','), function (format) { - if (!formatChangeData[format]) { - formatChangeData[format] = []; - formatChangeData[format].similar = similar; - } - - formatChangeData[format].push(callback); - }); - - return this; - } - - /** - * Returns a preview css text for the specified format. - * - * @method getCssText - * @param {String/Object} format Format to generate preview css text for. - * @return {String} Css text for the specified format. - * @example - * var cssText1 = editor.formatter.getCssText('bold'); - * var cssText2 = editor.formatter.getCssText({inline: 'b'}); - */ - function getCssText(format) { - return Preview.getCssText(ed, format); - } - - // Expose to public - extend(this, { - get: get, - register: register, - unregister: unregister, - apply: apply, - remove: remove, - toggle: toggle, - match: match, - matchAll: matchAll, - matchNode: matchNode, - canApply: canApply, - formatChanged: formatChanged, - getCssText: getCssText - }); - - // Initialize - defaultFormats(); - addKeyboardShortcuts(); - ed.on('BeforeGetContent', function (e) { - if (markCaretContainersBogus && e.format != 'raw') { - markCaretContainersBogus(); - } - }); - ed.on('mouseup keydown', function (e) { - if (disableCaretContainer) { - disableCaretContainer(e); - } - }); - - // Private functions - - /** - * Checks if the specified nodes name matches the format inline/block or selector. - * - * @private - * @param {Node} node Node to match against the specified format. - * @param {Object} format Format object o match with. - * @return {boolean} true/false if the format matches. - */ - function matchName(node, format) { - // Check for inline match - if (isEq(node, format.inline)) { - return TRUE; - } - - // Check for block match - if (isEq(node, format.block)) { - return TRUE; - } - - // Check for selector match - if (format.selector) { - return node.nodeType == 1 && dom.is(node, format.selector); - } - } - - /** - * Compares two string/nodes regardless of their case. - * - * @private - * @param {String/Node} str1 Node or string to compare. - * @param {String/Node} str2 Node or string to compare. - * @return {boolean} True/false if they match. - */ - function isEq(str1, str2) { - str1 = str1 || ''; - str2 = str2 || ''; - - str1 = '' + (str1.nodeName || str1); - str2 = '' + (str2.nodeName || str2); - - return str1.toLowerCase() == str2.toLowerCase(); - } - - function processChildElements(node, filter, process) { - each(node.childNodes, function (node) { - if (isElementNode(node)) { - if (filter(node)) { - process(node); - } - if (node.hasChildNodes()) { - processChildElements(node, filter, process); - } - } - }); - } - - function isElementNode(node) { - return node && node.nodeType === 1 && !isBookmarkNode(node) && !isCaretNode(node) && !NodeType.isBogus(node); - } - - function hasStyle(name) { - return Fun.curry(function (name, node) { - return !!(node && getStyle(node, name)); - }, name); - } - - function applyStyle(name, value) { - return Fun.curry(function (name, value, node) { - dom.setStyle(node, name, value); - }, name, value); - } - - /** - * Returns the style by name on the specified node. This method modifies the style - * contents to make it more easy to match. This will resolve a few browser issues. - * - * @private - * @param {Node} node to get style from. - * @param {String} name Style name to get. - * @return {String} Style item value. - */ - function getStyle(node, name) { - return normalizeStyleValue(dom.getStyle(node, name), name); - } - - /** - * Normalize style value by name. This method modifies the style contents - * to make it more easy to match. This will resolve a few browser issues. - * - * @private - * @param {String} value Value to get style from. - * @param {String} name Style name to get. - * @return {String} Style item value. - */ - function normalizeStyleValue(value, name) { - // Force the format to hex - if (name == 'color' || name == 'backgroundColor') { - value = dom.toHex(value); - } - - // Opera will return bold as 700 - if (name == 'fontWeight' && value == 700) { - value = 'bold'; - } - - // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" - if (name == 'fontFamily') { - value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); - } - - return '' + value; - } - - /** - * Replaces variables in the value. The variable format is %var. - * - * @private - * @param {String} value Value to replace variables in. - * @param {Object} vars Name/value array with variables to replace. - * @return {String} New value with replaced variables. - */ - function replaceVars(value, vars) { - if (typeof value != "string") { - value = value(vars); - } else if (vars) { - value = value.replace(/%(\w+)/g, function (str, name) { - return vars[name] || str; - }); - } - - return value; - } - - function isWhiteSpaceNode(node) { - return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); - } - - function wrap(node, name, attrs) { - var wrapper = dom.create(name, attrs); - - node.parentNode.insertBefore(wrapper, node); - wrapper.appendChild(node); - - return wrapper; - } - - /** - * Expands the specified range like object to depending on format. - * - * For example on block formats it will move the start/end position - * to the beginning of the current block. - * - * @private - * @param {Object} rng Range like object. - * @param {Array} format Array with formats to expand by. - * @param {Boolean} remove - * @return {Object} Expanded range like object. - */ - function expandRng(rng, format, remove) { - var lastIdx, leaf, endPoint, - startContainer = rng.startContainer, - startOffset = rng.startOffset, - endContainer = rng.endContainer, - endOffset = rng.endOffset; - - // This function walks up the tree if there is no siblings before/after the node - function findParentContainer(start) { - var container, parent, sibling, siblingName, root; - - container = parent = start ? startContainer : endContainer; - siblingName = start ? 'previousSibling' : 'nextSibling'; - root = dom.getRoot(); - - function isBogusBr(node) { - return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; - } - - // If it's a text node and the offset is inside the text - if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { - if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { - return container; - } - } - - /*eslint no-constant-condition:0 */ - while (true) { - // Stop expanding on block elements - if (!format[0].block_expand && isBlock(parent)) { - return parent; - } - - // Walk left/right - for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { - if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { - return parent; - } - } - - // Check if we can move up are we at root level or body level - if (parent == root || parent.parentNode == root) { - container = parent; - break; - } - - parent = parent.parentNode; - } - - return container; - } - - // This function walks down the tree to find the leaf at the selection. - // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. - function findLeaf(node, offset) { - if (offset === undef) { - offset = node.nodeType === 3 ? node.length : node.childNodes.length; - } - - while (node && node.hasChildNodes()) { - node = node.childNodes[offset]; - if (node) { - offset = node.nodeType === 3 ? node.length : node.childNodes.length; - } - } - return { node: node, offset: offset }; - } - - // If index based start position then resolve it - if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { - lastIdx = startContainer.childNodes.length - 1; - startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; - - if (startContainer.nodeType == 3) { - startOffset = 0; - } - } - - // If index based end position then resolve it - if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { - lastIdx = endContainer.childNodes.length - 1; - endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; - - if (endContainer.nodeType == 3) { - endOffset = endContainer.nodeValue.length; - } - } - - // Expands the node to the closes contentEditable false element if it exists - function findParentContentEditable(node) { - var parent = node; - - while (parent) { - if (parent.nodeType === 1 && getContentEditable(parent)) { - return getContentEditable(parent) === "false" ? parent : node; - } - - parent = parent.parentNode; - } - - return node; - } - - function findWordEndPoint(container, offset, start) { - var walker, node, pos, lastTextNode; - - function findSpace(node, offset) { - var pos, pos2, str = node.nodeValue; - - if (typeof offset == "undefined") { - offset = start ? str.length : 0; - } - - if (start) { - pos = str.lastIndexOf(' ', offset); - pos2 = str.lastIndexOf('\u00a0', offset); - pos = pos > pos2 ? pos : pos2; - - // Include the space on remove to avoid tag soup - if (pos !== -1 && !remove) { - pos++; - } - } else { - pos = str.indexOf(' ', offset); - pos2 = str.indexOf('\u00a0', offset); - pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; - } - - return pos; - } - - if (container.nodeType === 3) { - pos = findSpace(container, offset); - - if (pos !== -1) { - return { container: container, offset: pos }; - } - - lastTextNode = container; - } - - // Walk the nodes inside the block - walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); - while ((node = walker[start ? 'prev' : 'next']())) { - if (node.nodeType === 3) { - lastTextNode = node; - pos = findSpace(node); - - if (pos !== -1) { - return { container: node, offset: pos }; - } - } else if (isBlock(node)) { - break; - } - } - - if (lastTextNode) { - if (start) { - offset = 0; - } else { - offset = lastTextNode.length; - } - - return { container: lastTextNode, offset: offset }; - } - } - - function findSelectorEndPoint(container, siblingName) { - var parents, i, y, curFormat; - - if (container.nodeType == 3 && container.nodeValue.length === 0 && container[siblingName]) { - container = container[siblingName]; - } - - parents = getParents(container); - for (i = 0; i < parents.length; i++) { - for (y = 0; y < format.length; y++) { - curFormat = format[y]; - - // If collapsed state is set then skip formats that doesn't match that - if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { - continue; - } - - if (dom.is(parents[i], curFormat.selector)) { - return parents[i]; - } - } - } - - return container; - } - - function findBlockEndPoint(container, siblingName) { - var node, root = dom.getRoot(); - - // Expand to block of similar type - if (!format[0].wrapper) { - node = dom.getParent(container, format[0].block, root); - } - - // Expand to first wrappable block element or any block element - if (!node) { - node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function (node) { - // Fixes #6183 where it would expand to editable parent element in inline mode - return node != root && isTextBlock(node); - }); - } - - // Exclude inner lists from wrapping - if (node && format[0].wrapper) { - node = getParents(node, 'ul,ol').reverse()[0] || node; - } - - // Didn't find a block element look for first/last wrappable element - if (!node) { - node = container; - - while (node[siblingName] && !isBlock(node[siblingName])) { - node = node[siblingName]; - - // Break on BR but include it will be removed later on - // we can't remove it now since we need to check if it can be wrapped - if (isEq(node, 'br')) { - break; - } - } - } - - return node || container; - } - - // Expand to closest contentEditable element - startContainer = findParentContentEditable(startContainer); - endContainer = findParentContentEditable(endContainer); - - // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { - startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; - startContainer = startContainer.nextSibling || startContainer; - - if (startContainer.nodeType == 3) { - startOffset = 0; - } - } - - if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { - endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; - endContainer = endContainer.previousSibling || endContainer; - - if (endContainer.nodeType == 3) { - endOffset = endContainer.length; - } - } - - if (format[0].inline) { - if (rng.collapsed) { - // Expand left to closest word boundary - endPoint = findWordEndPoint(startContainer, startOffset, true); - if (endPoint) { - startContainer = endPoint.container; - startOffset = endPoint.offset; - } - - // Expand right to closest word boundary - endPoint = findWordEndPoint(endContainer, endOffset); - if (endPoint) { - endContainer = endPoint.container; - endOffset = endPoint.offset; - } - } - - // Avoid applying formatting to a trailing space. - leaf = findLeaf(endContainer, endOffset); - if (leaf.node) { - while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { - leaf = findLeaf(leaf.node.previousSibling); - } - - if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && - leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { - - if (leaf.offset > 1) { - endContainer = leaf.node; - endContainer.splitText(leaf.offset - 1); - } - } - } - } - - // Move start/end point up the tree if the leaves are sharp and if we are in different containers - // Example * becomes !: !*texttext*
! - // This will reduce the number of wrapper elements that needs to be created - // Move start point up the tree - if (format[0].inline || format[0].block_expand) { - if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { - startContainer = findParentContainer(true); - } - - if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { - endContainer = findParentContainer(); - } - } - - // Expand start/end container to matching selector - if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { - // Find new startContainer/endContainer if there is better one - startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); - endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); - } - - // Expand start/end container to matching block element or text node - if (format[0].block || format[0].selector) { - // Find new startContainer/endContainer if there is better one - startContainer = findBlockEndPoint(startContainer, 'previousSibling'); - endContainer = findBlockEndPoint(endContainer, 'nextSibling'); - - // Non block element then try to expand up the leaf - if (format[0].block) { - if (!isBlock(startContainer)) { - startContainer = findParentContainer(true); - } - - if (!isBlock(endContainer)) { - endContainer = findParentContainer(); - } - } - } - - // Setup index for startContainer - if (startContainer.nodeType == 1) { - startOffset = nodeIndex(startContainer); - startContainer = startContainer.parentNode; - } - - // Setup index for endContainer - if (endContainer.nodeType == 1) { - endOffset = nodeIndex(endContainer) + 1; - endContainer = endContainer.parentNode; - } - - // Return new range like object - return { - startContainer: startContainer, - startOffset: startOffset, - endContainer: endContainer, - endOffset: endOffset - }; - } - - function isColorFormatAndAnchor(node, format) { - return format.links && node.tagName == 'A'; - } - - /** - * Removes the specified format for the specified node. It will also remove the node if it doesn't have - * any attributes if the format specifies it to do so. - * - * @private - * @param {Object} format Format object with items to remove from node. - * @param {Object} vars Name/value object with variables to apply to format. - * @param {Node} node Node to remove the format styles on. - * @param {Node} compareNode Optional compare node, if specified the styles will be compared to that node. - * @return {Boolean} True/false if the node was removed or not. - */ - function removeFormat(format, vars, node, compareNode) { - var i, attrs, stylesModified; - - // Check if node matches format - if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { - return FALSE; - } - - // Should we compare with format attribs and styles - if (format.remove != 'all') { - // Remove styles - each(format.styles, function (value, name) { - value = normalizeStyleValue(replaceVars(value, vars), name); - - // Indexed array - if (typeof name === 'number') { - name = value; - compareNode = 0; - } - - if (format.remove_similar || (!compareNode || isEq(getStyle(compareNode, name), value))) { - dom.setStyle(node, name, ''); - } - - stylesModified = 1; - }); - - // Remove style attribute if it's empty - if (stylesModified && dom.getAttrib(node, 'style') === '') { - node.removeAttribute('style'); - node.removeAttribute('data-mce-style'); - } - - // Remove attributes - each(format.attributes, function (value, name) { - var valueOut; - - value = replaceVars(value, vars); - - // Indexed array - if (typeof name === 'number') { - name = value; - compareNode = 0; - } - - if (!compareNode || isEq(dom.getAttrib(compareNode, name), value)) { - // Keep internal classes - if (name == 'class') { - value = dom.getAttrib(node, name); - if (value) { - // Build new class value where everything is removed except the internal prefixed classes - valueOut = ''; - each(value.split(/\s+/), function (cls) { - if (/mce\-\w+/.test(cls)) { - valueOut += (valueOut ? ' ' : '') + cls; - } - }); - - // We got some internal classes left - if (valueOut) { - dom.setAttrib(node, name, valueOut); - return; - } - } - } - - // IE6 has a bug where the attribute doesn't get removed correctly - if (name == "class") { - node.removeAttribute('className'); - } - - // Remove mce prefixed attributes - if (MCE_ATTR_RE.test(name)) { - node.removeAttribute('data-mce-' + name); - } - - node.removeAttribute(name); - } - }); - - // Remove classes - each(format.classes, function (value) { - value = replaceVars(value, vars); - - if (!compareNode || dom.hasClass(compareNode, value)) { - dom.removeClass(node, value); - } - }); - - // Check for non internal attributes - attrs = dom.getAttribs(node); - for (i = 0; i < attrs.length; i++) { - var attrName = attrs[i].nodeName; - if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) { - return FALSE; - } - } - } - - // Remove the inline child if it's empty for example or - if (format.remove != 'none') { - removeNode(node, format); - return TRUE; - } - } - - /** - * Removes the node and wrap it's children in paragraphs before doing so or - * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. - * - * If the div in the node below gets removed: - * text|
- formatNode.parentNode.replaceChild(caretContainer, formatNode); - } else { - // Insert caret container after the formatted node - dom.insertAfter(caretContainer, formatNode); - } - - // Move selection to text node - selection.setCursorLocation(node, 1); - - // If the formatNode is empty, we can remove it safely. - if (dom.isEmpty(formatNode)) { - dom.remove(formatNode); - } - } - } - - // Checks if the parent caret container node isn't empty if that is the case it - // will remove the bogus state on all children that isn't empty - function unmarkBogusCaretParents() { - var caretContainer; - - caretContainer = getParentCaretContainer(selection.getStart()); - if (caretContainer && !dom.isEmpty(caretContainer)) { - walk(caretContainer, function (node) { - if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { - dom.setAttrib(node, 'data-mce-bogus', null); - } - }, 'childNodes'); - } - } - - // Only bind the caret events once - if (!ed._hasCaretEvents) { - // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements - markCaretContainersBogus = function () { - var nodes = [], i; - - if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { - // Mark children - i = nodes.length; - while (i--) { - dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); - } - } - }; - - disableCaretContainer = function (e) { - var keyCode = e.keyCode; - - removeCaretContainer(); - - // Remove caret container if it's empty - if (keyCode == 8 && selection.isCollapsed() && selection.getStart().innerHTML == INVISIBLE_CHAR) { - removeCaretContainer(getParentCaretContainer(selection.getStart())); - } - - // Remove caret container on keydown and it's left/right arrow keys - if (keyCode == 37 || keyCode == 39) { - removeCaretContainer(getParentCaretContainer(selection.getStart())); - } - - unmarkBogusCaretParents(); - }; - - // Remove bogus state if they got filled by contents using editor.selection.setContent - ed.on('SetContent', function (e) { - if (e.selection) { - unmarkBogusCaretParents(); - } - }); - ed._hasCaretEvents = true; - } - - // Do apply or remove caret format - if (type == "apply") { - applyCaretFormat(); - } else { - removeCaretFormat(); - } - } - - /** - * Moves the start to the first suitable text node. - */ - function moveStart(rng) { - var container = rng.startContainer, - offset = rng.startOffset, - walker, node, nodes; - - if (rng.startContainer == rng.endContainer) { - if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { - return; - } - } - - // Convert text node into index if possible - if (container.nodeType == 3 && offset >= container.nodeValue.length) { - // Get the parent container location and walk from there - offset = nodeIndex(container); - container = container.parentNode; - } - - // Move startContainer/startOffset in to a suitable node - if (container.nodeType == 1) { - nodes = container.childNodes; - if (offset < nodes.length) { - container = nodes[offset]; - walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); - } else { - container = nodes[nodes.length - 1]; - walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); - walker.next(true); - } - - for (node = walker.current(); node; node = walker.next()) { - if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { - rng.setStart(node, 0); - selection.setRng(rng); - - return; - } - } - } - } - }; - } -); - -/** - * Diff.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. - * - * @class tinymce.undo.Diff - * @private - */ -define( - 'tinymce.core.undo.Diff', - [ - ], - function () { - var KEEP = 0, INSERT = 1, DELETE = 2; - - var diff = function (left, right) { - var size = left.length + right.length + 2; - var vDown = new Array(size); - var vUp = new Array(size); - - var snake = function (start, end, diag) { - return { - start: start, - end: end, - diag: diag - }; - }; - - var buildScript = function (start1, end1, start2, end2, script) { - var middle = getMiddleSnake(start1, end1, start2, end2); - - if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || - middle.end === start1 && middle.diag === start1 - start2) { - var i = start1; - var j = start2; - while (i < end1 || j < end2) { - if (i < end1 && j < end2 && left[i] === right[j]) { - script.push([KEEP, left[i]]); - ++i; - ++j; - } else { - if (end1 - start1 > end2 - start2) { - script.push([DELETE, left[i]]); - ++i; - } else { - script.push([INSERT, right[j]]); - ++j; - } - } - } - } else { - buildScript(start1, middle.start, start2, middle.start - middle.diag, script); - for (var i2 = middle.start; i2 < middle.end; ++i2) { - script.push([KEEP, left[i2]]); - } - buildScript(middle.end, end1, middle.end - middle.diag, end2, script); - } - }; - - var buildSnake = function (start, diag, end1, end2) { - var end = start; - while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { - ++end; - } - return snake(start, end, diag); - }; - - var getMiddleSnake = function (start1, end1, start2, end2) { - // Myers Algorithm - // Initialisations - var m = end1 - start1; - var n = end2 - start2; - if (m === 0 || n === 0) { - return null; - } - - var delta = m - n; - var sum = n + m; - var offset = (sum % 2 === 0 ? sum : sum + 1) / 2; - vDown[1 + offset] = start1; - vUp[1 + offset] = end1 + 1; - - for (var d = 0; d <= offset; ++d) { - // Down - for (var k = -d; k <= d; k += 2) { - // First step - - var i = k + offset; - if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) { - vDown[i] = vDown[i + 1]; - } else { - vDown[i] = vDown[i - 1] + 1; - } - - var x = vDown[i]; - var y = x - start1 + start2 - k; - - while (x < end1 && y < end2 && left[x] === right[y]) { - vDown[i] = ++x; - ++y; - } - // Second step - if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { - if (vUp[i - delta] <= vDown[i]) { - return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); - } - } - } - - // Up - for (k = delta - d; k <= delta + d; k += 2) { - // First step - i = k + offset - delta; - if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) { - vUp[i] = vUp[i + 1] - 1; - } else { - vUp[i] = vUp[i - 1]; - } - - x = vUp[i] - 1; - y = x - start1 + start2 - k; - while (x >= start1 && y >= start2 && left[x] === right[y]) { - vUp[i] = x--; - y--; - } - // Second step - if (delta % 2 === 0 && -d <= k && k <= d) { - if (vUp[i] <= vDown[i + delta]) { - return buildSnake(vUp[i], k + start1 - start2, end1, end2); - } - } - } - } - }; - - var script = []; - buildScript(0, left.length, 0, right.length, script); - return script; - }; - - return { - KEEP: KEEP, - DELETE: DELETE, - INSERT: INSERT, - diff: diff - }; - } -); -/** - * Fragments.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This module reads and applies html fragments from/to dom nodes. - * - * @class tinymce.undo.Fragments - * @private - */ -define( - 'tinymce.core.undo.Fragments', - [ - "tinymce.core.util.Arr", - "tinymce.core.html.Entities", - "tinymce.core.undo.Diff" - ], - function (Arr, Entities, Diff) { - var getOuterHtml = function (elm) { - if (elm.nodeType === 1) { - return elm.outerHTML; - } else if (elm.nodeType === 3) { - return Entities.encodeRaw(elm.data, false); - } else if (elm.nodeType === 8) { - return ''; - } - - return ''; - }; - - var createFragment = function (html) { - var frag, node, container; - - container = document.createElement("div"); - frag = document.createDocumentFragment(); - - if (html) { - container.innerHTML = html; - } - - while ((node = container.firstChild)) { - frag.appendChild(node); - } - - return frag; - }; - - var insertAt = function (elm, html, index) { - var fragment = createFragment(html); - if (elm.hasChildNodes() && index < elm.childNodes.length) { - var target = elm.childNodes[index]; - target.parentNode.insertBefore(fragment, target); - } else { - elm.appendChild(fragment); - } - }; - - var removeAt = function (elm, index) { - if (elm.hasChildNodes() && index < elm.childNodes.length) { - var target = elm.childNodes[index]; - target.parentNode.removeChild(target); - } - }; - - var applyDiff = function (diff, elm) { - var index = 0; - Arr.each(diff, function (action) { - if (action[0] === Diff.KEEP) { - index++; - } else if (action[0] === Diff.INSERT) { - insertAt(elm, action[1], index); - index++; - } else if (action[0] === Diff.DELETE) { - removeAt(elm, index); - } - }); - }; - - var read = function (elm) { - return Arr.filter(Arr.map(elm.childNodes, getOuterHtml), function (item) { - return item.length > 0; - }); - }; - - var write = function (fragments, elm) { - var currentFragments = Arr.map(elm.childNodes, getOuterHtml); - applyDiff(Diff.diff(currentFragments, fragments), elm); - return elm; - }; - - return { - read: read, - write: write - }; - } -); -/** - * Levels.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This module handles getting/setting undo levels to/from editor instances. - * - * @class tinymce.undo.Levels - * @private - */ -define( - 'tinymce.core.undo.Levels', - [ - "tinymce.core.util.Arr", - "tinymce.core.undo.Fragments" - ], - function (Arr, Fragments) { - var hasIframes = function (html) { - return html.indexOf('') !== -1; - }; - - var createFragmentedLevel = function (fragments) { - return { - type: 'fragmented', - fragments: fragments, - content: '', - bookmark: null, - beforeBookmark: null - }; - }; - - var createCompleteLevel = function (content) { - return { - type: 'complete', - fragments: null, - content: content, - bookmark: null, - beforeBookmark: null - }; - }; - - var createFromEditor = function (editor) { - var fragments, content, trimmedFragments; - - fragments = Fragments.read(editor.getBody()); - trimmedFragments = Arr.map(fragments, function (html) { - return editor.serializer.trimContent(html); - }); - content = trimmedFragments.join(''); - - return hasIframes(content) ? createFragmentedLevel(trimmedFragments) : createCompleteLevel(content); - }; - - var applyToEditor = function (editor, level, before) { - if (level.type === 'fragmented') { - Fragments.write(level.fragments, editor.getBody()); - } else { - editor.setContent(level.content, { format: 'raw' }); - } - - editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark); - }; - - var getLevelContent = function (level) { - return level.type === 'fragmented' ? level.fragments.join('') : level.content; - }; - - var isEq = function (level1, level2) { - return getLevelContent(level1) === getLevelContent(level2); - }; - - return { - createFragmentedLevel: createFragmentedLevel, - createCompleteLevel: createCompleteLevel, - createFromEditor: createFromEditor, - applyToEditor: applyToEditor, - isEq: isEq - }; - } -); -/** - * UndoManager.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed. - * - * @class tinymce.UndoManager - */ -define( - 'tinymce.core.UndoManager', - [ - "tinymce.core.util.VK", - "tinymce.core.util.Tools", - "tinymce.core.undo.Levels" - ], - function (VK, Tools, Levels) { return function (editor) { - var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; + var formats = FormatRegistry(editor); + var formatChangeState = Cell(null); - var isUnlocked = function () { - return locks === 0; - }; + FormatShortcuts.setup(editor); - var setTyping = function (typing) { - if (isUnlocked()) { - self.typing = typing; - } - }; - - function setDirty(state) { - editor.setDirty(state); - } - - function addNonTypingUndoLevel(e) { - setTyping(false); - self.add({}, e); - } - - function endTyping() { - if (self.typing) { - setTyping(false); - self.add(); - } - } - - // Add initial undo level when the editor is initialized - editor.on('init', function () { - self.add(); - }); - - // Get position before an execCommand is processed - editor.on('BeforeExecCommand', function (e) { - var cmd = e.command; - - if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { - endTyping(); - self.beforeChange(); - } - }); - - // Add undo level after an execCommand call was made - editor.on('ExecCommand', function (e) { - var cmd = e.command; - - if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { - addNonTypingUndoLevel(e); - } - }); - - editor.on('ObjectResizeStart Cut', function () { - self.beforeChange(); - }); - - editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); - editor.on('DragEnd', addNonTypingUndoLevel); - - editor.on('KeyUp', function (e) { - var keyCode = e.keyCode; - - // If key is prevented then don't add undo level - // This would happen on keyboard shortcuts for example - if (e.isDefaultPrevented()) { - return; - } - - if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) { - addNonTypingUndoLevel(); - editor.nodeChanged(); - } - - if (keyCode === 46 || keyCode === 8) { - editor.nodeChanged(); - } - - // Fire a TypingUndo/Change event on the first character entered - if (isFirstTypedCharacter && self.typing && Levels.isEq(Levels.createFromEditor(editor), data[0]) === false) { - if (editor.isDirty() === false) { - setDirty(true); - editor.fire('change', { level: data[0], lastLevel: null }); - } - - editor.fire('TypingUndo'); - isFirstTypedCharacter = false; - editor.nodeChanged(); - } - }); - - editor.on('KeyDown', function (e) { - var keyCode = e.keyCode; - - // If key is prevented then don't add undo level - // This would happen on keyboard shortcuts for example - if (e.isDefaultPrevented()) { - return; - } - - // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter - if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) { - if (self.typing) { - addNonTypingUndoLevel(e); - } - - return; - } - - // If key isn't Ctrl+Alt/AltGr - var modKey = (e.ctrlKey && !e.altKey) || e.metaKey; - if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) { - self.beforeChange(); - setTyping(true); - self.add({}, e); - isFirstTypedCharacter = true; - } - }); - - editor.on('MouseDown', function (e) { - if (self.typing) { - addNonTypingUndoLevel(e); - } - }); - - // Add keyboard shortcuts for undo/redo keys - editor.addShortcut('meta+z', '', 'Undo'); - editor.addShortcut('meta+y,meta+shift+z', '', 'Redo'); - - editor.on('AddUndo Undo Redo ClearUndos', function (e) { - if (!e.isDefaultPrevented()) { - editor.nodeChanged(); - } - }); - - /*eslint consistent-this:0 */ - self = { - // Explode for debugging reasons - data: data, - - /** - * State if the user is currently typing or not. This will add a typing operation into one undo - * level instead of one new level for each keystroke. - * - * @field {Boolean} typing - */ - typing: false, - - /** - * Stores away a bookmark to be used when performing an undo action so that the selection is before - * the change has been made. - * - * @method beforeChange - */ - beforeChange: function () { - if (isUnlocked()) { - beforeBookmark = editor.selection.getBookmark(2, true); - } - }, - - /** - * Adds a new undo level/snapshot to the undo list. - * - * @method add - * @param {Object} level Optional undo level object to add. - * @param {DOMEvent} event Optional event responsible for the creation of the undo level. - * @return {Object} Undo level that got added or null it a level wasn't needed. - */ - add: function (level, event) { - var i, settings = editor.settings, lastLevel, currentLevel; - - currentLevel = Levels.createFromEditor(editor); - level = level || {}; - level = Tools.extend(level, currentLevel); - - if (isUnlocked() === false || editor.removed) { - return null; - } - - lastLevel = data[index]; - if (editor.fire('BeforeAddUndo', { level: level, lastLevel: lastLevel, originalEvent: event }).isDefaultPrevented()) { - return null; - } - - // Add undo level if needed - if (lastLevel && Levels.isEq(lastLevel, level)) { - return null; - } - - // Set before bookmark on previous level - if (data[index]) { - data[index].beforeBookmark = beforeBookmark; - } - - // Time to compress - if (settings.custom_undo_redo_levels) { - if (data.length > settings.custom_undo_redo_levels) { - for (i = 0; i < data.length - 1; i++) { - data[i] = data[i + 1]; - } - - data.length--; - index = data.length; - } - } - - // Get a non intrusive normalized bookmark - level.bookmark = editor.selection.getBookmark(2, true); - - // Crop array if needed - if (index < data.length - 1) { - data.length = index + 1; - } - - data.push(level); - index = data.length - 1; - - var args = { level: level, lastLevel: lastLevel, originalEvent: event }; - - editor.fire('AddUndo', args); - - if (index > 0) { - setDirty(true); - editor.fire('change', args); - } - - return level; - }, - - /** - * Undoes the last action. - * - * @method undo - * @return {Object} Undo level or null if no undo was performed. - */ - undo: function () { - var level; - - if (self.typing) { - self.add(); - self.typing = false; - setTyping(false); - } - - if (index > 0) { - level = data[--index]; - Levels.applyToEditor(editor, level, true); - setDirty(true); - editor.fire('undo', { level: level }); - } - - return level; - }, - - /** - * Redoes the last action. - * - * @method redo - * @return {Object} Redo level or null if no redo was performed. - */ - redo: function () { - var level; - - if (index < data.length - 1) { - level = data[++index]; - Levels.applyToEditor(editor, level, false); - setDirty(true); - editor.fire('redo', { level: level }); - } - - return level; - }, - - /** - * Removes all undo levels. - * - * @method clear - */ - clear: function () { - data = []; - index = 0; - self.typing = false; - self.data = data; - editor.fire('ClearUndos'); - }, - - /** - * Returns true/false if the undo manager has any undo levels. - * - * @method hasUndo - * @return {Boolean} true/false if the undo manager has any undo levels. - */ - hasUndo: function () { - // Has undo levels or typing and content isn't the same as the initial level - return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0])); - }, - - /** - * Returns true/false if the undo manager has any redo levels. - * - * @method hasRedo - * @return {Boolean} true/false if the undo manager has any redo levels. - */ - hasRedo: function () { - return index < data.length - 1 && !self.typing; - }, - - /** - * Executes the specified mutator function as an undo transaction. The selection - * before the modification will be stored to the undo stack and if the DOM changes - * it will add a new undo level. Any logic within the translation that adds undo levels will - * be ignored. So a translation can include calls to execCommand or editor.insertContent. - * - * @method transact - * @param {function} callback Function that gets executed and has dom manipulation logic in it. - * @return {Object} Undo level that got added or null it a level wasn't needed. - */ - transact: function (callback) { - endTyping(); - self.beforeChange(); - self.ignore(callback); - return self.add(); - }, - - /** - * Executes the specified mutator function as an undo transaction. But without adding an undo level. - * Any logic within the translation that adds undo levels will be ignored. So a translation can - * include calls to execCommand or editor.insertContent. - * - * @method ignore - * @param {function} callback Function that gets executed and has dom manipulation logic in it. - * @return {Object} Undo level that got added or null it a level wasn't needed. - */ - ignore: function (callback) { - try { - locks++; - callback(); - } finally { - locks--; - } - }, - - /** - * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack - * then roll back that change and do the second mutation on top of the stack. This will produce an extra - * undo level that the user doesn't see until they undo. - * - * @method extra - * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level. - * @param {function} callback2 Function that does mutation but gets displayed to the user. - */ - extra: function (callback1, callback2) { - var lastLevel, bookmark; - - if (self.transact(callback1)) { - bookmark = data[index].bookmark; - lastLevel = data[index - 1]; - Levels.applyToEditor(editor, lastLevel, true); - - if (self.transact(callback2)) { - data[index - 1].beforeBookmark = bookmark; - } - } - } - }; - - return self; - }; - } -); - -define( - 'ephox.sugar.api.node.Body', - - [ - 'ephox.katamari.api.Thunk', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'global!document' - ], - - function (Thunk, Element, Node, document) { - - // Node.contains() is very, very, very good performance - // http://jsperf.com/closest-vs-contains/5 - var inBody = function (element) { - // Technically this is only required on IE, where contains() returns false for text nodes. - // But it's cheap enough to run everywhere and Sugar doesn't have platform detection (yet). - var dom = Node.isText(element) ? element.dom().parentNode : element.dom(); - - // use ownerDocument.body to ensure this works inside iframes. - // Normally contains is bad because an element "contains" itself, but here we want that. - return dom !== undefined && dom !== null && dom.ownerDocument.body.contains(dom); - }; - - var body = Thunk.cached(function() { - return getBody(Element.fromDom(document)); - }); - - var getBody = function (doc) { - var body = doc.dom().body; - if (body === null || body === undefined) throw 'Body is not available yet'; - return Element.fromDom(body); - }; - - return { - body: body, - getBody: getBody, - inBody: inBody - }; - } -); - -define( - 'ephox.sugar.impl.ClosestOrAncestor', - - [ - 'ephox.katamari.api.Type', - 'ephox.katamari.api.Option' - ], - - function (Type, Option) { - return function (is, ancestor, scope, a, isRoot) { - return is(scope, a) ? - Option.some(scope) : - Type.isFunction(isRoot) && isRoot(scope) ? - Option.none() : - ancestor(scope, a, isRoot); - }; - } -); -define( - 'ephox.sugar.api.search.PredicateFind', - - [ - 'ephox.katamari.api.Type', - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.sugar.api.node.Body', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.impl.ClosestOrAncestor' - ], - - function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) { - var first = function (predicate) { - return descendant(Body.body(), predicate); - }; - - var ancestor = function (scope, predicate, isRoot) { - var element = scope.dom(); - var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); - - while (element.parentNode) { - element = element.parentNode; - var el = Element.fromDom(element); - - if (predicate(el)) return Option.some(el); - else if (stop(el)) break; - } - return Option.none(); - }; - - var closest = function (scope, predicate, isRoot) { - // This is required to avoid ClosestOrAncestor passing the predicate to itself - var is = function (scope) { - return predicate(scope); - }; - return ClosestOrAncestor(is, ancestor, scope, predicate, isRoot); - }; - - var sibling = function (scope, predicate) { - var element = scope.dom(); - if (!element.parentNode) return Option.none(); - - return child(Element.fromDom(element.parentNode), function (x) { - return !Compare.eq(scope, x) && predicate(x); - }); - }; - - var child = function (scope, predicate) { - var result = Arr.find(scope.dom().childNodes, - Fun.compose(predicate, Element.fromDom)); - return result.map(Element.fromDom); - }; - - var descendant = function (scope, predicate) { - var descend = function (element) { - for (var i = 0; i < element.childNodes.length; i++) { - if (predicate(Element.fromDom(element.childNodes[i]))) - return Option.some(Element.fromDom(element.childNodes[i])); - - var res = descend(element.childNodes[i]); - if (res.isSome()) - return res; - } - - return Option.none(); - }; - - return descend(scope.dom()); - }; - - return { - first: first, - ancestor: ancestor, - closest: closest, - sibling: sibling, - child: child, - descendant: descendant - }; - } -); - -/** - * CaretUtils.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * Utility functions shared by the caret logic. - * - * @private - * @class tinymce.caret.CaretUtils - */ -define( - 'tinymce.core.caret.CaretUtils', - [ - "tinymce.core.util.Fun", - "tinymce.core.dom.TreeWalker", - "tinymce.core.dom.NodeType", - "tinymce.core.caret.CaretPosition", - "tinymce.core.caret.CaretContainer", - "tinymce.core.caret.CaretCandidate" - ], - function (Fun, TreeWalker, NodeType, CaretPosition, CaretContainer, CaretCandidate) { - var isContentEditableTrue = NodeType.isContentEditableTrue, - isContentEditableFalse = NodeType.isContentEditableFalse, - isBlockLike = NodeType.matchStyleValues('display', 'block table table-cell table-caption list-item'), - isCaretContainer = CaretContainer.isCaretContainer, - isCaretContainerBlock = CaretContainer.isCaretContainerBlock, - curry = Fun.curry, - isElement = NodeType.isElement, - isCaretCandidate = CaretCandidate.isCaretCandidate; - - function isForwards(direction) { - return direction > 0; - } - - function isBackwards(direction) { - return direction < 0; - } - - function skipCaretContainers(walk, shallow) { - var node; - - while ((node = walk(shallow))) { - if (!isCaretContainerBlock(node)) { - return node; - } - } - - return null; - } - - function findNode(node, direction, predicateFn, rootNode, shallow) { - var walker = new TreeWalker(node, rootNode); - - if (isBackwards(direction)) { - if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { - node = skipCaretContainers(walker.prev, true); - if (predicateFn(node)) { - return node; - } - } - - while ((node = skipCaretContainers(walker.prev, shallow))) { - if (predicateFn(node)) { - return node; - } - } - } - - if (isForwards(direction)) { - if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { - node = skipCaretContainers(walker.next, true); - if (predicateFn(node)) { - return node; - } - } - - while ((node = skipCaretContainers(walker.next, shallow))) { - if (predicateFn(node)) { - return node; - } - } - } - - return null; - } - - function getEditingHost(node, rootNode) { - for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { - if (isContentEditableTrue(node)) { - return node; - } - } - - return rootNode; - } - - function getParentBlock(node, rootNode) { - while (node && node != rootNode) { - if (isBlockLike(node)) { - return node; - } - - node = node.parentNode; - } - - return null; - } - - function isInSameBlock(caretPosition1, caretPosition2, rootNode) { - return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode); - } - - function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) { - return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode); - } - - function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) { - var container, offset; - - if (!caretPosition) { - return null; - } - - container = caretPosition.container(); - offset = caretPosition.offset(); - - if (!isElement(container)) { - return null; - } - - return container.childNodes[offset + relativeOffset]; - } - - function beforeAfter(before, node) { - var range = node.ownerDocument.createRange(); - - if (before) { - range.setStartBefore(node); - range.setEndBefore(node); - } else { - range.setStartAfter(node); - range.setEndAfter(node); - } - - return range; - } - - function isNodesInSameBlock(rootNode, node1, node2) { - return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode); - } - - function lean(left, rootNode, node) { - var sibling, siblingName; - - if (left) { - siblingName = 'previousSibling'; - } else { - siblingName = 'nextSibling'; - } - - while (node && node != rootNode) { - sibling = node[siblingName]; - - if (isCaretContainer(sibling)) { - sibling = sibling[siblingName]; - } - - if (isContentEditableFalse(sibling)) { - if (isNodesInSameBlock(rootNode, sibling, node)) { - return sibling; - } - - break; - } - - if (isCaretCandidate(sibling)) { - break; - } - - node = node.parentNode; - } - - return null; - } - - var before = curry(beforeAfter, true); - var after = curry(beforeAfter, false); - - function normalizeRange(direction, rootNode, range) { - var node, container, offset, location; - var leanLeft = curry(lean, true, rootNode); - var leanRight = curry(lean, false, rootNode); - - container = range.startContainer; - offset = range.startOffset; - - if (CaretContainer.isCaretContainerBlock(container)) { - if (!isElement(container)) { - container = container.parentNode; - } - - location = container.getAttribute('data-mce-caret'); - - if (location == 'before') { - node = container.nextSibling; - if (isContentEditableFalse(node)) { - return before(node); - } - } - - if (location == 'after') { - node = container.previousSibling; - if (isContentEditableFalse(node)) { - return after(node); - } - } - } - - if (!range.collapsed) { - return range; - } - - if (NodeType.isText(container)) { - if (isCaretContainer(container)) { - if (direction === 1) { - node = leanRight(container); - if (node) { - return before(node); - } - - node = leanLeft(container); - if (node) { - return after(node); - } - } - - if (direction === -1) { - node = leanLeft(container); - if (node) { - return after(node); - } - - node = leanRight(container); - if (node) { - return before(node); - } - } - - return range; - } - - if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) { - if (direction === 1) { - node = leanRight(container); - if (node) { - return before(node); - } - } - - return range; - } - - if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) { - if (direction === -1) { - node = leanLeft(container); - if (node) { - return after(node); - } - } - - return range; - } - - if (offset === container.data.length) { - node = leanRight(container); - if (node) { - return before(node); - } - - return range; - } - - if (offset === 0) { - node = leanLeft(container); - if (node) { - return after(node); - } - - return range; - } - } - - return range; - } - - function isNextToContentEditableFalse(relativeOffset, caretPosition) { - return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition)); - } - - return { - isForwards: isForwards, - isBackwards: isBackwards, - findNode: findNode, - getEditingHost: getEditingHost, - getParentBlock: getParentBlock, - isInSameBlock: isInSameBlock, - isInSameEditingHost: isInSameEditingHost, - isBeforeContentEditableFalse: curry(isNextToContentEditableFalse, 0), - isAfterContentEditableFalse: curry(isNextToContentEditableFalse, -1), - normalizeRange: normalizeRange - }; - } -); - -/** - * CaretWalker.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This module contains logic for moving around a virtual caret in logical order within a DOM element. - * - * It ignores the most obvious invalid caret locations such as within a script element or within a - * contentEditable=false element but it will return locations that isn't possible to render visually. - * - * @private - * @class tinymce.caret.CaretWalker - * @example - * var caretWalker = new CaretWalker(rootElm); - * - * var prevLogicalCaretPosition = caretWalker.prev(CaretPosition.fromRangeStart(range)); - * var nextLogicalCaretPosition = caretWalker.next(CaretPosition.fromRangeEnd(range)); - */ -define( - 'tinymce.core.caret.CaretWalker', - [ - "tinymce.core.dom.NodeType", - "tinymce.core.caret.CaretCandidate", - "tinymce.core.caret.CaretPosition", - "tinymce.core.caret.CaretUtils", - "tinymce.core.util.Arr", - "tinymce.core.util.Fun" - ], - function (NodeType, CaretCandidate, CaretPosition, CaretUtils, Arr, Fun) { - var isContentEditableFalse = NodeType.isContentEditableFalse, - isText = NodeType.isText, - isElement = NodeType.isElement, - isBr = NodeType.isBr, - isForwards = CaretUtils.isForwards, - isBackwards = CaretUtils.isBackwards, - isCaretCandidate = CaretCandidate.isCaretCandidate, - isAtomic = CaretCandidate.isAtomic, - isEditableCaretCandidate = CaretCandidate.isEditableCaretCandidate; - - function getParents(node, rootNode) { - var parents = []; - - while (node && node != rootNode) { - parents.push(node); - node = node.parentNode; - } - - return parents; - } - - function nodeAtIndex(container, offset) { - if (container.hasChildNodes() && offset < container.childNodes.length) { - return container.childNodes[offset]; - } - - return null; - } - - function getCaretCandidatePosition(direction, node) { - if (isForwards(direction)) { - if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { - return CaretPosition.before(node); - } - - if (isText(node)) { - return CaretPosition(node, 0); - } - } - - if (isBackwards(direction)) { - if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) { - return CaretPosition.after(node); - } - - if (isText(node)) { - return CaretPosition(node, node.data.length); - } - } - - if (isBackwards(direction)) { - if (isBr(node)) { - return CaretPosition.before(node); - } - - return CaretPosition.after(node); - } - - return CaretPosition.before(node); - } - - // Jumps over BR elements|
a
->|a
- function isBrBeforeBlock(node, rootNode) { - var next; - - if (!NodeType.isBr(node)) { - return false; - } - - next = findCaretPosition(1, CaretPosition.after(node), rootNode); - if (!next) { - return false; - } - - return !CaretUtils.isInSameBlock(CaretPosition.before(node), CaretPosition.before(next), rootNode); - } - - function findCaretPosition(direction, startCaretPosition, rootNode) { - var container, offset, node, nextNode, innerNode, - rootContentEditableFalseElm, caretPosition; - - if (!isElement(rootNode) || !startCaretPosition) { - return null; - } - - if (startCaretPosition.isEqual(CaretPosition.after(rootNode)) && rootNode.lastChild) { - caretPosition = CaretPosition.after(rootNode.lastChild); - if (isBackwards(direction) && isCaretCandidate(rootNode.lastChild) && isElement(rootNode.lastChild)) { - return isBr(rootNode.lastChild) ? CaretPosition.before(rootNode.lastChild) : caretPosition; - } - } else { - caretPosition = startCaretPosition; - } - - container = caretPosition.container(); - offset = caretPosition.offset(); - - if (isText(container)) { - if (isBackwards(direction) && offset > 0) { - return CaretPosition(container, --offset); - } - - if (isForwards(direction) && offset < container.length) { - return CaretPosition(container, ++offset); - } - - node = container; - } else { - if (isBackwards(direction) && offset > 0) { - nextNode = nodeAtIndex(container, offset - 1); - if (isCaretCandidate(nextNode)) { - if (!isAtomic(nextNode)) { - innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); - if (innerNode) { - if (isText(innerNode)) { - return CaretPosition(innerNode, innerNode.data.length); - } - - return CaretPosition.after(innerNode); - } - } - - if (isText(nextNode)) { - return CaretPosition(nextNode, nextNode.data.length); - } - - return CaretPosition.before(nextNode); - } - } - - if (isForwards(direction) && offset < container.childNodes.length) { - nextNode = nodeAtIndex(container, offset); - if (isCaretCandidate(nextNode)) { - if (isBrBeforeBlock(nextNode, rootNode)) { - return findCaretPosition(direction, CaretPosition.after(nextNode), rootNode); - } - - if (!isAtomic(nextNode)) { - innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); - if (innerNode) { - if (isText(innerNode)) { - return CaretPosition(innerNode, 0); - } - - return CaretPosition.before(innerNode); - } - } - - if (isText(nextNode)) { - return CaretPosition(nextNode, 0); - } - - return CaretPosition.after(nextNode); - } - } - - node = caretPosition.getNode(); - } - - if ((isForwards(direction) && caretPosition.isAtEnd()) || (isBackwards(direction) && caretPosition.isAtStart())) { - node = CaretUtils.findNode(node, direction, Fun.constant(true), rootNode, true); - if (isEditableCaretCandidate(node)) { - return getCaretCandidatePosition(direction, node); - } - } - - nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); - - rootContentEditableFalseElm = Arr.last(Arr.filter(getParents(container, rootNode), isContentEditableFalse)); - if (rootContentEditableFalseElm && (!nextNode || !rootContentEditableFalseElm.contains(nextNode))) { - if (isForwards(direction)) { - caretPosition = CaretPosition.after(rootContentEditableFalseElm); - } else { - caretPosition = CaretPosition.before(rootContentEditableFalseElm); - } - - return caretPosition; - } - - if (nextNode) { - return getCaretCandidatePosition(direction, nextNode); - } - - return null; - } - - return function (rootNode) { return { /** - * Returns the next logical caret position from the specificed input - * caretPoisiton or null if there isn't any more positions left for example - * at the end specified root element. + * Returns the format by name or all formats if no name is specified. * - * @method next - * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. - * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. + * @method get + * @param {String} name Optional name to retrieve by. + * @return {Array/Object} Array/Object with all registered formats or a specific format. */ - next: function (caretPosition) { - return findCaretPosition(1, caretPosition, rootNode); - }, + get: formats.get, /** - * Returns the previous logical caret position from the specificed input - * caretPoisiton or null if there isn't any more positions left for example - * at the end specified root element. + * Registers a specific format by name. * - * @method prev - * @param {tinymce.caret.CaretPosition} caretPosition Caret position to start from. - * @return {tinymce.caret.CaretPosition} CaretPosition or null if no position was found. + * @method register + * @param {Object/String} name Name of the format for example "bold". + * @param {Object/Array} format Optional format object or array of format variants + * can only be omitted if the first arg is an object. */ - prev: function (caretPosition) { - return findCaretPosition(-1, caretPosition, rootNode); - } + register: formats.register, + + /** + * Unregister a specific format by name. + * + * @method unregister + * @param {String} name Name of the format for example "bold". + */ + unregister: formats.unregister, + + /** + * Applies the specified format to the current selection or specified node. + * + * @method apply + * @param {String} name Name of format to apply. + * @param {Object} vars Optional list of variables to replace within format before applying it. + * @param {Node} node Optional node to apply the format to defaults to current selection. + */ + apply: Fun.curry(ApplyFormat.applyFormat, editor), + + /** + * Removes the specified format from the current selection or specified node. + * + * @method remove + * @param {String} name Name of format to remove. + * @param {Object} vars Optional list of variables to replace within format before removing it. + * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. + */ + remove: Fun.curry(RemoveFormat.remove, editor), + + /** + * Toggles the specified format on/off. + * + * @method toggle + * @param {String} name Name of format to apply/remove. + * @param {Object} vars Optional list of variables to replace within format before applying/removing it. + * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. + */ + toggle: Fun.curry(ToggleFormat.toggle, editor, formats), + + /** + * Matches the current selection or specified node against the specified format name. + * + * @method match + * @param {String} name Name of format to match. + * @param {Object} vars Optional list of variables to replace before checking it. + * @param {Node} node Optional node to check. + * @return {boolean} true/false if the specified selection/node matches the format. + */ + match: Fun.curry(MatchFormat.match, editor), + + /** + * Matches the current selection against the array of formats and returns a new array with matching formats. + * + * @method matchAll + * @param {Array} names Name of format to match. + * @param {Object} vars Optional list of variables to replace before checking it. + * @return {Array} Array with matched formats. + */ + matchAll: Fun.curry(MatchFormat.matchAll, editor), + + /** + * Return true/false if the specified node has the specified format. + * + * @method matchNode + * @param {Node} node Node to check the format on. + * @param {String} name Format name to check. + * @param {Object} vars Optional list of variables to replace before checking it. + * @param {Boolean} similar Match format that has similar properties. + * @return {Object} Returns the format object it matches or undefined if it doesn't match. + */ + matchNode: Fun.curry(MatchFormat.matchNode, editor), + + /** + * Returns true/false if the specified format can be applied to the current selection or not. It + * will currently only check the state for selector formats, it returns true on all other format types. + * + * @method canApply + * @param {String} name Name of format to check. + * @return {boolean} true/false if the specified format can be applied to the current selection/node. + */ + canApply: Fun.curry(MatchFormat.canApply, editor), + + /** + * Executes the specified callback when the current selection matches the formats or not. + * + * @method formatChanged + * @param {String} formats Comma separated list of formats to check for. + * @param {function} callback Callback with state and args when the format is changed/toggled on/off. + * @param {Boolean} similar True/false state if the match should handle similar or exact formats. + */ + formatChanged: Fun.curry(FormatChanged.formatChanged, editor, formatChangeState), + + /** + * Returns a preview css text for the specified format. + * + * @method getCssText + * @param {String/Object} format Format to generate preview css text for. + * @return {String} Css text for the specified format. + * @example + * var cssText1 = editor.formatter.getCssText('bold'); + * var cssText2 = editor.formatter.getCssText({inline: 'b'}); + */ + getCssText: Fun.curry(Preview.getCssText, editor) }; }; } ); -/** - * CaretFinder.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ define( - 'tinymce.core.caret.CaretFinder', - [ - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'tinymce.core.caret.CaretCandidate', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.caret.CaretUtils', - 'tinymce.core.caret.CaretWalker', - 'tinymce.core.dom.NodeType' - ], - function (Fun, Option, CaretCandidate, CaretPosition, CaretUtils, CaretWalker, NodeType) { - var walkToPositionIn = function (forward, rootNode, startNode) { - var position = forward ? CaretPosition.before(startNode) : CaretPosition.after(startNode); - return fromPosition(forward, rootNode, position); - }; - - var afterElement = function (node) { - return NodeType.isBr(node) ? CaretPosition.before(node) : CaretPosition.after(node); - }; - - var isBeforeOrStart = function (position) { - if (CaretPosition.isTextPosition(position)) { - return position.offset() === 0; - } else { - return CaretCandidate.isCaretCandidate(position.getNode()); - } - }; - - var isAfterOrEnd = function (position) { - if (CaretPosition.isTextPosition(position)) { - return position.offset() === position.container().data.length; - } else { - return CaretCandidate.isCaretCandidate(position.getNode(true)); - } - }; - - var isBeforeAfterSameElement = function (from, to) { - return !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true); - }; - - var isAtBr = function (position) { - return !CaretPosition.isTextPosition(position) && NodeType.isBr(position.getNode()); - }; - - var shouldSkipPosition = function (forward, from, to) { - if (forward) { - return !isBeforeAfterSameElement(from, to) && !isAtBr(from) && isAfterOrEnd(from) && isBeforeOrStart(to); - } else { - return !isBeforeAfterSameElement(to, from) && isBeforeOrStart(from) && isAfterOrEnd(to); - } - }; - - // Finds:a|b
->a|b
- var fromPosition = function (forward, rootNode, position) { - var walker = new CaretWalker(rootNode); - return Option.from(forward ? walker.next(position) : walker.prev(position)); - }; - - // Finds:a|b
->ab|
- var navigate = function (forward, rootNode, from) { - return fromPosition(forward, rootNode, from).bind(function (to) { - if (CaretUtils.isInSameBlock(from, to, rootNode) && shouldSkipPosition(forward, from, to)) { - return fromPosition(forward, rootNode, to); - } else { - return Option.some(to); - } - }); - }; - - var positionIn = function (forward, element) { - var startNode = forward ? element.firstChild : element.lastChild; - if (NodeType.isText(startNode)) { - return Option.some(new CaretPosition(startNode, forward ? 0 : startNode.data.length)); - } else if (startNode) { - if (CaretCandidate.isCaretCandidate(startNode)) { - return Option.some(forward ? CaretPosition.before(startNode) : afterElement(startNode)); - } else { - return walkToPositionIn(forward, element, startNode); - } - } else { - return Option.none(); - } - }; - - return { - fromPosition: fromPosition, - navigate: navigate, - positionIn: positionIn - }; - } -); - -/** - * DeleteUtils.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.DeleteUtils', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Option', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'ephox.sugar.api.search.PredicateFind' - ], - function (Arr, Option, Compare, Element, Node, PredicateFind) { - var toLookup = function (names) { - var lookup = Arr.foldl(names, function (acc, name) { - acc[name] = true; - return acc; - }, { }); - - return function (elm) { - return lookup[Node.name(elm)] === true; - }; - }; - - var isTextBlock = toLookup([ - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', 'blockquote', 'center', - 'dir', 'fieldset', 'header', 'footer', 'article', 'section', 'hgroup', 'aside', 'nav', 'figure' - ]); - - var isBeforeRoot = function (rootNode) { - return function (elm) { - return Compare.eq(rootNode, Element.fromDom(elm.dom().parentNode)); - }; - }; - - var getParentTextBlock = function (rootNode, elm) { - return Compare.contains(rootNode, elm) ? PredicateFind.closest(elm, isTextBlock, isBeforeRoot(rootNode)) : Option.none(); - }; - - return { - getParentTextBlock: getParentTextBlock - }; - } -); - -define( - 'ephox.sugar.api.search.SelectorFind', - - [ - 'ephox.sugar.api.search.PredicateFind', - 'ephox.sugar.api.search.Selectors', - 'ephox.sugar.impl.ClosestOrAncestor' - ], - - function (PredicateFind, Selectors, ClosestOrAncestor) { - // TODO: An internal SelectorFilter module that doesn't Element.fromDom() everything - - var first = function (selector) { - return Selectors.one(selector); - }; - - var ancestor = function (scope, selector, isRoot) { - return PredicateFind.ancestor(scope, function (e) { - return Selectors.is(e, selector); - }, isRoot); - }; - - var sibling = function (scope, selector) { - return PredicateFind.sibling(scope, function (e) { - return Selectors.is(e, selector); - }); - }; - - var child = function (scope, selector) { - return PredicateFind.child(scope, function (e) { - return Selectors.is(e, selector); - }); - }; - - var descendant = function (scope, selector) { - return Selectors.one(selector, scope); - }; - - var closest = function (scope, selector, isRoot) { - return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot); - }; - - return { - first: first, - ancestor: ancestor, - sibling: sibling, - child: child, - descendant: descendant, - closest: closest - }; - } -); - -define( - 'ephox.sugar.api.search.SelectorExists', - - [ - 'ephox.sugar.api.search.SelectorFind' - ], - - function (SelectorFind) { - var any = function (selector) { - return SelectorFind.first(selector).isSome(); - }; - - var ancestor = function (scope, selector, isRoot) { - return SelectorFind.ancestor(scope, selector, isRoot).isSome(); - }; - - var sibling = function (scope, selector) { - return SelectorFind.sibling(scope, selector).isSome(); - }; - - var child = function (scope, selector) { - return SelectorFind.child(scope, selector).isSome(); - }; - - var descendant = function (scope, selector) { - return SelectorFind.descendant(scope, selector).isSome(); - }; - - var closest = function (scope, selector, isRoot) { - return SelectorFind.closest(scope, selector, isRoot).isSome(); - }; - - return { - any: any, - ancestor: ancestor, - sibling: sibling, - child: child, - descendant: descendant, - closest: closest - }; - } -); - -/** - * Empty.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.dom.Empty', - [ - 'ephox.katamari.api.Fun', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.SelectorExists', - 'tinymce.core.caret.CaretCandidate', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.TreeWalker' - ], - function (Fun, Compare, Element, SelectorExists, CaretCandidate, NodeType, TreeWalker) { - var hasWhitespacePreserveParent = function (rootNode, node) { - var rootElement = Element.fromDom(rootNode); - var startNode = Element.fromDom(node); - return SelectorExists.ancestor(startNode, 'pre,code', Fun.curry(Compare.eq, rootElement)); - }; - - var isWhitespace = function (rootNode, node) { - return NodeType.isText(node) && /^[ \t\r\n]*$/.test(node.data) && hasWhitespacePreserveParent(rootNode, node) === false; - }; - - var isNamedAnchor = function (node) { - return NodeType.isElement(node) && node.nodeName === 'A' && node.hasAttribute('name'); - }; - - var isContent = function (rootNode, node) { - return (CaretCandidate.isCaretCandidate(node) && isWhitespace(rootNode, node) === false) || isNamedAnchor(node) || isBookmark(node); - }; - - var isBookmark = NodeType.hasAttribute('data-mce-bookmark'); - var isBogus = NodeType.hasAttribute('data-mce-bogus'); - var isBogusAll = NodeType.hasAttributeValue('data-mce-bogus', 'all'); - - var isEmptyNode = function (targetNode) { - var walker, node, brCount = 0; - - if (isContent(targetNode, targetNode)) { - return false; - } else { - node = targetNode.firstChild; - if (!node) { - return true; - } - - walker = new TreeWalker(node, targetNode); - do { - if (isBogusAll(node)) { - node = walker.next(true); - continue; - } - - if (isBogus(node)) { - node = walker.next(); - continue; - } - - if (NodeType.isBr(node)) { - brCount++; - node = walker.next(); - continue; - } - - if (isContent(targetNode, node)) { - return false; - } - - node = walker.next(); - } while (node); - - return brCount <= 1; - } - }; - - var isEmpty = function (elm) { - return isEmptyNode(elm.dom()); - }; - - return { - isEmpty: isEmpty - }; - } -); - -/** - * BlockBoundary.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.BlockBoundary', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Options', - 'ephox.katamari.api.Struct', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'ephox.sugar.api.search.PredicateFind', - 'ephox.sugar.api.search.Traverse', - 'tinymce.core.caret.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.delete.DeleteUtils', - 'tinymce.core.dom.Empty', - 'tinymce.core.dom.NodeType' - ], - function (Arr, Fun, Option, Options, Struct, Compare, Element, Node, PredicateFind, Traverse, CaretFinder, CaretPosition, DeleteUtils, Empty, NodeType) { - var BlockPosition = Struct.immutable('block', 'position'); - var BlockBoundary = Struct.immutable('from', 'to'); - - var getBlockPosition = function (rootNode, pos) { - var rootElm = Element.fromDom(rootNode); - var containerElm = Element.fromDom(pos.container()); - return DeleteUtils.getParentTextBlock(rootElm, containerElm).map(function (block) { - return BlockPosition(block, pos); - }); - }; - - var isDifferentBlocks = function (blockBoundary) { - return Compare.eq(blockBoundary.from().block(), blockBoundary.to().block()) === false; - }; - - var hasSameParent = function (blockBoundary) { - return Traverse.parent(blockBoundary.from().block()).bind(function (parent1) { - return Traverse.parent(blockBoundary.to().block()).filter(function (parent2) { - return Compare.eq(parent1, parent2); - }); - }).isSome(); - }; - - var isEditable = function (blockBoundary) { - return NodeType.isContentEditableFalse(blockBoundary.from().block()) === false && NodeType.isContentEditableFalse(blockBoundary.to().block()) === false; - }; - - var skipLastBr = function (rootNode, forward, blockPosition) { - if (NodeType.isBr(blockPosition.position().getNode()) && Empty.isEmpty(blockPosition.block()) === false) { - return CaretFinder.positionIn(false, blockPosition.block().dom()).bind(function (lastPositionInBlock) { - if (lastPositionInBlock.isEqual(blockPosition.position())) { - return CaretFinder.fromPosition(forward, rootNode, lastPositionInBlock).bind(function (to) { - return getBlockPosition(rootNode, to); - }); - } else { - return Option.some(blockPosition); - } - }).getOr(blockPosition); - } else { - return blockPosition; - } - }; - - var readFromRange = function (rootNode, forward, rng) { - var fromBlockPos = getBlockPosition(rootNode, CaretPosition.fromRangeStart(rng)); - var toBlockPos = fromBlockPos.bind(function (blockPos) { - return CaretFinder.fromPosition(forward, rootNode, blockPos.position()).bind(function (to) { - return getBlockPosition(rootNode, to).map(function (blockPos) { - return skipLastBr(rootNode, forward, blockPos); - }); - }); - }); - - return Options.liftN([fromBlockPos, toBlockPos], BlockBoundary).filter(function (blockBoundary) { - return isDifferentBlocks(blockBoundary) && hasSameParent(blockBoundary) && isEditable(blockBoundary); - }); - }; - - var read = function (rootNode, forward, rng) { - return rng.collapsed ? readFromRange(rootNode, forward, rng) : Option.none(); - }; - - return { - read: read - }; - } -); - -/** - * MergeBlocks.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.MergeBlocks', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Option', - 'ephox.sugar.api.dom.Insert', - 'ephox.sugar.api.dom.Remove', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.Traverse', - 'tinymce.core.caret.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.Empty', - 'tinymce.core.dom.NodeType' - ], - function (Arr, Option, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, Empty, NodeType) { - var mergeBlocksAndReposition = function (forward, fromBlock, toBlock, toPosition) { - var children = Traverse.children(fromBlock); - - if (NodeType.isBr(toPosition.getNode())) { - Remove.remove(Element.fromDom(toPosition.getNode())); - toPosition = CaretFinder.positionIn(false, toBlock.dom()).getOr(toPosition); - } - - if (Empty.isEmpty(fromBlock) === false) { - Arr.each(children, function (node) { - Insert.append(toBlock, node); - }); - } - - if (Empty.isEmpty(fromBlock)) { - Remove.remove(fromBlock); - } - - return children.length > 0 ? Option.from(toPosition) : Option.none(); - }; - - var mergeBlocks = function (forward, block1, block2) { - if (forward) { - if (Empty.isEmpty(block1)) { - Remove.remove(block1); - return CaretFinder.positionIn(true, block2.dom()); - } else { - return CaretFinder.positionIn(false, block1.dom()).bind(function (toPosition) { - return mergeBlocksAndReposition(forward, block2, block1, toPosition); - }); - } - } else { - if (Empty.isEmpty(block2)) { - Remove.remove(block2); - return CaretFinder.positionIn(true, block1.dom()); - } else { - return CaretFinder.positionIn(false, block2.dom()).bind(function (toPosition) { - return mergeBlocksAndReposition(forward, block1, block2, toPosition); - }); - } - } - }; - - return { - mergeBlocks: mergeBlocks - }; - } -); - -/** - * BlockBoundaryDelete.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.BlockBoundaryDelete', - [ - 'tinymce.core.delete.BlockBoundary', - 'tinymce.core.delete.MergeBlocks' - ], - function (BlockBoundary, MergeBlocks) { - var backspaceDelete = function (editor, forward) { - var position; - - position = BlockBoundary.read(editor.getBody(), forward, editor.selection.getRng()).bind(function (blockBoundary) { - return MergeBlocks.mergeBlocks(forward, blockBoundary.from().block(), blockBoundary.to().block()); - }); - - position.each(function (pos) { - editor.selection.setRng(pos.toRange()); - }); - - return position.isSome(); - }; - - return { - backspaceDelete: backspaceDelete - }; - } -); - -/** - * BlockRangeDelete.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.BlockRangeDelete', - [ - 'ephox.katamari.api.Options', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'tinymce.core.delete.DeleteUtils', - 'tinymce.core.delete.MergeBlocks' - ], - function (Options, Compare, Element, DeleteUtils, MergeBlocks) { - var deleteRange = function (rootNode, selection) { - var rng = selection.getRng(); - - return Options.liftN([ - DeleteUtils.getParentTextBlock(rootNode, Element.fromDom(rng.startContainer)), - DeleteUtils.getParentTextBlock(rootNode, Element.fromDom(rng.endContainer)) - ], function (block1, block2) { - if (Compare.eq(block1, block2) === false) { - rng.deleteContents(); - - MergeBlocks.mergeBlocks(true, block1, block2).each(function (pos) { - selection.setRng(pos.toRange()); - }); - - return true; - } else { - return false; - } - }).getOr(false); - }; - - var backspaceDelete = function (editor, forward) { - var rootNode = Element.fromDom(editor.getBody()); - - if (editor.selection.isCollapsed() === false) { - return deleteRange(rootNode, editor.selection); - } else { - return false; - } - }; - - return { - backspaceDelete: backspaceDelete - }; - } -); - -define( - 'ephox.katamari.api.Adt', + 'ephox.sugar.api.properties.Attr', [ + 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', 'ephox.katamari.api.Obj', - 'ephox.katamari.api.Type', - 'global!Array', + 'ephox.sugar.api.node.Node', 'global!Error', 'global!console' ], - function (Arr, Obj, Type, Array, Error, console) { - /* - * Generates a church encoded ADT (https://en.wikipedia.org/wiki/Church_encoding) - * For syntax and use, look at the test code. - */ - var generate = function (cases) { - // validation - if (!Type.isArray(cases)) { - throw new Error('cases must be an array'); + /* + * Direct attribute manipulation has been around since IE8, but + * was apparently unstable until IE10. + */ + function (Type, Arr, Obj, Node, Error, console) { + var rawSet = function (dom, key, value) { + /* + * JQuery coerced everything to a string, and silently did nothing on text node/null/undefined. + * + * We fail on those invalid cases, only allowing numbers and booleans. + */ + if (Type.isString(value) || Type.isBoolean(value) || Type.isNumber(value)) { + dom.setAttribute(key, value + ''); + } else { + console.error('Invalid call to Attr.set. Key ', key, ':: Value ', value, ':: Element ', dom); + throw new Error('Attribute value was not simple'); } - if (cases.length === 0) { - throw new Error('there must be at least one case'); - } - - var constructors = [ ]; - - // adt is mutated to add the individual cases - var adt = {}; - Arr.each(cases, function (acase, count) { - var keys = Obj.keys(acase); - - // validation - if (keys.length !== 1) { - throw new Error('one and only one name per case'); - } - - var key = keys[0]; - var value = acase[key]; - - // validation - if (adt[key] !== undefined) { - throw new Error('duplicate key detected:' + key); - } else if (key === 'cata') { - throw new Error('cannot have a case named cata (sorry)'); - } else if (!Type.isArray(value)) { - // this implicitly checks if acase is an object - throw new Error('case arguments must be an array'); - } - - constructors.push(key); - // - // constructor for key - // - adt[key] = function () { - var argLength = arguments.length; - - // validation - if (argLength !== value.length) { - throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); - } - - // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome - var args = new Array(argLength); - for (var i = 0; i < args.length; i++) args[i] = arguments[i]; - - - var match = function (branches) { - var branchKeys = Obj.keys(branches); - if (constructors.length !== branchKeys.length) { - throw new Error('Wrong number of arguments to match. Expected: ' + constructors.join(',') + '\nActual: ' + branchKeys.join(',')); - } - - var allReqd = Arr.forall(constructors, function (reqKey) { - return Arr.contains(branchKeys, reqKey); - }); - - if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); - - return branches[key].apply(null, args); - }; - - // - // the fold function for key - // - return { - fold: function (/* arguments */) { - // runtime validation - if (arguments.length !== cases.length) { - throw new Error('Wrong number of arguments to fold. Expected ' + cases.length + ', got ' + arguments.length); - } - var target = arguments[count]; - return target.apply(null, args); - }, - match: match, - - // NOTE: Only for debugging. - log: function (label) { - console.log(label, { - constructors: constructors, - constructor: key, - params: args - }); - } - }; - }; - }); - - return adt; - }; - return { - generate: generate - }; - } -); -/** - * CefDeleteAction.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.CefDeleteAction', - [ - 'ephox.katamari.api.Adt', - 'ephox.katamari.api.Option', - 'ephox.sugar.api.node.Element', - 'tinymce.core.caret.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.caret.CaretUtils', - 'tinymce.core.delete.DeleteUtils', - 'tinymce.core.dom.Empty', - 'tinymce.core.dom.NodeType' - ], - function (Adt, Option, Element, CaretFinder, CaretPosition, CaretUtils, DeleteUtils, Empty, NodeType) { - var DeleteAction = Adt.generate([ - { remove: [ 'element' ] }, - { moveToElement: [ 'element' ] }, - { moveToPosition: [ 'position' ] } - ]); - - var isAtContentEditableBlockCaret = function (forward, from) { - var elm = from.getNode(forward === false); - var caretLocation = forward ? 'after' : 'before'; - return NodeType.isElement(elm) && elm.getAttribute('data-mce-caret') === caretLocation; }; - var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) { - var toCefElm = to.getNode(forward === false); - return DeleteUtils.getParentTextBlock(Element.fromDom(rootNode), Element.fromDom(from.getNode())).map(function (blockElm) { - return Empty.isEmpty(blockElm) ? DeleteAction.remove(blockElm.dom()) : DeleteAction.moveToElement(toCefElm); - }).orThunk(function () { - return Option.some(DeleteAction.moveToElement(toCefElm)); + var set = function (element, key, value) { + rawSet(element.dom(), key, value); + }; + + var setAll = function (element, attrs) { + var dom = element.dom(); + Obj.each(attrs, function (v, k) { + rawSet(dom, k, v); }); }; - var findCefPosition = function (rootNode, forward, from) { - return CaretFinder.fromPosition(forward, rootNode, from).bind(function (to) { - if (forward && NodeType.isContentEditableFalse(to.getNode())) { - return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); - } else if (forward === false && NodeType.isContentEditableFalse(to.getNode(true))) { - return deleteEmptyBlockOrMoveToCef(rootNode, forward, from, to); - } else if (forward && CaretUtils.isAfterContentEditableFalse(from)) { - return Option.some(DeleteAction.moveToPosition(to)); - } else if (forward === false && CaretUtils.isBeforeContentEditableFalse(from)) { - return Option.some(DeleteAction.moveToPosition(to)); - } else { - return Option.none(); - } + var get = function (element, key) { + var v = element.dom().getAttribute(key); + + // undefined is the more appropriate value for JS, and this matches JQuery + return v === null ? undefined : v; + }; + + var has = function (element, key) { + var dom = element.dom(); + + // return false for non-element nodes, no point in throwing an error + return dom && dom.hasAttribute ? dom.hasAttribute(key) : false; + }; + + var remove = function (element, key) { + element.dom().removeAttribute(key); + }; + + var hasNone = function (element) { + var attrs = element.dom().attributes; + return attrs === undefined || attrs === null || attrs.length === 0; + }; + + var clone = function (element) { + return Arr.foldl(element.dom().attributes, function (acc, attr) { + acc[attr.name] = attr.value; + return acc; + }, {}); + }; + + var transferOne = function (source, destination, attr) { + // NOTE: We don't want to clobber any existing attributes + if (has(source, attr) && !has(destination, attr)) set(destination, attr, get(source, attr)); + }; + + // Transfer attributes(attrs) from source to destination, unless they are already present + var transfer = function (source, destination, attrs) { + if (!Node.isElement(source) || !Node.isElement(destination)) return; + Arr.each(attrs, function (attr) { + transferOne(source, destination, attr); }); }; - var getContentEditableBlockAction = function (forward, elm) { - if (forward && NodeType.isContentEditableFalse(elm.nextSibling)) { - return Option.some(DeleteAction.moveToElement(elm.nextSibling)); - } else if (forward === false && NodeType.isContentEditableFalse(elm.previousSibling)) { - return Option.some(DeleteAction.moveToElement(elm.previousSibling)); - } else { - return Option.none(); - } - }; - - var getContentEditableAction = function (rootNode, forward, from) { - if (isAtContentEditableBlockCaret(forward, from)) { - return getContentEditableBlockAction(forward, from.getNode(forward === false)) - .fold( - function () { - return findCefPosition(rootNode, forward, from); - }, - Option.some - ); - } else { - return findCefPosition(rootNode, forward, from); - } - }; - - var read = function (rootNode, forward, rng) { - var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng); - var from = CaretPosition.fromRangeStart(normalizedRange); - - if (forward === false && CaretUtils.isAfterContentEditableFalse(from)) { - return Option.some(DeleteAction.remove(from.getNode(true))); - } else if (forward && CaretUtils.isBeforeContentEditableFalse(from)) { - return Option.some(DeleteAction.remove(from.getNode())); - } else { - return getContentEditableAction(rootNode, forward, from); - } - }; - return { - read: read + clone: clone, + set: set, + setAll: setAll, + get: get, + has: has, + remove: remove, + hasNone: hasNone, + transfer: transfer }; } ); -/** - * Bidi.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - define( - 'tinymce.core.text.Bidi', + 'ephox.sugar.impl.Style', + [ + ], + function () { - var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; - - var hasStrongRtl = function (text) { - return strongRtl.test(text); + // some elements, such as mathml, don't have style attributes + var isSupported = function (dom) { + return dom.style !== undefined; }; return { - hasStrongRtl: hasStrongRtl + isSupported: isSupported }; } ); +defineGlobal("global!window", window); +define( + 'ephox.sugar.api.properties.Css', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.impl.Style', + 'ephox.katamari.api.Strings', + 'global!Error', + 'global!console', + 'global!window' + ], + + function (Type, Arr, Obj, Option, Attr, Body, Element, Node, Style, Strings, Error, console, window) { + var internalSet = function (dom, property, value) { + // This is going to hurt. Apologies. + // JQuery coerces numbers to pixels for certain property names, and other times lets numbers through. + // we're going to be explicit; strings only. + if (!Type.isString(value)) { + console.error('Invalid call to CSS.set. Property ', property, ':: Value ', value, ':: Element ', dom); + throw new Error('CSS value must be a string: ' + value); + } + + // removed: support for dom().style[property] where prop is camel case instead of normal property name + if (Style.isSupported(dom)) dom.style.setProperty(property, value); + }; + + var internalRemove = function (dom, property) { + /* + * IE9 and above - MDN doesn't have details, but here's a couple of random internet claims + * + * http://help.dottoro.com/ljopsjck.php + * http://stackoverflow.com/a/7901886/7546 + */ + if (Style.isSupported(dom)) dom.style.removeProperty(property); + }; + + var set = function (element, property, value) { + var dom = element.dom(); + internalSet(dom, property, value); + }; + + var setAll = function (element, css) { + var dom = element.dom(); + + Obj.each(css, function (v, k) { + internalSet(dom, k, v); + }); + }; + + var setOptions = function(element, css) { + var dom = element.dom(); + + Obj.each(css, function (v, k) { + v.fold(function () { + internalRemove(dom, k); + }, function (value) { + internalSet(dom, k, value); + }); + }); + }; + + /* + * NOTE: For certain properties, this returns the "used value" which is subtly different to the "computed value" (despite calling getComputedStyle). + * Blame CSS 2.0. + * + * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + */ + var get = function (element, property) { + var dom = element.dom(); + /* + * IE9 and above per + * https://developer.mozilla.org/en/docs/Web/API/window.getComputedStyle + * + * Not in numerosity, because it doesn't memoize and looking this up dynamically in performance critical code would be horrendous. + * + * JQuery has some magic here for IE popups, but we don't really need that. + * It also uses element.ownerDocument.defaultView to handle iframes but that hasn't been required since FF 3.6. + */ + var styles = window.getComputedStyle(dom); + var r = styles.getPropertyValue(property); + + // jquery-ism: If r is an empty string, check that the element is not in a document. If it isn't, return the raw value. + // Turns out we do this a lot. + var v = (r === '' && !Body.inBody(element)) ? getUnsafeProperty(dom, property) : r; + + // undefined is the more appropriate value for JS. JQuery coerces to an empty string, but screw that! + return v === null ? undefined : v; + }; + + var getUnsafeProperty = function (dom, property) { + // removed: support for dom().style[property] where prop is camel case instead of normal property name + // empty string is what the browsers (IE11 and Chrome) return when the propertyValue doesn't exists. + return Style.isSupported(dom) ? dom.style.getPropertyValue(property) : ''; + }; + + /* + * Gets the raw value from the style attribute. Useful for retrieving "used values" from the DOM: + * https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * + * Returns NONE if the property isn't set, or the value is an empty string. + */ + var getRaw = function (element, property) { + var dom = element.dom(); + var raw = getUnsafeProperty(dom, property); + + return Option.from(raw).filter(function (r) { return r.length > 0; }); + }; + + var isValidValue = function (tag, property, value) { + var element = Element.fromTag(tag); + set(element, property, value); + var style = getRaw(element, property); + return style.isSome(); + }; + + var remove = function (element, property) { + var dom = element.dom(); + + internalRemove(dom, property); + + if (Attr.has(element, 'style') && Strings.trim(Attr.get(element, 'style')) === '') { + // No more styles left, remove the style attribute as well + Attr.remove(element, 'style'); + } + }; + + var preserve = function (element, f) { + var oldStyles = Attr.get(element, 'style'); + var result = f(element); + var restore = oldStyles === undefined ? Attr.remove : Attr.set; + restore(element, 'style', oldStyles); + return result; + }; + + var copy = function (source, target) { + var sourceDom = source.dom(); + var targetDom = target.dom(); + if (Style.isSupported(sourceDom) && Style.isSupported(targetDom)) { + targetDom.style.cssText = sourceDom.style.cssText; + } + }; + + var reflow = function (e) { + /* NOTE: + * do not rely on this return value. + * It's here so the closure compiler doesn't optimise the property access away. + */ + return e.dom().offsetWidth; + }; + + var transferOne = function (source, destination, style) { + getRaw(source, style).each(function (value) { + // NOTE: We don't want to clobber any existing inline styles. + if (getRaw(destination, style).isNone()) set(destination, style, value); + }); + }; + + var transfer = function (source, destination, styles) { + if (!Node.isElement(source) || !Node.isElement(destination)) return; + Arr.each(styles, function (style) { + transferOne(source, destination, style); + }); + }; + + return { + copy: copy, + set: set, + preserve: preserve, + setAll: setAll, + setOptions: setOptions, + remove: remove, + get: get, + getRaw: getRaw, + isValidValue: isValidValue, + reflow: reflow, + transfer: transfer + }; + } +); + /** - * InlineUtils.js + * EditorView.js * * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( - 'tinymce.core.keyboard.InlineUtils', + 'tinymce.core.EditorView', [ - 'ephox.katamari.api.Arr', 'ephox.katamari.api.Fun', 'ephox.katamari.api.Option', - 'ephox.katamari.api.Options', - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.caret.CaretUtils', - 'tinymce.core.caret.CaretWalker', - 'tinymce.core.dom.DOMUtils', - 'tinymce.core.dom.NodeType', - 'tinymce.core.text.Bidi' + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.Traverse' ], - function (Arr, Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, NodeType, Bidi) { - var isInlineTarget = function (elm) { - return DOMUtils.DOM.is(elm, 'a[href],code'); + function (Fun, Option, Compare, Element, Css, Traverse) { + var getProp = function (propName, elm) { + var rawElm = elm.dom(); + return rawElm[propName]; }; - var isRtl = function (element) { - return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || Bidi.hasStrongRtl(element.textContent); + var getComputedSizeProp = function (propName, elm) { + return parseInt(Css.get(elm, propName), 10); }; - var findInlineParents = function (rootNode, pos) { - return Arr.filter(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget); + var getClientWidth = Fun.curry(getProp, 'clientWidth'); + var getClientHeight = Fun.curry(getProp, 'clientHeight'); + var getMarginTop = Fun.curry(getComputedSizeProp, 'margin-top'); + var getMarginLeft = Fun.curry(getComputedSizeProp, 'margin-left'); + + var getBoundingClientRect = function (elm) { + return elm.dom().getBoundingClientRect(); }; - var findInline = function (rootNode, pos) { - var parents = findInlineParents(rootNode, pos); - return Option.from(parents[0]); + var isInsideElementContentArea = function (bodyElm, clientX, clientY) { + var clientWidth = getClientWidth(bodyElm); + var clientHeight = getClientHeight(bodyElm); + + return clientX >= 0 && clientY >= 0 && clientX <= clientWidth && clientY <= clientHeight; }; - var findRootInline = function (rootNode, pos) { - var parents = findInlineParents(rootNode, pos); - return Option.from(parents[parents.length - 1]); + var transpose = function (inline, elm, clientX, clientY) { + var clientRect = getBoundingClientRect(elm); + var deltaX = inline ? clientRect.left + elm.dom().clientLeft + getMarginLeft(elm) : 0; + var deltaY = inline ? clientRect.top + elm.dom().clientTop + getMarginTop(elm) : 0; + var x = clientX - deltaX; + var y = clientY - deltaY; + + return { x: x, y: y }; }; - var hasSameParentBlock = function (rootNode, node1, node2) { - var block1 = CaretUtils.getParentBlock(node1, rootNode); - var block2 = CaretUtils.getParentBlock(node2, rootNode); - return block1 && block1 === block2; + // Checks if the specified coordinate is within the visual content area excluding the scrollbars + var isXYInContentArea = function (editor, clientX, clientY) { + var bodyElm = Element.fromDom(editor.getBody()); + var targetElm = editor.inline ? bodyElm : Traverse.documentElement(bodyElm); + var transposedPoint = transpose(editor.inline, targetElm, clientX, clientY); + + return isInsideElementContentArea(targetElm, transposedPoint.x, transposedPoint.y); }; - var isInInline = function (rootNode, pos) { - return pos ? findRootInline(rootNode, pos).isSome() : false; + var fromDomSafe = function (node) { + return Option.from(node).map(Element.fromDom); }; - var isAtInlineEndPoint = function (rootNode, pos) { - return findRootInline(rootNode, pos).map(function (inline) { - return findCaretPosition(inline, false, pos).isNone() || findCaretPosition(inline, true, pos).isNone(); + var isEditorAttachedToDom = function (editor) { + var rawContainer = editor.inline ? editor.getBody() : editor.getContentAreaContainer(); + + return fromDomSafe(rawContainer).map(function (container) { + return Compare.contains(Traverse.owner(container), container); }).getOr(false); }; - var isAtZwsp = function (pos) { - return CaretContainer.isBeforeInline(pos) || CaretContainer.isAfterInline(pos); - }; - - var findCaretPositionIn = function (node, forward) { - return CaretFinder.positionIn(forward, node); - }; - - var findCaretPosition = function (rootNode, forward, from) { - return CaretFinder.fromPosition(forward, rootNode, from); - }; - - var normalizePosition = function (forward, pos) { - var container = pos.container(), offset = pos.offset(); - - if (forward) { - if (CaretContainer.isCaretContainerInline(container)) { - if (NodeType.isText(container.nextSibling)) { - return new CaretPosition(container.nextSibling, 0); - } else { - return CaretPosition.after(container); - } - } else { - return CaretContainer.isBeforeInline(pos) ? new CaretPosition(container, offset + 1) : pos; - } - } else { - if (CaretContainer.isCaretContainerInline(container)) { - if (NodeType.isText(container.previousSibling)) { - return new CaretPosition(container.previousSibling, container.previousSibling.data.length); - } else { - return CaretPosition.before(container); - } - } else { - return CaretContainer.isAfterInline(pos) ? new CaretPosition(container, offset - 1) : pos; - } - } - }; - - var normalizeForwards = Fun.curry(normalizePosition, true); - var normalizeBackwards = Fun.curry(normalizePosition, false); - return { - isInlineTarget: isInlineTarget, - findInline: findInline, - findRootInline: findRootInline, - isInInline: isInInline, - isRtl: isRtl, - isAtInlineEndPoint: isAtInlineEndPoint, - isAtZwsp: isAtZwsp, - findCaretPositionIn: findCaretPositionIn, - findCaretPosition: findCaretPosition, - normalizePosition: normalizePosition, - normalizeForwards: normalizeForwards, - normalizeBackwards: normalizeBackwards, - hasSameParentBlock: hasSameParentBlock - }; - } -); -/** - * DeleteElement.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.delete.DeleteElement', - [ - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Options', - 'ephox.sugar.api.dom.Insert', - 'ephox.sugar.api.dom.Remove', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'ephox.sugar.api.search.PredicateFind', - 'ephox.sugar.api.search.Traverse', - 'tinymce.core.caret.CaretCandidate', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.Empty', - 'tinymce.core.dom.NodeType', - 'tinymce.core.keyboard.InlineUtils' - ], - function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretPosition, Empty, NodeType, InlineUtils) { - var needsReposition = function (pos, elm) { - var container = pos.container(); - var offset = pos.offset(); - return CaretPosition.isTextPosition(pos) === false && container === elm.parentNode && offset > CaretPosition.before(elm).offset(); - }; - - var reposition = function (elm, pos) { - return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos; - }; - - var beforeOrStartOf = function (node) { - return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node); - }; - - var afterOrEndOf = function (node) { - return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node); - }; - - var getPreviousSiblingCaretPosition = function (elm) { - if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { - return Option.some(afterOrEndOf(elm.previousSibling)); - } else { - return elm.previousSibling ? InlineUtils.findCaretPositionIn(elm.previousSibling, false) : Option.none(); - } - }; - - var getNextSiblingCaretPosition = function (elm) { - if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { - return Option.some(beforeOrStartOf(elm.nextSibling)); - } else { - return elm.nextSibling ? InlineUtils.findCaretPositionIn(elm.nextSibling, true) : Option.none(); - } - }; - - var findCaretPositionBackwardsFromElm = function (rootElement, elm) { - var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode); - return InlineUtils.findCaretPosition(rootElement, false, startPosition).fold( - function () { - return InlineUtils.findCaretPosition(rootElement, true, CaretPosition.after(elm)); - }, - Option.some - ); - }; - - var findCaretPositionForwardsFromElm = function (rootElement, elm) { - return InlineUtils.findCaretPosition(rootElement, true, CaretPosition.after(elm)).fold( - function () { - return InlineUtils.findCaretPosition(rootElement, false, CaretPosition.before(elm)); - }, - Option.some - ); - }; - - var findCaretPositionBackwards = function (rootElement, elm) { - return getPreviousSiblingCaretPosition(elm).orThunk(function () { - return getNextSiblingCaretPosition(elm); - }).orThunk(function () { - return findCaretPositionBackwardsFromElm(rootElement, elm); - }); - }; - - var findCaretPositionForward = function (rootElement, elm) { - return getNextSiblingCaretPosition(elm).orThunk(function () { - return getPreviousSiblingCaretPosition(elm); - }).orThunk(function () { - return findCaretPositionForwardsFromElm(rootElement, elm); - }); - }; - - var findCaretPosition = function (forward, rootElement, elm) { - return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); - }; - - var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) { - return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); - }; - - var setSelection = function (editor, forward, pos) { - pos.fold( - function () { - editor.focus(); - }, - function (pos) { - editor.selection.setRng(pos.toRange(), forward); - } - ); - }; - - var eqRawNode = function (rawNode) { - return function (elm) { - return elm.dom() === rawNode; - }; - }; - - var isBlock = function (editor, elm) { - return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm)); - }; - - var paddEmptyBlock = function (elm) { - if (Empty.isEmpty(elm)) { - var br = Element.fromHtml('|a when there is only one item left except the zwsp caret container nodes
- var hasOnlyTwoOrLessPositionsLeft = function (elm) {
- return Options.liftN([
- InlineUtils.findCaretPositionIn(elm, true),
- InlineUtils.findCaretPositionIn(elm, false)
- ], function (firstPos, lastPos) {
- var normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos);
- var normalizedLastPos = InlineUtils.normalizePosition(false, lastPos);
-
- return InlineUtils.findCaretPosition(elm, true, normalizedFirstPos).map(function (pos) {
- return pos.isEqual(normalizedLastPos);
- }).getOr(true);
- }).getOr(true);
- };
-
- var setCaretLocation = function (editor, caret) {
- return function (location) {
- return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
- BoundarySelection.setCaretPosition(editor, pos);
- return true;
- }).getOr(false);
- };
- };
-
- var deleteFromTo = function (editor, caret, from, to) {
- var rootNode = editor.getBody();
-
- editor.undoManager.ignore(function () {
- editor.selection.setRng(rangeFromPositions(from, to));
- editor.execCommand('Delete');
-
- BoundaryLocation.readLocation(rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
- .map(BoundaryLocation.inside)
- .map(setCaretLocation(editor, caret));
- });
-
- editor.nodeChanged();
- };
-
- var rescope = function (rootNode, node) {
- var parentBlock = CaretUtils.getParentBlock(node, rootNode);
- return parentBlock ? parentBlock : rootNode;
- };
-
- var backspaceDeleteCollapsed = function (editor, caret, forward, from) {
- var rootNode = rescope(editor.getBody(), from.container());
- var fromLocation = BoundaryLocation.readLocation(rootNode, from);
-
- return fromLocation.bind(function (location) {
- if (forward) {
- return location.fold(
- Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before
- Option.none, // Start
- Fun.constant(Option.some(BoundaryLocation.outside(location))), // End
- Option.none // After
- );
- } else {
- return location.fold(
- Option.none, // Before
- Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start
- Option.none, // End
- Fun.constant(Option.some(BoundaryLocation.inside(location))) // After
- );
- }
- })
- .map(setCaretLocation(editor, caret))
- .getOrThunk(function () {
- var toPosition = CaretFinder.navigate(forward, rootNode, from);
- var toLocation = toPosition.bind(function (pos) {
- return BoundaryLocation.readLocation(rootNode, pos);
- });
-
- if (fromLocation.isSome() && toLocation.isSome()) {
- return InlineUtils.findRootInline(rootNode, from).map(function (elm) {
- if (hasOnlyTwoOrLessPositionsLeft(elm)) {
- DeleteElement.deleteElement(editor, forward, Element.fromDom(elm));
- return true;
- } else {
- return false;
- }
- }).getOr(false);
- } else {
- return toLocation.bind(function (_) {
- return toPosition.map(function (to) {
- if (forward) {
- deleteFromTo(editor, caret, from, to);
- } else {
- deleteFromTo(editor, caret, to, from);
- }
-
- return true;
- });
- }).getOr(false);
- }
- });
- };
-
- var backspaceDelete = function (editor, caret, forward) {
- if (editor.selection.isCollapsed() && isFeatureEnabled(editor)) {
- var from = CaretPosition.fromRangeStart(editor.selection.getRng());
- return backspaceDeleteCollapsed(editor, caret, forward, from);
- }
-
- return false;
- };
-
- return {
- backspaceDelete: backspaceDelete
- };
- }
-);
-/**
- * Commands.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-define(
- 'tinymce.core.delete.DeleteCommands',
- [
- 'tinymce.core.delete.BlockBoundaryDelete',
- 'tinymce.core.delete.BlockRangeDelete',
- 'tinymce.core.delete.CefDelete',
- 'tinymce.core.delete.InlineBoundaryDelete'
- ],
- function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, BoundaryDelete) {
- var nativeCommand = function (editor, command) {
- editor.getDoc().execCommand(command, false, null);
- };
-
- var paddEmptyBody = function (editor) {
- var dom = editor.dom;
-
- // Check if body is empty after the delete call if so then set the contents
- // to an empty string and move the caret to any block produced by that operation
- // this fixes the issue with root blocks not being properly produced after a delete call on IE
- var body = editor.getBody();
-
- if (dom.isEmpty(body)) {
- editor.setContent('');
-
- if (body.firstChild && dom.isBlock(body.firstChild)) {
- editor.selection.setCursorLocation(body.firstChild, 0);
- } else {
- editor.selection.setCursorLocation(body, 0);
- }
- }
- };
-
- var deleteCommand = function (editor) {
- if (CefDelete.backspaceDelete(editor, false)) {
- return;
- } else if (BoundaryDelete.backspaceDelete(editor, false)) {
- return;
- } else if (BlockBoundaryDelete.backspaceDelete(editor, false)) {
- return;
- } else if (BlockRangeDelete.backspaceDelete(editor, false)) {
- return;
- } else {
- nativeCommand(editor, 'Delete');
- paddEmptyBody(editor);
- }
- };
-
- var forwardDeleteCommand = function (editor) {
- if (CefDelete.backspaceDelete(editor, true)) {
- return;
- } else if (BoundaryDelete.backspaceDelete(editor, true)) {
- return;
- } else if (BlockBoundaryDelete.backspaceDelete(editor, true)) {
- return;
- } else if (BlockRangeDelete.backspaceDelete(editor, true)) {
- return;
- } else {
- nativeCommand(editor, 'ForwardDelete');
- }
- };
-
- return {
- deleteCommand: deleteCommand,
- forwardDeleteCommand: forwardDeleteCommand
- };
- }
-);
-/**
- * RangeNormalizer.js
- *
- * Released under LGPL License.
- * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
-
-define(
- 'tinymce.core.dom.RangeNormalizer',
- [
- 'tinymce.core.caret.CaretFinder',
- 'tinymce.core.caret.CaretPosition',
- 'tinymce.core.caret.CaretUtils',
- 'tinymce.core.dom.NodeType'
- ],
- function (CaretFinder, CaretPosition, CaretUtils, NodeType) {
- var isTextBlock = function (elm) {
- return NodeType.isElement(elm) && /^(P|H[1-6]|DIV)$/.test(elm.nodeName);
- };
-
- var matchEndContainer = function (rng, predicate) {
- return predicate(rng.endContainer);
- };
-
- var createRange = function (sc, so, ec, eo) {
- var rng = document.createRange();
- rng.setStart(sc, so);
- rng.setEnd(ec, eo);
- return rng;
- };
-
- // If you tripple click a paragraph in this case:
- // a
b
- // It would become this range in webkit: - //[a
]b
- // We would want it to be: - //[a]
b
- // Since it would otherwise produces spans out of thin air on insertContent for example. - var normalizeBlockSelection = function (rng) { - var startPos = CaretPosition.fromRangeStart(rng); - var endPos = CaretPosition.fromRangeEnd(rng); - var rootNode = rng.commonAncestorContainer; - - if (rng.collapsed === false && matchEndContainer(rng, isTextBlock) && rng.endOffset === 0) { - return CaretFinder.fromPosition(false, rootNode, endPos) - .map(function (newEndPos) { - if (!CaretUtils.isInSameBlock(startPos, endPos, rootNode) && CaretUtils.isInSameBlock(startPos, newEndPos, rootNode)) { - return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset()); - } else { - return rng; - } - }).getOr(rng); - } else { - return rng; - } - }; - - var normalize = function (rng) { - return normalizeBlockSelection(rng); - }; - - return { - normalize: normalize - }; - } -); -/** - * InsertList.js + * DomUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -28721,1765 +21119,131 @@ define( */ /** - * Handles inserts of lists into the editor instance. + * Private UI DomUtils proxy. * - * @class tinymce.InsertList * @private + * @class tinymce.ui.DomUtils */ define( - 'tinymce.core.InsertList', + 'tinymce.core.ui.DomUtils', [ + "tinymce.core.Env", "tinymce.core.util.Tools", - "tinymce.core.caret.CaretWalker", - "tinymce.core.caret.CaretPosition" + "tinymce.core.dom.DOMUtils" ], - function (Tools, CaretWalker, CaretPosition) { - var isListFragment = function (fragment) { - var firstChild = fragment.firstChild; - var lastChild = fragment.lastChild; + function (Env, Tools, DOMUtils) { + "use strict"; - // Skip meta since it's likelybug on IE 8 #6178 + DOMUtils.DOM.setHTML(elm, html); } - - // Check for whitespace before/after value - if (/^ | $/.test(value)) { - value = trimOrPaddLeftRight(value); - } - - // Setup parser and serializer - parser = editor.parser; - merge = details.merge; - - serializer = new Serializer({ - validate: editor.settings.validate - }, editor.schema); - bookmarkHtml = ''; - - // Run beforeSetContent handlers on the HTML to be inserted - args = { content: value, format: 'html', selection: true }; - editor.fire('BeforeSetContent', args); - value = args.content; - - // Add caret at end of contents if it's missing - if (value.indexOf('{$caret}') == -1) { - value += '{$caret}'; - } - - // Replace the caret marker with a span bookmark element - value = value.replace(/\{\$caret\}/, bookmarkHtml); - - // If selection is at
| then move it into|
- rng = selection.getRng(); - var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); - var body = editor.getBody(); - if (caretElement === body && selection.isCollapsed()) { - if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) { - rng = dom.createRng(); - rng.setStart(body.firstChild, 0); - rng.setEnd(body.firstChild, 0); - selection.setRng(rng); - } - } - - // Insert node maker where we will insert the new HTML and get it's parent - if (!selection.isCollapsed()) { - // Fix for #2595 seems that delete removes one extra character on - // WebKit for some odd reason if you double click select a word - editor.selection.setRng(RangeNormalizer.normalize(editor.selection.getRng())); - editor.getDoc().execCommand('Delete', false, null); - trimNbspAfterDeleteAndPaddValue(); - } - - parentNode = selection.getNode(); - - // Parse the fragment within the context of the parent node - var parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data }; - fragment = parser.parse(value, parserArgs); - - // Custom handling of lists - if (details.paste === true && InsertList.isListFragment(fragment) && InsertList.isParentBlockLi(dom, parentNode)) { - rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment); - editor.selection.setRng(rng); - editor.fire('SetContent', args); - return; - } - - markFragmentElements(fragment); - - // Move the caret to a more suitable location - node = fragment.lastChild; - if (node.attr('id') == 'mce_marker') { - marker = node; - - for (node = node.prev; node; node = node.walk(true)) { - if (node.type == 3 || !dom.isBlock(node.name)) { - if (editor.schema.isValidChild(node.parent.name, 'span')) { - node.parent.insert(marker, node, node.name === 'br'); - } - break; - } - } - } - - editor._selectionOverrides.showBlockCaretContainer(parentNode); - - // If parser says valid we can insert the contents into that parent - if (!parserArgs.invalid) { - value = serializer.serialize(fragment); - validInsertion(editor, value, parentNode); - } else { - // If the fragment was invalid within that context then we need - // to parse and process the parent it's inserted into - - // Insert bookmark node and get the parent - selection.setContent(bookmarkHtml); - parentNode = selection.getNode(); - rootNode = editor.getBody(); - - // Opera will return the document node when selection is in root - if (parentNode.nodeType == 9) { - parentNode = node = rootNode; - } else { - node = parentNode; - } - - // Find the ancestor just before the root element - while (node !== rootNode) { - parentNode = node; - node = node.parentNode; - } - - // Get the outer/inner HTML depending on if we are in the root and parser and serialize that - value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); - value = serializer.serialize( - parser.parse( - // Need to replace by using a function since $ in the contents would otherwise be a problem - value.replace(//i, function () { - return serializer.serialize(fragment); - }) - ) - ); - - // Set the inner/outer HTML depending on if we are in the root or not - if (parentNode == rootNode) { - dom.setHTML(rootNode, value); - } else { - dom.setOuterHTML(parentNode, value); - } - } - - reduceInlineTextElements(); - moveSelectionToMarker(dom.get('mce_marker')); - umarkFragmentElements(editor.getBody()); - editor.fire('SetContent', args); - editor.addVisual(); }; - var processValue = function (value) { - var details; - - if (typeof value !== 'string') { - details = Tools.extend({ - paste: value.paste, - data: { - paste: value.paste - } - }, value); - - return { - content: value.content, - details: details - }; - } - - return { - content: value, - details: {} - }; - }; - - var insertAtCaret = function (editor, value) { - var result = processValue(value); - insertHtmlAtCaret(editor, result.content, result.details); - }; - - return { - insertAtCaret: insertAtCaret - }; + return funcs; } ); -/** - * EditorCommands.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class enables you to add custom editor commands and it contains - * overrides for native browser commands to address various bugs and issues. - * - * @class tinymce.EditorCommands - */ -define( - 'tinymce.core.EditorCommands', - [ - 'tinymce.core.delete.DeleteCommands', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangeUtils', - 'tinymce.core.dom.TreeWalker', - 'tinymce.core.Env', - 'tinymce.core.InsertContent', - 'tinymce.core.util.Tools' - ], - function (DeleteCommands, NodeType, RangeUtils, TreeWalker, Env, InsertContent, Tools) { - // Added for compression purposes - var each = Tools.each, extend = Tools.extend; - var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; - var isOldIE = Env.ie && Env.ie < 11; - var TRUE = true, FALSE = false; - - return function (editor) { - var dom, selection, formatter, - commands = { state: {}, exec: {}, value: {} }, - settings = editor.settings, - bookmark; - - editor.on('PreInit', function () { - dom = editor.dom; - selection = editor.selection; - settings = editor.settings; - formatter = editor.formatter; - }); - - /** - * Executes the specified command. - * - * @method execCommand - * @param {String} command Command to execute. - * @param {Boolean} ui Optional user interface state. - * @param {Object} value Optional value for command. - * @param {Object} args Optional extra arguments to the execCommand. - * @return {Boolean} true/false if the command was found or not. - */ - function execCommand(command, ui, value, args) { - var func, customCommand, state = 0; - - if (editor.removed) { - return; - } - - if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { - editor.focus(); - } - - args = editor.fire('BeforeExecCommand', { command: command, ui: ui, value: value }); - if (args.isDefaultPrevented()) { - return false; - } - - customCommand = command.toLowerCase(); - if ((func = commands.exec[customCommand])) { - func(customCommand, ui, value); - editor.fire('ExecCommand', { command: command, ui: ui, value: value }); - return true; - } - - // Plugin commands - each(editor.plugins, function (p) { - if (p.execCommand && p.execCommand(command, ui, value)) { - editor.fire('ExecCommand', { command: command, ui: ui, value: value }); - state = true; - return false; - } - }); - - if (state) { - return state; - } - - // Theme commands - if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) { - editor.fire('ExecCommand', { command: command, ui: ui, value: value }); - return true; - } - - // Browser commands - try { - state = editor.getDoc().execCommand(command, ui, value); - } catch (ex) { - // Ignore old IE errors - } - - if (state) { - editor.fire('ExecCommand', { command: command, ui: ui, value: value }); - return true; - } - - return false; - } - - /** - * Queries the current state for a command for example if the current selection is "bold". - * - * @method queryCommandState - * @param {String} command Command to check the state of. - * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. - */ - function queryCommandState(command) { - var func; - - if (editor.quirks.isHidden() || editor.removed) { - return; - } - - command = command.toLowerCase(); - if ((func = commands.state[command])) { - return func(command); - } - - // Browser commands - try { - return editor.getDoc().queryCommandState(command); - } catch (ex) { - // Fails sometimes see bug: 1896577 - } - - return false; - } - - /** - * Queries the command value for example the current fontsize. - * - * @method queryCommandValue - * @param {String} command Command to check the value of. - * @return {Object} Command value of false if it's not found. - */ - function queryCommandValue(command) { - var func; - - if (editor.quirks.isHidden() || editor.removed) { - return; - } - - command = command.toLowerCase(); - if ((func = commands.value[command])) { - return func(command); - } - - // Browser commands - try { - return editor.getDoc().queryCommandValue(command); - } catch (ex) { - // Fails sometimes see bug: 1896577 - } - } - - /** - * Adds commands to the command collection. - * - * @method addCommands - * @param {Object} commandList Name/value collection with commands to add, the names can also be comma separated. - * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. - */ - function addCommands(commandList, type) { - type = type || 'exec'; - - each(commandList, function (callback, command) { - each(command.toLowerCase().split(','), function (command) { - commands[type][command] = callback; - }); - }); - } - - function addCommand(command, callback, scope) { - command = command.toLowerCase(); - commands.exec[command] = function (command, ui, value, args) { - return callback.call(scope || editor, ui, value, args); - }; - } - - /** - * Returns true/false if the command is supported or not. - * - * @method queryCommandSupported - * @param {String} command Command that we check support for. - * @return {Boolean} true/false if the command is supported or not. - */ - function queryCommandSupported(command) { - command = command.toLowerCase(); - - if (commands.exec[command]) { - return true; - } - - // Browser commands - try { - return editor.getDoc().queryCommandSupported(command); - } catch (ex) { - // Fails sometimes see bug: 1896577 - } - - return false; - } - - function addQueryStateHandler(command, callback, scope) { - command = command.toLowerCase(); - commands.state[command] = function () { - return callback.call(scope || editor); - }; - } - - function addQueryValueHandler(command, callback, scope) { - command = command.toLowerCase(); - commands.value[command] = function () { - return callback.call(scope || editor); - }; - } - - function hasCustomCommand(command) { - command = command.toLowerCase(); - return !!commands.exec[command]; - } - - // Expose public methods - extend(this, { - execCommand: execCommand, - queryCommandState: queryCommandState, - queryCommandValue: queryCommandValue, - queryCommandSupported: queryCommandSupported, - addCommands: addCommands, - addCommand: addCommand, - addQueryStateHandler: addQueryStateHandler, - addQueryValueHandler: addQueryValueHandler, - hasCustomCommand: hasCustomCommand - }); - - // Private methods - - function execNativeCommand(command, ui, value) { - if (ui === undefined) { - ui = FALSE; - } - - if (value === undefined) { - value = null; - } - - return editor.getDoc().execCommand(command, ui, value); - } - - function isFormatMatch(name) { - return formatter.match(name); - } - - function toggleFormat(name, value) { - formatter.toggle(name, value ? { value: value } : undefined); - editor.nodeChanged(); - } - - function storeSelection(type) { - bookmark = selection.getBookmark(type); - } - - function restoreSelection() { - selection.moveToBookmark(bookmark); - } - - // Add execCommand overrides - addCommands({ - // Ignore these, added for compatibility - 'mceResetDesignMode,mceBeginUndoLevel': function () { }, - - // Add undo manager logic - 'mceEndUndoLevel,mceAddUndoLevel': function () { - editor.undoManager.add(); - }, - - 'Cut,Copy,Paste': function (command) { - var doc = editor.getDoc(), failed; - - // Try executing the native command - try { - execNativeCommand(command); - } catch (ex) { - // Command failed - failed = TRUE; - } - - // Chrome reports the paste command as supported however older IE:s will return false for cut/paste - if (command === 'paste' && !doc.queryCommandEnabled(command)) { - failed = true; - } - - // Present alert message about clipboard access not being available - if (failed || !doc.queryCommandSupported(command)) { - var msg = editor.translate( - "Your browser doesn't support direct access to the clipboard. " + - "Please use the Ctrl+X/C/V keyboard shortcuts instead." - ); - - if (Env.mac) { - msg = msg.replace(/Ctrl\+/g, '\u2318+'); - } - - editor.notificationManager.open({ text: msg, type: 'error' }); - } - }, - - // Override unlink command - unlink: function () { - if (selection.isCollapsed()) { - var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); - if (elm) { - editor.dom.remove(elm, true); - } - - return; - } - - formatter.remove("link"); - }, - - // Override justify commands to use the text formatter engine - 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function (command) { - var align = command.substring(7); - - if (align == 'full') { - align = 'justify'; - } - - // Remove all other alignments first - each('left,center,right,justify'.split(','), function (name) { - if (align != name) { - formatter.remove('align' + name); - } - }); - - if (align != 'none') { - toggleFormat('align' + align); - } - }, - - // Override list commands to fix WebKit bug - 'InsertUnorderedList,InsertOrderedList': function (command) { - var listElm, listParent; - - execNativeCommand(command); - - // WebKit produces lists within block elements so we need to split them - // we will replace the native list creation logic to custom logic later on - // TODO: Remove this when the list creation logic is removed - listElm = dom.getParent(selection.getNode(), 'ol,ul'); - if (listElm) { - listParent = listElm.parentNode; - - // If list is within a text block then split that block - if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { - storeSelection(); - dom.split(listParent, listElm); - restoreSelection(); - } - } - }, - - // Override commands to use the text formatter engine - 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { - toggleFormat(command); - }, - - // Override commands to use the text formatter engine - 'ForeColor,HiliteColor,FontName': function (command, ui, value) { - toggleFormat(command, value); - }, - - FontSize: function (command, ui, value) { - var fontClasses, fontSizes; - - // Convert font size 1-7 to styles - if (value >= 1 && value <= 7) { - fontSizes = explode(settings.font_size_style_values); - fontClasses = explode(settings.font_size_classes); - - if (fontClasses) { - value = fontClasses[value - 1] || value; - } else { - value = fontSizes[value - 1] || value; - } - } - - toggleFormat(command, value); - }, - - RemoveFormat: function (command) { - formatter.remove(command); - }, - - mceBlockQuote: function () { - toggleFormat('blockquote'); - }, - - FormatBlock: function (command, ui, value) { - return toggleFormat(value || 'p'); - }, - - mceCleanup: function () { - var bookmark = selection.getBookmark(); - - editor.setContent(editor.getContent({ cleanup: TRUE }), { cleanup: TRUE }); - - selection.moveToBookmark(bookmark); - }, - - mceRemoveNode: function (command, ui, value) { - var node = value || selection.getNode(); - - // Make sure that the body node isn't removed - if (node != editor.getBody()) { - storeSelection(); - editor.dom.remove(node, TRUE); - restoreSelection(); - } - }, - - mceSelectNodeDepth: function (command, ui, value) { - var counter = 0; - - dom.getParent(selection.getNode(), function (node) { - if (node.nodeType == 1 && counter++ == value) { - selection.select(node); - return FALSE; - } - }, editor.getBody()); - }, - - mceSelectNode: function (command, ui, value) { - selection.select(value); - }, - - mceInsertContent: function (command, ui, value) { - InsertContent.insertAtCaret(editor, value); - }, - - mceInsertRawHTML: function (command, ui, value) { - selection.setContent('tiny_mce_marker'); - editor.setContent( - editor.getContent().replace(/tiny_mce_marker/g, function () { - return value; - }) - ); - }, - - mceToggleFormat: function (command, ui, value) { - toggleFormat(value); - }, - - mceSetContent: function (command, ui, value) { - editor.setContent(value); - }, - - 'Indent,Outdent': function (command) { - var intentValue, indentUnit, value; - - // Setup indent level - intentValue = settings.indentation; - indentUnit = /[a-z%]+$/i.exec(intentValue); - intentValue = parseInt(intentValue, 10); - - if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { - // If forced_root_blocks is set to false we don't have a block to indent so lets create a div - if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { - formatter.apply('div'); - } - - each(selection.getSelectedBlocks(), function (element) { - if (dom.getContentEditable(element) === "false") { - return; - } - - if (element.nodeName !== "LI") { - var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; - indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName; - indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; - - if (command == 'outdent') { - value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); - dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); - } else { - value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; - dom.setStyle(element, indentStyleName, value); - } - } - }); - } else { - execNativeCommand(command); - } - }, - - mceRepaint: function () { - }, - - InsertHorizontalRule: function () { - editor.execCommand('mceInsertContent', false, '|
- rng = selection.getRng(); - if (!rng.item) { - rng.moveToElementText(root); - rng.select(); - } - } - }, - - "delete": function () { - DeleteCommands.deleteCommand(editor); - }, - - "forwardDelete": function () { - DeleteCommands.forwardDeleteCommand(editor); - }, - - mceNewDocument: function () { - editor.setContent(''); - }, - - InsertLineBreak: function (command, ui, value) { - // We load the current event in from EnterKey.js when appropriate to heed - // certain event-specific variations such as ctrl-enter in a list - var evt = value; - var brElm, extraBr, marker; - var rng = selection.getRng(true); - new RangeUtils(dom).normalize(rng); - - var offset = rng.startOffset; - var container = rng.startContainer; - - // Resolve node index - if (container.nodeType == 1 && container.hasChildNodes()) { - var isAfterLastNodeInContainer = offset > container.childNodes.length - 1; - - container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; - if (isAfterLastNodeInContainer && container.nodeType == 3) { - offset = container.nodeValue.length; - } else { - offset = 0; - } - } - - var parentBlock = dom.getParent(container, dom.isBlock); - var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; - var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - - // Enter inside block contained within a LI then split or insert before/after LI - var isControlKey = evt && evt.ctrlKey; - if (containerBlockName == 'LI' && !isControlKey) { - parentBlock = containerBlock; - parentBlockName = containerBlockName; - } - - // Walks the parent block to the right and look for BR elements - function hasRightSideContent() { - var walker = new TreeWalker(container, parentBlock), node; - var nonEmptyElementsMap = editor.schema.getNonEmptyElements(); - - while ((node = walker.next())) { - if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { - return true; - } - } - } - - if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { - // Insert extra BR element at the end block elements - if (!isOldIE && !hasRightSideContent()) { - brElm = dom.create('br'); - rng.insertNode(brElm); - rng.setStartAfter(brElm); - rng.setEndAfter(brElm); - extraBr = true; - } - } - - brElm = dom.create('br'); - rng.insertNode(brElm); - - // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it - var documentMode = dom.doc.documentMode; - if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { - brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); - } - - // Insert temp marker and scroll to that - marker = dom.create('span', {}, ' '); - brElm.parentNode.insertBefore(marker, brElm); - selection.scrollIntoView(marker); - dom.remove(marker); - - if (!extraBr) { - rng.setStartAfter(brElm); - rng.setEndAfter(brElm); - } else { - rng.setStartBefore(brElm); - rng.setEndBefore(brElm); - } - - selection.setRng(rng); - editor.undoManager.add(); - - return TRUE; - } - }); - - // Add queryCommandState overrides - addCommands({ - // Override justify commands - 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function (command) { - var name = 'align' + command.substring(7); - var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); - var matches = map(nodes, function (node) { - return !!formatter.matchNode(node, name); - }); - return inArray(matches, TRUE) !== -1; - }, - - 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { - return isFormatMatch(command); - }, - - mceBlockQuote: function () { - return isFormatMatch('blockquote'); - }, - - Outdent: function () { - var node; - - if (settings.inline_styles) { - if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { - return TRUE; - } - - if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { - return TRUE; - } - } - - return ( - queryCommandState('InsertUnorderedList') || - queryCommandState('InsertOrderedList') || - (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) - ); - }, - - 'InsertUnorderedList,InsertOrderedList': function (command) { - var list = dom.getParent(selection.getNode(), 'ul,ol'); - - return list && - ( - command === 'insertunorderedlist' && list.tagName === 'UL' || - command === 'insertorderedlist' && list.tagName === 'OL' - ); - } - }, 'state'); - - // Add queryCommandValue overrides - addCommands({ - 'FontSize,FontName': function (command) { - var value = 0, parent; - - if ((parent = dom.getParent(selection.getNode(), 'span'))) { - if (command == 'fontsize') { - value = parent.style.fontSize; - } else { - value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); - } - } - - return value; - } - }, 'value'); - - // Add undo manager logic - addCommands({ - Undo: function () { - editor.undoManager.undo(); - }, - - Redo: function () { - editor.undoManager.redo(); - } - }); - }; - } -); - -/** - * URI.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class handles parsing, modification and serialization of URI/URL strings. - * @class tinymce.util.URI - */ -define( - 'tinymce.core.util.URI', - [ - 'global!document', - 'tinymce.core.util.Tools' - ], - function (document, Tools) { - var each = Tools.each, trim = Tools.trim; - var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' '); - var DEFAULT_PORTS = { - 'ftp': 21, - 'http': 80, - 'https': 443, - 'mailto': 25 - }; - - /** - * Constructs a new URI instance. - * - * @constructor - * @method URI - * @param {String} url URI string to parse. - * @param {Object} settings Optional settings object. - */ - function URI(url, settings) { - var self = this, baseUri, baseUrl; - - url = trim(url); - 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; - return; - } - - var isProtocolRelative = url.indexOf('//') === 0; - - // Absolute path with no host, fake host and protocol - if (url.indexOf('/') === 0 && !isProtocolRelative) { - url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; - } - - // Relative path http:// or protocol relative //path - if (!/^[\w\-]*:?\/\//.test(url)) { - baseUrl = settings.base_uri ? settings.base_uri.path : new URI(document.location.href).directory; - if (settings.base_uri.protocol === "") { - url = '//mce_host' + self.toAbsPath(baseUrl, url); - } else { - url = /([^#?]*)([#?]?.*)/.exec(url); - url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(baseUrl, 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 - - /*jshint maxlen: 255 */ - /*eslint max-len: 0 */ - url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); - - each(queryParts, function (v, i) { - var part = url[i]; - - // Zope 3 workaround, they use @@something - if (part) { - part = part.replace(/\(mce_at\)/g, '@@'); - } - - self[v] = part; - }); - - if (baseUri) { - if (!self.protocol) { - self.protocol = baseUri.protocol; - } - - if (!self.userInfo) { - self.userInfo = baseUri.userInfo; - } - - if (!self.port && self.host === 'mce_host') { - self.port = baseUri.port; - } - - if (!self.host || self.host === 'mce_host') { - self.host = baseUri.host; - } - - self.source = ''; - } - - if (isProtocolRelative) { - self.protocol = ''; - } - - //t.path = t.path || '/'; - } - - URI.prototype = { - /** - * Sets the internal path part of the URI. - * - * @method setPath - * @param {string} path Path string to set. - */ - setPath: function (path) { - var self = this; - - path = /^(.*?)\/?(\w+)?$/.exec(path); - - // Update path parts - self.path = path[0]; - self.directory = path[1]; - self.file = path[2]; - - // Rebuild source - self.source = ''; - self.getURI(); - }, - - /** - * Converts the specified URI into a relative URI based on the current URI instance location. - * - * @method toRelative - * @param {String} uri URI to convert into a relative path/URI. - * @return {String} Relative URI from the point specified in the current URI instance. - * @example - * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm - * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); - */ - toRelative: function (uri) { - var self = this, output; - - if (uri === "./") { - return uri; - } - - uri = new URI(uri, { base_uri: self }); - - // Not on same domain/port or protocol - if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || - (self.protocol != uri.protocol && uri.protocol !== "")) { - return uri.getURI(); - } - - var tu = self.getURI(), uu = uri.getURI(); - - // Allow usage of the base_uri when relative_urls = true - if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { - return tu; - } - - output = self.toRelPath(self.path, uri.path); - - // Add query - if (uri.query) { - output += '?' + uri.query; - } - - // Add anchor - if (uri.anchor) { - output += '#' + uri.anchor; - } - - return output; - }, - - /** - * Converts the specified URI into a absolute URI based on the current URI instance location. - * - * @method toAbsolute - * @param {String} uri URI to convert into a relative path/URI. - * @param {Boolean} noHost No host and protocol prefix. - * @return {String} Absolute URI from the point specified in the current URI instance. - * @example - * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm - * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); - */ - toAbsolute: function (uri, noHost) { - uri = new URI(uri, { base_uri: this }); - - return uri.getURI(noHost && this.isSameOrigin(uri)); - }, - - /** - * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. - * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they - * won't match, if the port specifications differ. - * - * @method isSameOrigin - * @param {tinymce.util.URI} uri Uri instance to compare. - * @returns {Boolean} True if the origins are the same. - */ - isSameOrigin: function (uri) { - if (this.host == uri.host && this.protocol == uri.protocol) { - if (this.port == uri.port) { - return true; - } - - var defaultPort = DEFAULT_PORTS[this.protocol]; - if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { - return true; - } - } - - return false; - }, - - /** - * Converts a absolute path into a relative path. - * - * @method toRelPath - * @param {String} base Base point to convert the path from. - * @param {String} path Absolute path to convert into a relative path. - */ - toRelPath: function (base, path) { - var items, breakPoint = 0, out = '', i, l; - - // Split the paths - base = base.substring(0, base.lastIndexOf('/')); - base = base.split('/'); - items = path.split('/'); - - if (base.length >= items.length) { - for (i = 0, l = base.length; i < l; i++) { - if (i >= items.length || base[i] != items[i]) { - breakPoint = i + 1; - break; - } - } - } - - if (base.length < items.length) { - for (i = 0, l = items.length; i < l; i++) { - if (i >= base.length || base[i] != items[i]) { - breakPoint = i + 1; - break; - } - } - } - - if (breakPoint === 1) { - return path; - } - - for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { - out += "../"; - } - - for (i = breakPoint - 1, l = items.length; i < l; i++) { - if (i != breakPoint - 1) { - out += "/" + items[i]; - } else { - out += items[i]; - } - } - - return out; - }, - - /** - * Converts a relative path into a absolute path. - * - * @method toAbsPath - * @param {String} base Base point to convert the path from. - * @param {String} path Relative path to convert into an absolute path. - */ - toAbsPath: function (base, path) { - var i, nb = 0, o = [], tr, outPath; - - // Split paths - tr = /\/$/.test(path) ? '/' : ''; - base = base.split('/'); - path = path.split('/'); - - // Remove empty chunks - each(base, function (k) { - if (k) { - o.push(k); - } - }); - - base = o; - - // Merge relURLParts chunks - for (i = path.length - 1, o = []; i >= 0; i--) { - // Ignore empty or . - if (path[i].length === 0 || path[i] === ".") { - continue; - } - - // Is parent - if (path[i] === '..') { - nb++; - continue; - } - - // Move up - if (nb > 0) { - nb--; - continue; - } - - o.push(path[i]); - } - - i = base.length - nb; - - // If /a/b/c or / - if (i <= 0) { - outPath = o.reverse().join('/'); - } else { - outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); - } - - // Add front / if it's needed - if (outPath.indexOf('/') !== 0) { - outPath = '/' + outPath; - } - - // Add traling / if it's needed - if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { - outPath += tr; - } - - return outPath; - }, - - /** - * Returns the full URI of the internal structure. - * - * @method getURI - * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. - */ - getURI: function (noProtoHost) { - var s, self = this; - - // Rebuild source - if (!self.source || noProtoHost) { - s = ''; - - if (!noProtoHost) { - if (self.protocol) { - s += self.protocol + '://'; - } else { - s += '//'; - } - - if (self.userInfo) { - s += self.userInfo + '@'; - } - - if (self.host) { - s += self.host; - } - - if (self.port) { - s += ':' + self.port; - } - } - - if (self.path) { - s += self.path; - } - - if (self.query) { - s += '?' + self.query; - } - - if (self.anchor) { - s += '#' + self.anchor; - } - - self.source = s; - } - - return self.source; - } - }; - - URI.parseDataUri = function (uri) { - var type, matches; - - uri = decodeURIComponent(uri).split(','); - - matches = /data:([^;]+)/.exec(uri[0]); - if (matches) { - type = matches[1]; - } - - return { - type: type, - data: uri[1] - }; - }; - - URI.getDocumentBaseUrl = function (loc) { - var baseUrl; - - // Pass applewebdata:// and other non web protocols though - if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') { - baseUrl = loc.href; - } else { - baseUrl = loc.protocol + '//' + loc.host + loc.pathname; - } - - if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) { - baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); - - if (!/[\/\\]$/.test(baseUrl)) { - baseUrl += '/'; - } - } - - return baseUrl; - }; - - return URI; - } -); - /** * Class.js * @@ -30950,6 +21714,90 @@ define( } ); +/** + * Binding.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class gets dynamically extended to provide a binding between two models. This makes it possible to + * sync the state of two properties in two models by a layer of abstraction. + * + * @private + * @class tinymce.data.Binding + */ +define( + 'tinymce.core.data.Binding', + [ + ], + function () { + /** + * Constructs a new bidning. + * + * @constructor + * @method Binding + * @param {Object} settings Settings to the binding. + */ + function Binding(settings) { + this.create = settings.create; + } + + /** + * Creates a binding for a property on a model. + * + * @method create + * @param {tinymce.data.ObservableObject} model Model to create binding to. + * @param {String} name Name of property to bind. + * @return {tinymce.data.Binding} Binding instance. + */ + Binding.create = function (model, name) { + return new Binding({ + create: function (otherModel, otherName) { + var bindings; + + function fromSelfToOther(e) { + otherModel.set(otherName, e.value); + } + + function fromOtherToSelf(e) { + model.set(name, e.value); + } + + otherModel.on('change:' + otherName, fromOtherToSelf); + model.on('change:' + name, fromSelfToOther); + + // Keep track of the bindings + bindings = otherModel._bindings; + + if (!bindings) { + bindings = otherModel._bindings = []; + + otherModel.on('destroy', function () { + var i = bindings.length; + + while (i--) { + bindings[i](); + } + }); + } + + bindings.push(function () { + model.off('change:' + name, fromSelfToOther); + }); + + return model.get(name); + } + }); + }; + + return Binding; + } +); /** * Observable.js * @@ -31087,90 +21935,6 @@ define( }; } ); -/** - * Binding.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class gets dynamically extended to provide a binding between two models. This makes it possible to - * sync the state of two properties in two models by a layer of abstraction. - * - * @private - * @class tinymce.data.Binding - */ -define( - 'tinymce.core.data.Binding', - [ - ], - function () { - /** - * Constructs a new bidning. - * - * @constructor - * @method Binding - * @param {Object} settings Settings to the binding. - */ - function Binding(settings) { - this.create = settings.create; - } - - /** - * Creates a binding for a property on a model. - * - * @method create - * @param {tinymce.data.ObservableObject} model Model to create binding to. - * @param {String} name Name of property to bind. - * @return {tinymce.data.Binding} Binding instance. - */ - Binding.create = function (model, name) { - return new Binding({ - create: function (otherModel, otherName) { - var bindings; - - function fromSelfToOther(e) { - otherModel.set(otherName, e.value); - } - - function fromOtherToSelf(e) { - model.set(name, e.value); - } - - otherModel.on('change:' + otherName, fromOtherToSelf); - model.on('change:' + name, fromSelfToOther); - - // Keep track of the bindings - bindings = otherModel._bindings; - - if (!bindings) { - bindings = otherModel._bindings = []; - - otherModel.on('destroy', function () { - var i = bindings.length; - - while (i--) { - bindings[i](); - } - }); - } - - bindings.push(function () { - model.off('change:' + name, fromSelfToOther); - }); - - return model.get(name); - } - }); - }; - - return Binding; - } -); /** * ObservableObject.js * @@ -32184,142 +22948,6 @@ define( return Collection; } ); -/** - * DomUtils.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * Private UI DomUtils proxy. - * - * @private - * @class tinymce.ui.DomUtils - */ -define( - 'tinymce.core.ui.DomUtils', - [ - "tinymce.core.Env", - "tinymce.core.util.Tools", - "tinymce.core.dom.DOMUtils" - ], - function (Env, Tools, DOMUtils) { - "use strict"; - - var count = 0; - - var funcs = { - id: function () { - return 'mceu_' + (count++); - }, - - create: function (name, attrs, children) { - var elm = document.createElement(name); - - DOMUtils.DOM.setAttribs(elm, attrs); - - if (typeof children === 'string') { - elm.innerHTML = children; - } else { - Tools.each(children, function (child) { - if (child.nodeType) { - elm.appendChild(child); - } - }); - } - - return elm; - }, - - createFragment: function (html) { - return DOMUtils.DOM.createFragment(html); - }, - - getWindowSize: function () { - return DOMUtils.DOM.getViewPort(); - }, - - getSize: function (elm) { - var width, height; - - if (elm.getBoundingClientRect) { - var rect = elm.getBoundingClientRect(); - - width = Math.max(rect.width || (rect.right - rect.left), elm.offsetWidth); - height = Math.max(rect.height || (rect.bottom - rect.bottom), elm.offsetHeight); - } else { - width = elm.offsetWidth; - height = elm.offsetHeight; - } - - return { width: width, height: height }; - }, - - getPos: function (elm, root) { - return DOMUtils.DOM.getPos(elm, root || funcs.getContainer()); - }, - - getContainer: function () { - return Env.container ? Env.container : document.body; - }, - - getViewPort: function (win) { - return DOMUtils.DOM.getViewPort(win); - }, - - get: function (id) { - return document.getElementById(id); - }, - - addClass: function (elm, cls) { - return DOMUtils.DOM.addClass(elm, cls); - }, - - removeClass: function (elm, cls) { - return DOMUtils.DOM.removeClass(elm, cls); - }, - - hasClass: function (elm, cls) { - return DOMUtils.DOM.hasClass(elm, cls); - }, - - toggleClass: function (elm, cls, state) { - return DOMUtils.DOM.toggleClass(elm, cls, state); - }, - - css: function (elm, name, value) { - return DOMUtils.DOM.setStyle(elm, name, value); - }, - - getRuntimeStyle: function (elm, name) { - return DOMUtils.DOM.getStyle(elm, name, true); - }, - - on: function (target, name, callback, scope) { - return DOMUtils.DOM.bind(target, name, callback, scope); - }, - - off: function (target, name, callback) { - return DOMUtils.DOM.unbind(target, name, callback); - }, - - fire: function (target, name, args) { - return DOMUtils.DOM.fire(target, name, args); - }, - - innerHtml: function (elm, html) { - // Workaround forbug on IE 8 #6178 - DOMUtils.DOM.setHTML(elm, html); - } - }; - - return funcs; - } -); /** * BoxUtils.js * @@ -33966,6 +24594,949 @@ define( } ); +/** + * Movable.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Movable mixin. Makes controls movable absolute and relative to other elements. + * + * @mixin tinymce.ui.Movable + */ +define( + 'tinymce.core.ui.Movable', + [ + "tinymce.core.ui.DomUtils" + ], + function (DomUtils) { + "use strict"; + + function calculateRelativePosition(ctrl, targetElm, rel) { + var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size; + + viewport = DomUtils.getViewPort(); + + // Get pos of target + pos = DomUtils.getPos(targetElm); + x = pos.x; + y = pos.y; + + if (ctrl.state.get('fixed') && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') { + x -= viewport.x; + y -= viewport.y; + } + + // Get size of self + ctrlElm = ctrl.getEl(); + size = DomUtils.getSize(ctrlElm); + selfW = size.width; + selfH = size.height; + + // Get size of target + size = DomUtils.getSize(targetElm); + targetW = size.width; + targetH = size.height; + + // Parse align string + rel = (rel || '').split(''); + + // Target corners + if (rel[0] === 'b') { + y += targetH; + } + + if (rel[1] === 'r') { + x += targetW; + } + + if (rel[0] === 'c') { + y += Math.round(targetH / 2); + } + + if (rel[1] === 'c') { + x += Math.round(targetW / 2); + } + + // Self corners + if (rel[3] === 'b') { + y -= selfH; + } + + if (rel[4] === 'r') { + x -= selfW; + } + + if (rel[3] === 'c') { + y -= Math.round(selfH / 2); + } + + if (rel[4] === 'c') { + x -= Math.round(selfW / 2); + } + + return { + x: x, + y: y, + w: selfW, + h: selfH + }; + } + + return { + /** + * Tests various positions to get the most suitable one. + * + * @method testMoveRel + * @param {DOMElement} elm Element to position against. + * @param {Array} rels Array with relative positions. + * @return {String} Best suitable relative position. + */ + testMoveRel: function (elm, rels) { + var viewPortRect = DomUtils.getViewPort(); + + for (var i = 0; i < rels.length; i++) { + var pos = calculateRelativePosition(this, elm, rels[i]); + + if (this.state.get('fixed')) { + if (pos.x > 0 && pos.x + pos.w < viewPortRect.w && pos.y > 0 && pos.y + pos.h < viewPortRect.h) { + return rels[i]; + } + } else { + if (pos.x > viewPortRect.x && pos.x + pos.w < viewPortRect.w + viewPortRect.x && + pos.y > viewPortRect.y && pos.y + pos.h < viewPortRect.h + viewPortRect.y) { + return rels[i]; + } + } + } + + return rels[0]; + }, + + /** + * Move relative to the specified element. + * + * @method moveRel + * @param {Element} elm Element to move relative to. + * @param {String} rel Relative mode. For example: br-tl. + * @return {tinymce.ui.Control} Current control instance. + */ + moveRel: function (elm, rel) { + if (typeof rel != 'string') { + rel = this.testMoveRel(elm, rel); + } + + var pos = calculateRelativePosition(this, elm, rel); + return this.moveTo(pos.x, pos.y); + }, + + /** + * Move by a relative x, y values. + * + * @method moveBy + * @param {Number} dx Relative x position. + * @param {Number} dy Relative y position. + * @return {tinymce.ui.Control} Current control instance. + */ + moveBy: function (dx, dy) { + var self = this, rect = self.layoutRect(); + + self.moveTo(rect.x + dx, rect.y + dy); + + return self; + }, + + /** + * Move to absolute position. + * + * @method moveTo + * @param {Number} x Absolute x position. + * @param {Number} y Absolute y position. + * @return {tinymce.ui.Control} Current control instance. + */ + moveTo: function (x, y) { + var self = this; + + // TODO: Move this to some global class + function constrain(value, max, size) { + if (value < 0) { + return 0; + } + + if (value + size > max) { + value = max - size; + return value < 0 ? 0 : value; + } + + return value; + } + + if (self.settings.constrainToViewport) { + var viewPortRect = DomUtils.getViewPort(window); + var layoutRect = self.layoutRect(); + + x = constrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w); + y = constrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h); + } + + if (self.state.get('rendered')) { + self.layoutRect({ x: x, y: y }).repaint(); + } else { + self.settings.x = x; + self.settings.y = y; + } + + self.fire('move', { x: x, y: y }); + + return self; + } + }; + } +); +/** + * Tooltip.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Creates a tooltip instance. + * + * @-x-less ToolTip.less + * @class tinymce.ui.ToolTip + * @extends tinymce.ui.Control + * @mixes tinymce.ui.Movable + */ +define( + 'tinymce.core.ui.Tooltip', + [ + "tinymce.core.ui.Control", + "tinymce.core.ui.Movable" + ], + function (Control, Movable) { + return Control.extend({ + Mixins: [Movable], + + Defaults: { + classes: 'widget tooltip tooltip-n' + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, prefix = self.classPrefix; + + return ( + '
|
would become this:|
+ sibling = startContainer.previousSibling; + if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { + sibling.innerHTML = ''; + } else { + sibling = null; + } + + startContainer.innerHTML = ''; + ieRng.moveToElementText(startContainer.lastChild); + ieRng.select(); + dom.doc.selection.clear(); + startContainer.innerHTML = ''; + + if (sibling) { + sibling.innerHTML = ''; + } + return; + } + + startOffset = dom.nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } + + if (startOffset == endOffset - 1) { + try { + ctrlElm = startContainer.childNodes[startOffset]; + ctrlRng = body.createControlRange(); + ctrlRng.addElement(ctrlElm); + ctrlRng.select(); + + // Check if the range produced is on the correct element and is a control range + // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 + nativeRng = selection.getRng(); + if (nativeRng.item && ctrlElm === nativeRng.item(0)) { + return; + } + } catch (ex) { + // Ignore + } + } + } + + // Set start/end point of selection + setEndPoint(true); + setEndPoint(); + + // Select the new range and scroll it into view + ieRng.select(); + }; + + // Expose range method + this.getRangeAt = getRange; + } + + return Selection; + } +); + +define( + 'ephox.sugar.api.dom.Replication', + + [ + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.search.Traverse' + ], + + function (Attr, Element, Insert, InsertAll, Remove, Traverse) { + var clone = function (original, deep) { + return Element.fromDom(original.dom().cloneNode(deep)); + }; + + /** Shallow clone - just the tag, no children */ + var shallow = function (original) { + return clone(original, false); + }; + + /** Deep clone - everything copied including children */ + var deep = function (original) { + return clone(original, true); + }; + + /** Shallow clone, with a new tag */ + var shallowAs = function (original, tag) { + var nu = Element.fromTag(tag); + + var attributes = Attr.clone(original); + Attr.setAll(nu, attributes); + + return nu; + }; + + /** Deep clone, with a new tag */ + var copy = function (original, tag) { + var nu = shallowAs(original, tag); + + // NOTE + // previously this used serialisation: + // nu.dom().innerHTML = original.dom().innerHTML; + // + // Clone should be equivalent (and faster), but if TD <-> TH toggle breaks, put it back. + + var cloneChildren = Traverse.children(deep(original)); + InsertAll.append(nu, cloneChildren); + + return nu; + }; + + /** Change the tag name, but keep all children */ + var mutate = function (original, tag) { + var nu = shallowAs(original, tag); + + Insert.before(original, nu); + var children = Traverse.children(original); + InsertAll.append(nu, children); + Remove.remove(original); + return nu; + }; + + return { + shallow: shallow, + shallowAs: shallowAs, + deep: deep, + copy: copy, + mutate: mutate + }; + } +); + +define( + 'ephox.sugar.api.node.Fragment', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'global!document' + ], + + function (Arr, Element, document) { + var fromElements = function (elements, scope) { + var doc = scope || document; + var fragment = doc.createDocumentFragment(); + Arr.each(elements, function (element) { + fragment.appendChild(element.dom()); + }); + return Element.fromDom(fragment); + }; + + return { + fromElements: fromElements + }; + } +); + +define( + 'ephox.sugar.impl.ClosestOrAncestor', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Option' + ], + + function (Type, Option) { + return function (is, ancestor, scope, a, isRoot) { + return is(scope, a) ? + Option.some(scope) : + Type.isFunction(isRoot) && isRoot(scope) ? + Option.none() : + ancestor(scope, a, isRoot); + }; + } +); +define( + 'ephox.sugar.api.search.PredicateFind', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.impl.ClosestOrAncestor' + ], + + function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) { + var first = function (predicate) { + return descendant(Body.body(), predicate); + }; + + var ancestor = function (scope, predicate, isRoot) { + var element = scope.dom(); + var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); + + while (element.parentNode) { + element = element.parentNode; + var el = Element.fromDom(element); + + if (predicate(el)) return Option.some(el); + else if (stop(el)) break; + } + return Option.none(); + }; + + var closest = function (scope, predicate, isRoot) { + // This is required to avoid ClosestOrAncestor passing the predicate to itself + var is = function (scope) { + return predicate(scope); + }; + return ClosestOrAncestor(is, ancestor, scope, predicate, isRoot); + }; + + var sibling = function (scope, predicate) { + var element = scope.dom(); + if (!element.parentNode) return Option.none(); + + return child(Element.fromDom(element.parentNode), function (x) { + return !Compare.eq(scope, x) && predicate(x); + }); + }; + + var child = function (scope, predicate) { + var result = Arr.find(scope.dom().childNodes, + Fun.compose(predicate, Element.fromDom)); + return result.map(Element.fromDom); + }; + + var descendant = function (scope, predicate) { + var descend = function (element) { + for (var i = 0; i < element.childNodes.length; i++) { + if (predicate(Element.fromDom(element.childNodes[i]))) + return Option.some(Element.fromDom(element.childNodes[i])); + + var res = descend(element.childNodes[i]); + if (res.isSome()) + return res; + } + + return Option.none(); + }; + + return descend(scope.dom()); + }; + + return { + first: first, + ancestor: ancestor, + closest: closest, + sibling: sibling, + child: child, + descendant: descendant + }; + } +); + +define( + 'ephox.sugar.api.search.SelectorFind', + + [ + 'ephox.sugar.api.search.PredicateFind', + 'ephox.sugar.api.search.Selectors', + 'ephox.sugar.impl.ClosestOrAncestor' + ], + + function (PredicateFind, Selectors, ClosestOrAncestor) { + // TODO: An internal SelectorFilter module that doesn't Element.fromDom() everything + + var first = function (selector) { + return Selectors.one(selector); + }; + + var ancestor = function (scope, selector, isRoot) { + return PredicateFind.ancestor(scope, function (e) { + return Selectors.is(e, selector); + }, isRoot); + }; + + var sibling = function (scope, selector) { + return PredicateFind.sibling(scope, function (e) { + return Selectors.is(e, selector); + }); + }; + + var child = function (scope, selector) { + return PredicateFind.child(scope, function (e) { + return Selectors.is(e, selector); + }); + }; + + var descendant = function (scope, selector) { + return Selectors.one(selector, scope); + }; + + var closest = function (scope, selector, isRoot) { + return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot); + }; + + return { + first: first, + ancestor: ancestor, + sibling: sibling, + child: child, + descendant: descendant, + closest: closest + }; + } +); + +/** + * Parents.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.dom.Parents', + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.search.Traverse' + ], + function (Fun, Compare, Traverse) { + var dropLast = function (xs) { + return xs.slice(0, -1); + }; + + var parentsUntil = function (startNode, rootElm, predicate) { + if (Compare.contains(rootElm, startNode)) { + return dropLast(Traverse.parents(startNode, function (elm) { + return predicate(elm) || Compare.eq(elm, rootElm); + })); + } else { + return []; + } + }; + + var parents = function (startNode, rootElm) { + return parentsUntil(startNode, rootElm, Fun.constant(false)); + }; + + var parentsAndSelf = function (startNode, rootElm) { + return [startNode].concat(parents(startNode, rootElm)); + }; + + return { + parentsUntil: parentsUntil, + parents: parents, + parentsAndSelf: parentsAndSelf + }; + } +); + +define( + 'ephox.katamari.api.Options', + + [ + 'ephox.katamari.api.Option' + ], + + function (Option) { + /** cat :: [Option a] -> [a] */ + var cat = function (arr) { + var r = []; + var push = function (x) { + r.push(x); + }; + for (var i = 0; i < arr.length; i++) { + arr[i].each(push); + } + return r; + }; + + /** findMap :: ([a], (a, Int -> Option b)) -> Option b */ + var findMap = function (arr, f) { + for (var i = 0; i < arr.length; i++) { + var r = f(arr[i], i); + if (r.isSome()) { + return r; + } + } + return Option.none(); + }; + + /** + * if all elements in arr are 'some', their inner values are passed as arguments to f + * f must have arity arr.length + */ + var liftN = function(arr, f) { + var r = []; + for (var i = 0; i < arr.length; i++) { + var x = arr[i]; + if (x.isSome()) { + r.push(x.getOrDie()); + } else { + return Option.none(); + } + } + return Option.some(f.apply(null, r)); + }; + + return { + cat: cat, + findMap: findMap, + liftN: liftN + }; + } +); + +/** + * SelectionUtils.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.selection.SelectionUtils', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.Traverse', + 'tinymce.core.dom.NodeType' + ], + function (Arr, Fun, Option, Options, Compare, Element, Node, Traverse, NodeType) { + var getStartNode = function (rng) { + var sc = rng.startContainer, so = rng.startOffset; + if (NodeType.isText(sc)) { + return so === 0 ? Option.some(Element.fromDom(sc)) : Option.none(); + } else { + return Option.from(sc.childNodes[so]).map(Element.fromDom); + } + }; + + var getEndNode = function (rng) { + var ec = rng.endContainer, eo = rng.endOffset; + if (NodeType.isText(ec)) { + return eo === ec.data.length ? Option.some(Element.fromDom(ec)) : Option.none(); + } else { + return Option.from(ec.childNodes[eo - 1]).map(Element.fromDom); + } + }; + + var getFirstChildren = function (node) { + return Traverse.firstChild(node).fold( + Fun.constant([node]), + function (child) { + return [node].concat(getFirstChildren(child)); + } + ); + }; + + var getLastChildren = function (node) { + return Traverse.lastChild(node).fold( + Fun.constant([node]), + function (child) { + if (Node.name(child) === 'br') { + return Traverse.prevSibling(child).map(function (sibling) { + return [node].concat(getLastChildren(sibling)); + }).getOr([]); + } else { + return [node].concat(getLastChildren(child)); + } + } + ); + }; + + var hasAllContentsSelected = function (elm, rng) { + return Options.liftN([getStartNode(rng), getEndNode(rng)], function (startNode, endNode) { + var start = Arr.find(getFirstChildren(elm), Fun.curry(Compare.eq, startNode)); + var end = Arr.find(getLastChildren(elm), Fun.curry(Compare.eq, endNode)); + return start.isSome() && end.isSome(); + }).getOr(false); + }; + + return { + hasAllContentsSelected: hasAllContentsSelected + }; + } +); + +/** + * SimpleTableModel.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.selection.SimpleTableModel', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Struct', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.dom.Replication', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.search.SelectorFilter' + ], + function (Arr, Option, Struct, Compare, Insert, InsertAll, Replication, Element, Attr, SelectorFilter) { + var tableModel = Struct.immutable('element', 'width', 'rows'); + var tableRow = Struct.immutable('element', 'cells'); + var cellPosition = Struct.immutable('x', 'y'); + + var getSpan = function (td, key) { + var value = parseInt(Attr.get(td, key), 10); + return isNaN(value) ? 1 : value; + }; + + var fillout = function (table, x, y, tr, td) { + var rowspan = getSpan(td, 'rowspan'); + var colspan = getSpan(td, 'colspan'); + var rows = table.rows(); + + for (var y2 = y; y2 < y + rowspan; y2++) { + if (!rows[y2]) { + rows[y2] = tableRow(Replication.deep(tr), []); + } + + for (var x2 = x; x2 < x + colspan; x2++) { + var cells = rows[y2].cells(); + + // not filler td:s are purposely not cloned so that we can + // find cells in the model by element object references + cells[x2] = y2 == y && x2 == x ? td : Replication.shallow(td); + } + } + }; + + var cellExists = function (table, x, y) { + var rows = table.rows(); + var cells = rows[y] ? rows[y].cells() : []; + return !!cells[x]; + }; + + var skipCellsX = function (table, x, y) { + while (cellExists(table, x, y)) { + x++; + } + + return x; + }; + + var getWidth = function (rows) { + return Arr.foldl(rows, function (acc, row) { + return row.cells().length > acc ? row.cells().length : acc; + }, 0); + }; + + var findElementPos = function (table, element) { + var rows = table.rows(); + for (var y = 0; y < rows.length; y++) { + var cells = rows[y].cells(); + for (var x = 0; x < cells.length; x++) { + if (Compare.eq(cells[x], element)) { + return Option.some(cellPosition(x, y)); + } + } + } + + return Option.none(); + }; + + var extractRows = function (table, sx, sy, ex, ey) { + var newRows = []; + var rows = table.rows(); + + for (var y = sy; y <= ey; y++) { + var cells = rows[y].cells(); + var slice = sx < ex ? cells.slice(sx, ex + 1) : cells.slice(ex, sx + 1); + newRows.push(tableRow(rows[y].element(), slice)); + } + + return newRows; + }; + + var subTable = function (table, startPos, endPos) { + var sx = startPos.x(), sy = startPos.y(); + var ex = endPos.x(), ey = endPos.y(); + var newRows = sy < ey ? extractRows(table, sx, sy, ex, ey) : extractRows(table, sx, ey, ex, sy); + + return tableModel(table.element(), getWidth(newRows), newRows); + }; + + var createDomTable = function (table, rows) { + var tableElement = Replication.shallow(table.element()); + var tableBody = Element.fromTag('tbody'); + + InsertAll.append(tableBody, rows); + Insert.append(tableElement, tableBody); + + return tableElement; + }; + + var modelRowsToDomRows = function (table) { + return Arr.map(table.rows(), function (row) { + var cells = Arr.map(row.cells(), function (cell) { + var td = Replication.deep(cell); + Attr.remove(td, 'colspan'); + Attr.remove(td, 'rowspan'); + return td; + }); + + var tr = Replication.shallow(row.element()); + InsertAll.append(tr, cells); + return tr; + }); + }; + + var fromDom = function (tableElm) { + var table = tableModel(Replication.shallow(tableElm), 0, []); + + Arr.each(SelectorFilter.descendants(tableElm, 'tr'), function (tr, y) { + Arr.each(SelectorFilter.descendants(tr, 'td,th'), function (td, x) { + fillout(table, skipCellsX(table, x, y), y, tr, td); + }); + }); + + return tableModel(table.element(), getWidth(table.rows()), table.rows()); + }; + + var toDom = function (table) { + return createDomTable(table, modelRowsToDomRows(table)); + }; + + var subsection = function (table, startElement, endElement) { + return findElementPos(table, startElement).bind(function (startPos) { + return findElementPos(table, endElement).map(function (endPos) { + return subTable(table, startPos, endPos); + }); + }); + }; + + return { + fromDom: fromDom, + toDom: toDom, + subsection: subsection + }; + } +); + +/** + * MultiRange.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.selection.MultiRange', + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'tinymce.core.dom.RangeUtils' + ], + function (Arr, Element, RangeUtils) { + var getRanges = function (selection) { + var ranges = []; + + for (var i = 0; i < selection.rangeCount; i++) { + ranges.push(selection.getRangeAt(i)); + } + + return ranges; + }; + + var getSelectedNodes = function (ranges) { + return Arr.bind(ranges, function (range) { + var node = RangeUtils.getSelectedNode(range); + return node ? [ Element.fromDom(node) ] : []; + }); + }; + + var hasMultipleRanges = function (selection) { + return getRanges(selection).length > 1; + }; + + return { + getRanges: getRanges, + getSelectedNodes: getSelectedNodes, + hasMultipleRanges: hasMultipleRanges + }; + } +); + +/** + * TableCellSelection.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.selection.TableCellSelection', + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.SelectorFilter', + 'tinymce.core.dom.ElementType', + 'tinymce.core.selection.MultiRange' + ], + function (Arr, Element, SelectorFilter, ElementType, MultiRange) { + var getCellsFromRanges = function (ranges) { + return Arr.filter(MultiRange.getSelectedNodes(ranges), ElementType.isTableCell); + }; + + var getCellsFromElement = function (elm) { + var selectedCells = SelectorFilter.descendants(elm, 'td[data-mce-selected],th[data-mce-selected]'); + return selectedCells; + }; + + var getCellsFromElementOrRanges = function (ranges, element) { + var selectedCells = getCellsFromElement(element); + var rangeCells = getCellsFromRanges(ranges); + return selectedCells.length > 0 ? selectedCells : rangeCells; + }; + + var getCellsFromEditor = function (editor) { + return getCellsFromElementOrRanges(MultiRange.getRanges(editor.selection.getSel()), Element.fromDom(editor.getBody())); + }; + + return { + getCellsFromRanges: getCellsFromRanges, + getCellsFromElement: getCellsFromElement, + getCellsFromElementOrRanges: getCellsFromElementOrRanges, + getCellsFromEditor: getCellsFromEditor + }; + } +); + +/** + * FragmentReader.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.selection.FragmentReader', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.Replication', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Fragment', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.SelectorFind', + 'ephox.sugar.api.search.Traverse', + 'tinymce.core.dom.ElementType', + 'tinymce.core.dom.Parents', + 'tinymce.core.selection.SelectionUtils', + 'tinymce.core.selection.SimpleTableModel', + 'tinymce.core.selection.TableCellSelection' + ], + function (Arr, Fun, Compare, Insert, Replication, Element, Fragment, Node, SelectorFind, Traverse, ElementType, Parents, SelectionUtils, SimpleTableModel, TableCellSelection) { + var findParentListContainer = function (parents) { + return Arr.find(parents, function (elm) { + return Node.name(elm) === 'ul' || Node.name(elm) === 'ol'; + }); + }; + + var getFullySelectedListWrappers = function (parents, rng) { + return Arr.find(parents, function (elm) { + return Node.name(elm) === 'li' && SelectionUtils.hasAllContentsSelected(elm, rng); + }).fold( + Fun.constant([]), + function (li) { + return findParentListContainer(parents).map(function (listCont) { + return [ + Element.fromTag('li'), + Element.fromTag(Node.name(listCont)) + ]; + }).getOr([]); + } + ); + }; + + var wrap = function (innerElm, elms) { + var wrapped = Arr.foldl(elms, function (acc, elm) { + Insert.append(elm, acc); + return elm; + }, innerElm); + return elms.length > 0 ? Fragment.fromElements([wrapped]) : wrapped; + }; + + var directListWrappers = function (commonAnchorContainer) { + if (ElementType.isListItem(commonAnchorContainer)) { + return Traverse.parent(commonAnchorContainer).filter(ElementType.isList).fold( + Fun.constant([]), + function (listElm) { + return [ commonAnchorContainer, listElm ]; + } + ); + } else { + return ElementType.isList(commonAnchorContainer) ? [ commonAnchorContainer ] : [ ]; + } + }; + + var getWrapElements = function (rootNode, rng) { + var commonAnchorContainer = Element.fromDom(rng.commonAncestorContainer); + var parents = Parents.parentsAndSelf(commonAnchorContainer, rootNode); + var wrapElements = Arr.filter(parents, function (elm) { + return ElementType.isInline(elm) || ElementType.isHeading(elm); + }); + var listWrappers = getFullySelectedListWrappers(parents, rng); + var allWrappers = wrapElements.concat(listWrappers.length ? listWrappers : directListWrappers(commonAnchorContainer)); + return Arr.map(allWrappers, Replication.shallow); + }; + + var emptyFragment = function () { + return Fragment.fromElements([]); + }; + + var getFragmentFromRange = function (rootNode, rng) { + return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng)); + }; + + var getParentTable = function (rootElm, cell) { + return SelectorFind.ancestor(cell, 'table', Fun.curry(Compare.eq, rootElm)); + }; + + var getTableFragment = function (rootNode, selectedTableCells) { + return getParentTable(rootNode, selectedTableCells[0]).bind(function (tableElm) { + var firstCell = selectedTableCells[0]; + var lastCell = selectedTableCells[selectedTableCells.length - 1]; + var fullTableModel = SimpleTableModel.fromDom(tableElm); + + return SimpleTableModel.subsection(fullTableModel, firstCell, lastCell).map(function (sectionedTableModel) { + return Fragment.fromElements([SimpleTableModel.toDom(sectionedTableModel)]); + }); + }).getOrThunk(emptyFragment); + }; + + var getSelectionFragment = function (rootNode, ranges) { + return ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0]); + }; + + var read = function (rootNode, ranges) { + var selectedCells = TableCellSelection.getCellsFromElementOrRanges(ranges, rootNode); + return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges); + }; + + return { + read: read + }; + } +); + +/** + * Selection.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles text and control selection it's an crossbrowser utility class. + * Consult the TinyMCE Wiki API for more details and examples on how to use this class. + * + * @class tinymce.dom.Selection + * @example + * // Getting the currently selected node for the active editor + * alert(tinymce.activeEditor.selection.getNode().nodeName); + */ +define( + 'tinymce.core.dom.Selection', + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.BookmarkManager', + 'tinymce.core.dom.ControlSelection', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.dom.ScrollIntoView', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.dom.TridentSelection', + 'tinymce.core.Env', + 'tinymce.core.selection.FragmentReader', + 'tinymce.core.selection.MultiRange', + 'tinymce.core.text.Zwsp', + 'tinymce.core.util.Tools' + ], + function ( + Arr, Compare, Element, CaretPosition, BookmarkManager, ControlSelection, NodeType, RangeUtils, ScrollIntoView, TreeWalker, TridentSelection, Env, FragmentReader, + MultiRange, Zwsp, Tools + ) { + var each = Tools.each, trim = Tools.trim; + var isIE = Env.ie; + + var isAttachedToDom = function (node) { + return !!(node && node.ownerDocument) && Compare.contains(Element.fromDom(node.ownerDocument), Element.fromDom(node)); + }; + + var isValidRange = function (rng) { + if (!rng) { + return false; + } else if (rng.select) { // Native IE range still produced by placeCaretAt + return true; + } else { + return isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer); + } + }; + + var eventProcessRanges = function (editor, ranges) { + return Arr.map(ranges, function (range) { + var evt = editor.fire('GetSelectionRange', { range: range }); + return evt.range !== range ? evt.range : range; + }); + }; + + /** + * Constructs a new selection instance. + * + * @constructor + * @method Selection + * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference. + * @param {Window} win Window to bind the selection object to. + * @param {tinymce.Editor} editor Editor instance of the selection. + * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent. + */ + function Selection(dom, win, serializer, editor) { + var self = this; + + 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); + } + } + + Selection.prototype = { + /** + * Move the selection cursor range to the specified node and offset. + * If there is no node specified it will move it to the first suitable location within the body. + * + * @method setCursorLocation + * @param {Node} node Optional node to put the cursor in. + * @param {Number} offset Optional offset from the start of the node to put the cursor at. + */ + setCursorLocation: function (node, offset) { + var self = this, rng = self.dom.createRng(); + + if (!node) { + self._moveEndPoint(rng, self.editor.getBody(), true); + self.setRng(rng); + } else { + rng.setStart(node, offset); + rng.setEnd(node, offset); + self.setRng(rng); + self.collapse(false); + } + }, + + /** + * Returns the selected contents using the DOM serializer passed in to this class. + * + * @method getContent + * @param {Object} args Optional settings class with for example output format text or html. + * @return {String} Selected contents in for example HTML format. + * @example + * // Alerts the currently selected contents + * alert(tinymce.activeEditor.selection.getContent()); + * + * // Alerts the currently selected contents as plain text + * alert(tinymce.activeEditor.selection.getContent({format: 'text'})); + */ + getContent: function (args) { + var self = this, rng = self.getRng(), tmpElm = self.dom.create("body"); + var se = self.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment; + var ranges = eventProcessRanges(self.editor, MultiRange.getRanges(this.getSel())); + + args = args || {}; + whiteSpaceBefore = whiteSpaceAfter = ''; + args.get = true; + args.format = args.format || 'html'; + args.selection = true; + self.editor.fire('BeforeGetContent', args); + + if (args.format === 'text') { + return self.isCollapsed() ? '' : Zwsp.trim(rng.text || (se.toString ? se.toString() : '')); + } + + if (rng.cloneContents) { + fragment = args.contextual ? FragmentReader.read(Element.fromDom(self.editor.getBody()), ranges).dom() : rng.cloneContents(); + if (fragment) { + tmpElm.appendChild(fragment); + } + } else if (rng.item !== undefined || rng.htmlText !== undefined) { + // IE will produce invalid markup if elements are present that + // it doesn't understand like custom elements or HTML5 elements. + // Adding a BR in front of the contents and then remoiving it seems to fix it though. + tmpElm.innerHTML = 'a
b
c will becomea
b
c
+ * + * @example + * var parser = new tinymce.html.DomParser({validate: true}, schema); + * var rootNode = parser.parse('x
->x
+ function trim(rootBlockNode) { + if (rootBlockNode) { + node = rootBlockNode.firstChild; + if (node && node.type == 3) { + node.value = node.value.replace(startWhiteSpaceRegExp, ''); + } + + node = rootBlockNode.lastChild; + if (node && node.type == 3) { + node.value = node.value.replace(endWhiteSpaceRegExp, ''); + } + } + } + + // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root + if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { + return; + } + + while (node) { + next = node.next; + + if (node.type == 3 || (node.type == 1 && node.name !== 'p' && + !blockElements[node.name] && !node.attr('data-mce-type'))) { + if (!rootBlockNode) { + // Create a new root block element + rootBlockNode = createNode(rootBlockName, 1); + rootBlockNode.attr(settings.forced_root_block_attrs); + rootNode.insert(rootBlockNode, node); + rootBlockNode.append(node); + } else { + rootBlockNode.append(node); + } + } else { + trim(rootBlockNode); + rootBlockNode = null; + } + + node = next; + } + + trim(rootBlockNode); + } + + function createNode(name, type) { + var node = new Node(name, type), list; + + if (name in nodeFilters) { + list = matchedNodes[name]; + + if (list) { + list.push(node); + } else { + matchedNodes[name] = [node]; + } + } + + return node; + } + + function removeWhitespaceBefore(node) { + var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements(); + + for (textNode = node.prev; textNode && textNode.type === 3;) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); + + // Found a text node with non whitespace then trim that and break + if (textVal.length > 0) { + textNode.value = textVal; + return; + } + + textNodeNext = textNode.next; + + // Fix for bug #7543 where bogus nodes would produce empty + // text nodes and these would be removed if a nested list was before it + if (textNodeNext) { + if (textNodeNext.type == 3 && textNodeNext.value.length) { + textNode = textNode.prev; + continue; + } + + if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') { + textNode = textNode.prev; + continue; + } + } + + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } + + function cloneAndExcludeBlocks(input) { + var name, output = {}; + + for (name in input) { + if (name !== 'li' && name != 'p') { + output[name] = input[name]; + } + } + + return output; + } + + parser = new SaxParser({ + validate: validate, + allow_script_urls: settings.allow_script_urls, + allow_conditional_comments: settings.allow_conditional_comments, + + // Exclude P and LI from DOM parsing since it's treated better by the DOM parser + self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), + + cdata: function (text) { + node.append(createNode('#cdata', 4)).value = text; + }, + + text: function (text, raw) { + var textNode; + + // Trim all redundant whitespace on non white space elements + if (!isInWhiteSpacePreservedElement) { + text = text.replace(allWhiteSpaceRegExp, ' '); + + if (node.lastChild && blockElements[node.lastChild.name]) { + text = text.replace(startWhiteSpaceRegExp, ''); + } + } + + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; + } + }, + + comment: function (text) { + node.append(createNode('#comment', 8)).value = text; + }, + + pi: function (name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, + + doctype: function (text) { + var newNode; + + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, + + start: function (name, attrs, empty) { + var newNode, attrFiltersLen, elementRule, attrName, parent; + + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; + + node.append(newNode); + + // Check if node is valid child of the parent node is the child is + // unknown we don't collect it since it's probably a custom element + parent = children[node.name]; + if (parent && children[newNode.name] && !parent[newNode.name]) { + invalidChildren.push(newNode); + } + + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; + + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; + + if (list) { + list.push(newNode); + } else { + matchedAttributes[attrName] = [newNode]; + } + } + } + + // Trim whitespace before block + if (blockElements[name]) { + removeWhitespaceBefore(newNode); + } + + // Change current node if the element wasn't empty i.e nota
+ lastParent = node; + while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { + lastParent = parent; + + if (blockElements[parent.name]) { + break; + } + + parent = parent.parent; + } + + if (lastParent === parent && settings.padd_empty_with_br !== true) { + textNode = new Node('#text', 3); + textNode.value = '\u00a0'; + node.replace(textNode); + } + } + } + }); + } + + + self.addAttributeFilter('href', function (nodes) { + var i = nodes.length, node; + + var appendRel = function (rel) { + var parts = rel.split(' ').filter(function (p) { + return p.length > 0; + }); + return parts.concat(['noopener']).sort().join(' '); + }; + + var addNoOpener = function (rel) { + var newRel = rel ? Tools.trim(rel) : ''; + if (!/\b(noopener)\b/g.test(newRel)) { + return appendRel(newRel); + } else { + return newRel; + } + }; + + if (!settings.allow_unsafe_link_target) { + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.attr('target') === '_blank') { + node.attr('rel', addNoOpener(node.attr('rel'))); + } + } } }); - editor.on('ResizeEditor', positionNotifications); - editor.on('ResizeWindow', resizeWindowEvent); + // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. + if (!settings.allow_html_in_named_anchor) { + self.addAttributeFilter('id,name', function (nodes) { + var i = nodes.length, sibling, prevSibling, parent, node; + + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.firstChild && !node.attr('href')) { + parent = node.parent; + + // Move children after current node + sibling = node.lastChild; + do { + prevSibling = sibling.prev; + parent.insert(sibling, node); + sibling = prevSibling; + } while (sibling); + } + } + }); + } + + if (settings.fix_list_elements) { + self.addNodeFilter('ul,ol', function (nodes) { + var i = nodes.length, node, parentNode; + + while (i--) { + node = nodes[i]; + parentNode = node.parent; + + if (parentNode.name === 'ul' || parentNode.name === 'ol') { + if (node.prev && node.prev.name === 'li') { + node.prev.append(node); + } else { + var li = new Node('li', 1); + li.attr('style', 'list-style-type: none'); + node.wrap(li); + } + } + } + }); + } + + 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); + } + }); + } + }; + } +); +/** + * Writer.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. + * + * @class tinymce.html.Writer + * @example + * var writer = new tinymce.html.Writer({indent: true}); + * var parser = new tinymce.html.SaxParser(writer).parse('
.
+ *
+ * @method start
+ * @param {String} name Name of the element.
+ * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
+ * @param {Boolean} empty Optional empty state if the tag should end like
.
+ */
+ start: function (name, attrs, empty) {
+ var i, l, attr, value;
+
+ if (indent && indentBefore[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+
+ html.push('<', name);
+
+ if (attrs) {
+ for (i = 0, l = attrs.length; i < l; i++) {
+ attr = attrs[i];
+ html.push(' ', attr.name, '="', encode(attr.value, true), '"');
+ }
+ }
+
+ if (!empty || htmlOutput) {
+ html[html.length] = '>';
+ } else {
+ html[html.length] = ' />';
+ }
+
+ if (empty && indent && indentAfter[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+ },
+
+ /**
+ * Writes the a end element such as
text
')); + * @class tinymce.html.Serializer + * @version 3.4 + */ +define( + 'tinymce.core.html.Serializer', + [ + "tinymce.core.html.Writer", + "tinymce.core.html.Schema" + ], + function (Writer, Schema) { + /** + * Constructs a new Serializer instance. + * + * @constructor + * @method Serializer + * @param {Object} settings Name/value settings object. + * @param {tinymce.html.Schema} schema Schema instance to use. + */ + return function (settings, schema) { + var self = this, writer = new Writer(settings); + + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + + self.schema = schema = schema || new Schema(); + self.writer = writer; /** - * Opens a new notification. + * Serializes the specified node into a string. * - * @method open - * @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc. + * @example + * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('text
')); + * @method serialize + * @param {tinymce.html.Node} node Node instance to serialize. + * @return {String} String with HTML based on DOM tree. */ - self.open = function (args) { - // Never open notification if editor has been removed. + self.serialize = function (node) { + var handlers, validate; + + validate = settings.validate; + + handlers = { + // #text + 3: function (node) { + writer.text(node.value, node.raw); + }, + + // #comment + 8: function (node) { + writer.comment(node.value); + }, + + // Processing instruction + 7: function (node) { + writer.pi(node.name, node.value); + }, + + // Doctype + 10: function (node) { + writer.doctype(node.value); + }, + + // CDATA + 4: function (node) { + writer.cdata(node.value); + }, + + // Document fragment + 11: function (node) { + if ((node = node.firstChild)) { + do { + walk(node); + } while ((node = node.next)); + } + } + }; + + writer.reset(); + + function walk(node) { + var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; + + if (!handler) { + name = node.name; + isEmpty = node.shortEnded; + attrs = node.attributes; + + // Sort attributes + if (validate && attrs && attrs.length > 1) { + sortedAttrs = []; + sortedAttrs.map = {}; + + elementRule = schema.getElementRule(node.name); + if (elementRule) { + for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { + attrName = elementRule.attributesOrder[i]; + + if (attrName in attrs.map) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({ name: attrName, value: attrValue }); + } + } + + for (i = 0, l = attrs.length; i < l; i++) { + attrName = attrs[i].name; + + if (!(attrName in sortedAttrs.map)) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({ name: attrName, value: attrValue }); + } + } + + attrs = sortedAttrs; + } + } + + writer.start(node.name, attrs, isEmpty); + + if (!isEmpty) { + if ((node = node.firstChild)) { + do { + walk(node); + } while ((node = node.next)); + } + + writer.end(name); + } + } else { + handler(node); + } + } + + // Serialize element and treat all non elements as fragments + if (node.type == 1 && !settings.inner) { + walk(node); + } else { + handlers[11](node); + } + + return writer.getContent(); + }; + }; + } +); + +/** + * Serializer.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for + * more details and examples on how to use this class. + * + * @class tinymce.dom.Serializer + */ +define( + 'tinymce.core.dom.Serializer', + [ + "tinymce.core.dom.DOMUtils", + "tinymce.core.html.DomParser", + "tinymce.core.html.SaxParser", + "tinymce.core.html.Entities", + "tinymce.core.html.Serializer", + "tinymce.core.html.Node", + "tinymce.core.html.Schema", + "tinymce.core.Env", + "tinymce.core.util.Tools", + "tinymce.core.text.Zwsp" + ], + function (DOMUtils, DomParser, SaxParser, Entities, Serializer, Node, Schema, Env, Tools, Zwsp) { + var each = Tools.each, trim = Tools.trim; + var DOM = DOMUtils.DOM; + + /** + * IE 11 has a fantastic bug where it will produce two trailing BR elements to iframe bodies when + * the iframe is hidden by display: none on a parent container. The DOM is actually out of sync + * with innerHTML in this case. It's like IE adds shadow DOM BR elements that appears on innerHTML + * but not as the lastChild of the body. So this fix simply removes the last two + * BR elements at the end of the document. + * + * Example of what happens: text becomes text|a when there is only one item left except the zwsp caret container nodes
+ var hasOnlyTwoOrLessPositionsLeft = function (elm) {
+ return Options.liftN([
+ CaretFinder.firstPositionIn(elm),
+ CaretFinder.lastPositionIn(elm)
+ ], function (firstPos, lastPos) {
+ var normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos);
+ var normalizedLastPos = InlineUtils.normalizePosition(false, lastPos);
+
+ return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) {
+ return pos.isEqual(normalizedLastPos);
+ }).getOr(true);
+ }).getOr(true);
+ };
+
+ var setCaretLocation = function (editor, caret) {
+ return function (location) {
+ return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
+ BoundarySelection.setCaretPosition(editor, pos);
+ return true;
+ }).getOr(false);
+ };
+ };
+
+ var deleteFromTo = function (editor, caret, from, to) {
+ var rootNode = editor.getBody();
+ var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
+
+ editor.undoManager.ignore(function () {
+ editor.selection.setRng(rangeFromPositions(from, to));
+ editor.execCommand('Delete');
+
+ BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
+ .map(BoundaryLocation.inside)
+ .map(setCaretLocation(editor, caret));
+ });
+
+ editor.nodeChanged();
+ };
+
+ var rescope = function (rootNode, node) {
+ var parentBlock = CaretUtils.getParentBlock(node, rootNode);
+ return parentBlock ? parentBlock : rootNode;
+ };
+
+ var backspaceDeleteCollapsed = function (editor, caret, forward, from) {
+ var rootNode = rescope(editor.getBody(), from.container());
+ var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
+ var fromLocation = BoundaryLocation.readLocation(isInlineTarget, rootNode, from);
+
+ return fromLocation.bind(function (location) {
+ if (forward) {
+ return location.fold(
+ Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before
+ Option.none, // Start
+ Fun.constant(Option.some(BoundaryLocation.outside(location))), // End
+ Option.none // After
+ );
+ } else {
+ return location.fold(
+ Option.none, // Before
+ Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start
+ Option.none, // End
+ Fun.constant(Option.some(BoundaryLocation.inside(location))) // After
+ );
+ }
+ })
+ .map(setCaretLocation(editor, caret))
+ .getOrThunk(function () {
+ var toPosition = CaretFinder.navigate(forward, rootNode, from);
+ var toLocation = toPosition.bind(function (pos) {
+ return BoundaryLocation.readLocation(isInlineTarget, rootNode, pos);
+ });
+
+ if (fromLocation.isSome() && toLocation.isSome()) {
+ return InlineUtils.findRootInline(isInlineTarget, rootNode, from).map(function (elm) {
+ if (hasOnlyTwoOrLessPositionsLeft(elm)) {
+ DeleteElement.deleteElement(editor, forward, Element.fromDom(elm));
+ return true;
+ } else {
+ return false;
+ }
+ }).getOr(false);
+ } else {
+ return toLocation.bind(function (_) {
+ return toPosition.map(function (to) {
+ if (forward) {
+ deleteFromTo(editor, caret, from, to);
+ } else {
+ deleteFromTo(editor, caret, to, from);
+ }
+
+ return true;
+ });
+ }).getOr(false);
+ }
+ });
+ };
+
+ var backspaceDelete = function (editor, caret, forward) {
+ if (editor.selection.isCollapsed() && isFeatureEnabled(editor)) {
+ var from = CaretPosition.fromRangeStart(editor.selection.getRng());
+ return backspaceDeleteCollapsed(editor, caret, forward, from);
+ }
+
+ return false;
+ };
+
+ return {
+ backspaceDelete: backspaceDelete
+ };
+ }
+);
+define(
+ 'tinymce.core.delete.TableDeleteAction',
+
+ [
+ 'ephox.katamari.api.Adt',
+ 'ephox.katamari.api.Arr',
+ 'ephox.katamari.api.Fun',
+ 'ephox.katamari.api.Option',
+ 'ephox.katamari.api.Options',
+ 'ephox.katamari.api.Struct',
+ 'ephox.sugar.api.dom.Compare',
+ 'ephox.sugar.api.node.Element',
+ 'ephox.sugar.api.search.SelectorFilter',
+ 'ephox.sugar.api.search.SelectorFind'
+ ],
+
+ function (Adt, Arr, Fun, Option, Options, Struct, Compare, Element, SelectorFilter, SelectorFind) {
+ var tableCellRng = Struct.immutable('start', 'end');
+ var tableSelection = Struct.immutable('rng', 'table', 'cells');
+ var deleteAction = Adt.generate([
+ { removeTable: [ 'element' ] },
+ { emptyCells: [ 'cells' ] }
+ ]);
+
+ var getClosestCell = function (container, isRoot) {
+ return SelectorFind.closest(Element.fromDom(container), 'td,th', isRoot);
+ };
+
+ var getClosestTable = function (cell, isRoot) {
+ return SelectorFind.ancestor(cell, 'table', isRoot);
+ };
+
+ var isExpandedCellRng = function (cellRng) {
+ return Compare.eq(cellRng.start(), cellRng.end()) === false;
+ };
+
+ var getTableFromCellRng = function (cellRng, isRoot) {
+ return getClosestTable(cellRng.start(), isRoot)
+ .bind(function (startParentTable) {
+ return getClosestTable(cellRng.end(), isRoot)
+ .bind(function (endParentTable) {
+ return Compare.eq(startParentTable, endParentTable) ? Option.some(startParentTable) : Option.none();
+ });
+ });
+ };
+
+ var getCellRng = function (rng, isRoot) {
+ return Options.liftN([ // get start and end cell
+ getClosestCell(rng.startContainer, isRoot),
+ getClosestCell(rng.endContainer, isRoot)
+ ], tableCellRng)
+ .filter(isExpandedCellRng);
+ };
+
+ var getTableSelectionFromCellRng = function (cellRng, isRoot) {
+ return getTableFromCellRng(cellRng, isRoot)
+ .bind(function (table) {
+ var cells = SelectorFilter.descendants(table, 'td,th');
+
+ return tableSelection(cellRng, table, cells);
+ });
+ };
+
+ var getTableSelectionFromRng = function (rootNode, rng) {
+ var isRoot = Fun.curry(Compare.eq, rootNode);
+
+ return getCellRng(rng, isRoot)
+ .map(function (cellRng) {
+ return getTableSelectionFromCellRng(cellRng, isRoot);
+ });
+ };
+
+ var getCellIndex = function (cellArray, cell) {
+ return Arr.findIndex(cellArray, function (x) {
+ return Compare.eq(x, cell);
+ });
+ };
+
+ var getSelectedCells = function (tableSelection) {
+ return Options.liftN([
+ getCellIndex(tableSelection.cells(), tableSelection.rng().start()),
+ getCellIndex(tableSelection.cells(), tableSelection.rng().end())
+ ], function (startIndex, endIndex) {
+ return tableSelection.cells().slice(startIndex, endIndex + 1);
+ });
+ };
+
+ var getAction = function (tableSelection) {
+ return getSelectedCells(tableSelection)
+ .bind(function (selected) {
+ var cells = tableSelection.cells();
+
+ return selected.length === cells.length ? deleteAction.removeTable(tableSelection.table()) : deleteAction.emptyCells(selected);
+ });
+ };
+
+ var getActionFromCells = function (cells) {
+ return deleteAction.emptyCells(cells);
+ };
+
+ var getActionFromRange = function (rootNode, rng) {
+ return getTableSelectionFromRng(rootNode, rng)
+ .map(getAction);
+ };
+
+ return {
+ getActionFromRange: getActionFromRange,
+ getActionFromCells: getActionFromCells
+ };
+ }
+);
+
+/**
+ * TableDelete.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define(
+ 'tinymce.core.delete.TableDelete',
+ [
+ 'ephox.katamari.api.Arr',
+ 'ephox.katamari.api.Fun',
+ 'ephox.sugar.api.node.Element',
+ 'tinymce.core.delete.DeleteElement',
+ 'tinymce.core.delete.TableDeleteAction',
+ 'tinymce.core.dom.PaddingBr',
+ 'tinymce.core.selection.TableCellSelection'
+ ],
+ function (Arr, Fun, Element, DeleteElement, TableDeleteAction, PaddingBr, TableCellSelection) {
+ var emptyCells = function (editor, cells) {
+ Arr.each(cells, PaddingBr.fillWithPaddingBr);
+ editor.selection.setCursorLocation(cells[0].dom(), 0);
+
+ return true;
+ };
+
+ var deleteTableElement = function (editor, table) {
+ DeleteElement.deleteElement(editor, false, table);
+
+ return true;
+ };
+
+ var handleCellRange = function (editor, rootNode, rng) {
+ return TableDeleteAction.getActionFromRange(rootNode, rng)
+ .map(function (action) {
+ return action.fold(
+ Fun.curry(deleteTableElement, editor),
+ Fun.curry(emptyCells, editor)
+ );
+ }).getOr(false);
+ };
+
+ var deleteRange = function (editor) {
+ var rootNode = Element.fromDom(editor.getBody());
+ var rng = editor.selection.getRng();
+ var selectedCells = TableCellSelection.getCellsFromEditor(editor);
+
+ return selectedCells.length !== 0 ? emptyCells(editor, selectedCells) : handleCellRange(editor, rootNode, rng);
+ };
+
+ var backspaceDelete = function (editor) {
+ return editor.selection.isCollapsed() ? false : deleteRange(editor);
+ };
+
+ return {
+ backspaceDelete: backspaceDelete
+ };
+ }
+);
+
+/**
+ * Commands.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+define(
+ 'tinymce.core.delete.DeleteCommands',
+ [
+ 'tinymce.core.delete.BlockBoundaryDelete',
+ 'tinymce.core.delete.BlockRangeDelete',
+ 'tinymce.core.delete.CefDelete',
+ 'tinymce.core.delete.DeleteUtils',
+ 'tinymce.core.delete.InlineBoundaryDelete',
+ 'tinymce.core.delete.TableDelete'
+ ],
+ function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, DeleteUtils, BoundaryDelete, TableDelete) {
+ var nativeCommand = function (editor, command) {
+ editor.getDoc().execCommand(command, false, null);
+ };
+
+ var deleteCommand = function (editor) {
+ if (CefDelete.backspaceDelete(editor, false)) {
+ return;
+ } else if (BoundaryDelete.backspaceDelete(editor, false)) {
+ return;
+ } else if (BlockBoundaryDelete.backspaceDelete(editor, false)) {
+ return;
+ } else if (TableDelete.backspaceDelete(editor)) {
+ return;
+ } else if (BlockRangeDelete.backspaceDelete(editor, false)) {
+ return;
+ } else {
+ nativeCommand(editor, 'Delete');
+ DeleteUtils.paddEmptyBody(editor);
+ }
+ };
+
+ var forwardDeleteCommand = function (editor) {
+ if (CefDelete.backspaceDelete(editor, true)) {
+ return;
+ } else if (BoundaryDelete.backspaceDelete(editor, true)) {
+ return;
+ } else if (BlockBoundaryDelete.backspaceDelete(editor, true)) {
+ return;
+ } else if (TableDelete.backspaceDelete(editor)) {
+ return;
+ } else if (BlockRangeDelete.backspaceDelete(editor, true)) {
+ return;
+ } else {
+ nativeCommand(editor, 'ForwardDelete');
+ }
+ };
+
+ return {
+ deleteCommand: deleteCommand,
+ forwardDeleteCommand: forwardDeleteCommand
+ };
+ }
+);
+/**
+ * InsertList.js
+ *
+ * Released under LGPL License.
+ * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/**
+ * Handles inserts of lists into the editor instance.
+ *
+ * @class tinymce.InsertList
+ * @private
+ */
+define(
+ 'tinymce.core.InsertList',
+ [
+ "tinymce.core.util.Tools",
+ "tinymce.core.caret.CaretWalker",
+ "tinymce.core.caret.CaretPosition"
+ ],
+ function (Tools, CaretWalker, CaretPosition) {
+ var hasOnlyOneChild = function (node) {
+ return node.firstChild && node.firstChild === node.lastChild;
+ };
+
+ var isPaddingNode = function (node) {
+ return node.name === 'br' || node.value === '\u00a0';
+ };
+
+ var isPaddedEmptyBlock = function (schema, node) {
+ var blockElements = schema.getBlockElements();
+ return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild);
+ };
+
+ var isEmptyFragmentElement = function (schema, node) {
+ var nonEmptyElements = schema.getNonEmptyElements();
+ return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node));
+ };
+
+ var isListFragment = function (schema, fragment) {
+ var firstChild = fragment.firstChild;
+ var lastChild = fragment.lastChild;
+
+ // Skip meta since it's likely |
+ rng = selection.getRng(); + var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); + var body = editor.getBody(); + if (caretElement === body && selection.isCollapsed()) { + if (dom.isBlock(body.firstChild) && canHaveChildren(body.firstChild) && dom.isEmpty(body.firstChild)) { + rng = dom.createRng(); + rng.setStart(body.firstChild, 0); + rng.setEnd(body.firstChild, 0); + selection.setRng(rng); + } + } + + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) { + // Fix for #2595 seems that delete removes one extra character on + // WebKit for some odd reason if you double click select a word + editor.selection.setRng(RangeNormalizer.normalize(editor.selection.getRng())); + editor.getDoc().execCommand('Delete', false, null); + trimNbspAfterDeleteAndPaddValue(); + } + + parentNode = selection.getNode(); + + // Parse the fragment within the context of the parent node + var parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data }; + fragment = parser.parse(value, parserArgs); + + // Custom handling of lists + if (details.paste === true && InsertList.isListFragment(editor.schema, fragment) && InsertList.isParentBlockLi(dom, parentNode)) { + rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(true), fragment); + editor.selection.setRng(rng); + editor.fire('SetContent', args); + return; + } + + markFragmentElements(fragment); + + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; + + for (node = node.prev; node; node = node.walk(true)) { + if (node.type == 3 || !dom.isBlock(node.name)) { + if (editor.schema.isValidChild(node.parent.name, 'span')) { + node.parent.insert(marker, node, node.name === 'br'); + } + break; + } + } + } + + editor._selectionOverrides.showBlockCaretContainer(parentNode); + + // If parser says valid we can insert the contents into that parent + if (!parserArgs.invalid) { + value = serializer.serialize(fragment); + validInsertion(editor, value, parentNode); + } else { + // If the fragment was invalid within that context then we need + // to parse and process the parent it's inserted into + + // Insert bookmark node and get the parent + selection.setContent(bookmarkHtml); + parentNode = selection.getNode(); + rootNode = editor.getBody(); + + // Opera will return the document node when selection is in root + if (parentNode.nodeType == 9) { + parentNode = node = rootNode; + } else { + node = parentNode; + } + + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; + } + + // Get the outer/inner HTML depending on if we are in the root and parser and serialize that + value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); + value = serializer.serialize( + parser.parse( + // Need to replace by using a function since $ in the contents would otherwise be a problem + value.replace(//i, function () { + return serializer.serialize(fragment); + }) + ) + ); + + // Set the inner/outer HTML depending on if we are in the root or not + if (parentNode == rootNode) { + dom.setHTML(rootNode, value); + } else { + dom.setOuterHTML(parentNode, value); + } + } + + reduceInlineTextElements(); + moveSelectionToMarker(dom.get('mce_marker')); + umarkFragmentElements(editor.getBody()); + trimBrsFromTableCell(editor.dom, editor.selection.getStart()); + + editor.fire('SetContent', args); + editor.addVisual(); + }; + + var processValue = function (value) { + var details; + + if (typeof value !== 'string') { + details = Tools.extend({ + paste: value.paste, + data: { + paste: value.paste + } + }, value); + + return { + content: value.content, + details: details + }; + } + + return { + content: value, + details: {} + }; + }; + + var insertAtCaret = function (editor, value) { + var result = processValue(value); + insertHtmlAtCaret(editor, result.content, result.details); + }; + + return { + insertAtCaret: insertAtCaret + }; + } +); +/** + * EditorCommands.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class enables you to add custom editor commands and it contains + * overrides for native browser commands to address various bugs and issues. + * + * @class tinymce.EditorCommands + */ +define( + 'tinymce.core.EditorCommands', + [ + 'tinymce.core.delete.DeleteCommands', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.Env', + 'tinymce.core.InsertContent', + 'tinymce.core.util.Tools' + ], + function (DeleteCommands, NodeType, RangeUtils, TreeWalker, Env, InsertContent, Tools) { + // Added for compression purposes + var each = Tools.each, extend = Tools.extend; + var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode; + var isOldIE = Env.ie && Env.ie < 11; + var TRUE = true, FALSE = false; + + return function (editor) { + var dom, selection, formatter, + commands = { state: {}, exec: {}, value: {} }, + settings = editor.settings, + bookmark; + + editor.on('PreInit', function () { + dom = editor.dom; + selection = editor.selection; + settings = editor.settings; + formatter = editor.formatter; + }); + + /** + * Executes the specified command. + * + * @method execCommand + * @param {String} command Command to execute. + * @param {Boolean} ui Optional user interface state. + * @param {Object} value Optional value for command. + * @param {Object} args Optional extra arguments to the execCommand. + * @return {Boolean} true/false if the command was found or not. + */ + function execCommand(command, ui, value, args) { + var func, customCommand, state = 0; + if (editor.removed) { return; } - var notif; + if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { + editor.focus(); + } - editor.editorManager.setActive(editor); + args = editor.fire('BeforeExecCommand', { command: command, ui: ui, value: value }); + if (args.isDefaultPrevented()) { + return false; + } - var duplicate = findDuplicateMessage(notifications, args); + customCommand = command.toLowerCase(); + if ((func = commands.exec[customCommand])) { + func(customCommand, ui, value); + editor.fire('ExecCommand', { command: command, ui: ui, value: value }); + return true; + } - if (duplicate === null) { - notif = new Notification(args); - notifications.push(notif); + // Plugin commands + each(editor.plugins, function (p) { + if (p.execCommand && p.execCommand(command, ui, value)) { + editor.fire('ExecCommand', { command: command, ui: ui, value: value }); + state = true; + return false; + } + }); - //If we have a timeout value - if (args.timeout > 0) { - notif.timer = setTimeout(function () { - notif.close(); - }, args.timeout); + if (state) { + return state; + } + + // Theme commands + if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) { + editor.fire('ExecCommand', { command: command, ui: ui, value: value }); + return true; + } + + // Browser commands + try { + state = editor.getDoc().execCommand(command, ui, value); + } catch (ex) { + // Ignore old IE errors + } + + if (state) { + editor.fire('ExecCommand', { command: command, ui: ui, value: value }); + return true; + } + + return false; + } + + /** + * Queries the current state for a command for example if the current selection is "bold". + * + * @method queryCommandState + * @param {String} command Command to check the state of. + * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found. + */ + function queryCommandState(command) { + var func; + + if (editor.quirks.isHidden() || editor.removed) { + return; + } + + command = command.toLowerCase(); + if ((func = commands.state[command])) { + return func(command); + } + + // Browser commands + try { + return editor.getDoc().queryCommandState(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + + return false; + } + + /** + * Queries the command value for example the current fontsize. + * + * @method queryCommandValue + * @param {String} command Command to check the value of. + * @return {Object} Command value of false if it's not found. + */ + function queryCommandValue(command) { + var func; + + if (editor.quirks.isHidden() || editor.removed) { + return; + } + + command = command.toLowerCase(); + if ((func = commands.value[command])) { + return func(command); + } + + // Browser commands + try { + return editor.getDoc().queryCommandValue(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + } + + /** + * Adds commands to the command collection. + * + * @method addCommands + * @param {Object} commandList Name/value collection with commands to add, the names can also be comma separated. + * @param {String} type Optional type to add, defaults to exec. Can be value or state as well. + */ + function addCommands(commandList, type) { + type = type || 'exec'; + + each(commandList, function (callback, command) { + each(command.toLowerCase().split(','), function (command) { + commands[type][command] = callback; + }); + }); + } + + function addCommand(command, callback, scope) { + command = command.toLowerCase(); + commands.exec[command] = function (command, ui, value, args) { + return callback.call(scope || editor, ui, value, args); + }; + } + + /** + * Returns true/false if the command is supported or not. + * + * @method queryCommandSupported + * @param {String} command Command that we check support for. + * @return {Boolean} true/false if the command is supported or not. + */ + function queryCommandSupported(command) { + command = command.toLowerCase(); + + if (commands.exec[command]) { + return true; + } + + // Browser commands + try { + return editor.getDoc().queryCommandSupported(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + + return false; + } + + function addQueryStateHandler(command, callback, scope) { + command = command.toLowerCase(); + commands.state[command] = function () { + return callback.call(scope || editor); + }; + } + + function addQueryValueHandler(command, callback, scope) { + command = command.toLowerCase(); + commands.value[command] = function () { + return callback.call(scope || editor); + }; + } + + function hasCustomCommand(command) { + command = command.toLowerCase(); + return !!commands.exec[command]; + } + + // Expose public methods + extend(this, { + execCommand: execCommand, + queryCommandState: queryCommandState, + queryCommandValue: queryCommandValue, + queryCommandSupported: queryCommandSupported, + addCommands: addCommands, + addCommand: addCommand, + addQueryStateHandler: addQueryStateHandler, + addQueryValueHandler: addQueryValueHandler, + hasCustomCommand: hasCustomCommand + }); + + // Private methods + + function execNativeCommand(command, ui, value) { + if (ui === undefined) { + ui = FALSE; + } + + if (value === undefined) { + value = null; + } + + return editor.getDoc().execCommand(command, ui, value); + } + + function isFormatMatch(name) { + return formatter.match(name); + } + + function toggleFormat(name, value) { + formatter.toggle(name, value ? { value: value } : undefined); + editor.nodeChanged(); + } + + function storeSelection(type) { + bookmark = selection.getBookmark(type); + } + + function restoreSelection() { + selection.moveToBookmark(bookmark); + } + + // Add execCommand overrides + addCommands({ + // Ignore these, added for compatibility + 'mceResetDesignMode,mceBeginUndoLevel': function () { }, + + // Add undo manager logic + 'mceEndUndoLevel,mceAddUndoLevel': function () { + editor.undoManager.add(); + }, + + 'Cut,Copy,Paste': function (command) { + var doc = editor.getDoc(), failed; + + // Try executing the native command + try { + execNativeCommand(command); + } catch (ex) { + // Command failed + failed = TRUE; } - notif.on('close', function () { - var i = notifications.length; + // Chrome reports the paste command as supported however older IE:s will return false for cut/paste + if (command === 'paste' && !doc.queryCommandEnabled(command)) { + failed = true; + } - if (notif.timer) { - editor.getWin().clearTimeout(notif.timer); + // Present alert message about clipboard access not being available + if (failed || !doc.queryCommandSupported(command)) { + var msg = editor.translate( + "Your browser doesn't support direct access to the clipboard. " + + "Please use the Ctrl+X/C/V keyboard shortcuts instead." + ); + + if (Env.mac) { + msg = msg.replace(/Ctrl\+/g, '\u2318+'); } - while (i--) { - if (notifications[i] === notif) { - notifications.splice(i, 1); + editor.notificationManager.open({ text: msg, type: 'error' }); + } + }, + + // Override unlink command + unlink: function () { + if (selection.isCollapsed()) { + var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); + if (elm) { + editor.dom.remove(elm, true); + } + + return; + } + + formatter.remove("link"); + }, + + // Override justify commands to use the text formatter engine + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function (command) { + var align = command.substring(7); + + if (align == 'full') { + align = 'justify'; + } + + // Remove all other alignments first + each('left,center,right,justify'.split(','), function (name) { + if (align != name) { + formatter.remove('align' + name); + } + }); + + if (align != 'none') { + toggleFormat('align' + align); + } + }, + + // Override list commands to fix WebKit bug + 'InsertUnorderedList,InsertOrderedList': function (command) { + var listElm, listParent; + + execNativeCommand(command); + + // WebKit produces lists within block elements so we need to split them + // we will replace the native list creation logic to custom logic later on + // TODO: Remove this when the list creation logic is removed + listElm = dom.getParent(selection.getNode(), 'ol,ul'); + if (listElm) { + listParent = listElm.parentNode; + + // If list is within a text block then split that block + if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) { + storeSelection(); + dom.split(listParent, listElm); + restoreSelection(); + } + } + }, + + // Override commands to use the text formatter engine + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { + toggleFormat(command); + }, + + // Override commands to use the text formatter engine + 'ForeColor,HiliteColor,FontName': function (command, ui, value) { + toggleFormat(command, value); + }, + + FontSize: function (command, ui, value) { + var fontClasses, fontSizes; + + // Convert font size 1-7 to styles + if (value >= 1 && value <= 7) { + fontSizes = explode(settings.font_size_style_values); + fontClasses = explode(settings.font_size_classes); + + if (fontClasses) { + value = fontClasses[value - 1] || value; + } else { + value = fontSizes[value - 1] || value; + } + } + + toggleFormat(command, value); + }, + + RemoveFormat: function (command) { + formatter.remove(command); + }, + + mceBlockQuote: function () { + toggleFormat('blockquote'); + }, + + FormatBlock: function (command, ui, value) { + return toggleFormat(value || 'p'); + }, + + mceCleanup: function () { + var bookmark = selection.getBookmark(); + + editor.setContent(editor.getContent({ cleanup: TRUE }), { cleanup: TRUE }); + + selection.moveToBookmark(bookmark); + }, + + mceRemoveNode: function (command, ui, value) { + var node = value || selection.getNode(); + + // Make sure that the body node isn't removed + if (node != editor.getBody()) { + storeSelection(); + editor.dom.remove(node, TRUE); + restoreSelection(); + } + }, + + mceSelectNodeDepth: function (command, ui, value) { + var counter = 0; + + dom.getParent(selection.getNode(), function (node) { + if (node.nodeType == 1 && counter++ == value) { + selection.select(node); + return FALSE; + } + }, editor.getBody()); + }, + + mceSelectNode: function (command, ui, value) { + selection.select(value); + }, + + mceInsertContent: function (command, ui, value) { + InsertContent.insertAtCaret(editor, value); + }, + + mceInsertRawHTML: function (command, ui, value) { + selection.setContent('tiny_mce_marker'); + editor.setContent( + editor.getContent().replace(/tiny_mce_marker/g, function () { + return value; + }) + ); + }, + + mceToggleFormat: function (command, ui, value) { + toggleFormat(value); + }, + + mceSetContent: function (command, ui, value) { + editor.setContent(value); + }, + + 'Indent,Outdent': function (command) { + var intentValue, indentUnit, value; + + // Setup indent level + intentValue = settings.indentation; + indentUnit = /[a-z%]+$/i.exec(intentValue); + intentValue = parseInt(intentValue, 10); + + if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) { + // If forced_root_blocks is set to false we don't have a block to indent so lets create a div + if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) { + formatter.apply('div'); + } + + each(selection.getSelectedBlocks(), function (element) { + if (dom.getContentEditable(element) === "false") { + return; + } + + if (element.nodeName !== "LI") { + var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding'; + indentStyleName = element.nodeName === 'TABLE' ? 'margin' : indentStyleName; + indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left'; + + if (command == 'outdent') { + value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue); + dom.setStyle(element, indentStyleName, value ? value + indentUnit : ''); + } else { + value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit; + dom.setStyle(element, indentStyleName, value); + } + } + }); + } else { + execNativeCommand(command); + } + }, + + mceRepaint: function () { + }, + + InsertHorizontalRule: function () { + editor.execCommand('mceInsertContent', false, '|
+ rng = selection.getRng(); + if (!rng.item) { + rng.moveToElementText(root); + rng.select(); + } + } + }, + + "delete": function () { + DeleteCommands.deleteCommand(editor); + }, + + "forwardDelete": function () { + DeleteCommands.forwardDeleteCommand(editor); + }, + + mceNewDocument: function () { + editor.setContent(''); + }, + + InsertLineBreak: function (command, ui, value) { + // We load the current event in from EnterKey.js when appropriate to heed + // certain event-specific variations such as ctrl-enter in a list + var evt = value; + var brElm, extraBr, marker; + var rng = selection.getRng(true); + new RangeUtils(dom).normalize(rng); + + var offset = rng.startOffset; + var container = rng.startContainer; + + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + var isAfterLastNodeInContainer = offset > container.childNodes.length - 1; + + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } + } + + var parentBlock = dom.getParent(container, dom.isBlock); + var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; + var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + + // Enter inside block contained within a LI then split or insert before/after LI + var isControlKey = evt && evt.ctrlKey; + if (containerBlockName == 'LI' && !isControlKey) { + parentBlock = containerBlock; + parentBlockName = containerBlockName; + } + + // Walks the parent block to the right and look for BR elements + function hasRightSideContent() { + var walker = new TreeWalker(container, parentBlock), node; + var nonEmptyElementsMap = editor.schema.getNonEmptyElements(); + + while ((node = walker.next())) { + if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { + return true; } } + } - positionNotifications(); - }); + if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { + // Insert extra BR element at the end block elements + if (!isOldIE && !hasRightSideContent()) { + brElm = dom.create('br'); + rng.insertNode(brElm); + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + extraBr = true; + } + } - notif.renderTo(); + brElm = dom.create('br'); + rng.insertNode(brElm); - positionNotifications(); - } else { - notif = duplicate; - } + // Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it + var documentMode = dom.doc.documentMode; + if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) { + brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm); + } - return notif; - }; + // Insert temp marker and scroll to that + marker = dom.create('span', {}, ' '); + brElm.parentNode.insertBefore(marker, brElm); + selection.scrollIntoView(marker); + dom.remove(marker); - /** - * Closes the top most notification. - * - * @method close - */ - self.close = function () { - if (getLastNotification()) { - getLastNotification().close(); - } - }; + if (!extraBr) { + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + } else { + rng.setStartBefore(brElm); + rng.setEndBefore(brElm); + } - /** - * Returns the currently opened notification objects. - * - * @method getNotifications - * @return {Array} Array of the currently opened notifications. - */ - self.getNotifications = function () { - return notifications; - }; + selection.setRng(rng); + editor.undoManager.add(); - editor.on('SkinLoaded', function () { - var serviceMessage = editor.settings.service_message; - - if (serviceMessage) { - editor.notificationManager.open({ - text: serviceMessage, - type: 'warning', - timeout: 0, - icon: '' - }); + return TRUE; } }); - /** - * Finds any existing notification with the same properties as the new one. - * Returns either the found notification or null. - * - * @param {Notification[]} notificationArray - Array of current notifications - * @param {type: string, } newNotification - New notification object - * @returns {?Notification} - */ - function findDuplicateMessage(notificationArray, newNotification) { - if (!isPlainTextNotification(newNotification)) { - return null; + // Add queryCommandState overrides + addCommands({ + // Override justify commands + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function (command) { + var name = 'align' + command.substring(7); + var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks(); + var matches = map(nodes, function (node) { + return !!formatter.matchNode(node, name); + }); + return inArray(matches, TRUE) !== -1; + }, + + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { + return isFormatMatch(command); + }, + + mceBlockQuote: function () { + return isFormatMatch('blockquote'); + }, + + Outdent: function () { + var node; + + if (settings.inline_styles) { + if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { + return TRUE; + } + + if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { + return TRUE; + } + } + + return ( + queryCommandState('InsertUnorderedList') || + queryCommandState('InsertOrderedList') || + (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) + ); + }, + + 'InsertUnorderedList,InsertOrderedList': function (command) { + var list = dom.getParent(selection.getNode(), 'ul,ol'); + + return list && + ( + command === 'insertunorderedlist' && list.tagName === 'UL' || + command === 'insertorderedlist' && list.tagName === 'OL' + ); + } + }, 'state'); + + // Add queryCommandValue overrides + addCommands({ + 'FontSize,FontName': function (command) { + var value = 0, parent; + + if ((parent = dom.getParent(selection.getNode(), 'span'))) { + if (command == 'fontsize') { + value = parent.style.fontSize; + } else { + value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); + } + } + + return value; + } + }, 'value'); + + // Add undo manager logic + addCommands({ + Undo: function () { + editor.undoManager.undo(); + }, + + Redo: function () { + editor.undoManager.redo(); + } + }); + }; + } +); + +/** + * EditorFocus.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.EditorFocus', + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.dom.ElementType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.Env' + ], + function (Option, Compare, Element, CaretFinder, ElementType, RangeUtils, Env) { + var getContentEditableHost = function (editor, node) { + return editor.dom.getParent(node, function (node) { + return editor.dom.getContentEditable(node) === "true"; + }); + }; + + var getCollapsedNode = function (rng) { + return rng.collapsed ? Option.from(RangeUtils.getNode(rng.startContainer, rng.startOffset)).map(Element.fromDom) : Option.none(); + }; + + var getFocusInElement = function (root, rng) { + return getCollapsedNode(rng).bind(function (node) { + if (ElementType.isTableSection(node)) { + return Option.some(node); + } else if (Compare.contains(root, node) === false) { + return Option.some(root); + } else { + return Option.none(); + } + }); + }; + + var normalizeSelection = function (editor, rng) { + getFocusInElement(Element.fromDom(editor.getBody()), rng).bind(function (elm) { + return CaretFinder.firstPositionIn(elm.dom()); + }).fold( + function () { + editor.selection.normalize(); + }, + function (caretPos) { + editor.selection.setRng(caretPos.toRange()); + } + ); + }; + + var focusBody = function (body) { + if (body.setActive) { + // IE 11 sometimes throws "Invalid function" then fallback to focus + // setActive is better since it doesn't scroll to the element being focused + try { + body.setActive(); + } catch (ex) { + body.focus(); + } + } else { + body.focus(); + } + }; + + var focusEditor = function (editor) { + var selection = editor.selection, contentEditable = editor.settings.content_editable, rng; + var controlElm, doc = editor.getDoc(), body = editor.getBody(), contentEditableHost; + + // Get selected control element + rng = selection.getRng(); + if (rng.item) { + controlElm = rng.item(0); + } + + editor.quirks.refreshContentEditable(); + + // Move focus to contentEditable=true child if needed + contentEditableHost = getContentEditableHost(editor, selection.getNode()); + if (editor.$.contains(body, contentEditableHost)) { + focusBody(contentEditableHost); + normalizeSelection(editor, rng); + activateEditor(editor); + return; + } + + // Focus the window iframe + if (!contentEditable) { + // WebKit needs this call to fire focusin event properly see #5948 + // But Opera pre Blink engine will produce an empty selection so skip Opera + if (!Env.opera) { + focusBody(body); } - var filteredNotifications = Tools.grep(notificationArray, function (notification) { - return isSameNotification(newNotification, notification); - }); - - return filteredNotifications.length === 0 ? null : filteredNotifications[0]; + editor.getWin().focus(); } - /** - * Checks if the passed in args object has the same - * type and text properties as the sent in notification. - * - * @param {type: string, text: string} a - New notification args object - * @param {Notification} b - Old notification - * @returns {boolean} - */ - function isSameNotification(a, b) { - return a.type === b.settings.type && a.text === b.settings.text; + // Focus the body as well since it's contentEditable + if (Env.gecko || contentEditable) { + // Restore previous selection before focus to prevent Chrome from + // jumping to the top of the document in long inline editors + if (contentEditable && document.activeElement !== body) { + editor.selection.setRng(editor.lastRng); + } + + focusBody(body); + normalizeSelection(editor, rng); } - /** - * Checks that the notification does not have a progressBar - * or timeour property. - * - * @param {Notification} notification - Notification to check - * @returns {boolean} - */ - function isPlainTextNotification(notification) { - return !notification.progressBar && !notification.timeout; + // Restore selected control element + // This is needed when for example an image is selected within a + // layer a call to focus will then remove the control selection + if (controlElm && controlElm.ownerDocument === doc) { + rng = doc.body.createControlRange(); + rng.addElement(controlElm); + rng.select(); } - //self.positionNotifications = positionNotifications; + activateEditor(editor); + }; + + var activateEditor = function (editor) { + editor.editorManager.setActive(editor); + }; + + var focus = function (editor, skipFocus) { + if (editor.removed) { + return; + } + + skipFocus ? activateEditor(editor) : focusEditor(editor); + }; + + return { + focus: focus }; } ); @@ -37835,7 +38474,7 @@ define( } delegate = function (e) { - var target = e.target, editors = editor.editorManager.editors, i = editors.length; + var target = e.target, editors = editor.editorManager.get(), i = editors.length; while (i--) { var body = editors[i].getBody(); @@ -37939,228 +38578,6 @@ define( } ); -/** - * Shortcuts.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * Contains all logic for handling of keyboard shortcuts. - * - * @class tinymce.Shortcuts - * @example - * editor.shortcuts.add('ctrl+a', function() {}); - * editor.shortcuts.add('meta+a', function() {}); // "meta" maps to Command on Mac and Ctrl on PC - * editor.shortcuts.add('ctrl+alt+a', function() {}); - * editor.shortcuts.add('access+a', function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC - */ -define( - 'tinymce.core.Shortcuts', - [ - "tinymce.core.util.Tools", - "tinymce.core.Env" - ], - function (Tools, Env) { - var each = Tools.each, explode = Tools.explode; - - var keyCodeLookup = { - "f9": 120, - "f10": 121, - "f11": 122 - }; - - var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access'); - - return function (editor) { - var self = this, shortcuts = {}, pendingPatterns = []; - - function parseShortcut(pattern) { - var id, key, shortcut = {}; - - // Parse modifiers and keys ctrl+alt+b for example - each(explode(pattern, '+'), function (value) { - if (value in modifierNames) { - shortcut[value] = true; - } else { - // 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); - } - } - }); - - // Generate unique id for modifier combination and set default state for unused modifiers - id = [shortcut.keyCode]; - for (key in modifierNames) { - if (shortcut[key]) { - id.push(key); - } else { - shortcut[key] = false; - } - } - shortcut.id = id.join(','); - - // Handle special access modifier differently depending on Mac/Win - if (shortcut.access) { - shortcut.alt = true; - - if (Env.mac) { - shortcut.ctrl = true; - } else { - shortcut.shift = true; - } - } - - // Handle special meta modifier differently depending on Mac/Win - if (shortcut.meta) { - if (Env.mac) { - shortcut.meta = true; - } else { - shortcut.ctrl = true; - shortcut.meta = false; - } - } - - return shortcut; - } - - function createShortcut(pattern, desc, cmdFunc, scope) { - var shortcuts; - - shortcuts = Tools.map(explode(pattern, '>'), parseShortcut); - shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], { - func: cmdFunc, - scope: scope || editor - }); - - return Tools.extend(shortcuts[0], { - desc: editor.translate(desc), - subpatterns: shortcuts.slice(1) - }); - } - - function hasModifier(e) { - return e.altKey || e.ctrlKey || e.metaKey; - } - - function isFunctionKey(e) { - return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123; - } - - function matchShortcut(e, shortcut) { - if (!shortcut) { - return false; - } - - if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) { - return false; - } - - if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { - return false; - } - - if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { - e.preventDefault(); - return true; - } - - return false; - } - - function executeShortcutAction(shortcut) { - return shortcut.func ? shortcut.func.call(shortcut.scope) : null; - } - - editor.on('keyup keypress keydown', function (e) { - if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) { - each(shortcuts, function (shortcut) { - if (matchShortcut(e, shortcut)) { - pendingPatterns = shortcut.subpatterns.slice(0); - - if (e.type == "keydown") { - executeShortcutAction(shortcut); - } - - return true; - } - }); - - if (matchShortcut(e, pendingPatterns[0])) { - if (pendingPatterns.length === 1) { - if (e.type == "keydown") { - executeShortcutAction(pendingPatterns[0]); - } - } - - pendingPatterns.shift(); - } - } - }); - - /** - * Adds a keyboard shortcut for some command or function. - * - * @method add - * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. - * @param {String} desc Text description for the command. - * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. - * @param {Object} scope Optional scope to execute the function in. - * @return {Boolean} true/false state if the shortcut was added or not. - */ - self.add = function (pattern, desc, cmdFunc, scope) { - var cmd; - - cmd = cmdFunc; - - if (typeof cmdFunc === 'string') { - cmdFunc = function () { - editor.execCommand(cmd, false, null); - }; - } else if (Tools.isArray(cmd)) { - cmdFunc = function () { - editor.execCommand(cmd[0], cmd[1], cmd[2]); - }; - } - - each(explode(Tools.trim(pattern.toLowerCase())), function (pattern) { - var shortcut = createShortcut(pattern, desc, cmdFunc, scope); - shortcuts[shortcut.id] = shortcut; - }); - - return true; - }; - - /** - * Remove a keyboard shortcut by pattern. - * - * @method remove - * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. - * @return {Boolean} true/false state if the shortcut was removed or not. - */ - self.remove = function (pattern) { - var shortcut = createShortcut(pattern); - - if (shortcuts[shortcut.id]) { - delete shortcuts[shortcut.id]; - return true; - } - - return false; - }; - }; - } -); - -defineGlobal("global!window", window); /** * ErrorReporter.js * @@ -38180,7 +38597,7 @@ defineGlobal("global!window", window); define( 'tinymce.core.ErrorReporter', [ - "tinymce.core.AddOnManager" + 'tinymce.core.AddOnManager' ], function (AddOnManager) { var PluginManager = AddOnManager.PluginManager; @@ -38228,10 +38645,6 @@ define( displayError(editor, pluginUrlToMessage(editor, url)); }; - var contentCssError = function (editor, urls) { - displayError(editor, 'Failed to load content css: ' + urls[0]); - }; - var initError = function (message) { var console = window.console; if (console && !window.test) { // Skip test env @@ -38247,7 +38660,6 @@ define( pluginLoadError: pluginLoadError, uploadError: uploadError, displayError: displayError, - contentCssError: contentCssError, initError: initError }; } @@ -38269,11 +38681,15 @@ define( 'tinymce.core.caret.CaretContainerInput', [ 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.SelectorFind', 'tinymce.core.caret.CaretContainer' ], - function (Fun, CaretContainer) { + function (Fun, Element, SelectorFind, CaretContainer) { var findBlockCaretContainer = function (editor) { - return editor.dom.select('*[data-mce-caret]')[0]; + return SelectorFind.descendant(Element.fromDom(editor.getBody()), '*[data-mce-caret]').fold(Fun.constant(null), function (elm) { + return elm.dom(); + }); }; var removeIeControlRect = function (editor) { @@ -38524,6 +38940,45 @@ define( }; } ); +define( + 'ephox.sand.api.Window', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /****************************************************************************************** + * BIG BIG WARNING: Don't put anything other than top-level window functions in here. + * + * Objects that are technically available as window.X should be in their own module X (e.g. Blob, FileReader, URL). + ****************************************************************************************** + */ + + /* + * IE10 and above per + * https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame + */ + var requestAnimationFrame = function (callback) { + var f = Global.getOrDie('requestAnimationFrame'); + f(callback); + }; + + /* + * IE10 and above per + * https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64.atob + */ + var atob = function (base64) { + var f = Global.getOrDie('atob'); + return f(base64); + }; + + return { + atob: atob, + requestAnimationFrame: requestAnimationFrame + }; + } +); /** * Conversions.js * @@ -38543,9 +38998,10 @@ define( define( 'tinymce.core.file.Conversions', [ - "tinymce.core.util.Promise" + 'ephox.sand.api.Window', + 'tinymce.core.util.Promise' ], - function (Promise) { + function (Window, Promise) { function blobUriToBlob(url) { return new Promise(function (resolve, reject) { @@ -38603,7 +39059,7 @@ define( // Might throw error if data isn't proper base64 try { - str = atob(uri.data); + str = Window.atob(uri.data); } catch (e) { resolve(new Blob([])); return; @@ -38825,6 +39281,40 @@ define( }; } ); +define( + 'ephox.sand.api.URL', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * IE10 and above per + * https://developer.mozilla.org/en-US/docs/Web/API/URL.createObjectURL + * + * Also Safari 6.1+ + * Safari 6.0 has 'webkitURL' instead, but doesn't support flexbox so we + * aren't supporting it anyway + */ + var url = function () { + return Global.getOrDie('URL'); + }; + + var createObjectURL = function (blob) { + return url().createObjectURL(blob); + }; + + var revokeObjectURL = function (u) { + url().revokeObjectURL(u); + }; + + return { + createObjectURL: createObjectURL, + revokeObjectURL: revokeObjectURL + }; + } +); /** * Uuid.js * @@ -38867,7 +39357,6 @@ define( } ); -defineGlobal("global!URL", URL); /** * BlobCache.js * @@ -38887,12 +39376,12 @@ defineGlobal("global!URL", URL); define( 'tinymce.core.file.BlobCache', [ + 'ephox.sand.api.URL', 'tinymce.core.util.Arr', 'tinymce.core.util.Fun', - 'tinymce.core.util.Uuid', - 'global!URL' + 'tinymce.core.util.Uuid' ], - function (Arr, Fun, Uuid, URL) { + function (URL, Arr, Fun, Uuid) { return function () { var cache = [], constant = Fun.constant; @@ -39263,7 +39752,7 @@ define( var blobInfo = blobCache.getByUri(blobUri); if (!blobInfo) { - blobInfo = Arr.reduce(editor.editorManager.editors, function (result, editor) { + blobInfo = Arr.reduce(editor.editorManager.get(), function (result, editor) { return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri); }, null); } @@ -40317,9 +40806,16 @@ define( }; }; + var execute = function (patterns, evt) { + return Arr.find(match(patterns, evt), function (pattern) { + return pattern.action(); + }); + }; + return { match: match, - action: action + action: action, + execute: execute }; } ); @@ -40336,27 +40832,21 @@ define( define( 'tinymce.core.keyboard.ArrowKeys', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Cell', 'tinymce.core.keyboard.BoundarySelection', 'tinymce.core.keyboard.CefNavigation', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], - function (Arr, Cell, BoundarySelection, CefNavigation, MatchKeys, VK) { + function (BoundarySelection, CefNavigation, MatchKeys, VK) { var executeKeydownOverride = function (editor, caret, evt) { - var matches = MatchKeys.match([ + MatchKeys.execute([ { keyCode: VK.RIGHT, action: CefNavigation.moveH(editor, true) }, { keyCode: VK.LEFT, action: CefNavigation.moveH(editor, false) }, { keyCode: VK.UP, action: CefNavigation.moveV(editor, false) }, { keyCode: VK.DOWN, action: CefNavigation.moveV(editor, true) }, { keyCode: VK.RIGHT, action: BoundarySelection.move(editor, caret, true) }, { keyCode: VK.LEFT, action: BoundarySelection.move(editor, caret, false) } - ], evt); - - Arr.find(matches, function (pattern) { - return pattern.action(); - }).each(function (_) { + ], evt).each(function (_) { evt.preventDefault(); }); }; @@ -40388,7 +40878,6 @@ define( define( 'tinymce.core.keyboard.DeleteBackspaceKeys', [ - 'ephox.katamari.api.Arr', 'tinymce.core.delete.BlockBoundaryDelete', 'tinymce.core.delete.BlockRangeDelete', 'tinymce.core.delete.CefDelete', @@ -40396,9 +40885,9 @@ define( 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], - function (Arr, BlockBoundaryDelete, BlockRangeDelete, CefDelete, InlineBoundaryDelete, MatchKeys, VK) { + function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, InlineBoundaryDelete, MatchKeys, VK) { var executeKeydownOverride = function (editor, caret, evt) { - var matches = MatchKeys.match([ + MatchKeys.execute([ { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.backspaceDelete, editor, true) }, { keyCode: VK.BACKSPACE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, false) }, @@ -40407,24 +40896,16 @@ define( { keyCode: VK.DELETE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, true) }, { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, false) }, { keyCode: VK.DELETE, action: MatchKeys.action(BlockBoundaryDelete.backspaceDelete, editor, true) } - ], evt); - - Arr.find(matches, function (pattern) { - return pattern.action(); - }).each(function (_) { + ], evt).each(function (_) { evt.preventDefault(); }); }; var executeKeyupOverride = function (editor, evt) { - var matches = MatchKeys.match([ + MatchKeys.execute([ { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) }, { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) } ], evt); - - Arr.find(matches, function (pattern) { - return pattern.action(); - }); }; var setup = function (editor, caret) { @@ -40448,7 +40929,7 @@ define( ); /** - * EnterKey.js + * InsertNewLine.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -40457,23 +40938,17 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Contains logic for handling the enter key to split/generate block elements. - */ define( - 'tinymce.core.keyboard.EnterKey', + 'tinymce.core.keyboard.InsertNewLine', [ 'tinymce.core.caret.CaretContainer', 'tinymce.core.dom.NodeType', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', - 'tinymce.core.Env', 'tinymce.core.text.Zwsp', 'tinymce.core.util.Tools' ], - function (CaretContainer, NodeType, RangeUtils, TreeWalker, Env, Zwsp, Tools) { - var isIE = Env.ie && Env.ie < 11; - + function (CaretContainer, NodeType, RangeUtils, TreeWalker, Zwsp, Tools) { var isEmptyAnchor = function (elm) { return elm && elm.nodeName === "A" && Tools.trim(Zwsp.trim(elm.innerText || elm.textContent)).length === 0; }; @@ -40482,15 +40957,30 @@ define( return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); }; + var hasFirstChild = function (elm, name) { + return elm.firstChild && elm.firstChild.nodeName == name; + }; + + var hasParent = function (elm, parentName) { + return elm && elm.parentNode && elm.parentNode.nodeName === parentName; + }; + var emptyBlock = function (elm) { - // BR is needed in empty blocks on non IE browsers - elm.innerHTML = !isIE ? 'x
becomes this:x
var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) { var node = block, firstChilds = [], i; @@ -40572,601 +41049,593 @@ define( } }; - var setup = function (editor) { + // Inserts a BR element if the forced_root_block option is set to false or empty string + var insertBr = function (editor, evt) { + editor.execCommand("InsertLineBreak", false, evt); + }; + + // Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element + var trimLeadingLineBreaks = function (node) { + do { + if (node.nodeType === 3) { + node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); + } + + node = node.firstChild; + } while (node); + }; + + var getEditableRoot = function (dom, node) { + var root = dom.getRoot(), parent, editableRoot; + + // Get all parents until we hit a non editable parent or the root + parent = node; + while (parent !== root && dom.getContentEditable(parent) !== "false") { + if (dom.getContentEditable(parent) === "true") { + editableRoot = parent; + } + + parent = parent.parentNode; + } + + return parent !== root ? editableRoot : root; + }; + + var setForcedBlockAttrs = function (editor, node) { + var forcedRootBlockName = editor.settings.forced_root_block; + + if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { + editor.dom.setAttribs(node, editor.settings.forced_root_block_attrs); + } + }; + + // Wraps any text nodes or inline elements in the specified forced root block name + var wrapSelfAndSiblingsInDefaultBlock = function (editor, newBlockName, rng, container, offset) { + var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P'; + var dom = editor.dom, editableRoot = getEditableRoot(dom, container); + + // Not in a block element or in a table cell or caption + parentBlock = dom.getParent(container, dom.isBlock); + if (!parentBlock || !canSplitBlock(dom, parentBlock)) { + parentBlock = parentBlock || editableRoot; + + if (parentBlock == editor.getBody() || isTableCell(parentBlock)) { + rootBlockName = parentBlock.nodeName.toLowerCase(); + } else { + rootBlockName = parentBlock.parentNode.nodeName.toLowerCase(); + } + + if (!parentBlock.hasChildNodes()) { + newBlock = dom.create(blockName); + setForcedBlockAttrs(editor, newBlock); + parentBlock.appendChild(newBlock); + rng.setStart(newBlock, 0); + rng.setEnd(newBlock, 0); + return newBlock; + } + + // Find parent that is the first child of parentBlock + node = container; + while (node.parentNode != parentBlock) { + node = node.parentNode; + } + + // Loop left to find start node start wrapping at + while (node && !dom.isBlock(node)) { + startNode = node; + node = node.previousSibling; + } + + if (startNode && editor.schema.isValidChild(rootBlockName, blockName.toLowerCase())) { + newBlock = dom.create(blockName); + setForcedBlockAttrs(editor, newBlock); + startNode.parentNode.insertBefore(newBlock, startNode); + + // Start wrapping until we hit a block + node = startNode; + while (node && !dom.isBlock(node)) { + next = node.nextSibling; + newBlock.appendChild(node); + node = next; + } + + // Restore range to it's past location + rng.setStart(container, offset); + rng.setEnd(container, offset); + } + } + + return container; + }; + + // Adds a BR at the end of blocks that only contains an IMG or INPUT since + // these might be floated and then they won't expand the block + var addBrToBlockIfNeeded = function (dom, block) { + var lastChild; + + // IE will render the blocks correctly other browsers needs a BR + block.normalize(); // Remove empty text nodes that got left behind by the extract + + // Check if the block is empty or contains a floated last child + lastChild = block.lastChild; + if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { + dom.add(block, 'br'); + } + }; + + var getContainerBlock = function (containerBlock) { + var containerBlockParent = containerBlock.parentNode; + + if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { + return containerBlockParent; + } + + return containerBlock; + }; + + var isFirstOrLastLi = function (containerBlock, parentBlock, first) { + var node = containerBlock[first ? 'firstChild' : 'lastChild']; + + // Find first/last element since there might be whitespace there + while (node) { + if (node.nodeType == 1) { + break; + } + + node = node[first ? 'nextSibling' : 'previousSibling']; + } + + return node === parentBlock; + }; + + var insert = function (editor, evt) { + var tmpRng, editableRoot, container, offset, parentBlock, shiftKey; + var newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; var dom = editor.dom, selection = editor.selection, settings = editor.settings; - var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(), - moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); + var schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); + var rng = editor.selection.getRng(); - function handleEnterKey(evt) { - var rng, tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, - newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; + // Moves the caret to a suitable position within the root for example in the first non + // pure whitespace text node or before an image + function moveToCaretPosition(root) { + var walker, node, rng, lastNode = root, tempElm; + var moveCaretBeforeOnEnterElementsMap = schema.getMoveCaretBeforeOnEnterElements(); - // Moves the caret to a suitable position within the root for example in the first non - // pure whitespace text node or before an image - function moveToCaretPosition(root) { - var walker, node, rng, lastNode = root, tempElm; - - if (!root) { - return; - } - - // Old IE versions doesn't properly render blocks with br elements in them - // For exampletext|
text|
text|text2
a |
- if (dom.isEmpty(newBlock)) { - dom.remove(newBlock); - insertNewBlockAfter(); + if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { + newBlock = fragment.firstChild; + dom.insertAfter(fragment, containerBlock); } else { - moveToCaretPosition(newBlock); + dom.insertAfter(fragment, containerBlock); + dom.insertAfter(newBlock, containerBlock); } } - dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique + dom.remove(parentBlock); + moveToCaretPosition(newBlock); + } - // Allow custom handling of new blocks - editor.fire('NewBlock', { newBlock: newBlock }); + function insertNewBlockAfter() { + // If the caret is at the end of a header we produce a P tag after it similar to Word unless we are in a hgroup + if (/^(H[1-6]|PRE|FIGURE)$/.test(parentBlockName) && containerBlockName != 'HGROUP') { + newBlock = createNewBlock(newBlockName); + } else { + newBlock = createNewBlock(); + } + // Split the current container block element if enter is pressed inside an empty inner block element + if (settings.end_container_on_empty_block && canSplitBlock(dom, containerBlock) && dom.isEmpty(parentBlock)) { + // Split container block for example a BLOCKQUOTE at the current blockParent location for example a P + newBlock = dom.split(containerBlock, parentBlock); + } else { + dom.insertAfter(newBlock, parentBlock); + } + + moveToCaretPosition(newBlock); + } + + // Setup range items and newBlockName + new RangeUtils(dom).normalize(rng); + container = rng.startContainer; + offset = rng.startOffset; + newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; + newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; + shiftKey = evt.shiftKey; + + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + isAfterLastNodeInContainer = offset > container.childNodes.length - 1; + + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } + } + + // Get editable root node, normally the body element but sometimes a div or span + editableRoot = getEditableRoot(dom, container); + + // If there is no editable root then enter is done inside a contentEditable false element + if (!editableRoot) { + return; + } + + // If editable root isn't block nor the root of the editor + if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { + if (!newBlockName || shiftKey) { + insertBr(editor, evt); + } + + return; + } + + // Wrap the current node and it's sibling in a default block if it's needed. + // for example thistext|text2
a |
+ if (dom.isEmpty(newBlock)) { + dom.remove(newBlock); + insertNewBlockAfter(); + } else { + moveToCaretPosition(newBlock); + } + } + + dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique + + // Allow custom handling of new blocks + editor.fire('NewBlock', { newBlock: newBlock }); + }; + + return { + insert: insert + }; + } +); + +/** + * EnterKey.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.core.keyboard.EnterKey', + [ + 'tinymce.core.keyboard.InsertNewLine', + 'tinymce.core.util.VK' + ], + function (InsertNewLine, VK) { + var endTypingLevel = function (undoManager) { + if (undoManager.typing) { undoManager.typing = false; undoManager.add(); } + }; - editor.on('keydown', function (evt) { - if (evt.keyCode == 13) { - if (handleEnterKey(evt) !== false) { - evt.preventDefault(); - } + var handleEnterKeyEvent = function (editor, event) { + if (event.isDefaultPrevented()) { + return; + } + + event.preventDefault(); + + endTypingLevel(editor.undoManager); + editor.undoManager.transact(function () { + if (editor.selection.isCollapsed() === false) { + editor.execCommand('Delete'); + } + + InsertNewLine.insert(editor, event); + }); + }; + + var setup = function (editor) { + editor.on('keydown', function (event) { + if (event.keyCode === VK.ENTER) { + handleEnterKeyEvent(editor, event); } }); }; @@ -41193,9 +41662,10 @@ define( 'ephox.katamari.api.Fun', 'tinymce.core.caret.CaretPosition', 'tinymce.core.dom.NodeType', - 'tinymce.core.keyboard.BoundaryLocation' + 'tinymce.core.keyboard.BoundaryLocation', + 'tinymce.core.keyboard.InlineUtils' ], - function (Fun, CaretPosition, NodeType, BoundaryLocation) { + function (Fun, CaretPosition, NodeType, BoundaryLocation, InlineUtils) { var isValidInsertPoint = function (location, caretPosition) { return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container()); }; @@ -41218,8 +41688,9 @@ define( }; var insertAtCaret = function (editor) { + var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); - var boundaryLocation = BoundaryLocation.readLocation(editor.getBody(), caretPosition); + var boundaryLocation = BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), caretPosition); return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false); }; @@ -41255,20 +41726,15 @@ define( define( 'tinymce.core.keyboard.SpaceKey', [ - 'ephox.katamari.api.Arr', 'tinymce.core.keyboard.InsertSpace', 'tinymce.core.keyboard.MatchKeys', 'tinymce.core.util.VK' ], - function (Arr, InsertSpace, MatchKeys, VK) { + function (InsertSpace, MatchKeys, VK) { var executeKeydownOverride = function (editor, evt) { - var matches = MatchKeys.match([ + MatchKeys.execute([ { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) } - ], evt); - - Arr.find(matches, function (pattern) { - return pattern.action(); - }).each(function (_) { + ], evt).each(function (_) { evt.preventDefault(); }); }; @@ -41411,9 +41877,10 @@ define( editor.on('SelectionChange', function () { var startElm = editor.selection.getStart(true); + // When focusout from after cef element to other input element the startelm can be undefined. // IE 8 will fire a selectionchange event with an incorrect selection // when focusing out of table cells. Click inside cell -> toolbar = Invalid SelectionChange event - if (!Env.range && editor.selection.isCollapsed()) { + if (!startElm || (!Env.range && editor.selection.isCollapsed())) { return; } @@ -41479,210 +41946,6 @@ define( } ); -/** - * FakeCaret.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This module contains logic for rendering a fake visual caret. - * - * @private - * @class tinymce.caret.FakeCaret - */ -define( - 'tinymce.core.caret.FakeCaret', - [ - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretContainerRemove', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.DomQuery', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangeUtils', - 'tinymce.core.geom.ClientRect', - 'tinymce.core.util.Delay' - ], - function (CaretContainer, CaretContainerRemove, CaretPosition, DomQuery, NodeType, RangeUtils, ClientRect, Delay) { - var isContentEditableFalse = NodeType.isContentEditableFalse; - - var isTableCell = function (node) { - return node && /^(TD|TH)$/i.test(node.nodeName); - }; - - return function (rootNode, isBlock) { - var cursorInterval, $lastVisualCaret, caretContainerNode; - - function getAbsoluteClientRect(node, before) { - var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before), - docElm, scrollX, scrollY, margin, rootRect; - - if (rootNode.tagName == 'BODY') { - docElm = rootNode.ownerDocument.documentElement; - scrollX = rootNode.scrollLeft || docElm.scrollLeft; - scrollY = rootNode.scrollTop || docElm.scrollTop; - } else { - rootRect = rootNode.getBoundingClientRect(); - scrollX = rootNode.scrollLeft - rootRect.left; - scrollY = rootNode.scrollTop - rootRect.top; - } - - clientRect.left += scrollX; - clientRect.right += scrollX; - clientRect.top += scrollY; - clientRect.bottom += scrollY; - clientRect.width = 1; - - margin = node.offsetWidth - node.clientWidth; - - if (margin > 0) { - if (before) { - margin *= -1; - } - - clientRect.left += margin; - clientRect.right += margin; - } - - return clientRect; - } - - function trimInlineCaretContainers() { - var contentEditableFalseNodes, node, sibling, i, data; - - contentEditableFalseNodes = DomQuery('*[contentEditable=false]', rootNode); - for (i = 0; i < contentEditableFalseNodes.length; i++) { - node = contentEditableFalseNodes[i]; - - sibling = node.previousSibling; - if (CaretContainer.endsWithCaretContainer(sibling)) { - data = sibling.data; - - if (data.length == 1) { - sibling.parentNode.removeChild(sibling); - } else { - sibling.deleteData(data.length - 1, 1); - } - } - - sibling = node.nextSibling; - if (CaretContainer.startsWithCaretContainer(sibling)) { - data = sibling.data; - - if (data.length == 1) { - sibling.parentNode.removeChild(sibling); - } else { - sibling.deleteData(0, 1); - } - } - } - - return null; - } - - function show(before, node) { - var clientRect, rng; - - hide(); - - if (isTableCell(node)) { - return null; - } - - if (isBlock(node)) { - caretContainerNode = CaretContainer.insertBlock('p', node, before); - clientRect = getAbsoluteClientRect(node, before); - DomQuery(caretContainerNode).css('top', clientRect.top); - - $lastVisualCaret = DomQuery('').css(clientRect).appendTo(rootNode); - - if (before) { - $lastVisualCaret.addClass('mce-visual-caret-before'); - } - - startBlink(); - - rng = node.ownerDocument.createRange(); - rng.setStart(caretContainerNode, 0); - rng.setEnd(caretContainerNode, 0); - } else { - caretContainerNode = CaretContainer.insertInline(node, before); - rng = node.ownerDocument.createRange(); - - if (isContentEditableFalse(caretContainerNode.nextSibling)) { - rng.setStart(caretContainerNode, 0); - rng.setEnd(caretContainerNode, 0); - } else { - rng.setStart(caretContainerNode, 1); - rng.setEnd(caretContainerNode, 1); - } - - return rng; - } - - return rng; - } - - function hide() { - trimInlineCaretContainers(); - - if (caretContainerNode) { - CaretContainerRemove.remove(caretContainerNode); - caretContainerNode = null; - } - - if ($lastVisualCaret) { - $lastVisualCaret.remove(); - $lastVisualCaret = null; - } - - clearInterval(cursorInterval); - } - - function startBlink() { - cursorInterval = Delay.setInterval(function () { - DomQuery('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden'); - }, 500); - } - - function destroy() { - Delay.clearInterval(cursorInterval); - } - - function getCss() { - return ( - '.mce-visual-caret {' + - 'position: absolute;' + - 'background-color: black;' + - 'background-color: currentcolor;' + - '}' + - '.mce-visual-caret-hidden {' + - 'display: none;' + - '}' + - '*[data-mce-caret] {' + - 'position: absolute;' + - 'left: -1000px;' + - 'right: auto;' + - 'top: 0;' + - 'margin: 0;' + - 'padding: 0;' + - '}' - ); - } - - return { - show: show, - hide: hide, - getCss: getCss, - destroy: destroy - }; - }; - } -); /** * MousePosition.js * @@ -42053,6 +42316,210 @@ define( } ); +/** + * FakeCaret.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module contains logic for rendering a fake visual caret. + * + * @private + * @class tinymce.caret.FakeCaret + */ +define( + 'tinymce.core.caret.FakeCaret', + [ + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.caret.CaretContainerRemove', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.geom.ClientRect', + 'tinymce.core.util.Delay' + ], + function (CaretContainer, CaretContainerRemove, CaretPosition, DomQuery, NodeType, RangeUtils, ClientRect, Delay) { + var isContentEditableFalse = NodeType.isContentEditableFalse; + + var isTableCell = function (node) { + return node && /^(TD|TH)$/i.test(node.nodeName); + }; + + return function (rootNode, isBlock) { + var cursorInterval, $lastVisualCaret, caretContainerNode; + + function getAbsoluteClientRect(node, before) { + var clientRect = ClientRect.collapse(node.getBoundingClientRect(), before), + docElm, scrollX, scrollY, margin, rootRect; + + if (rootNode.tagName == 'BODY') { + docElm = rootNode.ownerDocument.documentElement; + scrollX = rootNode.scrollLeft || docElm.scrollLeft; + scrollY = rootNode.scrollTop || docElm.scrollTop; + } else { + rootRect = rootNode.getBoundingClientRect(); + scrollX = rootNode.scrollLeft - rootRect.left; + scrollY = rootNode.scrollTop - rootRect.top; + } + + clientRect.left += scrollX; + clientRect.right += scrollX; + clientRect.top += scrollY; + clientRect.bottom += scrollY; + clientRect.width = 1; + + margin = node.offsetWidth - node.clientWidth; + + if (margin > 0) { + if (before) { + margin *= -1; + } + + clientRect.left += margin; + clientRect.right += margin; + } + + return clientRect; + } + + function trimInlineCaretContainers() { + var contentEditableFalseNodes, node, sibling, i, data; + + contentEditableFalseNodes = DomQuery('*[contentEditable=false]', rootNode); + for (i = 0; i < contentEditableFalseNodes.length; i++) { + node = contentEditableFalseNodes[i]; + + sibling = node.previousSibling; + if (CaretContainer.endsWithCaretContainer(sibling)) { + data = sibling.data; + + if (data.length == 1) { + sibling.parentNode.removeChild(sibling); + } else { + sibling.deleteData(data.length - 1, 1); + } + } + + sibling = node.nextSibling; + if (CaretContainer.startsWithCaretContainer(sibling)) { + data = sibling.data; + + if (data.length == 1) { + sibling.parentNode.removeChild(sibling); + } else { + sibling.deleteData(0, 1); + } + } + } + + return null; + } + + function show(before, node) { + var clientRect, rng; + + hide(); + + if (isTableCell(node)) { + return null; + } + + if (isBlock(node)) { + caretContainerNode = CaretContainer.insertBlock('p', node, before); + clientRect = getAbsoluteClientRect(node, before); + DomQuery(caretContainerNode).css('top', clientRect.top); + + $lastVisualCaret = DomQuery('').css(clientRect).appendTo(rootNode); + + if (before) { + $lastVisualCaret.addClass('mce-visual-caret-before'); + } + + startBlink(); + + rng = node.ownerDocument.createRange(); + rng.setStart(caretContainerNode, 0); + rng.setEnd(caretContainerNode, 0); + } else { + caretContainerNode = CaretContainer.insertInline(node, before); + rng = node.ownerDocument.createRange(); + + if (isContentEditableFalse(caretContainerNode.nextSibling)) { + rng.setStart(caretContainerNode, 0); + rng.setEnd(caretContainerNode, 0); + } else { + rng.setStart(caretContainerNode, 1); + rng.setEnd(caretContainerNode, 1); + } + + return rng; + } + + return rng; + } + + function hide() { + trimInlineCaretContainers(); + + if (caretContainerNode) { + CaretContainerRemove.remove(caretContainerNode); + caretContainerNode = null; + } + + if ($lastVisualCaret) { + $lastVisualCaret.remove(); + $lastVisualCaret = null; + } + + clearInterval(cursorInterval); + } + + function startBlink() { + cursorInterval = Delay.setInterval(function () { + DomQuery('div.mce-visual-caret', rootNode).toggleClass('mce-visual-caret-hidden'); + }, 500); + } + + function destroy() { + Delay.clearInterval(cursorInterval); + } + + function getCss() { + return ( + '.mce-visual-caret {' + + 'position: absolute;' + + 'background-color: black;' + + 'background-color: currentcolor;' + + '}' + + '.mce-visual-caret-hidden {' + + 'display: none;' + + '}' + + '*[data-mce-caret] {' + + 'position: absolute;' + + 'left: -1000px;' + + 'right: auto;' + + 'top: 0;' + + 'margin: 0;' + + 'padding: 0;' + + '}' + ); + } + + return { + show: show, + hide: hide, + getCss: getCss, + destroy: destroy + }; + }; + } +); /** * SelectionOverrides.js * @@ -42079,23 +42546,32 @@ define( define( 'tinymce.core.SelectionOverrides', [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.SelectorFind', + 'tinymce.core.DragDropOverrides', + 'tinymce.core.EditorView', + 'tinymce.core.Env', 'tinymce.core.caret.CaretContainer', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', 'tinymce.core.caret.CaretWalker', 'tinymce.core.caret.FakeCaret', 'tinymce.core.caret.LineUtils', + 'tinymce.core.dom.ElementType', 'tinymce.core.dom.NodeType', - 'tinymce.core.DragDropOverrides', - 'tinymce.core.Env', - 'tinymce.core.geom.ClientRect', + 'tinymce.core.dom.RangePoint', 'tinymce.core.keyboard.CefUtils', - 'tinymce.core.util.Arr', 'tinymce.core.util.Delay', - 'tinymce.core.util.Fun', 'tinymce.core.util.VK' ], - function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, FakeCaret, LineUtils, NodeType, DragDropOverrides, Env, ClientRect, CefUtils, Arr, Delay, Fun, VK) { + function ( + Arr, Remove, Element, Attr, SelectorFilter, SelectorFind, DragDropOverrides, EditorView, Env, CaretContainer, CaretPosition, CaretUtils, CaretWalker, FakeCaret, + LineUtils, ElementType, NodeType, RangePoint, CefUtils, Delay, VK + ) { var isContentEditableTrue = NodeType.isContentEditableTrue, isContentEditableFalse = NodeType.isContentEditableFalse, isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse, @@ -42186,22 +42662,12 @@ define( return null; } - function isXYWithinRange(clientX, clientY, range) { - if (range.collapsed) { - return false; - } - - return Arr.reduce(range.getClientRects(), function (state, rect) { - return state || ClientRect.containsXY(rect, clientX, clientY); - }, false); - } - // Some browsers (Chrome) lets you place the caret after a cE=false // Make sure we render the caret container in this case - editor.on('mouseup', function () { + editor.on('mouseup', function (e) { var range = getRange(); - if (range.collapsed) { + if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) { setRange(CefUtils.renderCaretAtRange(editor, range)); } }); @@ -42288,6 +42754,10 @@ define( editor.on('mousedown', function (e) { var contentEditableRoot; + if (EditorView.isXYInContentArea(editor, e.clientX, e.clientY) === false) { + return; + } + contentEditableRoot = getContentEditableRoot(e.target); if (contentEditableRoot) { if (isContentEditableFalse(contentEditableRoot)) { @@ -42297,8 +42767,8 @@ define( removeContentEditableSelection(); // Check that we're not attempting a shift + click select within a contenteditable='true' element - if (!(isContentEditableTrue(contentEditableRoot) && e.shiftKey) && !isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) { - editor.selection.placeCaretAt(e.clientX, e.clientY); + if (!(isContentEditableTrue(contentEditableRoot) && e.shiftKey) && !RangePoint.isXYWithinRange(e.clientX, e.clientY, editor.selection.getRng())) { + ElementType.isVoid(Element.fromDom(e.target)) ? editor.selection.select(e.target) : editor.selection.placeCaretAt(e.clientX, e.clientY); } } } else { @@ -42490,8 +42960,16 @@ define( return null; } + $realSelectionContainer = SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).fold( + function () { + return $([]); + }, + function (elm) { + return $([elm.dom()]); + } + ); + targetClone = e.targetClone; - $realSelectionContainer = $('#' + realSelectionId); if ($realSelectionContainer.length === 0) { $realSelectionContainer = $( '' @@ -42524,7 +43002,10 @@ define( sel.removeAllRanges(); sel.addRange(range); - editor.$('*[data-mce-selected]').removeAttr('data-mce-selected'); + Arr.each(SelectorFilter.descendants(Element.fromDom(editor.getBody()), '*[data-mce-selected]'), function (elm) { + Attr.remove(elm, 'data-mce-selected'); + }); + node.setAttribute('data-mce-selected', 1); selectedContentEditableNode = node; hideFakeCaret(); @@ -42535,7 +43016,7 @@ define( function removeContentEditableSelection() { if (selectedContentEditableNode) { selectedContentEditableNode.removeAttribute('data-mce-selected'); - editor.$('#' + realSelectionId).remove(); + SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove); selectedContentEditableNode = null; } } @@ -42566,6 +43047,744 @@ define( } ); +/** + * Diff.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * JS Implementation of the O(ND) Difference Algorithm by Eugene W. Myers. + * + * @class tinymce.undo.Diff + * @private + */ +define( + 'tinymce.core.undo.Diff', + [ + ], + function () { + var KEEP = 0, INSERT = 1, DELETE = 2; + + var diff = function (left, right) { + var size = left.length + right.length + 2; + var vDown = new Array(size); + var vUp = new Array(size); + + var snake = function (start, end, diag) { + return { + start: start, + end: end, + diag: diag + }; + }; + + var buildScript = function (start1, end1, start2, end2, script) { + var middle = getMiddleSnake(start1, end1, start2, end2); + + if (middle === null || middle.start === end1 && middle.diag === end1 - end2 || + middle.end === start1 && middle.diag === start1 - start2) { + var i = start1; + var j = start2; + while (i < end1 || j < end2) { + if (i < end1 && j < end2 && left[i] === right[j]) { + script.push([KEEP, left[i]]); + ++i; + ++j; + } else { + if (end1 - start1 > end2 - start2) { + script.push([DELETE, left[i]]); + ++i; + } else { + script.push([INSERT, right[j]]); + ++j; + } + } + } + } else { + buildScript(start1, middle.start, start2, middle.start - middle.diag, script); + for (var i2 = middle.start; i2 < middle.end; ++i2) { + script.push([KEEP, left[i2]]); + } + buildScript(middle.end, end1, middle.end - middle.diag, end2, script); + } + }; + + var buildSnake = function (start, diag, end1, end2) { + var end = start; + while (end - diag < end2 && end < end1 && left[end] === right[end - diag]) { + ++end; + } + return snake(start, end, diag); + }; + + var getMiddleSnake = function (start1, end1, start2, end2) { + // Myers Algorithm + // Initialisations + var m = end1 - start1; + var n = end2 - start2; + if (m === 0 || n === 0) { + return null; + } + + var delta = m - n; + var sum = n + m; + var offset = (sum % 2 === 0 ? sum : sum + 1) / 2; + vDown[1 + offset] = start1; + vUp[1 + offset] = end1 + 1; + + for (var d = 0; d <= offset; ++d) { + // Down + for (var k = -d; k <= d; k += 2) { + // First step + + var i = k + offset; + if (k === -d || k != d && vDown[i - 1] < vDown[i + 1]) { + vDown[i] = vDown[i + 1]; + } else { + vDown[i] = vDown[i - 1] + 1; + } + + var x = vDown[i]; + var y = x - start1 + start2 - k; + + while (x < end1 && y < end2 && left[x] === right[y]) { + vDown[i] = ++x; + ++y; + } + // Second step + if (delta % 2 != 0 && delta - d <= k && k <= delta + d) { + if (vUp[i - delta] <= vDown[i]) { + return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2); + } + } + } + + // Up + for (k = delta - d; k <= delta + d; k += 2) { + // First step + i = k + offset - delta; + if (k === delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) { + vUp[i] = vUp[i + 1] - 1; + } else { + vUp[i] = vUp[i - 1]; + } + + x = vUp[i] - 1; + y = x - start1 + start2 - k; + while (x >= start1 && y >= start2 && left[x] === right[y]) { + vUp[i] = x--; + y--; + } + // Second step + if (delta % 2 === 0 && -d <= k && k <= d) { + if (vUp[i] <= vDown[i + delta]) { + return buildSnake(vUp[i], k + start1 - start2, end1, end2); + } + } + } + } + }; + + var script = []; + buildScript(0, left.length, 0, right.length, script); + return script; + }; + + return { + KEEP: KEEP, + DELETE: DELETE, + INSERT: INSERT, + diff: diff + }; + } +); +/** + * Fragments.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module reads and applies html fragments from/to dom nodes. + * + * @class tinymce.undo.Fragments + * @private + */ +define( + 'tinymce.core.undo.Fragments', + [ + "tinymce.core.util.Arr", + "tinymce.core.html.Entities", + "tinymce.core.undo.Diff" + ], + function (Arr, Entities, Diff) { + var getOuterHtml = function (elm) { + if (elm.nodeType === 1) { + return elm.outerHTML; + } else if (elm.nodeType === 3) { + return Entities.encodeRaw(elm.data, false); + } else if (elm.nodeType === 8) { + return ''; + } + + return ''; + }; + + var createFragment = function (html) { + var frag, node, container; + + container = document.createElement("div"); + frag = document.createDocumentFragment(); + + if (html) { + container.innerHTML = html; + } + + while ((node = container.firstChild)) { + frag.appendChild(node); + } + + return frag; + }; + + var insertAt = function (elm, html, index) { + var fragment = createFragment(html); + if (elm.hasChildNodes() && index < elm.childNodes.length) { + var target = elm.childNodes[index]; + target.parentNode.insertBefore(fragment, target); + } else { + elm.appendChild(fragment); + } + }; + + var removeAt = function (elm, index) { + if (elm.hasChildNodes() && index < elm.childNodes.length) { + var target = elm.childNodes[index]; + target.parentNode.removeChild(target); + } + }; + + var applyDiff = function (diff, elm) { + var index = 0; + Arr.each(diff, function (action) { + if (action[0] === Diff.KEEP) { + index++; + } else if (action[0] === Diff.INSERT) { + insertAt(elm, action[1], index); + index++; + } else if (action[0] === Diff.DELETE) { + removeAt(elm, index); + } + }); + }; + + var read = function (elm) { + return Arr.filter(Arr.map(elm.childNodes, getOuterHtml), function (item) { + return item.length > 0; + }); + }; + + var write = function (fragments, elm) { + var currentFragments = Arr.map(elm.childNodes, getOuterHtml); + applyDiff(Diff.diff(currentFragments, fragments), elm); + return elm; + }; + + return { + read: read, + write: write + }; + } +); +/** + * Levels.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This module handles getting/setting undo levels to/from editor instances. + * + * @class tinymce.undo.Levels + * @private + */ +define( + 'tinymce.core.undo.Levels', + [ + "tinymce.core.util.Arr", + "tinymce.core.undo.Fragments" + ], + function (Arr, Fragments) { + var hasIframes = function (html) { + return html.indexOf('') !== -1; + }; + + var createFragmentedLevel = function (fragments) { + return { + type: 'fragmented', + fragments: fragments, + content: '', + bookmark: null, + beforeBookmark: null + }; + }; + + var createCompleteLevel = function (content) { + return { + type: 'complete', + fragments: null, + content: content, + bookmark: null, + beforeBookmark: null + }; + }; + + var createFromEditor = function (editor) { + var fragments, content, trimmedFragments; + + fragments = Fragments.read(editor.getBody()); + trimmedFragments = Arr.map(fragments, function (html) { + return editor.serializer.trimContent(html); + }); + content = trimmedFragments.join(''); + + return hasIframes(content) ? createFragmentedLevel(trimmedFragments) : createCompleteLevel(content); + }; + + var applyToEditor = function (editor, level, before) { + if (level.type === 'fragmented') { + Fragments.write(level.fragments, editor.getBody()); + } else { + editor.setContent(level.content, { format: 'raw' }); + } + + editor.selection.moveToBookmark(before ? level.beforeBookmark : level.bookmark); + }; + + var getLevelContent = function (level) { + return level.type === 'fragmented' ? level.fragments.join('') : level.content; + }; + + var isEq = function (level1, level2) { + return !!level1 && !!level2 && getLevelContent(level1) === getLevelContent(level2); + }; + + return { + createFragmentedLevel: createFragmentedLevel, + createCompleteLevel: createCompleteLevel, + createFromEditor: createFromEditor, + applyToEditor: applyToEditor, + isEq: isEq + }; + } +); +/** + * UndoManager.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed. + * + * @class tinymce.UndoManager + */ +define( + 'tinymce.core.UndoManager', + [ + "tinymce.core.util.VK", + "tinymce.core.util.Tools", + "tinymce.core.undo.Levels" + ], + function (VK, Tools, Levels) { + return function (editor) { + var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0; + + var isUnlocked = function () { + return locks === 0; + }; + + var setTyping = function (typing) { + if (isUnlocked()) { + self.typing = typing; + } + }; + + function setDirty(state) { + editor.setDirty(state); + } + + function addNonTypingUndoLevel(e) { + setTyping(false); + self.add({}, e); + } + + function endTyping() { + if (self.typing) { + setTyping(false); + self.add(); + } + } + + // Add initial undo level when the editor is initialized + editor.on('init', function () { + self.add(); + }); + + // Get position before an execCommand is processed + editor.on('BeforeExecCommand', function (e) { + var cmd = e.command; + + if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { + endTyping(); + self.beforeChange(); + } + }); + + // Add undo level after an execCommand call was made + editor.on('ExecCommand', function (e) { + var cmd = e.command; + + if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') { + addNonTypingUndoLevel(e); + } + }); + + editor.on('ObjectResizeStart Cut', function () { + self.beforeChange(); + }); + + editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel); + editor.on('DragEnd', addNonTypingUndoLevel); + + editor.on('KeyUp', function (e) { + var keyCode = e.keyCode; + + // If key is prevented then don't add undo level + // This would happen on keyboard shortcuts for example + if (e.isDefaultPrevented()) { + return; + } + + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) { + addNonTypingUndoLevel(); + editor.nodeChanged(); + } + + if (keyCode === 46 || keyCode === 8) { + editor.nodeChanged(); + } + + // Fire a TypingUndo/Change event on the first character entered + if (isFirstTypedCharacter && self.typing && Levels.isEq(Levels.createFromEditor(editor), data[0]) === false) { + if (editor.isDirty() === false) { + setDirty(true); + editor.fire('change', { level: data[0], lastLevel: null }); + } + + editor.fire('TypingUndo'); + isFirstTypedCharacter = false; + editor.nodeChanged(); + } + }); + + editor.on('KeyDown', function (e) { + var keyCode = e.keyCode; + + // If key is prevented then don't add undo level + // This would happen on keyboard shortcuts for example + if (e.isDefaultPrevented()) { + return; + } + + // Is character position keys left,right,up,down,home,end,pgdown,pgup,enter + if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) { + if (self.typing) { + addNonTypingUndoLevel(e); + } + + return; + } + + // If key isn't Ctrl+Alt/AltGr + var modKey = (e.ctrlKey && !e.altKey) || e.metaKey; + if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) { + self.beforeChange(); + setTyping(true); + self.add({}, e); + isFirstTypedCharacter = true; + } + }); + + editor.on('MouseDown', function (e) { + if (self.typing) { + addNonTypingUndoLevel(e); + } + }); + + // Add keyboard shortcuts for undo/redo keys + editor.addShortcut('meta+z', '', 'Undo'); + editor.addShortcut('meta+y,meta+shift+z', '', 'Redo'); + + editor.on('AddUndo Undo Redo ClearUndos', function (e) { + if (!e.isDefaultPrevented()) { + editor.nodeChanged(); + } + }); + + /*eslint consistent-this:0 */ + self = { + // Explode for debugging reasons + data: data, + + /** + * State if the user is currently typing or not. This will add a typing operation into one undo + * level instead of one new level for each keystroke. + * + * @field {Boolean} typing + */ + typing: false, + + /** + * Stores away a bookmark to be used when performing an undo action so that the selection is before + * the change has been made. + * + * @method beforeChange + */ + beforeChange: function () { + if (isUnlocked()) { + beforeBookmark = editor.selection.getBookmark(2, true); + } + }, + + /** + * Adds a new undo level/snapshot to the undo list. + * + * @method add + * @param {Object} level Optional undo level object to add. + * @param {DOMEvent} event Optional event responsible for the creation of the undo level. + * @return {Object} Undo level that got added or null it a level wasn't needed. + */ + add: function (level, event) { + var i, settings = editor.settings, lastLevel, currentLevel; + + currentLevel = Levels.createFromEditor(editor); + level = level || {}; + level = Tools.extend(level, currentLevel); + + if (isUnlocked() === false || editor.removed) { + return null; + } + + lastLevel = data[index]; + if (editor.fire('BeforeAddUndo', { level: level, lastLevel: lastLevel, originalEvent: event }).isDefaultPrevented()) { + return null; + } + + // Add undo level if needed + if (lastLevel && Levels.isEq(lastLevel, level)) { + return null; + } + + // Set before bookmark on previous level + if (data[index]) { + data[index].beforeBookmark = beforeBookmark; + } + + // Time to compress + if (settings.custom_undo_redo_levels) { + if (data.length > settings.custom_undo_redo_levels) { + for (i = 0; i < data.length - 1; i++) { + data[i] = data[i + 1]; + } + + data.length--; + index = data.length; + } + } + + // Get a non intrusive normalized bookmark + level.bookmark = editor.selection.getBookmark(2, true); + + // Crop array if needed + if (index < data.length - 1) { + data.length = index + 1; + } + + data.push(level); + index = data.length - 1; + + var args = { level: level, lastLevel: lastLevel, originalEvent: event }; + + editor.fire('AddUndo', args); + + if (index > 0) { + setDirty(true); + editor.fire('change', args); + } + + return level; + }, + + /** + * Undoes the last action. + * + * @method undo + * @return {Object} Undo level or null if no undo was performed. + */ + undo: function () { + var level; + + if (self.typing) { + self.add(); + self.typing = false; + setTyping(false); + } + + if (index > 0) { + level = data[--index]; + Levels.applyToEditor(editor, level, true); + setDirty(true); + editor.fire('undo', { level: level }); + } + + return level; + }, + + /** + * Redoes the last action. + * + * @method redo + * @return {Object} Redo level or null if no redo was performed. + */ + redo: function () { + var level; + + if (index < data.length - 1) { + level = data[++index]; + Levels.applyToEditor(editor, level, false); + setDirty(true); + editor.fire('redo', { level: level }); + } + + return level; + }, + + /** + * Removes all undo levels. + * + * @method clear + */ + clear: function () { + data = []; + index = 0; + self.typing = false; + self.data = data; + editor.fire('ClearUndos'); + }, + + /** + * Returns true/false if the undo manager has any undo levels. + * + * @method hasUndo + * @return {Boolean} true/false if the undo manager has any undo levels. + */ + hasUndo: function () { + // Has undo levels or typing and content isn't the same as the initial level + return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0])); + }, + + /** + * Returns true/false if the undo manager has any redo levels. + * + * @method hasRedo + * @return {Boolean} true/false if the undo manager has any redo levels. + */ + hasRedo: function () { + return index < data.length - 1 && !self.typing; + }, + + /** + * Executes the specified mutator function as an undo transaction. The selection + * before the modification will be stored to the undo stack and if the DOM changes + * it will add a new undo level. Any logic within the translation that adds undo levels will + * be ignored. So a translation can include calls to execCommand or editor.insertContent. + * + * @method transact + * @param {function} callback Function that gets executed and has dom manipulation logic in it. + * @return {Object} Undo level that got added or null it a level wasn't needed. + */ + transact: function (callback) { + endTyping(); + self.beforeChange(); + self.ignore(callback); + return self.add(); + }, + + /** + * Executes the specified mutator function as an undo transaction. But without adding an undo level. + * Any logic within the translation that adds undo levels will be ignored. So a translation can + * include calls to execCommand or editor.insertContent. + * + * @method ignore + * @param {function} callback Function that gets executed and has dom manipulation logic in it. + * @return {Object} Undo level that got added or null it a level wasn't needed. + */ + ignore: function (callback) { + try { + locks++; + callback(); + } finally { + locks--; + } + }, + + /** + * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack + * then roll back that change and do the second mutation on top of the stack. This will produce an extra + * undo level that the user doesn't see until they undo. + * + * @method extra + * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level. + * @param {function} callback2 Function that does mutation but gets displayed to the user. + */ + extra: function (callback1, callback2) { + var lastLevel, bookmark; + + if (self.transact(callback1)) { + bookmark = data[index].bookmark; + lastLevel = data[index - 1]; + Levels.applyToEditor(editor, lastLevel, true); + + if (self.transact(callback2)) { + data[index - 1].beforeBookmark = bookmark; + } + } + } + }; + + return self; + }; + } +); + /** * NodePath.js * @@ -43430,7 +44649,11 @@ define( // Normalize selection for example a|a becomes a|a except for Ctrl+A since it selects everything editor.on('keyup focusin mouseup', function (e) { if (e.keyCode != 65 || !VK.metaKeyPressed(e)) { - selection.normalize(); + // We can't normalize on non collapsed ranges on keyboard events since that would cause + // issues with moving the selection over empty paragraphs. See #TINY-1130 + if (e.type !== 'keyup' || editor.selection.isCollapsed()) { + selection.normalize(); + } } }, true); } @@ -43670,50 +44893,6 @@ define( return (!sel || !sel.rangeCount || sel.rangeCount === 0); } - /** - * Properly empties the editor if all contents is selected and deleted this to - * prevent empty paragraphs from being produced at beginning/end of contents. - */ - function emptyEditorOnDeleteEverything() { - function isEverythingSelected(editor) { - var caretWalker = new CaretWalker(editor.getBody()); - var rng = editor.selection.getRng(); - var startCaretPos = CaretPosition.fromRangeStart(rng); - var endCaretPos = CaretPosition.fromRangeEnd(rng); - var prev = caretWalker.prev(startCaretPos); - var next = caretWalker.next(endCaretPos); - - return !editor.selection.isCollapsed() && - (!prev || (prev.isAtStart() && startCaretPos.isEqual(prev))) && - (!next || (next.isAtEnd() && startCaretPos.isEqual(next))); - } - - // Type over case delete and insert this won't cover typeover with a IME but at least it covers the common case - editor.on('keypress', function (e) { - if (!isDefaultPrevented(e) && !selection.isCollapsed() && e.charCode > 31 && !VK.metaKeyPressed(e)) { - if (isEverythingSelected(editor)) { - e.preventDefault(); - editor.setContent(String.fromCharCode(e.charCode)); - editor.selection.select(editor.getBody(), true); - editor.selection.collapse(false); - editor.nodeChanged(); - } - } - }); - - editor.on('keydown', function (e) { - var keyCode = e.keyCode; - - if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { - if (isEverythingSelected(editor)) { - e.preventDefault(); - editor.setContent(''); - editor.nodeChanged(); - } - } - }); - } - // All browsers removeBlockQuoteOnBackSpace(); emptyEditorWhenDeleting(); @@ -43726,7 +44905,6 @@ define( // WebKit if (isWebKit) { - emptyEditorOnDeleteEverything(); inputMethodFocus(); selectControlElements(); setDefaultBlockType(); @@ -43771,7 +44949,6 @@ define( // Gecko if (isGecko) { - emptyEditorOnDeleteEverything(); removeHrOnBackspace(); focusBody(); removeStylesWhenDeletingAcrossBlockElements(); @@ -43803,8 +44980,12 @@ define( define( 'tinymce.core.init.InitContentBody', [ + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Attr', 'global!document', 'global!window', + 'tinymce.core.api.Formatter', 'tinymce.core.caret.CaretContainerInput', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.Selection', @@ -43812,7 +44993,6 @@ define( 'tinymce.core.EditorUpload', 'tinymce.core.ErrorReporter', 'tinymce.core.ForceBlocks', - 'tinymce.core.Formatter', 'tinymce.core.html.DomParser', 'tinymce.core.html.Node', 'tinymce.core.html.Schema', @@ -43825,11 +45005,19 @@ define( 'tinymce.core.util.Tools' ], function ( - document, window, CaretContainerInput, DOMUtils, Selection, Serializer, EditorUpload, ErrorReporter, ForceBlocks, Formatter, DomParser, Node, Schema, KeyboardOverrides, - NodeChange, SelectionOverrides, UndoManager, Delay, Quirks, Tools + Insert, Element, Attr, document, window, Formatter, CaretContainerInput, DOMUtils, Selection, Serializer, EditorUpload, ErrorReporter, ForceBlocks, DomParser, + Node, Schema, KeyboardOverrides, NodeChange, SelectionOverrides, UndoManager, Delay, Quirks, Tools ) { var DOM = DOMUtils.DOM; + var appendStyle = function (editor, text) { + var head = Element.fromDom(editor.getDoc().head); + var tag = Element.fromTag('style'); + Attr.set(tag, 'type', 'text/css'); + Insert.append(tag, Element.fromText(text)); + Insert.append(head, tag); + }; + var createParser = function (editor) { var parser = new DomParser(editor.settings, editor.schema); @@ -43935,6 +45123,10 @@ define( autoFocus(editor); }; + var getStyleSheetLoader = function (editor) { + return editor.inline ? DOM.styleSheetLoader : editor.dom.styleSheetLoader; + }; + var initContentBody = function (editor, skipWrite) { var settings = editor.settings, targetElm = editor.getElement(), doc = editor.getDoc(), body, contentCssText; @@ -44072,16 +45264,20 @@ define( editor.dom.addStyle(contentCssText); } - editor.dom.styleSheetLoader.loadAll( + getStyleSheetLoader(editor).loadAll( editor.contentCSS, function (_) { initEditor(editor); }, function (urls) { initEditor(editor); - ErrorReporter.contentCssError(editor, urls); } ); + + // Append specified content CSS last + if (settings.content_style) { + appendStyle(editor, settings.content_style); + } }; return { @@ -44181,11 +45377,16 @@ define( } }; + var trimLegacyPrefix = function (name) { + // Themes and plugins can be prefixed with - to prevent them from being lazy loaded + return name.replace(/^\-/, ''); + }; + var initPlugins = function (editor) { var initializedPlugins = []; - Tools.each(editor.settings.plugins.replace(/\-/g, '').split(/[ ,]/), function (name) { - initPlugin(editor, initializedPlugins, name); + Tools.each(editor.settings.plugins.split(/[ ,]/), function (name) { + initPlugin(editor, initializedPlugins, trimLegacyPrefix(name)); }); }; @@ -44194,7 +45395,8 @@ define( if (settings.theme) { if (typeof settings.theme != "function") { - settings.theme = settings.theme.replace(/-/, ''); + settings.theme = trimLegacyPrefix(settings.theme); + Theme = ThemeManager.get(settings.theme); editor.theme = new Theme(editor, ThemeManager.urls[settings.theme]); @@ -44379,11 +45581,6 @@ define( }); } - // Load specified content CSS last - if (settings.content_style) { - editor.contentStyles.push(settings.content_style); - } - // Content editable mode ends here if (settings.content_editable) { return InitContentBody.initContentBody(editor); @@ -44424,19 +45621,19 @@ define( 'tinymce.core.init.Render', [ 'global!window', + 'tinymce.core.api.NotificationManager', + 'tinymce.core.api.WindowManager', 'tinymce.core.dom.DOMUtils', 'tinymce.core.dom.EventUtils', 'tinymce.core.dom.ScriptLoader', 'tinymce.core.Env', 'tinymce.core.ErrorReporter', 'tinymce.core.init.Init', - 'tinymce.core.NotificationManager', 'tinymce.core.PluginManager', 'tinymce.core.ThemeManager', - 'tinymce.core.util.Tools', - 'tinymce.core.WindowManager' + 'tinymce.core.util.Tools' ], - function (window, DOMUtils, EventUtils, ScriptLoader, Env, ErrorReporter, Init, NotificationManager, PluginManager, ThemeManager, Tools, WindowManager) { + function (window, NotificationManager, WindowManager, DOMUtils, EventUtils, ScriptLoader, Env, ErrorReporter, Init, PluginManager, ThemeManager, Tools) { var DOM = DOMUtils.DOM; var loadScripts = function (editor, suffix) { @@ -44714,6 +45911,227 @@ define( }; } ); +/** + * Shortcuts.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * Contains logic for handling keyboard shortcuts. + * + * @class tinymce.Shortcuts + * @example + * editor.shortcuts.add('ctrl+a', "description of the shortcut", function() {}); + * editor.shortcuts.add('meta+a', "description of the shortcut", function() {}); // "meta" maps to Command on Mac and Ctrl on PC + * editor.shortcuts.add('ctrl+alt+a', "description of the shortcut", function() {}); + * editor.shortcuts.add('access+a', "description of the shortcut", function() {}); // "access" maps to ctrl+alt on Mac and shift+alt on PC + */ +define( + 'tinymce.core.Shortcuts', + [ + 'tinymce.core.util.Tools', + 'tinymce.core.Env' + ], + function (Tools, Env) { + var each = Tools.each, explode = Tools.explode; + + var keyCodeLookup = { + "f9": 120, + "f10": 121, + "f11": 122 + }; + + var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access'); + + return function (editor) { + var self = this, shortcuts = {}, pendingPatterns = []; + + function parseShortcut(pattern) { + var id, key, shortcut = {}; + + // Parse modifiers and keys ctrl+alt+b for example + each(explode(pattern, '+'), function (value) { + if (value in modifierNames) { + shortcut[value] = true; + } else { + // 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); + } + } + }); + + // Generate unique id for modifier combination and set default state for unused modifiers + id = [shortcut.keyCode]; + for (key in modifierNames) { + if (shortcut[key]) { + id.push(key); + } else { + shortcut[key] = false; + } + } + shortcut.id = id.join(','); + + // Handle special access modifier differently depending on Mac/Win + if (shortcut.access) { + shortcut.alt = true; + + if (Env.mac) { + shortcut.ctrl = true; + } else { + shortcut.shift = true; + } + } + + // Handle special meta modifier differently depending on Mac/Win + if (shortcut.meta) { + if (Env.mac) { + shortcut.meta = true; + } else { + shortcut.ctrl = true; + shortcut.meta = false; + } + } + + return shortcut; + } + + function createShortcut(pattern, desc, cmdFunc, scope) { + var shortcuts; + + shortcuts = Tools.map(explode(pattern, '>'), parseShortcut); + shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], { + func: cmdFunc, + scope: scope || editor + }); + + return Tools.extend(shortcuts[0], { + desc: editor.translate(desc), + subpatterns: shortcuts.slice(1) + }); + } + + function hasModifier(e) { + return e.altKey || e.ctrlKey || e.metaKey; + } + + function isFunctionKey(e) { + return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123; + } + + function matchShortcut(e, shortcut) { + if (!shortcut) { + return false; + } + + if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) { + return false; + } + + if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { + return false; + } + + if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { + e.preventDefault(); + return true; + } + + return false; + } + + function executeShortcutAction(shortcut) { + return shortcut.func ? shortcut.func.call(shortcut.scope) : null; + } + + editor.on('keyup keypress keydown', function (e) { + if ((hasModifier(e) || isFunctionKey(e)) && !e.isDefaultPrevented()) { + each(shortcuts, function (shortcut) { + if (matchShortcut(e, shortcut)) { + pendingPatterns = shortcut.subpatterns.slice(0); + + if (e.type == "keydown") { + executeShortcutAction(shortcut); + } + + return true; + } + }); + + if (matchShortcut(e, pendingPatterns[0])) { + if (pendingPatterns.length === 1) { + if (e.type == "keydown") { + executeShortcutAction(pendingPatterns[0]); + } + } + + pendingPatterns.shift(); + } + } + }); + + /** + * Adds a keyboard shortcut for some command or function. + * + * @method add + * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. + * @param {String} desc Text description for the command. + * @param {String/Function} cmdFunc Command name string or function to execute when the key is pressed. + * @param {Object} scope Optional scope to execute the function in. + * @return {Boolean} true/false state if the shortcut was added or not. + */ + self.add = function (pattern, desc, cmdFunc, scope) { + var cmd; + + cmd = cmdFunc; + + if (typeof cmdFunc === 'string') { + cmdFunc = function () { + editor.execCommand(cmd, false, null); + }; + } else if (Tools.isArray(cmd)) { + cmdFunc = function () { + editor.execCommand(cmd[0], cmd[1], cmd[2]); + }; + } + + each(explode(Tools.trim(pattern.toLowerCase())), function (pattern) { + var shortcut = createShortcut(pattern, desc, cmdFunc, scope); + shortcuts[shortcut.id] = shortcut; + }); + + return true; + }; + + /** + * Remove a keyboard shortcut by pattern. + * + * @method remove + * @param {String} pattern Shortcut pattern. Like for example: ctrl+alt+o. + * @return {Boolean} true/false state if the shortcut was removed or not. + */ + self.remove = function (pattern) { + var shortcut = createShortcut(pattern); + + if (shortcuts[shortcut.id]) { + delete shortcuts[shortcut.id]; + return true; + } + + return false; + }; + }; + } +); + /** * Sidebar.js * @@ -44748,6 +46166,444 @@ define( } ); +/** + * URI.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * This class handles parsing, modification and serialization of URI/URL strings. + * @class tinymce.util.URI + */ +define( + 'tinymce.core.util.URI', + [ + 'global!document', + 'tinymce.core.util.Tools' + ], + function (document, Tools) { + var each = Tools.each, trim = Tools.trim; + var queryParts = "source protocol authority userInfo user password host port relative path directory file query anchor".split(' '); + var DEFAULT_PORTS = { + 'ftp': 21, + 'http': 80, + 'https': 443, + 'mailto': 25 + }; + + /** + * Constructs a new URI instance. + * + * @constructor + * @method URI + * @param {String} url URI string to parse. + * @param {Object} settings Optional settings object. + */ + function URI(url, settings) { + var self = this, baseUri, baseUrl; + + url = trim(url); + 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; + return; + } + + var isProtocolRelative = url.indexOf('//') === 0; + + // Absolute path with no host, fake host and protocol + if (url.indexOf('/') === 0 && !isProtocolRelative) { + url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; + } + + // Relative path http:// or protocol relative //path + if (!/^[\w\-]*:?\/\//.test(url)) { + baseUrl = settings.base_uri ? settings.base_uri.path : new URI(document.location.href).directory; + if (settings.base_uri.protocol === "") { + url = '//mce_host' + self.toAbsPath(baseUrl, url); + } else { + url = /([^#?]*)([#?]?.*)/.exec(url); + url = ((baseUri && baseUri.protocol) || 'http') + '://mce_host' + self.toAbsPath(baseUrl, 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 + + /*jshint maxlen: 255 */ + /*eslint max-len: 0 */ + url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); + + each(queryParts, function (v, i) { + var part = url[i]; + + // Zope 3 workaround, they use @@something + if (part) { + part = part.replace(/\(mce_at\)/g, '@@'); + } + + self[v] = part; + }); + + if (baseUri) { + if (!self.protocol) { + self.protocol = baseUri.protocol; + } + + if (!self.userInfo) { + self.userInfo = baseUri.userInfo; + } + + if (!self.port && self.host === 'mce_host') { + self.port = baseUri.port; + } + + if (!self.host || self.host === 'mce_host') { + self.host = baseUri.host; + } + + self.source = ''; + } + + if (isProtocolRelative) { + self.protocol = ''; + } + + //t.path = t.path || '/'; + } + + URI.prototype = { + /** + * Sets the internal path part of the URI. + * + * @method setPath + * @param {string} path Path string to set. + */ + setPath: function (path) { + var self = this; + + path = /^(.*?)\/?(\w+)?$/.exec(path); + + // Update path parts + self.path = path[0]; + self.directory = path[1]; + self.file = path[2]; + + // Rebuild source + self.source = ''; + self.getURI(); + }, + + /** + * Converts the specified URI into a relative URI based on the current URI instance location. + * + * @method toRelative + * @param {String} uri URI to convert into a relative path/URI. + * @return {String} Relative URI from the point specified in the current URI instance. + * @example + * // Converts an absolute URL to an relative URL url will be somedir/somefile.htm + * var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm'); + */ + toRelative: function (uri) { + var self = this, output; + + if (uri === "./") { + return uri; + } + + uri = new URI(uri, { base_uri: self }); + + // Not on same domain/port or protocol + if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || + (self.protocol != uri.protocol && uri.protocol !== "")) { + return uri.getURI(); + } + + var tu = self.getURI(), uu = uri.getURI(); + + // Allow usage of the base_uri when relative_urls = true + if (tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) { + return tu; + } + + output = self.toRelPath(self.path, uri.path); + + // Add query + if (uri.query) { + output += '?' + uri.query; + } + + // Add anchor + if (uri.anchor) { + output += '#' + uri.anchor; + } + + return output; + }, + + /** + * Converts the specified URI into a absolute URI based on the current URI instance location. + * + * @method toAbsolute + * @param {String} uri URI to convert into a relative path/URI. + * @param {Boolean} noHost No host and protocol prefix. + * @return {String} Absolute URI from the point specified in the current URI instance. + * @example + * // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm + * var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm'); + */ + toAbsolute: function (uri, noHost) { + uri = new URI(uri, { base_uri: this }); + + return uri.getURI(noHost && this.isSameOrigin(uri)); + }, + + /** + * Determine whether the given URI has the same origin as this URI. Based on RFC-6454. + * Supports default ports for protocols listed in DEFAULT_PORTS. Unsupported protocols will fail safe: they + * won't match, if the port specifications differ. + * + * @method isSameOrigin + * @param {tinymce.util.URI} uri Uri instance to compare. + * @returns {Boolean} True if the origins are the same. + */ + isSameOrigin: function (uri) { + if (this.host == uri.host && this.protocol == uri.protocol) { + if (this.port == uri.port) { + return true; + } + + var defaultPort = DEFAULT_PORTS[this.protocol]; + if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { + return true; + } + } + + return false; + }, + + /** + * Converts a absolute path into a relative path. + * + * @method toRelPath + * @param {String} base Base point to convert the path from. + * @param {String} path Absolute path to convert into a relative path. + */ + toRelPath: function (base, path) { + var items, breakPoint = 0, out = '', i, l; + + // Split the paths + base = base.substring(0, base.lastIndexOf('/')); + base = base.split('/'); + items = path.split('/'); + + if (base.length >= items.length) { + for (i = 0, l = base.length; i < l; i++) { + if (i >= items.length || base[i] != items[i]) { + breakPoint = i + 1; + break; + } + } + } + + if (base.length < items.length) { + for (i = 0, l = items.length; i < l; i++) { + if (i >= base.length || base[i] != items[i]) { + breakPoint = i + 1; + break; + } + } + } + + if (breakPoint === 1) { + return path; + } + + for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { + out += "../"; + } + + for (i = breakPoint - 1, l = items.length; i < l; i++) { + if (i != breakPoint - 1) { + out += "/" + items[i]; + } else { + out += items[i]; + } + } + + return out; + }, + + /** + * Converts a relative path into a absolute path. + * + * @method toAbsPath + * @param {String} base Base point to convert the path from. + * @param {String} path Relative path to convert into an absolute path. + */ + toAbsPath: function (base, path) { + var i, nb = 0, o = [], tr, outPath; + + // Split paths + tr = /\/$/.test(path) ? '/' : ''; + base = base.split('/'); + path = path.split('/'); + + // Remove empty chunks + each(base, function (k) { + if (k) { + o.push(k); + } + }); + + base = o; + + // Merge relURLParts chunks + for (i = path.length - 1, o = []; i >= 0; i--) { + // Ignore empty or . + if (path[i].length === 0 || path[i] === ".") { + continue; + } + + // Is parent + if (path[i] === '..') { + nb++; + continue; + } + + // Move up + if (nb > 0) { + nb--; + continue; + } + + o.push(path[i]); + } + + i = base.length - nb; + + // If /a/b/c or / + if (i <= 0) { + outPath = o.reverse().join('/'); + } else { + outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); + } + + // Add front / if it's needed + if (outPath.indexOf('/') !== 0) { + outPath = '/' + outPath; + } + + // Add traling / if it's needed + if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { + outPath += tr; + } + + return outPath; + }, + + /** + * Returns the full URI of the internal structure. + * + * @method getURI + * @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false. + */ + getURI: function (noProtoHost) { + var s, self = this; + + // Rebuild source + if (!self.source || noProtoHost) { + s = ''; + + if (!noProtoHost) { + if (self.protocol) { + s += self.protocol + '://'; + } else { + s += '//'; + } + + if (self.userInfo) { + s += self.userInfo + '@'; + } + + if (self.host) { + s += self.host; + } + + if (self.port) { + s += ':' + self.port; + } + } + + if (self.path) { + s += self.path; + } + + if (self.query) { + s += '?' + self.query; + } + + if (self.anchor) { + s += '#' + self.anchor; + } + + self.source = s; + } + + return self.source; + } + }; + + URI.parseDataUri = function (uri) { + var type, matches; + + uri = decodeURIComponent(uri).split(','); + + matches = /data:([^;]+)/.exec(uri[0]); + if (matches) { + type = matches[1]; + } + + return { + type: type, + data: uri[1] + }; + }; + + URI.getDocumentBaseUrl = function (loc) { + var baseUrl; + + // Pass applewebdata:// and other non web protocols though + if (loc.protocol.indexOf('http') !== 0 && loc.protocol !== 'file:') { + baseUrl = loc.href; + } else { + baseUrl = loc.protocol + '//' + loc.host + loc.pathname; + } + + if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) { + baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); + + if (!/[\/\\]$/.test(baseUrl)) { + baseUrl += '/'; + } + } + + return baseUrl; + }; + + return URI; + } +); + /** * Editor.js * @@ -44792,7 +46648,9 @@ define( 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.DOMUtils', 'tinymce.core.EditorCommands', + 'tinymce.core.EditorFocus', 'tinymce.core.EditorObservable', + 'tinymce.core.EditorSettings', 'tinymce.core.Env', 'tinymce.core.html.Serializer', 'tinymce.core.init.Render', @@ -44804,14 +46662,14 @@ define( 'tinymce.core.util.Uuid' ], function ( - AddOnManager, DomQuery, DOMUtils, EditorCommands, EditorObservable, Env, Serializer, Render, Mode, - Shortcuts, Sidebar, Tools, URI, Uuid + AddOnManager, DomQuery, DOMUtils, EditorCommands, EditorFocus, EditorObservable, EditorSettings, Env, Serializer, Render, Mode, Shortcuts, Sidebar, Tools, + URI, Uuid ) { // Shorten these names var DOM = DOMUtils.DOM; var extend = Tools.extend, each = Tools.each; var trim = Tools.trim, resolve = Tools.resolve; - var isGecko = Env.gecko, ie = Env.ie; + var ie = Env.ie; /** * Include Editor API docs. @@ -44829,11 +46687,10 @@ define( * @param {tinymce.EditorManager} editorManager EditorManager instance. */ function Editor(id, settings, editorManager) { - var self = this, documentBaseUrl, baseUri, defaultSettings; + var self = this, documentBaseUrl, baseUri; documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; baseUri = editorManager.baseURI; - defaultSettings = editorManager.defaultSettings; /** * Name/value collection with editor settings. @@ -44844,52 +46701,9 @@ define( * // Get the value of the theme setting * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); */ - settings = extend({ - id: id, - theme: 'modern', - delta_width: 0, - delta_height: 0, - popup_css: '', - plugins: '', - document_base_url: documentBaseUrl, - add_form_submit_trigger: true, - submit_patch: true, - add_unload_trigger: true, - convert_urls: true, - relative_urls: true, - remove_script_host: true, - object_resizing: true, - doctype: '', - visual: true, - font_size_style_values: 'xx-small,x-small,small,medium,large,x-large,xx-large', - - // See: http://www.w3.org/TR/CSS2/fonts.html#propdef-font-size - font_size_legacy_values: 'xx-small,small,medium,large,x-large,xx-large,300%', - forced_root_block: 'p', - hidden_input: true, - padd_empty_editor: true, - 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,th,ul,ol,li,dl,dt,dd,area,table,thead,' + - 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist', - indent_after: 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,' + - 'tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist', - validate: true, - entity_encoding: 'named', - url_converter: self.convertURL, - url_converter_scope: self, - ie7_compat: true - }, defaultSettings, settings); - - // Merge external_plugins - if (defaultSettings && defaultSettings.external_plugins && settings.external_plugins) { - settings.external_plugins = extend({}, defaultSettings.external_plugins, settings.external_plugins); - } - + settings = EditorSettings.getEditorSettings(self, id, documentBaseUrl, editorManager.defaultSettings, settings); self.settings = settings; + AddOnManager.language = settings.language || 'en'; AddOnManager.languageLoad = settings.language_load; AddOnManager.baseURL = editorManager.baseURL; @@ -44900,7 +46714,7 @@ define( * @property id * @type String */ - self.id = settings.id = id; + self.id = id; /** * State to force the editor to return false on a isDirty call. @@ -44934,7 +46748,7 @@ define( * // Get absolute URL from the location of document_base_url * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); */ - self.documentBaseURI = new URI(settings.document_base_url || documentBaseUrl, { + self.documentBaseURI = new URI(settings.document_base_url, { base_uri: baseUri }); @@ -44975,7 +46789,6 @@ define( self.suffix = editorManager.suffix; self.editorManager = editorManager; self.inline = settings.inline; - self.settings.content_editable = self.inline; if (settings.cache_suffix) { Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, ''); @@ -45024,78 +46837,7 @@ define( * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor. */ focus: function (skipFocus) { - var self = this, selection = self.selection, contentEditable = self.settings.content_editable, rng; - var controlElm, doc = self.getDoc(), body = self.getBody(), contentEditableHost; - - function getContentEditableHost(node) { - return self.dom.getParent(node, function (node) { - return self.dom.getContentEditable(node) === "true"; - }); - } - - if (self.removed) { - return; - } - - if (!skipFocus) { - // Get selected control element - rng = selection.getRng(); - if (rng.item) { - controlElm = rng.item(0); - } - - self.quirks.refreshContentEditable(); - - // Move focus to contentEditable=true child if needed - contentEditableHost = getContentEditableHost(selection.getNode()); - if (self.$.contains(body, contentEditableHost)) { - contentEditableHost.focus(); - selection.normalize(); - self.editorManager.setActive(self); - return; - } - - // Focus the window iframe - if (!contentEditable) { - // WebKit needs this call to fire focusin event properly see #5948 - // But Opera pre Blink engine will produce an empty selection so skip Opera - if (!Env.opera) { - self.getBody().focus(); - } - - self.getWin().focus(); - } - - // Focus the body as well since it's contentEditable - if (isGecko || contentEditable) { - // Check for setActive since it doesn't scroll to the element - if (body.setActive) { - // IE 11 sometimes throws "Invalid function" then fallback to focus - try { - body.setActive(); - } catch (ex) { - body.focus(); - } - } else { - body.focus(); - } - - if (contentEditable) { - selection.normalize(); - } - } - - // Restore selected control element - // This is needed when for example an image is selected within a - // layer a call to focus will then remove the control selection - if (controlElm && controlElm.ownerDocument == doc) { - rng = doc.body.createControlRange(); - rng.addElement(controlElm); - rng.select(); - } - } - - self.editorManager.setActive(self); + EditorFocus.focus(this, skipFocus); }, /** @@ -45139,16 +46881,14 @@ define( * @return {String} Translated string. */ translate: function (text) { - var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; + if (text && Tools.is(text, 'string')) { + var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; - if (!text) { - return ''; + text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function (a, b) { + return i18n.data[lang + '.' + b] || '{#' + b + '}'; + }); } - text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function (a, b) { - return i18n.data[lang + '.' + b] || '{#' + b + '}'; - }); - return this.editorManager.translate(text); }, @@ -46187,150 +47927,6 @@ define( } ); -/** - * I18n.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * I18n class that handles translation of TinyMCE UI. - * Uses po style with csharp style parameters. - * - * @class tinymce.util.I18n - */ -define( - 'tinymce.core.util.I18n', - [ - "tinymce.core.util.Tools" - ], - function (Tools) { - "use strict"; - - var data = {}, code = "en"; - - return { - /** - * Sets the current language code. - * - * @method setCode - * @param {String} newCode Current language code. - */ - setCode: function (newCode) { - if (newCode) { - code = newCode; - this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false; - } - }, - - /** - * Returns the current language code. - * - * @method getCode - * @return {String} Current language code. - */ - getCode: function () { - return code; - }, - - /** - * Property gets set to true if a RTL language pack was loaded. - * - * @property rtl - * @type Boolean - */ - rtl: false, - - /** - * Adds translations for a specific language code. - * - * @method add - * @param {String} code Language code like sv_SE. - * @param {Array} items Name/value array with English en_US to sv_SE. - */ - add: function (code, items) { - var langData = data[code]; - - if (!langData) { - data[code] = langData = {}; - } - - for (var name in items) { - langData[name] = items[name]; - } - - this.setCode(code); - }, - - /** - * Translates the specified text. - * - * It has a few formats: - * I18n.translate("Text"); - * I18n.translate(["Text {0}/{1}", 0, 1]); - * I18n.translate({raw: "Raw string"}); - * - * @method translate - * @param {String/Object/Array} text Text to translate. - * @return {String} String that got translated. - */ - translate: function (text) { - var langData = data[code] || {}; - - /** - * number - string - * null, undefined and empty string - empty string - * array - comma-delimited string - * object - in [object Object] - * function - in [object Function] - * - * @param obj - * @returns {string} - */ - function toString(obj) { - if (Tools.is(obj, 'function')) { - return Object.prototype.toString.call(obj); - } - return !isEmpty(obj) ? '' + obj : ''; - } - - function isEmpty(text) { - return text === '' || text === null || Tools.is(text, 'undefined'); - } - - function getLangData(text) { - // make sure we work on a string and return a string - text = toString(text); - return Tools.hasOwn(langData, text) ? toString(langData[text]) : text; - } - - - if (isEmpty(text)) { - return ''; - } - - if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) { - return toString(text.raw); - } - - if (Tools.is(text, 'array')) { - var values = text.slice(1); - text = getLangData(text[0]).replace(/\{([0-9]+)\}/g, function ($1, $2) { - return Tools.hasOwn(values, $2) ? toString(values[$2]) : $1; - }); - } - - return getLangData(text).replace(/{context:\w+}$/, ''); - }, - - data: data - }; - } -); /** * FocusManager.js * @@ -46469,25 +48065,6 @@ define( } }); } - - // 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; - - if (activeEditor && activeEditor.selection) { - var rng = activeEditor.selection.getRng(); - - // Store when it's non collapsed - if (rng && !rng.collapsed) { - editor.lastRng = rng; - } - } - }; - - DOM.bind(document, 'selectionchange', selectionChangeHandler); - } } }); @@ -46712,6 +48289,150 @@ define( }; } ); +/** + * I18n.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +/** + * I18n class that handles translation of TinyMCE UI. + * Uses po style with csharp style parameters. + * + * @class tinymce.util.I18n + */ +define( + 'tinymce.core.util.I18n', + [ + "tinymce.core.util.Tools" + ], + function (Tools) { + "use strict"; + + var data = {}, code = "en"; + + return { + /** + * Sets the current language code. + * + * @method setCode + * @param {String} newCode Current language code. + */ + setCode: function (newCode) { + if (newCode) { + code = newCode; + this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false; + } + }, + + /** + * Returns the current language code. + * + * @method getCode + * @return {String} Current language code. + */ + getCode: function () { + return code; + }, + + /** + * Property gets set to true if a RTL language pack was loaded. + * + * @property rtl + * @type Boolean + */ + rtl: false, + + /** + * Adds translations for a specific language code. + * + * @method add + * @param {String} code Language code like sv_SE. + * @param {Array} items Name/value array with English en_US to sv_SE. + */ + add: function (code, items) { + var langData = data[code]; + + if (!langData) { + data[code] = langData = {}; + } + + for (var name in items) { + langData[name] = items[name]; + } + + this.setCode(code); + }, + + /** + * Translates the specified text. + * + * It has a few formats: + * I18n.translate("Text"); + * I18n.translate(["Text {0}/{1}", 0, 1]); + * I18n.translate({raw: "Raw string"}); + * + * @method translate + * @param {String/Object/Array} text Text to translate. + * @return {String} String that got translated. + */ + translate: function (text) { + var langData = data[code] || {}; + + /** + * number - string + * null, undefined and empty string - empty string + * array - comma-delimited string + * object - in [object Object] + * function - in [object Function] + * + * @param obj + * @returns {string} + */ + function toString(obj) { + if (Tools.is(obj, 'function')) { + return Object.prototype.toString.call(obj); + } + return !isEmpty(obj) ? '' + obj : ''; + } + + function isEmpty(text) { + return text === '' || text === null || Tools.is(text, 'undefined'); + } + + function getLangData(text) { + // make sure we work on a string and return a string + text = toString(text); + return Tools.hasOwn(langData, text) ? toString(langData[text]) : text; + } + + + if (isEmpty(text)) { + return ''; + } + + if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) { + return toString(text.raw); + } + + if (Tools.is(text, 'array')) { + var values = text.slice(1); + text = getLangData(text[0]).replace(/\{([0-9]+)\}/g, function ($1, $2) { + return Tools.hasOwn(values, $2) ? toString(values[$2]) : $1; + }); + } + + return getLangData(text).replace(/{context:\w+}$/, ''); + }, + + data: data + }; + } +); /** * EditorManager.js * @@ -46735,6 +48456,8 @@ define( define( 'tinymce.core.EditorManager', [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Type', 'tinymce.core.AddOnManager', 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.DOMUtils', @@ -46749,13 +48472,20 @@ define( 'tinymce.core.util.Tools', 'tinymce.core.util.URI' ], - function (AddOnManager, DomQuery, DOMUtils, Editor, Env, ErrorReporter, FocusManager, LegacyInput, I18n, Observable, Promise, Tools, URI) { + function (Arr, Type, AddOnManager, DomQuery, DOMUtils, Editor, Env, ErrorReporter, FocusManager, LegacyInput, I18n, Observable, Promise, Tools, URI) { var DOM = DOMUtils.DOM; var explode = Tools.explode, each = Tools.each, extend = Tools.extend; var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false; + var legacyEditors = [], editors = []; + + var isValidLegacyKey = function (id) { + // In theory we could filter out any editor id:s that clash + // with array prototype items but that could break existing integrations + return id !== 'length'; + }; function globalEventDelegate(e) { - each(EditorManager.editors, function (editor) { + each(EditorManager.get(), function (editor) { if (e.type === 'scroll') { editor.fire('ScrollWindow', e); } else { @@ -46764,7 +48494,7 @@ define( }); } - function toggleGlobalEvents(editors, state) { + function toggleGlobalEvents(state) { if (state !== boundGlobalEvents) { if (state) { DomQuery(window).on('resize scroll', globalEventDelegate); @@ -46776,30 +48506,32 @@ define( } } - function removeEditorFromList(editor) { - var editors = EditorManager.editors, removedFromList; + function removeEditorFromList(targetEditor) { + var oldEditors = editors; - delete editors[editor.id]; - - for (var i = 0; i < editors.length; i++) { - if (editors[i] == editor) { - editors.splice(i, 1); - removedFromList = true; + delete legacyEditors[targetEditor.id]; + for (var i = 0; i < legacyEditors.length; i++) { + if (legacyEditors[i] === targetEditor) { + legacyEditors.splice(i, 1); break; } } + editors = Arr.filter(editors, function (editor) { + return targetEditor !== editor; + }); + // Select another editor since the active one was removed - if (EditorManager.activeEditor == editor) { - EditorManager.activeEditor = editors[0]; + if (EditorManager.activeEditor === targetEditor) { + EditorManager.activeEditor = editors.length > 0 ? editors[0] : null; } // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor - if (EditorManager.focusedEditor == editor) { + if (EditorManager.focusedEditor === targetEditor) { EditorManager.focusedEditor = null; } - return removedFromList; + return oldEditors.length !== editors.length; } function purgeDestroyedEditor(editor) { @@ -46838,7 +48570,7 @@ define( * @property minorVersion * @type String */ - minorVersion: '6.3', + minorVersion: '6.7', /** * Release date of TinyMCE build. @@ -46846,18 +48578,15 @@ define( * @property releaseDate * @type String */ - releaseDate: '2017-05-30', + releaseDate: '2017-09-18', /** - * Collection of editor instances. + * Collection of editor instances. Deprecated use tinymce.get() instead. * * @property editors * @type Object - * @example - * for (edId in tinymce.editors) - * tinymce.editors[edId].save(); */ - editors: [], + editors: legacyEditors, /** * Collection of language pack data. @@ -46878,6 +48607,8 @@ define( */ activeEditor: null, + settings: {}, + setup: function () { var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; @@ -47211,24 +48942,35 @@ define( * * @method get * @param {String/Number} id Editor instance id or index to return. - * @return {tinymce.Editor} Editor instance to return. + * @return {tinymce.Editor/Array} Editor instance to return or array of editor instances. * @example - * // Adds an onclick event to an editor by id (shorter version) + * // Adds an onclick event to an editor by id * tinymce.get('mytextbox').on('click', function(e) { * ed.windowManager.alert('Hello world!'); * }); * + * // Adds an onclick event to an editor by index + * tinymce.get(0).on('click', function(e) { + * ed.windowManager.alert('Hello world!'); + * }); + * * // Adds an onclick event to an editor by id (longer version) * tinymce.EditorManager.get('mytextbox').on('click', function(e) { * ed.windowManager.alert('Hello world!'); * }); */ get: function (id) { - if (!arguments.length) { - return this.editors; + if (arguments.length === 0) { + return editors.slice(0); + } else if (Type.isString(id)) { + return Arr.find(editors, function (editor) { + return editor.id === id; + }).getOr(null); + } else if (Type.isNumber(id)) { + return editors[id] ? editors[id] : null; + } else { + return null; } - - return id in this.editors ? this.editors[id] : null; }, /** @@ -47239,13 +48981,28 @@ define( * @return {tinymce.Editor} The same instance that got passed in. */ add: function (editor) { - var self = this, editors = self.editors; + var self = this, existingEditor; - // Add named and index editor instance - editors[editor.id] = editor; - editors.push(editor); + // Prevent existing editors from beeing added again this could happen + // if a user calls createEditor then render or add multiple times. + existingEditor = legacyEditors[editor.id]; + if (existingEditor === editor) { + return editor; + } - toggleGlobalEvents(editors, true); + if (self.get(editor.id) === null) { + // Add to legacy editors array, this is what breaks in HTML5 where ID:s with numbers are valid + // We can't get rid of this strange object and array at the same time since it seems to be used all over the web + if (isValidLegacyKey(editor.id)) { + legacyEditors[editor.id] = editor; + } + + legacyEditors.push(editor); + + editors.push(editor); + } + + toggleGlobalEvents(true); // Doesn't call setActive method since we don't want // to fire a bunch of activate/deactivate calls while initializing @@ -47297,7 +49054,7 @@ define( * @return {tinymce.Editor} The editor that got passed in will be return if it was found otherwise null. */ remove: function (selector) { - var self = this, i, editors = self.editors, editor; + var self = this, i, editor; // Remove all editors if (!selector) { @@ -47309,11 +49066,11 @@ define( } // Remove editors by selector - if (typeof selector == "string") { + if (Type.isString(selector)) { selector = selector.selector || selector; each(DOM.select(selector), function (elm) { - editor = editors[elm.id]; + editor = self.get(elm.id); if (editor) { self.remove(editor); @@ -47327,7 +49084,7 @@ define( editor = selector; // Not in the collection - if (!editors[editor.id]) { + if (Type.isNull(self.get(editor.id))) { return null; } @@ -47335,13 +49092,13 @@ define( self.fire('RemoveEditor', { editor: editor }); } - if (!editors.length) { + if (editors.length === 0) { DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); } editor.remove(); - toggleGlobalEvents(editors, editors.length > 0); + toggleGlobalEvents(editors.length > 0); return editor; }, @@ -47406,7 +49163,7 @@ define( * tinyMCE.triggerSave(); */ triggerSave: function () { - each(this.editors, function (editor) { + each(editors, function (editor) { editor.save(); }); }, @@ -47464,7 +49221,7 @@ define( ); /** - * XHR.js + * Rect.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -47474,939 +49231,211 @@ define( */ /** - * This class enables you to send XMLHTTPRequests cross browser. - * @class tinymce.util.XHR - * @mixes tinymce.util.Observable - * @static - * @example - * // Sends a low level Ajax request - * tinymce.util.XHR.send({ - * url: 'someurl', - * success: function(text) { - * console.debug(text); - * } - * }); + * Contains various tools for rect/position calculation. * - * // Add custom header to XHR request - * tinymce.util.XHR.on('beforeSend', function(e) { - * e.xhr.setRequestHeader('X-Requested-With', 'Something'); - * }); + * @class tinymce.geom.Rect */ define( - 'tinymce.core.util.XHR', - [ - "tinymce.core.util.Observable", - "tinymce.core.util.Tools" - ], - function (Observable, Tools) { - var XHR = { - /** - * Sends a XMLHTTPRequest. - * Consult the Wiki for details on what settings this method takes. - * - * @method send - * @param {Object} settings Object will target URL, callbacks and other info needed to make the request. - */ - send: function (settings) { - var xhr, count = 0; - - function ready() { - if (!settings.async || xhr.readyState == 4 || count++ > 10000) { - if (settings.success && count < 10000 && xhr.status == 200) { - settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings); - } else if (settings.error) { - settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings); - } - - xhr = null; - } else { - setTimeout(ready, 10); - } - } - - // Default settings - settings.scope = settings.scope || this; - settings.success_scope = settings.success_scope || settings.scope; - settings.error_scope = settings.error_scope || settings.scope; - settings.async = settings.async === false ? false : true; - settings.data = settings.data || ''; - - XHR.fire('beforeInitialize', { settings: settings }); - - xhr = new XMLHttpRequest(); - - if (xhr) { - if (xhr.overrideMimeType) { - xhr.overrideMimeType(settings.content_type); - } - - xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async); - - if (settings.crossDomain) { - xhr.withCredentials = true; - } - - if (settings.content_type) { - xhr.setRequestHeader('Content-Type', settings.content_type); - } - - if (settings.requestheaders) { - Tools.each(settings.requestheaders, function (header) { - xhr.setRequestHeader(header.key, header.value); - }); - } - - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - - xhr = XHR.fire('beforeSend', { xhr: xhr, settings: settings }).xhr; - xhr.send(settings.data); - - // Syncronous request - if (!settings.async) { - return ready(); - } - - // Wait for response, onReadyStateChange can not be used since it leaks memory in IE - setTimeout(ready, 10); - } - } - }; - - Tools.extend(XHR, Observable); - - return XHR; - } -); - -/** - * JSON.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * JSON parser and serializer class. - * - * @class tinymce.util.JSON - * @static - * @example - * // JSON parse a string into an object - * var obj = tinymce.util.JSON.parse(somestring); - * - * // JSON serialize a object into an string - * var str = tinymce.util.JSON.serialize(obj); - */ -define( - 'tinymce.core.util.JSON', + 'tinymce.core.geom.Rect', [ ], function () { - function serialize(o, quote) { - var i, v, t, name; + "use strict"; - quote = quote || '"'; - - if (o === null) { - return 'null'; - } - - t = typeof o; - - if (t == 'string') { - v = '\bb\tt\nn\ff\rr\""\'\'\\\\'; - - /*eslint no-control-regex:0 */ - return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function (a, b) { - // Make sure single quotes never get encoded inside double quotes for JSON compatibility - if (quote === '"' && a === "'") { - return a; - } - - i = v.indexOf(b); - - if (i + 1) { - return '\\' + v.charAt(i + 1); - } - - a = b.charCodeAt().toString(16); - - return '\\u' + '0000'.substring(a.length) + a; - }) + quote; - } - - if (t == 'object') { - if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') { - for (i = 0, v = '['; i < o.length; i++) { - v += (i > 0 ? ',' : '') + serialize(o[i], quote); - } - - return v + ']'; - } - - v = '{'; - - for (name in o) { - if (o.hasOwnProperty(name)) { - v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name + - quote + ':' + serialize(o[name], quote) : ''; - } - } - - return v + '}'; - } - - return '' + o; - } - - return { - /** - * Serializes the specified object as a JSON string. - * - * @method serialize - * @param {Object} obj Object to serialize as a JSON string. - * @param {String} quote Optional quote string defaults to ". - * @return {string} JSON string serialized from input. - */ - serialize: serialize, - - /** - * Unserializes/parses the specified JSON string into a object. - * - * @method parse - * @param {string} s JSON String to parse into a JavaScript object. - * @return {Object} Object from input JSON string or undefined if it failed. - */ - parse: function (text) { - try { - // Trick uglify JS - return window[String.fromCharCode(101) + 'val']('(' + text + ')'); - } catch (ex) { - // Ignore - } - } - - /**#@-*/ - }; - } -); - -/** - * JSONRequest.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class enables you to use JSON-RPC to call backend methods. - * - * @class tinymce.util.JSONRequest - * @example - * var json = new tinymce.util.JSONRequest({ - * url: 'somebackend.php' - * }); - * - * // Send RPC call 1 - * json.send({ - * method: 'someMethod1', - * params: ['a', 'b'], - * success: function(result) { - * console.dir(result); - * } - * }); - * - * // Send RPC call 2 - * json.send({ - * method: 'someMethod2', - * params: ['a', 'b'], - * success: function(result) { - * console.dir(result); - * } - * }); - */ -define( - 'tinymce.core.util.JSONRequest', - [ - "tinymce.core.util.JSON", - "tinymce.core.util.XHR", - "tinymce.core.util.Tools" - ], - function (JSON, XHR, Tools) { - var extend = Tools.extend; - - function JSONRequest(settings) { - this.settings = extend({}, settings); - this.count = 0; - } - - /** - * Simple helper function to send a JSON-RPC request without the need to initialize an object. - * Consult the Wiki API documentation for more details on what you can pass to this function. - * - * @method sendRPC - * @static - * @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc. - */ - JSONRequest.sendRPC = function (o) { - return new JSONRequest().send(o); - }; - - JSONRequest.prototype = { - /** - * Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function. - * - * @method send - * @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc. - */ - send: function (args) { - var ecb = args.error, scb = args.success; - - args = extend(this.settings, args); - - args.success = function (c, x) { - c = JSON.parse(c); - - if (typeof c == 'undefined') { - c = { - error: 'JSON Parse error.' - }; - } - - if (c.error) { - ecb.call(args.error_scope || args.scope, c.error, x); - } else { - scb.call(args.success_scope || args.scope, c.result); - } - }; - - args.error = function (ty, x) { - if (ecb) { - ecb.call(args.error_scope || args.scope, ty, x); - } - }; - - args.data = JSON.serialize({ - id: args.id || 'c' + (this.count++), - method: args.method, - params: args.params - }); - - // JSON content type for Ruby on rails. Bug: #1883287 - args.content_type = 'application/json'; - - XHR.send(args); - } - }; - - return JSONRequest; - } -); -/** - * JSONP.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.core.util.JSONP', - [ - "tinymce.core.dom.DOMUtils" - ], - function (DOMUtils) { - return { - callbacks: {}, - count: 0, - - send: function (settings) { - var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count; - var id = 'tinymce_jsonp_' + count; - - self.callbacks[count] = function (json) { - dom.remove(id); - delete self.callbacks[count]; - - settings.callback(json); - }; - - dom.add(dom.doc.body, 'script', { - id: id, - src: settings.url, - type: 'text/javascript' - }); - - self.count++; - } - }; - } -); -/** - * LocalStorage.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/** - * This class will simulate LocalStorage on IE 7 and return the native version on modern browsers. - * Storage is done using userData on IE 7 and a special serialization format. The format is designed - * to be as small as possible by making sure that the keys and values doesn't need to be encoded. This - * makes it possible to store for example HTML data. - * - * Storage format for userData: - *\xa0
').append(m),a.setStartAfter(d[0].firstChild.firstChild),a.setEndAfter(m)):(d.empty().append("\xa0").append(m).append("\xa0"),a.setStart(d[0].firstChild,1),a.setEnd(d[0].lastChild,0)),d.css({top:p.getPos(c,g.getBody()).y}),d[0].focus(),e=g.selection.getSel(),e.removeAllRanges(),e.addRange(a),g.$("*[data-mce-selected]").removeAttr("data-mce-selected"),c.setAttribute("data-mce-selected",1),J=c,I(),a)):null)}function G(){J&&(J.removeAttribute("data-mce-selected"),g.$("#"+M).remove(),J=null)}function H(){L.destroy(),J=null}function I(){L.hide()}var J,K=g.getBody(),L=new e(g.getBody(),u),M="sel-"+g.dom.uniqueId();return i.ceFalse&&(B(),C()),{showCaret:y,showBlockCaretContainer:A,hideFakeCaret:I,destroy:H}}var q=g.isContentEditableTrue,r=g.isContentEditableFalse,s=c.isAfterContentEditableFalse,t=c.isBeforeContentEditableFalse;return p});g("6s",["e"],function(a){function b(b,c,d){for(var e=[];c&&c!=b;c=c.parentNode)e.push(a.nodeIndex(c,d));return e}function c(a,b){var c,d,e;for(d=a,c=b.length-1;c>=0;c--){if(e=d.childNodes,b[c]>e.length-1)return null;d=e[b[c]]}return d}return{create:b,resolve:c}}),g("66",["p","h","c","6s","i","d","6","9","5","1k","1n","4d"],function(a,b,c,d,e,f,g,h,i,j,k,l){return function(c){function d(a,b){try{c.getDoc().execCommand(a,!1,b)}catch(a){}}function m(){var a=c.getDoc().documentMode;return a?a:6}function n(a){return a.isDefaultPrevented()}function o(a){var b,d;a.dataTransfer&&(c.selection.isCollapsed()&&"IMG"==a.target.tagName&&_.select(a.target),b=c.selection.getContent(),b.length>0&&(d=ga+escape(c.id)+","+escape(b),a.dataTransfer.setData(ha,d)))}function p(a){var b;return a.dataTransfer&&(b=a.dataTransfer.getData(ha),b&&b.indexOf(ga)>=0)?(b=b.substr(ga.length).split(","),{id:unescape(b[0]),html:unescape(b[1])}):null}function q(a,b){c.queryCommandSupported("mceInsertClipboardContent")?c.execCommand("mceInsertClipboardContent",!1,{content:a,internal:b}):c.execCommand("mceInsertContent",!1,a)}function r(){function a(a){var b=$.create("body"),c=a.cloneContents();return b.appendChild(c),_.serializer.serialize(b,{format:"html"})}function d(d){if(!d.setStart){if(d.item)return!1;var e=d.duplicate();return e.moveToElementText(c.getBody()),b.compareRanges(d,e)}var f=a(d),g=$.createRng();g.selectNode(c.getBody());var h=a(g);return f===h}c.on("keydown",function(a){var b,e,f=a.keyCode;if(!n(a)&&(f==Z||f==Y)){if(b=c.selection.isCollapsed(),e=c.getBody(),b&&!$.isEmpty(e))return;if(!b&&!d(c.selection.getRng()))return;a.preventDefault(),c.setContent(""),e.firstChild&&$.isBlock(e.firstChild)?c.selection.setCursorLocation(e.firstChild,0):c.selection.setCursorLocation(e,0),c.nodeChanged()}})}function s(){c.shortcuts.add("meta+a",null,"SelectAll")}function t(){c.settings.content_editable||$.bind(c.getDoc(),"mousedown mouseup",function(a){var b;if(a.target==c.getDoc().documentElement)if(b=_.getRng(),c.getBody().focus(),"mousedown"==a.type){if(j.isCaretContainer(b.startContainer))return;_.placeCaretAt(a.clientX,a.clientY)}else _.setRng(b)})}function u(){c.on("keydown",function(a){if(!n(a)&&a.keyCode===Y){if(!c.getBody().getElementsByTagName("hr").length)return;if(_.isCollapsed()&&0===_.getRng(!0).startOffset){var b=_.getNode(),d=b.previousSibling;if("HR"==b.nodeName)return $.remove(b),void a.preventDefault();d&&d.nodeName&&"hr"===d.nodeName.toLowerCase()&&($.remove(d),a.preventDefault())}}})}function v(){window.Range.prototype.getClientRects||c.on("mousedown",function(a){ -if(!n(a)&&"HTML"===a.target.nodeName){var b=c.getBody();b.blur(),i.setEditorTimeout(c,function(){b.focus()})}})}function w(){c.on("click",function(a){var b=a.target;/^(IMG|HR)$/.test(b.nodeName)&&"false"!==$.getContentEditableParent(b)&&(a.preventDefault(),c.selection.select(b),c.nodeChanged()),"A"==b.nodeName&&$.hasClass(b,"mce-item-anchor")&&(a.preventDefault(),_.select(b))})}function x(){function a(){var a=$.getAttribs(_.getStart().cloneNode(!1));return function(){var b=_.getStart();b!==c.getBody()&&($.setAttrib(b,"style",null),X(a,function(a){b.setAttributeNode(a.cloneNode(!0))}))}}function b(){return!_.isCollapsed()&&$.getParent(_.getStart(),$.isBlock)!=$.getParent(_.getEnd(),$.isBlock)}c.on("keypress",function(d){var e;if(!n(d)&&(8==d.keyCode||46==d.keyCode)&&b())return e=a(),c.getDoc().execCommand("delete",!1,null),e(),d.preventDefault(),!1}),$.bind(c.getDoc(),"cut",function(d){var e;!n(d)&&b()&&(e=a(),i.setEditorTimeout(c,function(){e()}))})}function y(){document.body.setAttribute("role","application")}function z(){c.on("keydown",function(a){if(!n(a)&&a.keyCode===Y&&_.isCollapsed()&&0===_.getRng(!0).startOffset){var b=_.getNode().previousSibling;if(b&&b.nodeName&&"table"===b.nodeName.toLowerCase())return a.preventDefault(),!1}})}function A(){m()>7||(d("RespectVisibilityInDesign",!0),c.contentStyles.push(".mceHideBrInPre pre br {display: none}"),$.addClass(c.getBody(),"mceHideBrInPre"),ba.addNodeFilter("pre",function(a){for(var b,c,d,f,g=a.length;g--;)for(b=a[g].getAll("br"),c=b.length;c--;)d=b[c],f=d.prev,f&&3===f.type&&"\n"!=f.value.charAt(f.value-1)?f.value+="\n":d.parent.insert(new e("#text",3),d,!0).value="\n"}),ca.addNodeFilter("pre",function(a){for(var b,c,d,e,f=a.length;f--;)for(b=a[f].getAll("br"),c=b.length;c--;)d=b[c],e=d.prev,e&&3==e.type&&(e.value=e.value.replace(/\r?\n$/,""))}))}function B(){$.bind(c.getBody(),"mouseup",function(){var a,b=_.getNode();"IMG"==b.nodeName&&((a=$.getStyle(b,"width"))&&($.setAttrib(b,"width",a.replace(/[^0-9%]+/g,"")),$.setStyle(b,"width","")),(a=$.getStyle(b,"height"))&&($.setAttrib(b,"height",a.replace(/[^0-9%]+/g,"")),$.setStyle(b,"height","")))})}function C(){c.on("keydown",function(b){var d,e,f,g,h;if(!n(b)&&b.keyCode==a.BACKSPACE&&(d=_.getRng(),e=d.startContainer,f=d.startOffset,g=$.getRoot(),h=e,d.collapsed&&0===f)){for(;h&&h.parentNode&&h.parentNode.firstChild==h&&h.parentNode!=g;)h=h.parentNode;"BLOCKQUOTE"===h.tagName&&(c.formatter.toggle("blockquote",null,h),d=$.createRng(),d.setStart(e,0),d.setEnd(e,0),_.setRng(d))}})}function D(){function a(){U(),d("StyleWithCSS",!1),d("enableInlineTableEditing",!1),aa.object_resizing||d("enableObjectResizing",!1)}aa.readonly||c.on("BeforeExecCommand MouseDown",a)}function E(){function a(){X($.select("a"),function(a){var b=a.parentNode,c=$.getRoot();if(b.lastChild===a){for(;b&&!$.isBlock(b);){if(b.parentNode.lastChild!==b||b===c)return;b=b.parentNode}$.add(b,"br",{"data-mce-bogus":1})}})}c.on("SetContent ExecCommand",function(b){"setcontent"!=b.type&&"mceInsertLink"!==b.command||a()})}function F(){aa.forced_root_block&&c.on("init",function(){d("DefaultParagraphSeparator",aa.forced_root_block)})}function G(){c.on("keydown",function(a){var b;n(a)||a.keyCode!=Y||(b=c.getDoc().selection.createRange(),b&&b.item&&(a.preventDefault(),c.undoManager.beforeChange(),$.remove(b.item(0)),c.undoManager.add()))})}function H(){var a;m()>=10&&(a="",X("p div h1 h2 h3 h4 h5 h6".split(" "),function(b,c){a+=(c>0?",":"")+b+":empty"}),c.contentStyles.push(a+"{padding-right: 1px !important}"))}function I(){m()<9&&(ba.addNodeFilter("noscript",function(a){for(var b,c,d=a.length;d--;)b=a[d],c=b.firstChild,c&&b.attr("data-mce-innertext",c.value)}),ca.addNodeFilter("noscript",function(a){for(var b,c,d,g=a.length;g--;)b=a[g],c=a[g].firstChild,c?c.value=f.decode(c.value):(d=b.attributes.map["data-mce-innertext"],d&&(b.attr("data-mce-innertext",null),c=new e("#text",3),c.value=d,c.raw=!0,b.append(c)))}))}function J(){function a(a,b){var c=h.createTextRange();try{c.moveToPoint(a,b)}catch(a){c=null}return c}function b(b){var d;b.button?(d=a(b.x,b.y),d&&(d.compareEndPoints("StartToStart",e)>0?d.setEndPoint("StartToStart",e):d.setEndPoint("EndToEnd",e),d.select())):c()}function c(){var a=g.selection.createRange();e&&!a.item&&0===a.compareEndPoints("StartToEnd",a)&&e.select(),$.unbind(g,"mouseup",c),$.unbind(g,"mousemove",b),e=d=0}var d,e,f,g=$.doc,h=g.body;g.documentElement.unselectable=!0,$.bind(g,"mousedown contextmenu",function(h){if("HTML"===h.target.nodeName){if(d&&c(),f=g.documentElement,f.scrollHeight>f.clientHeight)return;d=1,e=a(h.x,h.y),e&&($.bind(g,"mouseup",c),$.bind(g,"mousemove",b),$.getRoot().focus(),e.select())}})}function K(){c.on("keyup focusin mouseup",function(b){65==b.keyCode&&a.metaKeyPressed(b)||_.normalize()},!0)}function L(){c.contentStyles.push("img:-moz-broken {-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}")}function M(){c.inline||c.on("keydown",function(){document.activeElement==document.body&&c.getWin().focus()})}function N(){c.inline||(c.contentStyles.push("body {min-height: 150px}"),c.on("click",function(a){var b;if("HTML"==a.target.nodeName){if(g.ie>11)return void c.getBody().focus();b=c.selection.getRng(),c.getBody().focus(),c.selection.setRng(b),c.selection.normalize(),c.nodeChanged()}}))}function O(){g.mac&&c.on("keydown",function(b){!a.metaKeyPressed(b)||b.shiftKey||37!=b.keyCode&&39!=b.keyCode||(b.preventDefault(),c.selection.getSel().modify("move",37==b.keyCode?"backward":"forward","lineboundary"))})}function P(){d("AutoUrlDetect",!1)}function Q(){c.on("click",function(a){var b=a.target;do if("A"===b.tagName)return void a.preventDefault();while(b=b.parentNode)}),c.contentStyles.push(".mce-content-body {-webkit-touch-callout: none}")}function R(){c.on("init",function(){c.dom.bind(c.getBody(),"submit",function(a){a.preventDefault()})})}function S(){ba.addNodeFilter("br",function(a){for(var b=a.length;b--;)"Apple-interchange-newline"==a[b].attr("class")&&a[b].remove()})}function T(){c.on("dragstart",function(a){o(a)}),c.on("drop",function(a){if(!n(a)){var d=p(a);if(d&&d.id!=c.id){a.preventDefault();var e=b.getCaretRangeFromPoint(a.x,a.y,c.getDoc());_.setRng(e),q(d.html,!0)}}})}function U(){}function V(){var a;return!da||c.removed?0:(a=c.selection.getSel(),!a||!a.rangeCount||0===a.rangeCount)}function W(){function b(a){var b=new l(a.getBody()),c=a.selection.getRng(),d=k.fromRangeStart(c),e=k.fromRangeEnd(c),f=b.prev(d),g=b.next(e);return!a.selection.isCollapsed()&&(!f||f.isAtStart()&&d.isEqual(f))&&(!g||g.isAtEnd()&&d.isEqual(g))}c.on("keypress",function(d){!n(d)&&!_.isCollapsed()&&d.charCode>31&&!a.metaKeyPressed(d)&&b(c)&&(d.preventDefault(),c.setContent(String.fromCharCode(d.charCode)),c.selection.select(c.getBody(),!0),c.selection.collapse(!1),c.nodeChanged())}),c.on("keydown",function(a){var d=a.keyCode;n(a)||d!=Z&&d!=Y||b(c)&&(a.preventDefault(),c.setContent(""),c.nodeChanged())})}var X=h.each,Y=a.BACKSPACE,Z=a.DELETE,$=c.dom,_=c.selection,aa=c.settings,ba=c.parser,ca=c.serializer,da=g.gecko,ea=g.ie,fa=g.webkit,ga="data:text/mce-internal,",ha=ea?"Text":"URL";return C(),r(),g.windowsPhone||K(),fa&&(W(),t(),w(),F(),R(),z(),S(),g.iOS?(M(),N(),Q()):s()),ea&&g.ie<11&&(u(),y(),A(),B(),G(),H(),I(),J()),g.ie>=11&&(N(),z()),g.ie&&(s(),P(),T()),da&&(W(),u(),v(),x(),D(),E(),L(),O(),z()),{refreshContentEditable:U,isHidden:V}}}),g("5i",["1y","4k","60","e","s","o","61","26","62","t","l","i","j","63","64","65","u","5","66","9"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t){var u=d.DOM,v=function(a){var b=new k(a.settings,a.schema);return b.addAttributeFilter("src,href,style,tabindex",function(b,c){for(var d,e,f,g=b.length,h=a.dom;g--;)if(d=b[g],e=d.attr(c),f="data-mce-"+c,!d.attributes.map[f]){if(0===e.indexOf("data:")||0===e.indexOf("blob:"))continue;"style"===c?(e=h.serializeStyle(h.parseStyle(e),d.name),e.length||(e=null),d.attr(f,e),d.attr(c,e)):"tabindex"===c?(d.attr(f,e),d.attr(c,null)):d.attr(f,a.convertURL(e,c,d.name))}}),b.addNodeFilter("script",function(a){for(var b,c,d=a.length;d--;)b=a[d],c=b.attr("type")||"no/type",0!==c.indexOf("mce-")&&b.attr("type","mce-"+c)}),b.addNodeFilter("#cdata",function(a){for(var b,c=a.length;c--;)b=a[c],b.type=8,b.name="#comment",b.value="[CDATA["+b.value+"]]"}),b.addNodeFilter("p,h1,h2,h3,h4,h5,h6,div",function(b){for(var c,d=b.length,e=a.schema.getNonEmptyElements();d--;)c=b[d],c.isEmpty(e)&&0===c.getAll("br").length&&(c.append(new l("br",1)).shortEnded=!0)}),b},w=function(a){a.settings.auto_focus&&r.setEditorTimeout(a,function(){var b;b=a.settings.auto_focus===!0?a:a.editorManager.get(a.settings.auto_focus),b.destroyed||b.focus()},100)},x=function(a){a.bindPendingEventDelegates(),a.initialized=!0,a.fire("init"),a.focus(!0),a.nodeChanged({initial:!0}),a.execCallback("init_instance_callback",a),w(a)},y=function(k,l){var r,w,y=k.settings,z=k.getElement(),A=k.getDoc();y.inline||(k.getElement().style.visibility=k.orgVisibility),l||y.content_editable||(A.open(),A.write(k.iframeHTML),A.close()),y.content_editable&&(k.on("remove",function(){var a=this.getBody();u.removeClass(a,"mce-content-body"),u.removeClass(a,"mce-edit-focus"),u.setAttrib(a,"contentEditable",null)}),u.addClass(z,"mce-content-body"),k.contentDocument=A=y.content_document||a,k.contentWindow=y.content_window||b,k.bodyElement=z,y.content_document=y.content_window=null,y.root_name=z.nodeName.toLowerCase()),r=k.getBody(),r.disabled=!0,k.readonly=y.readonly,k.readonly||(k.inline&&"static"===u.getStyle(r,"position",!0)&&(r.style.position="relative"),r.contentEditable=k.getParam("content_editable_state",!0)),r.disabled=!1,k.editorUpload=new g(k),k.schema=new m(y),k.dom=new d(A,{keep_values:!0,url_converter:k.convertURL,url_converter_scope:k,hex_colors:y.force_hex_style_colors,class_filter:y.class_filter,update_styles:!0,root_element:k.inline?k.getBody():null,collect:y.content_editable,schema:k.schema,onSetAttrib:function(a){k.fire("SetAttrib",a)}}),k.parser=v(k),k.serializer=new f(y,k),k.selection=new e(k.dom,k.getWin(),k.serializer,k),k.formatter=new j(k),k.undoManager=new q(k),k._nodeChangeDispatcher=new o(k),k._selectionOverrides=new p(k),c.setup(k),n.setup(k),i.setup(k),k.fire("PreInit"),y.browser_spellcheck||y.gecko_spellcheck||(A.body.spellcheck=!1,u.setAttrib(r,"spellcheck","false")),k.quirks=new s(k),k.fire("PostRender"),y.directionality&&(r.dir=y.directionality),y.nowrap&&(r.style.whiteSpace="nowrap"),y.protect&&k.on("BeforeSetContent",function(a){t.each(y.protect,function(b){a.content=a.content.replace(b,function(a){return""})})}),k.on("SetContent",function(){k.addVisual(k.getBody())}),y.padd_empty_editor&&k.on("PostProcess",function(a){a.content=a.content.replace(/^(]*>( | |\s|\u00a0| \xa0 ]*>( | |\s|\u00a0|
|)<\/p>[\r\n]*|
[\r\n]*)$/,"")}),k.load({initial:!0,format:"html"}),k.startContent=k.getContent({format:"raw"}),k.on("compositionstart compositionend",function(a){k.composing="compositionstart"===a.type}),k.contentStyles.length>0&&(w="",t.each(k.contentStyles,function(a){w+=a+"\r\n"}),k.dom.addStyle(w)),k.dom.styleSheetLoader.loadAll(k.contentCSS,function(a){x(k)},function(a){x(k),h.contentCssError(k,a)})};return{initContentBody:y}}),g("4m",["g"],function(a){return a.PluginManager}),g("4n",["g"],function(a){return a.ThemeManager}),g("4l",["1y","4k","e","6","5i","4m","4n","9","25"],function(a,b,c,d,e,f,g,h,i){var j=c.DOM,k=function(a,b,c){var d,e,g=f.get(c);if(d=f.urls[c]||a.documentBaseUrl.replace(/\/$/,""),c=h.trim(c),g&&h.inArray(b,c)===-1){if(h.each(f.dependencies(c),function(c){k(a,b,c)}),a.plugins[c])return;e=new g(a,d,a.$),a.plugins[c]=e,e.init&&(e.init(a,d),b.push(c))}},l=function(a){var b=[];h.each(a.settings.plugins.replace(/\-/g,"").split(/[ ,]/),function(c){k(a,b,c)})},m=function(a){var b,c=a.settings;c.theme&&("function"!=typeof c.theme?(c.theme=c.theme.replace(/-/,""),b=g.get(c.theme),a.theme=new b(a,g.urls[c.theme]),a.theme.init&&a.theme.init(a,g.urls[c.theme]||a.documentBaseUrl.replace(/\/$/,""),a.$)):a.theme=c.theme)},n=function(a){var b,c,d,e,f,g=a.settings,h=a.getElement();return g.render_ui&&a.theme&&(a.orgDisplay=h.style.display,"function"!=typeof g.theme?(b=g.width||j.getStyle(h,"width")||"100%",c=g.height||j.getStyle(h,"height")||h.offsetHeight,d=g.min_height||100,e=/^[0-9\.]+(|px)$/i,e.test(""+b)&&(b=Math.max(parseInt(b,10),100)),e.test(""+c)&&(c=Math.max(parseInt(c,10),d)),f=a.theme.renderUI({targetNode:h,width:b,height:c,deltaWidth:g.delta_width,deltaHeight:g.delta_height}),g.content_editable||(c=(f.iframeHeight||c)+("number"==typeof c?f.deltaHeight||0:""),c
';var g=j.create("iframe",{id:a.id+"_ifr",frameBorder:"0",allowTransparency:"true",title:a.editorManager.translate("Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help"),style:{width:"100%",height:b.height,display:"block"}});g.onload=function(){g.onload=null,a.fire("load")};var h=o(a,g);return a.contentAreaContainer=b.iframeContainer,a.iframeElement=g,j.add(b.iframeContainer,g),h},q=function(a){var b,c=a.settings,d=a.getElement();if(a.rtl=c.rtl_ui||a.editorManager.i18n.rtl,a.editorManager.i18n.setCode(c.language),c.aria_label=c.aria_label||j.getAttrib(d,"aria-label",a.getLang("aria.rich_text_area")),a.fire("ScriptsLoaded"),m(a),l(a),b=n(a),c.content_css&&h.each(h.explode(c.content_css),function(b){a.contentCSS.push(a.documentBaseURI.toAbsolute(b))}),c.content_style&&a.contentStyles.push(c.content_style),c.content_editable)return e.initContentBody(a);var f=p(a,b);b.editorContainer&&(j.get(b.editorContainer).style.display=a.orgDisplay,a.hidden=j.isHidden(b.editorContainer)),a.getElement().style.display="none",j.setAttrib(a.id,"aria-hidden",!0),f||e.initContentBody(a)};return{init:q}}),g("22",["4k","e","7","f","6","26","4l","11","4m","4n","9","10"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=b.DOM,n=function(a,b){var c=a.settings,e=d.ScriptLoader;if(c.language&&"en"!=c.language&&!c.language_url&&(c.language_url=a.editorManager.baseURL+"/langs/"+c.language+".js"),c.language_url&&e.add(c.language_url),c.theme&&"function"!=typeof c.theme&&"-"!=c.theme.charAt(0)&&!j.urls[c.theme]){var h=c.theme_url;h=h?a.documentBaseURI.toAbsolute(h):"themes/"+c.theme+"/theme"+b+".js",j.load(c.theme,h)}k.isArray(c.plugins)&&(c.plugins=c.plugins.join(" ")),k.each(c.external_plugins,function(a,b){i.load(b,a),c.plugins+=" "+b}),k.each(c.plugins.split(/[ ,]/),function(a){if(a=k.trim(a),a&&!i.urls[a])if("-"===a.charAt(0)){a=a.substr(1,a.length);var c=i.dependencies(a);k.each(c,function(a){var c={prefix:"plugins/",resource:a,suffix:"/plugin"+b+".js"};a=i.createUrl(c,a),i.load(a.resource,a)})}else i.load(a,{prefix:"plugins/",resource:a,suffix:"/plugin"+b+".js"})}),e.loadQueue(function(){a.removed||g.init(a)},a,function(b){f.pluginLoadError(a,b[0]),a.removed||g.init(a)})},o=function(b){function d(){m.unbind(a,"ready",d),b.render()}var f=b.settings,g=b.id;if(!c.Event.domLoaded)return void m.bind(a,"ready",d);if(b.getElement()&&e.contentEditable){f.inline?b.inline=!0:(b.orgVisibility=b.getElement().style.visibility,b.getElement().style.visibility="hidden");var i=b.getElement().form||m.getParent(g,"form");i&&(b.formElement=i,f.hidden_input&&!/TEXTAREA|INPUT/i.test(b.getElement().nodeName)&&(m.insertAfter(m.create("input",{type:"hidden",name:g}),g),b.hasHiddenInput=!0),b.formEventDelegate=function(a){b.fire(a.type,a)},m.bind(i,"submit reset",b.formEventDelegate),b.on("reset",function(){b.setContent(b.startContent,{format:"raw"})}),!f.submit_patch||i.submit.nodeType||i.submit.length||i._mceOldSubmit||(i._mceOldSubmit=i.submit,i.submit=function(){return b.editorManager.triggerSave(),b.setDirty(!1),i._mceOldSubmit(i)})),b.windowManager=new l(b),b.notificationManager=new h(b),"xml"===f.encoding&&b.on("GetContent",function(a){a.save&&(a.content=m.encode(a.content))}),f.add_form_submit_trigger&&b.on("submit",function(){b.initialized&&b.save()}),f.add_unload_trigger&&(b._beforeUnload=function(){!b.initialized||b.destroyed||b.isHidden()||b.save({format:"raw",no_events:!0,set_dirty:!1})},b.editorManager.on("BeforeUnload",b._beforeUnload)),b.editorManager.add(b),n(b,b.suffix)}};return{render:o}}),g("23",[],function(){function a(a,b,c){try{a.getDoc().execCommand(b,!1,c)}catch(a){}}function b(a){var b,c;return b=a.getBody(),c=function(b){a.dom.getParents(b.target,"a").length>0&&b.preventDefault()},a.dom.bind(b,"click",c),{unbind:function(){a.dom.unbind(b,"click",c)}}}function c(c,d){c._clickBlocker&&(c._clickBlocker.unbind(),c._clickBlocker=null),d?(c._clickBlocker=b(c),c.selection.controlSelection.hideResizeRect(),c.readonly=!0,c.getBody().contentEditable=!1):(c.readonly=!1,c.getBody().contentEditable=!0,a(c,"StyleWithCSS",!1),a(c,"enableInlineTableEditing",!1),a(c,"enableObjectResizing",!1),c.focus(),c.nodeChanged())}function d(a,b){var d=a.readonly?"readonly":"design";b!=d&&(a.initialized?c(a,"readonly"==b):a.on("init",function(){c(a,"readonly"==b)}),a.fire("SwitchMode",{mode:b}))}return{setMode:d}}),g("24",[],function(){var a=function(a,b,c){var d=a.sidebars?a.sidebars:[];d.push({name:b,settings:c}),a.sidebars=d};return{add:a}}),g("14",["g","a","e","v","12","6","n","22","23","13","24","9","w","25"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){function o(c,e,g){var h,i,k,l=this;h=l.documentBaseUrl=g.documentBaseURL,i=g.baseURI,k=g.defaultSettings,e=q({id:c,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:h,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,padd_empty_editor:!0,render_ui:!0,indentation:"30px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",validate:!0,entity_encoding:"named",url_converter:l.convertURL,url_converter_scope:l,ie7_compat:!0},k,e),k&&k.external_plugins&&e.external_plugins&&(e.external_plugins=q({},k.external_plugins,e.external_plugins)),l.settings=e,a.language=e.language||"en",a.languageLoad=e.language_load,a.baseURL=g.baseURL,l.id=e.id=c,l.setDirty(!1),l.plugins={},l.documentBaseURI=new m(e.document_base_url||h,{base_uri:i}),l.baseURI=i,l.contentCSS=[],l.contentStyles=[],l.shortcuts=new j(l),l.loadedCSS={},l.editorCommands=new d(l),l.suffix=g.suffix,l.editorManager=g,l.inline=e.inline,l.settings.content_editable=l.inline,e.cache_suffix&&(f.cacheSuffix=e.cache_suffix.replace(/^[\?\&]+/,"")),e.override_viewport===!1&&(f.overrideViewPort=!1),g.fire("SetupEditor",l),l.execCallback("setup",l),l.$=b.overrideDefaults(function(){return{context:l.inline?l.getBody():l.getDoc(),element:l.getBody()}})}var p=c.DOM,q=l.extend,r=l.each,s=l.trim,t=l.resolve,u=f.gecko,v=f.ie;return o.prototype={render:function(){h.render(this)},focus:function(a){function b(a){return g.dom.getParent(a,function(a){return"true"===g.dom.getContentEditable(a)})}var c,d,e,g=this,h=g.selection,i=g.settings.content_editable,j=g.getDoc(),k=g.getBody();if(!g.removed){if(!a){if(c=h.getRng(),c.item&&(d=c.item(0)),g.quirks.refreshContentEditable(),e=b(h.getNode()),g.$.contains(k,e))return e.focus(),h.normalize(),void g.editorManager.setActive(g);if(i||(f.opera||g.getBody().focus(),g.getWin().focus()),u||i){if(k.setActive)try{k.setActive()}catch(a){k.focus()}else k.focus();i&&h.normalize()}d&&d.ownerDocument==j&&(c=j.body.createControlRange(),c.addElement(d),c.select())}g.editorManager.setActive(g)}},execCallback:function(a){var b,c=this,d=c.settings[a];if(d)return c.callbackLookup&&(b=c.callbackLookup[a])&&(d=b.func,b=b.scope),"string"==typeof d&&(b=d.replace(/\.\w+$/,""),b=b?t(b):0,d=t(d),c.callbackLookup=c.callbackLookup||{},c.callbackLookup[a]={func:d,scope:b}),d.apply(b||c,Array.prototype.slice.call(arguments,1))},translate:function(a){var b=this.settings.language||"en",c=this.editorManager.i18n;return a?(a=c.data[b+"."+a]||a.replace(/\{\#([^\}]+)\}/g,function(a,d){return c.data[b+"."+d]||"{#"+d+"}"}),this.editorManager.translate(a)):""},getLang:function(a,b){return this.editorManager.i18n.data[(this.settings.language||"en")+"."+a]||(void 0!==b?b:"{#"+a+"}")},getParam:function(a,b,c){var d,e=a in this.settings?this.settings[a]:b;return"hash"===c?(d={},"string"==typeof e?r(e.indexOf("=")>0?e.split(/[;,](?![^=;,]*(?:[;,]|$))/):e.split(","),function(a){a=a.split("="),a.length>1?d[s(a[0])]=s(a[1]):d[s(a[0])]=s(a)}):d=e,d):e},nodeChanged:function(a){this._nodeChangeDispatcher.nodeChanged(a)},addButton:function(a,b){var c=this;b.cmd&&(b.onclick=function(){c.execCommand(b.cmd)}),b.text||b.icon||(b.icon=a),c.buttons=c.buttons||{},b.tooltip=b.tooltip||b.title,c.buttons[a]=b},addSidebar:function(a,b){return k.add(this,a,b)},addMenuItem:function(a,b){var c=this;b.cmd&&(b.onclick=function(){c.execCommand(b.cmd)}),c.menuItems=c.menuItems||{},c.menuItems[a]=b},addContextToolbar:function(a,b){var c,d=this;d.contextToolbars=d.contextToolbars||[],"string"==typeof a&&(c=a,a=function(a){return d.dom.is(a,c)}),d.contextToolbars.push({id:n.uuid("mcet"),predicate:a,items:b})},addCommand:function(a,b,c){this.editorCommands.addCommand(a,b,c)},addQueryStateHandler:function(a,b,c){this.editorCommands.addQueryStateHandler(a,b,c)},addQueryValueHandler:function(a,b,c){this.editorCommands.addQueryValueHandler(a,b,c)},addShortcut:function(a,b,c,d){this.shortcuts.add(a,b,c,d)},execCommand:function(a,b,c,d){return this.editorCommands.execCommand(a,b,c,d)},queryCommandState:function(a){return this.editorCommands.queryCommandState(a)},queryCommandValue:function(a){return this.editorCommands.queryCommandValue(a)},queryCommandSupported:function(a){return this.editorCommands.queryCommandSupported(a)},show:function(){var a=this;a.hidden&&(a.hidden=!1,a.inline?a.getBody().contentEditable=!0:(p.show(a.getContainer()),p.hide(a.id)),a.load(),a.fire("show"))},hide:function(){var a=this,b=a.getDoc();a.hidden||(v&&b&&!a.inline&&b.execCommand("SelectAll"),a.save(),a.inline?(a.getBody().contentEditable=!1,a==a.editorManager.focusedEditor&&(a.editorManager.focusedEditor=null)):(p.hide(a.getContainer()),p.setStyle(a.id,"display",a.orgDisplay)),a.hidden=!0,a.fire("hide"))},isHidden:function(){return!!this.hidden},setProgressState:function(a,b){this.fire("ProgressState",{state:a,time:b})},load:function(a){var b,c=this,d=c.getElement();return c.removed?"":d?(a=a||{},a.load=!0,b=c.setContent(void 0!==d.value?d.value:d.innerHTML,a),a.element=d,a.no_events||c.fire("LoadContent",a),a.element=d=null,b):void 0},save:function(a){var b,c,d=this,e=d.getElement();if(e&&d.initialized&&!d.removed)return a=a||{},a.save=!0,a.element=e,b=a.content=d.getContent(a),a.no_events||d.fire("SaveContent",a),"raw"==a.format&&d.fire("RawSaveContent",a),b=a.content,/TEXTAREA|INPUT/i.test(e.nodeName)?e.value=b:(d.inline||(e.innerHTML=b),(c=p.getParent(d.id,"form"))&&r(c.elements,function(a){if(a.name==d.id)return a.value=b,!1})),a.element=e=null,a.set_dirty!==!1&&d.setDirty(!1),b},setContent:function(a,b){var c,d,e=this,f=e.getBody();return b=b||{},b.format=b.format||"html",b.set=!0,b.content=a,b.no_events||e.fire("BeforeSetContent",b),a=b.content,0===a.length||/^\s+$/.test(a)?(d=v&&v<11?"":'
',"TABLE"==f.nodeName?a=" ":/^(UL|OL)$/.test(f.nodeName)&&(a=""+d+"
'),e.dom.setHTML(f,a),e.fire("SetContent",b)):("raw"!==b.format&&(a=new g({validate:e.validate},e.schema).serialize(e.parser.parse(a,{isRootContent:!0}))),b.content=s(a),e.dom.setHTML(f,b.content),b.no_events||e.fire("SetContent",b)),b.content},getContent:function(a){var b,c=this,d=c.getBody();return c.removed?"":(a=a||{},a.format=a.format||"html",a.get=!0,a.getInner=!0,a.no_events||c.fire("BeforeGetContent",a),b="raw"==a.format?l.trim(c.serializer.getTrimmedContent()):"text"==a.format?d.innerText||d.textContent:c.serializer.serialize(d,a),"text"!=a.format?a.content=s(b):a.content=b,a.no_events||c.fire("GetContent",a),a.content)},insertContent:function(a,b){b&&(a=q({content:a},b)),this.execCommand("mceInsertContent",!1,a)},isDirty:function(){return!this.isNotDirty},setDirty:function(a){var b=!this.isNotDirty;this.isNotDirty=!a,a&&a!=b&&this.fire("dirty")},setMode:function(a){i.setMode(this,a)},getContainer:function(){var a=this;return a.container||(a.container=p.get(a.editorContainer||a.id+"_parent")),a.container},getContentAreaContainer:function(){return this.contentAreaContainer},getElement:function(){return this.targetElm||(this.targetElm=p.get(this.id)),this.targetElm},getWin:function(){var a,b=this;return b.contentWindow||(a=b.iframeElement,a&&(b.contentWindow=a.contentWindow)),b.contentWindow},getDoc:function(){var a,b=this;return b.contentDocument||(a=b.getWin(),a&&(b.contentDocument=a.document)),b.contentDocument},getBody:function(){var a=this.getDoc();return this.bodyElement||(a?a.body:null)},convertURL:function(a,b,c){var d=this,e=d.settings;return e.urlconverter_callback?d.execCallback("urlconverter_callback",a,c,!0,b):!e.convert_urls||c&&"LINK"==c.nodeName||0===a.indexOf("file:")||0===a.length?a:e.relative_urls?d.documentBaseURI.toRelative(a):a=d.documentBaseURI.toAbsolute(a,e.remove_script_host)},addVisual:function(a){var b,c=this,d=c.settings,e=c.dom;a=a||c.getBody(),void 0===c.hasVisual&&(c.hasVisual=d.visual),r(e.select("table,a",a),function(a){var f;switch(a.nodeName){case"TABLE":return b=d.visual_table_class||"mce-item-table",f=e.getAttrib(a,"border"),void(f&&"0"!=f||!c.hasVisual?e.removeClass(a,b):e.addClass(a,b));case"A":return void(e.getAttrib(a,"href",!1)||(f=e.getAttrib(a,"name")||a.id,b=d.visual_anchor_class||"mce-item-anchor",f&&c.hasVisual?e.addClass(a,b):e.removeClass(a,b)))}}),c.fire("VisualAid",{element:a,hasVisual:c.hasVisual})},remove:function(){var a=this;a.removed||(a.save(),a.removed=1,a.unbindAllNativeEvents(),a.hasHiddenInput&&p.remove(a.getElement().nextSibling),a.inline||(v&&v<10&&a.getDoc().execCommand("SelectAll",!1,null),p.setStyle(a.id,"display",a.orgDisplay),a.getBody().onload=null),a.fire("remove"),a.editorManager.remove(a),p.remove(a.getContainer()),a._selectionOverrides.destroy(),a.editorUpload.destroy(),a.destroy())},destroy:function(a){var b,c=this;if(!c.destroyed){if(!a&&!c.removed)return void c.remove();a||(c.editorManager.off("beforeunload",c._beforeUnload),c.theme&&c.theme.destroy&&c.theme.destroy(),c.selection.destroy(),c.dom.destroy()),b=c.formElement,b&&(b._mceOldSubmit&&(b.submit=b._mceOldSubmit,b._mceOldSubmit=null),p.unbind(b,"submit reset",c.formEventDelegate)),c.contentAreaContainer=c.formElement=c.container=c.editorContainer=null,c.bodyElement=c.contentDocument=c.contentWindow=null,c.iframeElement=c.targetElm=null,c.selection&&(c.selection=c.selection.win=c.selection.dom=c.selection.dom.doc=null),c.destroyed=1}},uploadImages:function(a){return this.editorUpload.uploadImages(a)},_scanForImages:function(){return this.editorUpload.scanForImages()}},q(o.prototype,e),o}),g("15",["9"],function(a){"use strict";var b={},c="en";return{setCode:function(a){a&&(c=a,this.rtl=!!this.data[a]&&"rtl"===this.data[a]._dir)},getCode:function(){return c},rtl:!1,add:function(a,c){var d=b[a];d||(b[a]=d={});for(var e in c)d[e]=c[e];this.setCode(a)},translate:function(d){function e(b){return a.is(b,"function")?Object.prototype.toString.call(b):f(b)?"":""+b}function f(b){return""===b||null===b||a.is(b,"undefined")}function g(b){return b=e(b),a.hasOwn(h,b)?e(h[b]):b}var h=b[c]||{};if(f(d))return"";if(a.is(d,"object")&&a.hasOwn(d,"raw"))return e(d.raw);if(a.is(d,"array")){var i=d.slice(1);d=g(d[0]).replace(/\{([0-9]+)\}/g,function(b,c){return a.hasOwn(i,c)?e(i[c]):b})}return g(d).replace(/{context:\w+}$/,"")},data:b}}),g("16",["e","5","6"],function(a,b,c){function d(a){function d(){try{return document.activeElement}catch(a){return document.body}}function j(a,b){if(b&&b.startContainer){if(!a.isChildOf(b.startContainer,a.getRoot())||!a.isChildOf(b.endContainer,a.getRoot()))return;return{startContainer:b.startContainer,startOffset:b.startOffset,endContainer:b.endContainer,endOffset:b.endOffset}}return b}function l(a,b){var c;return b.startContainer?(c=a.getDoc().createRange(),c.setStart(b.startContainer,b.startOffset),c.setEnd(b.endContainer,b.endOffset)):c=b,c}function m(m){var n=m.editor;n.on("init",function(){(n.inline||c.ie)&&("onbeforedeactivate"in document&&c.ie<9?n.dom.bind(n.getBody(),"beforedeactivate",function(a){if(a.target==n.getBody())try{n.lastRng=n.selection.getRng()}catch(a){}}):n.on("nodechange mouseup keyup",function(a){var b=d();"nodechange"==a.type&&a.selectionChange||(b&&b.id==n.id+"_ifr"&&(b=n.getBody()),n.dom.isChildOf(b,n.getBody())&&(n.lastRng=n.selection.getRng()))}),c.webkit&&!e&&(e=function(){var b=a.activeEditor;if(b&&b.selection){var c=b.selection.getRng();c&&!c.collapsed&&(n.lastRng=c)}},h.bind(document,"selectionchange",e)))}),n.on("setcontent",function(){n.lastRng=null}),n.on("mousedown",function(){n.selection.lastFocusBookmark=null}),n.on("focusin",function(){var b,c=a.focusedEditor;n.selection.lastFocusBookmark&&(b=l(n,n.selection.lastFocusBookmark),n.selection.lastFocusBookmark=null,n.selection.setRng(b)),c!=n&&(c&&c.fire("blur",{focusedEditor:n}),a.setActive(n),a.focusedEditor=n,n.fire("focus",{blurredEditor:c}),n.focus(!0)),n.lastRng=null}),n.on("focusout",function(){b.setEditorTimeout(n,function(){var b=a.focusedEditor;i(n,d())||b!=n||(n.fire("blur",{focusedEditor:null}),a.focusedEditor=null,n.selection&&(n.selection.lastFocusBookmark=null))})}),f||(f=function(b){var c,d=a.activeEditor;c=b.target,d&&c.ownerDocument===document&&(d.selection&&c!==d.getBody()&&k(n,c)&&(d.selection.lastFocusBookmark=j(d.dom,d.lastRng)),c===document.body||i(d,c)||a.focusedEditor!==d||(d.fire("blur",{focusedEditor:null}),a.focusedEditor=null))},h.bind(document,"focusin",f)),n.inline&&!g&&(g=function(b){var c=a.activeEditor,d=c.dom;if(c.inline&&d&&!d.isChildOf(b.target,c.getBody())){var e=c.selection.getRng();e.collapsed||(c.lastRng=e)}},h.bind(document,"mouseup",g));
-}function n(b){a.focusedEditor==b.editor&&(a.focusedEditor=null),a.activeEditor||(h.unbind(document,"selectionchange",e),h.unbind(document,"focusin",f),h.unbind(document,"mouseup",g),e=f=g=null)}a.on("AddEditor",m),a.on("RemoveEditor",n)}var e,f,g,h=a.DOM,i=function(a,b){var c=a?a.settings.custom_ui_selector:"",e=h.getParent(b,function(b){return d.isEditorUIElement(b)||!!c&&a.dom.is(b,c)});return null!==e},j=function(a){return a.inline===!0},k=function(a,b){return j(a)===!1||a.dom.isChildOf(b,a.getBody())===!1};return d.isEditorUIElement=function(a){return a.className.toString().indexOf("mce-")!==-1},d._isUIElement=i,d}),g("27",["9"],function(a){var b=a.each,c=a.explode,d=function(a){a.on("AddEditor",function(a){var d=a.editor;d.on("preInit",function(){function a(a,c){b(c,function(b,c){b&&h.setStyle(a,c,b)}),h.rename(a,"span")}function e(a){h=d.dom,i.convert_fonts_to_spans&&b(h.select("font,u,strike",a.node),function(a){f[a.nodeName.toLowerCase()](h,a)})}var f,g,h,i=d.settings;i.inline_styles&&(g=c(i.font_size_legacy_values),f={font:function(b,c){a(c,{backgroundColor:c.style.backgroundColor,color:c.color,fontFamily:c.face,fontSize:g[parseInt(c.size,10)-1]})},u:function(b,c){"html4"===d.settings.schema&&a(c,{textDecoration:"underline"})},strike:function(b,c){a(c,{textDecoration:"line-through"})}},d.on("PreProcess SetContent",e))})})};return{register:d}}),g("17",["g","a","e","14","6","26","16","27","15","z","4","9","w"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(a){v(s.editors,function(b){"scroll"===a.type?b.fire("ScrollWindow",a):b.fire("ResizeWindow",a)})}function o(a,c){c!==y&&(c?b(window).on("resize scroll",n):b(window).off("resize scroll",n),y=c)}function p(a){var b,c=s.editors;delete c[a.id];for(var d=0;d
"+c,d.removeChild(d.firstChild)}catch(b){a("").html("
"+c).contents().slice(1).appendTo(d)}return c}}):b.html(c)},getOuterHTML:function(b){return b=this.get(b),1==b.nodeType&&"outerHTML"in b?b.outerHTML:a("").append(a(b).clone()).html()},setOuterHTML:function(b,c){var d=this;d.$$(b).each(function(){try{if("outerHTML"in this)return void(this.outerHTML=c)}catch(a){}d.remove(a(this).html(c),!0)})},decode:h.decode,encode:h.encodeAllRaw,insertAfter:function(a,b){return b=this.get(b),this.run(a,function(a){var c,d;return c=b.parentNode,d=b.nextSibling,d?c.insertBefore(a,d):c.appendChild(a),a})},replace:function(a,b,c){var d=this;return d.run(b,function(b){return q(b,"array")&&(a=a.cloneNode(!0)),c&&p(r(b.childNodes),function(b){a.appendChild(b)}),b.parentNode.replaceChild(a,b)})},rename:function(a,b){var c,d=this;return a.nodeName!=b.toUpperCase()&&(c=d.create(b),p(d.getAttribs(a),function(b){d.setAttrib(c,b.nodeName,d.getAttrib(a,b.nodeName))}),d.replace(c,a,1)),c||a},findCommonAncestor:function(a,b){for(var c,d=a;d;){for(c=b;c&&d!=c;)c=c.parentNode;if(d==c)break;d=d.parentNode}return!d&&a.ownerDocument?a.ownerDocument.documentElement:d},toHex:function(a){return this.styles.toHex(k.trim(a))},run:function(a,b,c){var d,e=this;return"string"==typeof a&&(a=e.get(a)),!!a&&(c=c||this,a.nodeType||!a.length&&0!==a.length?b.call(c,a):(d=[],p(a,function(a,f){a&&("string"==typeof a&&(a=e.get(a)),d.push(b.call(c,a,f)))}),d))},getAttribs:function(a){var b;if(a=this.get(a),!a)return[];if(t){if(b=[],"OBJECT"==a.nodeName)return a.attributes;"OPTION"===a.nodeName&&this.getAttrib(a,"selected")&&b.push({specified:1,nodeName:"selected"});var c=/<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi;return a.cloneNode(!1).outerHTML.replace(c,"").replace(/[\w:\-]+/gi,function(a){b.push({specified:1,nodeName:a})}),b}return a.attributes},isEmpty:function(a,b){var c,d,e,g,h,i,j=this,k=0;if(a=a.firstChild){h=new f(a,a.parentNode),b=b||(j.schema?j.schema.getNonEmptyElements():null),g=j.schema?j.schema.getWhiteSpaceElements():{};do{if(e=a.nodeType,1===e){var l=a.getAttribute("data-mce-bogus");if(l){a=h.next("all"===l);continue}if(i=a.nodeName.toLowerCase(),b&&b[i]){if("br"===i){k++,a=h.next();continue}return!1}for(d=j.getAttribs(a),c=d.length;c--;)if(i=d[c].nodeName,"name"===i||"data-mce-bookmark"===i)return!1}if(8==e)return!1;if(3===e&&!v.test(a.nodeValue))return!1;if(3===e&&a.parentNode&&g[a.parentNode.nodeName]&&v.test(a.nodeValue))return!1;a=h.next()}while(a)}return k<=1},createRng:function(){var a=this.doc;return a.createRange?a.createRange():new c(this)},nodeIndex:n,split:function(a,b,c){function d(a){function b(a){var b=a.previousSibling&&"SPAN"==a.previousSibling.nodeName,c=a.nextSibling&&"SPAN"==a.nextSibling.nodeName;return b&&c}var c,e=a.childNodes,f=a.nodeType;if(1!=f||"bookmark"!=a.getAttribute("data-mce-type")){for(c=e.length-1;c>=0;c--)d(e[c]);if(9!=f){if(3==f&&a.nodeValue.length>0){var g=s(a.nodeValue).length;if(!h.isBlock(a.parentNode)||g>0||0===g&&b(a))return}else if(1==f&&(e=a.childNodes,1==e.length&&e[0]&&1==e[0].nodeType&&"bookmark"==e[0].getAttribute("data-mce-type")&&a.parentNode.insertBefore(e[0],a),e.length||/^(br|hr|input|img)$/i.test(a.nodeName)))return;h.remove(a)}return a}}var e,f,g,h=this,i=h.createRng();if(a&&b)return i.setStart(a.parentNode,h.nodeIndex(a)),i.setEnd(b.parentNode,h.nodeIndex(b)),e=i.extractContents(),i=h.createRng(),i.setStart(b.parentNode,h.nodeIndex(b)+1),i.setEnd(a.parentNode,h.nodeIndex(a)+1),f=i.extractContents(),g=a.parentNode,g.insertBefore(d(e),a),c?g.insertBefore(c,a):g.insertBefore(b,a),g.insertBefore(d(f),a),h.remove(a),c||b},bind:function(a,b,c,d){var e=this;if(k.isArray(a)){for(var f=a.length;f--;)a[f]=e.bind(a[f],b,c,d);return a}return!e.settings.collect||a!==e.doc&&a!==e.win||e.boundEvents.push([a,b,c,d]),e.events.bind(a,b,c,d||e)},unbind:function(a,b,c){var d,e=this;if(k.isArray(a)){for(d=a.length;d--;)a[d]=e.unbind(a[d],b,c);return a}if(e.boundEvents&&(a===e.doc||a===e.win))for(d=e.boundEvents.length;d--;){var f=e.boundEvents[d];a!=f[0]||b&&b!=f[1]||c&&c!=f[2]||this.events.unbind(f[0],f[1],f[2])}return this.events.unbind(a,b,c)},fire:function(a,b,c){return this.events.fire(a,b,c)},getContentEditable:function(a){var b;return a&&1==a.nodeType?(b=a.getAttribute("data-mce-contenteditable"),b&&"inherit"!==b?b:"inherit"!==a.contentEditable?a.contentEditable:null):null},getContentEditableParent:function(a){for(var b=this.getRoot(),c=null;a&&a!==b&&(c=this.getContentEditable(a),null===c);a=a.parentNode);return c},destroy:function(){var a=this;if(a.boundEvents){for(var b=a.boundEvents.length;b--;){var c=a.boundEvents[b];this.events.unbind(c[0],c[1],c[2])}a.boundEvents=null}d.setDocument&&d.setDocument(),a.win=a.doc=a.root=a.events=a.frag=null},isChildOf:function(a,b){for(;a;){if(b===a)return!0;a=a.parentNode}return!1},dumpRng:function(a){return"startContainer: "+a.startContainer.nodeName+", startOffset: "+a.startOffset+", endContainer: "+a.endContainer.nodeName+", endOffset: "+a.endOffset},_findSib:function(a,b,c){var d=this,e=b;if(a)for("string"==typeof e&&(e=function(a){return d.is(a,b)}),a=a[c];a;a=a[c])if(e(a))return a;return null}},o.DOM=new o(document),o.nodeIndex=n,o}),g("f",["c","1d"],function(a,b){function c(){function a(a,c,e){function f(){k.remove(j),i&&(i.onreadystatechange=i.onload=i=null),c()}function h(){g(e)?e():"undefined"!=typeof console&&console.log&&console.log("Failed to load script: "+a)}var i,j,k=d;j=k.uniqueId(),i=document.createElement("script"),i.id=j,i.type="text/javascript",i.src=b._addCacheSuffix(a),"onreadystatechange"in i?i.onreadystatechange=function(){/loaded|complete/.test(i.readyState)&&f()}:i.onload=f,i.onerror=h,(document.getElementsByTagName("head")[0]||document.body).appendChild(i)}var c,h=0,i=1,j=2,k=3,l={},m=[],n={},o=[],p=0;this.isDone=function(a){return l[a]==j},this.markDone=function(a){l[a]=j},this.add=this.load=function(a,b,d,e){var f=l[a];f==c&&(m.push(a),l[a]=h),b&&(n[a]||(n[a]=[]),n[a].push({success:b,failure:e,scope:d||this}))},this.remove=function(a){delete l[a],delete n[a]},this.loadQueue=function(a,b,c){this.loadScripts(m,a,b,c)},this.loadScripts=function(b,d,h,m){function q(a,b){e(n[b],function(b){g(b[a])&&b[a].call(b.scope)}),n[b]=c}var r,s=[];o.push({success:d,failure:m,scope:h||this}),(r=function(){var c=f(b);b.length=0,e(c,function(b){return l[b]===j?void q("success",b):l[b]===k?void q("failure",b):void(l[b]!==i&&(l[b]=i,p++,a(b,function(){l[b]=j,p--,q("success",b),r()},function(){l[b]=k,p--,s.push(b),q("failure",b),r()})))}),p||(e(o,function(a){0===s.length?g(a.success)&&a.success.call(a.scope):g(a.failure)&&a.failure.call(a.scope,s)}),o.length=0)})()}}var d=a.DOM,e=b.each,f=b.grep,g=function(a){return"function"==typeof a};return c.ScriptLoader=new c,c}),g("5",["f","1d"],function(a,b){function c(){var a=this;a.items=[],a.urls={},a.lookup={}}var d=b.each;return c.prototype={get:function(a){if(this.lookup[a])return this.lookup[a].instance},dependencies:function(a){var b;return this.lookup[a]&&(b=this.lookup[a].dependencies),b||[]},requireLangPack:function(b,d){var e=c.language;if(e&&c.languageLoad!==!1){if(d)if(d=","+d+",",d.indexOf(","+e.substr(0,2)+",")!=-1)e=e.substr(0,2);else if(d.indexOf(","+e+",")==-1)return;a.ScriptLoader.add(this.urls[b]+"/langs/"+e+".js")}},add:function(a,b,c){return this.items.push(b),this.lookup[a]={instance:b,dependencies:c},b},remove:function(a){delete this.urls[a],delete this.lookup[a]},createUrl:function(a,b){return"object"==typeof b?b:{prefix:a.prefix,resource:b,suffix:a.suffix}},addComponents:function(b,c){var e=this.urls[b];d(c,function(b){a.ScriptLoader.add(e+"/"+b)})},load:function(b,e,f,g,h){function i(){var c=j.dependencies(b);d(c,function(a){var b=j.createUrl(e,a);j.load(b.resource,b,void 0,void 0)}),f&&(g?f.call(g):f.call(a))}var j=this,k=e;j.urls[b]||("object"==typeof e&&(k=e.prefix+e.resource+e.suffix),0!==k.indexOf("/")&&k.indexOf("://")==-1&&(k=c.baseURL+"/"+k),j.urls[b]=k.substring(0,k.lastIndexOf("/")),j.lookup[b]?i():a.ScriptLoader.add(k,i,g,h))}},c.PluginManager=new c,c.ThemeManager=new c,c}),g("1k",[],function(){var a=function(b){var c=b,d=function(){return c},e=function(a){c=a},f=function(){return a(d())};return{get:d,set:e,clone:f}};return a}),g("21",[],function(){function a(a){return function(b){return!!b&&b.nodeType==a}}function b(a){return a=a.toLowerCase().split(" "),function(b){var c,d;if(b&&b.nodeType)for(d=b.nodeName.toLowerCase(),c=0;c
'),a}function j(){var c,d;return c=i.createRng(),d=a.resolve(i.getRoot(),b.start),c.setStart(d.container(),d.offset()),d=a.resolve(i.getRoot(),b.end),c.setEnd(d.container(),d.offset()),c}var k,l,m,n,o,p;if(b)if(h.isArray(b.start)){if(k=i.createRng(),l=i.getRoot(),g.tridentSel)return g.tridentSel.moveToBookmark(b);c(!0)&&c()&&g.setRng(k)}else"string"==typeof b.start?g.setRng(j(b)):b.id?(d("start"),d("end"),m&&(k=i.createRng(),k.setStart(e(m),o),k.setEnd(e(n),p),g.setRng(k))):b.name?g.select(i.select(b.name)[b.index]):b.rng&&g.setRng(b.rng)}}var j=d.isContentEditableFalse,k=function(a,b){var c,d;for(d=g.trim(a.data.slice(0,b)).length,c=a.previousSibling;c&&3===c.nodeType;c=c.previousSibling)d+=g.trim(c.data).length;return d},l=function(a){d.isText(a)&&0===a.data.length&&a.parentNode.removeChild(a)};return i.isBookmarkNode=function(a){return a&&"SPAN"===a.tagName&&"bookmark"===a.getAttribute("data-mce-type")},i}),g("62",["4i","j","21","20","1z","4j"],function(a,b,c,d,e,f){function g(a){return a>0}function h(a){return a<0}function i(a,b){for(var c;c=a(b);)if(!y(c))return c;return null}function j(a,c,d,e,f){var j=new b(a,e);if(h(c)){if((v(a)||y(a))&&(a=i(j.prev,!0),d(a)))return a;for(;a=i(j.prev,f);)if(d(a))return a}if(g(c)){if((v(a)||y(a))&&(a=i(j.next,!0),d(a)))return a;for(;a=i(j.next,f);)if(d(a))return a}return null}function k(a,b){for(a=a.parentNode;a&&a!=b;a=a.parentNode)if(u(a))return a;return b}function l(a,b){for(;a&&a!=b;){if(w(a))return a;a=a.parentNode}return null}function m(a,b,c){return l(a.container(),c)==l(b.container(),c)}function n(a,b,c){return k(a.container(),c)==k(b.container(),c)}function o(a,b){var c,d;return b?(c=b.container(),d=b.offset(),A(c)?c.childNodes[d+a]:null):null}function p(a,b){var c=b.ownerDocument.createRange();return a?(c.setStartBefore(b),c.setEndBefore(b)):(c.setStartAfter(b),c.setEndAfter(b)),c}function q(a,b,c){return l(b,a)==l(c,a)}function r(a,b,c){var d,e;for(e=a?"previousSibling":"nextSibling";c&&c!=b;){if(d=c[e],x(d)&&(d=d[e]),v(d)){if(q(b,d,c))return d;break}if(B(d))break;c=c.parentNode}return null}function s(a,b,d){var f,g,h,i,j=z(r,!0,b),k=z(r,!1,b);if(g=d.startContainer,h=d.startOffset,e.isCaretContainerBlock(g)){if(A(g)||(g=g.parentNode),i=g.getAttribute("data-mce-caret"),"before"==i&&(f=g.nextSibling,v(f)))return C(f);if("after"==i&&(f=g.previousSibling,v(f)))return D(f)}if(!d.collapsed)return d;if(c.isText(g)){if(x(g)){if(1===a){if(f=k(g))return C(f);if(f=j(g))return D(f)}if(a===-1){if(f=j(g))return D(f);if(f=k(g))return C(f)}return d}if(e.endsWithCaretContainer(g)&&h>=g.data.length-1)return 1===a&&(f=k(g))?C(f):d;if(e.startsWithCaretContainer(g)&&h<=1)return a===-1&&(f=j(g))?D(f):d;if(h===g.data.length)return f=k(g),f?C(f):d;if(0===h)return f=j(g),f?D(f):d}return d}function t(a,b){return v(o(a,b))}var u=c.isContentEditableTrue,v=c.isContentEditableFalse,w=c.matchStyleValues("display","block table table-cell table-caption list-item"),x=e.isCaretContainer,y=e.isCaretContainerBlock,z=a.curry,A=c.isElement,B=f.isCaretCandidate,C=z(p,!0),D=z(p,!1);return{isForwards:g,isBackwards:h,findNode:j,getEditingHost:k,getParentBlock:l,isInSameBlock:m,isInSameEditingHost:n,isBeforeContentEditableFalse:z(t,0),isAfterContentEditableFalse:z(t,-1),normalizeRange:s}}),g("5j",["21","4j","20","62","1h","4i"],function(a,b,c,d,e,f){function g(a,b){for(var c=[];a&&a!=b;)c.push(a),a=a.parentNode;return c}function h(a,b){return a.hasChildNodes()&&b
'))},m=function(a){return e.isText(a)?"\xa0"===f.get(a):i.isBr(a)},n=function(b){return 1===a.filter(h.children(b),m).length},o=function(a){h.lastChild(a).each(function(b){h.prevSibling(b).each(function(d){i.isBlock(a)&&i.isBr(b)&&i.isBlock(d)&&c.remove(b)})})};return{removeTrailingBr:k,fillWithPaddingBr:l,isPaddedElement:n,trimBlockTrailingBr:o}}),g("4p",["j"],function(a){var b=function(a){return a&&/^(IMG)$/.test(a.nodeName)},c=function(c,d,e){var f,h,i,j=e.startContainer,k=e.startOffset;if((e.startContainer!==e.endContainer||!b(e.startContainer.childNodes[e.startOffset]))&&(3===j.nodeType&&k>=j.nodeValue.length&&(k=c.nodeIndex(j),j=j.parentNode),1===j.nodeType))for(i=j.childNodes,k
").append(b.childNodes)}var i,j,k=d.selection.getRng();i=b.matchNodeNames("pre"),k.collapsed||(j=d.selection.getSelectedBlocks(),h(g(g(j,i),e),function(a){f(a.previousSibling,a)}))}),{postProcess:e}}),g("5k",["9","1d"],function(a,b){function c(b){this.compare=function(c,e){function f(a){var c={};return d(b.getAttribs(a),function(d){var e=d.nodeName.toLowerCase();0!==e.indexOf("_")&&"style"!==e&&0!==e.indexOf("data-")&&(c[e]=b.getAttrib(a,e))}),c}function g(a,b){var c,d;for(d in a)if(a.hasOwnProperty(d)){if(c=b[d],"undefined"==typeof c)return!1;if(a[d]!=c)return!1;delete b[d]}for(d in b)if(b.hasOwnProperty(d))return!1;return!0}return c.nodeName==e.nodeName&&(!!g(f(c),f(e))&&(!!g(b.parseStyle(b.getAttrib(c,"style")),b.parseStyle(b.getAttrib(e,"style")))&&(!a.isBookmarkNode(c)&&!a.isBookmarkNode(e))))}}var d=b.each;return c}),g("1q",["1","9","e","j","4n","4o","4p","1o","1d"],function(a,b,c,d,e,f,g,h,i){var j=/^(src|href|style)$/,k=i.each,l=g.isEq,m=function(a){return/^(TH|TD)$/.test(a.nodeName)},n=function(a,b,c){var e,f,g;return e=b[c?"startContainer":"endContainer"],f=b[c?"startOffset":"endOffset"],1===e.nodeType&&(g=e.childNodes.length-1,!c&&f&&f--,e=e.childNodes[f>g?g:f]),3===e.nodeType&&c&&f>=e.nodeValue.length&&(e=new d(e,a.getBody()).next()||e),3!==e.nodeType||c||0!==f||(e=new d(e,a.getBody()).prev()||e),e},o=function(a,b,c,d){var e=a.create(c,d);return b.parentNode.insertBefore(e,b),e.appendChild(b),e},p=function(a,b,c){return!!l(b,c.inline)||(!!l(b,c.block)||(c.selector?1===b.nodeType&&a.is(b,c.selector):void 0))},q=function(a,b){return b.links&&"A"===a.tagName},r=function(a,b,c,d){return b=g.getNonWhiteSpaceSibling(b,c,d),!b||"BR"===b.nodeName||a.isBlock(b)},s=function(a,b,c){var d,e=b.parentNode,f=a.dom,h=a.settings.forced_root_block;c.block&&(h?e===f.getRoot()&&(c.list_block&&l(b,c.list_block)||k(i.grep(b.childNodes),function(b){g.isValid(a,h,b.nodeName.toLowerCase())?d?d.appendChild(b):(d=o(f,b,h),f.setAttribs(d,a.settings.forced_root_block_attrs)):d=0})):f.isBlock(b)&&!f.isBlock(e)&&(r(f,b,!1)||r(f,b.firstChild,!0,1)||b.insertBefore(f.create("br"),b.firstChild),r(f,b,!0)||r(f,b.lastChild,!1,1)||b.appendChild(f.create("br")))),c.selector&&c.inline&&!l(c.inline,b)||f.remove(b,1)},t=function(a,b,c,d,e){var f,h,i,m=a.dom;if(!p(m,d,b)&&!q(d,b))return!1;if("all"!==b.remove)for(k(b.styles,function(a,f){a=g.normalizeStyleValue(m,g.replaceVars(a,c),f),"number"==typeof f&&(f=a,e=0),(b.remove_similar||!e||l(g.getStyle(m,e,f),a))&&m.setStyle(d,f,""),i=1}),i&&""===m.getAttrib(d,"style")&&(d.removeAttribute("style"),d.removeAttribute("data-mce-style")),k(b.attributes,function(a,b){var f;if(a=g.replaceVars(a,c),"number"==typeof b&&(b=a,e=0),!e||l(m.getAttrib(e,b),a)){if("class"===b&&(a=m.getAttrib(d,b),a&&(f="",k(a.split(/\s+/),function(a){/mce\-\w+/.test(a)&&(f+=(f?" ":"")+a)}),f)))return void m.setAttrib(d,b,f);"class"===b&&d.removeAttribute("className"),j.test(b)&&d.removeAttribute("data-mce-"+b),d.removeAttribute(b)}}),k(b.classes,function(a){a=g.replaceVars(a,c),e&&!m.hasClass(e,a)||m.removeClass(d,a)}),h=m.getAttribs(d),f=0;f
"+(g.item?g.item(0).outerHTML:g.htmlText),h.removeChild(h.firstChild)):h.innerHTML=g.toString(),/^\s/.test(h.innerHTML)&&(b=" "),/\s+$/.test(h.innerHTML)&&(d=" "),a.getInner=!0,a.content=f.isCollapsed()?"":b+f.serializer.serialize(h,a)+d,f.editor.fire("GetContent",a),a.content)},setContent:function(a,b){var c,d,e,f=this,g=f.getRng(),h=f.win.document;if(b=b||{format:"html"},b.set=!0,b.selection=!0,b.content=a,b.no_events||f.editor.fire("BeforeSetContent",b),a=b.content,g.insertNode){a+='_',g.startContainer==h&&g.endContainer==h?h.body.innerHTML=a:(g.deleteContents(),0===h.body.childNodes.length?h.body.innerHTML=a:g.createContextualFragment?g.insertNode(g.createContextualFragment(a)):(d=h.createDocumentFragment(),e=h.createElement("div"),d.appendChild(e),e.outerHTML=a,g.insertNode(d))),c=f.dom.get("__caret"),g=h.createRange(),g.setStartBefore(c),g.setEndBefore(c),f.setRng(g),f.dom.remove("__caret");try{f.setRng(g)}catch(a){}}else g.item&&(h.execCommand("Delete",!1,null),g=f.getRng()),/^\s+/.test(a)?(g.pasteHTML('_'+a),f.dom.remove("__mce_tmp")):g.pasteHTML(a);b.no_events||f.editor.fire("SetContent",b)},getStart:function(a){var b,c,d,e,f=this,g=f.getRng();if(g.duplicate||g.item){if(g.item)return g.item(0);for(d=g.duplicate(),d.collapse(1),b=d.parentElement(),b.ownerDocument!==f.dom.doc&&(b=f.dom.getRoot()),c=e=g.parentElement();e=e.parentNode;)if(e==b){b=c;break}return b}return b=g.startContainer,1==b.nodeType&&b.hasChildNodes()&&(a&&g.collapsed||(b=b.childNodes[Math.min(b.childNodes.length-1,g.startOffset)])),b&&3==b.nodeType?b.parentNode:b},getEnd:function(a){var b,c,d=this,e=d.getRng();return e.duplicate||e.item?e.item?e.item(0):(e=e.duplicate(),e.collapse(0),b=e.parentElement(),b.ownerDocument!==d.dom.doc&&(b=d.dom.getRoot()),b&&"BODY"==b.nodeName?b.lastChild||b:b):(b=e.endContainer,c=e.endOffset,1==b.nodeType&&b.hasChildNodes()&&(a&&e.collapsed||(b=b.childNodes[c>0?c-1:c])),b&&3==b.nodeType?b.parentNode:b)},getBookmark:function(a,b){return this.bookmarkManager.getBookmark(a,b)},moveToBookmark:function(a){return this.bookmarkManager.moveToBookmark(a)},select:function(a,b){var c,d=this,e=d.dom,f=e.createRng();if(d.lastFocusBookmark=null,a){if(!b&&d.controlSelection.controlSelect(a))return;c=e.nodeIndex(a),f.setStart(a.parentNode,c),f.setEnd(a.parentNode,c+1),b&&(d._moveEndPoint(f,a,!0),d._moveEndPoint(f,a)),d.setRng(f)}return a},isCollapsed:function(){var a=this,b=a.getRng(),c=a.getSel();return!(!b||b.item)&&(b.compareEndPoints?0===b.compareEndPoints("StartToEnd",b):!c||b.collapsed)},collapse:function(a){var b,c=this,d=c.getRng();d.item&&(b=d.item(0),d=c.win.document.body.createTextRange(),d.moveToElementText(b)),d.collapse(!!a),c.setRng(d)},getSel:function(){var a=this.win;return a.getSelection?a.getSelection():a.document.selection},getRng:function(a){function b(a,b,c){try{return b.compareBoundaryPoints(a,c)}catch(a){return-1}}var c,d,e,f,g,h=this;if(!h.win)return null;if(f=h.win.document,"undefined"==typeof f||null===f)return null;if(!a&&h.lastFocusBookmark){var i=h.lastFocusBookmark;return i.startContainer?(d=f.createRange(),d.setStart(i.startContainer,i.startOffset),d.setEnd(i.endContainer,i.endOffset)):d=i,d}if(a&&h.tridentSel)return h.tridentSel.getRangeAt(0);try{(c=h.getSel())&&(d=c.rangeCount>0?c.getRangeAt(0):c.createRange?c.createRange():f.createRange())}catch(a){}if(d=w(h.editor,[d])[0],t&&d&&d.setStart&&f.selection){try{g=f.selection.createRange()}catch(a){}g&&g.item&&(e=g.item(0),d=f.createRange(),d.setStartBefore(e),d.setEndAfter(e))}return d||(d=f.createRange?f.createRange():f.body.createTextRange()),d.setStart&&9===d.startContainer.nodeType&&d.collapsed&&(e=h.dom.getRoot(),d.setStart(e,0),d.setEnd(e,0)),h.selectedRange&&h.explicitRange&&(0===b(d.START_TO_START,d,h.selectedRange)&&0===b(d.END_TO_END,d,h.selectedRange)?d=h.explicitRange:(h.selectedRange=null,h.explicitRange=null)),d},setRng:function(a,b){var c,d,e,f=this;if(v(a))if(a.select){f.explicitRange=null;try{a.select()}catch(a){}}else if(f.tridentSel){if(a.cloneRange)try{f.tridentSel.addRange(a)}catch(a){}}else{if(c=f.getSel(),e=f.editor.fire("SetSelectionRange",{range:a,forward:b}),a=e.range,c){f.explicitRange=a;try{c.removeAllRanges(),c.addRange(a)}catch(a){}b===!1&&c.extend&&(c.collapse(a.endContainer,a.endOffset),c.extend(a.startContainer,a.startOffset)),f.selectedRange=c.rangeCount>0?c.getRangeAt(0):null}a.collapsed||a.startContainer!==a.endContainer||!c.setBaseAndExtent||l.ie||a.endOffset-a.startOffset<2&&a.startContainer.hasChildNodes()&&(d=a.startContainer.childNodes[a.startOffset],d&&"IMG"===d.tagName&&(c.setBaseAndExtent(a.startContainer,a.startOffset,a.endContainer,a.endOffset),c.anchorNode===a.startContainer&&c.focusNode===a.endContainer||c.setBaseAndExtent(d,0,d,1))),f.editor.fire("AfterSetSelectionRange",{range:a,forward:b})}},setNode:function(a){var b=this;return b.setContent(b.dom.getOuterHTML(a)),a},getNode:function(){function a(a,b){for(var c=a;a&&3===a.nodeType&&0===a.length;)a=b?a.nextSibling:a.previousSibling;return a||c}var b,c,d,e,f,g=this,h=g.getRng(),i=g.dom.getRoot();return h?(c=h.startContainer,d=h.endContainer,e=h.startOffset,f=h.endOffset,h.setStart?(b=h.commonAncestorContainer,!h.collapsed&&(c==d&&f-e<2&&c.hasChildNodes()&&(b=c.childNodes[e]),3===c.nodeType&&3===d.nodeType&&(c=c.length===e?a(c.nextSibling,!0):c.parentNode,d=0===f?a(d.previousSibling,!1):d.parentNode,c&&c===d))?c:b&&3==b.nodeType?b.parentNode:b):(b=h.item?h.item(0):h.parentElement(),b.ownerDocument!==g.win.document&&(b=i),b)):i},getSelectedBlocks:function(a,b){var c,d,e=this,f=e.dom,g=[];if(d=f.getRoot(),a=f.getParent(a||e.getStart(),f.isBlock),b=f.getParent(b||e.getEnd(),f.isBlock),a&&a!=d&&g.push(a),a&&b&&a!=b){c=a;for(var h=new j(a,d);(c=h.next())&&c!=b;)f.isBlock(c)&&g.push(c)}return b&&a!=b&&b!=d&&g.push(b),g},isForward:function(){var a,b,c=this.dom,d=this.getSel();return!(d&&d.anchorNode&&d.focusNode)||(a=c.createRng(),a.setStart(d.anchorNode,d.anchorOffset),a.collapse(!0),b=c.createRng(),b.setStart(d.focusNode,d.focusOffset),b.collapse(!0),a.compareBoundaryPoints(a.START_TO_START,b)<=0)},normalize:function(){var a=this,b=a.getRng();return new h(a.dom).normalize(b)&&!n.hasMultipleRanges(a.getSel())&&a.setRng(b,a.isForward()),b},selectorChanged:function(a,b){var c,d=this;return d.selectorChangedData||(d.selectorChangedData={},c={},d.editor.on("NodeChange",function(a){var b=a.element,e=d.dom,f=e.getParents(b,null,e.getRoot()),g={};r(d.selectorChangedData,function(a,b){r(f,function(d){if(e.is(d,b))return c[b]||(r(a,function(a){a(!0,{node:d,selector:b,parents:f})}),c[b]=a),g[b]=a,!1})}),r(c,function(a,d){g[d]||(delete c[d],r(a,function(a){a(!1,{node:b,selector:d,parents:f})}))})})),d.selectorChangedData[a]||(d.selectorChangedData[a]=[]),d.selectorChangedData[a].push(b),d},getScrollContainer:function(){for(var a,b=this.dom.getRoot();b&&"BODY"!=b.nodeName;){if(b.scrollHeight>b.clientHeight){a=b;break}b=b.parentNode}return a},scrollIntoView:function(a,b){i.scrollIntoView(this.editor,a,b)},placeCaretAt:function(a,b){this.setRng(h.getCaretRangeFromPoint(a,b,this.editor.getDoc()))},_moveEndPoint:function(a,b,c){var d=b,e=new j(b,d),f=this.dom.schema.getNonEmptyElements();do{if(3==b.nodeType&&0!==s(b.nodeValue).length)return void(c?a.setStart(b,0):a.setEnd(b,b.nodeValue.length));if(f[b.nodeName]&&!/^(TD|TH)$/.test(b.nodeName))return void(c?a.setStartBefore(b):"BR"==b.nodeName?a.setEndBefore(b):a.setEndAfter(b));if(l.ie&&l.ie<11&&this.dom.isBlock(b)&&this.dom.isEmpty(b))return void(c?a.setStart(b,0):a.setEnd(b,0))}while(b=c?e.next():e.prev());"BODY"==d.nodeName&&(c?a.setStart(d,0):a.setEnd(d,d.childNodes.length))},getBoundingClientRect:function(){var a=this.getRng();return a.collapsed?d.fromRangeStart(a).getClientRects()[0]:a.getBoundingClientRect()},destroy:function(){this.win=null,this.controlSelection.destroy()}},q}),g("t",[],function(){function a(a,b,c){var d,e,f=c?"lastChild":"firstChild",g=c?"prev":"next";if(a[f])return a[f];if(a!==b){if(d=a[g])return d;for(e=a.parent;e&&e!==b;e=e.parent)if(d=e[g])return d}}function b(a,b){this.name=a,this.type=b,1===b&&(this.attributes=[],this.attributes.map={})}var c=/^[ \t\r\n]*$/,d={"#text":3,"#comment":8,"#cdata":4,"#pi":7,"#doctype":10,"#document-fragment":11};return b.prototype={replace:function(a){var b=this;return a.parent&&a.remove(),b.insert(a,b),b.remove(),b},attr:function(a,b){var c,d,e,f=this;if("string"!=typeof a){for(d in a)f.attr(d,a[d]);return f}if(c=f.attributes){if(b!==e){if(null===b){if(a in c.map)for(delete c.map[a],d=c.length;d--;)if(c[d].name===a)return c=c.splice(d,1),f;return f}if(a in c.map){for(d=c.length;d--;)if(c[d].name===a){c[d].value=b;break}}else c.push({name:a,value:b});return c.map[a]=b,f}return c.map[a]}},clone:function(){var a,c,d,e,f,g=this,h=new b(g.name,g.type);if(d=g.attributes){for(f=[],f.map={},a=0,c=d.length;a
');return e.empty(a),d.append(a,c),b.some(l.before(c.dom()))}return b.none()},E=function(a,b){return c.liftN([i.prevSibling(a),i.nextSibling(a),b],function(b,c,d){var f,g=b.dom(),h=c.dom();return n.isText(g)&&n.isText(h)?(f=g.data.length,g.appendData(h.data),e.remove(c),e.remove(a),d.container()===h?new l(g,f):d):(e.remove(a),d)}).orThunk(function(){return e.remove(a),b})},F=function(c,d,e){var f=z(d,c.getBody(),e.dom()),g=h.ancestor(e,a.curry(C,c),B(c.getBody())),i=E(e,f);g.bind(D).fold(function(){A(c,d,i)},function(a){A(c,d,b.some(a))})};return{deleteElement:F}}),g("5f",["1t","6a","23","6c","20","6p","6q","5g","21"],function(a,b,c,d,e,f,g,h,i){var j=function(a,b){return function(d){return g.deleteElement(a,b,c.fromDom(d)),!0}},k=function(a,b){return function(c){var d=b?e.before(c):e.after(c);return a.selection.setRng(d.toRange()),!0}},l=function(a){return function(b){return a.selection.setRng(b.toRange()),!0}},m=function(a,b){var c=f.read(a.getBody(),b,a.selection.getRng()).map(function(c){return c.fold(j(a,b),k(a,b),l(a))});return c.getOr(!1)},n=function(c){a.each(d.descendants(c,".mce-offscreen-selection"),b.remove)},o=function(a,b){var d=a.selection.getNode();return!!i.isContentEditableFalse(d)&&(n(c.fromDom(a.getBody())),g.deleteElement(a,b,c.fromDom(a.selection.getNode())),h.paddEmptyBody(a),!0)},p=function(a,b){for(;b&&b!==a;){if(i.isContentEditableTrue(b)||i.isContentEditableFalse(b))return b;b=b.parentNode}return null},q=function(a){var b,c=p(a.getBody(),a.selection.getNode());return i.isContentEditableTrue(c)&&a.dom.isBlock(c)&&a.dom.isEmpty(c)&&(b=a.dom.create("br",{"data-mce-bogus":"1"}),a.dom.setHTML(c,""),c.appendChild(b),a.selection.setRng(e.before(b).toRange())),!0},r=function(a,b){return a.selection.isCollapsed()?m(a,b):o(a,b)};return{backspaceDelete:r,paddEmptyElement:q}}),g("7a",["1","21","22"],function(a,b,c){var d=b.isText,e=function(a){return d(a)&&a.data[0]===c.ZWSP},f=function(a){return d(a)&&a.data[a.data.length-1]===c.ZWSP},g=function(a){return a.ownerDocument.createTextNode(c.ZWSP)},h=function(a){if(d(a.previousSibling))return f(a.previousSibling)?a.previousSibling:(a.previousSibling.appendData(c.ZWSP),a.previousSibling);if(d(a))return e(a)?a:(a.insertData(0,c.ZWSP),a);var b=g(a);return a.parentNode.insertBefore(b,a),b},i=function(a){if(d(a.nextSibling))return e(a.nextSibling)?a.nextSibling:(a.nextSibling.insertData(0,c.ZWSP),a.nextSibling);if(d(a))return f(a)?a:(a.appendData(c.ZWSP),a);var b=g(a);return a.nextSibling?a.parentNode.insertBefore(b,a.nextSibling):a.parentNode.appendChild(b),b},j=function(a,b){return a?h(b):i(b)};return{insertInline:j,insertInlineBefore:a.curry(j,!0),insertInlineAfter:a.curry(j,!1)}}),g("7b",["1t","1z","20","21","22","1d"],function(a,b,c,d,e,f){var g=d.isElement,h=d.isText,i=function(a){var b=a.parentNode;b&&b.removeChild(a)},j=function(a){try{return a.nodeValue}catch(a){return""}},k=function(a,b){0===b.length?i(a):a.nodeValue=b},l=function(a){var b=e.trim(a);return{count:a.length-b.length,text:b}},m=function(a,b){return s(a),b},n=function(a,b){var d=l(a.data.substr(0,b.offset())),e=l(a.data.substr(b.offset())),f=d.text+e.text;return f.length>0?(k(a,f),new c(a,b.offset()-d.count)):b},o=function(b,d){var e=d.container(),f=a.indexOf(e.childNodes,b).map(function(a){return a
|)$/," "))),a}function p(){var a,c,d;a=J.getRng(!0),c=a.startContainer,d=a.startOffset,3==c.nodeType&&a.collapsed&&("\xa0"===c.data[d]?(c.deleteData(d,1),/[\u00a0| ]$/.test(b)||(b+=" ")):"\xa0"===c.data[d-1]&&(c.deleteData(d-1,1),/[\u00a0| ]$/.test(b)||(b=" "+b)))}function q(){if(H){var b=a.getBody(),c=new e(K);l.each(K.select("*[data-mce-fragment]"),function(a){for(var d=a.parentNode;d&&d!=b;d=d.parentNode)I[a.nodeName.toLowerCase()]&&c.compare(d,a)&&K.remove(a,!0)})}}function r(a){for(var b=a;b=b.walk();)1===b.type&&b.attr("data-mce-fragment","1")}function s(a){l.each(a.getElementsByTagName("*"),function(a){a.removeAttribute("data-mce-fragment")})}function t(a){return!!a.getAttribute("data-mce-fragment")}function u(b){return b&&!a.schema.getShortEndedElements()[b.nodeName]}function v(b){function e(b){for(var c=a.getBody();b&&b!==c;b=b.parentNode)if("false"===a.dom.getContentEditable(b))return b;return null}function f(b){var e=c.fromRangeStart(b),f=new d(a.getBody());if(e=f.next(e))return e.toRange()}var g,h,j;if(b){if(J.scrollIntoView(b),g=e(b))return K.remove(b),void J.select(g);D=K.createRng(),E=b.previousSibling,E&&3==E.nodeType?(D.setStart(E,E.nodeValue.length),i.ie||(F=b.nextSibling,F&&3==F.nodeType&&(E.appendData(F.data),F.parentNode.removeChild(F)))):(D.setStartBefore(b),D.setEndBefore(b)),h=K.getParent(b,K.isBlock),K.remove(b),h&&K.isEmpty(h)&&(a.$(h).empty(),D.setStart(h,0),D.setEnd(h,0),m(h)||t(h)||!(j=f(D))?K.add(h,K.create("br",{"data-mce-bogus":"1"})):(D=j,K.remove(h))),J.setRng(D)}}var w,x,y,z,A,B,C,D,E,F,G,H,I=a.schema.getTextInlineElements(),J=a.selection,K=a.dom;/^ | $/.test(b)&&(b=g(b)),w=a.parser,H=f.merge,x=new j({validate:a.settings.validate},a.schema),G='',B={content:b,format:"html",selection:!0},a.fire("BeforeSetContent",B),b=B.content,b.indexOf("{$caret}")==-1&&(b+="{$caret}"),b=b.replace(/\{\$caret\}/,G),D=J.getRng();var L=D.startContainer||(D.parentElement?D.parentElement():null),M=a.getBody();L===M&&J.isCollapsed()&&K.isBlock(M.firstChild)&&u(M.firstChild)&&K.isEmpty(M.firstChild)&&(D=K.createRng(),D.setStart(M.firstChild,0),D.setEnd(M.firstChild,0),J.setRng(D)),J.isCollapsed()||(a.selection.setRng(h.normalize(a.selection.getRng())),a.getDoc().execCommand("Delete",!1,null),p()),y=J.getNode();var N={context:y.nodeName.toLowerCase(),data:f.data};if(A=w.parse(b,N),f.paste===!0&&k.isListFragment(a.schema,A)&&k.isParentBlockLi(K,y))return D=k.insertAtCaret(x,K,a.selection.getRng(!0),A),a.selection.setRng(D),void a.fire("SetContent",B);if(r(A),E=A.lastChild,"mce_marker"==E.attr("id"))for(C=E,E=E.prev;E;E=E.walk(!0))if(3==E.type||!K.isBlock(E.name)){a.schema.isValidChild(E.parent.name,"span")&&E.parent.insert(C,E,"br"===E.name);break}if(a._selectionOverrides.showBlockCaretContainer(y),N.invalid){for(J.setContent(G),y=J.getNode(),z=a.getBody(),9==y.nodeType?y=E=z:E=y;E!==z;)y=E,E=E.parentNode;b=y==z?z.innerHTML:K.getOuterHTML(y),b=x.serialize(w.parse(b.replace(//i,function(){return x.serialize(A)}))),y==z?K.setHTML(z,b):K.setOuterHTML(y,b)}else b=x.serialize(A),n(a,b,y);q(),v(K.get("mce_marker")),s(a.getBody()),o(a.dom,a.selection.getStart()),a.fire("SetContent",B),a.addVisual()},q=function(a){var b;return"string"!=typeof a?(b=l.extend({paste:a.paste,data:{paste:a.paste}},a),{content:a.content,details:b}):{content:a,details:{}}},r=function(a,b){var c=q(b);p(a,c.content,c.details)};return{insertAtCaret:r}}),g("l",["2b","21","e","j","o","2c","1d"],function(a,b,c,d,e,f,g){var h=g.each,i=g.extend,j=g.map,k=g.inArray,l=g.explode,m=e.ie&&e.ie<11,n=!0,o=!1;return function(g){function p(a,b,c,d){var e,f,i=0;if(!g.removed){if(/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(a)||d&&d.skip_focus||g.focus(),d=g.fire("BeforeExecCommand",{command:a,ui:b,value:c}),d.isDefaultPrevented())return!1;if(f=a.toLowerCase(),e=H.exec[f])return e(f,b,c),g.fire("ExecCommand",{command:a,ui:b,value:c}),!0;if(h(g.plugins,function(d){if(d.execCommand&&d.execCommand(a,b,c))return g.fire("ExecCommand",{command:a,ui:b,value:c}),i=!0,!1}),i)return i;if(g.theme&&g.theme.execCommand&&g.theme.execCommand(a,b,c))return g.fire("ExecCommand",{command:a,ui:b,value:c}),!0;try{i=g.getDoc().execCommand(a,b,c)}catch(a){}return!!i&&(g.fire("ExecCommand",{command:a,ui:b,value:c}),!0)}}function q(a){var b;if(!g.quirks.isHidden()&&!g.removed){if(a=a.toLowerCase(),b=H.state[a])return b(a);try{return g.getDoc().queryCommandState(a)}catch(a){}return!1}}function r(a){var b;if(!g.quirks.isHidden()&&!g.removed){if(a=a.toLowerCase(),b=H.value[a])return b(a);try{return g.getDoc().queryCommandValue(a)}catch(a){}}}function s(a,b){b=b||"exec",h(a,function(a,c){h(c.toLowerCase().split(","),function(c){H[b][c]=a})})}function t(a,b,c){a=a.toLowerCase(),H.exec[a]=function(a,d,e,f){return b.call(c||g,d,e,f)}}function u(a){if(a=a.toLowerCase(),H.exec[a])return!0;try{return g.getDoc().queryCommandSupported(a)}catch(a){}return!1}function v(a,b,c){a=a.toLowerCase(),H.state[a]=function(){return b.call(c||g)}}function w(a,b,c){a=a.toLowerCase(),H.value[a]=function(){return b.call(c||g)}}function x(a){return a=a.toLowerCase(),!!H.exec[a]}function y(a,b,c){return void 0===b&&(b=o),void 0===c&&(c=null),g.getDoc().execCommand(a,b,c)}function z(a){return F.match(a)}function A(a,b){F.toggle(a,b?{value:b}:void 0),g.nodeChanged()}function B(a){G=E.getBookmark(a)}function C(){E.moveToBookmark(G)}var D,E,F,G,H={state:{},exec:{},value:{}},I=g.settings;g.on("PreInit",function(){D=g.dom,E=g.selection,I=g.settings,F=g.formatter}),i(this,{execCommand:p,queryCommandState:q,queryCommandValue:r,queryCommandSupported:u,addCommands:s,addCommand:t,addQueryStateHandler:v,addQueryValueHandler:w,hasCustomCommand:x}),s({"mceResetDesignMode,mceBeginUndoLevel":function(){},"mceEndUndoLevel,mceAddUndoLevel":function(){g.undoManager.add()},"Cut,Copy,Paste":function(a){var b,c=g.getDoc();try{y(a)}catch(a){b=n}if("paste"!==a||c.queryCommandEnabled(a)||(b=!0),b||!c.queryCommandSupported(a)){var d=g.translate("Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.");e.mac&&(d=d.replace(/Ctrl\+/g,"\u2318+")),g.notificationManager.open({text:d,type:"error"})}},unlink:function(){if(E.isCollapsed()){var a=g.dom.getParent(g.selection.getStart(),"a");return void(a&&g.dom.remove(a,!0))}F.remove("link")},"JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone":function(a){var b=a.substring(7);"full"==b&&(b="justify"),h("left,center,right,justify".split(","),function(a){b!=a&&F.remove("align"+a)}),"none"!=b&&A("align"+b)},"InsertUnorderedList,InsertOrderedList":function(a){var b,c;y(a),b=D.getParent(E.getNode(),"ol,ul"),b&&(c=b.parentNode,/^(H[1-6]|P|ADDRESS|PRE)$/.test(c.nodeName)&&(B(),D.split(c,b),C()))},"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(a){A(a)},"ForeColor,HiliteColor,FontName":function(a,b,c){A(a,c)},FontSize:function(a,b,c){var d,e;c>=1&&c<=7&&(e=l(I.font_size_style_values),d=l(I.font_size_classes),c=d?d[c-1]||c:e[c-1]||c),A(a,c)},RemoveFormat:function(a){F.remove(a)},mceBlockQuote:function(){A("blockquote")},FormatBlock:function(a,b,c){return A(c||"p")},mceCleanup:function(){var a=E.getBookmark();g.setContent(g.getContent({cleanup:n}),{cleanup:n}),E.moveToBookmark(a)},mceRemoveNode:function(a,b,c){var d=c||E.getNode();d!=g.getBody()&&(B(),g.dom.remove(d,n),C())},mceSelectNodeDepth:function(a,b,c){var d=0;D.getParent(E.getNode(),function(a){if(1==a.nodeType&&d++==c)return E.select(a),o},g.getBody())},mceSelectNode:function(a,b,c){E.select(c)},mceInsertContent:function(a,b,c){f.insertAtCaret(g,c)},mceInsertRawHTML:function(a,b,c){E.setContent("tiny_mce_marker"),g.setContent(g.getContent().replace(/tiny_mce_marker/g,function(){return c}))},mceToggleFormat:function(a,b,c){A(c)},mceSetContent:function(a,b,c){g.setContent(c)},"Indent,Outdent":function(a){var b,c,d;b=I.indentation,c=/[a-z%]+$/i.exec(b),b=parseInt(b,10),q("InsertUnorderedList")||q("InsertOrderedList")?y(a):(I.forced_root_block||D.getParent(E.getNode(),D.isBlock)||F.apply("div"),h(E.getSelectedBlocks(),function(e){if("false"!==D.getContentEditable(e)&&"LI"!==e.nodeName){var f=g.getParam("indent_use_margin",!1)?"margin":"padding";f="TABLE"===e.nodeName?"margin":f,f+="rtl"==D.getStyle(e,"direction",!0)?"Right":"Left","outdent"==a?(d=Math.max(0,parseInt(e.style[f]||0,10)-b),D.setStyle(e,f,d?d+c:"")):(d=parseInt(e.style[f]||0,10)+b+c,D.setStyle(e,f,d))}}))},mceRepaint:function(){},InsertHorizontalRule:function(){g.execCommand("mceInsertContent",!1,"
")},mceToggleVisualAid:function(){g.hasVisual=!g.hasVisual,g.addVisual()},mceReplaceContent:function(a,b,c){g.execCommand("mceInsertContent",!1,c.replace(/\{\$selection\}/g,E.getContent({format:"text"})))},mceInsertLink:function(a,b,c){var d;"string"==typeof c&&(c={href:c}),d=D.getParent(E.getNode(),"a"),c.href=c.href.replace(" ","%20"),d&&c.href||F.remove("link"),c.href&&F.apply("link",c,d)},selectAll:function(){var a,c=D.getRoot();if(E.getRng().setStart){var d=D.getParent(E.getStart(),b.isContentEditableTrue);d&&(a=D.createRng(),a.selectNodeContents(d),E.setRng(a))}else a=E.getRng(),a.item||(a.moveToElementText(c),a.select())},"delete":function(){a.deleteCommand(g)},forwardDelete:function(){a.forwardDeleteCommand(g)},mceNewDocument:function(){g.setContent("")},InsertLineBreak:function(a,b,e){function f(){for(var a,b=new d(p,r),c=g.schema.getNonEmptyElements();a=b.next();)if(c[a.nodeName.toLowerCase()]||a.length>0)return!0}var h,i,j,k=e,l=E.getRng(!0);new c(D).normalize(l);var o=l.startOffset,p=l.startContainer;if(1==p.nodeType&&p.hasChildNodes()){var q=o>p.childNodes.length-1;p=p.childNodes[Math.min(o,p.childNodes.length-1)]||p,o=q&&3==p.nodeType?p.nodeValue.length:0}var r=D.getParent(p,D.isBlock),s=r?r.nodeName.toUpperCase():"",t=r?D.getParent(r.parentNode,D.isBlock):null,u=t?t.nodeName.toUpperCase():"",v=k&&k.ctrlKey;"LI"!=u||v||(r=t,s=u),p&&3==p.nodeType&&o>=p.nodeValue.length&&(m||f()||(h=D.create("br"),l.insertNode(h),l.setStartAfter(h),l.setEndAfter(h),i=!0)),h=D.create("br"),l.insertNode(h);var w=D.doc.documentMode;return m&&"PRE"==s&&(!w||w<8)&&h.parentNode.insertBefore(D.doc.createTextNode("\r"),h),j=D.create("span",{}," "),h.parentNode.insertBefore(j,h),E.scrollIntoView(j),D.remove(j),i?(l.setStartBefore(h),l.setEndBefore(h)):(l.setStartAfter(h),l.setEndAfter(h)),E.setRng(l),g.undoManager.add(),n}}),s({"JustifyLeft,JustifyCenter,JustifyRight,JustifyFull":function(a){var b="align"+a.substring(7),c=E.isCollapsed()?[D.getParent(E.getNode(),D.isBlock)]:E.getSelectedBlocks(),d=j(c,function(a){return!!F.matchNode(a,b)});return k(d,n)!==-1},"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(a){return z(a)},mceBlockQuote:function(){return z("blockquote")},Outdent:function(){var a;if(I.inline_styles){if((a=D.getParent(E.getStart(),D.isBlock))&&parseInt(a.style.paddingLeft,10)>0)return n;if((a=D.getParent(E.getEnd(),D.isBlock))&&parseInt(a.style.paddingLeft,10)>0)return n}return q("InsertUnorderedList")||q("InsertOrderedList")||!I.inline_styles&&!!D.getParent(E.getNode(),"BLOCKQUOTE")},"InsertUnorderedList,InsertOrderedList":function(a){var b=D.getParent(E.getNode(),"ul,ol");return b&&("insertunorderedlist"===a&&"UL"===b.tagName||"insertorderedlist"===a&&"OL"===b.tagName)}},"state"),s({"FontSize,FontName":function(a){var b,c=0;return(b=D.getParent(E.getNode(),"span"))&&(c="fontsize"==a?b.style.fontSize:b.style.fontFamily.replace(/, /g,",").replace(/[\'\"]/g,"").toLowerCase()),c}},"value"),s({Undo:function(){g.undoManager.undo()},Redo:function(){g.undoManager.redo()}})}}),g("2d",["1u","26","23","5n","58","e","o"],function(a,b,c,d,e,f,g){var h=function(a,b){return a.dom.getParent(b,function(b){return"true"===a.dom.getContentEditable(b)})},i=function(b){return b.collapsed?a.from(f.getNode(b.startContainer,b.startOffset)).map(c.fromDom):a.none()},j=function(c,d){return i(d).bind(function(d){return e.isTableSection(d)?a.some(d):b.contains(c,d)===!1?a.some(c):a.none()})},k=function(a,b){j(c.fromDom(a.getBody()),b).bind(function(a){return d.firstPositionIn(a.dom())}).fold(function(){a.selection.normalize()},function(b){a.selection.setRng(b.toRange())})},l=function(a){if(a.setActive)try{a.setActive()}catch(b){a.focus()}else a.focus()},m=function(a){var b,c,d,e=a.selection,f=a.settings.content_editable,i=a.getDoc(),j=a.getBody();return b=e.getRng(),b.item&&(c=b.item(0)),a.quirks.refreshContentEditable(),d=h(a,e.getNode()),a.$.contains(j,d)?(l(d),k(a,b),void n(a)):(f||(g.opera||l(j),a.getWin().focus()),(g.gecko||f)&&(f&&document.activeElement!==j&&a.selection.setRng(a.lastRng),l(j),k(a,b)),c&&c.ownerDocument===i&&(b=i.body.createControlRange(),b.addElement(c),b.select()),void n(a))},n=function(a){a.editorManager.setActive(a)},o=function(a,b){a.removed||(b?n(a):m(a))};return{focus:o}}),g("n",["1b","c","1d"],function(a,b,c){function d(a,b){return"selectionchange"==b?a.getDoc():!a.inline&&/^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(b)?a.getDoc().documentElement:a.settings.event_root?(a.eventRoot||(a.eventRoot=g.select(a.settings.event_root)[0]),a.eventRoot):a.getBody()}function e(a,b){function c(a){return!a.hidden&&!a.readonly}var e,h;if(a.delegates||(a.delegates={}),!a.delegates[b]&&!a.removed)if(e=d(a,b),a.settings.event_root){if(f||(f={},a.editorManager.on("removeEditor",function(){var b;if(!a.editorManager.activeEditor&&f){for(b in f)a.dom.unbind(d(a,b));f=null}})),f[b])return;h=function(d){for(var e=d.target,f=a.editorManager.get(),h=f.length;h--;){var i=f[h].getBody();(i===e||g.isChildOf(e,i))&&c(f[h])&&f[h].fire(b,d)}},f[b]=h,g.bind(e,b,h)}else h=function(d){c(a)&&a.fire(b,d)},g.bind(e,b,h),a.delegates[b]=h}var f,g=b.DOM,h={bindPendingEventDelegates:function(){var a=this;c.each(a._pendingNativeEvents,function(b){e(a,b)})},toggleNativeEvent:function(a,b){var c=this;"focus"!=a&&"blur"!=a&&(b?c.initialized?e(c,a):c._pendingNativeEvents?c._pendingNativeEvents.push(a):c._pendingNativeEvents=[a]:c.initialized&&(c.dom.unbind(d(c,a),a,c.delegates[a]),delete c.delegates[a]))},unbindAllNativeEvents:function(){var a,b=this;if(b.delegates){for(a in b.delegates)b.dom.unbind(d(b,a),a,b.delegates[a]);delete b.delegates}b.inline||(b.getBody().onload=null,b.dom.unbind(b.getWin()),b.dom.unbind(b.getDoc())),b.dom.unbind(b.getBody()),b.dom.unbind(b.getContainer())}};return h=c.extend({},a,h)}),g("2l",["5"],function(a){var b=a.PluginManager,c=function(a,c){for(var d in b.urls){var e=b.urls[d]+"/plugin"+c+".js";if(e===a)return d}return null},d=function(a,b){var d=c(b,a.suffix);return d?"Failed to load plugin: "+d+" from url "+b:"Failed to load plugin url: "+b},e=function(a,b){a.notificationManager.open({type:"error",text:b})},f=function(a,b){a._skinLoaded?e(a,b):a.on("SkinLoaded",function(){e(a,b)})},g=function(a,b){f(a,"Failed to upload image: "+b)},h=function(a,b){f(a,d(a,b))},i=function(a){var b=window.console;b&&!window.test&&(b.error?b.error.apply(b,arguments):b.log.apply(b,arguments))};return{pluginLoadError:h,uploadError:g,displayError:f,initError:i}}),g("7e",["1","23","57","1z"],function(a,b,c,d){var e=function(d){return c.descendant(b.fromDom(d.getBody()),"*[data-mce-caret]").fold(a.constant(null),function(a){return a.dom()})},f=function(a){a.selection.setRng(a.selection.getRng())},g=function(a,b){b.hasAttribute("data-mce-caret")&&(d.showCaretContainerBlock(b),f(a),a.selection.scrollIntoView(b))},h=function(a,b){var c=e(a);if(c)return"compositionstart"===b.type?(b.preventDefault(),b.stopPropagation(),void g(c)):void(d.hasContent(c)&&g(a,c))},i=function(b){b.on("keyup compositionstart",a.curry(h,b))};return{setup:i}}),g("7o",["1c","1d","4i"],function(a,b,c){return function(c,d){function e(a,b){return a?a.replace(/\/$/,"")+"/"+b.replace(/^\//,""):b}function f(a,b,c,f){var g,h;g=new XMLHttpRequest,g.open("POST",d.url),g.withCredentials=d.credentials,g.upload.onprogress=function(a){f(a.loaded/a.total*100)},g.onerror=function(){c("Image upload failed due to a XHR Transport error. Code: "+g.status)},g.onload=function(){var a;return g.status<200||g.status>=300?void c("HTTP Error: "+g.status):(a=JSON.parse(g.responseText),a&&"string"==typeof a.location?void b(e(d.basePath,a.location)):void c("Invalid JSON: "+g.responseText))},h=new FormData,h.append("file",a.blob(),a.filename()),g.send(h)}function g(){return new a(function(a){a([])})}function h(a,b){return{url:b,blobInfo:a,status:!0}}function i(a,b){return{url:"",blobInfo:a,status:!1,error:b}}function j(a,c){b.each(p[a],function(a){a(c)}),delete p[a]}function k(b,d,e){return c.markPending(b.blobUri()),new a(function(a){var f,g,k=function(){};try{var l=function(){f&&(f.close(),g=k)},m=function(d){l(),c.markUploaded(b.blobUri(),d),j(b.blobUri(),h(b,d)),a(h(b,d))},n=function(d){l(),c.removeFailed(b.blobUri()),j(b.blobUri(),i(b,d)),a(i(b,d))};g=function(a){a<0||a>100||(f||(f=e()),f.progressBar.value(a))},d(b,m,n,g)}catch(c){a(i(b,c.message))}})}function l(a){return a===f}function m(b){var c=b.blobUri();return new a(function(a){p[c]=p[c]||[],p[c].push(a)})}function n(e,f){return e=b.grep(e,function(a){return!c.isUploaded(a.blobUri())}),a.all(b.map(e,function(a){return c.isPending(a.blobUri())?m(a):k(a,d.handler,f)}))}function o(a,b){return!d.url&&l(d.handler)?g():n(a,b)}var p={};
+return d=b.extend({credentials:!1,handler:f},d),{upload:o}}}),g("8a",["66"],function(a){var b=function(b){var c=a.getOrDie("requestAnimationFrame");c(b)},c=function(b){var c=a.getOrDie("atob");return c(b)};return{atob:c,requestAnimationFrame:b}}),g("82",["8a","1c"],function(a,b){function c(a){return new b(function(b,c){var d=function(){c("Cannot convert "+a+" to Blob. Resource might not exist or is inaccessible.")};try{var e=new XMLHttpRequest;e.open("GET",a,!0),e.responseType="blob",e.onload=function(){200==this.status?b(this.response):d()},e.onerror=d,e.send()}catch(a){d()}})}function d(a){var b,c;return a=decodeURIComponent(a).split(","),c=/data:([^;]+)/.exec(a[0]),c&&(b=c[1]),{type:b,data:a[1]}}function e(c){return new b(function(b){var e,f,g;c=d(c);try{e=a.atob(c.data)}catch(a){return void b(new Blob([]))}for(f=new Uint8Array(e.length),g=0;g
'),b},x=function(a,c,e){var f,g,h,i=new d(a.getBody()),j=l.curry(q,i.next),k=l.curry(q,i.prev);if(e.collapsed&&a.settings.forced_root_block){if(f=a.dom.getParent(e.startContainer,"PRE"),!f)return;g=1===c?j(b.fromRangeStart(e)):k(b.fromRangeStart(e)),g||(h=w(a),1===c?a.$(f).after(h):a.$(f).before(h),a.selection.select(h,!0),a.selection.collapse())}},y=function(a,b){var c,e=new d(a.getBody()),f=l.curry(q,e.next),g=l.curry(q,e.prev),h=b?1:-1,i=b?f:g,j=b?p:o,k=a.selection.getRng();return(c=u(h,a,i,j,k))?c:(c=x(a,h,k),c?c:null)},z=function(a,b){var c,d=b?1:-1,e=b?f.downUntil:f.upUntil,g=a.selection.getRng();return(c=v(d,a,e,g))?c:(c=x(a,d,g),c?c:null)},A=function(a,b){return function(){var c=y(a,b);return!!c&&(a.selection.setRng(c),!0)}},B=function(a,b){return function(){var c=z(a,b);return!!c&&(a.selection.setRng(c),!0)}};return{moveH:A,moveV:B}}),g("8c",["2k","3","4"],function(a,b,c){var d=function(a,b){return b},e=function(b,c){var d=a.isObject(b)&&a.isObject(c);return d?g(b,c):c},f=function(a){return function(){for(var d=new b(arguments.length),e=0;e
|)<\/p>[\r\n]*|
[\r\n]*)$/,"")}),a.load({initial:!0,format:"html"}),a.startContent=a.getContent({format:"raw"}),a.on("compositionstart compositionend",function(b){a.composing="compositionstart"===b.type}),a.contentStyles.length>0&&(l="",w.each(a.contentStyles,function(a){l+=a+"\r\n"}),a.dom.addStyle(l)),C(a).loadAll(a.contentCSS,function(b){B(a)},function(b){B(a)}),n.content_style&&y(a,n.content_style)};return{initContentBody:D}}),g("5t",["5"],function(a){return a.PluginManager}),g("5u",["5"],function(a){return a.ThemeManager}),g("5s",["2i","5r","c","o","6w","5t","5u","1d","2j"],function(a,b,c,d,e,f,g,h,i){var j=c.DOM,k=function(a,b,c){var d,e,g=f.get(c);if(d=f.urls[c]||a.documentBaseUrl.replace(/\/$/,""),c=h.trim(c),g&&h.inArray(b,c)===-1){if(h.each(f.dependencies(c),function(c){k(a,b,c)}),a.plugins[c])return;e=new g(a,d,a.$),a.plugins[c]=e,e.init&&(e.init(a,d),b.push(c))}},l=function(a){return a.replace(/^\-/,"")},m=function(a){var b=[];h.each(a.settings.plugins.split(/[ ,]/),function(c){k(a,b,l(c))})},n=function(a){var b,c=a.settings;c.theme&&("function"!=typeof c.theme?(c.theme=l(c.theme),b=g.get(c.theme),a.theme=new b(a,g.urls[c.theme]),a.theme.init&&a.theme.init(a,g.urls[c.theme]||a.documentBaseUrl.replace(/\/$/,""),a.$)):a.theme=c.theme)},o=function(a){var b,c,d,e,f,g=a.settings,h=a.getElement();return g.render_ui&&a.theme&&(a.orgDisplay=h.style.display,"function"!=typeof g.theme?(b=g.width||j.getStyle(h,"width")||"100%",c=g.height||j.getStyle(h,"height")||h.offsetHeight,d=g.min_height||100,e=/^[0-9\.]+(|px)$/i,e.test(""+b)&&(b=Math.max(parseInt(b,10),100)),e.test(""+c)&&(c=Math.max(parseInt(c,10),d)),f=a.theme.renderUI({targetNode:h,width:b,height:c,deltaWidth:g.delta_width,deltaHeight:g.delta_height}),g.content_editable||(c=(f.iframeHeight||c)+("number"==typeof c?f.deltaHeight||0:""),c
';var g=j.create("iframe",{id:a.id+"_ifr",frameBorder:"0",allowTransparency:"true",title:a.editorManager.translate("Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help"),style:{width:"100%",height:b.height,display:"block"}});g.onload=function(){g.onload=null,a.fire("load")};var h=p(a,g);return a.contentAreaContainer=b.iframeContainer,a.iframeElement=g,j.add(b.iframeContainer,g),h},r=function(a){var b,c=a.settings,d=a.getElement();if(a.rtl=c.rtl_ui||a.editorManager.i18n.rtl,a.editorManager.i18n.setCode(c.language),c.aria_label=c.aria_label||j.getAttrib(d,"aria-label",a.getLang("aria.rich_text_area")),a.fire("ScriptsLoaded"),n(a),m(a),b=o(a),c.content_css&&h.each(h.explode(c.content_css),function(b){a.contentCSS.push(a.documentBaseURI.toAbsolute(b))}),c.content_editable)return e.initContentBody(a);var f=q(a,b);b.editorContainer&&(j.get(b.editorContainer).style.display=a.orgDisplay,a.hidden=j.isHidden(b.editorContainer)),a.getElement().style.display="none",j.setAttrib(a.id,"aria-hidden",!0),f||e.initContentBody(a)};return{init:r}}),g("2f",["5r","7","8","c","d","f","o","2l","5s","5t","5u","1d"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=d.DOM,n=function(a,b){var c=a.settings,d=f.ScriptLoader;if(c.language&&"en"!=c.language&&!c.language_url&&(c.language_url=a.editorManager.baseURL+"/langs/"+c.language+".js"),c.language_url&&d.add(c.language_url),c.theme&&"function"!=typeof c.theme&&"-"!=c.theme.charAt(0)&&!k.urls[c.theme]){var e=c.theme_url;e=e?a.documentBaseURI.toAbsolute(e):"themes/"+c.theme+"/theme"+b+".js",k.load(c.theme,e)}l.isArray(c.plugins)&&(c.plugins=c.plugins.join(" ")),l.each(c.external_plugins,function(a,b){j.load(b,a),c.plugins+=" "+b}),l.each(c.plugins.split(/[ ,]/),function(a){if(a=l.trim(a),a&&!j.urls[a])if("-"===a.charAt(0)){a=a.substr(1,a.length);var c=j.dependencies(a);l.each(c,function(a){var c={prefix:"plugins/",resource:a,suffix:"/plugin"+b+".js"};a=j.createUrl(c,a),j.load(a.resource,a)})}else j.load(a,{prefix:"plugins/",resource:a,suffix:"/plugin"+b+".js"})}),d.loadQueue(function(){a.removed||i.init(a)},a,function(b){h.pluginLoadError(a,b[0]),a.removed||i.init(a)})},o=function(d){function f(){m.unbind(a,"ready",f),d.render()}var h=d.settings,i=d.id;if(!e.Event.domLoaded)return void m.bind(a,"ready",f);if(d.getElement()&&g.contentEditable){h.inline?d.inline=!0:(d.orgVisibility=d.getElement().style.visibility,d.getElement().style.visibility="hidden");var j=d.getElement().form||m.getParent(i,"form");j&&(d.formElement=j,h.hidden_input&&!/TEXTAREA|INPUT/i.test(d.getElement().nodeName)&&(m.insertAfter(m.create("input",{type:"hidden",name:i}),i),d.hasHiddenInput=!0),d.formEventDelegate=function(a){d.fire(a.type,a)},m.bind(j,"submit reset",d.formEventDelegate),d.on("reset",function(){d.setContent(d.startContent,{format:"raw"})}),!h.submit_patch||j.submit.nodeType||j.submit.length||j._mceOldSubmit||(j._mceOldSubmit=j.submit,j.submit=function(){return d.editorManager.triggerSave(),d.setDirty(!1),j._mceOldSubmit(j)})),d.windowManager=new c(d),d.notificationManager=new b(d),"xml"===h.encoding&&d.on("GetContent",function(a){a.save&&(a.content=m.encode(a.content))}),h.add_form_submit_trigger&&d.on("submit",function(){d.initialized&&d.save()}),h.add_unload_trigger&&(d._beforeUnload=function(){!d.initialized||d.destroyed||d.isHidden()||d.save({format:"raw",no_events:!0,set_dirty:!1})},d.editorManager.on("BeforeUnload",d._beforeUnload)),d.editorManager.add(d),n(d,d.suffix)}};return{render:o}}),g("2g",[],function(){function a(a,b,c){try{a.getDoc().execCommand(b,!1,c)}catch(a){}}function b(a){var b,c;return b=a.getBody(),c=function(b){a.dom.getParents(b.target,"a").length>0&&b.preventDefault()},a.dom.bind(b,"click",c),{unbind:function(){a.dom.unbind(b,"click",c)}}}function c(c,d){c._clickBlocker&&(c._clickBlocker.unbind(),c._clickBlocker=null),d?(c._clickBlocker=b(c),c.selection.controlSelection.hideResizeRect(),c.readonly=!0,c.getBody().contentEditable=!1):(c.readonly=!1,c.getBody().contentEditable=!0,a(c,"StyleWithCSS",!1),a(c,"enableInlineTableEditing",!1),a(c,"enableObjectResizing",!1),c.focus(),c.nodeChanged())}function d(a,b){var d=a.readonly?"readonly":"design";b!=d&&(a.initialized?c(a,"readonly"==b):a.on("init",function(){c(a,"readonly"==b)}),a.fire("SwitchMode",{mode:b}))}return{setMode:d}}),g("z",["1d","o"],function(a,b){var c=a.each,d=a.explode,e={f9:120,f10:121,f11:122},f=a.makeMap("alt,ctrl,shift,meta,access");return function(g){function h(a){var g,h,i={};c(d(a,"+"),function(a){a in f?i[a]=!0:/^[0-9]{2,}$/.test(a)?i.keyCode=parseInt(a,10):(i.charCode=a.charCodeAt(0),i.keyCode=e[a]||a.toUpperCase().charCodeAt(0));
+}),g=[i.keyCode];for(h in f)i[h]?g.push(h):i[h]=!1;return i.id=g.join(","),i.access&&(i.alt=!0,b.mac?i.ctrl=!0:i.shift=!0),i.meta&&(b.mac?i.meta=!0:(i.ctrl=!0,i.meta=!1)),i}function i(b,c,e,f){var i;return i=a.map(d(b,">"),h),i[i.length-1]=a.extend(i[i.length-1],{func:e,scope:f||g}),a.extend(i[0],{desc:g.translate(c),subpatterns:i.slice(1)})}function j(a){return a.altKey||a.ctrlKey||a.metaKey}function k(a){return"keydown"===a.type&&a.keyCode>=112&&a.keyCode<=123}function l(a,b){return!!b&&(b.ctrl==a.ctrlKey&&b.meta==a.metaKey&&(b.alt==a.altKey&&b.shift==a.shiftKey&&(!!(a.keyCode==b.keyCode||a.charCode&&a.charCode==b.charCode)&&(a.preventDefault(),!0))))}function m(a){return a.func?a.func.call(a.scope):null}var n=this,o={},p=[];g.on("keyup keypress keydown",function(a){!j(a)&&!k(a)||a.isDefaultPrevented()||(c(o,function(b){if(l(a,b))return p=b.subpatterns.slice(0),"keydown"==a.type&&m(b),!0}),l(a,p[0])&&(1===p.length&&"keydown"==a.type&&m(p[0]),p.shift()))}),n.add=function(b,e,f,h){var j;return j=f,"string"==typeof f?f=function(){g.execCommand(j,!1,null)}:a.isArray(j)&&(f=function(){g.execCommand(j[0],j[1],j[2])}),c(d(a.trim(b.toLowerCase())),function(a){var b=i(a,e,f,h);o[b.id]=b}),!0},n.remove=function(a){var b=i(a);return!!o[b.id]&&(delete o[b.id],!0)}}}),g("2h",[],function(){var a=function(a,b,c){var d=a.sidebars?a.sidebars:[];d.push({name:b,settings:c}),a.sidebars=d};return{add:a}}),g("1e",["2i","1d"],function(a,b){function c(b,g){var h,i,j=this;if(b=e(b),g=j.settings=g||{},h=g.base_uri,/^([\w\-]+):([^\/]{2})/i.test(b)||/^\s*#/.test(b))return void(j.source=b);var k=0===b.indexOf("//");0!==b.indexOf("/")||k||(b=(h?h.protocol||"http":"http")+"://mce_host"+b),/^[\w\-]*:?\/\//.test(b)||(i=g.base_uri?g.base_uri.path:new c(a.location.href).directory,""===g.base_uri.protocol?b="//mce_host"+j.toAbsPath(i,b):(b=/([^#?]*)([#?]?.*)/.exec(b),b=(h&&h.protocol||"http")+"://mce_host"+j.toAbsPath(i,b[1])+b[2])),b=b.replace(/@@/g,"(mce_at)"),b=/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(b),d(f,function(a,c){var d=b[c];d&&(d=d.replace(/\(mce_at\)/g,"@@")),j[a]=d}),h&&(j.protocol||(j.protocol=h.protocol),j.userInfo||(j.userInfo=h.userInfo),j.port||"mce_host"!==j.host||(j.port=h.port),j.host&&"mce_host"!==j.host||(j.host=h.host),j.source=""),k&&(j.protocol="")}var d=b.each,e=b.trim,f="source protocol authority userInfo user password host port relative path directory file query anchor".split(" "),g={ftp:21,http:80,https:443,mailto:25};return c.prototype={setPath:function(a){var b=this;a=/^(.*?)\/?(\w+)?$/.exec(a),b.path=a[0],b.directory=a[1],b.file=a[2],b.source="",b.getURI()},toRelative:function(a){var b,d=this;if("./"===a)return a;if(a=new c(a,{base_uri:d}),"mce_host"!=a.host&&d.host!=a.host&&a.host||d.port!=a.port||d.protocol!=a.protocol&&""!==a.protocol)return a.getURI();var e=d.getURI(),f=a.getURI();return e==f||"/"==e.charAt(e.length-1)&&e.substr(0,e.length-1)==f?e:(b=d.toRelPath(d.path,a.path),a.query&&(b+="?"+a.query),a.anchor&&(b+="#"+a.anchor),b)},toAbsolute:function(a,b){return a=new c(a,{base_uri:this}),a.getURI(b&&this.isSameOrigin(a))},isSameOrigin:function(a){if(this.host==a.host&&this.protocol==a.protocol){if(this.port==a.port)return!0;var b=g[this.protocol];if(b&&(this.port||b)==(a.port||b))return!0}return!1},toRelPath:function(a,b){var c,d,e,f=0,g="";if(a=a.substring(0,a.lastIndexOf("/")),a=a.split("/"),c=b.split("/"),a.length>=c.length)for(d=0,e=a.length;d
',"TABLE"==f.nodeName?a=" ":/^(UL|OL)$/.test(f.nodeName)&&(a=""+d+"
'),e.dom.setHTML(f,a),e.fire("SetContent",b)):("raw"!==b.format&&(a=new i({validate:e.validate},e.schema).serialize(e.parser.parse(a,{isRootContent:!0}))),b.content=u(a),e.dom.setHTML(f,b.content),b.no_events||e.fire("SetContent",b)),b.content},getContent:function(a){var b,c=this,d=c.getBody();return c.removed?"":(a=a||{},a.format=a.format||"html",a.get=!0,a.getInner=!0,a.no_events||c.fire("BeforeGetContent",a),b="raw"==a.format?n.trim(c.serializer.getTrimmedContent()):"text"==a.format?d.innerText||d.textContent:c.serializer.serialize(d,a),"text"!=a.format?a.content=u(b):a.content=b,a.no_events||c.fire("GetContent",a),a.content)},insertContent:function(a,b){b&&(a=s({content:a},b)),this.execCommand("mceInsertContent",!1,a)},isDirty:function(){return!this.isNotDirty},setDirty:function(a){var b=!this.isNotDirty;this.isNotDirty=!a,a&&a!=b&&this.fire("dirty")},setMode:function(a){k.setMode(this,a)},getContainer:function(){var a=this;return a.container||(a.container=r.get(a.editorContainer||a.id+"_parent")),a.container},getContentAreaContainer:function(){return this.contentAreaContainer},getElement:function(){return this.targetElm||(this.targetElm=r.get(this.id)),this.targetElm},getWin:function(){var a,b=this;return b.contentWindow||(a=b.iframeElement,a&&(b.contentWindow=a.contentWindow)),b.contentWindow},getDoc:function(){var a,b=this;return b.contentDocument||(a=b.getWin(),a&&(b.contentDocument=a.document)),b.contentDocument},getBody:function(){var a=this.getDoc();return this.bodyElement||(a?a.body:null)},convertURL:function(a,b,c){var d=this,e=d.settings;return e.urlconverter_callback?d.execCallback("urlconverter_callback",a,c,!0,b):!e.convert_urls||c&&"LINK"==c.nodeName||0===a.indexOf("file:")||0===a.length?a:e.relative_urls?d.documentBaseURI.toRelative(a):a=d.documentBaseURI.toAbsolute(a,e.remove_script_host)},addVisual:function(a){var b,c=this,d=c.settings,e=c.dom;a=a||c.getBody(),void 0===c.hasVisual&&(c.hasVisual=d.visual),t(e.select("table,a",a),function(a){var f;switch(a.nodeName){case"TABLE":return b=d.visual_table_class||"mce-item-table",f=e.getAttrib(a,"border"),void(f&&"0"!=f||!c.hasVisual?e.removeClass(a,b):e.addClass(a,b));case"A":return void(e.getAttrib(a,"href",!1)||(f=e.getAttrib(a,"name")||a.id,b=d.visual_anchor_class||"mce-item-anchor",f&&c.hasVisual?e.addClass(a,b):e.removeClass(a,b)))}}),c.fire("VisualAid",{element:a,hasVisual:c.hasVisual})},remove:function(){var a=this;a.removed||(a.save(),a.removed=1,a.unbindAllNativeEvents(),a.hasHiddenInput&&r.remove(a.getElement().nextSibling),a.inline||(w&&w<10&&a.getDoc().execCommand("SelectAll",!1,null),r.setStyle(a.id,"display",a.orgDisplay),a.getBody().onload=null),a.fire("remove"),a.editorManager.remove(a),r.remove(a.getContainer()),a._selectionOverrides.destroy(),a.editorUpload.destroy(),a.destroy())},destroy:function(a){var b,c=this;if(!c.destroyed){if(!a&&!c.removed)return void c.remove();a||(c.editorManager.off("beforeunload",c._beforeUnload),c.theme&&c.theme.destroy&&c.theme.destroy(),c.selection.destroy(),c.dom.destroy()),b=c.formElement,b&&(b._mceOldSubmit&&(b.submit=b._mceOldSubmit,b._mceOldSubmit=null),r.unbind(b,"submit reset",c.formEventDelegate)),c.contentAreaContainer=c.formElement=c.container=c.editorContainer=null,c.bodyElement=c.contentDocument=c.contentWindow=null,c.iframeElement=c.targetElm=null,c.selection&&(c.selection=c.selection.win=c.selection.dom=c.selection.dom.doc=null),c.destroyed=1}},uploadImages:function(a){return this.editorUpload.uploadImages(a)},_scanForImages:function(){return this.editorUpload.scanForImages()}},s(q.prototype,f),q}),g("p",["c","14","o"],function(a,b,c){function d(a){function d(){try{return document.activeElement}catch(a){return document.body}}function j(a,b){if(b&&b.startContainer){if(!a.isChildOf(b.startContainer,a.getRoot())||!a.isChildOf(b.endContainer,a.getRoot()))return;return{startContainer:b.startContainer,startOffset:b.startOffset,endContainer:b.endContainer,endOffset:b.endOffset}}return b}function l(a,b){var c;return b.startContainer?(c=a.getDoc().createRange(),c.setStart(b.startContainer,b.startOffset),c.setEnd(b.endContainer,b.endOffset)):c=b,c}function m(e){var m=e.editor;m.on("init",function(){(m.inline||c.ie)&&("onbeforedeactivate"in document&&c.ie<9?m.dom.bind(m.getBody(),"beforedeactivate",function(a){if(a.target==m.getBody())try{m.lastRng=m.selection.getRng()}catch(a){}}):m.on("nodechange mouseup keyup",function(a){var b=d();"nodechange"==a.type&&a.selectionChange||(b&&b.id==m.id+"_ifr"&&(b=m.getBody()),m.dom.isChildOf(b,m.getBody())&&(m.lastRng=m.selection.getRng()))}))}),m.on("setcontent",function(){m.lastRng=null}),m.on("mousedown",function(){m.selection.lastFocusBookmark=null}),m.on("focusin",function(){var b,c=a.focusedEditor;m.selection.lastFocusBookmark&&(b=l(m,m.selection.lastFocusBookmark),m.selection.lastFocusBookmark=null,m.selection.setRng(b)),c!=m&&(c&&c.fire("blur",{focusedEditor:m}),a.setActive(m),a.focusedEditor=m,m.fire("focus",{blurredEditor:c}),m.focus(!0)),m.lastRng=null}),m.on("focusout",function(){b.setEditorTimeout(m,function(){var b=a.focusedEditor;i(m,d())||b!=m||(m.fire("blur",{focusedEditor:null}),a.focusedEditor=null,m.selection&&(m.selection.lastFocusBookmark=null))})}),f||(f=function(b){var c,d=a.activeEditor;c=b.target,d&&c.ownerDocument===document&&(d.selection&&c!==d.getBody()&&k(m,c)&&(d.selection.lastFocusBookmark=j(d.dom,d.lastRng)),c===document.body||i(d,c)||a.focusedEditor!==d||(d.fire("blur",{focusedEditor:null}),a.focusedEditor=null))},h.bind(document,"focusin",f)),m.inline&&!g&&(g=function(b){var c=a.activeEditor,d=c.dom;if(c.inline&&d&&!d.isChildOf(b.target,c.getBody())){var e=c.selection.getRng();e.collapsed||(c.lastRng=e)}},h.bind(document,"mouseup",g))}function n(b){a.focusedEditor==b.editor&&(a.focusedEditor=null),a.activeEditor||(h.unbind(document,"selectionchange",e),h.unbind(document,"focusin",f),h.unbind(document,"mouseup",g),e=f=g=null)}a.on("AddEditor",m),a.on("RemoveEditor",n)}var e,f,g,h=a.DOM,i=function(a,b){var c=a?a.settings.custom_ui_selector:"",e=h.getParent(b,function(b){return d.isEditorUIElement(b)||!!c&&a.dom.is(b,c)});return null!==e},j=function(a){return a.inline===!0},k=function(a,b){return j(a)===!1||a.dom.isChildOf(b,a.getBody())===!1};return d.isEditorUIElement=function(a){return a.className.toString().indexOf("mce-")!==-1},d._isUIElement=i,d}),g("2m",["1d"],function(a){var b=a.each,c=a.explode,d=function(a){a.on("AddEditor",function(a){var d=a.editor;d.on("preInit",function(){function a(a,c){b(c,function(b,c){b&&h.setStyle(a,c,b)}),h.rename(a,"span")}function e(a){h=d.dom,i.convert_fonts_to_spans&&b(h.select("font,u,strike",a.node),function(a){f[a.nodeName.toLowerCase()](h,a)})}var f,g,h,i=d.settings;i.inline_styles&&(g=c(i.font_size_legacy_values),f={font:function(b,c){a(c,{backgroundColor:c.style.backgroundColor,color:c.color,fontFamily:c.face,fontSize:g[parseInt(c.size,10)-1]})},u:function(b,c){"html4"===d.settings.schema&&a(c,{textDecoration:"underline"})},strike:function(b,c){a(c,{textDecoration:"line-through"})}},d.on("PreProcess SetContent",e))})})};return{register:d}}),g("16",["1d"],function(a){"use strict";var b={},c="en";return{setCode:function(a){a&&(c=a,this.rtl=!!this.data[a]&&"rtl"===this.data[a]._dir)},getCode:function(){return c},rtl:!1,add:function(a,c){var d=b[a];d||(b[a]=d={});for(var e in c)d[e]=c[e];this.setCode(a)},translate:function(d){function e(b){return a.is(b,"function")?Object.prototype.toString.call(b):f(b)?"":""+b}function f(b){return""===b||null===b||a.is(b,"undefined")}function g(b){return b=e(b),a.hasOwn(h,b)?e(h[b]):b}var h=b[c]||{};if(f(d))return"";if(a.is(d,"object")&&a.hasOwn(d,"raw"))return e(d.raw);if(a.is(d,"array")){var i=d.slice(1);d=g(d[0]).replace(/\{([0-9]+)\}/g,function(b,c){return a.hasOwn(i,c)?e(i[c]):b})}return g(d).replace(/{context:\w+}$/,"")},data:b}}),g("m",["1t","2k","5","b","c","k","o","2l","p","2m","16","1b","1c","1d","1e"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){function p(a){x(u.get(),function(b){"scroll"===a.type?b.fire("ScrollWindow",a):b.fire("ResizeWindow",a)})}function q(a){a!==A&&(a?d(window).on("resize scroll",p):d(window).off("resize scroll",p),A=a)}function r(b){var c=C;delete B[b.id];for(var d=0;d