diff --git a/grunt-settings.yaml b/grunt-settings.yaml index 02fafa1a11512..621f4f2028280 100644 --- a/grunt-settings.yaml +++ b/grunt-settings.yaml @@ -40,7 +40,7 @@ vendors: 'version': '1.4.1' 'dependencies': '' 'tinymce': - 'version': '4.6.7' + 'version': '4.7.0' 'dependencies': '' 'awesomplete': 'version': '1.1.2' diff --git a/media/vendor/tinymce/changelog.txt b/media/vendor/tinymce/changelog.txt index a3b46c5a7c517..8954514b8dacb 100644 --- a/media/vendor/tinymce/changelog.txt +++ b/media/vendor/tinymce/changelog.txt @@ -1,3 +1,14 @@ +Version 4.7.0 (2017-10-03) + Added new mobile ui that is specifically designed for mobile devices. + Updated the default skin to be more modern and white since white is preferred by most implementations. + Restructured the default menus to be more similar to common office suites like Google Docs. + Fixed so theme can be set to false on both inline and iframe editor modes. + Fixed bug where inline editor would add/remove the visualblocks css multiple times. + Fixed bug where selection wouldn't be properly restored when editor lost focus and commands where invoked. + Fixed bug where toc plugin would generate id:s for headers even though a toc wasn't inserted into the content. + Fixed bug where is wasn't possible to drag/drop contents within the editor if paste_data_images where set to true. + Fixed bug where getParam and close in WindowManager would get the first opened window instead of the last opened window. + Fixed bug where delete would delete between cells inside a table in Firefox. Version 4.6.7 (2017-09-18) Fixed bug where paste wasn't working in IOS. Fixed bug where the Word Count Plugin didn't count some mathematical operators correctly. diff --git a/media/vendor/tinymce/plugins/advlist/plugin.js b/media/vendor/tinymce/plugins/advlist/plugin.js index 573693e59a44b..08ac6d9203462 100644 --- a/media/vendor/tinymce/plugins/advlist/plugin.js +++ b/media/vendor/tinymce/plugins/advlist/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.advlist.Plugin","tinymce.core.PluginManager","tinymce.core.util.Tools","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.advlist.Plugin","tinymce.core.PluginManager","tinymce.core.util.Tools","tinymce.plugins.advlist.api.Commands","tinymce.plugins.advlist.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.advlist.core.Actions","tinymce.plugins.advlist.api.Settings","tinymce.plugins.advlist.core.ListUtils","tinymce.plugins.advlist.ui.ListStyles"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -125,7 +125,7 @@ define( ); /** - * Plugin.js + * Actions.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,122 +134,274 @@ define( * Contributing: http://www.tinymce.com/contributing */ +define( + 'tinymce.plugins.advlist.core.Actions', + [ + ], + function () { + var applyListFormat = function (editor, listName, styleValue) { + var cmd = listName === 'UL' ? 'InsertUnorderedList' : 'InsertOrderedList'; + editor.execCommand(cmd, false, styleValue === false ? null : { 'list-style-type': styleValue }); + }; + + return { + applyListFormat: applyListFormat + }; + } +); /** - * This class contains all core logic for the advlist plugin. + * Commands.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * - * @class tinymce.plugins.advlist.Plugin - * @private + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing */ + define( - 'tinymce.plugins.advlist.Plugin', + 'tinymce.plugins.advlist.api.Commands', [ - 'tinymce.core.PluginManager', - 'tinymce.core.util.Tools' + 'tinymce.plugins.advlist.core.Actions' ], - function (PluginManager, Tools) { - PluginManager.add('advlist', function (editor) { - var olMenuItems, ulMenuItems; + function (Actions) { + var register = function (editor) { + editor.addCommand('ApplyUnorderedListStyle', function (ui, value) { + Actions.applyListFormat(editor, 'UL', value['list-style-type']); + }); - var hasPlugin = function (editor, plugin) { - var plugins = editor.settings.plugins ? editor.settings.plugins : ''; - return Tools.inArray(plugins.split(/[ ,]/), plugin) !== -1; + editor.addCommand('ApplyOrderedListStyle', function (ui, value) { + Actions.applyListFormat(editor, 'OL', value['list-style-type']); + }); + }; + + return { + register: register + }; + } +); + + +/** + * Settings.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.advlist.api.Settings', + [ + ], + function () { + var getNumberStyles = function (editor) { + var styles = editor.getParam('advlist_number_styles', 'default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman'); + return styles ? styles.split(/[ ,]/) : []; + }; + + var getBulletStyles = function (editor) { + var styles = editor.getParam('advlist_bullet_styles', 'default,circle,disc,square'); + return styles ? styles.split(/[ ,]/) : []; + }; + + return { + getNumberStyles: getNumberStyles, + getBulletStyles: getBulletStyles + }; + } +); +/** + * ListUtils.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.advlist.core.ListUtils', + [ + ], + function () { + var isChildOfBody = function (editor, elm) { + return editor.$.contains(editor.getBody(), elm); + }; + + var isListNode = function (editor) { + return function (node) { + return node && (/^(OL|UL|DL)$/).test(node.nodeName) && isChildOfBody(editor, node); }; + }; - function isChildOfBody(elm) { - return editor.$.contains(editor.getBody(), elm); - } + var getSelectedStyleType = function (editor) { + var listElm = editor.dom.getParent(editor.selection.getNode(), 'ol,ul'); + return editor.dom.getStyle(listElm, 'listStyleType') || ''; + }; - function isListNode(node) { - return node && (/^(OL|UL|DL)$/).test(node.nodeName) && isChildOfBody(node); - } + return { + isListNode: isListNode, + getSelectedStyleType: getSelectedStyleType + }; + } +); +/** + * ListStyles.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 + */ - function buildMenuItems(listName, styleValues) { - var items = []; - if (styleValues) { - Tools.each(styleValues.split(/[ ,]/), function (styleValue) { - items.push({ - text: styleValue.replace(/\-/g, ' ').replace(/\b\w/g, function (chr) { - return chr.toUpperCase(); - }), - data: styleValue == 'default' ? '' : styleValue - }); - }); - } - return items; - } +define( + 'tinymce.plugins.advlist.ui.ListStyles', + [ + 'tinymce.core.util.Tools' + ], + function (Tools) { + var styleValueToText = function (styleValue) { + return styleValue.replace(/\-/g, ' ').replace(/\b\w/g, function (chr) { + return chr.toUpperCase(); + }); + }; - olMenuItems = buildMenuItems('OL', editor.getParam( - "advlist_number_styles", - "default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman" - )); + var toMenuItems = function (styles) { + return Tools.map(styles, function (styleValue) { + var text = styleValueToText(styleValue); + var data = styleValue === 'default' ? '' : styleValue; - ulMenuItems = buildMenuItems('UL', editor.getParam("advlist_bullet_styles", "default,circle,disc,square")); + return { text: text, data: data }; + }); + }; - function applyListFormat(listName, styleValue) { - var cmd = listName == 'UL' ? 'InsertUnorderedList' : 'InsertOrderedList'; - editor.execCommand(cmd, false, styleValue === false ? null : { 'list-style-type': styleValue }); - } + return { + toMenuItems: toMenuItems + }; + } +); +/** + * Buttons.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 + */ - function updateSelection(e) { - var listStyleType = editor.dom.getStyle(editor.dom.getParent(editor.selection.getNode(), 'ol,ul'), 'listStyleType') || ''; +define( + 'tinymce.plugins.advlist.ui.Buttons', + [ + 'tinymce.core.util.Tools', + 'tinymce.plugins.advlist.api.Settings', + 'tinymce.plugins.advlist.core.Actions', + 'tinymce.plugins.advlist.core.ListUtils', + 'tinymce.plugins.advlist.ui.ListStyles' + ], + function (Tools, Settings, Actions, ListUtils, ListStyles) { + var listState = function (editor, listName) { + return function (e) { + var ctrl = e.control; + editor.on('NodeChange', function (e) { + var lists = Tools.grep(e.parents, ListUtils.isListNode(editor)); + ctrl.active(lists.length > 0 && lists[0].nodeName === listName); + }); + }; + }; + + var updateSelection = function (editor) { + return function (e) { + var listStyleType = ListUtils.getSelectedStyleType(editor); e.control.items().each(function (ctrl) { ctrl.active(ctrl.settings.data === listStyleType); }); - } + }; + }; - var listState = function (listName) { - return function () { - var self = this; + var addSplitButton = function (editor, id, tooltip, cmd, nodeName, styles) { + editor.addButton(id, { + type: 'splitbutton', + tooltip: tooltip, + menu: ListStyles.toMenuItems(styles), + onPostRender: listState(editor, nodeName), + onshow: updateSelection(editor), + onselect: function (e) { + Actions.applyListFormat(editor, nodeName, e.control.settings.data); + }, + onclick: function () { + editor.execCommand(cmd); + } + }); + }; - editor.on('NodeChange', function (e) { - var lists = Tools.grep(e.parents, isListNode); - self.active(lists.length > 0 && lists[0].nodeName === listName); - }); - }; - }; + var addButton = function (editor, id, tooltip, cmd, nodeName, styles) { + editor.addButton(id, { + type: 'button', + tooltip: tooltip, + onPostRender: listState(editor, nodeName), + onclick: function () { + editor.execCommand(cmd); + } + }); + }; - if (hasPlugin(editor, "lists")) { - editor.addCommand('ApplyUnorderedListStyle', function (ui, value) { - applyListFormat('UL', value['list-style-type']); - }); + var addControl = function (editor, id, tooltip, cmd, nodeName, styles) { + if (styles.length > 0) { + addSplitButton(editor, id, tooltip, cmd, nodeName, styles); + } else { + addButton(editor, id, tooltip, cmd, nodeName, styles); + } + }; - editor.addCommand('ApplyOrderedListStyle', function (ui, value) { - applyListFormat('OL', value['list-style-type']); - }); + var register = function (editor) { + addControl(editor, 'numlist', 'Numbered list', 'InsertOrderedList', 'OL', Settings.getNumberStyles(editor)); + addControl(editor, 'bullist', 'Bullet list', 'InsertUnorderedList', 'UL', Settings.getBulletStyles(editor)); + }; - editor.addButton('numlist', { - type: (olMenuItems.length > 0) ? 'splitbutton' : 'button', - tooltip: 'Numbered list', - menu: olMenuItems, - onPostRender: listState('OL'), - onshow: updateSelection, - onselect: function (e) { - applyListFormat('OL', e.control.settings.data); - }, - onclick: function () { - editor.execCommand('InsertOrderedList'); - } - }); + return { + register: register + }; + } +); +/** + * Plugin.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 + */ - editor.addButton('bullist', { - type: (ulMenuItems.length > 0) ? 'splitbutton' : 'button', - tooltip: 'Bullet list', - onPostRender: listState('UL'), - menu: ulMenuItems, - onshow: updateSelection, - onselect: function (e) { - applyListFormat('UL', e.control.settings.data); - }, - onclick: function () { - editor.execCommand('InsertUnorderedList'); - } - }); +define( + 'tinymce.plugins.advlist.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.core.util.Tools', + 'tinymce.plugins.advlist.api.Commands', + 'tinymce.plugins.advlist.ui.Buttons' + ], + function (PluginManager, Tools, Commands, Buttons) { + PluginManager.add('advlist', function (editor) { + var hasPlugin = function (editor, plugin) { + var plugins = editor.settings.plugins ? editor.settings.plugins : ''; + return Tools.inArray(plugins.split(/[ ,]/), plugin) !== -1; + }; + + if (hasPlugin(editor, "lists")) { + Buttons.register(editor); + Commands.register(editor); } }); return function () { }; - } ); dem('tinymce.plugins.advlist.Plugin')(); diff --git a/media/vendor/tinymce/plugins/advlist/plugin.min.js b/media/vendor/tinymce/plugins/advlist/plugin.min.js index e281ace27a419..e9bce57516759 100644 --- a/media/vendor/tinymce/plugins/advlist/plugin.min.js +++ b/media/vendor/tinymce/plugins/advlist/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;i0&&f[0].nodeName===c)})}};j(a,"lists")&&(a.addCommand("ApplyUnorderedListStyle",function(a,b){f("UL",b["list-style-type"])}),a.addCommand("ApplyOrderedListStyle",function(a,b){f("OL",b["list-style-type"])}),a.addButton("numlist",{type:h.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:h,onPostRender:k("OL"),onshow:g,onselect:function(a){f("OL",a.control.settings.data)},onclick:function(){a.execCommand("InsertOrderedList")}}),a.addButton("bullist",{type:i.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:k("UL"),menu:i,onshow:g,onselect:function(a){f("UL",a.control.settings.data)},onclick:function(){a.execCommand("InsertUnorderedList")}}))}),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;i0&&g[0].nodeName===c)})}},g=function(a){return function(b){var c=d.getSelectedStyleType(a);b.control.items().each(function(a){a.active(a.settings.data===c)})}},h=function(a,b,d,h,i,j){a.addButton(b,{type:"splitbutton",tooltip:d,menu:e.toMenuItems(j),onPostRender:f(a,i),onshow:g(a),onselect:function(b){c.applyListFormat(a,i,b.control.settings.data)},onclick:function(){a.execCommand(h)}})},i=function(a,b,c,d,e,g){a.addButton(b,{type:"button",tooltip:c,onPostRender:f(a,e),onclick:function(){a.execCommand(d)}})},j=function(a,b,c,d,e,f){f.length>0?h(a,b,c,d,e,f):i(a,b,c,d,e,f)},k=function(a){j(a,"numlist","Numbered list","InsertOrderedList","OL",b.getNumberStyles(a)),j(a,"bullist","Bullet list","InsertUnorderedList","UL",b.getBulletStyles(a))};return{register:k}}),g("0",["1","2","3","4"],function(a,b,c,d){return a.add("advlist",function(a){var e=function(a,c){var d=a.settings.plugins?a.settings.plugins:"";return b.inArray(d.split(/[ ,]/),c)!==-1};e(a,"lists")&&(d.register(a),c.register(a))}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/anchor/plugin.js b/media/vendor/tinymce/plugins/anchor/plugin.js index bc7f23680e1b1..df3b727d27214 100644 --- a/media/vendor/tinymce/plugins/anchor/plugin.js +++ b/media/vendor/tinymce/plugins/anchor/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.anchor.Plugin","tinymce.core.Env","tinymce.core.PluginManager","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.anchor.Plugin","tinymce.core.PluginManager","tinymce.plugins.anchor.api.Commands","tinymce.plugins.anchor.core.FilterContent","tinymce.plugins.anchor.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.anchor.ui.Dialog","tinymce.plugins.anchor.core.Anchor"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -95,17 +95,17 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.Env', + 'tinymce.core.PluginManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.Env'); + return resolve('tinymce.PluginManager'); } ); /** - * ResolveGlobal.js + * Anchor.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -115,17 +115,46 @@ define( */ define( - 'tinymce.core.PluginManager', + 'tinymce.plugins.anchor.core.Anchor', [ - 'global!tinymce.util.Tools.resolve' ], - function (resolve) { - return resolve('tinymce.PluginManager'); + function () { + var isValidId = function (id) { + // Follows HTML4 rules: https://www.w3.org/TR/html401/types.html#type-id + return /^[A-Za-z][A-Za-z0-9\-:._]*$/.test(id); + }; + + var getId = function (editor) { + var selectedNode = editor.selection.getNode(); + var isAnchor = selectedNode.tagName === 'A' && editor.dom.getAttrib(selectedNode, 'href') === ''; + return isAnchor ? (selectedNode.id || selectedNode.name) : ''; + }; + + var insert = function (editor, id) { + var selectedNode = editor.selection.getNode(); + var isAnchor = selectedNode.tagName === 'A' && editor.dom.getAttrib(selectedNode, 'href') === ''; + + if (isAnchor) { + selectedNode.removeAttribute('name'); + selectedNode.id = id; + } else { + editor.focus(); + editor.selection.collapse(true); + editor.execCommand('mceInsertContent', false, editor.dom.createHTML('a', { + id: id + })); + } + }; + + return { + isValidId: isValidId, + getId: getId, + insert: insert + }; } ); - /** - * Plugin.js + * Dialog.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,89 +163,133 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class contains all core logic for the anchor plugin. - * - * @class tinymce.anchor.Plugin - * @private - */ define( - 'tinymce.plugins.anchor.Plugin', + 'tinymce.plugins.anchor.ui.Dialog', [ - 'tinymce.core.Env', - 'tinymce.core.PluginManager' + 'tinymce.plugins.anchor.core.Anchor' ], + function (Anchor) { + var insertAnchor = function (editor, newId) { + if (!Anchor.isValidId(newId)) { + editor.windowManager.alert( + 'Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.' + ); + return true; + } else { + Anchor.insert(editor, newId); + return false; + } + }; - function (Env, PluginManager) { - PluginManager.add('anchor', function (editor) { - var isAnchorNode = function (node) { - return !node.attr('href') && (node.attr('id') || node.attr('name')) && !node.firstChild; - }; + var open = function (editor) { + var currentId = Anchor.getId(editor); - var setContentEditable = function (state) { - return function (nodes) { - for (var i = 0; i < nodes.length; i++) { - if (isAnchorNode(nodes[i])) { - nodes[i].attr('contenteditable', state); - } + editor.windowManager.open({ + title: 'Anchor', + body: { type: 'textbox', name: 'id', size: 40, label: 'Id', value: currentId }, + onsubmit: function (e) { + var newId = e.data.id; + + if (insertAnchor(editor, newId)) { + e.preventDefault(); } - }; - }; + } + }); + }; - var isValidId = function (id) { - // Follows HTML4 rules: https://www.w3.org/TR/html401/types.html#type-id - return /^[A-Za-z][A-Za-z0-9\-:._]*$/.test(id); - }; + return { + open: open + }; + } +); +/** + * 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 + */ - var showDialog = function () { - var selectedNode = editor.selection.getNode(); - var isAnchor = selectedNode.tagName == 'A' && editor.dom.getAttrib(selectedNode, 'href') === ''; - var value = ''; +define( + 'tinymce.plugins.anchor.api.Commands', + [ + 'tinymce.plugins.anchor.ui.Dialog' + ], + function (Dialog) { + var register = function (editor) { + editor.addCommand('mceAnchor', function () { + Dialog.open(editor); + }); + }; - if (isAnchor) { - value = selectedNode.id || selectedNode.name || ''; - } + return { + register: register + }; + } +); +/** + * FilterContent.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 + */ - editor.windowManager.open({ - title: 'Anchor', - body: { type: 'textbox', name: 'id', size: 40, label: 'Id', value: value }, - onsubmit: function (e) { - var id = e.data.id; - - if (!isValidId(id)) { - e.preventDefault(); - editor.windowManager.alert( - 'Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.' - ); - return; - } - - if (isAnchor) { - selectedNode.removeAttribute('name'); - selectedNode.id = id; - } else { - editor.selection.collapse(true); - editor.execCommand('mceInsertContent', false, editor.dom.createHTML('a', { - id: id - })); - } +define( + 'tinymce.plugins.anchor.core.FilterContent', + [ + ], + function () { + var isAnchorNode = function (node) { + return !node.attr('href') && (node.attr('id') || node.attr('name')) && !node.firstChild; + }; + + var setContentEditable = function (state) { + return function (nodes) { + for (var i = 0; i < nodes.length; i++) { + if (isAnchorNode(nodes[i])) { + nodes[i].attr('contenteditable', state); } - }); + } }; + }; - if (Env.ceFalse) { - editor.on('PreInit', function () { - editor.parser.addNodeFilter('a', setContentEditable('false')); - editor.serializer.addNodeFilter('a', setContentEditable(null)); - }); - } + var setup = function (editor) { + editor.on('PreInit', function () { + editor.parser.addNodeFilter('a', setContentEditable('false')); + editor.serializer.addNodeFilter('a', setContentEditable(null)); + }); + }; - editor.addCommand('mceAnchor', showDialog); + return { + setup: setup + }; + } +); +/** + * Buttons.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.anchor.ui.Buttons', + [ + ], + function () { + var register = function (editor) { editor.addButton('anchor', { icon: 'anchor', tooltip: 'Anchor', - onclick: showDialog, + cmd: 'mceAnchor', stateSelector: 'a:not([href])' }); @@ -224,8 +297,38 @@ define( icon: 'anchor', text: 'Anchor', context: 'insert', - onclick: showDialog + cmd: 'mceAnchor' }); + }; + + return { + register: register + }; + } +); +/** + * Plugin.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.anchor.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.anchor.api.Commands', + 'tinymce.plugins.anchor.core.FilterContent', + 'tinymce.plugins.anchor.ui.Buttons' + ], + function (PluginManager, Commands, FilterContent, Buttons) { + PluginManager.add('anchor', function (editor) { + FilterContent.setup(editor); + Commands.register(editor); + Buttons.register(editor); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/anchor/plugin.min.js b/media/vendor/tinymce/plugins/anchor/plugin.min.js index 6b3361d26fc74..ef86762a50117 100644 --- a/media/vendor/tinymce/plugins/anchor/plugin.min.js +++ b/media/vendor/tinymce/plugins/anchor/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 len) { + index = len; } - }); - - function handleEclipse(editor) { - parseCurrentLine(editor, -1, '(', true); } - function handleSpacebar(editor) { - parseCurrentLine(editor, 0, '', true); - } + return index; + }; - function handleEnter(editor) { - parseCurrentLine(editor, -1, '', false); + var setStart = function (rng, container, offset) { + if (container.nodeType !== 1 || container.hasChildNodes()) { + rng.setStart(container, scopeIndex(container, offset)); + } else { + rng.setStartBefore(container); } + }; - function parseCurrentLine(editor, endOffset, delimiter) { - var rng, end, start, endContainer, bookmark, text, matches, prev, len, rngText; + var setEnd = function (rng, container, offset) { + if (container.nodeType !== 1 || container.hasChildNodes()) { + rng.setEnd(container, scopeIndex(container, offset)); + } else { + rng.setEndAfter(container); + } + }; - function scopeIndex(container, index) { - if (index < 0) { - index = 0; - } + var parseCurrentLine = function (editor, endOffset, delimiter) { + var rng, end, start, endContainer, bookmark, text, matches, prev, len, rngText; + var autoLinkPattern = Settings.getAutoLinkPattern(editor); + var defaultLinkTarget = Settings.getDefaultLinkTarget(editor); - if (container.nodeType == 3) { - var len = container.data.length; + // Never create a link when we are inside a link + if (editor.selection.getNode().tagName === 'A') { + return; + } - if (index > len) { - index = len; - } + // We need at least five characters to form a URL, + // hence, at minimum, five characters from the beginning of the line. + rng = editor.selection.getRng(true).cloneRange(); + if (rng.startOffset < 5) { + // During testing, the caret is placed between two text nodes. + // The previous text node contains the URL. + prev = rng.endContainer.previousSibling; + if (!prev) { + if (!rng.endContainer.firstChild || !rng.endContainer.firstChild.nextSibling) { + return; } - return index; - } - - function setStart(container, offset) { - if (container.nodeType != 1 || container.hasChildNodes()) { - rng.setStart(container, scopeIndex(container, offset)); - } else { - rng.setStartBefore(container); - } + prev = rng.endContainer.firstChild.nextSibling; } - function setEnd(container, offset) { - if (container.nodeType != 1 || container.hasChildNodes()) { - rng.setEnd(container, scopeIndex(container, offset)); - } else { - rng.setEndAfter(container); - } - } + len = prev.length; + setStart(rng, prev, len); + setEnd(rng, prev, len); - // Never create a link when we are inside a link - if (editor.selection.getNode().tagName == 'A') { + if (rng.endOffset < 5) { return; } - // We need at least five characters to form a URL, - // hence, at minimum, five characters from the beginning of the line. - rng = editor.selection.getRng(true).cloneRange(); - if (rng.startOffset < 5) { - // During testing, the caret is placed between two text nodes. - // The previous text node contains the URL. - prev = rng.endContainer.previousSibling; - if (!prev) { - if (!rng.endContainer.firstChild || !rng.endContainer.firstChild.nextSibling) { - return; - } + end = rng.endOffset; + endContainer = prev; + } else { + endContainer = rng.endContainer; - prev = rng.endContainer.firstChild.nextSibling; + // Get a text node + if (endContainer.nodeType !== 3 && endContainer.firstChild) { + while (endContainer.nodeType !== 3 && endContainer.firstChild) { + endContainer = endContainer.firstChild; } - len = prev.length; - setStart(prev, len); - setEnd(prev, len); - - if (rng.endOffset < 5) { - return; + // Move range to text node + if (endContainer.nodeType === 3) { + setStart(rng, endContainer, 0); + setEnd(rng, endContainer, endContainer.nodeValue.length); } + } - end = rng.endOffset; - endContainer = prev; + if (rng.endOffset === 1) { + end = 2; } else { - endContainer = rng.endContainer; + end = rng.endOffset - 1 - endOffset; + } + } - // Get a text node - if (endContainer.nodeType != 3 && endContainer.firstChild) { - while (endContainer.nodeType != 3 && endContainer.firstChild) { - endContainer = endContainer.firstChild; - } + start = end; + + do { + // Move the selection one character backwards. + setStart(rng, endContainer, end >= 2 ? end - 2 : 0); + setEnd(rng, endContainer, end >= 1 ? end - 1 : 0); + end -= 1; + rngText = rng.toString(); + + // Loop until one of the following is found: a blank space,  , delimiter, (end-2) >= 0 + } while (rngText !== ' ' && rngText !== '' && rngText.charCodeAt(0) !== 160 && (end - 2) >= 0 && rngText !== delimiter); + + if (rangeEqualsDelimiterOrSpace(rng.toString(), delimiter)) { + setStart(rng, endContainer, end); + setEnd(rng, endContainer, start); + end += 1; + } else if (rng.startOffset === 0) { + setStart(rng, endContainer, 0); + setEnd(rng, endContainer, start); + } else { + setStart(rng, endContainer, end); + setEnd(rng, endContainer, start); + } - // Move range to text node - if (endContainer.nodeType == 3) { - setStart(endContainer, 0); - setEnd(endContainer, endContainer.nodeValue.length); - } - } + // Exclude last . from word like "www.site.com." + text = rng.toString(); + if (text.charAt(text.length - 1) === '.') { + setEnd(rng, endContainer, start - 1); + } - if (rng.endOffset == 1) { - end = 2; - } else { - end = rng.endOffset - 1 - endOffset; - } - } + text = rng.toString(); + matches = text.match(autoLinkPattern); - start = end; - - do { - // Move the selection one character backwards. - setStart(endContainer, end >= 2 ? end - 2 : 0); - setEnd(endContainer, end >= 1 ? end - 1 : 0); - end -= 1; - rngText = rng.toString(); - - // Loop until one of the following is found: a blank space,  , delimiter, (end-2) >= 0 - } while (rngText != ' ' && rngText !== '' && rngText.charCodeAt(0) != 160 && (end - 2) >= 0 && rngText != delimiter); - - if (rangeEqualsDelimiterOrSpace(rng.toString(), delimiter)) { - setStart(endContainer, end); - setEnd(endContainer, start); - end += 1; - } else if (rng.startOffset === 0) { - setStart(endContainer, 0); - setEnd(endContainer, start); - } else { - setStart(endContainer, end); - setEnd(endContainer, start); + if (matches) { + if (matches[1] === 'www.') { + matches[1] = 'http://www.'; + } else if (/@$/.test(matches[1]) && !/^mailto:/.test(matches[1])) { + matches[1] = 'mailto:' + matches[1]; } - // Exclude last . from word like "www.site.com." - text = rng.toString(); - if (text.charAt(text.length - 1) == '.') { - setEnd(endContainer, start - 1); + bookmark = editor.selection.getBookmark(); + + editor.selection.setRng(rng); + editor.execCommand('createlink', false, matches[1] + matches[2]); + + if (defaultLinkTarget) { + editor.dom.setAttrib(editor.selection.getNode(), 'target', defaultLinkTarget); } - text = rng.toString(); - matches = text.match(AutoLinkPattern); + editor.selection.moveToBookmark(bookmark); + editor.nodeChanged(); + } + }; - if (matches) { - if (matches[1] == 'www.') { - matches[1] = 'http://www.'; - } else if (/@$/.test(matches[1]) && !/^mailto:/.test(matches[1])) { - matches[1] = 'mailto:' + matches[1]; - } + var setup = function (editor) { + var autoUrlDetectState; - bookmark = editor.selection.getBookmark(); + editor.on("keydown", function (e) { + if (e.keyCode === 13) { + return handleEnter(editor); + } + }); - editor.selection.setRng(rng); - editor.execCommand('createlink', false, matches[1] + matches[2]); + // Internet Explorer has built-in automatic linking for most cases + if (Env.ie) { + editor.on("focus", function () { + if (!autoUrlDetectState) { + autoUrlDetectState = true; - if (editor.settings.default_link_target) { - editor.dom.setAttrib(editor.selection.getNode(), 'target', editor.settings.default_link_target); + try { + editor.execCommand('AutoUrlDetect', false, true); + } catch (ex) { + // Ignore + } } + }); - editor.selection.moveToBookmark(bookmark); - editor.nodeChanged(); - } + return; } + + editor.on("keypress", function (e) { + if (e.keyCode === 41) { + return handleEclipse(editor); + } + }); + + editor.on("keyup", function (e) { + if (e.keyCode === 32) { + return handleSpacebar(editor); + } + }); + }; + + return { + setup: setup + }; + } +); +/** + * Plugin.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.autolink.Plugin', + [ + 'tinymce.core.Env', + 'tinymce.core.PluginManager', + 'tinymce.plugins.autolink.core.Keys' + ], + function (Env, PluginManager, Keys) { + PluginManager.add('autolink', function (editor) { + Keys.setup(editor); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/autolink/plugin.min.js b/media/vendor/tinymce/plugins/autolink/plugin.min.js index 8ba749841693d..25124f662ce72 100644 --- a/media/vendor/tinymce/plugins/autolink/plugin.min.js +++ b/media/vendor/tinymce/plugins/autolink/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;ic&&(b=c)}return b}function f(a,b){1!=a.nodeType||a.hasChildNodes()?h.setStart(a,e(a,b)):h.setStartBefore(a)}function g(a,b){1!=a.nodeType||a.hasChildNodes()?h.setEnd(a,e(a,b)):h.setEndAfter(a)}var h,j,k,l,m,n,o,p,q,r;if("A"!=a.selection.getNode().tagName){if(h=a.selection.getRng(!0).cloneRange(),h.startOffset<5){if(p=h.endContainer.previousSibling,!p){if(!h.endContainer.firstChild||!h.endContainer.firstChild.nextSibling)return;p=h.endContainer.firstChild.nextSibling}if(q=p.length,f(p,q),g(p,q),h.endOffset<5)return;j=h.endOffset,l=p}else{if(l=h.endContainer,3!=l.nodeType&&l.firstChild){for(;3!=l.nodeType&&l.firstChild;)l=l.firstChild;3==l.nodeType&&(f(l,0),g(l,l.nodeValue.length))}j=1==h.endOffset?2:h.endOffset-1-b}k=j;do f(l,j>=2?j-2:0),g(l,j>=1?j-1:0),j-=1,r=h.toString();while(" "!=r&&""!==r&&160!=r.charCodeAt(0)&&j-2>=0&&r!=d);c(h.toString(),d)?(f(l,j),g(l,k),j+=1):0===h.startOffset?(f(l,0),g(l,k)):(f(l,j),g(l,k)),n=h.toString(),"."==n.charAt(n.length-1)&&g(l,k-1),n=h.toString(),o=n.match(i),o&&("www."==o[1]?o[1]="http://www.":/@$/.test(o[1])&&!/^mailto:/.test(o[1])&&(o[1]="mailto:"+o[1]),m=a.selection.getBookmark(),a.selection.setRng(h),a.execCommand("createlink",!1,o[1]+o[2]),a.settings.default_link_target&&a.dom.setAttrib(a.selection.getNode(),"target",a.settings.default_link_target),a.selection.moveToBookmark(m),a.nodeChanged())}}var h,i=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return b.settings.autolink_pattern&&(i=b.settings.autolink_pattern),b.on("keydown",function(a){if(13==a.keyCode)return f(b)}),a.ie?void b.on("focus",function(){if(!h){h=!0;try{b.execCommand("AutoUrlDetect",!1,!0)}catch(a){}}}):(b.on("keypress",function(a){if(41==a.keyCode)return d(b)}),void b.on("keyup",function(a){if(32==a.keyCode)return e(b)}))}),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;ic&&(b=c)}return b},i=function(a,b,c){1!==b.nodeType||b.hasChildNodes()?a.setStart(b,h(b,c)):a.setStartBefore(b)},j=function(a,b,c){1!==b.nodeType||b.hasChildNodes()?a.setEnd(b,h(b,c)):a.setEndAfter(b)},k=function(a,b,e){var f,g,h,k,l,m,n,o,p,q,r=c.getAutoLinkPattern(a),s=c.getDefaultLinkTarget(a);if("A"!==a.selection.getNode().tagName){if(f=a.selection.getRng(!0).cloneRange(),f.startOffset<5){if(o=f.endContainer.previousSibling,!o){if(!f.endContainer.firstChild||!f.endContainer.firstChild.nextSibling)return;o=f.endContainer.firstChild.nextSibling}if(p=o.length,i(f,o,p),j(f,o,p),f.endOffset<5)return;g=f.endOffset,k=o}else{if(k=f.endContainer,3!==k.nodeType&&k.firstChild){for(;3!==k.nodeType&&k.firstChild;)k=k.firstChild;3===k.nodeType&&(i(f,k,0),j(f,k,k.nodeValue.length))}g=1===f.endOffset?2:f.endOffset-1-b}h=g;do i(f,k,g>=2?g-2:0),j(f,k,g>=1?g-1:0),g-=1,q=f.toString();while(" "!==q&&""!==q&&160!==q.charCodeAt(0)&&g-2>=0&&q!==e);d(f.toString(),e)?(i(f,k,g),j(f,k,h),g+=1):0===f.startOffset?(i(f,k,0),j(f,k,h)):(i(f,k,g),j(f,k,h)),m=f.toString(),"."===m.charAt(m.length-1)&&j(f,k,h-1),m=f.toString(),n=m.match(r),n&&("www."===n[1]?n[1]="http://www.":/@$/.test(n[1])&&!/^mailto:/.test(n[1])&&(n[1]="mailto:"+n[1]),l=a.selection.getBookmark(),a.selection.setRng(f),a.execCommand("createlink",!1,n[1]+n[2]),s&&a.dom.setAttrib(a.selection.getNode(),"target",s),a.selection.moveToBookmark(l),a.nodeChanged())}},l=function(b){var c;return b.on("keydown",function(a){if(13===a.keyCode)return g(b)}),a.ie?void b.on("focus",function(){if(!c){c=!0;try{b.execCommand("AutoUrlDetect",!1,!0)}catch(a){}}}):(b.on("keypress",function(a){if(41===a.keyCode)return e(b)}),void b.on("keyup",function(a){if(32===a.keyCode)return f(b)}))};return{setup:l}}),g("0",["1","2","3"],function(a,b,c){return b.add("autolink",function(a){c.setup(a)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/autoresize/plugin.js b/media/vendor/tinymce/plugins/autoresize/plugin.js index f453673379c58..03568431129c6 100644 --- a/media/vendor/tinymce/plugins/autoresize/plugin.js +++ b/media/vendor/tinymce/plugins/autoresize/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,13 +76,46 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.autoresize.Plugin","tinymce.core.dom.DOMUtils","tinymce.core.Env","tinymce.core.PluginManager","tinymce.core.util.Delay","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.autoresize.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.autoresize.api.Commands","tinymce.plugins.autoresize.core.Resize","global!tinymce.util.Tools.resolve","tinymce.core.Env","tinymce.core.util.Delay","tinymce.plugins.autoresize.api.Settings"] jsc*/ +define( + 'ephox.katamari.api.Cell', + + [ + ], + + function () { + var Cell = function (initial) { + var value = initial; + + var get = function () { + return value; + }; + + var set = function (v) { + value = v; + }; + + var clone = function () { + return Cell(get()); + }; + + return { + get: get, + set: set, + clone: clone + }; + }; + + return Cell; + } +); + defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** * ResolveGlobal.js @@ -95,12 +128,12 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.dom.DOMUtils', + 'tinymce.core.PluginManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.dom.DOMUtils'); + return resolve('tinymce.PluginManager'); } ); @@ -135,17 +168,17 @@ define( */ define( - 'tinymce.core.PluginManager', + 'tinymce.core.util.Delay', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.PluginManager'); + return resolve('tinymce.util.Delay'); } ); /** - * ResolveGlobal.js + * Settings.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -155,15 +188,39 @@ define( */ define( - 'tinymce.core.util.Delay', + 'tinymce.plugins.autoresize.api.Settings', [ - 'global!tinymce.util.Tools.resolve' ], - function (resolve) { - return resolve('tinymce.util.Delay'); + function () { + var getAutoResizeMinHeight = function (editor) { + return parseInt(editor.getParam('autoresize_min_height', editor.getElement().offsetHeight), 10); + }; + + var getAutoResizeMaxHeight = function (editor) { + return parseInt(editor.getParam('autoresize_max_height', 0), 10); + }; + + var getAutoResizeOverflowPadding = function (editor) { + return editor.getParam('autoresize_overflow_padding', 1); + }; + + var getAutoResizeBottomMargin = function (editor) { + return editor.getParam('autoresize_bottom_margin', 50); + }; + + var shouldAutoResizeOnInit = function (editor) { + return editor.getParam('autoresize_on_init', true); + }; + + return { + getAutoResizeMinHeight: getAutoResizeMinHeight, + getAutoResizeMaxHeight: getAutoResizeMaxHeight, + getAutoResizeOverflowPadding: getAutoResizeOverflowPadding, + getAutoResizeBottomMargin: getAutoResizeBottomMargin, + shouldAutoResizeOnInit: shouldAutoResizeOnInit + }; } ); - /** * Plugin.js * @@ -181,158 +238,199 @@ define( * @private */ define( - 'tinymce.plugins.autoresize.Plugin', + 'tinymce.plugins.autoresize.core.Resize', [ - 'tinymce.core.dom.DOMUtils', 'tinymce.core.Env', - 'tinymce.core.PluginManager', - 'tinymce.core.util.Delay' + 'tinymce.core.util.Delay', + 'tinymce.plugins.autoresize.api.Settings' ], - function (DOMUtils, Env, PluginManager, Delay) { - var DOM = DOMUtils.DOM; - - PluginManager.add('autoresize', function (editor) { - var settings = editor.settings, oldSize = 0; - - function isFullscreen() { - return editor.plugins.fullscreen && editor.plugins.fullscreen.isFullscreen(); - } - - if (editor.settings.inline) { + function (Env, Delay, Settings) { + var isFullscreen = function (editor) { + return editor.plugins.fullscreen && editor.plugins.fullscreen.isFullscreen(); + }; + + /** + * Calls the resize x times in 100ms intervals. We can't wait for load events since + * the CSS files might load async. + */ + var wait = function (editor, oldSize, times, interval, callback) { + Delay.setEditorTimeout(editor, function () { + resize(editor, oldSize); + + if (times--) { + wait(editor, oldSize, times, interval, callback); + } else if (callback) { + callback(); + } + }, interval); + }; + + /** + * This method gets executed each time the editor needs to resize. + */ + var resize = function (editor, oldSize) { + var deltaSize, doc, body, docElm, resizeHeight, myHeight; + var marginTop, marginBottom, paddingTop, paddingBottom, borderTop, borderBottom; + var dom = editor.dom; + + doc = editor.getDoc(); + if (!doc || isFullscreen(editor)) { return; } - /** - * This method gets executed each time the editor needs to resize. - */ - function resize(e) { - var deltaSize, doc, body, docElm, resizeHeight, myHeight, - marginTop, marginBottom, paddingTop, paddingBottom, borderTop, borderBottom; - - doc = editor.getDoc(); - if (!doc) { - return; - } - - body = doc.body; - docElm = doc.documentElement; - resizeHeight = settings.autoresize_min_height; - - if (!body || (e && e.type === "setcontent" && e.initial) || isFullscreen()) { - if (body && docElm) { - body.style.overflowY = "auto"; - docElm.style.overflowY = "auto"; // Old IE - } - - return; - } - - // Calculate outer height of the body element using CSS styles - marginTop = editor.dom.getStyle(body, 'margin-top', true); - marginBottom = editor.dom.getStyle(body, 'margin-bottom', true); - paddingTop = editor.dom.getStyle(body, 'padding-top', true); - paddingBottom = editor.dom.getStyle(body, 'padding-bottom', true); - borderTop = editor.dom.getStyle(body, 'border-top-width', true); - borderBottom = editor.dom.getStyle(body, 'border-bottom-width', true); - myHeight = body.offsetHeight + parseInt(marginTop, 10) + parseInt(marginBottom, 10) + - parseInt(paddingTop, 10) + parseInt(paddingBottom, 10) + - parseInt(borderTop, 10) + parseInt(borderBottom, 10); - - // Make sure we have a valid height - if (isNaN(myHeight) || myHeight <= 0) { - // Get height differently depending on the browser used - // eslint-disable-next-line no-nested-ternary - myHeight = Env.ie ? body.scrollHeight : (Env.webkit && body.clientHeight === 0 ? 0 : body.offsetHeight); - } - - // Don't make it smaller than the minimum height - if (myHeight > settings.autoresize_min_height) { - resizeHeight = myHeight; - } - - // If a maximum height has been defined don't exceed this height - if (settings.autoresize_max_height && myHeight > settings.autoresize_max_height) { - resizeHeight = settings.autoresize_max_height; - body.style.overflowY = "auto"; - docElm.style.overflowY = "auto"; // Old IE - } else { - body.style.overflowY = "hidden"; - docElm.style.overflowY = "hidden"; // Old IE - body.scrollTop = 0; - } + body = doc.body; + docElm = doc.documentElement; + resizeHeight = Settings.getAutoResizeMinHeight(editor); + + // Calculate outer height of the body element using CSS styles + marginTop = dom.getStyle(body, 'margin-top', true); + marginBottom = dom.getStyle(body, 'margin-bottom', true); + paddingTop = dom.getStyle(body, 'padding-top', true); + paddingBottom = dom.getStyle(body, 'padding-bottom', true); + borderTop = dom.getStyle(body, 'border-top-width', true); + borderBottom = dom.getStyle(body, 'border-bottom-width', true); + myHeight = body.offsetHeight + parseInt(marginTop, 10) + parseInt(marginBottom, 10) + + parseInt(paddingTop, 10) + parseInt(paddingBottom, 10) + + parseInt(borderTop, 10) + parseInt(borderBottom, 10); + + // Make sure we have a valid height + if (isNaN(myHeight) || myHeight <= 0) { + // Get height differently depending on the browser used + // eslint-disable-next-line no-nested-ternary + myHeight = Env.ie ? body.scrollHeight : (Env.webkit && body.clientHeight === 0 ? 0 : body.offsetHeight); + } - // Resize content element - if (resizeHeight !== oldSize) { - deltaSize = resizeHeight - oldSize; - DOM.setStyle(editor.iframeElement, 'height', resizeHeight + 'px'); - oldSize = resizeHeight; - - // WebKit doesn't decrease the size of the body element until the iframe gets resized - // So we need to continue to resize the iframe down until the size gets fixed - if (Env.webKit && deltaSize < 0) { - resize(e); - } - } + // Don't make it smaller than the minimum height + if (myHeight > Settings.getAutoResizeMinHeight(editor)) { + resizeHeight = myHeight; } - /** - * Calls the resize x times in 100ms intervals. We can't wait for load events since - * the CSS files might load async. - */ - function wait(times, interval, callback) { - Delay.setEditorTimeout(editor, function () { - resize({}); - - if (times--) { - wait(times, interval, callback); - } else if (callback) { - callback(); - } - }, interval); + // If a maximum height has been defined don't exceed this height + var maxHeight = Settings.getAutoResizeMaxHeight(editor); + if (maxHeight && myHeight > maxHeight) { + resizeHeight = maxHeight; + body.style.overflowY = "auto"; + docElm.style.overflowY = "auto"; // Old IE + } else { + body.style.overflowY = "hidden"; + docElm.style.overflowY = "hidden"; // Old IE + body.scrollTop = 0; } - // Define minimum height - settings.autoresize_min_height = parseInt(editor.getParam('autoresize_min_height', editor.getElement().offsetHeight), 10); + // Resize content element + if (resizeHeight !== oldSize.get()) { + deltaSize = resizeHeight - oldSize.get(); + dom.setStyle(editor.iframeElement, 'height', resizeHeight + 'px'); + oldSize.set(resizeHeight); - // Define maximum height - settings.autoresize_max_height = parseInt(editor.getParam('autoresize_max_height', 0), 10); + // WebKit doesn't decrease the size of the body element until the iframe gets resized + // So we need to continue to resize the iframe down until the size gets fixed + if (Env.webKit && deltaSize < 0) { + resize(editor); + } + } + }; - // Add padding at the bottom for better UX + var setup = function (editor, oldSize) { editor.on("init", function () { - var overflowPadding, bottomMargin; + var overflowPadding, bottomMargin, dom = editor.dom; - overflowPadding = editor.getParam('autoresize_overflow_padding', 1); - bottomMargin = editor.getParam('autoresize_bottom_margin', 50); + overflowPadding = Settings.getAutoResizeOverflowPadding(editor); + bottomMargin = Settings.getAutoResizeBottomMargin(editor); if (overflowPadding !== false) { - editor.dom.setStyles(editor.getBody(), { + dom.setStyles(editor.getBody(), { paddingLeft: overflowPadding, paddingRight: overflowPadding }); } if (bottomMargin !== false) { - editor.dom.setStyles(editor.getBody(), { + dom.setStyles(editor.getBody(), { paddingBottom: bottomMargin }); } }); - // Add appropriate listeners for resizing content area - editor.on("nodechange setcontent keyup FullscreenStateChanged", resize); + editor.on("nodechange setcontent keyup FullscreenStateChanged", function () { + resize(editor, oldSize); + }); - if (editor.getParam('autoresize_on_init', true)) { + if (Settings.shouldAutoResizeOnInit(editor)) { editor.on('init', function () { // Hit it 20 times in 100 ms intervals - wait(20, 100, function () { + wait(editor, oldSize, 20, 100, function () { // Hit it 5 times in 1 sec intervals - wait(5, 1000); + wait(editor, oldSize, 5, 1000); }); }); } + }; - // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceExample'); - editor.addCommand('mceAutoResize', resize); + return { + setup: setup, + resize: resize + }; + } +); +/** + * 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.plugins.autoresize.api.Commands', + [ + 'tinymce.plugins.autoresize.core.Resize' + ], + function (Resize) { + var register = function (editor, oldSize) { + editor.addCommand('mceAutoResize', function () { + Resize.resize(editor, oldSize); + }); + }; + + return { + register: register + }; + } +); +/** + * Plugin.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 contains all core logic for the autoresize plugin. + * + * @class tinymce.autoresize.Plugin + * @private + */ +define( + 'tinymce.plugins.autoresize.Plugin', + [ + 'ephox.katamari.api.Cell', + 'tinymce.core.PluginManager', + 'tinymce.plugins.autoresize.api.Commands', + 'tinymce.plugins.autoresize.core.Resize' + ], + function (Cell, PluginManager, Commands, Resize) { + PluginManager.add('autoresize', function (editor) { + if (!editor.inline) { + var oldSize = Cell(0); + Commands.register(editor, oldSize); + Resize.setup(editor, oldSize); + } }); return function () {}; diff --git a/media/vendor/tinymce/plugins/autoresize/plugin.min.js b/media/vendor/tinymce/plugins/autoresize/plugin.min.js index 081b5d32dc29f..b486f7a7d3105 100644 --- a/media/vendor/tinymce/plugins/autoresize/plugin.min.js +++ b/media/vendor/tinymce/plugins/autoresize/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;ih.autoresize_min_height&&(m=n),h.autoresize_max_height&&n>h.autoresize_max_height?(m=h.autoresize_max_height,k.style.overflowY="auto",l.style.overflowY="auto"):(k.style.overflowY="hidden",l.style.overflowY="hidden",k.scrollTop=0),m!==i&&(g=m-i,e.setStyle(a.iframeElement,"height",m+"px"),i=m,b.webKit&&g<0&&f(d))}}function g(b,c,e){d.setEditorTimeout(a,function(){f({}),b--?g(b,c,e):e&&e()},c)}var h=a.settings,i=0;a.settings.inline||(h.autoresize_min_height=parseInt(a.getParam("autoresize_min_height",a.getElement().offsetHeight),10),h.autoresize_max_height=parseInt(a.getParam("autoresize_max_height",0),10),a.on("init",function(){var b,c;b=a.getParam("autoresize_overflow_padding",1),c=a.getParam("autoresize_bottom_margin",50),b!==!1&&a.dom.setStyles(a.getBody(),{paddingLeft:b,paddingRight:b}),c!==!1&&a.dom.setStyles(a.getBody(),{paddingBottom:c})}),a.on("nodechange setcontent keyup FullscreenStateChanged",f),a.getParam("autoresize_on_init",!0)&&a.on("init",function(){g(20,100,function(){g(5,1e3)})}),a.addCommand("mceAutoResize",f))}),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;ic.getAutoResizeMinHeight(b)&&(k=l);var t=c.getAutoResizeMaxHeight(b);t&&l>t?(k=t,i.style.overflowY="auto",j.style.overflowY="auto"):(i.style.overflowY="hidden",j.style.overflowY="hidden",i.scrollTop=0),k!==e.get()&&(g=k-e.get(),s.setStyle(b.iframeElement,"height",k+"px"),e.set(k),a.webKit&&g<0&&f(b))}},g=function(a,b){a.on("init",function(){var b,d,e=a.dom;b=c.getAutoResizeOverflowPadding(a),d=c.getAutoResizeBottomMargin(a),b!==!1&&e.setStyles(a.getBody(),{paddingLeft:b,paddingRight:b}),d!==!1&&e.setStyles(a.getBody(),{paddingBottom:d})}),a.on("nodechange setcontent keyup FullscreenStateChanged",function(){f(a,b)}),c.shouldAutoResizeOnInit(a)&&a.on("init",function(){e(a,b,20,100,function(){e(a,b,5,1e3)})})};return{setup:g,resize:f}}),g("3",["4"],function(a){var b=function(b,c){b.addCommand("mceAutoResize",function(){a.resize(b,c)})};return{register:b}}),g("0",["1","2","3","4"],function(a,b,c,d){return b.add("autoresize",function(b){if(!b.inline){var e=a(0);c.register(b,e),d.setup(b,e)}}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/autosave/plugin.js b/media/vendor/tinymce/plugins/autosave/plugin.js index c461c0415ecd8..5a5da59935296 100644 --- a/media/vendor/tinymce/plugins/autosave/plugin.js +++ b/media/vendor/tinymce/plugins/autosave/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,13 +76,46 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.autosave.Plugin","tinymce.core.EditorManager","tinymce.core.PluginManager","tinymce.core.util.LocalStorage","tinymce.core.util.Tools","global!window","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.autosave.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.autosave.api.Api","tinymce.plugins.autosave.core.BeforeUnload","tinymce.plugins.autosave.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.autosave.core.Storage","global!window","tinymce.core.EditorManager","tinymce.core.util.Tools","tinymce.plugins.autosave.api.Settings","global!setInterval","tinymce.core.util.LocalStorage","tinymce.plugins.autosave.api.Events","global!document","tinymce.plugins.autosave.core.Time"] jsc*/ +define( + 'ephox.katamari.api.Cell', + + [ + ], + + function () { + var Cell = function (initial) { + var value = initial; + + var get = function () { + return value; + }; + + var set = function (v) { + value = v; + }; + + var clone = function () { + return Cell(get()); + }; + + return { + get: get, + set: set, + clone: clone + }; + }; + + return Cell; + } +); + defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** * ResolveGlobal.js @@ -95,15 +128,16 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.EditorManager', + 'tinymce.core.PluginManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.EditorManager'); + return resolve('tinymce.PluginManager'); } ); +defineGlobal("global!setInterval", setInterval); /** * ResolveGlobal.js * @@ -115,12 +149,12 @@ define( */ define( - 'tinymce.core.PluginManager', + 'tinymce.core.util.LocalStorage', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.PluginManager'); + return resolve('tinymce.util.LocalStorage'); } ); @@ -135,17 +169,17 @@ define( */ define( - 'tinymce.core.util.LocalStorage', + 'tinymce.core.util.Tools', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.util.LocalStorage'); + return resolve('tinymce.util.Tools'); } ); /** - * ResolveGlobal.js + * Events.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -155,18 +189,33 @@ define( */ define( - 'tinymce.core.util.Tools', + 'tinymce.plugins.autosave.api.Events', [ - 'global!tinymce.util.Tools.resolve' ], - function (resolve) { - return resolve('tinymce.util.Tools'); + function () { + var fireRestoreDraft = function (editor) { + return editor.fire('RestoreDraft'); + }; + + var fireStoreDraft = function (editor) { + return editor.fire('StoreDraft'); + }; + + var fireRemoveDraft = function (editor) { + return editor.fire('RemoveDraft'); + }; + + return { + fireRestoreDraft: fireRestoreDraft, + fireStoreDraft: fireStoreDraft, + fireRemoveDraft: fireRemoveDraft + }; } ); -defineGlobal("global!window", window); +defineGlobal("global!document", document); /** - * Plugin.js + * Time.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -175,170 +224,380 @@ defineGlobal("global!window", window); * Contributing: http://www.tinymce.com/contributing */ -/** - * This class contains all core logic for the autosave plugin. - * - * @class tinymce.autosave.Plugin - * @private - */ define( - 'tinymce.plugins.autosave.Plugin', + 'tinymce.plugins.autosave.core.Time', [ - 'tinymce.core.EditorManager', - 'tinymce.core.PluginManager', - 'tinymce.core.util.LocalStorage', - 'tinymce.core.util.Tools', - 'global!window' ], - function (EditorManager, PluginManager, LocalStorage, Tools, window) { - EditorManager._beforeUnloadHandler = function () { - var msg; + function () { + var parse = function (time, defaultTime) { + var multiples = { + s: 1000, + m: 60000 + }; - Tools.each(EditorManager.get(), function (editor) { - // Store a draft for each editor instance - if (editor.plugins.autosave) { - editor.plugins.autosave.storeDraft(); - } + time = /^(\d+)([ms]?)$/.exec('' + (time || defaultTime)); - // Setup a return message if the editor is dirty - if (!msg && editor.isDirty() && editor.getParam("autosave_ask_before_unload", true)) { - msg = editor.translate("You have unsaved changes are you sure you want to navigate away?"); - } - }); + return (time[2] ? multiples[time[2]] : 1) * parseInt(time, 10); + }; - return msg; + return { + parse: parse }; + } +); +/** + * Settings.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 + */ - PluginManager.add('autosave', function (editor) { - var settings = editor.settings, prefix, started; +define( + 'tinymce.plugins.autosave.api.Settings', + [ + 'global!document', + 'tinymce.plugins.autosave.core.Time' + ], + function (document, Time) { + var shouldAskBeforeUnload = function (editor) { + return editor.getParam("autosave_ask_before_unload", true); + }; + + var getAutoSavePrefix = function (editor) { + var prefix = editor.getParam('autosave_prefix', 'tinymce-autosave-{path}{query}-{id}-'); - prefix = settings.autosave_prefix || 'tinymce-autosave-{path}{query}-{id}-'; prefix = prefix.replace(/\{path\}/g, document.location.pathname); prefix = prefix.replace(/\{query\}/g, document.location.search); prefix = prefix.replace(/\{id\}/g, editor.id); - function parseTime(time, defaultTime) { - var multipels = { - s: 1000, - m: 60000 - }; + return prefix; + }; + + var shouldRestoreWhenEmpty = function (editor) { + return editor.getParam('autosave_restore_when_empty', false); + }; + + var getAutoSaveInterval = function (editor) { + return Time.parse(editor.settings.autosave_interval, '30s'); + }; - time = /^(\d+)([ms]?)$/.exec('' + (time || defaultTime)); + var getAutoSaveRetention = function (editor) { + return Time.parse(editor.settings.autosave_retention, '20m'); + }; - return (time[2] ? multipels[time[2]] : 1) * parseInt(time, 10); + return { + shouldAskBeforeUnload: shouldAskBeforeUnload, + getAutoSavePrefix: getAutoSavePrefix, + shouldRestoreWhenEmpty: shouldRestoreWhenEmpty, + getAutoSaveInterval: getAutoSaveInterval, + getAutoSaveRetention: getAutoSaveRetention + }; + } +); +/** + * Storage.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.autosave.core.Storage', + [ + 'global!setInterval', + 'tinymce.core.util.LocalStorage', + 'tinymce.core.util.Tools', + 'tinymce.plugins.autosave.api.Events', + 'tinymce.plugins.autosave.api.Settings' + ], + function (setInterval, LocalStorage, Tools, Events, Settings) { + var isEmpty = function (editor, html) { + var forcedRootBlockName = editor.settings.forced_root_block; + + html = Tools.trim(typeof html === "undefined" ? editor.getBody().innerHTML : html); + + return html === '' || new RegExp( + '^<' + forcedRootBlockName + '[^>]*>((\u00a0| |[ \t]|]*>)+?|)<\/' + forcedRootBlockName + '>|
$', 'i' + ).test(html); + }; + + var hasDraft = function (editor) { + var time = parseInt(LocalStorage.getItem(Settings.getAutoSavePrefix(editor) + "time"), 10) || 0; + + if (new Date().getTime() - time > Settings.getAutoSaveRetention(editor)) { + removeDraft(editor, false); + return false; } - function hasDraft() { - var time = parseInt(LocalStorage.getItem(prefix + "time"), 10) || 0; + return true; + }; - if (new Date().getTime() - time > settings.autosave_retention) { - removeDraft(false); - return false; - } + var removeDraft = function (editor, fire) { + var prefix = Settings.getAutoSavePrefix(editor); - return true; + LocalStorage.removeItem(prefix + "draft"); + LocalStorage.removeItem(prefix + "time"); + + if (fire !== false) { + Events.fireRemoveDraft(editor); } + }; - function removeDraft(fire) { - LocalStorage.removeItem(prefix + "draft"); - LocalStorage.removeItem(prefix + "time"); + var storeDraft = function (editor) { + var prefix = Settings.getAutoSavePrefix(editor); - if (fire !== false) { - editor.fire('RemoveDraft'); - } + if (!isEmpty(editor) && editor.isDirty()) { + LocalStorage.setItem(prefix + "draft", editor.getContent({ format: 'raw', no_events: true })); + LocalStorage.setItem(prefix + "time", new Date().getTime()); + Events.fireStoreDraft(editor); } + }; - function storeDraft() { - if (!isEmpty() && editor.isDirty()) { - LocalStorage.setItem(prefix + "draft", editor.getContent({ format: 'raw', no_events: true })); - LocalStorage.setItem(prefix + "time", new Date().getTime()); - editor.fire('StoreDraft'); - } + var restoreDraft = function (editor) { + var prefix = Settings.getAutoSavePrefix(editor); + + if (hasDraft(editor)) { + editor.setContent(LocalStorage.getItem(prefix + "draft"), { format: 'raw' }); + Events.fireRestoreDraft(editor); } + }; - function restoreDraft() { - if (hasDraft()) { - editor.setContent(LocalStorage.getItem(prefix + "draft"), { format: 'raw' }); - editor.fire('RestoreDraft'); - } + var startStoreDraft = function (editor, started) { + var interval = Settings.getAutoSaveInterval(editor); + + if (!started.get()) { + setInterval(function () { + if (!editor.removed) { + storeDraft(editor); + } + }, interval); + + started.set(true); } + }; - function startStoreDraft() { - if (!started) { - setInterval(function () { - if (!editor.removed) { - storeDraft(); - } - }, settings.autosave_interval); + var restoreLastDraft = function (editor) { + editor.undoManager.transact(function () { + restoreDraft(editor); + removeDraft(editor); + }); - started = true; + editor.focus(); + }; + + return { + isEmpty: isEmpty, + hasDraft: hasDraft, + removeDraft: removeDraft, + storeDraft: storeDraft, + restoreDraft: restoreDraft, + startStoreDraft: startStoreDraft, + restoreLastDraft: restoreLastDraft + }; + } +); +/** + * Api.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.autosave.api.Api', + [ + 'tinymce.plugins.autosave.core.Storage' + ], + function (Storage) { + // Inlined the curry function since adding Fun without tree shaking to every plugin would produce a lot of bloat + var curry = function (f, editor) { + return function () { + var args = Array.prototype.slice.call(arguments); + return f.apply(null, [editor].concat(args)); + }; + }; + + var get = function (editor) { + return { + hasDraft: curry(Storage.hasDraft, editor), + storeDraft: curry(Storage.storeDraft, editor), + restoreDraft: curry(Storage.restoreDraft, editor), + removeDraft: curry(Storage.removeDraft, editor), + isEmpty: curry(Storage.isEmpty, editor) + }; + }; + + return { + get: get + }; + } +); +defineGlobal("global!window", window); +/** + * 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.EditorManager', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.EditorManager'); + } +); + +/** + * BeforeUnload.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.autosave.core.BeforeUnload', + [ + 'global!window', + 'tinymce.core.EditorManager', + 'tinymce.core.util.Tools', + 'tinymce.plugins.autosave.api.Settings' + ], + function (window, EditorManager, Tools, Settings) { + EditorManager._beforeUnloadHandler = function () { + var msg; + + Tools.each(EditorManager.get(), function (editor) { + // Store a draft for each editor instance + if (editor.plugins.autosave) { + editor.plugins.autosave.storeDraft(); } - } - settings.autosave_interval = parseTime(settings.autosave_interval, '30s'); - settings.autosave_retention = parseTime(settings.autosave_retention, '20m'); + // Setup a return message if the editor is dirty + if (!msg && editor.isDirty() && Settings.shouldAskBeforeUnload(editor)) { + msg = editor.translate("You have unsaved changes are you sure you want to navigate away?"); + } + }); + + return msg; + }; + + var setup = function (editor) { + window.onbeforeunload = EditorManager._beforeUnloadHandler; + }; + + return { + setup: setup + }; + } +); +/** + * Buttons.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 + */ - function postRender() { - var self = this; +define( + 'tinymce.plugins.autosave.ui.Buttons', + [ + 'tinymce.plugins.autosave.core.Storage' + ], + function (Storage) { + var postRender = function (editor, started) { + return function (e) { + var ctrl = e.control; - self.disabled(!hasDraft()); + ctrl.disabled(!Storage.hasDraft(editor)); editor.on('StoreDraft RestoreDraft RemoveDraft', function () { - self.disabled(!hasDraft()); + ctrl.disabled(!Storage.hasDraft(editor)); }); - startStoreDraft(); - } - - function restoreLastDraft() { - editor.undoManager.beforeChange(); - restoreDraft(); - removeDraft(); - editor.undoManager.add(); - } + // TODO: Investigate why this is only done on postrender that would + // make the feature broken if only the menu item was rendered since + // it is rendered when the menu appears + Storage.startStoreDraft(editor, started); + }; + }; + var register = function (editor, started) { editor.addButton('restoredraft', { title: 'Restore last draft', - onclick: restoreLastDraft, - onPostRender: postRender + onclick: function () { + Storage.restoreLastDraft(editor); + }, + onPostRender: postRender(editor, started) }); editor.addMenuItem('restoredraft', { text: 'Restore last draft', - onclick: restoreLastDraft, - onPostRender: postRender, + onclick: function () { + Storage.restoreLastDraft(editor); + }, + onPostRender: postRender(editor, started), context: 'file' }); + }; - function isEmpty(html) { - var forcedRootBlockName = editor.settings.forced_root_block; - - html = Tools.trim(typeof html == "undefined" ? editor.getBody().innerHTML : html); - - return html === '' || new RegExp( - '^<' + forcedRootBlockName + '[^>]*>((\u00a0| |[ \t]|]*>)+?|)<\/' + forcedRootBlockName + '>|
$', 'i' - ).test(html); - } - - if (editor.settings.autosave_restore_when_empty !== false) { - editor.on('init', function () { - if (hasDraft() && isEmpty()) { - restoreDraft(); - } - }); + return { + register: register + }; + } +); +/** + * Plugin.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 + */ - editor.on('saveContent', function () { - removeDraft(); - }); - } +/** + * This class contains all core logic for the autosave plugin. + * + * @class tinymce.autosave.Plugin + * @private + */ +define( + 'tinymce.plugins.autosave.Plugin', + [ + 'ephox.katamari.api.Cell', + 'tinymce.core.PluginManager', + 'tinymce.plugins.autosave.api.Api', + 'tinymce.plugins.autosave.core.BeforeUnload', + 'tinymce.plugins.autosave.ui.Buttons' + ], + function (Cell, PluginManager, Api, BeforeUnload, Buttons) { + PluginManager.add('autosave', function (editor) { + var started = Cell(false); - window.onbeforeunload = EditorManager._beforeUnloadHandler; + BeforeUnload.setup(editor); + Buttons.register(editor, started); - this.hasDraft = hasDraft; - this.storeDraft = storeDraft; - this.restoreDraft = restoreDraft; - this.removeDraft = removeDraft; - this.isEmpty = isEmpty; + return Api.get(editor); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/autosave/plugin.min.js b/media/vendor/tinymce/plugins/autosave/plugin.min.js index 447ddf8361e01..a9e6b319af3ef 100644 --- a/media/vendor/tinymce/plugins/autosave/plugin.min.js +++ b/media/vendor/tinymce/plugins/autosave/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;iq.autosave_retention)||(h(!1),!1)}function h(a){c.removeItem(o+"draft"),c.removeItem(o+"time"),a!==!1&&b.fire("RemoveDraft")}function i(){!n()&&b.isDirty()&&(c.setItem(o+"draft",b.getContent({format:"raw",no_events:!0})),c.setItem(o+"time",(new Date).getTime()),b.fire("StoreDraft"))}function j(){g()&&(b.setContent(c.getItem(o+"draft"),{format:"raw"}),b.fire("RestoreDraft"))}function k(){p||(setInterval(function(){b.removed||i()},q.autosave_interval),p=!0)}function l(){var a=this;a.disabled(!g()),b.on("StoreDraft RestoreDraft RemoveDraft",function(){a.disabled(!g())}),k()}function m(){b.undoManager.beforeChange(),j(),h(),b.undoManager.add()}function n(a){var c=b.settings.forced_root_block;return a=d.trim("undefined"==typeof a?b.getBody().innerHTML:a),""===a||new RegExp("^<"+c+"[^>]*>((\xa0| |[ \t]|]*>)+?|)|
$","i").test(a)}var o,p,q=b.settings;o=q.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",o=o.replace(/\{path\}/g,document.location.pathname),o=o.replace(/\{query\}/g,document.location.search),o=o.replace(/\{id\}/g,b.id),q.autosave_interval=f(q.autosave_interval,"30s"),q.autosave_retention=f(q.autosave_retention,"20m"),b.addButton("restoredraft",{title:"Restore last draft",onclick:m,onPostRender:l}),b.addMenuItem("restoredraft",{text:"Restore last draft",onclick:m,onPostRender:l,context:"file"}),b.settings.autosave_restore_when_empty!==!1&&(b.on("init",function(){g()&&n()&&j()}),b.on("saveContent",function(){h()})),e.onbeforeunload=a._beforeUnloadHandler,this.hasDraft=g,this.storeDraft=i,this.restoreDraft=j,this.removeDraft=h,this.isEmpty=n}),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]*>((\xa0| |[ \t]|]*>)+?|)|
$","i").test(b)},g=function(a){var c=parseInt(b.getItem(e.getAutoSavePrefix(a)+"time"),10)||0;return!((new Date).getTime()-c>e.getAutoSaveRetention(a))||(h(a,!1),!1)},h=function(a,c){var f=e.getAutoSavePrefix(a);b.removeItem(f+"draft"),b.removeItem(f+"time"),c!==!1&&d.fireRemoveDraft(a)},i=function(a){var c=e.getAutoSavePrefix(a);!f(a)&&a.isDirty()&&(b.setItem(c+"draft",a.getContent({format:"raw",no_events:!0})),b.setItem(c+"time",(new Date).getTime()),d.fireStoreDraft(a))},j=function(a){var c=e.getAutoSavePrefix(a);g(a)&&(a.setContent(b.getItem(c+"draft"),{format:"raw"}),d.fireRestoreDraft(a))},k=function(b,c){var d=e.getAutoSaveInterval(b);c.get()||(a(function(){b.removed||i(b)},d),c.set(!0))},l=function(a){a.undoManager.transact(function(){j(a),h(a)}),a.focus()};return{isEmpty:f,hasDraft:g,removeDraft:h,storeDraft:i,restoreDraft:j,startStoreDraft:k,restoreLastDraft:l}}),g("3",["7"],function(a){var b=function(a,b){return function(){var c=Array.prototype.slice.call(arguments);return a.apply(null,[b].concat(c))}},c=function(c){return{hasDraft:b(a.hasDraft,c),storeDraft:b(a.storeDraft,c),restoreDraft:b(a.restoreDraft,c),removeDraft:b(a.removeDraft,c),isEmpty:b(a.isEmpty,c)}};return{get:c}}),h("8",window),g("9",["6"],function(a){return a("tinymce.EditorManager")}),g("4",["8","9","a","b"],function(a,b,c,d){b._beforeUnloadHandler=function(){var a;return c.each(b.get(),function(b){b.plugins.autosave&&b.plugins.autosave.storeDraft(),!a&&b.isDirty()&&d.shouldAskBeforeUnload(b)&&(a=b.translate("You have unsaved changes are you sure you want to navigate away?"))}),a};var e=function(c){a.onbeforeunload=b._beforeUnloadHandler};return{setup:e}}),g("5",["7"],function(a){var b=function(b,c){return function(d){var e=d.control;e.disabled(!a.hasDraft(b)),b.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!a.hasDraft(b))}),a.startStoreDraft(b,c)}},c=function(c,d){c.addButton("restoredraft",{title:"Restore last draft",onclick:function(){a.restoreLastDraft(c)},onPostRender:b(c,d)}),c.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){a.restoreLastDraft(c)},onPostRender:b(c,d),context:"file"})};return{register:c}}),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return b.add("autosave",function(b){var f=a(!1);return d.setup(b),e.register(b,f),c.get(b)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/bbcode/plugin.js b/media/vendor/tinymce/plugins/bbcode/plugin.js index 7aafab3224dec..8e84e2182b09e 100644 --- a/media/vendor/tinymce/plugins/bbcode/plugin.js +++ b/media/vendor/tinymce/plugins/bbcode/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.bbcode.Plugin","tinymce.core.PluginManager","tinymce.core.util.Tools","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.bbcode.Plugin","tinymce.core.PluginManager","tinymce.plugins.bbcode.core.Convert","global!tinymce.util.Tools.resolve","tinymce.core.util.Tools"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -125,7 +125,7 @@ define( ); /** - * Plugin.js + * Convert.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,127 +134,129 @@ define( * Contributing: http://www.tinymce.com/contributing */ +define( + 'tinymce.plugins.bbcode.core.Convert', + [ + 'tinymce.core.util.Tools' + ], + function (Tools) { + var html2bbcode = function (s) { + s = Tools.trim(s); + + var rep = function (re, str) { + s = s.replace(re, str); + }; + + // example: to [b] + rep(/(.*?)<\/a>/gi, "[url=$1]$2[/url]"); + rep(/(.*?)<\/font>/gi, "[code][color=$1]$2[/color][/code]"); + rep(/(.*?)<\/font>/gi, "[quote][color=$1]$2[/color][/quote]"); + rep(/(.*?)<\/font>/gi, "[code][color=$1]$2[/color][/code]"); + rep(/(.*?)<\/font>/gi, "[quote][color=$1]$2[/color][/quote]"); + rep(/(.*?)<\/span>/gi, "[color=$1]$2[/color]"); + rep(/(.*?)<\/font>/gi, "[color=$1]$2[/color]"); + rep(/(.*?)<\/span>/gi, "[size=$1]$2[/size]"); + rep(/(.*?)<\/font>/gi, "$1"); + rep(//gi, "[img]$1[/img]"); + rep(/(.*?)<\/span>/gi, "[code]$1[/code]"); + rep(/(.*?)<\/span>/gi, "[quote]$1[/quote]"); + rep(/(.*?)<\/strong>/gi, "[code][b]$1[/b][/code]"); + rep(/(.*?)<\/strong>/gi, "[quote][b]$1[/b][/quote]"); + rep(/(.*?)<\/em>/gi, "[code][i]$1[/i][/code]"); + rep(/(.*?)<\/em>/gi, "[quote][i]$1[/i][/quote]"); + rep(/(.*?)<\/u>/gi, "[code][u]$1[/u][/code]"); + rep(/(.*?)<\/u>/gi, "[quote][u]$1[/u][/quote]"); + rep(/<\/(strong|b)>/gi, "[/b]"); + rep(/<(strong|b)>/gi, "[b]"); + rep(/<\/(em|i)>/gi, "[/i]"); + rep(/<(em|i)>/gi, "[i]"); + rep(/<\/u>/gi, "[/u]"); + rep(/(.*?)<\/span>/gi, "[u]$1[/u]"); + rep(//gi, "[u]"); + rep(/]*>/gi, "[quote]"); + rep(/<\/blockquote>/gi, "[/quote]"); + rep(/
/gi, "\n"); + rep(//gi, "\n"); + rep(/
/gi, "\n"); + rep(/

/gi, ""); + rep(/<\/p>/gi, "\n"); + rep(/ |\u00a0/gi, " "); + rep(/"/gi, "\""); + rep(/</gi, "<"); + rep(/>/gi, ">"); + rep(/&/gi, "&"); + + return s; + }; + + var bbcode2html = function (s) { + s = Tools.trim(s); + + var rep = function (re, str) { + s = s.replace(re, str); + }; + + // example: [b] to + rep(/\n/gi, "
"); + rep(/\[b\]/gi, ""); + rep(/\[\/b\]/gi, ""); + rep(/\[i\]/gi, ""); + rep(/\[\/i\]/gi, ""); + rep(/\[u\]/gi, ""); + rep(/\[\/u\]/gi, ""); + rep(/\[url=([^\]]+)\](.*?)\[\/url\]/gi, "$2"); + rep(/\[url\](.*?)\[\/url\]/gi, "$1"); + rep(/\[img\](.*?)\[\/img\]/gi, ""); + rep(/\[color=(.*?)\](.*?)\[\/color\]/gi, "$2"); + rep(/\[code\](.*?)\[\/code\]/gi, "$1 "); + rep(/\[quote.*?\](.*?)\[\/quote\]/gi, "$1 "); + + return s; + }; + + return { + html2bbcode: html2bbcode, + bbcode2html: bbcode2html + }; + } +); /** - * This class contains all core logic for the bbcode plugin. + * Plugin.js * - * @class tinymce.bbcode.Plugin - * @private + * 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.bbcode.Plugin', [ 'tinymce.core.PluginManager', - 'tinymce.core.util.Tools' + 'tinymce.plugins.bbcode.core.Convert' ], - function (PluginManager, Tools) { + function (PluginManager, Convert) { PluginManager.add('bbcode', function () { return { - init: function (ed) { - var self = this, dialect = ed.getParam('bbcode_dialect', 'punbb').toLowerCase(); - - ed.on('beforeSetContent', function (e) { - e.content = self['_' + dialect + '_bbcode2html'](e.content); + init: function (editor) { + editor.on('beforeSetContent', function (e) { + e.content = Convert.bbcode2html(e.content); }); - ed.on('postProcess', function (e) { + editor.on('postProcess', function (e) { if (e.set) { - e.content = self['_' + dialect + '_bbcode2html'](e.content); + e.content = Convert.bbcode2html(e.content); } if (e.get) { - e.content = self['_' + dialect + '_html2bbcode'](e.content); + e.content = Convert.html2bbcode(e.content); } }); - }, - - getInfo: function () { - return { - longname: 'BBCode Plugin', - author: 'Ephox Corp', - authorurl: 'http://www.tinymce.com', - infourl: 'http://www.tinymce.com/wiki.php/Plugin:bbcode' - }; - }, - - // Private methods - - // HTML -> BBCode in PunBB dialect - _punbb_html2bbcode: function (s) { - s = Tools.trim(s); - - function rep(re, str) { - s = s.replace(re, str); - } - - // example: to [b] - rep(/(.*?)<\/a>/gi, "[url=$1]$2[/url]"); - rep(/(.*?)<\/font>/gi, "[code][color=$1]$2[/color][/code]"); - rep(/(.*?)<\/font>/gi, "[quote][color=$1]$2[/color][/quote]"); - rep(/(.*?)<\/font>/gi, "[code][color=$1]$2[/color][/code]"); - rep(/(.*?)<\/font>/gi, "[quote][color=$1]$2[/color][/quote]"); - rep(/(.*?)<\/span>/gi, "[color=$1]$2[/color]"); - rep(/(.*?)<\/font>/gi, "[color=$1]$2[/color]"); - rep(/(.*?)<\/span>/gi, "[size=$1]$2[/size]"); - rep(/(.*?)<\/font>/gi, "$1"); - rep(//gi, "[img]$1[/img]"); - rep(/(.*?)<\/span>/gi, "[code]$1[/code]"); - rep(/(.*?)<\/span>/gi, "[quote]$1[/quote]"); - rep(/(.*?)<\/strong>/gi, "[code][b]$1[/b][/code]"); - rep(/(.*?)<\/strong>/gi, "[quote][b]$1[/b][/quote]"); - rep(/(.*?)<\/em>/gi, "[code][i]$1[/i][/code]"); - rep(/(.*?)<\/em>/gi, "[quote][i]$1[/i][/quote]"); - rep(/(.*?)<\/u>/gi, "[code][u]$1[/u][/code]"); - rep(/(.*?)<\/u>/gi, "[quote][u]$1[/u][/quote]"); - rep(/<\/(strong|b)>/gi, "[/b]"); - rep(/<(strong|b)>/gi, "[b]"); - rep(/<\/(em|i)>/gi, "[/i]"); - rep(/<(em|i)>/gi, "[i]"); - rep(/<\/u>/gi, "[/u]"); - rep(/(.*?)<\/span>/gi, "[u]$1[/u]"); - rep(//gi, "[u]"); - rep(/]*>/gi, "[quote]"); - rep(/<\/blockquote>/gi, "[/quote]"); - rep(/
/gi, "\n"); - rep(//gi, "\n"); - rep(/
/gi, "\n"); - rep(/

/gi, ""); - rep(/<\/p>/gi, "\n"); - rep(/ |\u00a0/gi, " "); - rep(/"/gi, "\""); - rep(/</gi, "<"); - rep(/>/gi, ">"); - rep(/&/gi, "&"); - - return s; - }, - - // BBCode -> HTML from PunBB dialect - _punbb_bbcode2html: function (s) { - s = Tools.trim(s); - - function rep(re, str) { - s = s.replace(re, str); - } - - // example: [b] to - rep(/\n/gi, "
"); - rep(/\[b\]/gi, ""); - rep(/\[\/b\]/gi, ""); - rep(/\[i\]/gi, ""); - rep(/\[\/i\]/gi, ""); - rep(/\[u\]/gi, ""); - rep(/\[\/u\]/gi, ""); - rep(/\[url=([^\]]+)\](.*?)\[\/url\]/gi, "$2"); - rep(/\[url\](.*?)\[\/url\]/gi, "$1"); - rep(/\[img\](.*?)\[\/img\]/gi, ""); - rep(/\[color=(.*?)\](.*?)\[\/color\]/gi, "$2"); - rep(/\[code\](.*?)\[\/code\]/gi, "$1 "); - rep(/\[quote.*?\](.*?)\[\/quote\]/gi, "$1 "); - - return s; } }; }); + return function () { }; } ); diff --git a/media/vendor/tinymce/plugins/bbcode/plugin.min.js b/media/vendor/tinymce/plugins/bbcode/plugin.min.js index 966358a0d26f4..4f60d157dce38 100644 --- a/media/vendor/tinymce/plugins/bbcode/plugin.min.js +++ b/media/vendor/tinymce/plugins/bbcode/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(.*?)<\/a>/gi,"[url=$1]$2[/url]"),c(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/(.*?)<\/span>/gi,"[color=$1]$2[/color]"),c(/(.*?)<\/font>/gi,"[color=$1]$2[/color]"),c(/(.*?)<\/span>/gi,"[size=$1]$2[/size]"),c(/(.*?)<\/font>/gi,"$1"),c(//gi,"[img]$1[/img]"),c(/(.*?)<\/span>/gi,"[code]$1[/code]"),c(/(.*?)<\/span>/gi,"[quote]$1[/quote]"),c(/(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),c(/(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),c(/(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),c(/(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),c(/(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),c(/(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),c(/<\/(strong|b)>/gi,"[/b]"),c(/<(strong|b)>/gi,"[b]"),c(/<\/(em|i)>/gi,"[/i]"),c(/<(em|i)>/gi,"[i]"),c(/<\/u>/gi,"[/u]"),c(/(.*?)<\/span>/gi,"[u]$1[/u]"),c(//gi,"[u]"),c(/]*>/gi,"[quote]"),c(/<\/blockquote>/gi,"[/quote]"),c(/
/gi,"\n"),c(//gi,"\n"),c(/
/gi,"\n"),c(/

/gi,""),c(/<\/p>/gi,"\n"),c(/ |\u00a0/gi," "),c(/"/gi,'"'),c(/</gi,"<"),c(/>/gi,">"),c(/&/gi,"&"),a},_punbb_bbcode2html:function(a){function c(b,c){a=a.replace(b,c)}return a=b.trim(a),c(/\n/gi,"
"),c(/\[b\]/gi,""),c(/\[\/b\]/gi,""),c(/\[i\]/gi,""),c(/\[\/i\]/gi,""),c(/\[u\]/gi,""),c(/\[\/u\]/gi,""),c(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'$2'),c(/\[url\](.*?)\[\/url\]/gi,'$1'),c(/\[img\](.*?)\[\/img\]/gi,''),c(/\[color=(.*?)\](.*?)\[\/color\]/gi,'$2'),c(/\[code\](.*?)\[\/code\]/gi,'$1 '),c(/\[quote.*?\](.*?)\[\/quote\]/gi,'$1 '),a}}}),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(.*?)<\/a>/gi,"[url=$1]$2[/url]"),c(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/(.*?)<\/span>/gi,"[color=$1]$2[/color]"),c(/(.*?)<\/font>/gi,"[color=$1]$2[/color]"),c(/(.*?)<\/span>/gi,"[size=$1]$2[/size]"),c(/(.*?)<\/font>/gi,"$1"),c(//gi,"[img]$1[/img]"),c(/(.*?)<\/span>/gi,"[code]$1[/code]"),c(/(.*?)<\/span>/gi,"[quote]$1[/quote]"),c(/(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),c(/(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),c(/(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),c(/(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),c(/(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),c(/(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),c(/<\/(strong|b)>/gi,"[/b]"),c(/<(strong|b)>/gi,"[b]"),c(/<\/(em|i)>/gi,"[/i]"),c(/<(em|i)>/gi,"[i]"),c(/<\/u>/gi,"[/u]"),c(/(.*?)<\/span>/gi,"[u]$1[/u]"),c(//gi,"[u]"),c(/]*>/gi,"[quote]"),c(/<\/blockquote>/gi,"[/quote]"),c(/
/gi,"\n"),c(//gi,"\n"),c(/
/gi,"\n"),c(/

/gi,""),c(/<\/p>/gi,"\n"),c(/ |\u00a0/gi," "),c(/"/gi,'"'),c(/</gi,"<"),c(/>/gi,">"),c(/&/gi,"&"),b},c=function(b){b=a.trim(b);var c=function(a,c){b=b.replace(a,c)};return c(/\n/gi,"
"),c(/\[b\]/gi,""),c(/\[\/b\]/gi,""),c(/\[i\]/gi,""),c(/\[\/i\]/gi,""),c(/\[u\]/gi,""),c(/\[\/u\]/gi,""),c(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'$2'),c(/\[url\](.*?)\[\/url\]/gi,'$1'),c(/\[img\](.*?)\[\/img\]/gi,''),c(/\[color=(.*?)\](.*?)\[\/color\]/gi,'$2'),c(/\[code\](.*?)\[\/code\]/gi,'$1 '),c(/\[quote.*?\](.*?)\[\/quote\]/gi,'$1 '),b};return{html2bbcode:b,bbcode2html:c}}),g("0",["1","2"],function(a,b){return a.add("bbcode",function(){return{init:function(a){a.on("beforeSetContent",function(a){a.content=b.bbcode2html(a.content)}),a.on("postProcess",function(a){a.set&&(a.content=b.bbcode2html(a.content)),a.get&&(a.content=b.html2bbcode(a.content))})}}}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/charmap/plugin.js b/media/vendor/tinymce/plugins/charmap/plugin.js index 3e947c7b58e6a..facdb72d4145a 100644 --- a/media/vendor/tinymce/plugins/charmap/plugin.js +++ b/media/vendor/tinymce/plugins/charmap/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.charmap.Plugin","tinymce.core.PluginManager","tinymce.core.util.Tools","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.charmap.Plugin","tinymce.core.PluginManager","tinymce.plugins.charmap.api.Api","tinymce.plugins.charmap.api.Commands","tinymce.plugins.charmap.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.charmap.core.Actions","tinymce.plugins.charmap.core.CharMap","tinymce.plugins.charmap.ui.Dialog","tinymce.plugins.charmap.api.Events","tinymce.core.util.Tools","tinymce.plugins.charmap.api.Settings","tinymce.plugins.charmap.ui.GridHtml"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -104,6 +104,57 @@ define( } ); +/** + * Events.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.charmap.api.Events', + [ + ], + function () { + var fireInsertCustomChar = function (editor, chr) { + return editor.fire('insertCustomChar', { chr: chr }); + }; + + return { + fireInsertCustomChar: fireInsertCustomChar + }; + } +); + +/** + * Actions.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.charmap.core.Actions', + [ + 'tinymce.plugins.charmap.api.Events' + ], + function (Events) { + var insertChar = function (editor, chr) { + var evtChr = Events.fireInsertCustomChar(editor, chr).chr; + editor.execCommand('mceInsertContent', false, evtChr); + }; + + return { + insertChar: insertChar + }; + } +); /** * ResolveGlobal.js * @@ -125,7 +176,7 @@ define( ); /** - * Plugin.js + * Settings.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,463 +185,613 @@ define( * Contributing: http://www.tinymce.com/contributing */ +define( + 'tinymce.plugins.charmap.api.Settings', + [ + ], + function () { + var getCharMap = function (editor) { + return editor.settings.charmap; + }; + + var getCharMapAppend = function (editor) { + return editor.settings.charmap_append; + }; + + return { + getCharMap: getCharMap, + getCharMapAppend: getCharMapAppend + }; + } +); /** - * This class contains all core logic for the charmap plugin. + * CharMap.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * - * @class tinymce.charmap.Plugin - * @private + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing */ + define( - 'tinymce.plugins.charmap.Plugin', + 'tinymce.plugins.charmap.core.CharMap', [ - 'tinymce.core.PluginManager', - 'tinymce.core.util.Tools' + 'tinymce.core.util.Tools', + 'tinymce.plugins.charmap.api.Settings' ], - function (PluginManager, Tools) { - PluginManager.add('charmap', function (editor) { - var isArray = Tools.isArray; - - function getDefaultCharMap() { - return [ - ['160', 'no-break space'], - ['173', 'soft hyphen'], - ['34', 'quotation mark'], - // finance - ['162', 'cent sign'], - ['8364', 'euro sign'], - ['163', 'pound sign'], - ['165', 'yen sign'], - // signs - ['169', 'copyright sign'], - ['174', 'registered sign'], - ['8482', 'trade mark sign'], - ['8240', 'per mille sign'], - ['181', 'micro sign'], - ['183', 'middle dot'], - ['8226', 'bullet'], - ['8230', 'three dot leader'], - ['8242', 'minutes / feet'], - ['8243', 'seconds / inches'], - ['167', 'section sign'], - ['182', 'paragraph sign'], - ['223', 'sharp s / ess-zed'], - // quotations - ['8249', 'single left-pointing angle quotation mark'], - ['8250', 'single right-pointing angle quotation mark'], - ['171', 'left pointing guillemet'], - ['187', 'right pointing guillemet'], - ['8216', 'left single quotation mark'], - ['8217', 'right single quotation mark'], - ['8220', 'left double quotation mark'], - ['8221', 'right double quotation mark'], - ['8218', 'single low-9 quotation mark'], - ['8222', 'double low-9 quotation mark'], - ['60', 'less-than sign'], - ['62', 'greater-than sign'], - ['8804', 'less-than or equal to'], - ['8805', 'greater-than or equal to'], - ['8211', 'en dash'], - ['8212', 'em dash'], - ['175', 'macron'], - ['8254', 'overline'], - ['164', 'currency sign'], - ['166', 'broken bar'], - ['168', 'diaeresis'], - ['161', 'inverted exclamation mark'], - ['191', 'turned question mark'], - ['710', 'circumflex accent'], - ['732', 'small tilde'], - ['176', 'degree sign'], - ['8722', 'minus sign'], - ['177', 'plus-minus sign'], - ['247', 'division sign'], - ['8260', 'fraction slash'], - ['215', 'multiplication sign'], - ['185', 'superscript one'], - ['178', 'superscript two'], - ['179', 'superscript three'], - ['188', 'fraction one quarter'], - ['189', 'fraction one half'], - ['190', 'fraction three quarters'], - // math / logical - ['402', 'function / florin'], - ['8747', 'integral'], - ['8721', 'n-ary sumation'], - ['8734', 'infinity'], - ['8730', 'square root'], - ['8764', 'similar to'], - ['8773', 'approximately equal to'], - ['8776', 'almost equal to'], - ['8800', 'not equal to'], - ['8801', 'identical to'], - ['8712', 'element of'], - ['8713', 'not an element of'], - ['8715', 'contains as member'], - ['8719', 'n-ary product'], - ['8743', 'logical and'], - ['8744', 'logical or'], - ['172', 'not sign'], - ['8745', 'intersection'], - ['8746', 'union'], - ['8706', 'partial differential'], - ['8704', 'for all'], - ['8707', 'there exists'], - ['8709', 'diameter'], - ['8711', 'backward difference'], - ['8727', 'asterisk operator'], - ['8733', 'proportional to'], - ['8736', 'angle'], - // undefined - ['180', 'acute accent'], - ['184', 'cedilla'], - ['170', 'feminine ordinal indicator'], - ['186', 'masculine ordinal indicator'], - ['8224', 'dagger'], - ['8225', 'double dagger'], - // alphabetical special chars - ['192', 'A - grave'], - ['193', 'A - acute'], - ['194', 'A - circumflex'], - ['195', 'A - tilde'], - ['196', 'A - diaeresis'], - ['197', 'A - ring above'], - ['256', 'A - macron'], - ['198', 'ligature AE'], - ['199', 'C - cedilla'], - ['200', 'E - grave'], - ['201', 'E - acute'], - ['202', 'E - circumflex'], - ['203', 'E - diaeresis'], - ['274', 'E - macron'], - ['204', 'I - grave'], - ['205', 'I - acute'], - ['206', 'I - circumflex'], - ['207', 'I - diaeresis'], - ['298', 'I - macron'], - ['208', 'ETH'], - ['209', 'N - tilde'], - ['210', 'O - grave'], - ['211', 'O - acute'], - ['212', 'O - circumflex'], - ['213', 'O - tilde'], - ['214', 'O - diaeresis'], - ['216', 'O - slash'], - ['332', 'O - macron'], - ['338', 'ligature OE'], - ['352', 'S - caron'], - ['217', 'U - grave'], - ['218', 'U - acute'], - ['219', 'U - circumflex'], - ['220', 'U - diaeresis'], - ['362', 'U - macron'], - ['221', 'Y - acute'], - ['376', 'Y - diaeresis'], - ['562', 'Y - macron'], - ['222', 'THORN'], - ['224', 'a - grave'], - ['225', 'a - acute'], - ['226', 'a - circumflex'], - ['227', 'a - tilde'], - ['228', 'a - diaeresis'], - ['229', 'a - ring above'], - ['257', 'a - macron'], - ['230', 'ligature ae'], - ['231', 'c - cedilla'], - ['232', 'e - grave'], - ['233', 'e - acute'], - ['234', 'e - circumflex'], - ['235', 'e - diaeresis'], - ['275', 'e - macron'], - ['236', 'i - grave'], - ['237', 'i - acute'], - ['238', 'i - circumflex'], - ['239', 'i - diaeresis'], - ['299', 'i - macron'], - ['240', 'eth'], - ['241', 'n - tilde'], - ['242', 'o - grave'], - ['243', 'o - acute'], - ['244', 'o - circumflex'], - ['245', 'o - tilde'], - ['246', 'o - diaeresis'], - ['248', 'o slash'], - ['333', 'o macron'], - ['339', 'ligature oe'], - ['353', 's - caron'], - ['249', 'u - grave'], - ['250', 'u - acute'], - ['251', 'u - circumflex'], - ['252', 'u - diaeresis'], - ['363', 'u - macron'], - ['253', 'y - acute'], - ['254', 'thorn'], - ['255', 'y - diaeresis'], - ['563', 'y - macron'], - ['913', 'Alpha'], - ['914', 'Beta'], - ['915', 'Gamma'], - ['916', 'Delta'], - ['917', 'Epsilon'], - ['918', 'Zeta'], - ['919', 'Eta'], - ['920', 'Theta'], - ['921', 'Iota'], - ['922', 'Kappa'], - ['923', 'Lambda'], - ['924', 'Mu'], - ['925', 'Nu'], - ['926', 'Xi'], - ['927', 'Omicron'], - ['928', 'Pi'], - ['929', 'Rho'], - ['931', 'Sigma'], - ['932', 'Tau'], - ['933', 'Upsilon'], - ['934', 'Phi'], - ['935', 'Chi'], - ['936', 'Psi'], - ['937', 'Omega'], - ['945', 'alpha'], - ['946', 'beta'], - ['947', 'gamma'], - ['948', 'delta'], - ['949', 'epsilon'], - ['950', 'zeta'], - ['951', 'eta'], - ['952', 'theta'], - ['953', 'iota'], - ['954', 'kappa'], - ['955', 'lambda'], - ['956', 'mu'], - ['957', 'nu'], - ['958', 'xi'], - ['959', 'omicron'], - ['960', 'pi'], - ['961', 'rho'], - ['962', 'final sigma'], - ['963', 'sigma'], - ['964', 'tau'], - ['965', 'upsilon'], - ['966', 'phi'], - ['967', 'chi'], - ['968', 'psi'], - ['969', 'omega'], - // symbols - ['8501', 'alef symbol'], - ['982', 'pi symbol'], - ['8476', 'real part symbol'], - ['978', 'upsilon - hook symbol'], - ['8472', 'Weierstrass p'], - ['8465', 'imaginary part'], - // arrows - ['8592', 'leftwards arrow'], - ['8593', 'upwards arrow'], - ['8594', 'rightwards arrow'], - ['8595', 'downwards arrow'], - ['8596', 'left right arrow'], - ['8629', 'carriage return'], - ['8656', 'leftwards double arrow'], - ['8657', 'upwards double arrow'], - ['8658', 'rightwards double arrow'], - ['8659', 'downwards double arrow'], - ['8660', 'left right double arrow'], - ['8756', 'therefore'], - ['8834', 'subset of'], - ['8835', 'superset of'], - ['8836', 'not a subset of'], - ['8838', 'subset of or equal to'], - ['8839', 'superset of or equal to'], - ['8853', 'circled plus'], - ['8855', 'circled times'], - ['8869', 'perpendicular'], - ['8901', 'dot operator'], - ['8968', 'left ceiling'], - ['8969', 'right ceiling'], - ['8970', 'left floor'], - ['8971', 'right floor'], - ['9001', 'left-pointing angle bracket'], - ['9002', 'right-pointing angle bracket'], - ['9674', 'lozenge'], - ['9824', 'black spade suit'], - ['9827', 'black club suit'], - ['9829', 'black heart suit'], - ['9830', 'black diamond suit'], - ['8194', 'en space'], - ['8195', 'em space'], - ['8201', 'thin space'], - ['8204', 'zero width non-joiner'], - ['8205', 'zero width joiner'], - ['8206', 'left-to-right mark'], - ['8207', 'right-to-left mark'] - ]; + function (Tools, Settings) { + var isArray = Tools.isArray; + + var getDefaultCharMap = function () { + return [ + ['160', 'no-break space'], + ['173', 'soft hyphen'], + ['34', 'quotation mark'], + // finance + ['162', 'cent sign'], + ['8364', 'euro sign'], + ['163', 'pound sign'], + ['165', 'yen sign'], + // signs + ['169', 'copyright sign'], + ['174', 'registered sign'], + ['8482', 'trade mark sign'], + ['8240', 'per mille sign'], + ['181', 'micro sign'], + ['183', 'middle dot'], + ['8226', 'bullet'], + ['8230', 'three dot leader'], + ['8242', 'minutes / feet'], + ['8243', 'seconds / inches'], + ['167', 'section sign'], + ['182', 'paragraph sign'], + ['223', 'sharp s / ess-zed'], + // quotations + ['8249', 'single left-pointing angle quotation mark'], + ['8250', 'single right-pointing angle quotation mark'], + ['171', 'left pointing guillemet'], + ['187', 'right pointing guillemet'], + ['8216', 'left single quotation mark'], + ['8217', 'right single quotation mark'], + ['8220', 'left double quotation mark'], + ['8221', 'right double quotation mark'], + ['8218', 'single low-9 quotation mark'], + ['8222', 'double low-9 quotation mark'], + ['60', 'less-than sign'], + ['62', 'greater-than sign'], + ['8804', 'less-than or equal to'], + ['8805', 'greater-than or equal to'], + ['8211', 'en dash'], + ['8212', 'em dash'], + ['175', 'macron'], + ['8254', 'overline'], + ['164', 'currency sign'], + ['166', 'broken bar'], + ['168', 'diaeresis'], + ['161', 'inverted exclamation mark'], + ['191', 'turned question mark'], + ['710', 'circumflex accent'], + ['732', 'small tilde'], + ['176', 'degree sign'], + ['8722', 'minus sign'], + ['177', 'plus-minus sign'], + ['247', 'division sign'], + ['8260', 'fraction slash'], + ['215', 'multiplication sign'], + ['185', 'superscript one'], + ['178', 'superscript two'], + ['179', 'superscript three'], + ['188', 'fraction one quarter'], + ['189', 'fraction one half'], + ['190', 'fraction three quarters'], + // math / logical + ['402', 'function / florin'], + ['8747', 'integral'], + ['8721', 'n-ary sumation'], + ['8734', 'infinity'], + ['8730', 'square root'], + ['8764', 'similar to'], + ['8773', 'approximately equal to'], + ['8776', 'almost equal to'], + ['8800', 'not equal to'], + ['8801', 'identical to'], + ['8712', 'element of'], + ['8713', 'not an element of'], + ['8715', 'contains as member'], + ['8719', 'n-ary product'], + ['8743', 'logical and'], + ['8744', 'logical or'], + ['172', 'not sign'], + ['8745', 'intersection'], + ['8746', 'union'], + ['8706', 'partial differential'], + ['8704', 'for all'], + ['8707', 'there exists'], + ['8709', 'diameter'], + ['8711', 'backward difference'], + ['8727', 'asterisk operator'], + ['8733', 'proportional to'], + ['8736', 'angle'], + // undefined + ['180', 'acute accent'], + ['184', 'cedilla'], + ['170', 'feminine ordinal indicator'], + ['186', 'masculine ordinal indicator'], + ['8224', 'dagger'], + ['8225', 'double dagger'], + // alphabetical special chars + ['192', 'A - grave'], + ['193', 'A - acute'], + ['194', 'A - circumflex'], + ['195', 'A - tilde'], + ['196', 'A - diaeresis'], + ['197', 'A - ring above'], + ['256', 'A - macron'], + ['198', 'ligature AE'], + ['199', 'C - cedilla'], + ['200', 'E - grave'], + ['201', 'E - acute'], + ['202', 'E - circumflex'], + ['203', 'E - diaeresis'], + ['274', 'E - macron'], + ['204', 'I - grave'], + ['205', 'I - acute'], + ['206', 'I - circumflex'], + ['207', 'I - diaeresis'], + ['298', 'I - macron'], + ['208', 'ETH'], + ['209', 'N - tilde'], + ['210', 'O - grave'], + ['211', 'O - acute'], + ['212', 'O - circumflex'], + ['213', 'O - tilde'], + ['214', 'O - diaeresis'], + ['216', 'O - slash'], + ['332', 'O - macron'], + ['338', 'ligature OE'], + ['352', 'S - caron'], + ['217', 'U - grave'], + ['218', 'U - acute'], + ['219', 'U - circumflex'], + ['220', 'U - diaeresis'], + ['362', 'U - macron'], + ['221', 'Y - acute'], + ['376', 'Y - diaeresis'], + ['562', 'Y - macron'], + ['222', 'THORN'], + ['224', 'a - grave'], + ['225', 'a - acute'], + ['226', 'a - circumflex'], + ['227', 'a - tilde'], + ['228', 'a - diaeresis'], + ['229', 'a - ring above'], + ['257', 'a - macron'], + ['230', 'ligature ae'], + ['231', 'c - cedilla'], + ['232', 'e - grave'], + ['233', 'e - acute'], + ['234', 'e - circumflex'], + ['235', 'e - diaeresis'], + ['275', 'e - macron'], + ['236', 'i - grave'], + ['237', 'i - acute'], + ['238', 'i - circumflex'], + ['239', 'i - diaeresis'], + ['299', 'i - macron'], + ['240', 'eth'], + ['241', 'n - tilde'], + ['242', 'o - grave'], + ['243', 'o - acute'], + ['244', 'o - circumflex'], + ['245', 'o - tilde'], + ['246', 'o - diaeresis'], + ['248', 'o slash'], + ['333', 'o macron'], + ['339', 'ligature oe'], + ['353', 's - caron'], + ['249', 'u - grave'], + ['250', 'u - acute'], + ['251', 'u - circumflex'], + ['252', 'u - diaeresis'], + ['363', 'u - macron'], + ['253', 'y - acute'], + ['254', 'thorn'], + ['255', 'y - diaeresis'], + ['563', 'y - macron'], + ['913', 'Alpha'], + ['914', 'Beta'], + ['915', 'Gamma'], + ['916', 'Delta'], + ['917', 'Epsilon'], + ['918', 'Zeta'], + ['919', 'Eta'], + ['920', 'Theta'], + ['921', 'Iota'], + ['922', 'Kappa'], + ['923', 'Lambda'], + ['924', 'Mu'], + ['925', 'Nu'], + ['926', 'Xi'], + ['927', 'Omicron'], + ['928', 'Pi'], + ['929', 'Rho'], + ['931', 'Sigma'], + ['932', 'Tau'], + ['933', 'Upsilon'], + ['934', 'Phi'], + ['935', 'Chi'], + ['936', 'Psi'], + ['937', 'Omega'], + ['945', 'alpha'], + ['946', 'beta'], + ['947', 'gamma'], + ['948', 'delta'], + ['949', 'epsilon'], + ['950', 'zeta'], + ['951', 'eta'], + ['952', 'theta'], + ['953', 'iota'], + ['954', 'kappa'], + ['955', 'lambda'], + ['956', 'mu'], + ['957', 'nu'], + ['958', 'xi'], + ['959', 'omicron'], + ['960', 'pi'], + ['961', 'rho'], + ['962', 'final sigma'], + ['963', 'sigma'], + ['964', 'tau'], + ['965', 'upsilon'], + ['966', 'phi'], + ['967', 'chi'], + ['968', 'psi'], + ['969', 'omega'], + // symbols + ['8501', 'alef symbol'], + ['982', 'pi symbol'], + ['8476', 'real part symbol'], + ['978', 'upsilon - hook symbol'], + ['8472', 'Weierstrass p'], + ['8465', 'imaginary part'], + // arrows + ['8592', 'leftwards arrow'], + ['8593', 'upwards arrow'], + ['8594', 'rightwards arrow'], + ['8595', 'downwards arrow'], + ['8596', 'left right arrow'], + ['8629', 'carriage return'], + ['8656', 'leftwards double arrow'], + ['8657', 'upwards double arrow'], + ['8658', 'rightwards double arrow'], + ['8659', 'downwards double arrow'], + ['8660', 'left right double arrow'], + ['8756', 'therefore'], + ['8834', 'subset of'], + ['8835', 'superset of'], + ['8836', 'not a subset of'], + ['8838', 'subset of or equal to'], + ['8839', 'superset of or equal to'], + ['8853', 'circled plus'], + ['8855', 'circled times'], + ['8869', 'perpendicular'], + ['8901', 'dot operator'], + ['8968', 'left ceiling'], + ['8969', 'right ceiling'], + ['8970', 'left floor'], + ['8971', 'right floor'], + ['9001', 'left-pointing angle bracket'], + ['9002', 'right-pointing angle bracket'], + ['9674', 'lozenge'], + ['9824', 'black spade suit'], + ['9827', 'black club suit'], + ['9829', 'black heart suit'], + ['9830', 'black diamond suit'], + ['8194', 'en space'], + ['8195', 'em space'], + ['8201', 'thin space'], + ['8204', 'zero width non-joiner'], + ['8205', 'zero width joiner'], + ['8206', 'left-to-right mark'], + ['8207', 'right-to-left mark'] + ]; + }; + + var charmapFilter = function (charmap) { + return Tools.grep(charmap, function (item) { + return isArray(item) && item.length === 2; + }); + }; + + var getCharsFromSetting = function (settingValue) { + if (isArray(settingValue)) { + return [].concat(charmapFilter(settingValue)); } - function charmapFilter(charmap) { - return Tools.grep(charmap, function (item) { - return isArray(item) && item.length == 2; - }); + if (typeof settingValue === "function") { + return settingValue(); } - function getCharsFromSetting(settingValue) { - if (isArray(settingValue)) { - return [].concat(charmapFilter(settingValue)); - } + return []; + }; - if (typeof settingValue == "function") { - return settingValue(); - } + var extendCharMap = function (editor, charmap) { + var userCharMap = Settings.getCharMap(editor); + if (userCharMap) { + charmap = getCharsFromSetting(userCharMap); + } - return []; + var userCharMapAppend = Settings.getCharMapAppend(editor); + if (userCharMapAppend) { + return [].concat(charmap).concat(getCharsFromSetting(userCharMapAppend)); } - function extendCharMap(charmap) { - var settings = editor.settings; + return charmap; + }; - if (settings.charmap) { - charmap = getCharsFromSetting(settings.charmap); - } + var getCharMap = function (editor) { + return extendCharMap(editor, getDefaultCharMap()); + }; - if (settings.charmap_append) { - return [].concat(charmap).concat(getCharsFromSetting(settings.charmap_append)); - } + return { + getCharMap: getCharMap + }; + } +); +/** + * Api.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 + */ - return charmap; - } +define( + 'tinymce.plugins.charmap.api.Api', + [ + 'tinymce.plugins.charmap.core.Actions', + 'tinymce.plugins.charmap.core.CharMap' + ], + function (Actions, CharMap) { + var get = function (editor) { + var getCharMap = function () { + return CharMap.getCharMap(editor); + }; - function getCharMap() { - return extendCharMap(getDefaultCharMap()); - } + var insertChar = function (chr) { + Actions.insertChar(editor, chr); + }; - function insertChar(chr) { - editor.fire('insertCustomChar', { chr: chr }).chr; - editor.execCommand('mceInsertContent', false, chr); - } + return { + getCharMap: getCharMap, + insertChar: insertChar + }; + }; - function showDialog() { - var gridHtml, x, y, win; + return { + get: get + }; + } +); - function getParentTd(elm) { - while (elm) { - if (elm.nodeName == 'TD') { - return elm; - } - elm = elm.parentNode; +/** + * GridHtml.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.charmap.ui.GridHtml', + [ + ], + function () { + var getHtml = function (charmap) { + var gridHtml, x, y; + var width = Math.min(charmap.length, 25); + var height = Math.ceil(charmap.length / width); + + gridHtml = ''; + + for (y = 0; y < height; y++) { + gridHtml += ''; + + for (x = 0; x < width; x++) { + var index = y * width + x; + if (index < charmap.length) { + var chr = charmap[index]; + var chrText = chr ? String.fromCharCode(parseInt(chr[0], 10)) : ' '; + + gridHtml += ( + '' + ); + } else { + gridHtml += ''; +define( + 'tinymce.plugins.charmap.ui.Dialog', + [ + 'tinymce.plugins.charmap.core.Actions', + 'tinymce.plugins.charmap.core.CharMap', + 'tinymce.plugins.charmap.ui.GridHtml' + ], + function (Actions, CharMap, GridHtml) { + var getParentTd = function (elm) { + while (elm) { + if (elm.nodeName === 'TD') { + return elm; } - gridHtml += ''; + elm = elm.parentNode; + } + }; + + var open = function (editor) { + var win; - var charMapPanel = { - type: 'container', - html: gridHtml, - onclick: function (e) { - var target = e.target; + var charMapPanel = { + type: 'container', + html: GridHtml.getHtml(CharMap.getCharMap(editor)), + onclick: function (e) { + var target = e.target; - if (/^(TD|DIV)$/.test(target.nodeName)) { - var charDiv = getParentTd(target).firstChild; - if (charDiv && charDiv.hasAttribute('data-chr')) { - insertChar(charDiv.getAttribute('data-chr')); + if (/^(TD|DIV)$/.test(target.nodeName)) { + var charDiv = getParentTd(target).firstChild; + if (charDiv && charDiv.hasAttribute('data-chr')) { + Actions.insertChar(editor, charDiv.getAttribute('data-chr')); - if (!e.ctrlKey) { - win.close(); - } + if (!e.ctrlKey) { + win.close(); } } - }, - onmouseover: function (e) { - var td = getParentTd(e.target); - - if (td && td.firstChild) { - win.find('#preview').text(td.firstChild.firstChild.data); - win.find('#previewTitle').text(td.title); - } else { - win.find('#preview').text(' '); - win.find('#previewTitle').text(' '); - } } - }; - - win = editor.windowManager.open({ - title: "Special character", - spacing: 10, - padding: 10, - items: [ - charMapPanel, - { - type: 'container', - layout: 'flex', - direction: 'column', - align: 'center', - spacing: 5, - minWidth: 160, - minHeight: 160, - items: [ - { - type: 'label', - name: 'preview', - text: ' ', - style: 'font-size: 40px; text-align: center', - border: 1, - minWidth: 140, - minHeight: 80 - }, - { - type: 'spacer', - minHeight: 20 - }, - { - type: 'label', - name: 'previewTitle', - text: ' ', - style: 'white-space: pre-wrap;', - border: 1, - minWidth: 140 - } - ] - } - ], - buttons: [ - { - text: "Close", onclick: function () { - win.close(); + }, + onmouseover: function (e) { + var td = getParentTd(e.target); + + if (td && td.firstChild) { + win.find('#preview').text(td.firstChild.firstChild.data); + win.find('#previewTitle').text(td.title); + } else { + win.find('#preview').text(' '); + win.find('#previewTitle').text(' '); + } + } + }; + + win = editor.windowManager.open({ + title: "Special character", + spacing: 10, + padding: 10, + items: [ + charMapPanel, + { + type: 'container', + layout: 'flex', + direction: 'column', + align: 'center', + spacing: 5, + minWidth: 160, + minHeight: 160, + items: [ + { + type: 'label', + name: 'preview', + text: ' ', + style: 'font-size: 40px; text-align: center', + border: 1, + minWidth: 140, + minHeight: 80 + }, + { + type: 'spacer', + minHeight: 20 + }, + { + type: 'label', + name: 'previewTitle', + text: ' ', + style: 'white-space: pre-wrap;', + border: 1, + minWidth: 140 } + ] + } + ], + buttons: [ + { + text: "Close", onclick: function () { + win.close(); } - ] - }); - } + } + ] + }); + }; + + return { + open: open + }; + } +); +/** + * 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 + */ - editor.addCommand('mceShowCharmap', showDialog); +define( + 'tinymce.plugins.charmap.api.Commands', + [ + 'tinymce.plugins.charmap.ui.Dialog' + ], + function (Dialog) { + var register = function (editor) { + editor.addCommand('mceShowCharmap', function () { + Dialog.open(editor); + }); + }; + + return { + register: register + }; + } +); +/** + * Buttons.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.charmap.ui.Buttons', + [ + ], + function () { + var register = function (editor) { editor.addButton('charmap', { icon: 'charmap', tooltip: 'Special character', @@ -603,11 +804,37 @@ define( cmd: 'mceShowCharmap', context: 'insert' }); + }; - return { - getCharMap: getCharMap, - insertChar: insertChar - }; + return { + register: register + }; + } +); +/** + * Plugin.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.charmap.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.charmap.api.Api', + 'tinymce.plugins.charmap.api.Commands', + 'tinymce.plugins.charmap.ui.Buttons' + ], + function (PluginManager, Api, Commands, Buttons) { + PluginManager.add('charmap', function (editor) { + Commands.register(editor); + Buttons.register(editor); + + return Api.get(editor); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/charmap/plugin.min.js b/media/vendor/tinymce/plugins/charmap/plugin.min.js index 3fd9e23d5d873..4e790cea3f305 100644 --- a/media/vendor/tinymce/plugins/charmap/plugin.min.js +++ b/media/vendor/tinymce/plugins/charmap/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",d=0;d

'+n+"
"}else c+=""}c+=""}c+="";var o={type:"container",html:c,onclick:function(a){var c=a.target;if(/^(TD|DIV)$/.test(c.nodeName)){var d=b(c).firstChild;d&&d.hasAttribute("data-chr")&&(h(d.getAttribute("data-chr")),a.ctrlKey||f.close())}},onmouseover:function(a){var c=b(a.target);c&&c.firstChild?(f.find("#preview").text(c.firstChild.firstChild.data),f.find("#previewTitle").text(c.title)):(f.find("#preview").text(" "),f.find("#previewTitle").text(" "))}};f=a.windowManager.open({title:"Special character",spacing:10,padding:10,items:[o,{type:"container",layout:"flex",direction:"column",align:"center",spacing:5,minWidth:160,minHeight:160,items:[{type:"label",name:"preview",text:" ",style:"font-size: 40px; text-align: center",border:1,minWidth:140,minHeight:80},{type:"spacer",minHeight:20},{type:"label",name:"previewTitle",text:" ",style:"white-space: pre-wrap;",border:1,minWidth:140}]}],buttons:[{text:"Close",onclick:function(){f.close()}}]})}var j=b.isArray;return a.addCommand("mceShowCharmap",i),a.addButton("charmap",{icon:"charmap",tooltip:"Special character",cmd:"mceShowCharmap"}),a.addMenuItem("charmap",{icon:"charmap",text:"Special character",cmd:"mceShowCharmap",context:"insert"}),{getCharMap:g,insertChar:h}}),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',d=0;d",c=0;c
'+i+"
"}else b+=""}b+=""}return b+=""};return{getHtml:a}}),g("8",["6","7","c"],function(a,b,c){var d=function(a){for(;a;){if("TD"===a.nodeName)return a;a=a.parentNode}},e=function(e){var f,g={type:"container",html:c.getHtml(b.getCharMap(e)),onclick:function(b){var c=b.target;if(/^(TD|DIV)$/.test(c.nodeName)){var g=d(c).firstChild;g&&g.hasAttribute("data-chr")&&(a.insertChar(e,g.getAttribute("data-chr")),b.ctrlKey||f.close())}},onmouseover:function(a){var b=d(a.target);b&&b.firstChild?(f.find("#preview").text(b.firstChild.firstChild.data),f.find("#previewTitle").text(b.title)):(f.find("#preview").text(" "),f.find("#previewTitle").text(" "))}};f=e.windowManager.open({title:"Special character",spacing:10,padding:10,items:[g,{type:"container",layout:"flex",direction:"column",align:"center",spacing:5,minWidth:160,minHeight:160,items:[{type:"label",name:"preview",text:" ",style:"font-size: 40px; text-align: center",border:1,minWidth:140,minHeight:80},{type:"spacer",minHeight:20},{type:"label",name:"previewTitle",text:" ",style:"white-space: pre-wrap;",border:1,minWidth:140}]}],buttons:[{text:"Close",onclick:function(){f.close()}}]})};return{open:e}}),g("3",["8"],function(a){var b=function(b){b.addCommand("mceShowCharmap",function(){a.open(b)})};return{register:b}}),g("4",[],function(){var a=function(a){a.addButton("charmap",{icon:"charmap",tooltip:"Special character",cmd:"mceShowCharmap"}),a.addMenuItem("charmap",{icon:"charmap",text:"Special character",cmd:"mceShowCharmap",context:"insert"})};return{register:a}}),g("0",["1","2","3","4"],function(a,b,c,d){return a.add("charmap",function(a){return c.register(a),d.register(a),b.get(a)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/code/plugin.js b/media/vendor/tinymce/plugins/code/plugin.js index 2497305642c39..0a19440fb7000 100644 --- a/media/vendor/tinymce/plugins/code/plugin.js +++ b/media/vendor/tinymce/plugins/code/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.code.Plugin","tinymce.core.dom.DOMUtils","tinymce.core.PluginManager","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.code.Plugin","tinymce.core.PluginManager","tinymce.plugins.code.api.Commands","tinymce.plugins.code.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.code.ui.Dialog","tinymce.plugins.code.api.Settings","tinymce.plugins.code.core.Content","tinymce.core.dom.DOMUtils"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -95,12 +95,12 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.dom.DOMUtils', + 'tinymce.core.PluginManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.dom.DOMUtils'); + return resolve('tinymce.PluginManager'); } ); @@ -115,17 +115,17 @@ define( */ define( - 'tinymce.core.PluginManager', + 'tinymce.core.dom.DOMUtils', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.PluginManager'); + return resolve('tinymce.dom.DOMUtils'); } ); /** - * Plugin.js + * Settings.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,66 +134,201 @@ define( * Contributing: http://www.tinymce.com/contributing */ +define( + 'tinymce.plugins.code.api.Settings', + [ + 'tinymce.core.dom.DOMUtils' + ], + function (DOMUtils) { + var getMinWidth = function (editor) { + return editor.getParam('code_dialog_width', 600); + }; + + var getMinHeight = function (editor) { + return editor.getParam('code_dialog_height', Math.min(DOMUtils.DOM.getViewPort().h - 200, 500)); + }; + + return { + getMinWidth: getMinWidth, + getMinHeight: getMinHeight + }; + } +); /** - * This class contains all core logic for the code plugin. + * Content.js + * + * Released under LGPL License. + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * - * @class tinymce.code.Plugin - * @private + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing */ + define( - 'tinymce.plugins.code.Plugin', + 'tinymce.plugins.code.core.Content', [ - 'tinymce.core.dom.DOMUtils', - 'tinymce.core.PluginManager' ], - function (DOMUtils, PluginManager) { - PluginManager.add('code', function (editor) { - function showDialog() { - var win = editor.windowManager.open({ - title: "Source code", - body: { - type: 'textbox', - name: 'code', - multiline: true, - minWidth: editor.getParam("code_dialog_width", 600), - minHeight: editor.getParam("code_dialog_height", Math.min(DOMUtils.DOM.getViewPort().h - 200, 500)), - spellcheck: false, - style: 'direction: ltr; text-align: left' - }, - onSubmit: function (e) { - // We get a lovely "Wrong document" error in IE 11 if we - // don't move the focus to the editor before creating an undo - // transation since it tries to make a bookmark for the current selection - editor.focus(); - - editor.undoManager.transact(function () { - editor.setContent(e.data.code); - }); - - editor.selection.setCursorLocation(); - editor.nodeChanged(); - } - }); - - // Gecko has a major performance issue with textarea - // contents so we need to set it when all reflows are done - win.find('#code').value(editor.getContent({ source_view: true })); - } - - editor.addCommand("mceCodeEditor", showDialog); + function () { + var setContent = function (editor, html) { + // We get a lovely "Wrong document" error in IE 11 if we + // don't move the focus to the editor before creating an undo + // transation since it tries to make a bookmark for the current selection + editor.focus(); + editor.undoManager.transact(function () { + editor.setContent(html); + }); + + editor.selection.setCursorLocation(); + editor.nodeChanged(); + }; + + var getContent = function (editor) { + return editor.getContent({ source_view: true }); + }; + + return { + setContent: setContent, + getContent: getContent + }; + } +); +/** + * Dialog.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.code.ui.Dialog', + [ + 'tinymce.plugins.code.api.Settings', + 'tinymce.plugins.code.core.Content' + ], + function (Settings, Content) { + var open = function (editor) { + var minWidth = Settings.getMinWidth(editor); + var minHeight = Settings.getMinHeight(editor); + + var win = editor.windowManager.open({ + title: 'Source code', + body: { + type: 'textbox', + name: 'code', + multiline: true, + minWidth: minWidth, + minHeight: minHeight, + spellcheck: false, + style: 'direction: ltr; text-align: left' + }, + onSubmit: function (e) { + Content.setContent(editor, e.data.code); + } + }); + + // Gecko has a major performance issue with textarea + // contents so we need to set it when all reflows are done + win.find('#code').value(Content.getContent(editor)); + }; + + return { + open: open + }; + } +); +/** + * 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.plugins.code.api.Commands', + [ + 'tinymce.plugins.code.ui.Dialog' + ], + function (Dialog) { + var register = function (editor) { + editor.addCommand('mceCodeEditor', function () { + Dialog.open(editor); + }); + }; + + return { + register: register + }; + } +); +/** + * Buttons.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.code.ui.Buttons', + [ + 'tinymce.plugins.code.ui.Dialog' + ], + function (Dialog) { + var register = function (editor) { editor.addButton('code', { icon: 'code', tooltip: 'Source code', - onclick: showDialog + onclick: function () { + Dialog.open(editor); + } }); editor.addMenuItem('code', { icon: 'code', text: 'Source code', - context: 'tools', - onclick: showDialog + onclick: function () { + Dialog.open(editor); + } }); + }; + + return { + register: register + }; + } +); +/** + * Plugin.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.code.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.code.api.Commands', + 'tinymce.plugins.code.ui.Buttons' + ], + function (PluginManager, Commands, Buttons) { + PluginManager.add('code', function (editor) { + Commands.register(editor); + Buttons.register(editor); + + return {}; }); return function () { }; diff --git a/media/vendor/tinymce/plugins/code/plugin.min.js b/media/vendor/tinymce/plugins/code/plugin.min.js index 7a0437ed96a97..bbc39239b7630 100644 --- a/media/vendor/tinymce/plugins/code/plugin.min.js +++ b/media/vendor/tinymce/plugins/code/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;ia.length)break a;if(!(q instanceof e)){k.lastIndex=0;var r=k.exec(q);if(r){m&&(n=r[1].length);var s=r.index-1+n,r=r[0].slice(n),t=r.length,u=s+t,v=q.slice(0,s+1),w=q.slice(u+1),x=[p,1];v&&x.push(v);var y=new e(h,l?c.tokenize(r,l):r,o);x.push(y),w&&x.push(w),Array.prototype.splice.apply(f,x)}}}}}return f},hooks:{all:{},add:function(a,b){var d=c.hooks.all;d[a]=d[a]||[],d[a].push(b)},run:function(a,b){var d=c.hooks.all[a];if(d&&d.length)for(var e,f=0;e=d[f++];)e(b)}}},d=c.Token=function(a,b,c){this.type=a,this.content=b,this.alias=c};if(d.stringify=function(a,b,e){if("string"==typeof a)return a;if("Array"===c.util.type(a))return a.map(function(c){return d.stringify(c,b,a)}).join("");var f={type:a.type,content:d.stringify(a.content,b,e),tag:"span",classes:["token",a.type],attributes:{},language:b,parent:e};if("comment"==f.type&&(f.attributes.spellcheck="true"),a.alias){var g="Array"===c.util.type(a.alias)?a.alias:[a.alias];Array.prototype.push.apply(f.classes,g)}c.hooks.run("wrap",f);var h="";for(var i in f.attributes)h+=(h?" ":"")+i+'="'+(f.attributes[i]||"")+'"';return"<"+f.tag+' class="'+f.classes.join(" ")+'" '+h+">"+f.content+""},!b.document)return b.addEventListener?(b.addEventListener("message",function(a){var d=JSON.parse(a.data),e=d.language,f=d.code,g=d.immediateClose;b.postMessage(c.highlight(f,c.languages[e],e)),g&&b.close()},!1),b.Prism):b.Prism}();return"undefined"!=typeof module&&module.exports&&(module.exports=c),"undefined"!=typeof global&&(global.Prism=c),c.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?[^\s>\/=.]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),c.languages.xml=c.languages.markup,c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,c.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},c.languages.css.atrule.inside.rest=c.util.clone(c.languages.css),c.languages.markup&&(c.languages.insertBefore("markup","tag",{style:{pattern:/[\w\W]*?<\/style>/i,inside:{tag:{pattern:/|<\/style>/i,inside:c.languages.markup.tag.inside},rest:c.languages.css},alias:"language-css"}}),c.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:c.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:c.languages.css}},alias:"language-css"}},c.languages.markup.tag)),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),c.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),c.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.insertBefore("markup","tag",{script:{pattern:/[\w\W]*?<\/script>/i,inside:{tag:{pattern:/|<\/script>/i,inside:c.languages.markup.tag.inside},rest:c.languages.javascript},alias:"language-javascript"}}),c.languages.js=c.languages.javascript,c.languages.c=c.languages.extend("clike",{keyword:/\b(asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,operator:/\-[>-]?|\+\+?|!=?|<>?=?|==?|&&?|\|?\||[~^%?*\/]/,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)[ful]*\b/i}),c.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+([^\r\n\\]|\\.|\\(?:\r\n?|\n))*/im,lookbehind:!0,alias:"property",inside:{string:{pattern:/(#\s*include\s*)(<.+?>|("|')(\\?.)+?\3)/,lookbehind:!0}}}}),delete c.languages.c["class-name"],delete c.languages.c["boolean"],c.languages.csharp=c.languages.extend("clike",{keyword:/\b(abstract|as|async|await|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while|add|alias|ascending|async|await|descending|dynamic|from|get|global|group|into|join|let|orderby|partial|remove|select|set|value|var|where|yield)\b/,string:[/@("|')(\1\1|\\\1|\\?(?!\1)[\s\S])*\1/,/("|')(\\?.)*?\1/],number:/\b-?(0x[\da-f]+|\d*\.?\d+)\b/i}),c.languages.insertBefore("csharp","keyword",{preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0}}),c.languages.cpp=c.languages.extend("c",{keyword:/\b(alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|class|compl|const|constexpr|const_cast|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|float|for|friend|goto|if|inline|int|long|mutable|namespace|new|noexcept|nullptr|operator|private|protected|public|register|reinterpret_cast|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,"boolean":/\b(true|false)\b/,operator:/[-+]{1,2}|!=?|<{1,2}=?|>{1,2}=?|\->|:{1,2}|={1,2}|\^|~|%|&{1,2}|\|?\||\?|\*|\/|\b(and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/}),c.languages.insertBefore("cpp","keyword",{"class-name":{pattern:/(class\s+)[a-z0-9_]+/i,lookbehind:!0}}),c.languages.java=c.languages.extend("clike",{keyword:/\b(abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),c.languages.php=c.languages.extend("clike",{keyword:/\b(and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\w\W]*?\*\/|\/\/.*)/,lookbehind:!0}}),c.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),c.languages.insertBefore("php","keyword",{delimiter:/\?>|<\?(?:php)?/i,variable:/\$\w+\b/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),c.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),c.languages.markup&&(c.hooks.add("before-highlight",function(a){"php"===a.language&&(a.tokenStack=[],a.backupCode=a.code,a.code=a.code.replace(/(?:<\?php|<\?)[\w\W]*?(?:\?>)/gi,function(b){return a.tokenStack.push(b),"{{{PHP"+a.tokenStack.length+"}}}"}))}),c.hooks.add("before-insert",function(a){"php"===a.language&&(a.code=a.backupCode,delete a.backupCode)}),c.hooks.add("after-highlight",function(a){if("php"===a.language){for(var b,d=0;b=a.tokenStack[d];d++)a.highlightedCode=a.highlightedCode.replace("{{{PHP"+(d+1)+"}}}",c.highlight(b,a.grammar,"php").replace(/\$/g,"$$$$"));a.element.innerHTML=a.highlightedCode}}),c.hooks.add("wrap",function(a){"php"===a.language&&"markup"===a.type&&(a.content=a.content.replace(/(\{\{\{PHP[0-9]+\}\}\})/g,'$1'))}),c.languages.insertBefore("php","comment",{markup:{pattern:/<[^?]\/?(.*?)>/,inside:c.languages.markup},php:/\{\{\{PHP[0-9]+\}\}\}/})),c.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:/"""[\s\S]+?"""|'''[\s\S]+?'''|("|')(?:\\?.)*?\1/,"function":{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)[a-z0-9_]+/i,lookbehind:!0},keyword:/\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,"boolean":/\b(?:True|False)\b/,number:/\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,punctuation:/[{}[\];(),.:]/},function(a){a.languages.ruby=a.languages.extend("clike",{comment:/#(?!\{[^\r\n]*?\}).*/,keyword:/\b(alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var b={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:a.util.clone(a.languages.ruby)}};a.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,inside:{interpolation:b}},{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}],variable:/[@$]+[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/,symbol:/:[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/}),a.languages.insertBefore("ruby","number",{builtin:/\b(Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|File|Fixnum|Fload|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z][a-zA-Z_0-9]*(?:[?!]|\b)/}),a.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,inside:{interpolation:b}},{pattern:/("|')(#\{[^}]+\}|\\(?:\r?\n|\r)|\\?.)*?\1/,inside:{interpolation:b}}]}(c),c}),g("7",["6"],function(a){return a("tinymce.dom.DOMUtils")}),g("5",[],function(){function a(a){return a&&"PRE"==a.nodeName&&a.className.indexOf("language-")!==-1}function b(a){return function(b,c){return a(c)}}return{isCodeSample:a,trimArg:b}}),g("4",["7","3","5"],function(a,b,c){function d(a){var b=[{text:"HTML/XML",value:"markup"},{text:"JavaScript",value:"javascript"},{text:"CSS",value:"css"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"}],c=a.settings.codesample_languages;return c?c:b}function e(a,c,d){a.undoManager.transact(function(){var e=f(a);d=i.encode(d),e?(a.dom.setAttrib(e,"class","language-"+c),e.innerHTML=d,b.highlightElement(e),a.selection.select(e)):(a.insertContent('
'+d+"
"),a.selection.select(a.$("#__new").removeAttr("id")[0]))})}function f(a){var b=a.selection.getNode();return c.isCodeSample(b)?b:null}function g(a){var b=f(a);return b?b.textContent:""}function h(a){var b,c=f(a);return c?(b=c.className.match(/language-(\w+)/),b?b[1]:""):""}var i=a.DOM;return{open:function(a){a.windowManager.open({title:"Insert/Edit code sample",minWidth:Math.min(i.getViewPort().w,a.getParam("codesample_dialog_width",800)),minHeight:Math.min(i.getViewPort().h,a.getParam("codesample_dialog_height",650)),layout:"flex",direction:"column",align:"stretch",body:[{type:"listbox",name:"language",label:"Language",maxWidth:200,value:h(a),values:d(a)},{type:"textbox",name:"code",multiline:!0,spellcheck:!1,ariaLabel:"Code view",flex:1,style:"direction: ltr; text-align: left",classes:"monospace",value:g(a),autofocus:!0}],onSubmit:function(b){e(a,b.data.language,b.data.code)}})}}}),g("0",["1","2","3","4","5"],function(a,b,c,d,e){var f,g=e.trimArg;return b.add("codesample",function(b,h){function i(){var a,c=b.settings.codesample_content_css;b.inline&&f||!b.inline&&j||(b.inline?f=!0:j=!0,c!==!1&&(a=b.dom.create("link",{rel:"stylesheet",href:c?c:h+"/css/prism.css"}),b.getDoc().getElementsByTagName("head")[0].appendChild(a)))}var j,k=b.$;a.ceFalse&&(b.on("PreProcess",function(a){k("pre[contenteditable=false]",a.node).filter(g(e.isCodeSample)).each(function(a,b){var c=k(b),d=b.textContent;c.attr("class",k.trim(c.attr("class"))),c.removeAttr("contentEditable"),c.empty().append(k("").each(function(){this.textContent=d}))})}),b.on("SetContent",function(){var a=k("pre").filter(g(e.isCodeSample)).filter(function(a,b){return"false"!==b.contentEditable});a.length&&b.undoManager.transact(function(){a.each(function(a,d){k(d).find("br").each(function(a,c){c.parentNode.replaceChild(b.getDoc().createTextNode("\n"),c)}),d.contentEditable=!1,d.innerHTML=b.dom.encode(d.textContent),c.highlightElement(d),d.className=k.trim(d.className)})})}),b.addCommand("codesample",function(){var a=b.selection.getNode();b.selection.isCollapsed()||e.isCodeSample(a)?d.open(b):b.formatter.toggle("code")}),b.addButton("codesample",{cmd:"codesample",title:"Insert/Edit code sample"}),b.on("init",i))}),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;ia.length)break a;if(!(q instanceof e)){k.lastIndex=0;var r=k.exec(q);if(r){m&&(n=r[1].length);var s=r.index-1+n,r=r[0].slice(n),t=r.length,u=s+t,v=q.slice(0,s+1),w=q.slice(u+1),x=[p,1];v&&x.push(v);var y=new e(h,l?c.tokenize(r,l):r,o);x.push(y),w&&x.push(w),Array.prototype.splice.apply(f,x)}}}}}return f},hooks:{all:{},add:function(a,b){var d=c.hooks.all;d[a]=d[a]||[],d[a].push(b)},run:function(a,b){var d=c.hooks.all[a];if(d&&d.length)for(var e,f=0;e=d[f++];)e(b)}}},d=c.Token=function(a,b,c){this.type=a,this.content=b,this.alias=c};if(d.stringify=function(a,b,e){if("string"==typeof a)return a;if("Array"===c.util.type(a))return a.map(function(c){return d.stringify(c,b,a)}).join("");var f={type:a.type,content:d.stringify(a.content,b,e),tag:"span",classes:["token",a.type],attributes:{},language:b,parent:e};if("comment"==f.type&&(f.attributes.spellcheck="true"),a.alias){var g="Array"===c.util.type(a.alias)?a.alias:[a.alias];Array.prototype.push.apply(f.classes,g)}c.hooks.run("wrap",f);var h="";for(var i in f.attributes)h+=(h?" ":"")+i+'="'+(f.attributes[i]||"")+'"';return"<"+f.tag+' class="'+f.classes.join(" ")+'" '+h+">"+f.content+""},!b.document)return b.addEventListener?(b.addEventListener("message",function(a){var d=JSON.parse(a.data),e=d.language,f=d.code,g=d.immediateClose;b.postMessage(c.highlight(f,c.languages[e],e)),g&&b.close()},!1),b.Prism):b.Prism}();return"undefined"!=typeof module&&module.exports&&(module.exports=c),"undefined"!=typeof global&&(global.Prism=c),c.languages.markup={comment://,prolog:/<\?[\w\W]+?\?>/,doctype://,cdata://i,tag:{pattern:/<\/?[^\s>\/=.]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,inside:{punctuation:/[=>"']/}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/&#?[\da-z]{1,8};/i},c.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),c.languages.xml=c.languages.markup,c.languages.html=c.languages.markup,c.languages.mathml=c.languages.markup,c.languages.svg=c.languages.markup,c.languages.css={comment:/\/\*[\w\W]*?\*\//,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*\{))/i,inside:{rule:/@[\w-]+/}},url:/url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,selector:/[^\{\}\s][^\{\};]*?(?=\s*\{)/,string:/("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,property:/(\b|\B)[\w-]+(?=\s*:)/i,important:/\B!important\b/i,"function":/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:]/},c.languages.css.atrule.inside.rest=c.util.clone(c.languages.css),c.languages.markup&&(c.languages.insertBefore("markup","tag",{style:{pattern:/[\w\W]*?<\/style>/i,inside:{tag:{pattern:/|<\/style>/i,inside:c.languages.markup.tag.inside},rest:c.languages.css},alias:"language-css"}}),c.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|').*?\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:c.languages.markup.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:c.languages.css}},alias:"language-css"}},c.languages.markup.tag)),c.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/},c.languages.javascript=c.languages.extend("clike",{keyword:/\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,"function":/[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i}),c.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),c.languages.insertBefore("javascript","class-name",{"template-string":{pattern:/`(?:\\`|\\?[^`])*`/,inside:{interpolation:{pattern:/\$\{[^}]+\}/,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:c.languages.javascript}},string:/[\s\S]+/}}}),c.languages.markup&&c.languages.insertBefore("markup","tag",{script:{pattern:/[\w\W]*?<\/script>/i,inside:{tag:{pattern:/|<\/script>/i,inside:c.languages.markup.tag.inside},rest:c.languages.javascript},alias:"language-javascript"}}),c.languages.js=c.languages.javascript,c.languages.c=c.languages.extend("clike",{keyword:/\b(asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,operator:/\-[>-]?|\+\+?|!=?|<>?=?|==?|&&?|\|?\||[~^%?*\/]/,number:/\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)[ful]*\b/i}),c.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+([^\r\n\\]|\\.|\\(?:\r\n?|\n))*/im,lookbehind:!0,alias:"property",inside:{string:{pattern:/(#\s*include\s*)(<.+?>|("|')(\\?.)+?\3)/,lookbehind:!0}}}}),delete c.languages.c["class-name"],delete c.languages.c["boolean"],c.languages.csharp=c.languages.extend("clike",{keyword:/\b(abstract|as|async|await|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while|add|alias|ascending|async|await|descending|dynamic|from|get|global|group|into|join|let|orderby|partial|remove|select|set|value|var|where|yield)\b/,string:[/@("|')(\1\1|\\\1|\\?(?!\1)[\s\S])*\1/,/("|')(\\?.)*?\1/],number:/\b-?(0x[\da-f]+|\d*\.?\d+)\b/i}),c.languages.insertBefore("csharp","keyword",{preprocessor:{pattern:/(^\s*)#.*/m,lookbehind:!0}}),c.languages.cpp=c.languages.extend("c",{keyword:/\b(alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|class|compl|const|constexpr|const_cast|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|float|for|friend|goto|if|inline|int|long|mutable|namespace|new|noexcept|nullptr|operator|private|protected|public|register|reinterpret_cast|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,"boolean":/\b(true|false)\b/,operator:/[-+]{1,2}|!=?|<{1,2}=?|>{1,2}=?|\->|:{1,2}|={1,2}|\^|~|%|&{1,2}|\|?\||\?|\*|\/|\b(and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/}),c.languages.insertBefore("cpp","keyword",{"class-name":{pattern:/(class\s+)[a-z0-9_]+/i,lookbehind:!0}}),c.languages.java=c.languages.extend("clike",{keyword:/\b(abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while)\b/,number:/\b0b[01]+\b|\b0x[\da-f]*\.?[\da-fp\-]+\b|\b\d*\.?\d+(?:e[+-]?\d+)?[df]?\b/i,operator:{pattern:/(^|[^.])(?:\+[+=]?|-[-=]?|!=?|<>?>?=?|==?|&[&=]?|\|[|=]?|\*=?|\/=?|%=?|\^=?|[?:~])/m,lookbehind:!0}}),c.languages.php=c.languages.extend("clike",{keyword:/\b(and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\w\W]*?\*\/|\/\/.*)/,lookbehind:!0}}),c.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),c.languages.insertBefore("php","keyword",{delimiter:/\?>|<\?(?:php)?/i,variable:/\$\w+\b/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),c.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),c.languages.markup&&(c.hooks.add("before-highlight",function(a){"php"===a.language&&(a.tokenStack=[],a.backupCode=a.code,a.code=a.code.replace(/(?:<\?php|<\?)[\w\W]*?(?:\?>)/gi,function(b){return a.tokenStack.push(b),"{{{PHP"+a.tokenStack.length+"}}}"}))}),c.hooks.add("before-insert",function(a){"php"===a.language&&(a.code=a.backupCode,delete a.backupCode)}),c.hooks.add("after-highlight",function(a){if("php"===a.language){for(var b,d=0;b=a.tokenStack[d];d++)a.highlightedCode=a.highlightedCode.replace("{{{PHP"+(d+1)+"}}}",c.highlight(b,a.grammar,"php").replace(/\$/g,"$$$$"));a.element.innerHTML=a.highlightedCode}}),c.hooks.add("wrap",function(a){"php"===a.language&&"markup"===a.type&&(a.content=a.content.replace(/(\{\{\{PHP[0-9]+\}\}\})/g,'$1'))}),c.languages.insertBefore("php","comment",{markup:{pattern:/<[^?]\/?(.*?)>/,inside:c.languages.markup},php:/\{\{\{PHP[0-9]+\}\}\}/})),c.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},string:/"""[\s\S]+?"""|'''[\s\S]+?'''|("|')(?:\\?.)*?\1/,"function":{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)[a-z0-9_]+/i,lookbehind:!0},keyword:/\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,"boolean":/\b(?:True|False)\b/,number:/\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,punctuation:/[{}[\];(),.:]/},function(a){a.languages.ruby=a.languages.extend("clike",{comment:/#(?!\{[^\r\n]*?\}).*/,keyword:/\b(alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var b={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:a.util.clone(a.languages.ruby)}};a.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,inside:{interpolation:b}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,inside:{interpolation:b}},{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}],variable:/[@$]+[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/,symbol:/:[a-zA-Z_][a-zA-Z_0-9]*(?:[?!]|\b)/}),a.languages.insertBefore("ruby","number",{builtin:/\b(Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|File|Fixnum|Fload|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z][a-zA-Z_0-9]*(?:[?!]|\b)/}),a.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s\{\(\[<])(?:[^\\]|\\[\s\S])*?\1/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,inside:{interpolation:b}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,inside:{interpolation:b}},{pattern:/("|')(#\{[^}]+\}|\\(?:\r?\n|\r)|\\?.)*?\1/,inside:{interpolation:b}}]}(c),c}),g("9",[],function(){function a(a){return a&&"PRE"===a.nodeName&&a.className.indexOf("language-")!==-1}function b(a){return function(b,c){return a(c)}}return{isCodeSample:a,trimArg:b}}),g("d",["c","a","9"],function(a,b,c){var d=function(a){var b=a.selection.getNode();return c.isCodeSample(b)?b:null},e=function(c,e,f){c.undoManager.transact(function(){var g=d(c);f=a.DOM.encode(f),g?(c.dom.setAttrib(g,"class","language-"+e),g.innerHTML=f,b.highlightElement(g),c.selection.select(g)):(c.insertContent('
'+f+"
"),c.selection.select(c.$("#__new").removeAttr("id")[0]))})},f=function(a){var b=d(a);return b?b.textContent:""};return{getSelectedCodeSample:d,insertCodeSample:e,getCurrentCode:f}}),g("e",["b","d"],function(a,b){var c=function(b){var c=[{text:"HTML/XML",value:"markup"},{text:"JavaScript",value:"javascript"},{text:"CSS",value:"css"},{text:"PHP",value:"php"},{text:"Ruby",value:"ruby"},{text:"Python",value:"python"},{text:"Java",value:"java"},{text:"C",value:"c"},{text:"C#",value:"csharp"},{text:"C++",value:"cpp"}],d=a.getLanguages(b);return d?d:c},d=function(a){var c,d=b.getSelectedCodeSample(a);return d?(c=d.className.match(/language-(\w+)/),c?c[1]:""):""};return{getLanguages:c,getCurrentLanguage:d}}),g("8",["b","d","e"],function(a,b,c){return{open:function(d){var e=a.getDialogMinWidth(d),f=a.getDialogMinHeight(d),g=c.getCurrentLanguage(d),h=c.getLanguages(d),i=b.getCurrentCode(d);d.windowManager.open({title:"Insert/Edit code sample",minWidth:e,minHeight:f,layout:"flex",direction:"column",align:"stretch",body:[{type:"listbox",name:"language",label:"Language",maxWidth:200,value:g,values:h},{type:"textbox",name:"code",multiline:!0,spellcheck:!1,ariaLabel:"Code view",flex:1,style:"direction: ltr; text-align: left",classes:"monospace",value:i,autofocus:!0}],onSubmit:function(a){b.insertCodeSample(d,a.data.language,a.data.code)}})}}}),g("3",["8","9"],function(a,b){var c=function(c){c.addCommand("codesample",function(){var d=c.selection.getNode();c.selection.isCollapsed()||b.isCodeSample(d)?a.open(c):c.formatter.toggle("code")})};return{register:c}}),g("4",["a","9"],function(a,b){var c=function(c){var d=c.$;c.on("PreProcess",function(a){d("pre[contenteditable=false]",a.node).filter(b.trimArg(b.isCodeSample)).each(function(a,b){var c=d(b),e=b.textContent;c.attr("class",d.trim(c.attr("class"))),c.removeAttr("contentEditable"),c.empty().append(d("").each(function(){this.textContent=e}))})}),c.on("SetContent",function(){var e=d("pre").filter(b.trimArg(b.isCodeSample)).filter(function(a,b){return"false"!==b.contentEditable});e.length&&c.undoManager.transact(function(){e.each(function(b,e){d(e).find("br").each(function(a,b){b.parentNode.replaceChild(c.getDoc().createTextNode("\n"),b)}),e.contentEditable=!1,e.innerHTML=c.dom.encode(e.textContent),a.highlightElement(e),e.className=d.trim(e.className)})})})};return{setup:c}}),g("5",["b"],function(a){var b=function(b,c,d,e){var f,g=a.getContentCss(b);b.inline&&d.get()||!b.inline&&e.get()||(b.inline?d.set(!0):e.set(!0),g!==!1&&(f=b.dom.create("link",{rel:"stylesheet",href:g?g:c+"/css/prism.css"}),b.getDoc().getElementsByTagName("head")[0].appendChild(f)))};return{loadCss:b}}),g("6",[],function(){var a=function(a){a.addButton("codesample",{cmd:"codesample",title:"Insert/Edit code sample"}),a.addMenuItem("codesample",{cmd:"codesample",text:"Code sample",icon:"codesample"})};return{register:a}}),g("0",["1","2","3","4","5","6"],function(a,b,c,d,e,f){var g=a(!1);return b.add("codesample",function(b,h){var i=a(!1);d.setup(b),f.register(b),c.register(b),b.on("init",function(){e.loadCss(b,h,g,i)})}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/colorpicker/plugin.js b/media/vendor/tinymce/plugins/colorpicker/plugin.js index 2f4954f2152c2..9e5d8b9828ee8 100644 --- a/media/vendor/tinymce/plugins/colorpicker/plugin.js +++ b/media/vendor/tinymce/plugins/colorpicker/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.colorpicker.Plugin","tinymce.core.PluginManager","tinymce.core.util.Color","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.colorpicker.Plugin","tinymce.core.PluginManager","tinymce.plugins.colorpicker.ui.Dialog","global!tinymce.util.Tools.resolve","tinymce.core.util.Color"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -125,7 +125,7 @@ define( ); /** - * Plugin.js + * Dialog.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,117 +134,134 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class contains all core logic for the colorpicker plugin. - * - * @class tinymce.colorpicker.Plugin - * @private - */ define( - 'tinymce.plugins.colorpicker.Plugin', + 'tinymce.plugins.colorpicker.ui.Dialog', [ - 'tinymce.core.PluginManager', 'tinymce.core.util.Color' ], - function (PluginManager, Color) { - PluginManager.add('colorpicker', function (editor) { - function colorPickerCallback(callback, value) { - function setColor(value) { - var color = new Color(value), rgb = color.toRgb(); - - win.fromJSON({ - r: rgb.r, - g: rgb.g, - b: rgb.b, - hex: color.toHex().substr(1) - }); - - showPreview(color.toHex()); - } + function (Color) { + var showPreview = function (win, hexColor) { + win.find('#preview')[0].getEl().style.background = hexColor; + }; - function showPreview(hexColor) { - win.find('#preview')[0].getEl().style.background = hexColor; - } + var setColor = function (win, value) { + var color = new Color(value), rgb = color.toRgb(); + + win.fromJSON({ + r: rgb.r, + g: rgb.g, + b: rgb.b, + hex: color.toHex().substr(1) + }); + + showPreview(win, color.toHex()); + }; - var win = editor.windowManager.open({ - title: 'Color', - items: { - type: 'container', - layout: 'flex', - direction: 'row', - align: 'stretch', - padding: 5, - spacing: 10, - items: [ - { - type: 'colorpicker', - value: value, + var open = function (editor, callback, value) { + var win = editor.windowManager.open({ + title: 'Color', + items: { + type: 'container', + layout: 'flex', + direction: 'row', + align: 'stretch', + padding: 5, + spacing: 10, + items: [ + { + type: 'colorpicker', + value: value, + onchange: function () { + var rgb = this.rgb(); + + if (win) { + win.find('#r').value(rgb.r); + win.find('#g').value(rgb.g); + win.find('#b').value(rgb.b); + win.find('#hex').value(this.value().substr(1)); + showPreview(win, this.value()); + } + } + }, + { + type: 'form', + padding: 0, + labelGap: 5, + defaults: { + type: 'textbox', + size: 7, + value: '0', + flex: 1, + spellcheck: false, onchange: function () { - var rgb = this.rgb(); - - if (win) { - win.find('#r').value(rgb.r); - win.find('#g').value(rgb.g); - win.find('#b').value(rgb.b); - win.find('#hex').value(this.value().substr(1)); - showPreview(this.value()); + var colorPickerCtrl = win.find('colorpicker')[0]; + var name, value; + + name = this.name(); + value = this.value(); + + if (name === "hex") { + value = '#' + value; + setColor(win, value); + colorPickerCtrl.value(value); + return; } + + value = { + r: win.find('#r').value(), + g: win.find('#g').value(), + b: win.find('#b').value() + }; + + colorPickerCtrl.value(value); + setColor(win, value); } }, - { - type: 'form', - padding: 0, - labelGap: 5, - defaults: { - type: 'textbox', - size: 7, - value: '0', - flex: 1, - spellcheck: false, - onchange: function () { - var colorPickerCtrl = win.find('colorpicker')[0]; - var name, value; - - name = this.name(); - value = this.value(); - - if (name == "hex") { - value = '#' + value; - setColor(value); - colorPickerCtrl.value(value); - return; - } - - value = { - r: win.find('#r').value(), - g: win.find('#g').value(), - b: win.find('#b').value() - }; + items: [ + { name: 'r', label: 'R', autofocus: 1 }, + { name: 'g', label: 'G' }, + { name: 'b', label: 'B' }, + { name: 'hex', label: '#', value: '000000' }, + { name: 'preview', type: 'container', border: 1 } + ] + } + ] + }, + onSubmit: function () { + callback('#' + win.toJSON().hex); + } + }); - colorPickerCtrl.value(value); - setColor(value); - } - }, - items: [ - { name: 'r', label: 'R', autofocus: 1 }, - { name: 'g', label: 'G' }, - { name: 'b', label: 'B' }, - { name: 'hex', label: '#', value: '000000' }, - { name: 'preview', type: 'container', border: 1 } - ] - } - ] - }, - onSubmit: function () { - callback('#' + this.toJSON().hex); - } - }); - - setColor(value); - } + setColor(win, value); + }; + return { + open: open + }; + } +); +/** + * Plugin.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.colorpicker.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.colorpicker.ui.Dialog' + ], + function (PluginManager, Dialog) { + PluginManager.add('colorpicker', function (editor) { if (!editor.settings.color_picker_callback) { - editor.settings.color_picker_callback = colorPickerCallback; + editor.settings.color_picker_callback = function (callback, value) { + Dialog.open(editor, callback, value); + }; } }); diff --git a/media/vendor/tinymce/plugins/colorpicker/plugin.min.js b/media/vendor/tinymce/plugins/colorpicker/plugin.min.js index 35bfea2141f3e..132caaa2cc8b0 100644 --- a/media/vendor/tinymce/plugins/colorpicker/plugin.min.js +++ b/media/vendor/tinymce/plugins/colorpicker/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-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e=a.left&&b<=a.right&&c>=a.top&&c<=a.bottom},c=function(c,d,e){return!e.collapsed&&a.foldl(e.getClientRects(),function(a,e){return a||b(e,c,d)},!1)};return{isXYWithinRange:c}}),g("0",["1","2","3","4","5","6"],function(a,b,c,d,e,f){var g=a.DOM;return c.add("contextmenu",function(a){var c,h,i=a.settings.contextmenu_never_use_native,j=function(a){return a.ctrlKey&&!i},k=function(){return b.mac&&b.webkit},l=function(){return h===!0},m=function(a){return a&&"IMG"===a.nodeName},n=function(a,b){return m(a.target)&&f.isXYWithinRange(a.clientX,a.clientY,b)===!1};return a.on("mousedown",function(b){k()&&2===b.button&&!j(b)&&a.selection.isCollapsed()&&a.once("contextmenu",function(b){m(b.target)||a.selection.placeCaretAt(b.clientX,b.clientY)})}),a.on("contextmenu",function(b){var f;if(!j(b)){if(n(b,a.selection.getRng())&&a.selection.select(b.target),b.preventDefault(),f=a.settings.contextmenu||"link openlink image inserttable | cell row column deletetable",c)c.show();else{var i=[];e.each(f.split(/[ ,]/),function(b){var c=a.menuItems[b];"|"==b&&(c={text:b}),c&&(c.shortcut="",i.push(c))});for(var k=0;k-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e=a.left&&b<=a.right&&c>=a.top&&c<=a.bottom},c=function(c,d,e){return!e.collapsed&&a.foldl(e.getClientRects(),function(a,e){return a||b(e,c,d)},!1)};return{isXYWithinRange:c}}),g("c",["5"],function(a){return a("tinymce.ui.Factory")}),g("d",["5"],function(a){return a("tinymce.util.Tools")}),g("a",["c","d","8"],function(a,b,c){var d=function(d,e){var f,g,h=[];g=c.getContextMenu(d),b.each(g.split(/[ ,]/),function(a){var b=d.menuItems[a];"|"===a&&(b={text:a}),b&&(b.shortcut="",h.push(b))});for(var i=0;i'; - }); - - emoticonsHtml += ''; + emoticonsHtml += ''; }); - emoticonsHtml += ''; + emoticonsHtml += ''; + }); + + emoticonsHtml += ''; + + return emoticonsHtml; + }; + + return { + getHtml: getHtml + }; + } +); +/** + * Buttons.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.emoticons.ui.Buttons', + [ + 'tinymce.plugins.emoticons.ui.PanelHtml' + ], + function (PanelHtml) { + var insertEmoticon = function (editor, src, alt) { + editor.insertContent(editor.dom.createHTML('img', { src: src, alt: alt })); + }; - return emoticonsHtml; - } + var register = function (editor, pluginUrl) { + var panelHtml = PanelHtml.getHtml(pluginUrl); editor.addButton('emoticons', { type: 'panelbutton', panel: { role: 'application', autohide: true, - html: getHtml, + html: panelHtml, onclick: function (e) { var linkElm = editor.dom.getParent(e.target, 'a'); - if (linkElm) { - editor.insertContent( - '' + linkElm.getAttribute('data-mce-alt') +
-                '' - ); - + insertEmoticon(editor, linkElm.getAttribute('data-mce-url'), linkElm.getAttribute('data-mce-alt')); this.hide(); } } }, tooltip: 'Emoticons' }); + }; + + return { + register: register + }; + } +); +/** + * Plugin.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 contains all core logic for the emoticons plugin. + * + * @class tinymce.emoticons.Plugin + * @private + */ +define( + 'tinymce.plugins.emoticons.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.emoticons.ui.Buttons' + ], + function (PluginManager, Buttons) { + PluginManager.add('emoticons', function (editor, pluginUrl) { + Buttons.register(editor, pluginUrl); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/emoticons/plugin.min.js b/media/vendor/tinymce/plugins/emoticons/plugin.min.js index e29c829e77814..92363b3365db8 100644 --- a/media/vendor/tinymce/plugins/emoticons/plugin.min.js +++ b/media/vendor/tinymce/plugins/emoticons/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'}),a+=""}),a+=""}var e=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]];a.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:d,onclick:function(b){var c=a.dom.getParent(b.target,"a");c&&(a.insertContent(''+c.getAttribute('),this.hide())}},tooltip:"Emoticons"})}),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'}),d+=""}),d+=""};return{getHtml:c}}),g("2",["4"],function(a){var b=function(a,b,c){a.insertContent(a.dom.createHTML("img",{src:b,alt:c}))},c=function(c,d){var e=a.getHtml(d);c.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:e,onclick:function(a){var d=c.dom.getParent(a.target,"a");d&&(b(c,d.getAttribute("data-mce-url"),d.getAttribute("data-mce-alt")),this.hide())}},tooltip:"Emoticons"})};return{register:c}}),g("0",["1","2"],function(a,b){return a.add("emoticons",function(a,c){b.register(a,c)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/fullpage/plugin.js b/media/vendor/tinymce/plugins/fullpage/plugin.js index b98eb5d1b9b6d..696712efb158f 100644 --- a/media/vendor/tinymce/plugins/fullpage/plugin.js +++ b/media/vendor/tinymce/plugins/fullpage/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,13 +76,46 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.fullpage.Plugin","tinymce.core.html.DomParser","tinymce.core.html.Node","tinymce.core.html.Serializer","tinymce.core.PluginManager","tinymce.core.util.Tools","tinymce.plugins.fullpage.Protect","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.fullpage.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.fullpage.api.Commands","tinymce.plugins.fullpage.core.FilterContent","tinymce.plugins.fullpage.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.fullpage.ui.Dialog","tinymce.core.util.Tools","tinymce.plugins.fullpage.api.Settings","tinymce.plugins.fullpage.core.Parser","tinymce.plugins.fullpage.core.Protect","tinymce.core.html.DomParser","tinymce.core.html.Node","tinymce.core.html.Serializer"] jsc*/ +define( + 'ephox.katamari.api.Cell', + + [ + ], + + function () { + var Cell = function (initial) { + var value = initial; + + var get = function () { + return value; + }; + + var set = function (v) { + value = v; + }; + + var clone = function () { + return Cell(get()); + }; + + return { + get: get, + set: set, + clone: clone + }; + }; + + return Cell; + } +); + defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** * ResolveGlobal.js @@ -95,12 +128,12 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.html.DomParser', + 'tinymce.core.PluginManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.html.DomParser'); + return resolve('tinymce.PluginManager'); } ); @@ -115,12 +148,12 @@ define( */ define( - 'tinymce.core.html.Node', + 'tinymce.core.util.Tools', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.html.Node'); + return resolve('tinymce.util.Tools'); } ); @@ -135,12 +168,12 @@ define( */ define( - 'tinymce.core.html.Serializer', + 'tinymce.core.html.DomParser', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.html.Serializer'); + return resolve('tinymce.html.DomParser'); } ); @@ -155,12 +188,12 @@ define( */ define( - 'tinymce.core.PluginManager', + 'tinymce.core.html.Node', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.PluginManager'); + return resolve('tinymce.html.Node'); } ); @@ -175,46 +208,76 @@ define( */ define( - 'tinymce.core.util.Tools', + 'tinymce.core.html.Serializer', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.util.Tools'); + return resolve('tinymce.html.Serializer'); } ); +/** + * Settings.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.fullpage.Protect', + 'tinymce.plugins.fullpage.api.Settings', [ - 'tinymce.core.util.Tools' ], - function (Tools) { - var protectHtml = function (protect, html) { - Tools.each(protect, function (pattern) { - html = html.replace(pattern, function (str) { - return ''; - }); - }); + function () { + var shouldHideInSourceView = function (editor) { + return editor.getParam('fullpage_hide_in_source_view'); + }; - return html; + var getDefaultXmlPi = function (editor) { + return editor.getParam('fullpage_default_xml_pi'); }; - var unprotectHtml = function (html) { - return html.replace(//g, function (a, m) { - return unescape(m); - }); + var getDefaultEncoding = function (editor) { + return editor.getParam('fullpage_default_encoding'); + }; + + var getDefaultFontFamily = function (editor) { + return editor.getParam('fullpage_default_font_family'); + }; + + var getDefaultFontSize = function (editor) { + return editor.getParam('fullpage_default_font_size'); + }; + + var getDefaultTextColor = function (editor) { + return editor.getParam('fullpage_default_text_color'); + }; + + var getDefaultTitle = function (editor) { + return editor.getParam('fullpage_default_title'); + }; + + var getDefaultDocType = function (editor) { + return editor.getParam('fullpage_default_doctype', ''); }; return { - protectHtml: protectHtml, - unprotectHtml: unprotectHtml + shouldHideInSourceView: shouldHideInSourceView, + getDefaultXmlPi: getDefaultXmlPi, + getDefaultEncoding: getDefaultEncoding, + getDefaultFontFamily: getDefaultFontFamily, + getDefaultFontSize: getDefaultFontSize, + getDefaultTextColor: getDefaultTextColor, + getDefaultTitle: getDefaultTitle, + getDefaultDocType: getDefaultDocType }; } ); - /** - * Plugin.js + * Protect.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -223,492 +286,630 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class contains all core logic for the fullpage plugin. - * - * @class tinymce.fullpage.Plugin - * @private - */ define( - 'tinymce.plugins.fullpage.Plugin', + 'tinymce.plugins.fullpage.core.Parser', [ 'tinymce.core.html.DomParser', 'tinymce.core.html.Node', 'tinymce.core.html.Serializer', - 'tinymce.core.PluginManager', 'tinymce.core.util.Tools', - 'tinymce.plugins.fullpage.Protect' + 'tinymce.plugins.fullpage.api.Settings' ], - function (DomParser, Node, Serializer, PluginManager, Tools, Protect) { - PluginManager.add('fullpage', function (editor) { - var each = Tools.each; - var head, foot; - - function showDialog() { - var data = htmlToData(); - - editor.windowManager.open({ - title: 'Document properties', - data: data, - defaults: { type: 'textbox', size: 40 }, - body: [ - { name: 'title', label: 'Title' }, - { name: 'keywords', label: 'Keywords' }, - { name: 'description', label: 'Description' }, - { name: 'robots', label: 'Robots' }, - { name: 'author', label: 'Author' }, - { name: 'docencoding', label: 'Encoding' } - ], - onSubmit: function (e) { - dataToHtml(Tools.extend(data, e.data)); - } - }); - } + function (DomParser, Node, Serializer, Tools, Settings) { + var parseHeader = function (head) { + // Parse the contents with a DOM parser + return new DomParser({ + validate: false, + root_name: '#document' + }).parse(head); + }; + + var htmlToData = function (editor, head) { + var headerFragment = parseHeader(head), data = {}, elm, matches; - function htmlToData() { - var headerFragment = parseHeader(), data = {}, elm, matches; + function getAttr(elm, name) { + var value = elm.attr(name); - function getAttr(elm, name) { - var value = elm.attr(name); + return value || ''; + } - return value || ''; + // Default some values + // TODO: Not sure these are used anymore + data.fontface = Settings.getDefaultFontFamily(editor); + data.fontsize = Settings.getDefaultFontSize(editor); + + // Parse XML PI + elm = headerFragment.firstChild; + if (elm.type === 7) { + data.xml_pi = true; + matches = /encoding="([^"]+)"/.exec(elm.value); + if (matches) { + data.docencoding = matches[1]; } + } - // Default some values - data.fontface = editor.getParam("fullpage_default_fontface", ""); - data.fontsize = editor.getParam("fullpage_default_fontsize", ""); + // Parse doctype + elm = headerFragment.getAll('#doctype')[0]; + if (elm) { + data.doctype = '"; + } + + // Parse title element + elm = headerFragment.getAll('title')[0]; + if (elm && elm.firstChild) { + data.title = elm.firstChild.value; + } + + // Parse meta elements + Tools.each(headerFragment.getAll('meta'), function (meta) { + var name = meta.attr('name'), httpEquiv = meta.attr('http-equiv'), matches; + + if (name) { + data[name.toLowerCase()] = meta.attr('content'); + } else if (httpEquiv === "Content-Type") { + matches = /charset\s*=\s*(.*)\s*/gi.exec(meta.attr('content')); - // Parse XML PI - elm = headerFragment.firstChild; - if (elm.type == 7) { - data.xml_pi = true; - matches = /encoding="([^"]+)"/.exec(elm.value); if (matches) { data.docencoding = matches[1]; } } + }); - // Parse doctype - elm = headerFragment.getAll('#doctype')[0]; - if (elm) { - data.doctype = '"; - } + // Parse html attribs + elm = headerFragment.getAll('html')[0]; + if (elm) { + data.langcode = getAttr(elm, 'lang') || getAttr(elm, 'xml:lang'); + } - // Parse title element - elm = headerFragment.getAll('title')[0]; - if (elm && elm.firstChild) { - data.title = elm.firstChild.value; + // Parse stylesheets + data.stylesheets = []; + Tools.each(headerFragment.getAll('link'), function (link) { + if (link.attr('rel') === 'stylesheet') { + data.stylesheets.push(link.attr('href')); } + }); - // Parse meta elements - each(headerFragment.getAll('meta'), function (meta) { - var name = meta.attr('name'), httpEquiv = meta.attr('http-equiv'), matches; + // Parse body parts + elm = headerFragment.getAll('body')[0]; + if (elm) { + data.langdir = getAttr(elm, 'dir'); + data.style = getAttr(elm, 'style'); + data.visited_color = getAttr(elm, 'vlink'); + data.link_color = getAttr(elm, 'link'); + data.active_color = getAttr(elm, 'alink'); + } - if (name) { - data[name.toLowerCase()] = meta.attr('content'); - } else if (httpEquiv == "Content-Type") { - matches = /charset\s*=\s*(.*)\s*/gi.exec(meta.attr('content')); + return data; + }; - if (matches) { - data.docencoding = matches[1]; - } - } - }); + var dataToHtml = function (editor, data, head) { + var headerFragment, headElement, html, elm, value, dom = editor.dom; - // Parse html attribs - elm = headerFragment.getAll('html')[0]; - if (elm) { - data.langcode = getAttr(elm, 'lang') || getAttr(elm, 'xml:lang'); + function setAttr(elm, name, value) { + elm.attr(name, value ? value : undefined); + } + + function addHeadNode(node) { + if (headElement.firstChild) { + headElement.insert(node, headElement.firstChild); + } else { + headElement.append(node); } + } - // Parse stylesheets - data.stylesheets = []; - Tools.each(headerFragment.getAll('link'), function (link) { - if (link.attr('rel') == 'stylesheet') { - data.stylesheets.push(link.attr('href')); - } - }); + headerFragment = parseHeader(head); + headElement = headerFragment.getAll('head')[0]; + if (!headElement) { + elm = headerFragment.getAll('html')[0]; + headElement = new Node('head', 1); - // Parse body parts - elm = headerFragment.getAll('body')[0]; - if (elm) { - data.langdir = getAttr(elm, 'dir'); - data.style = getAttr(elm, 'style'); - data.visited_color = getAttr(elm, 'vlink'); - data.link_color = getAttr(elm, 'link'); - data.active_color = getAttr(elm, 'alink'); + if (elm.firstChild) { + elm.insert(headElement, elm.firstChild, true); + } else { + elm.append(headElement); } - - return data; } - function dataToHtml(data) { - var headerFragment, headElement, html, elm, value, dom = editor.dom; + // Add/update/remove XML-PI + elm = headerFragment.firstChild; + if (data.xml_pi) { + value = 'version="1.0"'; - function setAttr(elm, name, value) { - elm.attr(name, value ? value : undefined); + if (data.docencoding) { + value += ' encoding="' + data.docencoding + '"'; } - function addHeadNode(node) { - if (headElement.firstChild) { - headElement.insert(node, headElement.firstChild); - } else { - headElement.append(node); - } + if (elm.type !== 7) { + elm = new Node('xml', 7); + headerFragment.insert(elm, headerFragment.firstChild, true); } - headerFragment = parseHeader(); - headElement = headerFragment.getAll('head')[0]; - if (!headElement) { - elm = headerFragment.getAll('html')[0]; - headElement = new Node('head', 1); + elm.value = value; + } else if (elm && elm.type === 7) { + elm.remove(); + } + + // Add/update/remove doctype + elm = headerFragment.getAll('#doctype')[0]; + if (data.doctype) { + if (!elm) { + elm = new Node('#doctype', 10); - if (elm.firstChild) { - elm.insert(headElement, elm.firstChild, true); + if (data.xml_pi) { + headerFragment.insert(elm, headerFragment.firstChild); } else { - elm.append(headElement); + addHeadNode(elm); } } - // Add/update/remove XML-PI - elm = headerFragment.firstChild; - if (data.xml_pi) { - value = 'version="1.0"'; + elm.value = data.doctype.substring(9, data.doctype.length - 1); + } else if (elm) { + elm.remove(); + } - if (data.docencoding) { - value += ' encoding="' + data.docencoding + '"'; - } + // Add meta encoding + elm = null; + Tools.each(headerFragment.getAll('meta'), function (meta) { + if (meta.attr('http-equiv') === 'Content-Type') { + elm = meta; + } + }); - if (elm.type != 7) { - elm = new Node('xml', 7); - headerFragment.insert(elm, headerFragment.firstChild, true); - } + if (data.docencoding) { + if (!elm) { + elm = new Node('meta', 1); + elm.attr('http-equiv', 'Content-Type'); + elm.shortEnded = true; + addHeadNode(elm); + } - elm.value = value; - } else if (elm && elm.type == 7) { - elm.remove(); + elm.attr('content', 'text/html; charset=' + data.docencoding); + } else if (elm) { + elm.remove(); + } + + // Add/update/remove title + elm = headerFragment.getAll('title')[0]; + if (data.title) { + if (!elm) { + elm = new Node('title', 1); + addHeadNode(elm); + } else { + elm.empty(); } - // Add/update/remove doctype - elm = headerFragment.getAll('#doctype')[0]; - if (data.doctype) { - if (!elm) { - elm = new Node('#doctype', 10); + elm.append(new Node('#text', 3)).value = data.title; + } else if (elm) { + elm.remove(); + } + + // Add/update/remove meta + Tools.each('keywords,description,author,copyright,robots'.split(','), function (name) { + var nodes = headerFragment.getAll('meta'), i, meta, value = data[name]; - if (data.xml_pi) { - headerFragment.insert(elm, headerFragment.firstChild); + for (i = 0; i < nodes.length; i++) { + meta = nodes[i]; + + if (meta.attr('name') === name) { + if (value) { + meta.attr('content', value); } else { - addHeadNode(elm); + meta.remove(); } - } - elm.value = data.doctype.substring(9, data.doctype.length - 1); - } else if (elm) { - elm.remove(); - } - - // Add meta encoding - elm = null; - each(headerFragment.getAll('meta'), function (meta) { - if (meta.attr('http-equiv') == 'Content-Type') { - elm = meta; + return; } - }); + } - if (data.docencoding) { - if (!elm) { - elm = new Node('meta', 1); - elm.attr('http-equiv', 'Content-Type'); - elm.shortEnded = true; - addHeadNode(elm); - } + if (value) { + elm = new Node('meta', 1); + elm.attr('name', name); + elm.attr('content', value); + elm.shortEnded = true; - elm.attr('content', 'text/html; charset=' + data.docencoding); - } else if (elm) { - elm.remove(); + addHeadNode(elm); } + }); - // Add/update/remove title - elm = headerFragment.getAll('title')[0]; - if (data.title) { - if (!elm) { - elm = new Node('title', 1); - addHeadNode(elm); - } else { - elm.empty(); - } + var currentStyleSheetsMap = {}; + Tools.each(headerFragment.getAll('link'), function (stylesheet) { + if (stylesheet.attr('rel') === 'stylesheet') { + currentStyleSheetsMap[stylesheet.attr('href')] = stylesheet; + } + }); - elm.append(new Node('#text', 3)).value = data.title; - } else if (elm) { - elm.remove(); + // Add new + Tools.each(data.stylesheets, function (stylesheet) { + if (!currentStyleSheetsMap[stylesheet]) { + elm = new Node('link', 1); + elm.attr({ + rel: 'stylesheet', + text: 'text/css', + href: stylesheet + }); + elm.shortEnded = true; + addHeadNode(elm); } - // Add/update/remove meta - each('keywords,description,author,copyright,robots'.split(','), function (name) { - var nodes = headerFragment.getAll('meta'), i, meta, value = data[name]; + delete currentStyleSheetsMap[stylesheet]; + }); - for (i = 0; i < nodes.length; i++) { - meta = nodes[i]; + // Delete old + Tools.each(currentStyleSheetsMap, function (stylesheet) { + stylesheet.remove(); + }); - if (meta.attr('name') == name) { - if (value) { - meta.attr('content', value); - } else { - meta.remove(); - } + // Update body attributes + elm = headerFragment.getAll('body')[0]; + if (elm) { + setAttr(elm, 'dir', data.langdir); + setAttr(elm, 'style', data.style); + setAttr(elm, 'vlink', data.visited_color); + setAttr(elm, 'link', data.link_color); + setAttr(elm, 'alink', data.active_color); + + // Update iframe body as well + dom.setAttribs(editor.getBody(), { + style: data.style, + dir: data.dir, + vLink: data.visited_color, + link: data.link_color, + aLink: data.active_color + }); + } - return; - } - } + // Set html attributes + elm = headerFragment.getAll('html')[0]; + if (elm) { + setAttr(elm, 'lang', data.langcode); + setAttr(elm, 'xml:lang', data.langcode); + } - if (value) { - elm = new Node('meta', 1); - elm.attr('name', name); - elm.attr('content', value); - elm.shortEnded = true; + // No need for a head element + if (!headElement.firstChild) { + headElement.remove(); + } - addHeadNode(elm); - } - }); + // Serialize header fragment and crop away body part + html = new Serializer({ + validate: false, + indent: true, + apply_source_formatting: true, + indent_before: 'head,html,body,meta,title,script,link,style', + indent_after: 'head,html,body,meta,title,script,link,style' + }).serialize(headerFragment); - var currentStyleSheetsMap = {}; - Tools.each(headerFragment.getAll('link'), function (stylesheet) { - if (stylesheet.attr('rel') == 'stylesheet') { - currentStyleSheetsMap[stylesheet.attr('href')] = stylesheet; - } - }); + return html.substring(0, html.indexOf('')); + }; - // Add new - Tools.each(data.stylesheets, function (stylesheet) { - if (!currentStyleSheetsMap[stylesheet]) { - elm = new Node('link', 1); - elm.attr({ - rel: 'stylesheet', - text: 'text/css', - href: stylesheet - }); - elm.shortEnded = true; - addHeadNode(elm); - } + return { + parseHeader: parseHeader, + htmlToData: htmlToData, + dataToHtml: dataToHtml + }; + } +); - delete currentStyleSheetsMap[stylesheet]; - }); +/** + * Dialog.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 + */ - // Delete old - Tools.each(currentStyleSheetsMap, function (stylesheet) { - stylesheet.remove(); - }); +define( + 'tinymce.plugins.fullpage.ui.Dialog', + [ + 'tinymce.core.util.Tools', + 'tinymce.plugins.fullpage.core.Parser' + ], + function (Tools, Parser) { + var open = function (editor, headState) { + var data = Parser.htmlToData(editor, headState.get()); - // Update body attributes - elm = headerFragment.getAll('body')[0]; - if (elm) { - setAttr(elm, 'dir', data.langdir); - setAttr(elm, 'style', data.style); - setAttr(elm, 'vlink', data.visited_color); - setAttr(elm, 'link', data.link_color); - setAttr(elm, 'alink', data.active_color); - - // Update iframe body as well - dom.setAttribs(editor.getBody(), { - style: data.style, - dir: data.dir, - vLink: data.visited_color, - link: data.link_color, - aLink: data.active_color - }); + editor.windowManager.open({ + title: 'Document properties', + data: data, + defaults: { type: 'textbox', size: 40 }, + body: [ + { name: 'title', label: 'Title' }, + { name: 'keywords', label: 'Keywords' }, + { name: 'description', label: 'Description' }, + { name: 'robots', label: 'Robots' }, + { name: 'author', label: 'Author' }, + { name: 'docencoding', label: 'Encoding' } + ], + onSubmit: function (e) { + var headHtml = Parser.dataToHtml(editor, Tools.extend(data, e.data), headState.get()); + headState.set(headHtml); } + }); + }; - // Set html attributes - elm = headerFragment.getAll('html')[0]; - if (elm) { - setAttr(elm, 'lang', data.langcode); - setAttr(elm, 'xml:lang', data.langcode); - } + return { + open: open + }; + } +); - // No need for a head element - if (!headElement.firstChild) { - headElement.remove(); - } +/** + * 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 + */ - // Serialize header fragment and crop away body part - html = new Serializer({ - validate: false, - indent: true, - apply_source_formatting: true, - indent_before: 'head,html,body,meta,title,script,link,style', - indent_after: 'head,html,body,meta,title,script,link,style' - }).serialize(headerFragment); +define( + 'tinymce.plugins.fullpage.api.Commands', + [ + 'tinymce.plugins.fullpage.ui.Dialog' + ], + function (Dialog) { + var register = function (editor, headState) { + editor.addCommand('mceFullPageProperties', function () { + Dialog.open(editor, headState); + }); + }; - head = html.substring(0, html.indexOf('')); - } + return { + register: register + }; + } +); +/** + * Protect.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 + */ - function parseHeader() { - // Parse the contents with a DOM parser - return new DomParser({ - validate: false, - root_name: '#document' - }).parse(head); - } +define( + 'tinymce.plugins.fullpage.core.Protect', + [ + 'tinymce.core.util.Tools' + ], + function (Tools) { + var protectHtml = function (protect, html) { + Tools.each(protect, function (pattern) { + html = html.replace(pattern, function (str) { + return ''; + }); + }); - function setContent(evt) { - var startPos, endPos, content, headerFragment, styles = '', dom = editor.dom, elm; + return html; + }; - if (evt.selection) { - return; - } + var unprotectHtml = function (html) { + return html.replace(//g, function (a, m) { + return unescape(m); + }); + }; - function low(s) { - return s.replace(/<\/?[A-Z]+/g, function (a) { - return a.toLowerCase(); - }); - } + return { + protectHtml: protectHtml, + unprotectHtml: unprotectHtml + }; + } +); - content = Protect.protectHtml(editor.settings.protect, evt.content); +/** + * FilterContent.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 + */ - // Ignore raw updated if we already have a head, this will fix issues with undo/redo keeping the head/foot separate - if (evt.format == 'raw' && head) { - return; - } +define( + 'tinymce.plugins.fullpage.core.FilterContent', + [ + 'tinymce.core.util.Tools', + 'tinymce.plugins.fullpage.api.Settings', + 'tinymce.plugins.fullpage.core.Parser', + 'tinymce.plugins.fullpage.core.Protect' + ], + function (Tools, Settings, Parser, Protect) { + var each = Tools.each; - if (evt.source_view && editor.getParam('fullpage_hide_in_source_view')) { - return; - } + var low = function (s) { + return s.replace(/<\/?[A-Z]+/g, function (a) { + return a.toLowerCase(); + }); + }; - // Fixed so new document/setContent('') doesn't remove existing header/footer except when it's in source code view - if (content.length === 0 && !evt.source_view) { - content = Tools.trim(head) + '\n' + Tools.trim(content) + '\n' + Tools.trim(foot); - } + var handleSetContent = function (editor, headState, footState, evt) { + var startPos, endPos, content, headerFragment, styles = '', dom = editor.dom, elm; - // Parse out head, body and footer - content = content.replace(/<(\/?)BODY/gi, '<$1body'); - startPos = content.indexOf('', startPos); - head = low(content.substring(0, startPos + 1)); + content = Protect.protectHtml(editor.settings.protect, evt.content); - endPos = content.indexOf('', startPos); + headState.set(low(content.substring(0, startPos + 1))); - var headElm = editor.getDoc().getElementsByTagName('head')[0]; + endPos = content.indexOf('\n'); + } - // Needed for IE 6/7 - elm = dom.get('fullpage_styles'); - if (elm.styleSheet) { - elm.styleSheet.cssText = styles; - } + // Parse header and update iframe + headerFragment = Parser.parseHeader(headState.get()); + each(headerFragment.getAll('style'), function (node) { + if (node.firstChild) { + styles += node.firstChild.value; } + }); - var currentStyleSheetsMap = {}; - Tools.each(headElm.getElementsByTagName('link'), function (stylesheet) { - if (stylesheet.rel == 'stylesheet' && stylesheet.getAttribute('data-mce-fullpage')) { - currentStyleSheetsMap[stylesheet.href] = stylesheet; - } + elm = headerFragment.getAll('body')[0]; + if (elm) { + dom.setAttribs(editor.getBody(), { + style: elm.attr('style') || '', + dir: elm.attr('dir') || '', + vLink: elm.attr('vlink') || '', + link: elm.attr('link') || '', + aLink: elm.attr('alink') || '' }); + } - // Add new - Tools.each(headerFragment.getAll('link'), function (stylesheet) { - var href = stylesheet.attr('href'); - if (!href) { - return true; - } + dom.remove('fullpage_styles'); - if (!currentStyleSheetsMap[href] && stylesheet.attr('rel') == 'stylesheet') { - dom.add(headElm, 'link', { - rel: 'stylesheet', - text: 'text/css', - href: href, - 'data-mce-fullpage': '1' - }); - } + var headElm = editor.getDoc().getElementsByTagName('head')[0]; - delete currentStyleSheetsMap[href]; - }); + if (styles) { + dom.add(headElm, 'style', { + id: 'fullpage_styles' + }, styles); - // Delete old - Tools.each(currentStyleSheetsMap, function (stylesheet) { - stylesheet.parentNode.removeChild(stylesheet); - }); + // Needed for IE 6/7 + elm = dom.get('fullpage_styles'); + if (elm.styleSheet) { + elm.styleSheet.cssText = styles; + } } - function getDefaultHeader() { - var header = '', value, styles = ''; - - if (editor.getParam('fullpage_default_xml_pi')) { - header += '\n'; + var currentStyleSheetsMap = {}; + Tools.each(headElm.getElementsByTagName('link'), function (stylesheet) { + if (stylesheet.rel === 'stylesheet' && stylesheet.getAttribute('data-mce-fullpage')) { + currentStyleSheetsMap[stylesheet.href] = stylesheet; } + }); - header += editor.getParam('fullpage_default_doctype', ''); - header += '\n\n\n'; - - if ((value = editor.getParam('fullpage_default_title'))) { - header += '' + value + '\n'; + // Add new + Tools.each(headerFragment.getAll('link'), function (stylesheet) { + var href = stylesheet.attr('href'); + if (!href) { + return true; } - if ((value = editor.getParam('fullpage_default_encoding'))) { - header += '\n'; + if (!currentStyleSheetsMap[href] && stylesheet.attr('rel') === 'stylesheet') { + dom.add(headElm, 'link', { + rel: 'stylesheet', + text: 'text/css', + href: href, + 'data-mce-fullpage': '1' + }); } - if ((value = editor.getParam('fullpage_default_font_family'))) { - styles += 'font-family: ' + value + ';'; - } + delete currentStyleSheetsMap[href]; + }); - if ((value = editor.getParam('fullpage_default_font_size'))) { - styles += 'font-size: ' + value + ';'; - } + // Delete old + Tools.each(currentStyleSheetsMap, function (stylesheet) { + stylesheet.parentNode.removeChild(stylesheet); + }); + }; - if ((value = editor.getParam('fullpage_default_text_color'))) { - styles += 'color: ' + value + ';'; - } + var getDefaultHeader = function (editor) { + var header = '', value, styles = ''; + + if (Settings.getDefaultXmlPi(editor)) { + var piEncoding = Settings.getDefaultEncoding(editor); + header += '\n'; + } - header += '\n\n'; + header += Settings.getDefaultDocType(editor); + header += '\n\n\n'; - return header; + if ((value = Settings.getDefaultTitle(editor))) { + header += '' + value + '\n'; } - function getContent(evt) { - if (!evt.selection && (!evt.source_view || !editor.getParam('fullpage_hide_in_source_view'))) { - evt.content = Protect.unprotectHtml(Tools.trim(head) + '\n' + Tools.trim(evt.content) + '\n' + Tools.trim(foot)); - } + if ((value = Settings.getDefaultEncoding(editor))) { + header += '\n'; + } + + if ((value = Settings.getDefaultFontFamily(editor))) { + styles += 'font-family: ' + value + ';'; + } + + if ((value = Settings.getDefaultFontSize(editor))) { + styles += 'font-size: ' + value + ';'; + } + + if ((value = Settings.getDefaultTextColor(editor))) { + styles += 'color: ' + value + ';'; + } + + header += '\n\n'; + + return header; + }; + + var handleGetContent = function (editor, head, foot, evt) { + if (!evt.selection && (!evt.source_view || !Settings.shouldHideInSourceView(editor))) { + evt.content = Protect.unprotectHtml(Tools.trim(head) + '\n' + Tools.trim(evt.content) + '\n' + Tools.trim(foot)); } + }; + + var setup = function (editor, headState, footState) { + editor.on('BeforeSetContent', function (evt) { + handleSetContent(editor, headState, footState, evt); + }); + editor.on('GetContent', function (evt) { + handleGetContent(editor, headState.get(), footState.get(), evt); + }); + }; - editor.addCommand('mceFullPageProperties', showDialog); + return { + setup: setup + }; + } +); +/** + * Buttons.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.fullpage.ui.Buttons', + [ + ], + function () { + var register = function (editor) { editor.addButton('fullpage', { title: 'Document properties', cmd: 'mceFullPageProperties' @@ -719,10 +920,41 @@ define( cmd: 'mceFullPageProperties', context: 'file' }); + }; - editor.on('BeforeSetContent', setContent); - editor.on('GetContent', getContent); + return { + register: register + }; + } +); +/** + * Plugin.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.fullpage.Plugin', + [ + 'ephox.katamari.api.Cell', + 'tinymce.core.PluginManager', + 'tinymce.plugins.fullpage.api.Commands', + 'tinymce.plugins.fullpage.core.FilterContent', + 'tinymce.plugins.fullpage.ui.Buttons' + ], + function (Cell, PluginManager, Commands, FilterContent, Buttons) { + PluginManager.add('fullpage', function (editor) { + var headState = Cell(''), footState = Cell(''); + + Commands.register(editor, headState); + Buttons.register(editor); + FilterContent.setup(editor, headState, footState); }); + return function () { }; } ); diff --git a/media/vendor/tinymce/plugins/fullpage/plugin.min.js b/media/vendor/tinymce/plugins/fullpage/plugin.min.js index 6aa103dd6709f..87055444e7efc 100644 --- a/media/vendor/tinymce/plugins/fullpage/plugin.min.js +++ b/media/vendor/tinymce/plugins/fullpage/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"})}),c},c=function(a){return a.replace(//g,function(a,b){return unescape(b)})};return{protectHtml:b,unprotectHtml:c}}),g("0",["1","2","3","4","5","6"],function(a,b,c,d,e,f){return d.add("fullpage",function(d){function g(){var a=h();d.windowManager.open({title:"Document properties",data:a,defaults:{type:"textbox",size:40},body:[{name:"title",label:"Title"},{name:"keywords",label:"Keywords"},{name:"description",label:"Description"},{name:"robots",label:"Robots"},{name:"author",label:"Author"},{name:"docencoding",label:"Encoding"}],onSubmit:function(b){i(e.extend(a,b.data))}})}function h(){function a(a,b){var c=a.attr(b);return c||""}var b,c,f=j(),g={};return g.fontface=d.getParam("fullpage_default_fontface",""),g.fontsize=d.getParam("fullpage_default_fontsize",""),b=f.firstChild,7==b.type&&(g.xml_pi=!0,c=/encoding="([^"]+)"/.exec(b.value),c&&(g.docencoding=c[1])),b=f.getAll("#doctype")[0],b&&(g.doctype=""),b=f.getAll("title")[0],b&&b.firstChild&&(g.title=b.firstChild.value),p(f.getAll("meta"),function(a){var b,c=a.attr("name"),d=a.attr("http-equiv");c?g[c.toLowerCase()]=a.attr("content"):"Content-Type"==d&&(b=/charset\s*=\s*(.*)\s*/gi.exec(a.attr("content")),b&&(g.docencoding=b[1]))}),b=f.getAll("html")[0],b&&(g.langcode=a(b,"lang")||a(b,"xml:lang")),g.stylesheets=[],e.each(f.getAll("link"),function(a){"stylesheet"==a.attr("rel")&&g.stylesheets.push(a.attr("href"))}),b=f.getAll("body")[0],b&&(g.langdir=a(b,"dir"),g.style=a(b,"style"),g.visited_color=a(b,"vlink"),g.link_color=a(b,"link"),g.active_color=a(b,"alink")),g}function i(a){function f(a,b,c){a.attr(b,c?c:void 0)}function g(a){i.firstChild?i.insert(a,i.firstChild):i.append(a)}var h,i,k,l,m,o=d.dom;h=j(),i=h.getAll("head")[0],i||(l=h.getAll("html")[0],i=new b("head",1),l.firstChild?l.insert(i,l.firstChild,!0):l.append(i)),l=h.firstChild,a.xml_pi?(m='version="1.0"',a.docencoding&&(m+=' encoding="'+a.docencoding+'"'),7!=l.type&&(l=new b("xml",7),h.insert(l,h.firstChild,!0)),l.value=m):l&&7==l.type&&l.remove(),l=h.getAll("#doctype")[0],a.doctype?(l||(l=new b("#doctype",10),a.xml_pi?h.insert(l,h.firstChild):g(l)),l.value=a.doctype.substring(9,a.doctype.length-1)):l&&l.remove(),l=null,p(h.getAll("meta"),function(a){"Content-Type"==a.attr("http-equiv")&&(l=a)}),a.docencoding?(l||(l=new b("meta",1),l.attr("http-equiv","Content-Type"),l.shortEnded=!0,g(l)),l.attr("content","text/html; charset="+a.docencoding)):l&&l.remove(),l=h.getAll("title")[0],a.title?(l?l.empty():(l=new b("title",1),g(l)),l.append(new b("#text",3)).value=a.title):l&&l.remove(),p("keywords,description,author,copyright,robots".split(","),function(c){var d,e,f=h.getAll("meta"),i=a[c];for(d=0;d"))}function j(){return new a({validate:!1,root_name:"#document"}).parse(n)}function k(a){function b(a){return a.replace(/<\/?[A-Z]+/g,function(a){return a.toLowerCase()})}var c,g,h,i,k,m="",q=d.dom;if(!(a.selection||(h=f.protectHtml(d.settings.protect,a.content),"raw"==a.format&&n||a.source_view&&d.getParam("fullpage_hide_in_source_view")))){0!==h.length||a.source_view||(h=e.trim(n)+"\n"+e.trim(h)+"\n"+e.trim(o)),h=h.replace(/<(\/?)BODY/gi,"<$1body"),c=h.indexOf("",c),n=b(h.substring(0,c+1)),g=h.indexOf("\n"),i=j(),p(i.getAll("style"),function(a){a.firstChild&&(m+=a.firstChild.value)}),k=i.getAll("body")[0],k&&q.setAttribs(d.getBody(),{style:k.attr("style")||"",dir:k.attr("dir")||"",vLink:k.attr("vlink")||"",link:k.attr("link")||"",aLink:k.attr("alink")||""}),q.remove("fullpage_styles");var r=d.getDoc().getElementsByTagName("head")[0];m&&(q.add(r,"style",{id:"fullpage_styles"},m),k=q.get("fullpage_styles"),k.styleSheet&&(k.styleSheet.cssText=m));var s={};e.each(r.getElementsByTagName("link"),function(a){"stylesheet"==a.rel&&a.getAttribute("data-mce-fullpage")&&(s[a.href]=a)}),e.each(i.getAll("link"),function(a){var b=a.attr("href");return!b||(s[b]||"stylesheet"!=a.attr("rel")||q.add(r,"link",{rel:"stylesheet",text:"text/css",href:b,"data-mce-fullpage":"1"}),void delete s[b])}),e.each(s,function(a){a.parentNode.removeChild(a)})}}function l(){var a,b="",c="";return d.getParam("fullpage_default_xml_pi")&&(b+='\n'),b+=d.getParam("fullpage_default_doctype",""),b+="\n\n\n",(a=d.getParam("fullpage_default_title"))&&(b+=""+a+"\n"),(a=d.getParam("fullpage_default_encoding"))&&(b+='\n'),(a=d.getParam("fullpage_default_font_family"))&&(c+="font-family: "+a+";"),(a=d.getParam("fullpage_default_font_size"))&&(c+="font-size: "+a+";"),(a=d.getParam("fullpage_default_text_color"))&&(c+="color: "+a+";"),b+="\n\n"}function m(a){a.selection||a.source_view&&d.getParam("fullpage_hide_in_source_view")||(a.content=f.unprotectHtml(e.trim(n)+"\n"+e.trim(a.content)+"\n"+e.trim(o)))}var n,o,p=e.each;d.addCommand("mceFullPageProperties",g),d.addButton("fullpage",{title:"Document properties",cmd:"mceFullPageProperties"}),d.addMenuItem("fullpage",{text:"Document properties",cmd:"mceFullPageProperties",context:"file"}),d.on("BeforeSetContent",k),d.on("GetContent",m)}),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")};return{shouldHideInSourceView:a,getDefaultXmlPi:b,getDefaultEncoding:c,getDefaultFontFamily:d,getDefaultFontSize:e,getDefaultTextColor:f,getDefaultTitle:g,getDefaultDocType:h}}),g("a",["c","d","e","8","9"],function(a,b,c,d,e){var f=function(b){return new a({validate:!1,root_name:"#document"}).parse(b)},g=function(a,b){function c(a,b){var c=a.attr(b);return c||""}var g,h,i=f(b),j={};return j.fontface=e.getDefaultFontFamily(a),j.fontsize=e.getDefaultFontSize(a),g=i.firstChild,7===g.type&&(j.xml_pi=!0,h=/encoding="([^"]+)"/.exec(g.value),h&&(j.docencoding=h[1])),g=i.getAll("#doctype")[0],g&&(j.doctype=""),g=i.getAll("title")[0],g&&g.firstChild&&(j.title=g.firstChild.value),d.each(i.getAll("meta"),function(a){var b,c=a.attr("name"),d=a.attr("http-equiv");c?j[c.toLowerCase()]=a.attr("content"):"Content-Type"===d&&(b=/charset\s*=\s*(.*)\s*/gi.exec(a.attr("content")),b&&(j.docencoding=b[1]))}),g=i.getAll("html")[0],g&&(j.langcode=c(g,"lang")||c(g,"xml:lang")),j.stylesheets=[],d.each(i.getAll("link"),function(a){"stylesheet"===a.attr("rel")&&j.stylesheets.push(a.attr("href"))}),g=i.getAll("body")[0],g&&(j.langdir=c(g,"dir"),j.style=c(g,"style"),j.visited_color=c(g,"vlink"),j.link_color=c(g,"link"),j.active_color=c(g,"alink")),j},h=function(a,e,g){function h(a,b,c){a.attr(b,c?c:void 0)}function i(a){k.firstChild?k.insert(a,k.firstChild):k.append(a)}var j,k,l,m,n,o=a.dom;j=f(g),k=j.getAll("head")[0],k||(m=j.getAll("html")[0],k=new b("head",1),m.firstChild?m.insert(k,m.firstChild,!0):m.append(k)),m=j.firstChild,e.xml_pi?(n='version="1.0"',e.docencoding&&(n+=' encoding="'+e.docencoding+'"'),7!==m.type&&(m=new b("xml",7),j.insert(m,j.firstChild,!0)),m.value=n):m&&7===m.type&&m.remove(),m=j.getAll("#doctype")[0],e.doctype?(m||(m=new b("#doctype",10),e.xml_pi?j.insert(m,j.firstChild):i(m)),m.value=e.doctype.substring(9,e.doctype.length-1)):m&&m.remove(),m=null,d.each(j.getAll("meta"),function(a){"Content-Type"===a.attr("http-equiv")&&(m=a)}),e.docencoding?(m||(m=new b("meta",1),m.attr("http-equiv","Content-Type"),m.shortEnded=!0,i(m)),m.attr("content","text/html; charset="+e.docencoding)):m&&m.remove(),m=j.getAll("title")[0],e.title?(m?m.empty():(m=new b("title",1),i(m)),m.append(new b("#text",3)).value=e.title):m&&m.remove(),d.each("keywords,description,author,copyright,robots".split(","),function(a){var c,d,f=j.getAll("meta"),g=e[a];for(c=0;c"))};return{parseHeader:f,htmlToData:g,dataToHtml:h}}),g("7",["8","a"],function(a,b){var c=function(c,d){var e=b.htmlToData(c,d.get());c.windowManager.open({title:"Document properties",data:e,defaults:{type:"textbox",size:40},body:[{name:"title",label:"Title"},{name:"keywords",label:"Keywords"},{name:"description",label:"Description"},{name:"robots",label:"Robots"},{name:"author",label:"Author"},{name:"docencoding",label:"Encoding"}],onSubmit:function(f){var g=b.dataToHtml(c,a.extend(e,f.data),d.get());d.set(g)}})};return{open:c}}),g("3",["7"],function(a){var b=function(b,c){b.addCommand("mceFullPageProperties",function(){a.open(b,c)})};return{register:b}}),g("b",["8"],function(a){var b=function(b,c){return a.each(b,function(a){c=c.replace(a,function(a){return""})}),c},c=function(a){return a.replace(//g,function(a,b){return unescape(b)})};return{protectHtml:b,unprotectHtml:c}}),g("4",["8","9","a","b"],function(a,b,c,d){var e=a.each,f=function(a){return a.replace(/<\/?[A-Z]+/g,function(a){return a.toLowerCase()})},g=function(g,i,j,k){var l,m,n,o,p,q="",r=g.dom;if(!(k.selection||(n=d.protectHtml(g.settings.protect,k.content),"raw"===k.format&&i.get()||k.source_view&&b.shouldHideInSourceView(g)))){0!==n.length||k.source_view||(n=a.trim(i.get())+"\n"+a.trim(n)+"\n"+a.trim(j.get())),n=n.replace(/<(\/?)BODY/gi,"<$1body"),l=n.indexOf("",l),i.set(f(n.substring(0,l+1))),m=n.indexOf("\n")),o=c.parseHeader(i.get()),e(o.getAll("style"),function(a){a.firstChild&&(q+=a.firstChild.value)}),p=o.getAll("body")[0],p&&r.setAttribs(g.getBody(),{style:p.attr("style")||"",dir:p.attr("dir")||"",vLink:p.attr("vlink")||"",link:p.attr("link")||"",aLink:p.attr("alink")||""}),r.remove("fullpage_styles");var s=g.getDoc().getElementsByTagName("head")[0];q&&(r.add(s,"style",{id:"fullpage_styles"},q),p=r.get("fullpage_styles"),p.styleSheet&&(p.styleSheet.cssText=q));var t={};a.each(s.getElementsByTagName("link"),function(a){"stylesheet"===a.rel&&a.getAttribute("data-mce-fullpage")&&(t[a.href]=a)}),a.each(o.getAll("link"),function(a){var b=a.attr("href");return!b||(t[b]||"stylesheet"!==a.attr("rel")||r.add(s,"link",{rel:"stylesheet",text:"text/css",href:b,"data-mce-fullpage":"1"}),void delete t[b])}),a.each(t,function(a){a.parentNode.removeChild(a)})}},h=function(a){var c,d="",e="";if(b.getDefaultXmlPi(a)){var f=b.getDefaultEncoding(a);d+='\n'}return d+=b.getDefaultDocType(a),d+="\n\n\n",(c=b.getDefaultTitle(a))&&(d+=""+c+"\n"),(c=b.getDefaultEncoding(a))&&(d+='\n'),(c=b.getDefaultFontFamily(a))&&(e+="font-family: "+c+";"),(c=b.getDefaultFontSize(a))&&(e+="font-size: "+c+";"),(c=b.getDefaultTextColor(a))&&(e+="color: "+c+";"),d+="\n\n"},i=function(c,e,f,g){g.selection||g.source_view&&b.shouldHideInSourceView(c)||(g.content=d.unprotectHtml(a.trim(e)+"\n"+a.trim(g.content)+"\n"+a.trim(f)))},j=function(a,b,c){a.on("BeforeSetContent",function(d){g(a,b,c,d)}),a.on("GetContent",function(d){i(a,b.get(),c.get(),d)})};return{setup:j}}),g("5",[],function(){var a=function(a){a.addButton("fullpage",{title:"Document properties",cmd:"mceFullPageProperties"}),a.addMenuItem("fullpage",{text:"Document properties",cmd:"mceFullPageProperties",context:"file"})};return{register:a}}),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return b.add("fullpage",function(b){var f=a(""),g=a("");c.register(b,f),e.register(b),d.setup(b,f,g)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/fullscreen/plugin.js b/media/vendor/tinymce/plugins/fullscreen/plugin.js index 76bfcfcb9d960..cc3e81fbf3f40 100644 --- a/media/vendor/tinymce/plugins/fullscreen/plugin.js +++ b/media/vendor/tinymce/plugins/fullscreen/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,13 +76,46 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.fullscreen.Plugin","tinymce.core.dom.DOMUtils","tinymce.core.PluginManager","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.fullscreen.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.fullscreen.api.Api","tinymce.plugins.fullscreen.api.Commands","tinymce.plugins.fullscreen.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.fullscreen.core.Actions","global!document","global!window","tinymce.core.dom.DOMUtils","tinymce.plugins.fullscreen.api.Events"] jsc*/ +define( + 'ephox.katamari.api.Cell', + + [ + ], + + function () { + var Cell = function (initial) { + var value = initial; + + var get = function () { + return value; + }; + + var set = function (v) { + value = v; + }; + + var clone = function () { + return Cell(get()); + }; + + return { + get: get, + set: set, + clone: clone + }; + }; + + return Cell; + } +); + defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** * ResolveGlobal.js @@ -95,15 +128,46 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.dom.DOMUtils', + 'tinymce.core.PluginManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.dom.DOMUtils'); + return resolve('tinymce.PluginManager'); + } +); + +/** + * Api.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.fullscreen.api.Api', + [ + ], + function () { + var get = function (fullscreenState) { + return { + isFullscreen: function () { + return fullscreenState.get() !== null; + } + }; + }; + + return { + get: get + }; } ); +defineGlobal("global!document", document); +defineGlobal("global!window", window); /** * ResolveGlobal.js * @@ -115,17 +179,17 @@ define( */ define( - 'tinymce.core.PluginManager', + 'tinymce.core.dom.DOMUtils', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.PluginManager'); + return resolve('tinymce.dom.DOMUtils'); } ); /** - * Plugin.js + * Events.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -134,162 +198,254 @@ define( * Contributing: http://www.tinymce.com/contributing */ +define( + 'tinymce.plugins.fullscreen.api.Events', + [ + ], + function () { + var fireFullscreenStateChanged = function (editor, state) { + editor.fire('FullscreenStateChanged', { state: state }); + }; + + return { + fireFullscreenStateChanged: fireFullscreenStateChanged + }; + } +); + /** - * This class contains all core logic for the fullscreen plugin. + * Actions.js * - * @class tinymce.fullscreen.Plugin - * @private + * 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.fullscreen.Plugin', + 'tinymce.plugins.fullscreen.core.Actions', [ + 'global!document', + 'global!window', 'tinymce.core.dom.DOMUtils', - 'tinymce.core.PluginManager' + 'tinymce.plugins.fullscreen.api.Events' ], - function (DOMUtils, PluginManager) { + function (document, window, DOMUtils, Events) { var DOM = DOMUtils.DOM; - PluginManager.add('fullscreen', function (editor) { - var fullscreenState = false, iframeWidth, iframeHeight, resizeHandler; - var containerWidth, containerHeight, scrollPos; + var getWindowSize = function () { + var w, h, win = window, doc = document; + var body = doc.body; - if (editor.settings.inline) { - return; + // Old IE + if (body.offsetWidth) { + w = body.offsetWidth; + h = body.offsetHeight; } - function getWindowSize() { - var w, h, win = window, doc = document; - var body = doc.body; + // Modern browsers + if (win.innerWidth && win.innerHeight) { + w = win.innerWidth; + h = win.innerHeight; + } - // Old IE - if (body.offsetWidth) { - w = body.offsetWidth; - h = body.offsetHeight; - } + return { w: w, h: h }; + }; - // Modern browsers - if (win.innerWidth && win.innerHeight) { - w = win.innerWidth; - h = win.innerHeight; - } + var getScrollPos = function () { + var vp = DOM.getViewPort(); - return { w: w, h: h }; - } + return { + x: vp.x, + y: vp.y + }; + }; + + var setScrollPos = function (pos) { + window.scrollTo(pos.x, pos.y); + }; - function getScrollPos() { - var vp = DOM.getViewPort(); + var toggleFullscreen = function (editor, fullscreenInfo) { + var body = document.body, documentElement = document.documentElement, editorContainerStyle; + var editorContainer, iframe, iframeStyle; + + var resize = function () { + DOM.setStyle(iframe, 'height', getWindowSize().h - (editorContainer.clientHeight - iframe.clientHeight)); + }; - return { - x: vp.x, - y: vp.y + var removeResize = function () { + DOM.unbind(window, 'resize', resize); + }; + + editorContainer = editor.getContainer(); + editorContainerStyle = editorContainer.style; + iframe = editor.getContentAreaContainer().firstChild; + iframeStyle = iframe.style; + + if (!fullscreenInfo) { + var newFullScreenInfo = { + scrollPos: getScrollPos(), + containerWidth: editorContainerStyle.width, + containerHeight: editorContainerStyle.height, + iframeWidth: iframeStyle.width, + iframeHeight: iframeStyle.height, + resizeHandler: resize, + removeHandler: removeResize }; - } - function setScrollPos(pos) { - window.scrollTo(pos.x, pos.y); - } + iframeStyle.width = iframeStyle.height = '100%'; + editorContainerStyle.width = editorContainerStyle.height = ''; + + DOM.addClass(body, 'mce-fullscreen'); + DOM.addClass(documentElement, 'mce-fullscreen'); + DOM.addClass(editorContainer, 'mce-fullscreen'); - function toggleFullscreen() { - var body = document.body, documentElement = document.documentElement, editorContainerStyle; - var editorContainer, iframe, iframeStyle; + DOM.bind(window, 'resize', resize); + editor.on('remove', removeResize); - function resize() { - DOM.setStyle(iframe, 'height', getWindowSize().h - (editorContainer.clientHeight - iframe.clientHeight)); + resize(); + + Events.fireFullscreenStateChanged(editor, true); + + return newFullScreenInfo; + } else { + iframeStyle.width = fullscreenInfo.iframeWidth; + iframeStyle.height = fullscreenInfo.iframeHeight; + + if (fullscreenInfo.containerWidth) { + editorContainerStyle.width = fullscreenInfo.containerWidth; } - fullscreenState = !fullscreenState; - - editorContainer = editor.getContainer(); - editorContainerStyle = editorContainer.style; - iframe = editor.getContentAreaContainer().firstChild; - iframeStyle = iframe.style; - - if (fullscreenState) { - scrollPos = getScrollPos(); - iframeWidth = iframeStyle.width; - iframeHeight = iframeStyle.height; - iframeStyle.width = iframeStyle.height = '100%'; - containerWidth = editorContainerStyle.width; - containerHeight = editorContainerStyle.height; - editorContainerStyle.width = editorContainerStyle.height = ''; - - DOM.addClass(body, 'mce-fullscreen'); - DOM.addClass(documentElement, 'mce-fullscreen'); - DOM.addClass(editorContainer, 'mce-fullscreen'); - - DOM.bind(window, 'resize', resize); - resize(); - resizeHandler = resize; - } else { - iframeStyle.width = iframeWidth; - iframeStyle.height = iframeHeight; - - if (containerWidth) { - editorContainerStyle.width = containerWidth; - } - - if (containerHeight) { - editorContainerStyle.height = containerHeight; - } - - DOM.removeClass(body, 'mce-fullscreen'); - DOM.removeClass(documentElement, 'mce-fullscreen'); - DOM.removeClass(editorContainer, 'mce-fullscreen'); - DOM.unbind(window, 'resize', resizeHandler); - setScrollPos(scrollPos); + if (fullscreenInfo.containerHeight) { + editorContainerStyle.height = fullscreenInfo.containerHeight; } - editor.fire('FullscreenStateChanged', { state: fullscreenState }); + DOM.removeClass(body, 'mce-fullscreen'); + DOM.removeClass(documentElement, 'mce-fullscreen'); + DOM.removeClass(editorContainer, 'mce-fullscreen'); + setScrollPos(fullscreenInfo.scrollPos); + + DOM.unbind(window, 'resize', fullscreenInfo.resizeHandler); + editor.off('remove', fullscreenInfo.removeHandler); + + Events.fireFullscreenStateChanged(editor, false); + + return null; } + }; - editor.on('init', function () { - editor.addShortcut('Ctrl+Shift+F', '', toggleFullscreen); - }); + return { + toggleFullscreen: toggleFullscreen + }; + } +); - editor.on('remove', function () { - if (resizeHandler) { - DOM.unbind(window, 'resize', resizeHandler); - } +/** + * 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.plugins.fullscreen.api.Commands', + [ + 'tinymce.plugins.fullscreen.core.Actions' + ], + function (Actions) { + var register = function (editor, fullscreenState) { + editor.addCommand('mceFullScreen', function () { + fullscreenState.set(Actions.toggleFullscreen(editor, fullscreenState.get())); }); + }; - editor.addCommand('mceFullScreen', toggleFullscreen); + return { + register: register + }; + } +); +/** + * Buttons.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.fullscreen.ui.Buttons', + [ + ], + function () { + var postRender = function (editor) { + return function (e) { + var ctrl = e.control; + + editor.on('FullscreenStateChanged', function (e) { + ctrl.active(e.state); + }); + }; + }; + + var register = function (editor) { editor.addMenuItem('fullscreen', { text: 'Fullscreen', shortcut: 'Ctrl+Shift+F', selectable: true, - onClick: function () { - toggleFullscreen(); - editor.focus(); - }, - onPostRender: function () { - var self = this; - - editor.on('FullscreenStateChanged', function (e) { - self.active(e.state); - }); - }, + cmd: 'mceFullScreen', + onPostRender: postRender(editor), context: 'view' }); editor.addButton('fullscreen', { tooltip: 'Fullscreen', shortcut: 'Ctrl+Shift+F', - onClick: toggleFullscreen, - onPostRender: function () { - var self = this; - - editor.on('FullscreenStateChanged', function (e) { - self.active(e.state); - }); - } + cmd: 'mceFullScreen', + onPostRender: postRender(editor) }); + }; - return { - isFullscreen: function () { - return fullscreenState; - } - }; + return { + register: register + }; + } +); +/** + * Plugin.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.fullscreen.Plugin', + [ + 'ephox.katamari.api.Cell', + 'tinymce.core.PluginManager', + 'tinymce.plugins.fullscreen.api.Api', + 'tinymce.plugins.fullscreen.api.Commands', + 'tinymce.plugins.fullscreen.ui.Buttons' + ], + function (Cell, PluginManager, Api, Commands, Buttons) { + PluginManager.add('fullscreen', function (editor) { + var fullscreenState = Cell(null); + + Commands.register(editor, fullscreenState); + Buttons.register(editor); + + editor.addShortcut('Ctrl+Shift+F', '', 'mceFullScreen'); + + return Api.get(fullscreenState); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/fullscreen/plugin.min.js b/media/vendor/tinymce/plugins/fullscreen/plugin.min.js index 3db2176572917..2f2aa8bfb6401 100644 --- a/media/vendor/tinymce/plugins/fullscreen/plugin.min.js +++ b/media/vendor/tinymce/plugins/fullscreen/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${name}'); - - var maybeUrlize = function (editor, key) { - return Arr.find(PluginUrls.urls, function (x) { - return x.key === key; - }).fold(function () { - var getMetadata = editor.plugins[key].getMetadata; - return typeof getMetadata === 'function' ? makeLink(getMetadata()) : key; - }, function (x) { - return makeLink({ name: x.name, url: 'https://www.tinymce.com/docs/plugins/' + x.key }); - }); - }; + function (Arr, Fun, Obj, Strings, tinymce, I18n, PluginUrls) { + var makeLink = Fun.curry(Strings.supplant, '${name}'); + + var maybeUrlize = function (editor, key) { + return Arr.find(PluginUrls.urls, function (x) { + return x.key === key; + }).fold(function () { + var getMetadata = editor.plugins[key].getMetadata; + return typeof getMetadata === 'function' ? makeLink(getMetadata()) : key; + }, function (x) { + return makeLink({ name: x.name, url: 'https://www.tinymce.com/docs/plugins/' + x.key }); + }); + }; - var getPluginKeys = function (editor) { - var keys = Obj.keys(editor.plugins); - return editor.settings.forced_plugins === undefined ? - keys : - Arr.filter(keys, Fun.not(Fun.curry(Arr.contains, editor.settings.forced_plugins))); - }; + var getPluginKeys = function (editor) { + var keys = Obj.keys(editor.plugins); + return editor.settings.forced_plugins === undefined ? + keys : + Arr.filter(keys, Fun.not(Fun.curry(Arr.contains, editor.settings.forced_plugins))); + }; - var pluginLister = function (editor) { - var pluginKeys = getPluginKeys(editor); - var pluginLis = Arr.map(pluginKeys, function (key) { - return '
  • ' + maybeUrlize(editor, key) + '
  • '; - }); - var count = pluginLis.length; - var pluginsString = pluginLis.join(''); + var pluginLister = function (editor) { + var pluginKeys = getPluginKeys(editor); + var pluginLis = Arr.map(pluginKeys, function (key) { + return '
  • ' + maybeUrlize(editor, key) + '
  • '; + }); + var count = pluginLis.length; + var pluginsString = pluginLis.join(''); - return '

    ' + I18n.translate(['Plugins installed ({0}):', count ]) + '

    ' + - '
      ' + pluginsString + '
    '; - }; + return '

    ' + I18n.translate(['Plugins installed ({0}):', count ]) + '

    ' + + '
      ' + pluginsString + '
    '; + }; - var installedPlugins = function (editor) { - return { - type: 'container', - html: '
    ' + - pluginLister(editor) + - '
    ', - flex: 1 + var installedPlugins = function (editor) { + return { + type: 'container', + html: '
    ' + + pluginLister(editor) + + '
    ', + flex: 1 + }; }; - }; - var availablePlugins = function () { - return { - type: 'container', - html: '
    ' + - '

    ' + I18n.translate('Premium plugins:') + '

    ' + - '
      ' + - '
    • PowerPaste
    • ' + - '
    • Spell Checker Pro
    • ' + - '
    • Accessibility Checker
    • ' + - '
    • Advanced Code Editor
    • ' + - '
    • Enhanced Media Embed
    • ' + - '
    • Link Checker
    • ' + - '

    ' + - '

    ' + I18n.translate('Learn more...') + '

    ' + - '
    ', - flex: 1 + var availablePlugins = function () { + return { + type: 'container', + html: '
    ' + + '

    ' + I18n.translate('Premium plugins:') + '

    ' + + '
      ' + + '
    • PowerPaste
    • ' + + '
    • Spell Checker Pro
    • ' + + '
    • Accessibility Checker
    • ' + + '
    • Advanced Code Editor
    • ' + + '
    • Enhanced Media Embed
    • ' + + '
    • Link Checker
    • ' + + '

    ' + + '

    ' + I18n.translate('Learn more...') + '

    ' + + '
    ', + flex: 1 + }; + }; + + var makeTab = function (editor) { + return { + title: 'Plugins', + type: 'container', + style: 'overflow-y: auto; overflow-x: hidden;', + layout: 'flex', + padding: 10, + spacing: 10, + items: [ + installedPlugins(editor), + availablePlugins() + ] + }; }; - }; - var makeTab = function (editor) { return { - title: 'Plugins', - type: 'container', - style: 'overflow-y: auto; overflow-x: hidden;', - layout: 'flex', - padding: 10, - spacing: 10, - items: [ - installedPlugins(editor), - availablePlugins() - ] + makeTab: makeTab }; - }; + } +); - return { - makeTab: makeTab - }; -}); +/** + * ButtonsRow.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.help.ui.ButtonsRow', @@ -1292,7 +1353,7 @@ define( var makeRow = function () { var version = getVersion(EditorManager.majorVersion, EditorManager.minorVersion); - var changeLogLink = 'TinyMCE ' + version + ''; + var changeLogLink = 'TinyMCE ' + version + ''; return [ { @@ -1318,6 +1379,16 @@ define( } ); +/** + * Dialog.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.help.ui.Dialog', [ @@ -1327,7 +1398,7 @@ define( 'tinymce.plugins.help.ui.ButtonsRow' ], function (EditorManager, KeyboardShortcutsTab, PluginsTab, ButtonsRow) { - var openDialog = function (editor, url) { + var open = function (editor, pluginUrl) { return function () { editor.windowManager.open({ title: 'Help', @@ -1335,48 +1406,110 @@ define( layout: 'flex', body: [ KeyboardShortcutsTab.makeTab(), - PluginsTab.makeTab(editor, url) + PluginsTab.makeTab(editor, pluginUrl) ], buttons: ButtonsRow.makeRow(), onPostRender: function () { var title = this.getEl('title'); - title.innerHTML = 'TinyMCE Logo'; + title.innerHTML = 'TinyMCE Logo'; } }); }; }; return { - openDialog: openDialog + open: open }; }); +/** + * 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.plugins.help.Plugin', + 'tinymce.plugins.help.api.Commands', + [ + 'tinymce.plugins.help.ui.Dialog' + ], + function (Dialog) { + var register = function (editor, pluginUrl) { + editor.addCommand('mceHelp', Dialog.open(editor, pluginUrl)); + }; + + return { + register: register + }; + } +); + + + +/** + * Buttons.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.help.ui.Buttons', [ - 'tinymce.core.PluginManager', 'tinymce.plugins.help.ui.Dialog' ], - function (PluginManager, Dialog) { - var Plugin = function (editor, url) { + function (Dialog) { + var register = function (editor, pluginUrl) { editor.addButton('help', { icon: 'help', - onclick: Dialog.openDialog(editor, url) + onclick: Dialog.open(editor, pluginUrl) }); editor.addMenuItem('Help', { text: 'Help', icon: 'help', - context: 'view', - onclick: Dialog.openDialog(editor, url) + context: 'help', + onclick: Dialog.open(editor, pluginUrl) }); + }; - editor.addCommand('mceHelp', Dialog.openDialog(editor, url)); - - editor.shortcuts.add('Alt+0', 'Open help dialog', Dialog.openDialog(editor, url)); + return { + register: register }; + } +); + +/** + * PLugin.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 + */ - PluginManager.add('help', Plugin); +define( + 'tinymce.plugins.help.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.help.api.Commands', + 'tinymce.plugins.help.ui.Buttons', + 'tinymce.plugins.help.ui.Dialog' + ], + function (PluginManager, Commands, Buttons, Dialog) { + PluginManager.add('help', function (editor, pluginUrl) { + Buttons.register(editor, pluginUrl); + Commands.register(editor, pluginUrl); + editor.shortcuts.add('Alt+0', 'Open help dialog', 'mceHelp'); + }); return function () {}; } diff --git a/media/vendor/tinymce/plugins/help/plugin.min.js b/media/vendor/tinymce/plugins/help/plugin.min.js index fd421a3207a5a..f2e7e15f2e092 100644 --- a/media/vendor/tinymce/plugins/help/plugin.min.js +++ b/media/vendor/tinymce/plugins/help/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-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e Ctrl + Shift + P",action:"Focus to contextual toolbar"},{shortcut:b+" + K",action:"Insert link (if link plugin activated)"},{shortcut:b+" + S",action:"Save (if save plugin activated)"},{shortcut:b+" + F",action:"Find (if searchreplace plugin activated)"}];return{shortcuts:d}}),g("5",["8","9","a"],function(a,b,c){var d=function(){var d=function(a){return'aria-label="Action: '+a.action+", Shortcut: "+a.shortcut.replace(/Ctrl/g,"Control")+'"'},e=a.map(c.shortcuts,function(a){return'"+b.translate(a.action)+""+a.shortcut+""}).join("");return{title:"Handy Shortcuts",type:"container",style:"overflow-y: auto; overflow-x: hidden; max-height: 250px",items:[{type:"container",html:'
    "+e+"
    '+b.translate("Action")+""+b.translate("Shortcut")+"
    "}]}};return{makeTab:d}}),g("c",["f","k"],function(a,b){var c=function(){var a=b.keys,c=function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b};return void 0===a?c:a}(),d=function(a,b){for(var d=c(a),e=0,f=d.length;e${name}'),i=function(b,c){return a.find(g.urls,function(a){return a.key===c}).fold(function(){var a=b.plugins[c].getMetadata;return"function"==typeof a?h(a()):c},function(a){return h({name:a.name,url:"https://www.tinymce.com/docs/plugins/"+a.key})})},j=function(d){var e=c.keys(d.plugins);return void 0===d.settings.forced_plugins?e:a.filter(e,b.not(b.curry(a.contains,d.settings.forced_plugins)))},k=function(b){var c=j(b),d=a.map(c,function(a){return"
  • "+i(b,a)+"
  • "}),e=d.length,g=d.join("");return"

    "+f.translate(["Plugins installed ({0}):",e])+"

      "+g+"
    "},l=function(a){return{type:"container",html:'
    '+k(a)+"
    ",flex:1}},m=function(){return{type:"container",html:'

    '+f.translate("Premium plugins:")+'

    • PowerPaste
    • Spell Checker Pro
    • Accessibility Checker
    • Advanced Code Editor
    • Enhanced Media Embed
    • Link Checker

    '+f.translate("Learn more...")+"

    ",flex:1}},n=function(a){return{title:"Plugins",type:"container",style:"overflow-y: auto; overflow-x: hidden;",layout:"flex",padding:10,spacing:10,items:[l(a),m()]}};return{makeTab:n}}),g("7",["4","9"],function(a,b){var c=function(a,b){return 0===a.indexOf("@")?"X.X.X":a+"."+b},d=function(){var d=c(a.majorVersion,a.minorVersion),e='TinyMCE '+d+"";return[{type:"label",html:b.translate(["You are using {0}",e])},{type:"spacer",flex:1},{text:"Close",onclick:function(){this.parent().parent().close()}}]};return{makeRow:d}}),g("2",["4","5","6","7"],function(a,b,c,d){var e=function(a,e){return function(){a.windowManager.open({title:"Help",bodyType:"tabpanel",layout:"flex",body:[b.makeTab(),c.makeTab(a,e)],buttons:d.makeRow(),onPostRender:function(){var a=this.getEl("title");a.innerHTML='TinyMCE Logo'}})}};return{openDialog:e}}),g("0",["1","2"],function(a,b){var c=function(a,c){a.addButton("help",{icon:"help",onclick:b.openDialog(a,c)}),a.addMenuItem("Help",{text:"Help",icon:"help",context:"view",onclick:b.openDialog(a,c)}),a.addCommand("mceHelp",b.openDialog(a,c)),a.shortcuts.add("Alt+0","Open help dialog",b.openDialog(a,c))};return a.add("help",c),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-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e Ctrl + Shift + P",action:"Focus to contextual toolbar"},{shortcut:b+" + K",action:"Insert link (if link plugin activated)"},{shortcut:b+" + S",action:"Save (if save plugin activated)"},{shortcut:b+" + F",action:"Find (if searchreplace plugin activated)"}];return{shortcuts:d}}),g("7",["a","b","c"],function(a,b,c){var d=function(){var d=function(a){return'aria-label="Action: '+a.action+", Shortcut: "+a.shortcut.replace(/Ctrl/g,"Control")+'"'},e=a.map(c.shortcuts,function(a){return'"+b.translate(a.action)+""+a.shortcut+""}).join("");return{title:"Handy Shortcuts",type:"container",style:"overflow-y: auto; overflow-x: hidden; max-height: 250px",items:[{type:"container",html:'
    "+e+"
    '+b.translate("Action")+""+b.translate("Shortcut")+"
    "}]}};return{makeTab:d}}),g("e",["h","m"],function(a,b){var c=function(){var a=b.keys,c=function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b};return void 0===a?c:a}(),d=function(a,b){for(var d=c(a),e=0,f=d.length;e${name}'),i=function(b,c){return a.find(g.urls,function(a){return a.key===c}).fold(function(){var a=b.plugins[c].getMetadata;return"function"==typeof a?h(a()):c},function(a){return h({name:a.name,url:"https://www.tinymce.com/docs/plugins/"+a.key})})},j=function(d){var e=c.keys(d.plugins);return void 0===d.settings.forced_plugins?e:a.filter(e,b.not(b.curry(a.contains,d.settings.forced_plugins)))},k=function(b){var c=j(b),d=a.map(c,function(a){return"
  • "+i(b,a)+"
  • "}),e=d.length,g=d.join("");return"

    "+f.translate(["Plugins installed ({0}):",e])+"

      "+g+"
    "},l=function(a){return{type:"container",html:'
    '+k(a)+"
    ",flex:1}},m=function(){return{type:"container",html:'

    '+f.translate("Premium plugins:")+'

    • PowerPaste
    • Spell Checker Pro
    • Accessibility Checker
    • Advanced Code Editor
    • Enhanced Media Embed
    • Link Checker

    '+f.translate("Learn more...")+"

    ",flex:1}},n=function(a){return{title:"Plugins",type:"container",style:"overflow-y: auto; overflow-x: hidden;",layout:"flex",padding:10,spacing:10,items:[l(a),m()]}};return{makeTab:n}}),g("9",["6","b"],function(a,b){var c=function(a,b){return 0===a.indexOf("@")?"X.X.X":a+"."+b},d=function(){var d=c(a.majorVersion,a.minorVersion),e='TinyMCE '+d+"";return[{type:"label",html:b.translate(["You are using {0}",e])},{type:"spacer",flex:1},{text:"Close",onclick:function(){this.parent().parent().close()}}]};return{makeRow:d}}),g("4",["6","7","8","9"],function(a,b,c,d){var e=function(a,e){return function(){a.windowManager.open({title:"Help",bodyType:"tabpanel",layout:"flex",body:[b.makeTab(),c.makeTab(a,e)],buttons:d.makeRow(),onPostRender:function(){var a=this.getEl("title");a.innerHTML='TinyMCE Logo'}})}};return{open:e}}),g("2",["4"],function(a){var b=function(b,c){b.addCommand("mceHelp",a.open(b,c))};return{register:b}}),g("3",["4"],function(a){var b=function(b,c){b.addButton("help",{icon:"help",onclick:a.open(b,c)}),b.addMenuItem("Help",{text:"Help",icon:"help",context:"help",onclick:a.open(b,c)})};return{register:b}}),g("0",["1","2","3","4"],function(a,b,c,d){return a.add("help",function(a,d){c.register(a,d),b.register(a,d),a.shortcuts.add("Alt+0","Open help dialog","mceHelp")}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/hr/plugin.js b/media/vendor/tinymce/plugins/hr/plugin.js index c9862bc8ef45e..d1c343aaa4458 100644 --- a/media/vendor/tinymce/plugins/hr/plugin.js +++ b/media/vendor/tinymce/plugins/hr/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.hr.Plugin","tinymce.core.PluginManager","global!tinymce.util.Tools.resolve"] +["tinymce.plugins.hr.Plugin","tinymce.core.PluginManager","tinymce.plugins.hr.api.Commands","tinymce.plugins.hr.ui.Buttons","global!tinymce.util.Tools.resolve"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -105,7 +105,7 @@ define( ); /** - * Plugin.js + * Commands.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -114,23 +114,38 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class contains all core logic for the hr plugin. - * - * @class tinymce.hr.Plugin - * @private - */ define( - 'tinymce.plugins.hr.Plugin', + 'tinymce.plugins.hr.api.Commands', [ - 'tinymce.core.PluginManager' ], - function (PluginManager) { - PluginManager.add('hr', function (editor) { + function () { + var register = function (editor) { editor.addCommand('InsertHorizontalRule', function () { editor.execCommand('mceInsertContent', false, '
    '); }); + }; + return { + register: register + }; + } +); +/** + * Buttons.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.hr.ui.Buttons', + [ + ], + function () { + var register = function (editor) { editor.addButton('hr', { icon: 'hr', tooltip: 'Horizontal line', @@ -143,6 +158,34 @@ define( cmd: 'InsertHorizontalRule', context: 'insert' }); + }; + + return { + register: register + }; + } +); +/** + * Plugin.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.hr.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.hr.api.Commands', + 'tinymce.plugins.hr.ui.Buttons' + ], + function (PluginManager, Commands, Buttons) { + PluginManager.add('hr', function (editor) { + Commands.register(editor); + Buttons.register(editor); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/hr/plugin.min.js b/media/vendor/tinymce/plugins/hr/plugin.min.js index 6c0dfa44780b2..c1dfdcade4a85 100644 --- a/media/vendor/tinymce/plugins/hr/plugin.min.js +++ b/media/vendor/tinymce/plugins/hr/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")}),a.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),a.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})}),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")})};return{register:a}}),g("3",[],function(){var a=function(a){a.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),a.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})};return{register:a}}),g("0",["1","2","3"],function(a,b,c){return a.add("hr",function(a){b.register(a),c.register(a)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/image/plugin.js b/media/vendor/tinymce/plugins/image/plugin.js index 3395ec69b7050..37a3611fd6ccb 100644 --- a/media/vendor/tinymce/plugins/image/plugin.js +++ b/media/vendor/tinymce/plugins/image/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.image.Plugin","tinymce.core.PluginManager","tinymce.core.util.Tools","tinymce.plugins.image.ui.Dialog","global!tinymce.util.Tools.resolve","global!document","global!Math","global!RegExp","tinymce.core.Env","tinymce.core.ui.Factory","tinymce.core.util.JSON","tinymce.core.util.XHR","tinymce.plugins.image.core.Uploader","tinymce.plugins.image.core.Utils","tinymce.core.util.Promise"] +["tinymce.plugins.image.Plugin","tinymce.core.PluginManager","tinymce.plugins.image.api.Commands","tinymce.plugins.image.core.FilterContent","tinymce.plugins.image.ui.Buttons","global!tinymce.util.Tools.resolve","tinymce.plugins.image.ui.Dialog","tinymce.core.util.Tools","ephox.sand.api.URL","global!document","global!Math","global!RegExp","tinymce.core.Env","tinymce.core.ui.Factory","tinymce.core.util.JSON","tinymce.core.util.XHR","tinymce.plugins.image.api.Settings","tinymce.plugins.image.core.Uploader","tinymce.plugins.image.core.Utils","ephox.sand.util.Global","ephox.sand.api.XMLHttpRequest","global!window","tinymce.core.util.Promise","ephox.katamari.api.Resolve","ephox.katamari.api.Global"] jsc*/ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); /** @@ -104,6 +104,137 @@ define( } ); +define( + 'ephox.katamari.api.Global', + + [ + ], + + function () { + // Use window object as the global if it's available since CSP will block script evals + if (typeof window !== 'undefined') { + return window; + } else { + return Function('return this;')(); + } + } +); + + +define( + 'ephox.katamari.api.Resolve', + + [ + 'ephox.katamari.api.Global' + ], + + function (Global) { + /** path :: ([String], JsObj?) -> JsObj */ + var path = function (parts, scope) { + var o = scope !== undefined ? scope : Global; + for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) + o = o[parts[i]]; + return o; + }; + + /** resolve :: (String, JsObj?) -> JsObj */ + var resolve = function (p, scope) { + var parts = p.split('.'); + return path(parts, scope); + }; + + /** step :: (JsObj, String) -> JsObj */ + var step = function (o, part) { + if (o[part] === undefined || o[part] === null) + o[part] = {}; + return o[part]; + }; + + /** forge :: ([String], JsObj?) -> JsObj */ + var forge = function (parts, target) { + var o = target !== undefined ? target : Global; + for (var i = 0; i < parts.length; ++i) + o = step(o, parts[i]); + return o; + }; + + /** namespace :: (String, JsObj?) -> JsObj */ + var namespace = function (name, target) { + var parts = name.split('.'); + return forge(parts, target); + }; + + return { + path: path, + resolve: resolve, + forge: forge, + namespace: namespace + }; + } +); + + +define( + 'ephox.sand.util.Global', + + [ + 'ephox.katamari.api.Resolve' + ], + + function (Resolve) { + var unsafe = function (name, scope) { + return Resolve.resolve(name, scope); + }; + + var getOrDie = function (name, scope) { + var actual = unsafe(name, scope); + + if (actual === undefined) throw name + ' not available on this browser'; + return actual; + }; + + return { + getOrDie: getOrDie + }; + } +); +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 + }; + } +); +defineGlobal("global!document", document); +defineGlobal("global!Math", Math); +defineGlobal("global!RegExp", RegExp); /** * ResolveGlobal.js * @@ -115,18 +246,15 @@ define( */ define( - 'tinymce.core.util.Tools', + 'tinymce.core.Env', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.util.Tools'); + return resolve('tinymce.Env'); } ); -defineGlobal("global!document", document); -defineGlobal("global!Math", Math); -defineGlobal("global!RegExp", RegExp); /** * ResolveGlobal.js * @@ -138,12 +266,12 @@ defineGlobal("global!RegExp", RegExp); */ define( - 'tinymce.core.Env', + 'tinymce.core.ui.Factory', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.Env'); + return resolve('tinymce.ui.Factory'); } ); @@ -158,12 +286,12 @@ define( */ define( - 'tinymce.core.ui.Factory', + 'tinymce.core.util.JSON', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.ui.Factory'); + return resolve('tinymce.util.JSON'); } ); @@ -178,12 +306,12 @@ define( */ define( - 'tinymce.core.util.JSON', + 'tinymce.core.util.Tools', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.util.JSON'); + return resolve('tinymce.util.Tools'); } ); @@ -207,6 +335,84 @@ define( } ); +/** + * Settings.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.image.api.Settings', + [ + ], + function () { + var hasDimensions = function (editor) { + return editor.getParam('image_dimensions', true); + }; + + var hasAdvTab = function (editor) { + return editor.getParam('image_advtab', false); + }; + + var getPrependUrl = function (editor) { + return editor.getParam('image_prepend_url', ''); + }; + + var getClassList = function (editor) { + return editor.getParam('image_class_list'); + }; + + var hasDescription = function (editor) { + return editor.getParam('image_description', true); + }; + + var hasImageTitle = function (editor) { + return editor.getParam('image_title', false); + }; + + var hasImageCaption = function (editor) { + return editor.getParam('image_caption', false); + }; + + var getImageList = function (editor) { + return editor.getParam('image_list', false); + }; + + return { + hasDimensions: hasDimensions, + hasAdvTab: hasAdvTab, + getPrependUrl: getPrependUrl, + getClassList: getClassList, + hasDescription: hasDescription, + hasImageTitle: hasImageTitle, + hasImageCaption: hasImageCaption, + getImageList: getImageList + }; + } +); +define( + 'ephox.sand.api.XMLHttpRequest', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * IE8 and above per + * https://developer.mozilla.org/en/docs/XMLHttpRequest + */ + return function () { + var f = Global.getOrDie('XMLHttpRequest'); + return new f(); + }; + } +); +defineGlobal("global!window", window); /** * ResolveGlobal.js * @@ -240,30 +446,29 @@ define( /** * This is basically cut down version of tinymce.core.file.Uploader, which we could use directly * if it wasn't marked as private. - * - * @class tinymce.image.core.Uploader - * @private */ define( 'tinymce.plugins.image.core.Uploader', [ + 'ephox.sand.api.XMLHttpRequest', + 'global!document', + 'global!window', 'tinymce.core.util.Promise', - 'tinymce.core.util.Tools', - 'global!document' + 'tinymce.core.util.Tools' ], - function (Promise, Tools, document) { - return function (settings) { - var noop = function () {}; + function (XMLHttpRequest, document, window, Promise, Tools) { + var noop = function () {}; - function pathJoin(path1, path2) { - if (path1) { - return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); - } - - return path2; + var pathJoin = function (path1, path2) { + if (path1) { + return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); } - function defaultHandler(blobInfo, success, failure, progress) { + return path2; + }; + + return function (settings) { + var defaultHandler = function (blobInfo, success, failure, progress) { var xhr, formData; xhr = new XMLHttpRequest(); @@ -275,34 +480,34 @@ define( }; xhr.onerror = function () { - failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status); + failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); }; xhr.onload = function () { var json; if (xhr.status < 200 || xhr.status >= 300) { - failure("HTTP Error: " + xhr.status); + failure('HTTP Error: ' + xhr.status); return; } json = JSON.parse(xhr.responseText); - if (!json || typeof json.location != "string") { - failure("Invalid JSON: " + xhr.responseText); + if (!json || typeof json.location !== 'string') { + failure('Invalid JSON: ' + xhr.responseText); return; } success(pathJoin(settings.basePath, json.location)); }; - formData = new FormData(); + formData = new window.FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); xhr.send(formData); - } + }; - function uploadBlob(blobInfo, handler) { + var uploadBlob = function (blobInfo, handler) { return new Promise(function (resolve, reject) { try { handler(blobInfo, resolve, reject, noop); @@ -310,15 +515,15 @@ define( reject(ex.message); } }); - } + }; - function isDefaultHandler(handler) { + var isDefaultHandler = function (handler) { return handler === defaultHandler; - } + }; - function upload(blobInfo) { - return (!settings.url && isDefaultHandler(settings.handler)) ? Promise.reject("Upload url missng from the settings.") : uploadBlob(blobInfo, settings.handler); - } + var upload = function (blobInfo) { + return (!settings.url && isDefaultHandler(settings.handler)) ? Promise.reject('Upload url missng from the settings.') : uploadBlob(blobInfo, settings.handler); + }; settings = Tools.extend({ credentials: false, @@ -483,6 +688,7 @@ define( define( 'tinymce.plugins.image.ui.Dialog', [ + 'ephox.sand.api.URL', 'global!document', 'global!Math', 'global!RegExp', @@ -491,23 +697,23 @@ define( 'tinymce.core.util.JSON', 'tinymce.core.util.Tools', 'tinymce.core.util.XHR', + 'tinymce.plugins.image.api.Settings', 'tinymce.plugins.image.core.Uploader', 'tinymce.plugins.image.core.Utils' ], - function (document, Math, RegExp, Env, Factory, JSON, Tools, XHR, Uploader, Utils) { - + function (URL, document, Math, RegExp, Env, Factory, JSON, Tools, XHR, Settings, Uploader, Utils) { return function (editor) { function createImageList(callback) { - var imageList = editor.settings.image_list; + var imageList = Settings.getImageList(editor); - if (typeof imageList == "string") { + if (typeof imageList === "string") { XHR.send({ url: imageList, success: function (text) { callback(JSON.parse(text)); } }); - } else if (typeof imageList == "function") { + } else if (typeof imageList === "function") { imageList(callback); } else { callback(imageList); @@ -516,8 +722,7 @@ define( function showDialog(imageList) { var win, data = {}, imgElm, figureElm, dom = editor.dom, settings = editor.settings; - var width, height, imageListCtrl, classListCtrl, imageDimensions = settings.image_dimensions !== false; - + var width, height, imageListCtrl, classListCtrl, imageDimensions = Settings.hasDimensions(editor); function onFileInput() { var Throbber = Factory.get('Throbber'); @@ -576,7 +781,7 @@ define( newHeight = heightCtrl.value(); if (win.find('#constrain')[0].checked() && width && height && newWidth && newHeight) { - if (width != newWidth) { + if (width !== newWidth) { newHeight = Math.round((newWidth / width) * newHeight); if (!isNaN(newHeight)) { @@ -596,7 +801,7 @@ define( } function updateStyle() { - if (!editor.settings.image_advtab) { + if (!Settings.hasAdvTab(editor)) { return; } @@ -619,7 +824,7 @@ define( } function updateVSpaceHSpaceBorder() { - if (!editor.settings.image_advtab) { + if (!Settings.hasAdvTab(editor)) { return; } @@ -804,7 +1009,7 @@ define( srcURL = editor.convertURL(this.value(), 'src'); // Pattern test the src url and make sure we haven't already prepended the url - prependURL = editor.settings.image_prepend_url; + prependURL = Settings.getPrependUrl(editor); absoluteURLPattern = new RegExp('^(?:[a-z]+:)?//', 'i'); if (prependURL && !absoluteURLPattern.test(srcURL) && srcURL.substring(0, prependURL.length) !== prependURL) { srcURL = prependURL + srcURL; @@ -835,7 +1040,7 @@ define( } if (imgElm && - (imgElm.nodeName != 'IMG' || + (imgElm.nodeName !== 'IMG' || imgElm.getAttribute('data-mce-object') || imgElm.getAttribute('data-mce-placeholder'))) { imgElm = null; @@ -871,7 +1076,7 @@ define( onselect: function (e) { var altCtrl = win.find('#alt'); - if (!altCtrl.value() || (e.lastControl && altCtrl.value() == e.lastControl.text())) { + if (!altCtrl.value() || (e.lastControl && altCtrl.value() === e.lastControl.text())) { altCtrl.value(e.control.text()); } @@ -884,13 +1089,13 @@ define( }; } - if (editor.settings.image_class_list) { + if (Settings.getClassList(editor)) { classListCtrl = { name: 'class', type: 'listbox', label: 'Class', values: Utils.buildListItems( - editor.settings.image_class_list, + Settings.getClassList(editor), function (item) { if (item.value) { item.textStyle = function () { @@ -916,11 +1121,11 @@ define( imageListCtrl ]; - if (editor.settings.image_description !== false) { + if (Settings.hasDescription(editor)) { generalFormItems.push({ name: 'alt', type: 'textbox', label: 'Image description' }); } - if (editor.settings.image_title) { + if (Settings.hasImageTitle(editor)) { generalFormItems.push({ name: 'title', type: 'textbox', label: 'Image Title' }); } @@ -943,11 +1148,11 @@ define( generalFormItems.push(classListCtrl); - if (editor.settings.image_caption && Env.ceFalse) { + if (Settings.hasImageCaption(editor)) { generalFormItems.push({ name: 'caption', type: 'checkbox', label: 'Caption' }); } - if (editor.settings.image_advtab || editor.settings.images_upload_url) { + if (Settings.hasAdvTab(editor) || editor.settings.images_upload_url) { var body = [ { title: 'General', @@ -956,7 +1161,7 @@ define( } ]; - if (editor.settings.image_advtab) { + if (Settings.hasAdvTab(editor)) { // Parse styles from img if (imgElm) { if (imgElm.style.marginLeft && imgElm.style.marginRight && imgElm.style.marginLeft === imgElm.style.marginRight) { @@ -1079,7 +1284,7 @@ define( ); /** - * Plugin.js + * Commands.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -1088,52 +1293,91 @@ define( * Contributing: http://www.tinymce.com/contributing */ +define( + 'tinymce.plugins.image.api.Commands', + [ + 'tinymce.plugins.image.ui.Dialog' + ], + function (Dialog) { + var register = function (editor) { + editor.addCommand('mceImage', Dialog(editor).open); + }; + + return { + register: register + }; + } +); /** - * This class contains all core logic for the image plugin. + * FilterContent.js * - * @class tinymce.image.Plugin - * @private + * 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.image.Plugin', + 'tinymce.plugins.image.core.FilterContent', [ - 'tinymce.core.PluginManager', - 'tinymce.core.util.Tools', - 'tinymce.plugins.image.ui.Dialog' + 'tinymce.core.util.Tools' ], - function (PluginManager, Tools, Dialog) { - PluginManager.add('image', function (editor) { - - editor.on('preInit', function () { - function hasImageClass(node) { - var className = node.attr('class'); - return className && /\bimage\b/.test(className); - } + function (Tools) { + var hasImageClass = function (node) { + var className = node.attr('class'); + return className && /\bimage\b/.test(className); + }; - function toggleContentEditableState(state) { - return function (nodes) { - var i = nodes.length, node; + var toggleContentEditableState = function (state) { + return function (nodes) { + var i = nodes.length, node; - function toggleContentEditable(node) { - node.attr('contenteditable', state ? 'true' : null); - } + var toggleContentEditable = function (node) { + node.attr('contenteditable', state ? 'true' : null); + }; - while (i--) { - node = nodes[i]; + while (i--) { + node = nodes[i]; - if (hasImageClass(node)) { - node.attr('contenteditable', state ? 'false' : null); - Tools.each(node.getAll('figcaption'), toggleContentEditable); - Tools.each(node.getAll('img'), toggleContentEditable); - } - } - }; + if (hasImageClass(node)) { + node.attr('contenteditable', state ? 'false' : null); + Tools.each(node.getAll('figcaption'), toggleContentEditable); + Tools.each(node.getAll('img'), toggleContentEditable); + } } + }; + }; + var setup = function (editor) { + editor.on('preInit', function () { editor.parser.addNodeFilter('figure', toggleContentEditableState(true)); editor.serializer.addNodeFilter('figure', toggleContentEditableState(false)); }); + }; + return { + setup: setup + }; + } +); +/** + * Buttons.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.image.ui.Buttons', + [ + 'tinymce.plugins.image.ui.Dialog' + ], + function (Dialog) { + var register = function (editor) { editor.addButton('image', { icon: 'image', tooltip: 'Insert/edit image', @@ -1148,8 +1392,36 @@ define( context: 'insert', prependToContext: true }); + }; - editor.addCommand('mceImage', Dialog(editor).open); + return { + register: register + }; + } +); +/** + * Plugin.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.image.Plugin', + [ + 'tinymce.core.PluginManager', + 'tinymce.plugins.image.api.Commands', + 'tinymce.plugins.image.core.FilterContent', + 'tinymce.plugins.image.ui.Buttons' + ], + function (PluginManager, Commands, FilterContent, Buttons) { + PluginManager.add('image', function (editor) { + FilterContent.setup(editor); + Buttons.register(editor); + Commands.register(editor); }); return function () { }; diff --git a/media/vendor/tinymce/plugins/image/plugin.min.js b/media/vendor/tinymce/plugins/image/plugin.min.js index dc5369e5e4bca..bd96a7cacc060 100644 --- a/media/vendor/tinymce/plugins/image/plugin.min.js +++ b/media/vendor/tinymce/plugins/image/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=300?void e("HTTP Error: "+g.status):(a=JSON.parse(g.responseText),a&&"string"==typeof a.location?void b(d(c.basePath,a.location)):void e("Invalid JSON: "+g.responseText))},h=new FormData,h.append("file",a.blob(),a.filename()),g.send(h)}function f(b,c){return new a(function(a,d){try{c(b,a,d,i)}catch(a){d(a.message)}})}function g(a){return a===e}function h(b){return!c.url&&g(c.handler)?a.reject("Upload url missng from the settings."):f(b,c.handler)}var i=function(){};return c=b.extend({credentials:!1,handler:e},c),{upload:h}}}),g("d",["2","6","5"],function(a,b,c){var d=function(a,d){function e(a,b){f.parentNode&&f.parentNode.removeChild(f),d({width:a,height:b})}var f=c.createElement("img");f.onload=function(){e(b.max(f.width,f.clientWidth),b.max(f.height,f.clientHeight))},f.onerror=function(){e()};var g=f.style;g.visibility="hidden",g.position="fixed",g.bottom=g.left=0,g.width=g.height="auto",c.body.appendChild(f),f.src=a},e=function(b,c,d){function e(b,d){return d=d||[],a.each(b,function(a){var b={text:a.text||a.title};a.menu?b.menu=e(a.menu):(b.value=a.value,c(b)),d.push(b)}),d}return e(b,d||[])},f=function(a){return a&&(a=a.replace(/px$/,"")),a},g=function(a){return a.length>0&&/^[0-9]+$/.test(a)&&(a+="px"),a},h=function(a){if(a.margin){var b=a.margin.split(" ");switch(b.length){case 1:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[0],a["margin-bottom"]=a["margin-bottom"]||b[0],a["margin-left"]=a["margin-left"]||b[0];break;case 2:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[1],a["margin-bottom"]=a["margin-bottom"]||b[0],a["margin-left"]=a["margin-left"]||b[1];break;case 3:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[1],a["margin-bottom"]=a["margin-bottom"]||b[2],a["margin-left"]=a["margin-left"]||b[1];break;case 4:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[1],a["margin-bottom"]=a["margin-bottom"]||b[2],a["margin-left"]=a["margin-left"]||b[3]}delete a.margin}return a};return{getImageSize:d,buildListItems:e,removePixelSuffix:f,addPixelSuffix:g,mergeMargins:h}}),g("3",["5","6","7","8","9","a","2","b","c","d"],function(a,b,c,d,e,f,g,h,i,j){return function(a){function k(b){var c=a.settings.image_list;"string"==typeof c?h.send({url:c,success:function(a){b(f.parse(a))}}):"function"==typeof c?c(b):b(c)}function l(f){function h(){var b=e.get("Throbber"),c=new b(s.getEl()),d=this.value(),f=new i({url:B.images_upload_url,basePath:B.images_upload_base_path,credentials:B.images_upload_credentials,handler:B.images_upload_handler}),g=a.editorUpload.blobCache.create({blob:d,name:d.name?d.name.replace(/\.[^\.]+$/,""):null,base64:"data:image/fake;base64,="}),h=function(){c.hide(),URL.revokeObjectURL(g.blobUri())};return c.show(),f.upload(g).then(function(a){var b=s.find("#src");return b.value(a),s.find("tabpanel")[0].activateTab(0),b.fire("change"),h(),a},function(b){a.windowManager.alert(b),h()})}function k(b){return a.schema.getTextBlockElements()[b.nodeName]}function l(){var a,c,d,e;a=s.find("#width")[0],c=s.find("#height")[0],a&&c&&(d=a.value(),e=c.value(),s.find("#constrain")[0].checked()&&v&&w&&d&&e&&(v!=d?(e=b.round(d/v*e),isNaN(e)||c.value(e)):(d=b.round(e/w*d),isNaN(d)||a.value(d))),v=d,w=e)}function m(){if(a.settings.image_advtab){var b=s.toJSON(),c=A.parseStyle(b.style);c=j.mergeMargins(c),b.vspace&&(c["margin-top"]=c["margin-bottom"]=j.addPixelSuffix(b.vspace)),b.hspace&&(c["margin-left"]=c["margin-right"]=j.addPixelSuffix(b.hspace)),b.border&&(c["border-width"]=j.addPixelSuffix(b.border)),s.find("#style").value(A.serializeStyle(A.parseStyle(A.serializeStyle(c))))}}function n(){if(a.settings.image_advtab){var b=s.toJSON(),c=A.parseStyle(b.style);s.find("#vspace").value(""),s.find("#hspace").value(""),c=j.mergeMargins(c),(c["margin-top"]&&c["margin-bottom"]||c["margin-right"]&&c["margin-left"])&&(c["margin-top"]===c["margin-bottom"]?s.find("#vspace").value(j.removePixelSuffix(c["margin-top"])):s.find("#vspace").value(""),c["margin-right"]===c["margin-left"]?s.find("#hspace").value(j.removePixelSuffix(c["margin-right"])):s.find("#hspace").value("")),c["border-width"]&&s.find("#border").value(j.removePixelSuffix(c["border-width"])),s.find("#style").value(A.serializeStyle(A.parseStyle(A.serializeStyle(c))))}}function o(b){function c(){b.onload=b.onerror=null,a.selection&&(a.selection.select(b),a.nodeChanged())}b.onload=function(){z.width||z.height||!C||A.setAttribs(b,{width:b.clientWidth,height:b.clientHeight}),c()},b.onerror=c}function p(){var b,c;m(),l(),z=g.extend(z,s.toJSON()),z.alt||(z.alt=""),z.title||(z.title=""),""===z.width&&(z.width=null),""===z.height&&(z.height=null),z.style||(z.style=null),z={src:z.src,alt:z.alt,title:z.title,width:z.width,height:z.height,style:z.style,caption:z.caption,"class":z["class"]},a.undoManager.transact(function(){if(z.src){if(""===z.title&&(z.title=null),t?A.setAttribs(t,z):(z.id="__mcenew",a.focus(),a.selection.setContent(A.createHTML("img",z)),t=A.get("__mcenew"),A.setAttrib(t,"id",null)),a.editorUpload.uploadImagesAuto(),z.caption===!1&&A.is(t.parentNode,"figure.image")&&(b=t.parentNode,A.setAttrib(t,"contenteditable",null),A.insertAfter(t,b),A.remove(b),a.selection.select(t),a.nodeChanged()),z.caption!==!0)o(t);else if(!A.is(t.parentNode,"figure.image")){c=t,t=t.cloneNode(!0),t.contentEditable=!0,b=A.create("figure",{"class":"image"}),b.appendChild(t),b.appendChild(A.create("figcaption",{contentEditable:!0},"Caption")),b.contentEditable=!1;var d=A.getParent(c,k);d?A.split(d,c,b):A.replace(b,c),a.selection.select(b)}}else if(t){var e=A.is(t.parentNode,"figure.image")?t.parentNode:t;A.remove(e),a.focus(),a.nodeChanged(),A.isEmpty(a.getBody())&&(a.setContent(""),a.selection.setCursorLocation())}})}function q(b){var d,e,f,h=b.meta||{};x&&x.value(a.convertURL(this.value(),"src")),g.each(h,function(a,b){s.find("#"+b).value(a)}),h.width||h.height||(d=a.convertURL(this.value(),"src"),e=a.settings.image_prepend_url,f=new c("^(?:[a-z]+:)?//","i"),e&&!f.test(d)&&d.substring(0,e.length)!==e&&(d=e+d),this.value(d),j.getImageSize(a.documentBaseURI.toAbsolute(this.value()),function(a){a.width&&a.height&&C&&(v=a.width,w=a.height,s.find("#width").value(v),s.find("#height").value(w))}))}function r(a){a.meta=s.toJSON()}var s,t,u,v,w,x,y,z={},A=a.dom,B=a.settings,C=B.image_dimensions!==!1;t=a.selection.getNode(),u=A.getParent(t,"figure.image"),u&&(t=A.select("img",u)[0]),t&&("IMG"!=t.nodeName||t.getAttribute("data-mce-object")||t.getAttribute("data-mce-placeholder"))&&(t=null),t&&(v=A.getAttrib(t,"width"),w=A.getAttrib(t,"height"),z={src:A.getAttrib(t,"src"),alt:A.getAttrib(t,"alt"),title:A.getAttrib(t,"title"),"class":A.getAttrib(t,"class"),width:v,height:w,caption:!!u}),f&&(x={type:"listbox",label:"Image list",values:j.buildListItems(f,function(b){b.value=a.convertURL(b.value||b.url,"src")},[{text:"None",value:""}]),value:z.src&&a.convertURL(z.src,"src"),onselect:function(a){var b=s.find("#alt");(!b.value()||a.lastControl&&b.value()==a.lastControl.text())&&b.value(a.control.text()),s.find("#src").value(a.control.value()).fire("change")},onPostRender:function(){x=this}}),a.settings.image_class_list&&(y={name:"class",type:"listbox",label:"Class",values:j.buildListItems(a.settings.image_class_list,function(b){b.value&&(b.textStyle=function(){return a.formatter.getCssText({inline:"img",classes:[b.value]})})})});var D=[{name:"src",type:"filepicker",filetype:"image",label:"Source",autofocus:!0,onchange:q,onbeforecall:r},x];if(a.settings.image_description!==!1&&D.push({name:"alt",type:"textbox",label:"Image description"}),a.settings.image_title&&D.push({name:"title",type:"textbox",label:"Image Title"}),C&&D.push({type:"container",label:"Dimensions",layout:"flex",direction:"row",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:3,onchange:l,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:3,onchange:l,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}),D.push(y),a.settings.image_caption&&d.ceFalse&&D.push({name:"caption",type:"checkbox",label:"Caption"}),a.settings.image_advtab||a.settings.images_upload_url){var E=[{title:"General",type:"form",items:D}];if(a.settings.image_advtab&&(t&&(t.style.marginLeft&&t.style.marginRight&&t.style.marginLeft===t.style.marginRight&&(z.hspace=j.removePixelSuffix(t.style.marginLeft)),t.style.marginTop&&t.style.marginBottom&&t.style.marginTop===t.style.marginBottom&&(z.vspace=j.removePixelSuffix(t.style.marginTop)),t.style.borderWidth&&(z.border=j.removePixelSuffix(t.style.borderWidth)),z.style=a.dom.serializeStyle(a.dom.parseStyle(a.dom.getAttrib(t,"style")))),E.push({title:"Advanced",type:"form",pack:"start",items:[{label:"Style",name:"style",type:"textbox",onchange:n},{type:"form",layout:"grid",packV:"start",columns:2,padding:0,alignH:["left","right"],defaults:{type:"textbox",maxWidth:50,onchange:m},items:[{label:"Vertical space",name:"vspace"},{label:"Horizontal space",name:"hspace"},{label:"Border",name:"border"}]}]})),a.settings.images_upload_url){var F=".jpg,.jpeg,.png,.gif",G={title:"Upload",type:"form",layout:"flex",direction:"column",align:"stretch",padding:"20 20 20 20",items:[{type:"container",layout:"flex",direction:"column",align:"center",spacing:10,items:[{text:"Browse for an image",type:"browsebutton",accept:F,onchange:h},{text:"OR",type:"label"}]},{text:"Drop an image here",type:"dropzone",accept:F,height:100,onchange:h}]};E.push(G)}s=a.windowManager.open({title:"Insert/edit image",data:z,bodyType:"tabpanel",body:E,onSubmit:p})}else s=a.windowManager.open({title:"Insert/edit image",data:z,body:D,onSubmit:p})}function m(){k(l)}return{open:m}}}),g("0",["1","2","3"],function(a,b,c){return a.add("image",function(a){a.on("preInit",function(){function c(a){var b=a.attr("class");return b&&/\bimage\b/.test(b)}function d(a){return function(d){function e(b){b.attr("contenteditable",a?"true":null)}for(var f,g=d.length;g--;)f=d[g],c(f)&&(f.attr("contenteditable",a?"false":null),b.each(f.getAll("figcaption"),e),b.each(f.getAll("img"),e))}}a.parser.addNodeFilter("figure",d(!0)),a.serializer.addNodeFilter("figure",d(!1))}),a.addButton("image",{icon:"image",tooltip:"Insert/edit image",onclick:c(a).open,stateSelector:"img:not([data-mce-object],[data-mce-placeholder]),figure.image"}),a.addMenuItem("image",{icon:"image",text:"Image",onclick:c(a).open,context:"insert",prependToContext:!0}),a.addCommand("mceImage",c(a).open)}),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=300?void f("HTTP Error: "+i.status):(a=JSON.parse(i.responseText),a&&"string"==typeof a.location?void e(g(b.basePath,a.location)):void f("Invalid JSON: "+i.responseText))},j=new c.FormData,j.append("file",d.blob(),d.filename()),i.send(j)},i=function(a,b){return new d(function(c,d){try{b(a,c,d,f)}catch(a){d(a.message)}})},j=function(a){return a===h},k=function(a){return!b.url&&j(b.handler)?d.reject("Upload url missng from the settings."):i(a,b.handler)};return b=e.extend({credentials:!1,handler:h},b),{upload:k}}}),g("i",["7","a","9"],function(a,b,c){var d=function(a,d){function e(a,b){f.parentNode&&f.parentNode.removeChild(f),d({width:a,height:b})}var f=c.createElement("img");f.onload=function(){e(b.max(f.width,f.clientWidth),b.max(f.height,f.clientHeight))},f.onerror=function(){e()};var g=f.style;g.visibility="hidden",g.position="fixed",g.bottom=g.left=0,g.width=g.height="auto",c.body.appendChild(f),f.src=a},e=function(b,c,d){function e(b,d){return d=d||[],a.each(b,function(a){var b={text:a.text||a.title};a.menu?b.menu=e(a.menu):(b.value=a.value,c(b)),d.push(b)}),d}return e(b,d||[])},f=function(a){return a&&(a=a.replace(/px$/,"")),a},g=function(a){return a.length>0&&/^[0-9]+$/.test(a)&&(a+="px"),a},h=function(a){if(a.margin){var b=a.margin.split(" ");switch(b.length){case 1:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[0],a["margin-bottom"]=a["margin-bottom"]||b[0],a["margin-left"]=a["margin-left"]||b[0];break;case 2:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[1],a["margin-bottom"]=a["margin-bottom"]||b[0],a["margin-left"]=a["margin-left"]||b[1];break;case 3:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[1],a["margin-bottom"]=a["margin-bottom"]||b[2],a["margin-left"]=a["margin-left"]||b[1];break;case 4:a["margin-top"]=a["margin-top"]||b[0],a["margin-right"]=a["margin-right"]||b[1],a["margin-bottom"]=a["margin-bottom"]||b[2],a["margin-left"]=a["margin-left"]||b[3]}delete a.margin}return a};return{getImageSize:d,buildListItems:e,removePixelSuffix:f,addPixelSuffix:g,mergeMargins:h}}),g("6",["8","9","a","b","c","d","e","7","f","g","h","i"],function(a,b,c,d,e,f,g,h,i,j,k,l){return function(b){function e(a){var c=j.getImageList(b);"string"==typeof c?i.send({url:c,success:function(b){a(g.parse(b))}}):"function"==typeof c?c(a):a(c)}function m(e){function g(){var c=f.get("Throbber"),d=new c(t.getEl()),e=this.value(),g=new k({url:C.images_upload_url,basePath:C.images_upload_base_path,credentials:C.images_upload_credentials,handler:C.images_upload_handler}),h=b.editorUpload.blobCache.create({blob:e,name:e.name?e.name.replace(/\.[^\.]+$/,""):null,base64:"data:image/fake;base64,="}),i=function(){d.hide(),a.revokeObjectURL(h.blobUri())};return d.show(),g.upload(h).then(function(a){var b=t.find("#src");return b.value(a),t.find("tabpanel")[0].activateTab(0),b.fire("change"),i(),a},function(a){b.windowManager.alert(a),i()})}function i(a){return b.schema.getTextBlockElements()[a.nodeName]}function m(){var a,b,d,e;a=t.find("#width")[0],b=t.find("#height")[0],a&&b&&(d=a.value(),e=b.value(),t.find("#constrain")[0].checked()&&w&&x&&d&&e&&(w!==d?(e=c.round(d/w*e),isNaN(e)||b.value(e)):(d=c.round(e/x*d),isNaN(d)||a.value(d))),w=d,x=e)}function n(){if(j.hasAdvTab(b)){var a=t.toJSON(),c=B.parseStyle(a.style);c=l.mergeMargins(c),a.vspace&&(c["margin-top"]=c["margin-bottom"]=l.addPixelSuffix(a.vspace)),a.hspace&&(c["margin-left"]=c["margin-right"]=l.addPixelSuffix(a.hspace)),a.border&&(c["border-width"]=l.addPixelSuffix(a.border)),t.find("#style").value(B.serializeStyle(B.parseStyle(B.serializeStyle(c))))}}function o(){if(j.hasAdvTab(b)){var a=t.toJSON(),c=B.parseStyle(a.style);t.find("#vspace").value(""),t.find("#hspace").value(""),c=l.mergeMargins(c),(c["margin-top"]&&c["margin-bottom"]||c["margin-right"]&&c["margin-left"])&&(c["margin-top"]===c["margin-bottom"]?t.find("#vspace").value(l.removePixelSuffix(c["margin-top"])):t.find("#vspace").value(""),c["margin-right"]===c["margin-left"]?t.find("#hspace").value(l.removePixelSuffix(c["margin-right"])):t.find("#hspace").value("")),c["border-width"]&&t.find("#border").value(l.removePixelSuffix(c["border-width"])),t.find("#style").value(B.serializeStyle(B.parseStyle(B.serializeStyle(c))))}}function p(a){function c(){a.onload=a.onerror=null,b.selection&&(b.selection.select(a),b.nodeChanged())}a.onload=function(){A.width||A.height||!D||B.setAttribs(a,{width:a.clientWidth,height:a.clientHeight}),c()},a.onerror=c}function q(){var a,c;n(),m(),A=h.extend(A,t.toJSON()),A.alt||(A.alt=""),A.title||(A.title=""),""===A.width&&(A.width=null),""===A.height&&(A.height=null),A.style||(A.style=null),A={src:A.src,alt:A.alt,title:A.title,width:A.width,height:A.height,style:A.style,caption:A.caption,"class":A["class"]},b.undoManager.transact(function(){if(A.src){if(""===A.title&&(A.title=null),u?B.setAttribs(u,A):(A.id="__mcenew",b.focus(),b.selection.setContent(B.createHTML("img",A)),u=B.get("__mcenew"),B.setAttrib(u,"id",null)),b.editorUpload.uploadImagesAuto(),A.caption===!1&&B.is(u.parentNode,"figure.image")&&(a=u.parentNode,B.setAttrib(u,"contenteditable",null),B.insertAfter(u,a),B.remove(a),b.selection.select(u),b.nodeChanged()),A.caption!==!0)p(u);else if(!B.is(u.parentNode,"figure.image")){c=u,u=u.cloneNode(!0),u.contentEditable=!0,a=B.create("figure",{"class":"image"}),a.appendChild(u),a.appendChild(B.create("figcaption",{contentEditable:!0},"Caption")),a.contentEditable=!1;var d=B.getParent(c,i);d?B.split(d,c,a):B.replace(a,c),b.selection.select(a)}}else if(u){var e=B.is(u.parentNode,"figure.image")?u.parentNode:u;B.remove(e),b.focus(),b.nodeChanged(),B.isEmpty(b.getBody())&&(b.setContent(""),b.selection.setCursorLocation())}})}function r(a){var c,e,f,g=a.meta||{};y&&y.value(b.convertURL(this.value(),"src")),h.each(g,function(a,b){t.find("#"+b).value(a)}),g.width||g.height||(c=b.convertURL(this.value(),"src"),e=j.getPrependUrl(b),f=new d("^(?:[a-z]+:)?//","i"),e&&!f.test(c)&&c.substring(0,e.length)!==e&&(c=e+c),this.value(c),l.getImageSize(b.documentBaseURI.toAbsolute(this.value()),function(a){a.width&&a.height&&D&&(w=a.width,x=a.height,t.find("#width").value(w),t.find("#height").value(x))}))}function s(a){a.meta=t.toJSON()}var t,u,v,w,x,y,z,A={},B=b.dom,C=b.settings,D=j.hasDimensions(b);u=b.selection.getNode(),v=B.getParent(u,"figure.image"),v&&(u=B.select("img",v)[0]),u&&("IMG"!==u.nodeName||u.getAttribute("data-mce-object")||u.getAttribute("data-mce-placeholder"))&&(u=null),u&&(w=B.getAttrib(u,"width"),x=B.getAttrib(u,"height"),A={src:B.getAttrib(u,"src"),alt:B.getAttrib(u,"alt"),title:B.getAttrib(u,"title"),"class":B.getAttrib(u,"class"),width:w,height:x,caption:!!v}),e&&(y={type:"listbox",label:"Image list",values:l.buildListItems(e,function(a){a.value=b.convertURL(a.value||a.url,"src")},[{text:"None",value:""}]),value:A.src&&b.convertURL(A.src,"src"),onselect:function(a){var b=t.find("#alt");(!b.value()||a.lastControl&&b.value()===a.lastControl.text())&&b.value(a.control.text()),t.find("#src").value(a.control.value()).fire("change")},onPostRender:function(){y=this}}),j.getClassList(b)&&(z={name:"class",type:"listbox",label:"Class",values:l.buildListItems(j.getClassList(b),function(a){a.value&&(a.textStyle=function(){return b.formatter.getCssText({inline:"img",classes:[a.value]})})})});var E=[{name:"src",type:"filepicker",filetype:"image",label:"Source",autofocus:!0,onchange:r,onbeforecall:s},y];if(j.hasDescription(b)&&E.push({name:"alt",type:"textbox",label:"Image description"}),j.hasImageTitle(b)&&E.push({name:"title",type:"textbox",label:"Image Title"}),D&&E.push({type:"container",label:"Dimensions",layout:"flex",direction:"row",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:3,onchange:m,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:3,onchange:m,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}),E.push(z),j.hasImageCaption(b)&&E.push({name:"caption",type:"checkbox",label:"Caption"}),j.hasAdvTab(b)||b.settings.images_upload_url){var F=[{title:"General",type:"form",items:E}];if(j.hasAdvTab(b)&&(u&&(u.style.marginLeft&&u.style.marginRight&&u.style.marginLeft===u.style.marginRight&&(A.hspace=l.removePixelSuffix(u.style.marginLeft)),u.style.marginTop&&u.style.marginBottom&&u.style.marginTop===u.style.marginBottom&&(A.vspace=l.removePixelSuffix(u.style.marginTop)),u.style.borderWidth&&(A.border=l.removePixelSuffix(u.style.borderWidth)),A.style=b.dom.serializeStyle(b.dom.parseStyle(b.dom.getAttrib(u,"style")))),F.push({title:"Advanced",type:"form",pack:"start",items:[{label:"Style",name:"style",type:"textbox",onchange:o},{type:"form",layout:"grid",packV:"start",columns:2,padding:0,alignH:["left","right"],defaults:{type:"textbox",maxWidth:50,onchange:n},items:[{label:"Vertical space",name:"vspace"},{label:"Horizontal space",name:"hspace"},{label:"Border",name:"border"}]}]})),b.settings.images_upload_url){var G=".jpg,.jpeg,.png,.gif",H={title:"Upload",type:"form",layout:"flex",direction:"column",align:"stretch",padding:"20 20 20 20",items:[{type:"container",layout:"flex",direction:"column",align:"center",spacing:10,items:[{text:"Browse for an image",type:"browsebutton",accept:G,onchange:g},{text:"OR",type:"label"}]},{text:"Drop an image here",type:"dropzone",accept:G,height:100,onchange:g}]};F.push(H)}t=b.windowManager.open({title:"Insert/edit image",data:A,bodyType:"tabpanel",body:F,onSubmit:q})}else t=b.windowManager.open({title:"Insert/edit image",data:A,body:E,onSubmit:q})}function n(){e(m)}return{open:n}}}),g("2",["6"],function(a){var b=function(b){b.addCommand("mceImage",a(b).open)};return{register:b}}),g("3",["7"],function(a){var b=function(a){var b=a.attr("class");return b&&/\bimage\b/.test(b)},c=function(c){return function(d){for(var e,f=d.length,g=function(a){a.attr("contenteditable",c?"true":null)};f--;)e=d[f],b(e)&&(e.attr("contenteditable",c?"false":null),a.each(e.getAll("figcaption"),g),a.each(e.getAll("img"),g))}},d=function(a){a.on("preInit",function(){a.parser.addNodeFilter("figure",c(!0)),a.serializer.addNodeFilter("figure",c(!1))})};return{setup:d}}),g("4",["6"],function(a){var b=function(b){b.addButton("image",{icon:"image",tooltip:"Insert/edit image",onclick:a(b).open,stateSelector:"img:not([data-mce-object],[data-mce-placeholder]),figure.image"}),b.addMenuItem("image",{icon:"image",text:"Image",onclick:a(b).open,context:"insert",prependToContext:!0})};return{register:b}}),g("0",["1","2","3","4"],function(a,b,c,d){return a.add("image",function(a){c.setup(a),d.register(a),b.register(a)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/plugins/imagetools/plugin.js b/media/vendor/tinymce/plugins/imagetools/plugin.js index 108be3f4014a2..3e9e204e12bfb 100644 --- a/media/vendor/tinymce/plugins/imagetools/plugin.js +++ b/media/vendor/tinymce/plugins/imagetools/plugin.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,13 +76,87 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.plugins.imagetools.Plugin","ephox.imagetools.api.BlobConversions","ephox.imagetools.api.ImageTransformations","tinymce.core.Env","tinymce.core.PluginManager","tinymce.core.util.Delay","tinymce.core.util.Promise","tinymce.core.util.Tools","tinymce.core.util.URI","tinymce.plugins.imagetools.core.ImageSize","tinymce.plugins.imagetools.core.Proxy","tinymce.plugins.imagetools.ui.Dialog","ephox.imagetools.util.Conversions","ephox.imagetools.util.ImageResult","ephox.imagetools.transformations.Filters","ephox.imagetools.transformations.ImageTools","global!tinymce.util.Tools.resolve","tinymce.plugins.imagetools.core.Errors","tinymce.plugins.imagetools.core.Utils","global!Math","tinymce.core.dom.DOMUtils","tinymce.core.ui.Factory","tinymce.plugins.imagetools.core.UndoStack","tinymce.plugins.imagetools.ui.ImagePanel","ephox.imagetools.util.Promise","ephox.imagetools.util.Canvas","ephox.imagetools.util.Mime","ephox.imagetools.util.ImageSize","ephox.imagetools.transformations.ColorMatrix","ephox.imagetools.transformations.ImageResizerCanvas","ephox.katamari.api.Arr","ephox.katamari.api.Fun","tinymce.core.geom.Rect","tinymce.plugins.imagetools.ui.CropRect","ephox.katamari.api.Option","global!Array","global!Error","global!String","tinymce.core.dom.DomQuery","tinymce.core.util.Observable","tinymce.core.util.VK","global!Object"] +["tinymce.plugins.imagetools.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.imagetools.api.Commands","tinymce.plugins.imagetools.core.UploadSelectedImage","tinymce.plugins.imagetools.ui.Buttons","tinymce.plugins.imagetools.ui.ContextToolbar","global!tinymce.util.Tools.resolve","tinymce.core.util.Tools","tinymce.plugins.imagetools.core.Actions","ephox.katamari.api.Fun","tinymce.plugins.imagetools.api.Settings","ephox.imagetools.api.BlobConversions","ephox.imagetools.api.ImageTransformations","global!Array","global!Error","ephox.sand.api.URL","global!clearTimeout","tinymce.core.util.Delay","tinymce.core.util.Promise","tinymce.core.util.URI","tinymce.plugins.imagetools.core.ImageSize","tinymce.plugins.imagetools.core.Proxy","tinymce.plugins.imagetools.ui.Dialog","ephox.imagetools.util.Conversions","ephox.imagetools.util.ImageResult","ephox.imagetools.transformations.Filters","ephox.imagetools.transformations.ImageTools","ephox.sand.util.Global","tinymce.plugins.imagetools.core.Errors","tinymce.plugins.imagetools.core.Utils","global!Math","global!setTimeout","tinymce.core.dom.DOMUtils","tinymce.core.ui.Factory","tinymce.plugins.imagetools.core.UndoStack","tinymce.plugins.imagetools.ui.ImagePanel","ephox.imagetools.util.Promise","ephox.imagetools.util.Canvas","ephox.imagetools.util.Mime","ephox.imagetools.util.ImageSize","ephox.imagetools.transformations.ColorMatrix","ephox.imagetools.transformations.ImageResizerCanvas","ephox.katamari.api.Resolve","ephox.katamari.api.Arr","ephox.sand.api.FileReader","ephox.sand.api.XMLHttpRequest","global!document","global!Image","tinymce.core.geom.Rect","tinymce.plugins.imagetools.core.LoadImage","tinymce.plugins.imagetools.ui.CropRect","ephox.katamari.api.Global","ephox.katamari.api.Option","global!String","tinymce.core.dom.DomQuery","tinymce.core.util.Observable","tinymce.core.util.VK","global!Object"] jsc*/ +define( + 'ephox.katamari.api.Cell', + + [ + ], + + function () { + var Cell = function (initial) { + var value = initial; + + var get = function () { + return value; + }; + + var set = function (v) { + value = v; + }; + + var clone = function () { + return Cell(get()); + }; + + return { + get: get, + set: set, + clone: clone + }; + }; + + return Cell; + } +); + +defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); +/** + * 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.PluginManager', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.PluginManager'); + } +); + +/** + * 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.util.Tools', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.util.Tools'); + } +); + /* eslint-disable */ /* jshint ignore:start */ @@ -1355,47 +1429,228 @@ define( }; } ); -defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); -/** - * 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 - */ +defineGlobal("global!Array", Array); +defineGlobal("global!Error", Error); +define( + 'ephox.katamari.api.Fun', + + [ + 'global!Array', + 'global!Error' + ], + + function (Array, Error) { + + var noop = function () { }; + + var compose = function (fa, fb) { + return function () { + return fa(fb.apply(null, arguments)); + }; + }; + + var constant = function (value) { + return function () { + return value; + }; + }; + + var identity = function (x) { + return x; + }; + + var tripleEquals = function(a, b) { + return a === b; + }; + + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var curry = function (f) { + // equivalent to arguments.slice(1) + // starting at 1 because 0 is the f, makes things tricky. + // Pay attention to what variable is where, and the -1 magic. + // thankfully, we have tests for this. + var args = new Array(arguments.length - 1); + for (var i = 1; i < arguments.length; i++) args[i-1] = arguments[i]; + + return function () { + var newArgs = new Array(arguments.length); + for (var j = 0; j < newArgs.length; j++) newArgs[j] = arguments[j]; + + var all = args.concat(newArgs); + return f.apply(null, all); + }; + }; + + var not = function (f) { + return function () { + return !f.apply(null, arguments); + }; + }; + + var die = function (msg) { + return function () { + throw new Error(msg); + }; + }; + + var apply = function (f) { + return f(); + }; + + var call = function(f) { + f(); + }; + + var never = constant(false); + var always = constant(true); + + + return { + noop: noop, + compose: compose, + constant: constant, + identity: identity, + tripleEquals: tripleEquals, + curry: curry, + not: not, + die: die, + apply: apply, + call: call, + never: never, + always: always + }; + } +); define( - 'tinymce.core.Env', + 'ephox.katamari.api.Global', + [ - 'global!tinymce.util.Tools.resolve' ], - function (resolve) { - return resolve('tinymce.Env'); + + function () { + // Use window object as the global if it's available since CSP will block script evals + if (typeof window !== 'undefined') { + return window; + } else { + return Function('return this;')(); + } } ); -/** - * 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.PluginManager', + 'ephox.katamari.api.Resolve', + [ - 'global!tinymce.util.Tools.resolve' + 'ephox.katamari.api.Global' ], - function (resolve) { - return resolve('tinymce.PluginManager'); + + function (Global) { + /** path :: ([String], JsObj?) -> JsObj */ + var path = function (parts, scope) { + var o = scope !== undefined ? scope : Global; + for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) + o = o[parts[i]]; + return o; + }; + + /** resolve :: (String, JsObj?) -> JsObj */ + var resolve = function (p, scope) { + var parts = p.split('.'); + return path(parts, scope); + }; + + /** step :: (JsObj, String) -> JsObj */ + var step = function (o, part) { + if (o[part] === undefined || o[part] === null) + o[part] = {}; + return o[part]; + }; + + /** forge :: ([String], JsObj?) -> JsObj */ + var forge = function (parts, target) { + var o = target !== undefined ? target : Global; + for (var i = 0; i < parts.length; ++i) + o = step(o, parts[i]); + return o; + }; + + /** namespace :: (String, JsObj?) -> JsObj */ + var namespace = function (name, target) { + var parts = name.split('.'); + return forge(parts, target); + }; + + return { + path: path, + resolve: resolve, + forge: forge, + namespace: namespace + }; + } +); + + +define( + 'ephox.sand.util.Global', + + [ + 'ephox.katamari.api.Resolve' + ], + + function (Resolve) { + var unsafe = function (name, scope) { + return Resolve.resolve(name, scope); + }; + + var getOrDie = function (name, scope) { + var actual = unsafe(name, scope); + + if (actual === undefined) throw name + ' not available on this browser'; + return actual; + }; + + return { + getOrDie: getOrDie + }; } ); +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 + }; + } +); +defineGlobal("global!clearTimeout", clearTimeout); /** * ResolveGlobal.js * @@ -1447,17 +1702,17 @@ define( */ define( - 'tinymce.core.util.Tools', + 'tinymce.core.util.URI', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.util.Tools'); + return resolve('tinymce.util.URI'); } ); /** - * ResolveGlobal.js + * Settings.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -1467,12 +1722,22 @@ define( */ define( - 'tinymce.core.util.URI', + 'tinymce.plugins.imagetools.api.Settings', [ - 'global!tinymce.util.Tools.resolve' ], - function (resolve) { - return resolve('tinymce.util.URI'); + function () { + var getToolbarItems = function (editor) { + return editor.getParam('imagetools_toolbar', 'rotateleft rotateright | flipv fliph | crop editimage imageoptions'); + }; + + var getProxyUrl = function (editor) { + return editor.getParam('imagetools_proxy'); + }; + + return { + getToolbarItems: getToolbarItems, + getProxyUrl: getProxyUrl + }; } ); @@ -1562,99 +1827,6 @@ define( } ); -defineGlobal("global!Array", Array); -defineGlobal("global!Error", Error); -define( - 'ephox.katamari.api.Fun', - - [ - 'global!Array', - 'global!Error' - ], - - function (Array, Error) { - - var noop = function () { }; - - var compose = function (fa, fb) { - return function () { - return fa(fb.apply(null, arguments)); - }; - }; - - var constant = function (value) { - return function () { - return value; - }; - }; - - var identity = function (x) { - return x; - }; - - var tripleEquals = function(a, b) { - return a === b; - }; - - // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome - var curry = function (f) { - // equivalent to arguments.slice(1) - // starting at 1 because 0 is the f, makes things tricky. - // Pay attention to what variable is where, and the -1 magic. - // thankfully, we have tests for this. - var args = new Array(arguments.length - 1); - for (var i = 1; i < arguments.length; i++) args[i-1] = arguments[i]; - - return function () { - var newArgs = new Array(arguments.length); - for (var j = 0; j < newArgs.length; j++) newArgs[j] = arguments[j]; - - var all = args.concat(newArgs); - return f.apply(null, all); - }; - }; - - var not = function (f) { - return function () { - return !f.apply(null, arguments); - }; - }; - - var die = function (msg) { - return function () { - throw new Error(msg); - }; - }; - - var apply = function (f) { - return f(); - }; - - var call = function(f) { - f(); - }; - - var never = constant(false); - var always = constant(true); - - - return { - noop: noop, - compose: compose, - constant: constant, - identity: identity, - tripleEquals: tripleEquals, - curry: curry, - not: not, - die: die, - apply: apply, - call: call, - never: never, - always: always - }; - } -); - defineGlobal("global!Object", Object); define( 'ephox.katamari.api.Option', @@ -2107,6 +2279,14 @@ define( return copy; }; + var head = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[0]); + }; + + var last = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[xs.length - 1]); + }; + return { map: map, each: each, @@ -2131,7 +2311,45 @@ define( mapToObject: mapToObject, pure: pure, sort: sort, - range: range + range: range, + head: head, + last: last + }; + } +); +define( + 'ephox.sand.api.FileReader', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * IE10 and above per + * https://developer.mozilla.org/en-US/docs/Web/API/FileReader + */ + return function () { + var f = Global.getOrDie('FileReader'); + return new f(); + }; + } +); +define( + 'ephox.sand.api.XMLHttpRequest', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * IE8 and above per + * https://developer.mozilla.org/en/docs/XMLHttpRequest + */ + return function () { + var f = Global.getOrDie('XMLHttpRequest'); + return new f(); }; } ); @@ -2148,10 +2366,12 @@ define( define( 'tinymce.plugins.imagetools.core.Utils', [ + 'ephox.sand.api.FileReader', + 'ephox.sand.api.XMLHttpRequest', 'tinymce.core.util.Promise', 'tinymce.core.util.Tools' ], - function (Promise, Tools) { + function (FileReader, XMLHttpRequest, Promise, Tools) { var isValue = function (obj) { return obj !== null && obj !== undefined; }; @@ -2369,6 +2589,7 @@ define( ); defineGlobal("global!Math", Math); +defineGlobal("global!setTimeout", setTimeout); /** * ResolveGlobal.js * @@ -2456,7 +2677,7 @@ define( } function canRedo() { - return index != -1 && index < data.length - 1; + return index !== -1 && index < data.length - 1; } return { @@ -2471,6 +2692,8 @@ define( } ); +defineGlobal("global!document", document); +defineGlobal("global!Image", Image); /** * ResolveGlobal.js * @@ -2491,6 +2714,43 @@ define( } ); +/** + * LoadImage.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.imagetools.core.LoadImage', + [ + 'tinymce.core.util.Promise' + ], + function (Promise) { + var loadImage = function (image) { + return new Promise(function (resolve) { + var loaded = function () { + image.removeEventListener('load', loaded); + resolve(image); + }; + + if (image.complete) { + resolve(image); + } else { + image.addEventListener('load', loaded); + } + }); + }; + + return { + loadImage: loadImage + }; + } +); + /** * ResolveGlobal.js * @@ -2630,7 +2890,7 @@ define( h = 20; } - rect = currentRect = Rect.clamp({ x: x, y: y, w: w, h: h }, clampRect, handle.name == 'move'); + rect = currentRect = Rect.clamp({ x: x, y: y, w: w, h: h }, clampRect, handle.name === 'move'); rect = getRelativeRect(clampRect, rect); instance.fire('updateRect', { rect: rect }); @@ -2688,7 +2948,7 @@ define( var activeHandle; Tools.each(handles, function (handle) { - if (e.target.id == id + '-' + handle.name) { + if (e.target.id === id + '-' + handle.name) { activeHandle = handle; return false; } @@ -2837,28 +3097,16 @@ define( define( 'tinymce.plugins.imagetools.ui.ImagePanel', [ + 'global!document', + 'global!Image', 'tinymce.core.geom.Rect', 'tinymce.core.ui.Factory', 'tinymce.core.util.Promise', 'tinymce.core.util.Tools', + 'tinymce.plugins.imagetools.core.LoadImage', 'tinymce.plugins.imagetools.ui.CropRect' ], - function (Rect, Factory, Promise, Tools, CropRect) { - function loadImage(image) { - return new Promise(function (resolve) { - function loaded() { - image.removeEventListener('load', loaded); - resolve(image); - } - - if (image.complete) { - resolve(image); - } else { - image.addEventListener('load', loaded); - } - }); - } - + function (document, Image, Rect, Factory, Promise, Tools, LoadImage, CropRect) { var create = function (settings) { var Control = Factory.get('Control'); var ImagePanel = Control.extend({ @@ -2893,7 +3141,7 @@ define( img.src = url; - loadImage(img).then(function () { + LoadImage.loadImage(img).then(function () { var rect, $img, lastRect = self.state.get('viewRect'); $img = self.$el.find('img'); @@ -2910,7 +3158,7 @@ define( self.state.set('viewRect', rect); self.state.set('rect', Rect.inflate(rect, -20, -20)); - if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) { + if (!lastRect || lastRect.w !== rect.w || lastRect.h !== rect.h) { self.zoomFit(); } @@ -3079,7 +3327,9 @@ define( [ 'ephox.imagetools.api.BlobConversions', 'ephox.imagetools.api.ImageTransformations', + 'ephox.sand.api.URL', 'global!Math', + 'global!setTimeout', 'tinymce.core.dom.DOMUtils', 'tinymce.core.ui.Factory', 'tinymce.core.util.Promise', @@ -3087,7 +3337,7 @@ define( 'tinymce.plugins.imagetools.core.UndoStack', 'tinymce.plugins.imagetools.ui.ImagePanel' ], - function (BlobConversions, ImageTransformations, Math, DOMUtils, Factory, Promise, Tools, UndoStack, ImagePanel) { + function (BlobConversions, ImageTransformations, URL, Math, setTimeout, DOMUtils, Factory, Promise, Tools, UndoStack, ImagePanel) { function createState(blob) { return { blob: blob, @@ -3122,7 +3372,7 @@ define( newHeight = parseInt(heightCtrl.value(), 10); if (win.find('#constrain')[0].checked() && width && height && newWidth && newHeight) { - if (e.control.settings.name == 'w') { + if (e.control.settings.name === 'w') { newHeight = Math.round(newWidth * ratioW); heightCtrl.value(newHeight); } else { @@ -3159,7 +3409,7 @@ define( function switchPanel(targetPanel) { return function () { var hidePanels = Tools.grep(panels, function (panel) { - return panel.settings.name != targetPanel; + return panel.settings.name !== targetPanel; }); Tools.each(hidePanels, function (panel) { @@ -3421,7 +3671,7 @@ define( { type: 'spacer', flex: 1 }, { text: 'Apply', subtype: 'primary', onclick: crop } ]).hide().on('show hide', function (e) { - imagePanel.toggleCropRect(e.type == 'show'); + imagePanel.toggleCropRect(e.type === 'show'); }).on('show', disableUndoRedo); function toggleConstrain(e) { @@ -3600,7 +3850,7 @@ define( ); /** - * Plugin.js + * Actions.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -3610,194 +3860,201 @@ define( */ define( - 'tinymce.plugins.imagetools.Plugin', + 'tinymce.plugins.imagetools.core.Actions', [ 'ephox.imagetools.api.BlobConversions', 'ephox.imagetools.api.ImageTransformations', - 'tinymce.core.Env', - 'tinymce.core.PluginManager', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.URL', + 'global!clearTimeout', 'tinymce.core.util.Delay', 'tinymce.core.util.Promise', 'tinymce.core.util.Tools', 'tinymce.core.util.URI', + 'tinymce.plugins.imagetools.api.Settings', 'tinymce.plugins.imagetools.core.ImageSize', 'tinymce.plugins.imagetools.core.Proxy', 'tinymce.plugins.imagetools.ui.Dialog' ], - function ( - BlobConversions, ImageTransformations, Env, PluginManager, Delay, Promise, Tools, - URI, ImageSize, Proxy, Dialog - ) { - var plugin = function (editor) { - var count = 0, imageUploadTimer, lastSelectedImage; - - if (!Env.fileApi) { - return; - } + function (BlobConversions, ImageTransformations, Fun, URL, clearTimeout, Delay, Promise, Tools, URI, Settings, ImageSize, Proxy, Dialog) { + var count = 0; - function displayError(error) { - editor.notificationManager.open({ - text: error, - type: 'error' - }); - } + var isEditableImage = function (editor, img) { + var selectorMatched = editor.dom.is(img, 'img:not([data-mce-object],[data-mce-placeholder])'); - function getSelectedImage() { - return editor.selection.getNode(); - } + return selectorMatched && (isLocalImage(editor, img) || isCorsImage(editor, img) || editor.settings.imagetools_proxy); + }; - function extractFilename(url) { - var m = url.match(/\/([^\/\?]+)?\.(?:jpeg|jpg|png|gif)(?:\?|$)/i); - if (m) { - return editor.dom.encode(m[1]); - } - return null; - } + var displayError = function (editor, error) { + editor.notificationManager.open({ + text: error, + type: 'error' + }); + }; + + var getSelectedImage = function (editor) { + return editor.selection.getNode(); + }; - function createId() { - return 'imagetools' + count++; + var extractFilename = function (editor, url) { + var m = url.match(/\/([^\/\?]+)?\.(?:jpeg|jpg|png|gif)(?:\?|$)/i); + if (m) { + return editor.dom.encode(m[1]); } + return null; + }; - function isLocalImage(img) { - var url = img.src; + var createId = function () { + return 'imagetools' + count++; + }; - return url.indexOf('data:') === 0 || url.indexOf('blob:') === 0 || new URI(url).host === editor.documentBaseURI.host; - } + var isLocalImage = function (editor, img) { + var url = img.src; - function isCorsImage(img) { - return Tools.inArray(editor.settings.imagetools_cors_hosts, new URI(img.src).host) !== -1; - } + return url.indexOf('data:') === 0 || url.indexOf('blob:') === 0 || new URI(url).host === editor.documentBaseURI.host; + }; - function getApiKey() { - return editor.settings.api_key || editor.settings.imagetools_api_key; - } + var isCorsImage = function (editor, img) { + return Tools.inArray(editor.settings.imagetools_cors_hosts, new URI(img.src).host) !== -1; + }; - function imageToBlob(img) { - var src = img.src, apiKey; + var getApiKey = function (editor) { + return editor.settings.api_key || editor.settings.imagetools_api_key; + }; - if (isCorsImage(img)) { - return Proxy.getUrl(img.src, null); - } + var imageToBlob = function (editor, img) { + var src = img.src, apiKey; - if (!isLocalImage(img)) { - src = editor.settings.imagetools_proxy; - src += (src.indexOf('?') === -1 ? '?' : '&') + 'url=' + encodeURIComponent(img.src); - apiKey = getApiKey(); - return Proxy.getUrl(src, apiKey); - } + if (isCorsImage(editor, img)) { + return Proxy.getUrl(img.src, null); + } - return BlobConversions.imageToBlob(img); + if (!isLocalImage(editor, img)) { + src = Settings.getProxyUrl(editor); + src += (src.indexOf('?') === -1 ? '?' : '&') + 'url=' + encodeURIComponent(img.src); + apiKey = getApiKey(editor); + return Proxy.getUrl(src, apiKey); } - function findSelectedBlob() { - var blobInfo; - blobInfo = editor.editorUpload.blobCache.getByUri(getSelectedImage().src); - if (blobInfo) { - return Promise.resolve(blobInfo.blob()); - } + return BlobConversions.imageToBlob(img); + }; - return imageToBlob(getSelectedImage()); + var findSelectedBlob = function (editor) { + var blobInfo; + blobInfo = editor.editorUpload.blobCache.getByUri(getSelectedImage(editor).src); + if (blobInfo) { + return Promise.resolve(blobInfo.blob()); } - function startTimedUpload() { - imageUploadTimer = Delay.setEditorTimeout(editor, function () { - editor.editorUpload.uploadImagesAuto(); - }, editor.settings.images_upload_timeout || 30000); - } + return imageToBlob(editor, getSelectedImage(editor)); + }; - function cancelTimedUpload() { - clearTimeout(imageUploadTimer); - } + var startTimedUpload = function (editor, imageUploadTimerState) { + var imageUploadTimer = Delay.setEditorTimeout(editor, function () { + editor.editorUpload.uploadImagesAuto(); + }, editor.settings.images_upload_timeout || 30000); - function updateSelectedImage(ir, uploadImmediately) { - return ir.toBlob().then(function (blob) { - var uri, name, blobCache, blobInfo, selectedImage; + imageUploadTimerState.set(imageUploadTimer); + }; - blobCache = editor.editorUpload.blobCache; - selectedImage = getSelectedImage(); - uri = selectedImage.src; + var cancelTimedUpload = function (imageUploadTimerState) { + clearTimeout(imageUploadTimerState.get()); + }; - if (editor.settings.images_reuse_filename) { - blobInfo = blobCache.getByUri(uri); - if (blobInfo) { - uri = blobInfo.uri(); - name = blobInfo.name(); - } else { - name = extractFilename(uri); - } + var updateSelectedImage = function (editor, ir, uploadImmediately, imageUploadTimerState) { + return ir.toBlob().then(function (blob) { + var uri, name, blobCache, blobInfo, selectedImage; + + blobCache = editor.editorUpload.blobCache; + selectedImage = getSelectedImage(editor); + uri = selectedImage.src; + + if (editor.settings.images_reuse_filename) { + blobInfo = blobCache.getByUri(uri); + if (blobInfo) { + uri = blobInfo.uri(); + name = blobInfo.name(); + } else { + name = extractFilename(editor, uri); } + } - blobInfo = blobCache.create({ - id: createId(), - blob: blob, - base64: ir.toBase64(), - uri: uri, - name: name - }); + blobInfo = blobCache.create({ + id: createId(), + blob: blob, + base64: ir.toBase64(), + uri: uri, + name: name + }); - blobCache.add(blobInfo); + blobCache.add(blobInfo); - editor.undoManager.transact(function () { - function imageLoadedHandler() { - editor.$(selectedImage).off('load', imageLoadedHandler); - editor.nodeChanged(); + editor.undoManager.transact(function () { + function imageLoadedHandler() { + editor.$(selectedImage).off('load', imageLoadedHandler); + editor.nodeChanged(); - if (uploadImmediately) { - editor.editorUpload.uploadImagesAuto(); - } else { - cancelTimedUpload(); - startTimedUpload(); - } + if (uploadImmediately) { + editor.editorUpload.uploadImagesAuto(); + } else { + cancelTimedUpload(imageUploadTimerState); + startTimedUpload(editor, imageUploadTimerState); } + } - editor.$(selectedImage).on('load', imageLoadedHandler); - - editor.$(selectedImage).attr({ - src: blobInfo.blobUri() - }).removeAttr('data-mce-src'); - }); + editor.$(selectedImage).on('load', imageLoadedHandler); - return blobInfo; + editor.$(selectedImage).attr({ + src: blobInfo.blobUri() + }).removeAttr('data-mce-src'); }); - } - function selectedImageOperation(fn) { - return function () { - return editor._scanForImages(). - then(findSelectedBlob). - then(BlobConversions.blobToImageResult). - then(fn). - then(updateSelectedImage, displayError); - }; - } + return blobInfo; + }); + }; - function rotate(angle) { - return function () { - return selectedImageOperation(function (imageResult) { - var size = ImageSize.getImageSize(getSelectedImage()); + var selectedImageOperation = function (editor, imageUploadTimerState, fn) { + return function () { + return editor._scanForImages(). + then(Fun.curry(findSelectedBlob, editor)). + then(BlobConversions.blobToImageResult). + then(fn). + then(function (imageResult) { + return updateSelectedImage(editor, imageResult, false, imageUploadTimerState); + }, function (error) { + displayError(editor, error); + }); + }; + }; - if (size) { - ImageSize.setImageSize(getSelectedImage(), { - w: size.h, - h: size.w - }); - } + var rotate = function (editor, imageUploadTimerState, angle) { + return function () { + return selectedImageOperation(editor, imageUploadTimerState, function (imageResult) { + var size = ImageSize.getImageSize(getSelectedImage(editor)); - return ImageTransformations.rotate(imageResult, angle); - })(); - }; - } + if (size) { + ImageSize.setImageSize(getSelectedImage(editor), { + w: size.h, + h: size.w + }); + } - function flip(axis) { - return function () { - return selectedImageOperation(function (imageResult) { - return ImageTransformations.flip(imageResult, axis); - })(); - }; - } + return ImageTransformations.rotate(imageResult, angle); + })(); + }; + }; + + var flip = function (editor, imageUploadTimerState, axis) { + return function () { + return selectedImageOperation(editor, imageUploadTimerState, function (imageResult) { + return ImageTransformations.flip(imageResult, axis); + })(); + }; + }; - function editImageDialog() { - var img = getSelectedImage(), originalSize = ImageSize.getNaturalImageSize(img); + var editImageDialog = function (editor, imageUploadTimerState) { + return function () { + var img = getSelectedImage(editor), originalSize = ImageSize.getNaturalImageSize(img); var handleDialogBlob = function (blob) { return new Promise(function (resolve) { @@ -3805,7 +4062,7 @@ define( then(function (newImage) { var newSize = ImageSize.getNaturalImageSize(newImage); - if (originalSize.w != newSize.w || originalSize.h != newSize.h) { + if (originalSize.w !== newSize.w || originalSize.h !== newSize.h) { if (ImageSize.getImageSize(img)) { ImageSize.setImageSize(img, newSize); } @@ -3817,114 +4074,227 @@ define( }); }; - var openDialog = function (imageResult) { + var openDialog = function (editor, imageResult) { return Dialog.edit(editor, imageResult).then(handleDialogBlob). then(BlobConversions.blobToImageResult). then(function (imageResult) { - return updateSelectedImage(imageResult, true); + return updateSelectedImage(editor, imageResult, true, imageUploadTimerState); }, function () { // Close dialog }); }; - findSelectedBlob(). + findSelectedBlob(editor). then(BlobConversions.blobToImageResult). - then(openDialog, displayError); - } + then(Fun.curry(openDialog, editor), function (error) { + displayError(editor, error); + }); + }; + }; - function addButtons() { - editor.addButton('rotateleft', { - title: 'Rotate counterclockwise', - cmd: 'mceImageRotateLeft' - }); + return { + rotate: rotate, + flip: flip, + editImageDialog: editImageDialog, + isEditableImage: isEditableImage, + cancelTimedUpload: cancelTimedUpload + }; + } +); - editor.addButton('rotateright', { - title: 'Rotate clockwise', - cmd: 'mceImageRotateRight' - }); +/** + * 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 + */ - editor.addButton('flipv', { - title: 'Flip vertically', - cmd: 'mceImageFlipVertical' - }); +define( + 'tinymce.plugins.imagetools.api.Commands', + [ + 'tinymce.core.util.Tools', + 'tinymce.plugins.imagetools.core.Actions' + ], + function (Tools, Actions) { + var register = function (editor, imageUploadTimerState) { + Tools.each({ + mceImageRotateLeft: Actions.rotate(editor, imageUploadTimerState, -90), + mceImageRotateRight: Actions.rotate(editor, imageUploadTimerState, 90), + mceImageFlipVertical: Actions.flip(editor, imageUploadTimerState, 'v'), + mceImageFlipHorizontal: Actions.flip(editor, imageUploadTimerState, 'h'), + mceEditImage: Actions.editImageDialog(editor, imageUploadTimerState) + }, function (fn, cmd) { + editor.addCommand(cmd, fn); + }); + }; - editor.addButton('fliph', { - title: 'Flip horizontally', - cmd: 'mceImageFlipHorizontal' - }); + return { + register: register + }; + } +); - editor.addButton('editimage', { - title: 'Edit image', - cmd: 'mceEditImage' - }); +/** + * Plugin.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 + */ - editor.addButton('imageoptions', { - title: 'Image options', - icon: 'options', - cmd: 'mceImage' - }); +define( + 'tinymce.plugins.imagetools.core.UploadSelectedImage', + [ + 'tinymce.plugins.imagetools.core.Actions' + ], + function (Actions) { + var setup = function (editor, imageUploadTimerState, lastSelectedImageState) { + editor.on('NodeChange', function (e) { + var lastSelectedImage = lastSelectedImageState.get(); + + // If the last node we selected was an image + // And had a source that doesn't match the current blob url + // We need to attempt to upload it + if (lastSelectedImage && lastSelectedImage.src !== e.element.src) { + Actions.cancelTimedUpload(imageUploadTimerState); + editor.editorUpload.uploadImagesAuto(); + lastSelectedImageState.set(null); + } - /* - editor.addButton('crop', { - title: 'Crop', - onclick: startCrop - }); - */ - } - - function addEvents() { - editor.on('NodeChange', function (e) { - // If the last node we selected was an image - // And had a source that doesn't match the current blob url - // We need to attempt to upload it - if (lastSelectedImage && lastSelectedImage.src != e.element.src) { - cancelTimedUpload(); - editor.editorUpload.uploadImagesAuto(); - lastSelectedImage = undefined; - } + // Set up the lastSelectedImage + if (Actions.isEditableImage(editor, e.element)) { + lastSelectedImageState.set(e.element); + } + }); + }; - // Set up the lastSelectedImage - if (isEditableImage(e.element)) { - lastSelectedImage = e.element; - } - }); - } + return { + setup: setup + }; + } +); + +/** + * Buttons.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 + */ - function isEditableImage(img) { - var selectorMatched = editor.dom.is(img, 'img:not([data-mce-object],[data-mce-placeholder])'); +define( + 'tinymce.plugins.imagetools.ui.Buttons', + [ + ], + function () { + var register = function (editor) { + editor.addButton('rotateleft', { + title: 'Rotate counterclockwise', + cmd: 'mceImageRotateLeft' + }); - return selectorMatched && (isLocalImage(img) || isCorsImage(img) || editor.settings.imagetools_proxy); - } + editor.addButton('rotateright', { + title: 'Rotate clockwise', + cmd: 'mceImageRotateRight' + }); - function addToolbars() { - var toolbarItems = editor.settings.imagetools_toolbar; + editor.addButton('flipv', { + title: 'Flip vertically', + cmd: 'mceImageFlipVertical' + }); - if (!toolbarItems) { - toolbarItems = 'rotateleft rotateright | flipv fliph | crop editimage imageoptions'; - } + editor.addButton('fliph', { + title: 'Flip horizontally', + cmd: 'mceImageFlipHorizontal' + }); - editor.addContextToolbar( - isEditableImage, - toolbarItems - ); - } + editor.addButton('editimage', { + title: 'Edit image', + cmd: 'mceEditImage' + }); - Tools.each({ - mceImageRotateLeft: rotate(-90), - mceImageRotateRight: rotate(90), - mceImageFlipVertical: flip('v'), - mceImageFlipHorizontal: flip('h'), - mceEditImage: editImageDialog - }, function (fn, cmd) { - editor.addCommand(cmd, fn); + editor.addButton('imageoptions', { + title: 'Image options', + icon: 'options', + cmd: 'mceImage' }); + }; + + return { + register: register + }; + } +); + +/** + * ContextToolbar.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.imagetools.ui.ContextToolbar', + [ + 'ephox.katamari.api.Fun', + 'tinymce.plugins.imagetools.api.Settings', + 'tinymce.plugins.imagetools.core.Actions' + ], + function (Fun, Settings, Actions) { + var register = function (editor) { + editor.addContextToolbar( + Fun.curry(Actions.isEditableImage, editor), + Settings.getToolbarItems(editor) + ); + }; - addButtons(); - addToolbars(); - addEvents(); + return { + register: register }; + } +); + +/** + * Plugin.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.imagetools.Plugin', + [ + 'ephox.katamari.api.Cell', + 'tinymce.core.PluginManager', + 'tinymce.plugins.imagetools.api.Commands', + 'tinymce.plugins.imagetools.core.UploadSelectedImage', + 'tinymce.plugins.imagetools.ui.Buttons', + 'tinymce.plugins.imagetools.ui.ContextToolbar' + ], + function (Cell, PluginManager, Commands, UploadSelectedImage, Buttons, ContextToolbar) { + PluginManager.add('imagetools', function (editor) { + var imageUploadTimerState = Cell(0); + var lastSelectedImageState = Cell(null); + + Commands.register(editor, imageUploadTimerState); + Buttons.register(editor); + ContextToolbar.register(editor); - PluginManager.add('imagetools', plugin); + UploadSelectedImage.setup(editor, imageUploadTimerState, lastSelectedImageState); + }); return function () { }; } diff --git a/media/vendor/tinymce/plugins/imagetools/plugin.min.js b/media/vendor/tinymce/plugins/imagetools/plugin.min.js index d4f1dea46db9a..e4626b002b3d5 100644 --- a/media/vendor/tinymce/plugins/imagetools/plugin.min.js +++ b/media/vendor/tinymce/plugins/imagetools/plugin.min.js @@ -1,2 +1,2 @@ -!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;ic?a=c:a0?3*d:d),f=.3086,g=.6094,h=.082,c(b,[f*(1-e)+e,g*(1-e),h*(1-e),0,0,f*(1-e),g*(1-e)+e,h*(1-e),0,0,f*(1-e),g*(1-e),h*(1-e)+e,0,0,0,0,0,1,0,0,0,0,0,1])}function g(b,d){var e,f,g,h,i;return d=a(d,-180,180)/180*Math.PI,e=Math.cos(d),f=Math.sin(d),g=.213,h=.715,i=.072,c(b,[g+e*(1-g)+f*-g,h+e*-h+f*-h,i+e*-i+f*(1-i),0,0,g+e*-g+.143*f,h+e*(1-h)+.14*f,i+e*-i+f*-.283,0,0,g+e*-g+f*-(1-g),h+e*-h+f*h,i+e*(1-i)+f*i,0,0,0,0,0,1,0,0,0,0,0,1])}function h(b,d){return d=a(255*d,-255,255),c(b,[1,0,0,0,d,0,1,0,0,d,0,0,1,0,d,0,0,0,1,0,0,0,0,0,1])}function i(b,d,e,f){return d=a(d,0,2),e=a(e,0,2),f=a(f,0,2),c(b,[d,0,0,0,0,0,e,0,0,0,0,0,f,0,0,0,0,0,1,0,0,0,0,0,1])}function j(b,e){return e=a(e,0,1),c(b,d([.393,.769,.189,0,0,.349,.686,.168,0,0,.272,.534,.131,0,0,0,0,0,1,0,0,0,0,0,1],e))}function k(b,e){return e=a(e,0,1),c(b,d([.33,.34,.33,0,0,.33,.34,.33,0,0,.33,.34,.33,0,0,0,0,0,1,0,0,0,0,0,1],e))}var l=[0,.01,.02,.04,.05,.06,.07,.08,.1,.11,.12,.14,.15,.16,.17,.18,.2,.21,.22,.24,.25,.27,.28,.3,.32,.34,.36,.38,.4,.42,.44,.46,.48,.5,.53,.56,.59,.62,.65,.68,.71,.74,.77,.8,.83,.86,.89,.92,.95,.98,1,1.06,1.12,1.18,1.24,1.3,1.36,1.42,1.48,1.54,1.6,1.66,1.72,1.78,1.84,1.9,1.96,2,2.12,2.25,2.37,2.5,2.62,2.75,2.87,3,3.2,3.4,3.6,3.8,4,4.3,4.7,4.9,5,5.5,6,6.5,6.8,7,7.3,7.5,7.8,8,8.4,8.7,9,9.4,9.6,9.8,10];return{identity:b,adjust:d,multiply:c,adjustContrast:e,adjustBrightness:h,adjustSaturation:f,adjustHue:g,adjustColors:i,adjustSepia:j,adjustGrayscale:k}}),g("e",["p","d","s"],function(a,b,c){function d(c,d){function e(a,b){var c,d,e,f,g,h=a.data,i=b[0],j=b[1],k=b[2],l=b[3],m=b[4],n=b[5],o=b[6],p=b[7],q=b[8],r=b[9],s=b[10],t=b[11],u=b[12],v=b[13],w=b[14],x=b[15],y=b[16],z=b[17],A=b[18],B=b[19];for(g=0;gc?a=c:a2)&&(i=i<.5?.5:2,k=!0),(j<.5||j>2)&&(j=j<.5?.5:2,k=!0);var l=f(a,i,j);return k?l.then(function(a){return e(a,b,c)}):l}function f(b,e,f){return new a(function(a){var g=d.getWidth(b),h=d.getHeight(b),i=Math.floor(g*e),j=Math.floor(h*f),k=c.create(i,j),l=c.get2dContext(k);l.drawImage(b,0,0,g,h,0,0,i,j),a(k)})}return{scale:e}}),g("f",["p","d","t"],function(a,b,c){function d(c,d){var e=c.toCanvas(),f=a.create(e.width,e.height),g=a.get2dContext(f),h=0,i=0;return d=d<0?360+d:d,90!=d&&270!=d||a.resize(f,f.height,f.width),90!=d&&180!=d||(h=f.width),270!=d&&180!=d||(i=f.height),g.translate(h,i),g.rotate(d*Math.PI/180),g.drawImage(e,0,0),b.fromCanvas(f,c.getType())}function e(c,d){var e=c.toCanvas(),f=a.create(e.width,e.height),g=a.get2dContext(f);return"v"==d?(g.scale(1,-1),g.drawImage(e,0,-f.height)):(g.scale(-1,1),g.drawImage(e,-f.width,0)),b.fromCanvas(f,c.getType())}function f(c,d,e,f,g){var h=c.toCanvas(),i=a.create(f,g),j=a.get2dContext(i);return j.drawImage(h,-d,-e),b.fromCanvas(i,c.getType())}function g(a,d,e){return c.scale(a.toCanvas(),d,e).then(function(c){return b.fromCanvas(c,a.getType())})}return{rotate:d,flip:e,crop:f,resize:g}}),g("2",["e","f"],function(a,b){var c=function(b){return a.invert(b)},d=function(b){return a.sharpen(b)},e=function(b){return a.emboss(b)},f=function(b,c){return a.gamma(b,c)},g=function(b,c){return a.exposure(b,c)},h=function(b,c,d,e){return a.colorize(b,c,d,e)},i=function(b,c){return a.brightness(b,c)},j=function(b,c){return a.hue(b,c)},k=function(b,c){return a.saturate(b,c)},l=function(b,c){return a.contrast(b,c)},m=function(b,c){return a.grayscale(b,c)},n=function(b,c){return a.sepia(b,c)},o=function(a,c){return b.flip(a,c)},p=function(a,c,d,e,f){return b.crop(a,c,d,e,f)},q=function(a,c,d){return b.resize(a,c,d)},r=function(a,c){return b.rotate(a,c)};return{invert:c,sharpen:d,emboss:e,brightness:i,hue:j,saturate:k,contrast:l,grayscale:m,sepia:n,colorize:h,gamma:f,exposure:g,flip:o,crop:p,resize:q,rotate:r}}),h("g",tinymce.util.Tools.resolve),g("3",["g"],function(a){return a("tinymce.Env")}),g("4",["g"],function(a){return a("tinymce.PluginManager")}),g("5",["g"],function(a){return a("tinymce.util.Delay")}),g("6",["g"],function(a){return a("tinymce.util.Promise")}),g("7",["g"],function(a){return a("tinymce.util.Tools")}),g("8",["g"],function(a){return a("tinymce.util.URI")}),g("9",[],function(){function a(a){function b(a){return/^[0-9\.]+px$/.test(a)}var c,d;return c=a.style.width,d=a.style.height,c||d?b(c)&&b(d)?{w:parseInt(c,10),h:parseInt(d,10)}:null:(c=a.width,d=a.height,c&&d?{w:parseInt(c,10),h:parseInt(d,10)}:null)}function b(a,b){var c,d;b&&(c=a.style.width,d=a.style.height,(c||d)&&(a.style.width=b.w+"px",a.style.height=b.h+"px",a.removeAttribute("data-mce-style")),c=a.width,d=a.height,(c||d)&&(a.setAttribute("width",b.w),a.setAttribute("height",b.h)))}function c(a){return{w:a.naturalWidth,h:a.naturalHeight}}return{getImageSize:a,setImageSize:b,getNaturalImageSize:c}}),h("z",Array),h("10",Error),g("v",["z","10"],function(a,b){var c=function(){},d=function(a,b){return function(){return a(b.apply(null,arguments))}},e=function(a){return function(){return a}},f=function(a){return a},g=function(a,b){return a===b},h=function(b){for(var c=new a(arguments.length-1),d=1;d-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e=300?c.handleHttpError(b.status):a.resolve(b.blob)})}var f=function(a,b){var c=a.indexOf("?")===-1?"?":"&";return/[?&]apiKey=/.test(a)||!b?a:a+c+"apiKey="+encodeURIComponent(b)},g=function(b,e){return d.requestUrlAsBlob(f(b,e),{"Content-Type":"application/json;charset=UTF-8","tiny-api-key":e}).then(function(b){return b.status<200||b.status>=300?c.handleServiceErrorResponse(b.status,b.blob):a.resolve(b.blob)})},h=function(a,b){return b?g(a,b):e(a)};return{getUrl:h}}),h("j",Math),g("k",["g"],function(a){return a("tinymce.dom.DOMUtils")}),g("l",["g"],function(a){return a("tinymce.ui.Factory")}),g("m",[],function(){return function(){function a(a){var b;return b=f.splice(++g),f.push(a),{state:a,removed:b}}function b(){if(d())return f[--g]}function c(){if(e())return f[++g]}function d(){return g>0}function e(){return g!=-1&&g').appendTo(k),e.each(B,function(b){a("#"+D,k).append(''+this._super(a)}})}),g("r",["c","1","1l"],function(a,b,c){"use strict";return c.extend({Defaults:{classes:"widget btn",role:"button"},init:function(a){var b,c=this;c._super(a),a=c.settings,b=c.settings.size,c.on("click mousedown",function(a){a.preventDefault()}),c.on("touchstart",function(a){c.fire("click",a),a.preventDefault()}),a.subtype&&c.classes.add(a.subtype),b&&c.classes.add("btn-"+b),a.icon&&c.icon(a.icon)},icon:function(a){return arguments.length?(this.state.set("icon",a),this):this.state.get("icon")},repaint:function(){var a,b=this.getEl().firstChild;b&&(a=b.style,a.width=a.height="100%"),this._super()},renderHtml:function(){var a,c=this,d=c._id,e=c.classPrefix,f=c.state.get("icon"),g=c.state.get("text"),h="";return a=c.settings.image,a?(f="none","string"!=typeof a&&(a=b.getSelection?a[0]:a[1]),a=" style=\"background-image: url('"+a+"')\""):a="",g&&(c.classes.add("btn-has-text"),h=''+c.encode(g)+""),f=f?e+"ico "+e+"i-"+f:"",'
    "},bindStates:function(){function b(a){var b=d("span."+e,c.getEl());a?(b[0]||(d("button:first",c.getEl()).append(''),b=d("span."+e,c.getEl())),b.html(c.encode(a))):b.remove(),c.classes.toggle("btn-has-text",!!a)}var c=this,d=c.$,e=c.classPrefix+"txt";return c.state.on("change:text",function(a){b(a.value)}),c.state.on("change:icon",function(d){var e=d.value,f=c.classPrefix;c.settings.icon=e,e=e?f+"ico "+f+"i-"+c.settings.icon:"";var g=c.getEl().firstChild,h=g.getElementsByTagName("i")[0];e?(h&&h==g.firstChild||(h=a.createElement("i"),g.insertBefore(h,g.firstChild)),h.className=e):h&&g.removeChild(h),b(c.state.get("text"))}),c._super()}})}),h("3e",RegExp),g("q",["r","f","2y","2z","3e"],function(a,b,c,d,e){return a.extend({init:function(a){var c=this;a=b.extend({text:"Browse...",multiple:!1,accept:null},a),c._super(a),c.classes.add("browsebutton"),a.multiple&&c.classes.add("multiple")},postRender:function(){var a=this,b=c.create("input",{type:"file",id:a._id+"-browse",accept:a.settings.accept});a._super(),d(b).on("change",function(b){var c=b.target.files;a.value=function(){return c.length?a.settings.multiple?c:c[0]:null},b.preventDefault(),c.length&&a.fire("change",b)}),d(b).on("click",function(a){a.stopPropagation()}),d(a.getEl("button")).on("click",function(a){a.stopPropagation(),b.click()}),a.getEl().appendChild(b)},remove:function(){d(this.getEl("button")).off(),d(this.getEl("input")).off(),this._super()}})}),g("s",["z"],function(a){"use strict";return a.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var a=this,b=a._layout;return a.classes.add("btn-group"),a.preRender(),b.preRender(a),'
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "}})}),g("t",["c","1l"],function(a,b){"use strict";return b.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(a){var b=this;b._super(a),b.on("click mousedown",function(a){a.preventDefault()}),b.on("click",function(a){a.preventDefault(),b.disabled()||b.checked(!b.checked())}),b.checked(b.settings.checked)},checked:function(a){return arguments.length?(this.state.set("checked",a),this):this.state.get("checked")},value:function(a){return arguments.length?this.checked(a):this.checked()},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix;return'
    '+a.encode(a.state.get("text"))+"
    "},bindStates:function(){function b(a){c.classes.toggle("checked",a),c.aria("checked",a)}var c=this;return c.state.on("change:text",function(a){c.getEl("al").firstChild.data=c.translate(a.value)}),c.state.on("change:checked change:value",function(a){c.fire("change"),b(a.value)}),c.state.on("change:icon",function(b){var d=b.value,e=c.classPrefix;if("undefined"==typeof d)return c.settings.icon;c.settings.icon=d,d=d?e+"ico "+e+"i-"+c.settings.icon:"";var f=c.getEl().firstChild,g=f.getElementsByTagName("i")[0];d?(g&&g==f.firstChild||(g=a.createElement("i"),f.insertBefore(g,f.firstChild)),g.className=d):g&&f.removeChild(g)}),c.state.get("checked")&&b(!0),c._super()}})}),g("3f",["8"],function(a){return a("tinymce.util.VK")}),g("y",["c","2z","e","f","3f","2y","1l"],function(a,b,c,d,e,f,g){"use strict";return g.extend({init:function(a){var c=this;c._super(a),a=c.settings,c.classes.add("combobox"),c.subinput=!0,c.ariaTarget="inp",a.menu=a.menu||a.values,a.menu&&(a.icon="caret"),c.on("click",function(d){var e=d.target,f=c.getEl();if(b.contains(f,e)||e==f)for(;e&&e!=f;)e.id&&e.id.indexOf("-open")!=-1&&(c.fire("action"),a.menu&&(c.showMenu(),d.aria&&c.menu.items()[0].focus())),e=e.parentNode}),c.on("keydown",function(a){var b;13==a.keyCode&&"INPUT"===a.target.nodeName&&(a.preventDefault(),c.parents().reverse().each(function(a){if(a.toJSON)return b=a,!1}),c.fire("submit",{data:b.toJSON()}))}),c.on("keyup",function(a){if("INPUT"==a.target.nodeName){var b=c.state.get("value"),d=a.target.value;d!==b&&(c.state.set("value",d),c.fire("autocomplete",a))}}),c.on("mouseover",function(a){var b=c.tooltip().moveTo(-65535);if(c.statusLevel()&&a.target.className.indexOf(c.classPrefix+"status")!==-1){var d=c.statusMessage()||"Ok",e=b.text(d).show().testMoveRel(a.target,["bc-tc","bc-tl","bc-tr"]);b.classes.toggle("tooltip-n","bc-tc"==e),b.classes.toggle("tooltip-nw","bc-tl"==e),b.classes.toggle("tooltip-ne","bc-tr"==e),b.moveRel(a.target,e)}})},statusLevel:function(a){return arguments.length>0&&this.state.set("statusLevel",a),this.state.get("statusLevel")},statusMessage:function(a){return arguments.length>0&&this.state.set("statusMessage",a),this.state.get("statusMessage")},showMenu:function(){var a,b=this,d=b.settings;b.menu||(a=d.menu||[],a.length?a={type:"menu",items:a}:a.type=a.type||"menu",b.menu=c.create(a).parent(b).renderTo(b.getContainerElm()),b.fire("createmenu"),b.menu.reflow(),b.menu.on("cancel",function(a){a.control===b.menu&&b.focus()}),b.menu.on("show hide",function(a){a.control.items().each(function(a){a.active(a.value()==b.value())})}).fire("show"),b.menu.on("select",function(a){b.value(a.control.value())}),b.on("focusin",function(a){"INPUT"==a.target.tagName.toUpperCase()&&b.menu.hide()}),b.aria("expanded",!0)),b.menu.show(),b.menu.layoutRect({w:b.layoutRect().w}),b.menu.moveRel(b.getEl(),b.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var c,d,e=this,g=e.getEl(),h=e.getEl("open"),i=e.layoutRect(),j=0,k=g.firstChild;e.statusLevel()&&"none"!==e.statusLevel()&&(j=parseInt(f.getRuntimeStyle(k,"padding-right"),10)-parseInt(f.getRuntimeStyle(k,"padding-left"),10)),c=h?i.w-f.getSize(h).width-10:i.w-10;var l=a;return l.all&&(!l.documentMode||l.documentMode<=8)&&(d=e.layoutRect().h-2+"px"),b(k).css({width:c-j,lineHeight:d}),e._super(),e},postRender:function(){var a=this;return b(this.getEl("inp")).on("change",function(b){a.state.set("value",b.target.value),a.fire("change",b)}),a._super()},renderHtml:function(){var a,b,c=this,d=c._id,e=c.settings,f=c.classPrefix,g=c.state.get("value")||"",h="",i="",j="";return"spellcheck"in e&&(i+=' spellcheck="'+e.spellcheck+'"'),e.maxLength&&(i+=' maxlength="'+e.maxLength+'"'),e.size&&(i+=' size="'+e.size+'"'),e.subtype&&(i+=' type="'+e.subtype+'"'),j='',c.disabled()&&(i+=' disabled="disabled"'),a=e.icon,a&&"caret"!=a&&(a=f+"ico "+f+"i-"+e.icon),b=c.state.get("text"),(a||b)&&(h='
    ",c.classes.add("has-open")),'
    '+j+h+"
    "},value:function(a){return arguments.length?(this.state.set("value",a),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(a,b){var e=this;if(0===a.length)return void e.hideMenu();var f=function(a,b){return function(){e.fire("selectitem",{title:b,value:a})}};e.menu?e.menu.items().remove():e.menu=c.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(e).renderTo(),d.each(a,function(a){e.menu.add({text:a.title,url:a.previewUrl,match:b,classes:"menu-item-ellipsis",onclick:f(a.value,a.title)})}),e.menu.renderNew(),e.hideMenu(),e.menu.on("cancel",function(a){a.control.parent()===e.menu&&(a.stopPropagation(),e.focus(),e.hideMenu())}),e.menu.on("select",function(){e.focus()});var g=e.layoutRect().w;e.menu.layoutRect({w:g,minW:0,maxW:g}),e.menu.reflow(),e.menu.show(),e.menu.moveRel(e.getEl(),e.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var a=this;a.state.on("change:value",function(b){a.getEl("inp").value!=b.value&&(a.getEl("inp").value=b.value)}),a.state.on("change:disabled",function(b){a.getEl("inp").disabled=b.value}),a.state.on("change:statusLevel",function(b){var c=a.getEl("status"),d=a.classPrefix,e=b.value;f.css(c,"display","none"===e?"none":""),f.toggleClass(c,d+"i-checkmark","ok"===e),f.toggleClass(c,d+"i-warning","warn"===e),f.toggleClass(c,d+"i-error","error"===e),a.classes.toggle("has-status","none"!==e),a.repaint()}),f.on(a.getEl("status"),"mouseleave",function(){a.tooltip().hide()}),a.on("cancel",function(b){a.menu&&a.menu.visible()&&(b.stopPropagation(),a.hideMenu())});var b=function(a,b){b&&b.items().length>0&&b.items().eq(a)[0].focus()};return a.on("keydown",function(c){var d=c.keyCode;"INPUT"===c.target.nodeName&&(d===e.DOWN?(c.preventDefault(),a.fire("autocomplete"),b(0,a.menu)):d===e.UP&&(c.preventDefault(),b(-1,a.menu)))}),a._super()},remove:function(){b(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}})}),g("v",["y"],function(a){"use strict";return a.extend({init:function(a){var b=this;a.spellcheck=!1,a.onaction&&(a.icon="none"),b._super(a),b.classes.add("colorbox"),b.on("change keyup postrender",function(){b.repaintColor(b.value())})},repaintColor:function(a){var b=this.getEl("open"),c=b?b.getElementsByTagName("i")[0]:null;if(c)try{c.style.background=a}catch(a){}},bindStates:function(){var a=this;return a.state.on("change:value",function(b){a.state.get("rendered")&&a.repaintColor(b.value)}),a._super()}})}),g("22",["r","18"],function(a,b){"use strict";return a.extend({showPanel:function(){var a=this,c=a.settings;if(a.classes.add("opened"),a.panel)a.panel.show();else{var d=c.panel;d.type&&(d={layout:"grid",items:d}),d.role=d.role||"dialog",d.popover=!0,d.autohide=!0,d.ariaRoot=!0,a.panel=new b(d).on("hide",function(){a.classes.remove("opened")}).on("cancel",function(b){b.stopPropagation(),a.focus(),a.hidePanel()}).parent(a).renderTo(a.getContainerElm()),a.panel.fire("show"),a.panel.reflow()}var e=a.panel.testMoveRel(a.getEl(),c.popoverAlign||(a.isRtl()?["bc-tc","bc-tl","bc-tr"]:["bc-tc","bc-tr","bc-tl"]));a.panel.classes.toggle("start","bc-tl"===e),a.panel.classes.toggle("end","bc-tr"===e),a.panel.moveRel(a.getEl(),e)},hidePanel:function(){var a=this;a.panel&&a.panel.hide()},postRender:function(){var a=this;return a.aria("haspopup",!0),a.on("click",function(b){b.control===a&&(a.panel&&a.panel.visible()?a.hidePanel():(a.showPanel(),a.panel.focus(!!b.aria)))}),a._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}})}),g("w",["22","d"],function(a,b){"use strict";var c=b.DOM;return a.extend({init:function(a){this._super(a),this.classes.add("splitbtn"),this.classes.add("colorbutton")},color:function(a){return a?(this._color=a,this.getEl("preview").style.backgroundColor=a,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix,d=a.state.get("text"),e=a.settings.icon?c+"ico "+c+"i-"+a.settings.icon:"",f=a.settings.image?" style=\"background-image: url('"+a.settings.image+"')\"":"",g="";return d&&(a.classes.add("btn-has-text"),g=''+a.encode(d)+""),'
    '},postRender:function(){var a=this,b=a.settings.onclick;return a.on("click",function(d){d.aria&&"down"===d.aria.key||d.control!=a||c.getParent(d.target,"."+a.classPrefix+"open")||(d.stopImmediatePropagation(),b.call(a,d))}),delete a.settings.onclick,a._super()}})}),g("3g",["8"],function(a){return a("tinymce.util.Color")}),g("x",["1l","11","2y","3g"],function(a,b,c,d){"use strict";return a.extend({Defaults:{classes:"widget colorpicker"},init:function(a){this._super(a)},postRender:function(){function a(a,b){var d,e,f=c.getPos(a);return d=b.pageX-f.x,e=b.pageY-f.y,d=Math.max(0,Math.min(d/a.clientWidth,1)),e=Math.max(0,Math.min(e/a.clientHeight,1)),{x:d,y:e}}function e(a,b){var e=(360-a.h)/360;c.css(j,{top:100*e+"%"}),b||c.css(l,{left:a.s+"%",top:100-a.v+"%"}),k.style.background=new d({s:100,v:100,h:a.h}).toHex(),m.color().parse({s:a.s,v:a.v,h:a.h})}function f(b){var c;c=a(k,b),h.s=100*c.x,h.v=100*(1-c.y),e(h),m.fire("change")}function g(b){var c;c=a(i,b),h=n.toHsv(),h.h=360*(1-c.y),e(h,!0),m.fire("change")}var h,i,j,k,l,m=this,n=m.color();i=m.getEl("h"),j=m.getEl("hp"),k=m.getEl("sv"),l=m.getEl("svp"),m._repaint=function(){h=n.toHsv(),e(h)},m._super(),m._svdraghelper=new b(m._id+"-sv",{start:f,drag:f}),m._hdraghelper=new b(m._id+"-h",{start:g,drag:g}),m._repaint()},rgb:function(){return this.color().toRgb()},value:function(a){var b=this;return arguments.length?(b.color().parse(a),void(b._rendered&&b._repaint())):b.color().toHex()},color:function(){return this._color||(this._color=new d),this._color},renderHtml:function(){function a(){var a,b,c,d,g="";for(c="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",d=f.split(","),a=0,b=d.length-1;a';return g}var b,c=this,d=c._id,e=c.classPrefix,f="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000",g="background: -ms-linear-gradient(top,"+f+");background: linear-gradient(to bottom,"+f+");";return b='
    '+a()+'
    ','
    '+b+"
    "}})}),g("12",["1l","f","2y","3e"],function(a,b,c,d){return a.extend({init:function(a){var c=this;a=b.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},a),c._super(a),c.classes.add("dropzone"),a.multiple&&c.classes.add("multiple")},renderHtml:function(){var a,b,d=this,e=d.settings;return a={id:d._id,hidefocus:"1"},b=c.create("div",a,""+this.translate(e.text)+""),e.height&&c.css(b,"height",e.height+"px"),e.width&&c.css(b,"width",e.width+"px"),b.className=d.classes,b.outerHTML},postRender:function(){var a=this,c=function(b){b.preventDefault(),a.classes.toggle("dragenter"),a.getEl().className=a.classes},e=function(c){var e=a.settings.accept;if("string"!=typeof e)return c;var f=new d("("+e.split(/\s*,\s*/).join("|")+")$","i");return b.grep(c,function(a){return f.test(a.name)})};a._super(),a.$el.on("dragover",function(a){a.preventDefault()}),a.$el.on("dragenter",c),a.$el.on("dragleave",c),a.$el.on("drop",function(b){if(b.preventDefault(),!a.state.get("disabled")){var c=e(b.dataTransfer.files);a.value=function(){return c.length?a.settings.multiple?c:c[0]:null},c.length&&a.fire("change",b)}})},remove:function(){this.$el.off(),this._super()}})}),g("23",["1l"],function(a){"use strict";return a.extend({init:function(a){var b=this;a.delimiter||(a.delimiter="\xbb"),b._super(a),b.classes.add("path"),b.canFocus=!0,b.on("click",function(a){var c,d=a.target;(c=d.getAttribute("data-index"))&&b.fire("select",{value:b.row()[c],index:c})}),b.row(b.settings.row)},focus:function(){var a=this;return a.getEl().firstChild.focus(),a},row:function(a){return arguments.length?(this.state.set("row",a),this):this.state.get("row")},renderHtml:function(){var a=this;return'
    '+a._getDataPathHtml(a.state.get("row"))+"
    "},bindStates:function(){var a=this;return a.state.on("change:row",function(b){a.innerHtml(a._getDataPathHtml(b.value))}),a._super()},_getDataPathHtml:function(a){var b,c,d=this,e=a||[],f="",g=d.classPrefix;for(b=0,c=e.length;b0?'":"")+'
    '+e[b].name+"
    ";return f||(f='
    \xa0
    '),f}})}),g("13",["23"],function(a){return a.extend({postRender:function(){function a(a){if(1===a.nodeType){if("BR"==a.nodeName||a.getAttribute("data-mce-bogus"))return!0;if("bookmark"===a.getAttribute("data-mce-type"))return!0}return!1}var b=this,c=b.settings.editor;return c.settings.elementpath!==!1&&(b.on("select",function(a){c.focus(),c.selection.select(this.row()[a.index].element),c.nodeChanged()}),c.on("nodeChange",function(d){for(var e=[],f=d.parents,g=f.length;g--;)if(1==f[g].nodeType&&!a(f[g])){var h=c.fire("ResolveName",{name:f[g].nodeName.toLowerCase(),target:f[g]});if(h.isDefaultPrevented()||e.push({name:h.name,element:f[g]}),h.isPropagationStopped())break}b.row(e)})),b._super()}})}),g("1m",["z"],function(a){"use strict";return a.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var a=this,b=a._layout,c=a.classPrefix;return a.classes.add("formitem"),b.preRender(a),'
    '+(a.settings.title?'
    '+a.settings.title+"
    ":"")+'
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "}})}),g("1a",["z","1m","f"],function(a,b,c){"use strict";return a.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:15,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var a=this,d=a.items();a.settings.formItemDefaults||(a.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),d.each(function(d){var e,f=d.settings.label;f&&(e=new b(c.extend({items:{type:"label",id:d._id+"-l",text:f,flex:0,forId:d._id,disabled:d.disabled()}},a.settings.formItemDefaults)),e.type="formitem",d.aria("labelledby",d._id+"-l"),"undefined"==typeof d.settings.flex&&(d.settings.flex=1),a.replace(d,e),e.add(d))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){var a=this;a._super(),a.fromJSON(a.settings.data)},bindStates:function(){function a(){var a,c,d,e=0,f=[];if(b.settings.labelGapCalc!==!1)for(d="children"==b.settings.labelGapCalc?b.find("formitem"):b.items(),d.filter("formitem").each(function(a){var b=a.items()[0],c=b.getEl().clientWidth;e=c>e?c:e,f.push(b)}),c=b.settings.labelGap||0,a=f.length;a--;)f[a].settings.minWidth=e+c}var b=this;b._super(),b.on("show",a),a()}})}),g("14",["1a"],function(a){"use strict";return a.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var a=this,b=a._layout,c=a.classPrefix;return a.preRender(),b.preRender(a),'
    '+(a.settings.title?''+a.settings.title+"":"")+'
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "}})}),h("42",Date),h("43",Math),g("3v",["42","43","2w"],function(a,b,c){var d=0,e=function(e){var f=new a,g=f.getTime(),h=b.floor(1e9*b.random());return d++,e+"_"+h+d+c(g)};return{generate:e}}),g("3i",[],function(){return"undefined"==typeof console&&(console={log:function(){}}),console}),g("1d",["1c","2v","3i","c"],function(a,b,c,d){var e=function(a,b){var e=b||d,f=e.createElement("div");if(f.innerHTML=a,!f.hasChildNodes()||f.childNodes.length>1)throw c.error("HTML does not have a single root node",a),"HTML must have a single root node";return h(f.childNodes[0])},f=function(a,b){var c=b||d,e=c.createElement(a);return h(e)},g=function(a,b){var c=b||d,e=c.createTextNode(a);return h(e)},h=function(c){if(null===c||void 0===c)throw new b("Node cannot be null or undefined");return{dom:a.constant(c)}};return{fromHtml:e,fromTag:f,fromText:g,fromDom:h}}),g("45",[],function(){var a=function(a){var b,c=!1;return function(){return c||(c=!0,b=a.apply(null,arguments)),b}};return{cached:a}}),g("40",[],function(){return{ATTRIBUTE:2,CDATA_SECTION:4,COMMENT:8,DOCUMENT:9,DOCUMENT_TYPE:10,DOCUMENT_FRAGMENT:11,ELEMENT:1,TEXT:3,PROCESSING_INSTRUCTION:7,ENTITY_REFERENCE:5,ENTITY:6,NOTATION:12}}),g("3n",["40"],function(a){var b=function(a){var b=a.dom().nodeName;return b.toLowerCase()},c=function(a){return a.dom().nodeType},d=function(a){return a.dom().nodeValue},e=function(a){return function(b){return c(b)===a}},f=function(d){return c(d)===a.COMMENT||"#comment"===b(d)},g=e(a.ELEMENT),h=e(a.TEXT),i=e(a.DOCUMENT);return{name:b,type:c,value:d,isElement:g,isText:h,isDocument:i,isComment:f}}),g("3y",["45","1d","3n","c"],function(a,b,c,d){var e=function(a){var b=c.isText(a)?a.dom().parentNode:a.dom();return void 0!==b&&null!==b&&b.ownerDocument.body.contains(b)},f=a.cached(function(){return g(b.fromDom(d))}),g=function(a){var c=a.dom().body;if(null===c||void 0===c)throw"Body is not available yet";return b.fromDom(c)};return{body:f,getBody:g,inBody:e}}),g("3x",["2u","2w"],function(a,b){var c=function(c){if(null===c)return"null";var d=typeof c;return"object"===d&&a.prototype.isPrototypeOf(c)?"array":"object"===d&&b.prototype.isPrototypeOf(c)?"string":d},d=function(a){return function(b){return c(b)===a}};return{isString:d("string"),isObject:d("object"),isArray:d("array"),isNull:d("null"),isBoolean:d("boolean"),isUndefined:d("undefined"),isFunction:d("function"),isNumber:d("number")}}),g("4j",["1b","1c","2u","2v"],function(a,b,c,d){return function(){var e=arguments;return function(){for(var f=new c(arguments.length),g=0;g0&&e.unsuppMessage(m);var n={};return a.each(h,function(a){n[a]=b.constant(f[a]); +}),a.each(i,function(a){n[a]=b.constant(g.prototype.hasOwnProperty.call(f,a)?d.some(f[a]):d.none())}),n}}}),g("4c",["4j","4k"],function(a,b){return{immutable:a,immutableBag:b}}),g("4d",[],function(){var a=function(a,b){var c=[],d=function(a){return c.push(a),b(a)},e=b(a);do e=e.bind(d);while(e.isSome());return c};return{toArray:a}}),g("46",["3u"],function(a){var b=function(){var b=a.getOrDie("Node");return b},c=function(a,b,c){return 0!==(a.compareDocumentPosition(b)&c)},d=function(a,d){return c(a,d,b().DOCUMENT_POSITION_PRECEDING)},e=function(a,d){return c(a,d,b().DOCUMENT_POSITION_CONTAINED_BY)};return{documentPositionPreceding:d,documentPositionContainedBy:e}}),h("4p",Number),g("4l",["1b","4p","2w"],function(a,b,c){var d=function(a,b){for(var c=0;c0&&b0},C=function(b){var c=A(b);return a.filter(y(c).concat(z(c)),B)};return{find:C}}),g("15",["1b","1c","1","3h","1f","y","f"],function(a,b,c,d,e,f,g){"use strict";var h=function(){return c.tinymce?c.tinymce.activeEditor:e.activeEditor},i={},j=5,k=function(){i={}},l=function(a){return{title:a.title,value:{title:{raw:a.title},url:a.url,attach:a.attach}}},m=function(a){return g.map(a,l)},n=function(a,c){return{title:a,value:{title:a,url:c,attach:b.noop}}},o=function(b,c){var d=a.exists(c,function(a){return a.url===b});return!d},p=function(a,b,c){var d=b in a?a[b]:c;return d===!1?null:d},q=function(c,d,e,f){var h={title:"-"},j=function(c){var f=c.hasOwnProperty(e)?c[e]:[],h=a.filter(f,function(a){return o(a,d)});return g.map(h,function(a){return{title:a,value:{title:a,url:a,attach:b.noop}}})},k=function(b){var c=a.filter(d,function(a){return a.type===b});return m(c)},l=function(){var a=k("anchor"),b=p(f,"anchor_top","#top"),c=p(f,"anchor_bottom","#bottom");return null!==b&&a.unshift(n("",b)),null!==c&&a.push(n("",c)),a},q=function(b){return a.foldl(b,function(a,b){var c=0===a.length||0===b.length;return c?a.concat(b):a.concat(h,b)},[])};return f.typeahead_urls===!1?[]:"file"===e?q([s(c,j(i)),s(c,k("header")),s(c,l())]):s(c,j(i))},r=function(b,c){var d=i[c];/^https?/.test(b)&&(d?a.indexOf(d,b)===-1&&(i[c]=d.slice(0,j).concat(b)):i[c]=[b])},s=function(a,b){var c=a.toLowerCase(),d=g.grep(b,function(a){return a.title.toLowerCase().indexOf(c)!==-1});return 1===d.length&&d[0].title===a?[]:d},t=function(a){var b=a.title;return b.raw?b.raw:b},u=function(a,b,c,e){var f=function(f){var g=d.find(c),h=q(f,g,e,b);a.showAutoComplete(h,f)};a.on("autocomplete",function(){f(a.value())}),a.on("selectitem",function(b){var c=b.value;a.value(c.url);var d=t(c);"image"===e?a.fire("change",{meta:{alt:d,attach:c.attach}}):a.fire("change",{meta:{text:d,attach:c.attach}}),a.focus()}),a.on("click",function(b){0===a.value().length&&"INPUT"===b.target.nodeName&&f("")}),a.on("PostRender",function(){a.getRoot().on("submit",function(b){b.isDefaultPrevented()||r(a.value(),e)})})},v=function(a){var b=a.status,c=a.message;return"valid"===b?{status:"ok",message:c}:"unknown"===b?{status:"warn",message:c}:"invalid"===b?{status:"warn",message:c}:{status:"none",message:""}},w=function(a,b,c){var d=b.filepicker_validator_handler;if(d){var e=function(b){return 0===b.length?void a.statusLevel("none"):void d({url:b,type:c},function(b){var c=v(b);a.statusMessage(c.message),a.statusLevel(c.status)})};a.state.on("change:value",function(a){e(a.value)})}};return f.extend({Statics:{clearHistory:k},init:function(a){var b,d,e,f=this,i=h(),j=i.settings,k=a.filetype;a.spellcheck=!1,e=j.file_picker_types||j.file_browser_callback_types,e&&(e=g.makeMap(e,/[, ]/)),e&&!e[k]||(d=j.file_picker_callback,!d||e&&!e[k]?(d=j.file_browser_callback,!d||e&&!e[k]||(b=function(){d(f.getEl("inp").id,f.value(),k,c)})):b=function(){var a=f.fire("beforecall").meta;a=g.extend({filetype:k},a),d.call(i,function(a,b){f.value(a).fire("change",{meta:b})},f.value(),a)}),b&&(a.icon="browse",a.onaction=b),f._super(a),u(f,j,i.getBody(),k),w(f,j,k)}})}),g("16",["p"],function(a){"use strict";return a.extend({recalc:function(a){var b=a.layoutRect(),c=a.paddingBox;a.items().filter(":visible").each(function(a){a.layoutRect({x:c.left,y:c.top,w:b.innerW-c.right-c.left,h:b.innerH-c.top-c.bottom}),a.recalc&&a.recalc()})}})}),g("17",["p"],function(a){"use strict";return a.extend({recalc:function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N=[],O=Math.max,P=Math.min;for(d=a.items().filter(":visible"),e=a.layoutRect(),f=a.paddingBox,g=a.settings,m=a.isRtl()?g.direction||"row-reversed":g.direction,h=g.align,i=a.isRtl()?g.pack||"end":g.pack,j=g.spacing||0,"row-reversed"!=m&&"column-reverse"!=m||(d=d.set(d.toArray().reverse()),m=m.split("-")[0]),"column"==m?(z="y",x="h",y="minH",A="maxH",C="innerH",B="top",D="deltaH",E="contentH",J="left",H="w",F="x",G="innerW",I="minW",K="right",L="deltaW",M="contentW"):(z="x",x="w",y="minW",A="maxW",C="innerW",B="left",D="deltaW",E="contentW",J="top",H="h",F="y",G="innerH",I="minH",K="bottom",L="deltaH",M="contentH"),l=e[C]-f[B]-f[B],w=k=0,b=0,c=d.length;b0&&(k+=q,o[A]&&N.push(n),o.flex=q),l-=o[y],r=f[J]+o[I]+f[K],r>w&&(w=r);if(u={},l<0?u[y]=e[y]-l+e[D]:u[y]=e[C]-l+e[D],u[I]=w+e[L],u[E]=e[C]-l,u[M]=w,u.minW=P(u.minW,e.maxW),u.minH=P(u.minH,e.maxH),u.minW=O(u.minW,e.startMinWidth),u.minH=O(u.minH,e.startMinHeight),!e.autoResize||u.minW==e.minW&&u.minH==e.minH){for(t=l/k,b=0,c=N.length;bs?(l-=o[A]-o[y],k-=o.flex,o.flex=0,o.maxFlexSize=s):o.maxFlexSize=0;for(t=l/k,v=f[B],u={},0===k&&("end"==i?v=l+f[B]:"center"==i?(v=Math.round(e[C]/2-(e[C]-l)/2)+f[B],v<0&&(v=f[B])):"justify"==i&&(v=f[B],j=Math.floor(l/(d.length-1)))),u[F]=f[J],b=0,c=d.length;b0&&(r+=o.flex*t),u[x]=r,u[z]=v,n.layoutRect(u),n.recalc&&n.recalc(),v+=r+j}else if(u.w=u.minW,u.h=u.minH,a.layoutRect(u),this.recalc(a),null===a._lastRect){var Q=a.parent();Q&&(Q._lastRect=null,Q.recalc())}}})}),g("19",["1s"],function(a){return a.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(a){a.items().filter(":visible").each(function(a){a.recalc&&a.recalc()})},isNative:function(){return!0}})}),g("3l",["3x","2t"],function(a,b){return function(c,d,e,f,g){return c(e,f)?b.some(e):a.isFunction(g)&&g(e)?b.none():d(e,f,g)}}),g("3j",["3x","1b","1c","2t","3y","3z","1d","3l"],function(a,b,c,d,e,f,g,h){var i=function(a){return n(e.body(),a)},j=function(b,e,f){for(var h=b.dom(),i=a.isFunction(f)?f:c.constant(!1);h.parentNode;){h=h.parentNode;var j=g.fromDom(h);if(e(j))return d.some(j);if(i(j))break}return d.none()},k=function(a,b,c){var d=function(a){return b(a)};return h(d,j,a,b,c)},l=function(a,b){var c=a.dom();return c.parentNode?m(g.fromDom(c.parentNode),function(c){return!f.eq(a,c)&&b(c)}):d.none()},m=function(a,d){var e=b.find(a.dom().childNodes,c.compose(d,g.fromDom));return e.map(g.fromDom)},n=function(a,b){var c=function(a){for(var e=0;e0),!b.menu&&b.settings.menu&&b.visible(k(b.settings.menu)>0);var d=b.settings.format;d&&b.visible(a.formatter.canApply(d)),b.visible()||c--}),c}var m;m=e(),t({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"]},function(b,c){a.addButton(c,{tooltip:b[0],cmd:b[1]})}),t({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"]},function(b,c){a.addButton(c,{tooltip:b[0],cmd:b[1],onPostRender:f(c)})});var p=function(a){var b=a;return b.length>0&&"-"===b[0].text&&(b=b.slice(1)),b.length>0&&"-"===b[b.length-1].text&&(b=b.slice(0,b.length-1)),b},q=function(b){var c,d;if("string"==typeof b)d=b.split(" ");else if(i.isArray(b))return u(i.map(b,q));return c=i.grep(d,function(b){return"|"===b||b in a.menuItems}),i.map(c,function(b){return"|"===b?{text:"-"}:a.menuItems[b]})},r=function(b){var c=[{text:"-"}],d=i.grep(a.menuItems,function(a){return a.context===b});return i.each(d,function(a){"before"==a.separator&&c.push({text:"|"}),a.prependToContext?c.unshift(a):c.push(a),"after"==a.separator&&c.push({text:"|"})}),c},s=function(a){return p(a.insert_button_items?q(a.insert_button_items):r("insert"))};a.addButton("undo",{tooltip:"Undo",onPostRender:g("undo"),cmd:"undo"}),a.addButton("redo",{tooltip:"Redo",onPostRender:g("redo"),cmd:"redo"}),a.addMenuItem("newdocument",{text:"New document",icon:"newdocument",cmd:"mceNewDocument"}),a.addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:g("undo"),cmd:"undo"}),a.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:g("redo"),cmd:"redo"}),a.addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:h,cmd:"mceToggleVisualAid"}),a.addButton("remove",{tooltip:"Remove",icon:"remove",cmd:"Delete"}),a.addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(s(a.settings)),this.menu.renderNew()}}),t({cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"]},function(b,c){a.addMenuItem(c,{text:b[0],icon:c,shortcut:b[2],cmd:b[1]})}),a.on("mousedown",function(){n.hideAll()}),a.addButton("styleselect",{type:"menubutton",text:"Formats",menu:m,onShowMenu:function(){a.settings.style_formats_autohide&&l(this.menu)}}),a.addButton("fontselect",function(){var c="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",e=[],f=d(a.settings.font_formats||c);return t(f,function(a){e.push({text:{raw:a[0]},value:a[1],textStyle:a[1].indexOf("dings")==-1?"font-family:"+a[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:b(e),onselect:function(b){b.control.settings.value&&a.execCommand("FontName",!1,b.control.settings.value)}}}),a.addButton("fontsizeselect",function(){var b=[],d="8pt 10pt 12pt 14pt 18pt 24pt 36pt",e=a.settings.fontsize_formats||d;return t(e.split(" "),function(a){var c=a,d=a,e=a.split("=");e.length>1&&(c=e[0],d=e[1]),b.push({text:c,value:d})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:b,fixedWidth:!0,onPostRender:c(b),onclick:function(b){b.control.settings.value&&a.execCommand("FontSize",!1,b.control.settings.value)}}}),a.addMenuItem("formats",{text:"Formats",menu:m})}var t=i.each,u=function(b){return a.foldl(b,function(a,b){return a.concat(b)},[])};j.translate=function(a){return g.translate(a)},p.tooltips=!h.iOS;var v=function(a){r(a),s(a),q(a),l.register(a),k.register(a),m.register(a)};return{setup:v}}),g("1n",["p"],function(a){"use strict";return a.extend({recalc:function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E=[],F=[];b=a.settings,e=a.items().filter(":visible"),f=a.layoutRect(),d=b.columns||Math.ceil(Math.sqrt(e.length)),c=Math.ceil(e.length/d),s=b.spacingH||b.spacing||0,t=b.spacingV||b.spacing||0,u=b.alignH||b.align,v=b.alignV||b.align,q=a.paddingBox,C="reverseRows"in b?b.reverseRows:a.isRtl(),u&&"string"==typeof u&&(u=[u]),v&&"string"==typeof v&&(v=[v]);for(l=0;lE[l]?y:E[l],F[m]=z>F[m]?z:F[m];for(A=f.innerW-q.left-q.right,w=0,l=0;l0?s:0),A-=(l>0?s:0)+E[l];for(B=f.innerH-q.top-q.bottom,x=0,m=0;m0?t:0),B-=(m>0?t:0)+F[m];if(w+=q.left+q.right,x+=q.top+q.bottom,i={},i.minW=w+(f.w-f.innerW),i.minH=x+(f.h-f.innerH),i.contentW=i.minW-f.deltaW,i.contentH=i.minH-f.deltaH,i.minW=Math.min(i.minW,f.maxW),i.minH=Math.min(i.minH,f.maxH),i.minW=Math.max(i.minW,f.startMinWidth),i.minH=Math.max(i.minH,f.startMinHeight),!f.autoResize||i.minW==f.minW&&i.minH==f.minH){f.autoResize&&(i=a.layoutRect(i),i.contentW=i.minW-f.deltaW, +i.contentH=i.minH-f.deltaH);var G;G="start"==b.packV?0:B>0?Math.floor(B/c):0;var H=0,I=b.flexWidths;if(I)for(l=0;l'},src:function(a){this.getEl().src=a},html:function(a,c){var d=this,e=this.getEl().contentWindow.document.body;return e?(e.innerHTML=a,c&&c()):b.setTimeout(function(){d.html(a)}),this}})}),g("1p",["1l"],function(a){"use strict";return a.extend({init:function(a){var b=this;b._super(a),b.classes.add("widget").add("infobox"),b.canFocus=!1},severity:function(a){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(a)},help:function(a){this.state.set("help",a)},renderHtml:function(){var a=this,b=a.classPrefix;return'
    '+a.encode(a.state.get("text"))+'
    '},bindStates:function(){var a=this;return a.state.on("change:text",function(b){a.getEl("body").firstChild.data=a.encode(b.value),a.state.get("rendered")&&a.updateLayoutRect()}),a.state.on("change:help",function(b){a.classes.toggle("has-help",b.value),a.state.get("rendered")&&a.updateLayoutRect()}),a._super()}})}),g("1r",["1l","2y"],function(a,b){"use strict";return a.extend({init:function(a){var b=this;b._super(a),b.classes.add("widget").add("label"),b.canFocus=!1,a.multiline&&b.classes.add("autoscroll"),a.strong&&b.classes.add("strong")},initLayoutRect:function(){var a=this,c=a._super();if(a.settings.multiline){var d=b.getSize(a.getEl());d.width>c.maxW&&(c.minW=c.maxW,a.classes.add("multiline")),a.getEl().style.width=c.minW+"px",c.startMinH=c.h=c.minH=Math.min(c.maxH,b.getSize(a.getEl()).height)}return c},repaint:function(){var a=this;return a.settings.multiline||(a.getEl().style.lineHeight=a.layoutRect().h+"px"),a._super()},severity:function(a){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(a)},renderHtml:function(){var a,b,c=this,d=c.settings.forId,e=c.settings.html?c.settings.html:c.encode(c.state.get("text"));return!d&&(b=c.settings.forName)&&(a=c.getRoot().find("#"+b)[0],a&&(d=a._id)),d?'":''+e+""},bindStates:function(){var a=this;return a.state.on("change:text",function(b){a.innerHtml(a.encode(b.value)),a.state.get("rendered")&&a.updateLayoutRect()}),a._super()}})}),g("2j",["z"],function(a){"use strict";return a.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(a){var b=this;b._super(a),b.classes.add("toolbar")},postRender:function(){var a=this;return a.items().each(function(a){a.classes.add("toolbar-item")}),a._super()}})}),g("1v",["2j"],function(a){"use strict";return a.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}})}),g("1w",["1","e","r","1v"],function(a,b,c,d){"use strict";function e(a,b){for(;a;){if(b===a)return!0;a=a.parentNode}return!1}var f=c.extend({init:function(a){var b=this;b._renderOpen=!0,b._super(a),a=b.settings,b.classes.add("menubtn"),a.fixedWidth&&b.classes.add("fixed-width"),b.aria("haspopup",!0),b.state.set("menu",a.menu||b.render())},showMenu:function(a){var c,d=this;return d.menu&&d.menu.visible()&&a!==!1?d.hideMenu():(d.menu||(c=d.state.get("menu")||[],d.classes.add("opened"),c.length?c={type:"menu",animate:!0,items:c}:(c.type=c.type||"menu",c.animate=!0),c.renderTo?d.menu=c.parent(d).show().renderTo():d.menu=b.create(c).parent(d).renderTo(),d.fire("createmenu"),d.menu.reflow(),d.menu.on("cancel",function(a){a.control.parent()===d.menu&&(a.stopPropagation(),d.focus(),d.hideMenu())}),d.menu.on("select",function(){d.focus()}),d.menu.on("show hide",function(a){a.control===d.menu&&(d.activeMenu("show"==a.type),d.classes.toggle("opened","show"==a.type)),d.aria("expanded","show"==a.type)}).fire("show")),d.menu.show(),d.menu.layoutRect({w:d.layoutRect().w}),d.menu.moveRel(d.getEl(),d.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]),void d.fire("showmenu"))},hideMenu:function(){var a=this;a.menu&&(a.menu.items().each(function(a){a.hideMenu&&a.hideMenu()}),a.menu.hide())},activeMenu:function(a){this.classes.toggle("active",a)},renderHtml:function(){var b,c=this,e=c._id,f=c.classPrefix,g=c.settings.icon,h=c.state.get("text"),i="";return b=c.settings.image,b?(g="none","string"!=typeof b&&(b=a.getSelection?b[0]:b[1]),b=" style=\"background-image: url('"+b+"')\""):b="",h&&(c.classes.add("btn-has-text"),i=''+c.encode(h)+""),g=c.settings.icon?f+"ico "+f+"i-"+g:"",c.aria("role",c.parent()instanceof d?"menuitem":"button"),'
    '},postRender:function(){var a=this;return a.on("click",function(b){b.control===a&&e(b.target,a.getEl())&&(a.focus(),a.showMenu(!b.aria),b.aria&&a.menu.items().filter(":visible")[0].focus())}),a.on("mouseenter",function(b){var c,d=b.control,e=a.parent();d&&e&&d instanceof f&&d.parent()==e&&(e.items().filter("MenuButton").each(function(a){a.hideMenu&&a!=d&&(a.menu&&a.menu.visible()&&(c=!0),a.hideMenu())}),c&&(d.focus(),d.showMenu()))}),a._super()},bindStates:function(){var a=this;return a.state.on("change:menu",function(){a.menu&&a.menu.remove(),a.menu=null}),a._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}});return f}),g("1x",["1l","e","1g","2m"],function(a,b,c,d){"use strict";return a.extend({Defaults:{border:0,role:"menuitem"},init:function(a){var b,c=this;c._super(a),a=c.settings,c.classes.add("menu-item"),a.menu&&c.classes.add("menu-item-expand"),a.preview&&c.classes.add("menu-item-preview"),b=c.state.get("text"),"-"!==b&&"|"!==b||(c.classes.add("menu-item-sep"),c.aria("role","separator"),c.state.set("text","-")),a.selectable&&(c.aria("role","menuitemcheckbox"),c.classes.add("menu-item-checkbox"),a.icon="selected"),a.preview||a.selectable||c.classes.add("menu-item-normal"),c.on("mousedown",function(a){a.preventDefault()}),a.menu&&!a.ariaHideMenu&&c.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var a,c=this,d=c.settings,e=c.parent();if(e.items().each(function(a){a!==c&&a.hideMenu()}),d.menu){a=c.menu,a?a.show():(a=d.menu,a.length?a={type:"menu",animate:!0,items:a}:(a.type=a.type||"menu",a.animate=!0),e.settings.itemDefaults&&(a.itemDefaults=e.settings.itemDefaults),a=c.menu=b.create(a).parent(c).renderTo(),a.reflow(),a.on("cancel",function(b){b.stopPropagation(),c.focus(),a.hide()}),a.on("show hide",function(a){a.control.items&&a.control.items().each(function(a){a.active(a.settings.selected)})}).fire("show"),a.on("hide",function(b){b.control===a&&c.classes.remove("selected")}),a.submenu=!0),a._parentMenu=e,a.classes.add("menu-sub");var f=a.testMoveRel(c.getEl(),c.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);a.moveRel(c.getEl(),f),a.rel=f,f="menu-sub-"+f,a.classes.remove(a._lastRel).add(f),a._lastRel=f,c.classes.add("selected"),c.aria("expanded",!0)}},hideMenu:function(){var a=this;return a.menu&&(a.menu.items().each(function(a){a.hideMenu&&a.hideMenu()}),a.menu.hide(),a.aria("expanded",!1)),a},renderHtml:function(){function a(a){var b,d,e={};for(e=c.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},a=a.split("+"),b=0;b").replace(new RegExp(b("]mce~match!"),"g"),"")}var f=this,g=f._id,h=f.settings,i=f.classPrefix,j=f.state.get("text"),k=f.settings.icon,l="",m=h.shortcut,n=f.encode(h.url),o="";return k&&f.parent().classes.add("menu-has-icons"),h.image&&(l=" style=\"background-image: url('"+h.image+"')\""),m&&(m=a(m)),k=i+"ico "+i+"i-"+(f.settings.icon||"none"),o="-"!==j?'\xa0":"",j=e(f.encode(d(j))),n=e(f.encode(d(n))),'
    '+o+("-"!==j?''+j+"":"")+(m?'
    '+m+"
    ":"")+(h.menu?'
    ':"")+(n?'":"")+"
    "},postRender:function(){var a=this,b=a.settings,c=b.textStyle;if("function"==typeof c&&(c=c.call(this)),c){var e=a.getEl("text");e&&e.setAttribute("style",c)}return a.on("mouseenter click",function(c){c.control===a&&(b.menu||"click"!==c.type?(a.showMenu(),c.aria&&a.menu.focus(!0)):(a.fire("select"),d.requestAnimationFrame(function(){a.parent().hideAll()})))}),a._super(),a},hover:function(){var a=this;return a.parent().items().each(function(a){a.classes.remove("selected")}),a.classes.toggle("selected",!0),a},active:function(a){return"undefined"!=typeof a&&this.aria("checked",a),this._super(a)},remove:function(){this._super(),this.menu&&this.menu.remove()}})}),g("2i",["2z","10","2m"],function(a,b,c){"use strict";return function(d,e){var f,g,h=this,i=b.classPrefix;h.show=function(b,j){function k(){f&&(a(d).append('
    '),j&&j())}return h.hide(),f=!0,b?g=c.setTimeout(k,b):k(),h},h.hide=function(){var a=d.lastChild;return c.clearTimeout(g),a&&a.className.indexOf("throbber")!=-1&&a.parentNode.removeChild(a),f=!1,h}}}),g("1u",["1g","2m","f","18","1x","2i"],function(a,b,c,d,e,f){"use strict";return d.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(b){var d=this;if(b.autohide=!0,b.constrainToViewport=!0,"function"==typeof b.items&&(b.itemsFactory=b.items,b.items=[]),b.itemDefaults)for(var e=b.items,f=e.length;f--;)e[f]=c.extend({},b.itemDefaults,e[f]);d._super(b),d.classes.add("menu"),b.animate&&11!==a.ie&&d.classes.add("animate")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){var a=this;a.hideAll(),a.fire("select")},load:function(){function a(){d.throbber&&(d.throbber.hide(),d.throbber=null)}var b,c,d=this;c=d.settings.itemsFactory,c&&(d.throbber||(d.throbber=new f(d.getEl("body"),!0),0===d.items().length?(d.throbber.show(),d.fire("loading")):d.throbber.show(100,function(){d.items().remove(),d.fire("loading")}),d.on("hide close",a)),d.requestTime=b=(new Date).getTime(),d.settings.itemsFactory(function(c){return 0===c.length?void d.hide():void(d.requestTime===b&&(d.getEl().style.width="",d.getEl("body").style.width="",a(),d.items().remove(),d.getEl("body").innerHTML="",d.add(c),d.renderNew(),d.fire("loaded")))}))},hideAll:function(){var a=this;return this.find("menuitem").exec("hideMenu"),a._super()},preRender:function(){var a=this;return a.items().each(function(b){var c=b.settings;if(c.icon||c.image||c.selectable)return a._hasIcons=!0,!1}),a.settings.itemsFactory&&a.on("postrender",function(){a.settings.itemsFactory&&a.load()}),a.on("show hide",function(c){c.control===a&&("show"===c.type?b.setTimeout(function(){a.classes.add("in")},0):a.classes.remove("in"))}),a._super()}})}),g("1t",["1w","1u"],function(a,b){"use strict";return a.extend({init:function(a){function b(c){for(var f=0;f0&&(e=c[0].text,g.state.set("value",c[0].value)),g.state.set("menu",c)),g.state.set("text",a.text||e),g.classes.add("listbox"),g.on("select",function(b){var c=b.control;f&&(b.lastControl=f),a.multiple?c.active(!c.active()):g.value(b.control.value()),f=c})},bindStates:function(){function a(a,c){a instanceof b&&a.items().each(function(a){a.hasMenus()||a.active(a.value()===c)})}function c(a,b){var d;if(a)for(var e=0;e'},postRender:function(){var a=this;a._super(),a.resizeDragHelper=new b(this._id,{start:function(){a.fire("ResizeStart")},drag:function(b){"both"!=a.settings.direction&&(b.deltaX=0),a.fire("Resize",b)},stop:function(){a.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}})}),g("2a",["1l"],function(a){"use strict";function b(a){var b="";if(a)for(var c=0;c'+a[c]+"";return b}return a.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(a){var b=this;b._super(a),b.settings.size&&(b.size=b.settings.size),b.settings.options&&(b._options=b.settings.options),b.on("keydown",function(a){var c;13==a.keyCode&&(a.preventDefault(),b.parents().reverse().each(function(a){if(a.toJSON)return c=a,!1}),b.fire("submit",{data:c.toJSON()}))})},options:function(a){return arguments.length?(this.state.set("options",a),this):this.state.get("options")},renderHtml:function(){var a,c=this,d="";return a=b(c._options),c.size&&(d=' size = "'+c.size+'"'),'"},bindStates:function(){var a=this;return a.state.on("change:options",function(c){a.getEl().innerHTML=b(c.value)}),a._super()}})}),g("2c",["1l","11","2y"],function(a,b,c){"use strict";function d(a,b,c){return ac&&(a=c),a}function e(a,b,c){a.setAttribute("aria-"+b,c)}function f(a,b){var d,f,g,h,i,j;"v"==a.settings.orientation?(h="top",g="height",f="h"):(h="left",g="width",f="w"),j=a.getEl("handle"),d=(a.layoutRect()[f]||100)-c.getSize(j)[g],i=d*((b-a._minValue)/(a._maxValue-a._minValue))+"px",j.style[h]=i,j.style.height=a.layoutRect().h+"px",e(j,"valuenow",b),e(j,"valuetext",""+a.settings.previewFilter(b)),e(j,"valuemin",a._minValue),e(j,"valuemax",a._maxValue)}return a.extend({init:function(a){var b=this;a.previewFilter||(a.previewFilter=function(a){return Math.round(100*a)/100}),b._super(a),b.classes.add("slider"),"v"==a.orientation&&b.classes.add("vertical"),b._minValue=a.minValue||0,b._maxValue=a.maxValue||100,b._initValue=b.state.get("value")},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix;return'
    '},reset:function(){this.value(this._initValue).repaint()},postRender:function(){function a(a,b,c){return(c+a)/(b-a)}function e(a,b,c){return c*(b-a)-a}function f(b,c){function f(f){var g;g=n.value(),g=e(b,c,a(b,c,g)+.05*f),g=d(g,b,c),n.value(g),n.fire("dragstart",{value:g}),n.fire("drag",{value:g}),n.fire("dragend",{value:g})}n.on("keydown",function(a){switch(a.keyCode){case 37:case 38:f(-1);break;case 39:case 40:f(1)}})}function g(a,e,f){var g,h,i,o,p;n._dragHelper=new b(n._id,{handle:n._id+"-handle",start:function(a){g=a[j],h=parseInt(n.getEl("handle").style[k],10),i=(n.layoutRect()[m]||100)-c.getSize(f)[l],n.fire("dragstart",{value:p})},drag:function(b){var c=b[j]-g;o=d(h+c,0,i),f.style[k]=o+"px",p=a+o/i*(e-a),n.value(p),n.tooltip().text(""+n.settings.previewFilter(p)).show().moveRel(f,"bc tc"),n.fire("drag",{value:p})},stop:function(){n.tooltip().hide(),n.fire("dragend",{value:p})}})}var h,i,j,k,l,m,n=this;h=n._minValue,i=n._maxValue,"v"==n.settings.orientation?(j="screenY",k="top",l="height",m="h"):(j="screenX",k="left",l="width",m="w"),n._super(),f(h,i,n.getEl("handle")),g(h,i,n.getEl("handle"))},repaint:function(){this._super(),f(this,this.value())},bindStates:function(){var a=this;return a.state.on("change:value",function(b){f(a,b.value)}),a._super()}})}),g("2d",["1l"],function(a){"use strict";return a.extend({renderHtml:function(){var a=this;return a.classes.add("spacer"),a.canFocus=!1,'
    '}})}),g("2e",["1","2z","2y","1w"],function(a,b,c,d){return d.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var a,d,e=this,f=e.getEl(),g=e.layoutRect();return e._super(),a=f.firstChild,d=f.lastChild,b(a).css({width:g.w-c.getSize(d).width,height:g.h-2}),b(d).css({height:g.h-2}),e},activeMenu:function(a){var c=this;b(c.getEl().lastChild).toggleClass(c.classPrefix+"active",a)},renderHtml:function(){var b,c=this,d=c._id,e=c.classPrefix,f=c.state.get("icon"),g=c.state.get("text"),h="";return b=c.settings.image,b?(f="none","string"!=typeof b&&(b=a.getSelection?b[0]:b[1]),b=" style=\"background-image: url('"+b+"')\""):b="",f=c.settings.icon?e+"ico "+e+"i-"+f:"",g&&(c.classes.add("btn-has-text"),h=''+c.encode(g)+""),'
    '},postRender:function(){var a=this,b=a.settings.onclick;return a.on("click",function(a){var c=a.target;if(a.control==this)for(;c;){if(a.aria&&"down"!=a.aria.key||"BUTTON"==c.nodeName&&c.className.indexOf("open")==-1)return a.stopImmediatePropagation(),void(b&&b.call(this,a));c=c.parentNode}}),delete a.settings.onclick,a._super()}})}),g("2f",["19"],function(a){"use strict";return a.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}})}),g("2g",["21","2z","2y"],function(a,b,c){"use strict";return a.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(a){var c;this.activeTabId&&(c=this.getEl(this.activeTabId),b(c).removeClass(this.classPrefix+"active"),c.setAttribute("aria-selected","false")),this.activeTabId="t"+a,c=this.getEl("t"+a),c.setAttribute("aria-selected","true"),b(c).addClass(this.classPrefix+"active"),this.items()[a].show().fire("showtab"),this.reflow(),this.items().each(function(b,c){a!=c&&b.hide()})},renderHtml:function(){var a=this,b=a._layout,c="",d=a.classPrefix;return a.preRender(),b.preRender(a),a.items().each(function(b,e){var f=a._id+"-t"+e;b.aria("role","tabpanel"),b.aria("labelledby",f),c+='"}),'
    '+c+'
    '+b.renderHtml(a)+"
    "},postRender:function(){var a=this;a._super(),a.settings.activeTab=a.settings.activeTab||0,a.activateTab(a.settings.activeTab),this.on("click",function(b){var c=b.target.parentNode;if(c&&c.id==a._id+"-head")for(var d=c.childNodes.length;d--;)c.childNodes[d]==b.target&&a.activateTab(d)})},initLayoutRect:function(){var a,b,d,e=this;b=c.getSize(e.getEl("head")).width,b=b<0?0:b,d=0,e.items().each(function(a){b=Math.max(b,a.layoutRect().minW),d=Math.max(d,a.layoutRect().minH)}),e.items().each(function(a){a.settings.x=0,a.settings.y=0,a.settings.w=b,a.settings.h=d,a.layoutRect({x:0,y:0,w:b,h:d})});var f=c.getSize(e.getEl("head")).height;return e.settings.minWidth=b,e.settings.minHeight=d+f,a=e._super(),a.deltaH+=f,a.innerH=a.h-a.deltaH,a}})}),g("2h",["c","f","2y","1l"],function(a,b,c,d){return d.extend({init:function(a){var b=this;b._super(a),b.classes.add("textbox"),a.multiline?b.classes.add("multiline"):(b.on("keydown",function(a){var c;13==a.keyCode&&(a.preventDefault(),b.parents().reverse().each(function(a){if(a.toJSON)return c=a,!1}),b.fire("submit",{data:c.toJSON()}))}),b.on("keyup",function(a){b.state.set("value",a.target.value)}))},repaint:function(){var b,c,d,e,f,g=this,h=0;b=g.getEl().style,c=g._layoutRect,f=g._lastRepaintRect||{};var i=a;return!g.settings.multiline&&i.all&&(!i.documentMode||i.documentMode<=8)&&(b.lineHeight=c.h-h+"px"),d=g.borderBox,e=d.left+d.right+8,h=d.top+d.bottom+(g.settings.multiline?8:0),c.x!==f.x&&(b.left=c.x+"px",f.x=c.x),c.y!==f.y&&(b.top=c.y+"px",f.y=c.y),c.w!==f.w&&(b.width=c.w-e+"px",f.w=c.w),c.h!==f.h&&(b.height=c.h-h+"px",f.h=c.h),g._lastRepaintRect=f,g.fire("repaint",{},!1),g},renderHtml:function(){var a,d,e=this,f=e.settings;return a={id:e._id,hidefocus:"1"},b.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(b){a[b]=f[b]}),e.disabled()&&(a.disabled="disabled"),f.subtype&&(a.type=f.subtype),d=c.create(f.multiline?"textarea":"input",a),d.value=e.state.get("value"),d.className=e.classes,d.outerHTML},value:function(a){return arguments.length?(this.state.set("value",a),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var a=this;a.getEl().value=a.state.get("value"),a._super(),a.$el.on("change",function(b){a.state.set("value",b.target.value),a.fire("change",b)})},bindStates:function(){var a=this;return a.state.on("change:value",function(b){a.getEl().value!=b.value&&(a.getEl().value=b.value)}),a.state.on("change:disabled",function(b){a.getEl().disabled=b.value}),a._super()},remove:function(){this.$el.off(),this._super()}})}),g("6",["e","f","p","q","r","s","t","u","v","w","x","y","z","10","11","12","13","14","15","16","17","18","19","1a","7","1m","1n","1o","1p","1q","1r","1s","1t","1u","1v","1w","1x","1y","1z","20","21","22","23","24","25","26","27","28","29","2a","2b","2c","2d","2e","2f","2g","2h","2i","2j","2k","1l","2l"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca,da,ea,fa,ga,ha){var ia=function(){return{Selector:Y,Collection:h,ReflowQueue:T,Control:n,Factory:a,KeyboardNavigation:D,Container:m,DragHelper:o,Scrollable:W,Panel:O,Movable:M,Resizable:U,FloatPanel:v,Window:ha,MessageBox:L,Tooltip:fa,Widget:ga,Progress:R,Notification:N,Layout:F,AbsoluteLayout:c,Button:e,ButtonGroup:f,Checkbox:g,ComboBox:l,ColorBox:i,PanelButton:P,ColorButton:j,ColorPicker:k,Path:Q,ElementPath:q,FormItem:z,Form:x,FieldSet:r,FilePicker:s,FitLayout:t,FlexLayout:u,FlowLayout:w,FormatControls:y,GridLayout:A,Iframe:B,InfoBox:C,Label:E,Toolbar:ea,MenuBar:I,MenuButton:J,MenuItem:K,Throbber:da,Menu:H,ListBox:G,Radio:S,ResizeHandle:V,SelectBox:X,Slider:Z,Spacer:$,SplitButton:_,StackLayout:aa,TabPanel:ba,TextBox:ca,DropZone:p,BrowseButton:d}},ja=function(a){a.ui?b.each(ia(),function(b,c){a.ui[c]=b}):a.ui=ia()},ka=function(){b.each(ia(),function(b,c){a.add(c,b)})},la={appendTo:ja,registerToFactory:ka};return la}),g("0",["1","2","3","4","5","6","7"],function(a,b,c,d,e,f,g){return f.registerToFactory(),f.appendTo(a.tinymce?a.tinymce:{}),b.add("inlite",function(a){var b=new e;return g.setup(a),d.addToEditor(a,b),c.get(a,b)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/themes/mobile/index.js b/media/vendor/tinymce/themes/mobile/index.js new file mode 100644 index 0000000000000..ad02e831f6e70 --- /dev/null +++ b/media/vendor/tinymce/themes/mobile/index.js @@ -0,0 +1,7 @@ +// Exports the "mobile" theme for usage with module loaders +// Usage: +// CommonJS: +// require('tinymce/themes/mobile') +// ES2015: +// import 'tinymce/themes/mobile' +require('./theme.js'); \ No newline at end of file diff --git a/media/vendor/tinymce/themes/mobile/theme.js b/media/vendor/tinymce/themes/mobile/theme.js new file mode 100644 index 0000000000000..ef144d4a988a3 --- /dev/null +++ b/media/vendor/tinymce/themes/mobile/theme.js @@ -0,0 +1,23278 @@ +(function () { + +var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)} + +// Used when there is no 'main' module. +// The name is probably (hopefully) unique so minification removes for releases. +var register_3795 = function (id) { + var module = dem(id); + var fragments = id.split('.'); + var target = Function('return this;')(); + for (var i = 0; i < fragments.length - 1; ++i) { + if (target[fragments[i]] === undefined) + target[fragments[i]] = {}; + target = target[fragments[i]]; + } + target[fragments[fragments.length - 1]] = module; +}; + +var instantiate = function (id) { + var actual = defs[id]; + var dependencies = actual.deps; + var definition = actual.defn; + var len = dependencies.length; + var instances = new Array(len); + for (var i = 0; i < len; ++i) + instances[i] = dem(dependencies[i]); + var defResult = definition.apply(null, instances); + if (defResult === undefined) + throw 'module [' + id + '] returned undefined'; + actual.instance = defResult; +}; + +var def = function (id, dependencies, definition) { + if (typeof id !== 'string') + throw 'module id must be a string'; + else if (dependencies === undefined) + throw 'no dependencies for ' + id; + else if (definition === undefined) + throw 'no definition function for ' + id; + defs[id] = { + deps: dependencies, + defn: definition, + instance: undefined + }; +}; + +var dem = function (id) { + var actual = defs[id]; + if (actual === undefined) + throw 'module [' + id + '] was undefined'; + else if (actual.instance === undefined) + instantiate(id); + return actual.instance; +}; + +var req = function (ids, callback) { + var len = ids.length; + var instances = new Array(len); + for (var i = 0; i < len; ++i) + instances[i] = dem(ids[i]); + callback.apply(null, instances); +}; + +var ephox = {}; + +ephox.bolt = { + module: { + api: { + define: def, + require: req, + demand: dem + } + } +}; + +var define = def; +var require = req; +var demand = dem; +// this helps with minification when using a lot of global references +var defineGlobal = function (id, ref) { + define(id, [], function () { return ref; }); +}; +/*jsc +["tinymce.themes.mobile.Theme","ephox.alloy.api.behaviour.Swapping","ephox.alloy.api.events.AlloyTriggers","ephox.alloy.api.system.Attachment","ephox.alloy.debugging.Debugging","ephox.katamari.api.Cell","ephox.katamari.api.Fun","ephox.sand.api.PlatformDetection","ephox.sugar.api.dom.Focus","ephox.sugar.api.dom.Insert","ephox.sugar.api.node.Element","ephox.sugar.api.node.Node","tinymce.core.dom.DOMUtils","tinymce.core.ThemeManager","tinymce.themes.mobile.alien.TinyCodeDupe","tinymce.themes.mobile.channels.TinyChannels","tinymce.themes.mobile.features.Features","tinymce.themes.mobile.style.Styles","tinymce.themes.mobile.touch.view.Orientation","tinymce.themes.mobile.ui.AndroidRealm","tinymce.themes.mobile.ui.Buttons","tinymce.themes.mobile.ui.IosRealm","tinymce.themes.mobile.util.CssUrls","tinymce.themes.mobile.util.FormatChangers","tinymce.themes.mobile.util.SkinLoaded","ephox.alloy.api.behaviour.Behaviour","ephox.alloy.behaviour.swapping.SwapApis","ephox.alloy.behaviour.swapping.SwapSchema","ephox.alloy.api.events.SystemEvents","global!Array","global!Error","ephox.katamari.api.Merger","ephox.katamari.api.Obj","ephox.katamari.api.Arr","ephox.katamari.api.Option","ephox.sugar.api.search.Traverse","ephox.sugar.api.dom.Remove","ephox.sugar.api.node.Body","ephox.alloy.log.AlloyLogger","ephox.boulder.api.Objects","ephox.katamari.api.Options","global!console","ephox.katamari.api.Thunk","ephox.sand.core.PlatformDetection","global!navigator","ephox.sugar.api.dom.Compare","global!document","ephox.sugar.api.search.PredicateExists","ephox.sugar.api.node.NodeTypes","global!tinymce.util.Tools.resolve","ephox.alloy.api.behaviour.Receiving","ephox.alloy.api.behaviour.Toggling","ephox.alloy.api.component.Memento","ephox.katamari.api.Type","global!setTimeout","global!window","tinymce.themes.mobile.channels.Receivers","ephox.alloy.api.behaviour.Unselecting","ephox.alloy.api.ui.Button","tinymce.themes.mobile.util.UiDomFactory","tinymce.themes.mobile.ui.ColorSlider","tinymce.themes.mobile.ui.FontSizeSlider","tinymce.themes.mobile.ui.ImagePicker","tinymce.themes.mobile.ui.LinkButton","tinymce.themes.mobile.util.StyleFormats","ephox.sugar.api.events.DomEvent","global!clearInterval","global!Math","global!setInterval","ephox.alloy.api.behaviour.Replacing","ephox.katamari.api.Singleton","tinymce.themes.mobile.api.AndroidWebapp","tinymce.themes.mobile.toolbar.ScrollingToolbar","tinymce.themes.mobile.ui.CommonRealm","tinymce.themes.mobile.ui.Dropup","tinymce.themes.mobile.ui.OuterContainer","tinymce.themes.mobile.api.IosWebapp","tinymce.core.EditorManager","ephox.alloy.behaviour.common.Behaviour","ephox.alloy.behaviour.common.NoState","ephox.boulder.api.FieldSchema","ephox.boulder.combine.ResultCombine","ephox.boulder.core.ObjChanger","ephox.boulder.core.ObjReader","ephox.boulder.core.ObjWriter","ephox.boulder.api.ValueSchema","ephox.sugar.api.properties.Class","ephox.alloy.api.events.NativeEvents","ephox.sand.core.Browser","ephox.sand.core.OperatingSystem","ephox.sand.detect.DeviceType","ephox.sand.detect.UaString","ephox.sand.info.PlatformInfo","global!String","global!Object","ephox.katamari.api.Struct","ephox.sugar.alien.Recurse","ephox.sand.api.Node","ephox.sugar.api.search.Selectors","ephox.sugar.api.dom.InsertAll","ephox.alloy.alien.Truncate","ephox.sugar.api.search.PredicateFind","ephox.alloy.behaviour.receiving.ActiveReceiving","ephox.alloy.behaviour.receiving.ReceivingSchema","ephox.alloy.behaviour.toggling.ActiveToggle","ephox.alloy.behaviour.toggling.ToggleApis","ephox.alloy.behaviour.toggling.ToggleSchema","ephox.alloy.registry.Tagger","ephox.alloy.behaviour.unselecting.ActiveUnselecting","ephox.alloy.api.behaviour.Focusing","ephox.alloy.api.behaviour.Keying","ephox.alloy.api.ui.Sketcher","ephox.alloy.ui.common.ButtonBase","ephox.alloy.api.component.DomFactory","ephox.katamari.api.Strings","ephox.alloy.api.ui.Slider","ephox.sugar.api.properties.Css","tinymce.themes.mobile.ui.ToolbarWidgets","tinymce.themes.mobile.ui.SizeSlider","tinymce.themes.mobile.util.FontSizes","ephox.alloy.api.events.AlloyEvents","ephox.imagetools.api.BlobConversions","ephox.katamari.api.Id","ephox.alloy.api.behaviour.Representing","tinymce.themes.mobile.bridge.LinkBridge","tinymce.themes.mobile.ui.Inputs","tinymce.themes.mobile.ui.SerialisedDialog","tinymce.themes.mobile.util.RangePreserver","tinymce.themes.mobile.features.DefaultStyleFormats","tinymce.themes.mobile.ui.StylesMenu","tinymce.themes.mobile.util.StyleConversions","ephox.sugar.impl.FilteredEvent","ephox.alloy.behaviour.replacing.ReplaceApis","ephox.alloy.api.component.GuiFactory","tinymce.themes.mobile.android.core.AndroidMode","tinymce.themes.mobile.api.MobileSchema","tinymce.themes.mobile.touch.view.TapToEditMask","ephox.alloy.api.behaviour.AddEventsBehaviour","ephox.alloy.api.ui.Container","ephox.alloy.api.ui.Toolbar","ephox.alloy.api.ui.ToolbarGroup","tinymce.themes.mobile.ios.scroll.Scrollables","tinymce.themes.mobile.touch.scroll.Scrollable","ephox.alloy.api.behaviour.Sliding","ephox.alloy.api.system.Gui","tinymce.themes.mobile.ios.core.IosMode","ephox.alloy.alien.EventRoot","ephox.sand.detect.Version","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts","ephox.alloy.construct.EventHandler","ephox.katamari.api.Result","ephox.katamari.api.Results","ephox.alloy.debugging.FunctionAnnotator","ephox.alloy.dom.DomModification","ephox.boulder.api.FieldPresence","ephox.boulder.core.ValueProcessor","ephox.boulder.core.ChoiceProcessor","ephox.boulder.format.PrettyPrinter","ephox.alloy.behaviour.common.BehaviourState","ephox.sugar.api.properties.Toggler","ephox.sugar.api.properties.Attr","ephox.sugar.impl.ClassList","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.sand.util.Global","ephox.sugar.api.properties.Html","ephox.sugar.api.dom.Replication","ephox.sugar.impl.ClosestOrAncestor","ephox.alloy.data.Fields","ephox.alloy.behaviour.toggling.ToggleModes","ephox.alloy.ephemera.AlloyTags","global!Date","ephox.sugar.api.search.SelectorFind","ephox.alloy.behaviour.focusing.ActiveFocus","ephox.alloy.behaviour.focusing.FocusApis","ephox.alloy.behaviour.focusing.FocusSchema","ephox.alloy.behaviour.keyboard.KeyboardBranches","ephox.alloy.behaviour.keyboard.KeyingState","ephox.alloy.api.ui.GuiTypes","ephox.alloy.api.ui.UiSketcher","ephox.alloy.parts.AlloyParts","ephox.alloy.parts.PartType","ephox.alloy.ui.slider.SliderParts","ephox.alloy.ui.slider.SliderSchema","ephox.alloy.ui.slider.SliderUi","ephox.sugar.impl.Style","ephox.sugar.api.search.TransformFind","ephox.imagetools.util.Conversions","ephox.imagetools.util.ImageResult","ephox.alloy.behaviour.representing.ActiveRepresenting","ephox.alloy.behaviour.representing.RepresentApis","ephox.alloy.behaviour.representing.RepresentSchema","ephox.alloy.behaviour.representing.RepresentState","ephox.sugar.api.properties.TextContent","ephox.alloy.api.behaviour.Composing","ephox.alloy.api.ui.DataField","ephox.alloy.api.ui.Input","ephox.alloy.api.behaviour.Disabling","ephox.alloy.api.behaviour.Highlighting","ephox.alloy.api.ui.Form","ephox.sugar.api.search.SelectorFilter","ephox.sugar.api.view.Width","tinymce.themes.mobile.model.SwipingModel","ephox.alloy.api.behaviour.Transitioning","ephox.alloy.api.component.Component","ephox.alloy.api.component.ComponentApi","ephox.alloy.api.system.NoContextApi","ephox.alloy.events.DefaultEvents","ephox.alloy.spec.CustomSpec","ephox.alloy.api.ui.Menu","ephox.alloy.api.ui.TieredMenu","ephox.alloy.alien.AriaFocus","tinymce.themes.mobile.android.core.AndroidEvents","tinymce.themes.mobile.android.core.AndroidSetup","tinymce.themes.mobile.ios.core.PlatformEditor","tinymce.themes.mobile.util.Thor","tinymce.themes.mobile.touch.view.MetaViewport","ephox.katamari.api.Throttler","ephox.alloy.ui.schema.ToolbarSchema","ephox.alloy.api.behaviour.Tabstopping","ephox.alloy.ui.schema.ToolbarGroupSchema","ephox.alloy.behaviour.sliding.ActiveSliding","ephox.alloy.behaviour.sliding.SlidingApis","ephox.alloy.behaviour.sliding.SlidingSchema","ephox.alloy.behaviour.sliding.SlidingState","ephox.alloy.api.system.SystemApi","ephox.alloy.events.DescribedHandler","ephox.alloy.events.GuiEvents","ephox.alloy.events.Triggers","ephox.alloy.registry.Registry","tinymce.themes.mobile.ios.core.IosEvents","tinymce.themes.mobile.ios.core.IosSetup","tinymce.themes.mobile.ios.view.IosKeyboard","ephox.katamari.api.Resolve","global!Number","ephox.katamari.api.Adt","ephox.boulder.core.SchemaError","ephox.boulder.format.TypeTokens","ephox.sand.api.JSON","ephox.alloy.dom.DomDefinition","ephox.katamari.util.BagUtils","ephox.katamari.api.Contracts","ephox.sugar.api.properties.AttrList","ephox.sugar.api.node.Elements","ephox.alloy.menu.util.MenuMarkers","ephox.alloy.keying.CyclicType","ephox.alloy.keying.ExecutionType","ephox.alloy.keying.FlatgridType","ephox.alloy.keying.FlowType","ephox.alloy.keying.MatrixType","ephox.alloy.keying.MenuType","ephox.alloy.keying.SpecialType","ephox.alloy.parts.PartSubstitutes","ephox.alloy.spec.UiSubstitutes","ephox.alloy.spec.SpecSchema","ephox.alloy.ui.slider.SliderActions","ephox.alloy.behaviour.representing.DatasetStore","ephox.alloy.behaviour.representing.ManualStore","ephox.alloy.behaviour.representing.MemoryStore","ephox.sugar.impl.Dimension","ephox.imagetools.util.Promise","ephox.imagetools.util.Canvas","ephox.imagetools.util.Mime","ephox.imagetools.util.ImageSize","ephox.alloy.behaviour.composing.ComposeApis","ephox.alloy.behaviour.composing.ComposeSchema","ephox.alloy.ui.common.InputBase","ephox.alloy.behaviour.disabling.ActiveDisable","ephox.alloy.behaviour.disabling.DisableApis","ephox.alloy.behaviour.disabling.DisableSchema","ephox.alloy.behaviour.highlighting.HighlightApis","ephox.alloy.behaviour.highlighting.HighlightSchema","ephox.sugar.api.search.PredicateFilter","ephox.alloy.behaviour.transitioning.ActiveTransitioning","ephox.alloy.behaviour.transitioning.TransitionApis","ephox.alloy.behaviour.transitioning.TransitionSchema","ephox.alloy.api.component.CompBehaviours","ephox.alloy.behaviour.common.BehaviourBlob","ephox.alloy.construct.ComponentDom","ephox.alloy.construct.ComponentEvents","ephox.alloy.construct.CustomDefinition","ephox.alloy.dom.DomRender","ephox.alloy.ui.schema.MenuSchema","ephox.alloy.ui.single.MenuSpec","ephox.alloy.ui.single.TieredMenuSpec","tinymce.themes.mobile.util.TappingEvent","tinymce.themes.mobile.android.focus.ResumeEditing","tinymce.themes.mobile.util.DataAttributes","tinymce.themes.mobile.util.Rectangles","ephox.sugar.api.selection.WindowSelection","global!clearTimeout","ephox.alloy.behaviour.tabstopping.ActiveTabstopping","ephox.alloy.behaviour.tabstopping.TabstopSchema","ephox.sugar.api.properties.Classes","ephox.sugar.api.view.Height","ephox.alloy.alien.Keys","ephox.alloy.events.TapEvent","ephox.alloy.events.EventSource","ephox.alloy.events.SimulatedEvent","ephox.alloy.events.EventRegistry","ephox.sugar.api.view.Location","global!parseInt","tinymce.themes.mobile.ios.focus.FakeSelection","tinymce.themes.mobile.ios.scroll.IosScrolling","tinymce.themes.mobile.ios.smooth.BackgroundActivity","tinymce.themes.mobile.ios.view.Greenzone","tinymce.themes.mobile.ios.view.IosUpdates","tinymce.themes.mobile.ios.view.IosViewport","tinymce.themes.mobile.util.CaptureBin","tinymce.themes.mobile.ios.focus.ResumeEditing","ephox.katamari.api.Global","ephox.alloy.keying.KeyingType","ephox.alloy.navigation.ArrNavigation","ephox.alloy.navigation.KeyMatch","ephox.alloy.navigation.KeyRules","ephox.alloy.alien.EditableFields","ephox.alloy.keying.KeyingTypes","ephox.alloy.navigation.DomMovement","ephox.alloy.navigation.DomPinpoint","ephox.alloy.navigation.WrapArrNavigation","ephox.alloy.navigation.DomNavigation","ephox.alloy.navigation.MatrixNavigation","ephox.alloy.ui.slider.SliderModel","ephox.sugar.api.properties.Value","ephox.alloy.alien.Cycles","ephox.alloy.alien.ObjIndex","ephox.alloy.alien.PrioritySort","ephox.alloy.api.focus.FocusManagers","ephox.alloy.menu.build.ItemType","ephox.alloy.menu.build.SeparatorType","ephox.alloy.menu.build.WidgetType","ephox.alloy.menu.util.ItemEvents","ephox.alloy.menu.util.MenuEvents","ephox.alloy.menu.layered.LayeredState","ephox.alloy.alien.DelayedFunction","global!isNaN","ephox.sugar.api.selection.Awareness","ephox.sugar.api.selection.Selection","ephox.sugar.api.dom.DocumentPosition","ephox.sugar.api.node.Fragment","ephox.sugar.api.selection.Situ","ephox.sugar.selection.core.NativeRange","ephox.sugar.selection.core.SelectionDirection","ephox.sugar.selection.query.CaretRange","ephox.sugar.selection.query.Within","ephox.sugar.selection.quirks.Prefilter","ephox.alloy.alien.TransformFind","ephox.sugar.api.view.Position","ephox.sugar.api.dom.Dom","tinymce.themes.mobile.touch.focus.CursorRefresh","ephox.katamari.api.Future","tinymce.themes.mobile.ios.smooth.SmoothAnimation","tinymce.themes.mobile.ios.view.DeviceZones","ephox.katamari.api.LazyValue","ephox.katamari.api.Futures","ephox.sugar.api.properties.Direction","ephox.alloy.navigation.ArrPinpoint","ephox.sugar.api.view.Visibility","ephox.alloy.menu.build.WidgetParts","ephox.alloy.menu.layered.MenuPathing","ephox.sugar.api.node.Text","ephox.sugar.selection.query.ContainerPoint","ephox.sugar.selection.query.EdgePoint","ephox.katamari.async.Bounce","tinymce.themes.mobile.ios.view.Devices","ephox.katamari.async.AsyncValues","ephox.sugar.impl.NodeValue","ephox.sugar.selection.alien.Geometry","ephox.sugar.selection.query.TextPoint","ephox.sugar.api.selection.CursorPosition"] +jsc*/ +defineGlobal("global!Array", Array); +defineGlobal("global!Error", Error); +define( + 'ephox.katamari.api.Fun', + + [ + 'global!Array', + 'global!Error' + ], + + function (Array, Error) { + + var noop = function () { }; + + var compose = function (fa, fb) { + return function () { + return fa(fb.apply(null, arguments)); + }; + }; + + var constant = function (value) { + return function () { + return value; + }; + }; + + var identity = function (x) { + return x; + }; + + var tripleEquals = function(a, b) { + return a === b; + }; + + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var curry = function (f) { + // equivalent to arguments.slice(1) + // starting at 1 because 0 is the f, makes things tricky. + // Pay attention to what variable is where, and the -1 magic. + // thankfully, we have tests for this. + var args = new Array(arguments.length - 1); + for (var i = 1; i < arguments.length; i++) args[i-1] = arguments[i]; + + return function () { + var newArgs = new Array(arguments.length); + for (var j = 0; j < newArgs.length; j++) newArgs[j] = arguments[j]; + + var all = args.concat(newArgs); + return f.apply(null, all); + }; + }; + + var not = function (f) { + return function () { + return !f.apply(null, arguments); + }; + }; + + var die = function (msg) { + return function () { + throw new Error(msg); + }; + }; + + var apply = function (f) { + return f(); + }; + + var call = function(f) { + f(); + }; + + var never = constant(false); + var always = constant(true); + + + return { + noop: noop, + compose: compose, + constant: constant, + identity: identity, + tripleEquals: tripleEquals, + curry: curry, + not: not, + die: die, + apply: apply, + call: call, + never: never, + always: always + }; + } +); + +defineGlobal("global!Object", Object); +define( + 'ephox.katamari.api.Option', + + [ + 'ephox.katamari.api.Fun', + 'global!Object' + ], + + function (Fun, Object) { + + var never = Fun.never; + var always = Fun.always; + + /** + Option objects support the following methods: + + fold :: this Option a -> ((() -> b, a -> b)) -> Option b + + is :: this Option a -> a -> Boolean + + isSome :: this Option a -> () -> Boolean + + isNone :: this Option a -> () -> Boolean + + getOr :: this Option a -> a -> a + + getOrThunk :: this Option a -> (() -> a) -> a + + getOrDie :: this Option a -> String -> a + + or :: this Option a -> Option a -> Option a + - if some: return self + - if none: return opt + + orThunk :: this Option a -> (() -> Option a) -> Option a + - Same as "or", but uses a thunk instead of a value + + map :: this Option a -> (a -> b) -> Option b + - "fmap" operation on the Option Functor. + - same as 'each' + + ap :: this Option a -> Option (a -> b) -> Option b + - "apply" operation on the Option Apply/Applicative. + - Equivalent to <*> in Haskell/PureScript. + + each :: this Option a -> (a -> b) -> Option b + - same as 'map' + + bind :: this Option a -> (a -> Option b) -> Option b + - "bind"/"flatMap" operation on the Option Bind/Monad. + - Equivalent to >>= in Haskell/PureScript; flatMap in Scala. + + flatten :: {this Option (Option a))} -> () -> Option a + - "flatten"/"join" operation on the Option Monad. + + exists :: this Option a -> (a -> Boolean) -> Boolean + + forall :: this Option a -> (a -> Boolean) -> Boolean + + filter :: this Option a -> (a -> Boolean) -> Option a + + equals :: this Option a -> Option a -> Boolean + + equals_ :: this Option a -> (Option a, a -> Boolean) -> Boolean + + toArray :: this Option a -> () -> [a] + + */ + + var none = function () { return NONE; }; + + var NONE = (function () { + var eq = function (o) { + return o.isNone(); + }; + + // inlined from peanut, maybe a micro-optimisation? + var call = function (thunk) { return thunk(); }; + var id = function (n) { return n; }; + var noop = function () { }; + + var me = { + fold: function (n, s) { return n(); }, + is: never, + isSome: never, + isNone: always, + getOr: id, + getOrThunk: call, + getOrDie: function (msg) { + throw new Error(msg || 'error: getOrDie called on none.'); + }, + or: id, + orThunk: call, + map: none, + ap: none, + each: noop, + bind: none, + flatten: none, + exists: never, + forall: always, + filter: none, + equals: eq, + equals_: eq, + toArray: function () { return []; }, + toString: Fun.constant("none()") + }; + if (Object.freeze) Object.freeze(me); + return me; + })(); + + + /** some :: a -> Option a */ + var some = function (a) { + + // inlined from peanut, maybe a micro-optimisation? + var constant_a = function () { return a; }; + + var self = function () { + // can't Fun.constant this one + return me; + }; + + var map = function (f) { + return some(f(a)); + }; + + var bind = function (f) { + return f(a); + }; + + var me = { + fold: function (n, s) { return s(a); }, + is: function (v) { return a === v; }, + isSome: always, + isNone: never, + getOr: constant_a, + getOrThunk: constant_a, + getOrDie: constant_a, + or: self, + orThunk: self, + map: map, + ap: function (optfab) { + return optfab.fold(none, function(fab) { + return some(fab(a)); + }); + }, + each: function (f) { + f(a); + }, + bind: bind, + flatten: constant_a, + exists: bind, + forall: bind, + filter: function (f) { + return f(a) ? me : NONE; + }, + equals: function (o) { + return o.is(a); + }, + equals_: function (o, elementEq) { + return o.fold( + never, + function (b) { return elementEq(a, b); } + ); + }, + toArray: function () { + return [a]; + }, + toString: function () { + return 'some(' + a + ')'; + } + }; + return me; + }; + + /** from :: undefined|null|a -> Option a */ + var from = function (value) { + return value === null || value === undefined ? NONE : some(value); + }; + + return { + some: some, + none: none, + from: from + }; + } +); + +defineGlobal("global!String", String); +define( + 'ephox.katamari.api.Arr', + + [ + 'ephox.katamari.api.Option', + 'global!Array', + 'global!Error', + 'global!String' + ], + + function (Option, Array, Error, String) { + // Use the native Array.indexOf if it is available (IE9+) otherwise fall back to manual iteration + // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf + var rawIndexOf = (function () { + var pIndexOf = Array.prototype.indexOf; + + var fastIndex = function (xs, x) { return pIndexOf.call(xs, x); }; + + var slowIndex = function(xs, x) { return slowIndexOf(xs, x); }; + + return pIndexOf === undefined ? slowIndex : fastIndex; + })(); + + var indexOf = function (xs, x) { + // The rawIndexOf method does not wrap up in an option. This is for performance reasons. + var r = rawIndexOf(xs, x); + return r === -1 ? Option.none() : Option.some(r); + }; + + var contains = function (xs, x) { + return rawIndexOf(xs, x) > -1; + }; + + // Using findIndex is likely less optimal in Chrome (dynamic return type instead of bool) + // but if we need that micro-optimisation we can inline it later. + var exists = function (xs, pred) { + return findIndex(xs, pred).isSome(); + }; + + var range = function (num, f) { + var r = []; + for (var i = 0; i < num; i++) { + r.push(f(i)); + } + return r; + }; + + // It's a total micro optimisation, but these do make some difference. + // Particularly for browsers other than Chrome. + // - length caching + // http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69 + // - not using push + // http://jsperf.com/array-direct-assignment-vs-push/2 + + var chunk = function (array, size) { + var r = []; + for (var i = 0; i < array.length; i += size) { + var s = array.slice(i, i + size); + r.push(s); + } + return r; + }; + + var map = function(xs, f) { + // pre-allocating array size when it's guaranteed to be known + // http://jsperf.com/push-allocated-vs-dynamic/22 + var len = xs.length; + var r = new Array(len); + for (var i = 0; i < len; i++) { + var x = xs[i]; + r[i] = f(x, i, xs); + } + return r; + }; + + // Unwound implementing other functions in terms of each. + // The code size is roughly the same, and it should allow for better optimisation. + var each = function(xs, f) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + f(x, i, xs); + } + }; + + var eachr = function (xs, f) { + for (var i = xs.length - 1; i >= 0; i--) { + var x = xs[i]; + f(x, i, xs); + } + }; + + var partition = function(xs, pred) { + var pass = []; + var fail = []; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + var arr = pred(x, i, xs) ? pass : fail; + arr.push(x); + } + return { pass: pass, fail: fail }; + }; + + var filter = function(xs, pred) { + var r = []; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + r.push(x); + } + } + return r; + }; + + /* + * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f. + * + * f is a function that derives a value from an element - e.g. true or false, or a string. + * Elements are like if this function generates the same value for them (according to ===). + * + * + * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function. + * For a good explanation, see the group function (which is a special case of groupBy) + * http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group + */ + var groupBy = function (xs, f) { + if (xs.length === 0) { + return []; + } else { + var wasType = f(xs[0]); // initial case for matching + var r = []; + var group = []; + + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + var type = f(x); + if (type !== wasType) { + r.push(group); + group = []; + } + wasType = type; + group.push(x); + } + if (group.length !== 0) { + r.push(group); + } + return r; + } + }; + + var foldr = function (xs, f, acc) { + eachr(xs, function (x) { + acc = f(acc, x); + }); + return acc; + }; + + var foldl = function (xs, f, acc) { + each(xs, function (x) { + acc = f(acc, x); + }); + return acc; + }; + + var find = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + return Option.some(x); + } + } + return Option.none(); + }; + + var findIndex = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + return Option.some(i); + } + } + + return Option.none(); + }; + + var slowIndexOf = function (xs, x) { + for (var i = 0, len = xs.length; i < len; ++i) { + if (xs[i] === x) { + return i; + } + } + + return -1; + }; + + var push = Array.prototype.push; + var flatten = function (xs) { + // Note, this is possible because push supports multiple arguments: + // http://jsperf.com/concat-push/6 + // Note that in the past, concat() would silently work (very slowly) for array-like objects. + // With this change it will throw an error. + var r = []; + for (var i = 0, len = xs.length; i < len; ++i) { + // Ensure that each value is an array itself + if (! Array.prototype.isPrototypeOf(xs[i])) throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); + push.apply(r, xs[i]); + } + return r; + }; + + var bind = function (xs, f) { + var output = map(xs, f); + return flatten(output); + }; + + var forall = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; ++i) { + var x = xs[i]; + if (pred(x, i, xs) !== true) { + return false; + } + } + return true; + }; + + var equal = function (a1, a2) { + return a1.length === a2.length && forall(a1, function (x, i) { + return x === a2[i]; + }); + }; + + var slice = Array.prototype.slice; + var reverse = function (xs) { + var r = slice.call(xs, 0); + r.reverse(); + return r; + }; + + var difference = function (a1, a2) { + return filter(a1, function (x) { + return !contains(a2, x); + }); + }; + + var mapToObject = function(xs, f) { + var r = {}; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + r[String(x)] = f(x, i); + } + return r; + }; + + var pure = function(x) { + return [x]; + }; + + var sort = function (xs, comparator) { + var copy = slice.call(xs, 0); + copy.sort(comparator); + return copy; + }; + + var head = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[0]); + }; + + var last = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[xs.length - 1]); + }; + + return { + map: map, + each: each, + eachr: eachr, + partition: partition, + filter: filter, + groupBy: groupBy, + indexOf: indexOf, + foldr: foldr, + foldl: foldl, + find: find, + findIndex: findIndex, + flatten: flatten, + bind: bind, + forall: forall, + exists: exists, + contains: contains, + equal: equal, + reverse: reverse, + chunk: chunk, + difference: difference, + mapToObject: mapToObject, + pure: pure, + sort: sort, + range: range, + head: head, + last: last + }; + } +); +define( + 'ephox.katamari.api.Global', + + [ + ], + + function () { + // Use window object as the global if it's available since CSP will block script evals + if (typeof window !== 'undefined') { + return window; + } else { + return Function('return this;')(); + } + } +); + + +define( + 'ephox.katamari.api.Resolve', + + [ + 'ephox.katamari.api.Global' + ], + + function (Global) { + /** path :: ([String], JsObj?) -> JsObj */ + var path = function (parts, scope) { + var o = scope !== undefined ? scope : Global; + for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) + o = o[parts[i]]; + return o; + }; + + /** resolve :: (String, JsObj?) -> JsObj */ + var resolve = function (p, scope) { + var parts = p.split('.'); + return path(parts, scope); + }; + + /** step :: (JsObj, String) -> JsObj */ + var step = function (o, part) { + if (o[part] === undefined || o[part] === null) + o[part] = {}; + return o[part]; + }; + + /** forge :: ([String], JsObj?) -> JsObj */ + var forge = function (parts, target) { + var o = target !== undefined ? target : Global; + for (var i = 0; i < parts.length; ++i) + o = step(o, parts[i]); + return o; + }; + + /** namespace :: (String, JsObj?) -> JsObj */ + var namespace = function (name, target) { + var parts = name.split('.'); + return forge(parts, target); + }; + + return { + path: path, + resolve: resolve, + forge: forge, + namespace: namespace + }; + } +); + + +define( + 'ephox.sand.util.Global', + + [ + 'ephox.katamari.api.Resolve' + ], + + function (Resolve) { + var unsafe = function (name, scope) { + return Resolve.resolve(name, scope); + }; + + var getOrDie = function (name, scope) { + var actual = unsafe(name, scope); + + if (actual === undefined) throw name + ' not available on this browser'; + return actual; + }; + + return { + getOrDie: getOrDie + }; + } +); +define( + 'ephox.sand.api.Node', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * MDN says (yes) for IE, but it's undefined on IE8 + */ + var node = function () { + var f = Global.getOrDie('Node'); + return f; + }; + + /* + * Most of numerosity doesn't alter the methods on the object. + * We're making an exception for Node, because bitwise and is so easy to get wrong. + * + * Might be nice to ADT this at some point instead of having individual methods. + */ + + var compareDocumentPosition = function (a, b, match) { + // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions + // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation + // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition + return (a.compareDocumentPosition(b) & match) !== 0; + }; + + var documentPositionPreceding = function (a, b) { + return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING); + }; + + var documentPositionContainedBy = function (a, b) { + return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY); + }; + + return { + documentPositionPreceding: documentPositionPreceding, + documentPositionContainedBy: documentPositionContainedBy + }; + } +); +define( + 'ephox.katamari.api.Thunk', + + [ + ], + + function () { + + var cached = function (f) { + var called = false; + var r; + return function() { + if (!called) { + called = true; + r = f.apply(null, arguments); + } + return r; + }; + }; + + return { + cached: cached + }; + } +); + +defineGlobal("global!Number", Number); +define( + 'ephox.sand.detect.Version', + + [ + 'ephox.katamari.api.Arr', + 'global!Number', + 'global!String' + ], + + function (Arr, Number, String) { + var firstMatch = function (regexes, s) { + for (var i = 0; i < regexes.length; i++) { + var x = regexes[i]; + if (x.test(s)) return x; + } + return undefined; + }; + + var find = function (regexes, agent) { + var r = firstMatch(regexes, agent); + if (!r) return { major : 0, minor : 0 }; + var group = function(i) { + return Number(agent.replace(r, '$' + i)); + }; + return nu(group(1), group(2)); + }; + + var detect = function (versionRegexes, agent) { + var cleanedAgent = String(agent).toLowerCase(); + + if (versionRegexes.length === 0) return unknown(); + return find(versionRegexes, cleanedAgent); + }; + + var unknown = function () { + return nu(0, 0); + }; + + var nu = function (major, minor) { + return { major: major, minor: minor }; + }; + + return { + nu: nu, + detect: detect, + unknown: unknown + }; + } +); +define( + 'ephox.sand.core.Browser', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sand.detect.Version' + ], + + function (Fun, Version) { + var edge = 'Edge'; + var chrome = 'Chrome'; + var ie = 'IE'; + var opera = 'Opera'; + var firefox = 'Firefox'; + var safari = 'Safari'; + + var isBrowser = function (name, current) { + return function () { + return current === name; + }; + }; + + var unknown = function () { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; + + var nu = function (info) { + var current = info.current; + var version = info.version; + + return { + current: current, + version: version, + + // INVESTIGATE: Rename to Edge ? + isEdge: isBrowser(edge, current), + isChrome: isBrowser(chrome, current), + // NOTE: isIe just looks too weird + isIE: isBrowser(ie, current), + isOpera: isBrowser(opera, current), + isFirefox: isBrowser(firefox, current), + isSafari: isBrowser(safari, current) + }; + }; + + return { + unknown: unknown, + nu: nu, + edge: Fun.constant(edge), + chrome: Fun.constant(chrome), + ie: Fun.constant(ie), + opera: Fun.constant(opera), + firefox: Fun.constant(firefox), + safari: Fun.constant(safari) + }; + } +); +define( + 'ephox.sand.core.OperatingSystem', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sand.detect.Version' + ], + + function (Fun, Version) { + var windows = 'Windows'; + var ios = 'iOS'; + var android = 'Android'; + var linux = 'Linux'; + var osx = 'OSX'; + var solaris = 'Solaris'; + var freebsd = 'FreeBSD'; + + // Though there is a bit of dupe with this and Browser, trying to + // reuse code makes it much harder to follow and change. + var isOS = function (name, current) { + return function () { + return current === name; + }; + }; + + var unknown = function () { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; + + var nu = function (info) { + var current = info.current; + var version = info.version; + + return { + current: current, + version: version, + + isWindows: isOS(windows, current), + // TODO: Fix capitalisation + isiOS: isOS(ios, current), + isAndroid: isOS(android, current), + isOSX: isOS(osx, current), + isLinux: isOS(linux, current), + isSolaris: isOS(solaris, current), + isFreeBSD: isOS(freebsd, current) + }; + }; + + return { + unknown: unknown, + nu: nu, + + windows: Fun.constant(windows), + ios: Fun.constant(ios), + android: Fun.constant(android), + linux: Fun.constant(linux), + osx: Fun.constant(osx), + solaris: Fun.constant(solaris), + freebsd: Fun.constant(freebsd) + }; + } +); +define( + 'ephox.sand.detect.DeviceType', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + return function (os, browser, userAgent) { + var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; + var isiPhone = os.isiOS() && !isiPad; + var isAndroid3 = os.isAndroid() && os.version.major === 3; + var isAndroid4 = os.isAndroid() && os.version.major === 4; + var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true ); + var isTouch = os.isiOS() || os.isAndroid(); + var isPhone = isTouch && !isTablet; + + var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; + + return { + isiPad : Fun.constant(isiPad), + isiPhone: Fun.constant(isiPhone), + isTablet: Fun.constant(isTablet), + isPhone: Fun.constant(isPhone), + isTouch: Fun.constant(isTouch), + isAndroid: os.isAndroid, + isiOS: os.isiOS, + isWebView: Fun.constant(iOSwebview) + }; + }; + } +); +define( + 'ephox.sand.detect.UaString', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sand.detect.Version', + 'global!String' + ], + + function (Arr, Version, String) { + var detect = function (candidates, userAgent) { + var agent = String(userAgent).toLowerCase(); + return Arr.find(candidates, function (candidate) { + return candidate.search(agent); + }); + }; + + // They (browser and os) are the same at the moment, but they might + // not stay that way. + var detectBrowser = function (browsers, userAgent) { + return detect(browsers, userAgent).map(function (browser) { + var version = Version.detect(browser.versionRegexes, userAgent); + return { + current: browser.name, + version: version + }; + }); + }; + + var detectOs = function (oses, userAgent) { + return detect(oses, userAgent).map(function (os) { + var version = Version.detect(os.versionRegexes, userAgent); + return { + current: os.name, + version: version + }; + }); + }; + + return { + detectBrowser: detectBrowser, + detectOs: detectOs + }; + } +); +define( + 'ephox.katamari.str.StrAppend', + + [ + + ], + + function () { + var addToStart = function (str, prefix) { + return prefix + str; + }; + + var addToEnd = function (str, suffix) { + return str + suffix; + }; + + var removeFromStart = function (str, numChars) { + return str.substring(numChars); + }; + + var removeFromEnd = function (str, numChars) { + return str.substring(0, str.length - numChars); + }; + + return { + addToStart: addToStart, + addToEnd: addToEnd, + removeFromStart: removeFromStart, + removeFromEnd: removeFromEnd + }; + } +); +define( + 'ephox.katamari.str.StringParts', + + [ + 'ephox.katamari.api.Option', + 'global!Error' + ], + + function (Option, Error) { + /** Return the first 'count' letters from 'str'. +- * e.g. first("abcde", 2) === "ab" +- */ + var first = function(str, count) { + return str.substr(0, count); + }; + + /** Return the last 'count' letters from 'str'. + * e.g. last("abcde", 2) === "de" + */ + var last = function(str, count) { + return str.substr(str.length - count, str.length); + }; + + var head = function(str) { + return str === '' ? Option.none() : Option.some(str.substr(0, 1)); + }; + + var tail = function(str) { + return str === '' ? Option.none() : Option.some(str.substring(1)); + }; + + return { + first: first, + last: last, + head: head, + tail: tail + }; + } +); +define( + 'ephox.katamari.api.Strings', + + [ + 'ephox.katamari.str.StrAppend', + 'ephox.katamari.str.StringParts', + 'global!Error' + ], + + function (StrAppend, StringParts, Error) { + var checkRange = function(str, substr, start) { + if (substr === '') return true; + if (str.length < substr.length) return false; + var x = str.substr(start, start + substr.length); + return x === substr; + }; + + /** Given a string and object, perform template-replacements on the string, as specified by the object. + * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"] + * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format. + */ + var supplant = function(str, obj) { + var isStringOrNumber = function(a) { + var t = typeof a; + return t === 'string' || t === 'number'; + }; + + return str.replace(/\${([^{}]*)}/g, + function (a, b) { + var value = obj[b]; + return isStringOrNumber(value) ? value : a; + } + ); + }; + + var removeLeading = function (str, prefix) { + return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str; + }; + + var removeTrailing = function (str, prefix) { + return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str; + }; + + var ensureLeading = function (str, prefix) { + return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix); + }; + + var ensureTrailing = function (str, prefix) { + return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix); + }; + + var contains = function(str, substr) { + return str.indexOf(substr) !== -1; + }; + + var capitalize = function(str) { + return StringParts.head(str).bind(function (head) { + return StringParts.tail(str).map(function (tail) { + return head.toUpperCase() + tail; + }); + }).getOr(str); + }; + + /** Does 'str' start with 'prefix'? + * Note: all strings start with the empty string. + * More formally, for all strings x, startsWith(x, ""). + * This is so that for all strings x and y, startsWith(y + x, y) + */ + var startsWith = function(str, prefix) { + return checkRange(str, prefix, 0); + }; + + /** Does 'str' end with 'suffix'? + * Note: all strings end with the empty string. + * More formally, for all strings x, endsWith(x, ""). + * This is so that for all strings x and y, endsWith(x + y, y) + */ + var endsWith = function(str, suffix) { + return checkRange(str, suffix, str.length - suffix.length); + }; + + + /** removes all leading and trailing spaces */ + var trim = function(str) { + return str.replace(/^\s+|\s+$/g, ''); + }; + + var lTrim = function(str) { + return str.replace(/^\s+/g, ''); + }; + + var rTrim = function(str) { + return str.replace(/\s+$/g, ''); + }; + + return { + supplant: supplant, + startsWith: startsWith, + removeLeading: removeLeading, + removeTrailing: removeTrailing, + ensureLeading: ensureLeading, + ensureTrailing: ensureTrailing, + endsWith: endsWith, + contains: contains, + trim: trim, + lTrim: lTrim, + rTrim: rTrim, + capitalize: capitalize + }; + } +); + +define( + 'ephox.sand.info.PlatformInfo', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Strings' + ], + + function (Fun, Strings) { + var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; + + var checkContains = function (target) { + return function (uastring) { + return Strings.contains(uastring, target); + }; + }; + + var browsers = [ + { + name : 'Edge', + versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], + search: function (uastring) { + var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit'); + return monstrosity; + } + }, + { + name : 'Chrome', + versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], + search : function (uastring) { + return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe'); + } + }, + { + name : 'IE', + versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], + search: function (uastring) { + return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident'); + } + }, + // INVESTIGATE: Is this still the Opera user agent? + { + name : 'Opera', + versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], + search : checkContains('opera') + }, + { + name : 'Firefox', + versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], + search : checkContains('firefox') + }, + { + name : 'Safari', + versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], + search : function (uastring) { + return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit'); + } + } + ]; + + var oses = [ + { + name : 'Windows', + search : checkContains('win'), + versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name : 'iOS', + search : function (uastring) { + return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad'); + }, + versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] + }, + { + name : 'Android', + search : checkContains('android'), + versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name : 'OSX', + search : checkContains('os x'), + versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/] + }, + { + name : 'Linux', + search : checkContains('linux'), + versionRegexes: [ ] + }, + { name : 'Solaris', + search : checkContains('sunos'), + versionRegexes: [ ] + }, + { + name : 'FreeBSD', + search : checkContains('freebsd'), + versionRegexes: [ ] + } + ]; + + return { + browsers: Fun.constant(browsers), + oses: Fun.constant(oses) + }; + } +); +define( + 'ephox.sand.core.PlatformDetection', + + [ + 'ephox.sand.core.Browser', + 'ephox.sand.core.OperatingSystem', + 'ephox.sand.detect.DeviceType', + 'ephox.sand.detect.UaString', + 'ephox.sand.info.PlatformInfo' + ], + + function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) { + var detect = function (userAgent) { + var browsers = PlatformInfo.browsers(); + var oses = PlatformInfo.oses(); + + var browser = UaString.detectBrowser(browsers, userAgent).fold( + Browser.unknown, + Browser.nu + ); + var os = UaString.detectOs(oses, userAgent).fold( + OperatingSystem.unknown, + OperatingSystem.nu + ); + var deviceType = DeviceType(os, browser, userAgent); + + return { + browser: browser, + os: os, + deviceType: deviceType + }; + }; + + return { + detect: detect + }; + } +); +defineGlobal("global!navigator", navigator); +define( + 'ephox.sand.api.PlatformDetection', + + [ + 'ephox.katamari.api.Thunk', + 'ephox.sand.core.PlatformDetection', + 'global!navigator' + ], + + function (Thunk, PlatformDetection, navigator) { + var detect = Thunk.cached(function () { + var userAgent = navigator.userAgent; + return PlatformDetection.detect(userAgent); + }); + + return { + detect: detect + }; + } +); +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', + + [ + + ], + + function () { + return { + ATTRIBUTE: 2, + CDATA_SECTION: 4, + COMMENT: 8, + DOCUMENT: 9, + DOCUMENT_TYPE: 10, + DOCUMENT_FRAGMENT: 11, + ELEMENT: 1, + TEXT: 3, + PROCESSING_INSTRUCTION: 7, + ENTITY_REFERENCE: 5, + ENTITY: 6, + NOTATION: 12 + }; + } +); +define( + 'ephox.sugar.api.search.Selectors', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.NodeTypes', + 'global!Error', + 'global!document' + ], + + function (Arr, Option, Element, NodeTypes, Error, document) { + /* + * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons, + * instead of doing object lookup feature detection on every call + */ + var STANDARD = 0; + var MSSTANDARD = 1; + var WEBKITSTANDARD = 2; + var FIREFOXSTANDARD = 3; + + var selectorType = (function () { + var test = document.createElement('span'); + // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. + // Still check for the others, but do it last. + return test.matches !== undefined ? STANDARD : + test.msMatchesSelector !== undefined ? MSSTANDARD : + test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD : + test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD : + -1; + })(); + + + var ELEMENT = NodeTypes.ELEMENT; + var DOCUMENT = NodeTypes.DOCUMENT; + + var is = function (element, selector) { + var elem = element.dom(); + if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches + + // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. + // Still check for the others, but do it last. + else if (selectorType === STANDARD) return elem.matches(selector); + else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector); + else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector); + else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector); + else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :( + }; + + var bypassSelector = function (dom) { + // Only elements and documents support querySelector + return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT || + // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ + dom.childElementCount === 0; + }; + + var all = function (selector, scope) { + var base = scope === undefined ? document : scope.dom(); + return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom); + }; + + var one = function (selector, scope) { + var base = scope === undefined ? document : scope.dom(); + return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom); + }; + + return { + all: all, + is: is, + one: one + }; + } +); + +define( + 'ephox.sugar.api.dom.Compare', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.Node', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.search.Selectors' + ], + + function (Arr, Fun, Node, PlatformDetection, Selectors) { + + var eq = function (e1, e2) { + return e1.dom() === e2.dom(); + }; + + var isEqualNode = function (e1, e2) { + return e1.dom().isEqualNode(e2.dom()); + }; + + var member = function (element, elements) { + return Arr.exists(elements, Fun.curry(eq, element)); + }; + + // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself). + var regularContains = function (e1, e2) { + var d1 = e1.dom(), d2 = e2.dom(); + return d1 === d2 ? false : d1.contains(d2); + }; + + var ieContains = function (e1, e2) { + // IE only implements the contains() method for Element nodes. + // It fails for Text nodes, so implement it using compareDocumentPosition() + // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect + // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1': + // Also, compareDocumentPosition defines a node containing itself as false. + return Node.documentPositionContainedBy(e1.dom(), e2.dom()); + }; + + var browser = PlatformDetection.detect().browser; + + // Returns: true if node e1 contains e2, otherwise false. + // (returns false if e1===e2: A node does not contain itself). + var contains = browser.isIE() ? ieContains : regularContains; + + return { + eq: eq, + isEqualNode: isEqualNode, + member: member, + contains: contains, + + // Only used by DomUniverse. Remove (or should Selectors.is move here?) + is: Selectors.is + }; + } +); + +define( + 'ephox.alloy.alien.EventRoot', + + [ + 'ephox.sugar.api.dom.Compare' + ], + + function (Compare) { + var isSource = function (component, simulatedEvent) { + return Compare.eq(component.element(), simulatedEvent.event().target()); + }; + + return { + isSource: isSource + }; + } +); +define( + 'ephox.alloy.api.events.NativeEvents', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + return { + contextmenu: Fun.constant('contextmenu'), + touchstart: Fun.constant('touchstart'), + touchmove: Fun.constant('touchmove'), + touchend: Fun.constant('touchend'), + gesturestart: Fun.constant('gesturestart'), + mousedown: Fun.constant('mousedown'), + mousemove: Fun.constant('mousemove'), + mouseout: Fun.constant('mouseout'), + mouseup: Fun.constant('mouseup'), + mouseover: Fun.constant('mouseover'), + // Not really a native event as it has to be simulated + focusin: Fun.constant('focusin'), + + keydown: Fun.constant('keydown'), + + input: Fun.constant('input'), + change: Fun.constant('change'), + focus: Fun.constant('focus'), + + click: Fun.constant('click'), + + transitionend: Fun.constant('transitionend'), + selectstart: Fun.constant('selectstart') + }; + } +); + +define( + 'ephox.alloy.api.events.SystemEvents', + + [ + 'ephox.alloy.api.events.NativeEvents', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.PlatformDetection' + ], + + function (NativeEvents, Fun, PlatformDetection) { + var alloy = { tap: Fun.constant('alloy.tap') }; + + return { + // This is used to pass focus to a component. A component might interpret + // this event and pass the DOM focus to one of its children, depending on its + // focus model. + focus: Fun.constant('alloy.focus'), + + // This event is fired a small amount of time after the blur has fired. This + // allows the handler to know what was the focused element, and what is now. + postBlur: Fun.constant('alloy.blur.post'), + + // This event is fired by gui.broadcast*. It is defined by 'receivers' + receive: Fun.constant('alloy.receive'), + + // This event is for executing buttons and things that have (mostly) enter actions + execute: Fun.constant('alloy.execute'), + + // This event is used by a menu to tell an item to focus itself because it has been + // selected. This might automatically focus inside the item, it might focus the outer + // part of the widget etc. + focusItem: Fun.constant('alloy.focus.item'), + + // This event represents a touchstart and touchend on the same location, and fires on + // the touchend + tap: alloy.tap, + + // Tap event for touch device, otherwise click event + tapOrClick: PlatformDetection.detect().deviceType.isTouch() ? alloy.tap : NativeEvents.click, + + // This event represents a longpress on the same location + longpress: Fun.constant('alloy.longpress'), + + // Fire by a child element to tell the outer element to close + sandboxClose: Fun.constant('alloy.sandbox.close'), + + // Fired when adding to a world + systemInit: Fun.constant('alloy.system.init'), + + // Fired when the window scrolls + windowScroll: Fun.constant('alloy.system.scroll'), + + attachedToDom: Fun.constant('alloy.system.attached'), + detachedFromDom: Fun.constant('alloy.system.detached'), + + changeTab: Fun.constant('alloy.change.tab'), + dismissTab: Fun.constant('alloy.dismiss.tab') + }; + } +); +define( + 'ephox.katamari.api.Type', + + [ + 'global!Array', + 'global!String' + ], + + function (Array, String) { + var typeOf = function(x) { + if (x === null) return 'null'; + var t = typeof x; + if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array'; + if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string'; + return t; + }; + + var isType = function (type) { + return function (value) { + return typeOf(value) === type; + }; + }; + + return { + isString: isType('string'), + isObject: isType('object'), + isArray: isType('array'), + isNull: isType('null'), + isBoolean: isType('boolean'), + isUndefined: isType('undefined'), + isFunction: isType('function'), + isNumber: isType('number') + }; + } +); + + +define( + 'ephox.katamari.api.Merger', + + [ + 'ephox.katamari.api.Type', + 'global!Array', + 'global!Error' + ], + + function (Type, Array, Error) { + + var shallow = function (old, nu) { + return nu; + }; + + var deep = function (old, nu) { + var bothObjects = Type.isObject(old) && Type.isObject(nu); + return bothObjects ? deepMerge(old, nu) : nu; + }; + + var baseMerge = function (merger) { + return function() { + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var objects = new Array(arguments.length); + for (var i = 0; i < objects.length; i++) objects[i] = arguments[i]; + + if (objects.length === 0) throw new Error('Can\'t merge zero objects'); + + var ret = {}; + for (var j = 0; j < objects.length; j++) { + var curObject = objects[j]; + for (var key in curObject) if (curObject.hasOwnProperty(key)) { + ret[key] = merger(ret[key], curObject[key]); + } + } + return ret; + }; + }; + + var deepMerge = baseMerge(deep); + var merge = baseMerge(shallow); + + return { + deepMerge: deepMerge, + merge: merge + }; + } +); +define( + 'ephox.katamari.api.Obj', + + [ + 'ephox.katamari.api.Option', + 'global!Object' + ], + + function (Option, Object) { + // There are many variations of Object iteration that are faster than the 'for-in' style: + // http://jsperf.com/object-keys-iteration/107 + // + // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering + var keys = (function () { + var fastKeys = Object.keys; + + // This technically means that 'each' and 'find' on IE8 iterate through the object twice. + // This code doesn't run on IE8 much, so it's an acceptable tradeoff. + // If it becomes a problem we can always duplicate the feature detection inside each and find as well. + var slowKeys = function (o) { + var r = []; + for (var i in o) { + if (o.hasOwnProperty(i)) { + r.push(i); + } + } + return r; + }; + + return fastKeys === undefined ? slowKeys : fastKeys; + })(); + + + var each = function (obj, f) { + var props = keys(obj); + for (var k = 0, len = props.length; k < len; k++) { + var i = props[k]; + var x = obj[i]; + f(x, i, obj); + } + }; + + /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */ + var objectMap = function (obj, f) { + return tupleMap(obj, function (x, i, obj) { + return { + k: i, + v: f(x, i, obj) + }; + }); + }; + + /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */ + var tupleMap = function (obj, f) { + var r = {}; + each(obj, function (x, i) { + var tuple = f(x, i, obj); + r[tuple.k] = tuple.v; + }); + return r; + }; + + /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */ + var bifilter = function (obj, pred) { + var t = {}; + var f = {}; + each(obj, function(x, i) { + var branch = pred(x, i) ? t : f; + branch[i] = x; + }); + return { + t: t, + f: f + }; + }; + + /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */ + var mapToArray = function (obj, f) { + var r = []; + each(obj, function(value, name) { + r.push(f(value, name)); + }); + return r; + }; + + /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */ + var find = function (obj, pred) { + var props = keys(obj); + for (var k = 0, len = props.length; k < len; k++) { + var i = props[k]; + var x = obj[i]; + if (pred(x, i, obj)) { + return Option.some(x); + } + } + return Option.none(); + }; + + /** values :: JsObj(k, v) -> [v] */ + var values = function (obj) { + return mapToArray(obj, function (v) { + return v; + }); + }; + + var size = function (obj) { + return values(obj).length; + }; + + return { + bifilter: bifilter, + each: each, + map: objectMap, + mapToArray: mapToArray, + tupleMap: tupleMap, + find: find, + keys: keys, + values: values, + size: size + }; + } +); +define( + 'ephox.alloy.api.events.AlloyTriggers', + + [ + 'ephox.alloy.api.events.SystemEvents', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj' + ], + + function (SystemEvents, Fun, Merger, Obj) { + var emit = function (component, event) { + dispatchWith(component, component.element(), event, { }); + }; + + var emitWith = function (component, event, properties) { + dispatchWith(component, component.element(), event, properties); + }; + + var emitExecute = function (component) { + emit(component, SystemEvents.execute()); + }; + + var dispatch = function (component, target, event) { + dispatchWith(component, target, event, { }); + }; + + var dispatchWith = function (component, target, event, properties) { + var data = Merger.deepMerge({ + target: target + }, properties); + component.getSystem().triggerEvent(event, target, Obj.map(data, Fun.constant)); + }; + + var dispatchEvent = function (component, target, event, simulatedEvent) { + component.getSystem().triggerEvent(event, target, simulatedEvent.event()); + }; + + var dispatchFocus = function (component, target) { + component.getSystem().triggerFocus(target, component.element()); + }; + + return { + emit: emit, + emitWith: emitWith, + emitExecute: emitExecute, + dispatch: dispatch, + dispatchWith: dispatchWith, + dispatchEvent: dispatchEvent, + dispatchFocus: dispatchFocus + }; + } +); + +define( + 'ephox.katamari.api.Adt', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Type', + 'global!Array', + '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'); + } + 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 + }; + } +); +define( + 'ephox.boulder.api.FieldPresence', + + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Fun' + ], + + function (Adt, Fun) { + var adt = Adt.generate([ + { strict: [ ] }, + { defaultedThunk: [ 'fallbackThunk' ] }, + { asOption: [ ] }, + { asDefaultedOptionThunk: [ 'fallbackThunk' ] }, + { mergeWithThunk: [ 'baseThunk' ] } + ]); + + var defaulted = function (fallback) { + return adt.defaultedThunk( + Fun.constant(fallback) + ); + }; + + var asDefaultedOption = function (fallback) { + return adt.asDefaultedOptionThunk( + Fun.constant(fallback) + ); + }; + + var mergeWith = function (base) { + return adt.mergeWithThunk( + Fun.constant(base) + ); + }; + + return { + strict: adt.strict, + asOption: adt.asOption, + + defaulted: defaulted, + defaultedThunk: adt.defaultedThunk, + + asDefaultedOption: asDefaultedOption, + asDefaultedOptionThunk: adt.asDefaultedOptionThunk, + + mergeWith: mergeWith, + mergeWithThunk: adt.mergeWithThunk + }; + } +); +define( + 'ephox.katamari.api.Result', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option' + ], + + function (Fun, Option) { + /* The type signatures for Result + * is :: this Result a -> a -> Bool + * or :: this Result a -> Result a -> Result a + * orThunk :: this Result a -> (_ -> Result a) -> Result a + * map :: this Result a -> (a -> b) -> Result b + * each :: this Result a -> (a -> _) -> _ + * bind :: this Result a -> (a -> Result b) -> Result b + * fold :: this Result a -> (_ -> b, a -> b) -> b + * exists :: this Result a -> (a -> Bool) -> Bool + * forall :: this Result a -> (a -> Bool) -> Bool + * toOption :: this Result a -> Option a + * isValue :: this Result a -> Bool + * isError :: this Result a -> Bool + * getOr :: this Result a -> a -> a + * getOrThunk :: this Result a -> (_ -> a) -> a + * getOrDie :: this Result a -> a (or throws error) + */ + + var value = function (o) { + var is = function (v) { + return o === v; + }; + + var or = function (opt) { + return value(o); + }; + + var orThunk = function (f) { + return value(o); + }; + + var map = function (f) { + return value(f(o)); + }; + + var each = function (f) { + f(o); + }; + + var bind = function (f) { + return f(o); + }; + + var fold = function (_, onValue) { + return onValue(o); + }; + + var exists = function (f) { + return f(o); + }; + + var forall = function (f) { + return f(o); + }; + + var toOption = function () { + return Option.some(o); + }; + + return { + is: is, + isValue: Fun.constant(true), + isError: Fun.constant(false), + getOr: Fun.constant(o), + getOrThunk: Fun.constant(o), + getOrDie: Fun.constant(o), + or: or, + orThunk: orThunk, + fold: fold, + map: map, + each: each, + bind: bind, + exists: exists, + forall: forall, + toOption: toOption + }; + }; + + var error = function (message) { + var getOrThunk = function (f) { + return f(); + }; + + var getOrDie = function () { + return Fun.die(message)(); + }; + + var or = function (opt) { + return opt; + }; + + var orThunk = function (f) { + return f(); + }; + + var map = function (f) { + return error(message); + }; + + var bind = function (f) { + return error(message); + }; + + var fold = function (onError, _) { + return onError(message); + }; + + return { + is: Fun.constant(false), + isValue: Fun.constant(false), + isError: Fun.constant(true), + getOr: Fun.identity, + getOrThunk: getOrThunk, + getOrDie: getOrDie, + or: or, + orThunk: orThunk, + fold: fold, + map: map, + each: Fun.noop, + bind: bind, + exists: Fun.constant(false), + forall: Fun.constant(true), + toOption: Option.none + }; + }; + + return { + value: value, + error: error + }; + } +); + +define( + 'ephox.katamari.api.Results', + + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Arr' + ], + + function (Adt, Arr) { + var comparison = Adt.generate([ + { bothErrors: [ 'error1', 'error2' ] }, + { firstError: [ 'error1', 'value2' ] }, + { secondError: [ 'value1', 'error2' ] }, + { bothValues: [ 'value1', 'value2' ] } + ]); + + /** partition :: [Result a] -> { errors: [String], values: [a] } */ + var partition = function (results) { + var errors = []; + var values = []; + + Arr.each(results, function (result) { + result.fold( + function (err) { errors.push(err); }, + function (value) { values.push(value); } + ); + }); + + return { errors: errors, values: values }; + }; + + /** compare :: (Result a, Result b) -> Comparison a b */ + var compare = function (result1, result2) { + return result1.fold(function (err1) { + return result2.fold(function (err2) { + return comparison.bothErrors(err1, err2); + }, function (val2) { + return comparison.firstError(err1, val2); + }); + }, function (val1) { + return result2.fold(function (err2) { + return comparison.secondError(val1, err2); + }, function (val2) { + return comparison.bothValues(val1, val2); + }); + }); + }; + + return { + partition: partition, + compare: compare + }; + } +); +define( + 'ephox.boulder.combine.ResultCombine', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Result', + 'ephox.katamari.api.Results' + ], + + function (Arr, Fun, Merger, Result, Results) { + var mergeValues = function (values, base) { + return Result.value( + Merger.deepMerge.apply(undefined, [ base ].concat(values)) + ); + }; + + var mergeErrors = function (errors) { + return Fun.compose(Result.error, Arr.flatten)(errors); + }; + + var consolidateObj = function (objects, base) { + var partitions = Results.partition(objects); + return partitions.errors.length > 0 ? mergeErrors(partitions.errors) : mergeValues(partitions.values, base); + }; + + var consolidateArr = function (objects) { + var partitions = Results.partition(objects); + return partitions.errors.length > 0 ? mergeErrors(partitions.errors) : Result.value(partitions.values); + }; + + return { + consolidateObj: consolidateObj, + consolidateArr: consolidateArr + }; + } +); +define( + 'ephox.boulder.core.ObjChanger', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj' + ], + + function (Arr, Obj) { + var narrow = function (obj, fields) { + var r = { }; + Arr.each(fields, function (field) { + if (obj[field] !== undefined && obj.hasOwnProperty(field)) r[field] = obj[field]; + }); + + return r; + }; + + var indexOnKey = function (array, key) { + var obj = { }; + Arr.each(array, function (a) { + // FIX: Work out what to do here. + var keyValue = a[key]; + obj[keyValue] = a; + }); + return obj; + }; + + var exclude = function (obj, fields) { + var r = { }; + Obj.each(obj, function (v, k) { + if (! Arr.contains(fields, k)) { + r[k] = v; + } + }); + return r; + }; + + return { + narrow: narrow, + exclude: exclude, + indexOnKey: indexOnKey + }; + } +); +define( + 'ephox.boulder.core.ObjReader', + + [ + 'ephox.katamari.api.Option' + ], + + function (Option) { + var readOpt = function (key) { + return function (obj) { + return obj.hasOwnProperty(key) ? Option.from(obj[key]) : Option.none(); + }; + }; + + var readOr = function (key, fallback) { + return function (obj) { + return readOpt(key)(obj).getOr(fallback); + }; + }; + + var readOptFrom = function (obj, key) { + return readOpt(key)(obj); + }; + + var hasKey = function (obj, key) { + return obj.hasOwnProperty(key) && obj[key] !== undefined && obj[key] !== null; + }; + + return { + readOpt: readOpt, + readOr: readOr, + readOptFrom: readOptFrom, + hasKey: hasKey + }; + } +); +define( + 'ephox.boulder.core.ObjWriter', + + [ + 'ephox.katamari.api.Arr' + ], + + function (Arr) { + var wrap = function (key, value) { + var r = {}; + r[key] = value; + return r; + }; + + var wrapAll = function (keyvalues) { + var r = {}; + Arr.each(keyvalues, function (kv) { + r[kv.key] = kv.value; + }); + return r; + }; + + return { + wrap: wrap, + wrapAll: wrapAll + }; + } +); +define( + 'ephox.boulder.api.Objects', + + [ + 'ephox.boulder.combine.ResultCombine', + 'ephox.boulder.core.ObjChanger', + 'ephox.boulder.core.ObjReader', + 'ephox.boulder.core.ObjWriter' + ], + + function (ResultCombine, ObjChanger, ObjReader, ObjWriter) { + // Perhaps this level of indirection is unnecessary. + var narrow = function (obj, fields) { + return ObjChanger.narrow(obj, fields); + }; + + var exclude = function (obj, fields) { + return ObjChanger.exclude(obj, fields); + }; + + var readOpt = function (key) { + return ObjReader.readOpt(key); + }; + + var readOr = function (key, fallback) { + return ObjReader.readOr(key, fallback); + }; + + var readOptFrom = function (obj, key) { + return ObjReader.readOptFrom(obj, key); + }; + + var wrap = function (key, value) { + return ObjWriter.wrap(key, value); + }; + + var wrapAll = function (keyvalues) { + return ObjWriter.wrapAll(keyvalues); + }; + + var indexOnKey = function (array, key) { + return ObjChanger.indexOnKey(array, key); + }; + + var consolidate = function (objs, base) { + return ResultCombine.consolidateObj(objs, base); + }; + + var hasKey = function (obj, key) { + return ObjReader.hasKey(obj, key); + }; + + return { + narrow: narrow, + exclude: exclude, + readOpt: readOpt, + readOr: readOr, + readOptFrom: readOptFrom, + wrap: wrap, + wrapAll: wrapAll, + indexOnKey: indexOnKey, + hasKey: hasKey, + consolidate: consolidate + }; + } +); +define( + 'ephox.sand.api.JSON', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * IE8 and above per + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON + */ + var json = function () { + return Global.getOrDie('JSON'); + }; + + var parse = function (obj) { + return json().parse(obj); + }; + + var stringify = function (obj, replacer, space) { + return json().stringify(obj, replacer, space); + }; + + return { + parse: parse, + stringify: stringify + }; + } +); +define( + 'ephox.boulder.format.PrettyPrinter', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Type', + 'ephox.sand.api.JSON' + ], + + function (Arr, Obj, Type, Json) { + var formatObj = function (input) { + return Type.isObject(input) && Obj.keys(input).length > 100 ? ' removed due to size' : Json.stringify(input, null, 2); + + }; + + var formatErrors = function (errors) { + var es = errors.length > 10 ? errors.slice(0, 10).concat([ + { + path: [ ], + getErrorInfo: function () { + return '... (only showing first ten failures)'; + } + } + ]) : errors; + + // TODO: Work out a better split between PrettyPrinter and SchemaError + return Arr.map(es, function (e) { + return 'Failed path: (' + e.path.join(' > ') + ')\n' + e.getErrorInfo(); + }); + }; + + return { + formatObj: formatObj, + formatErrors: formatErrors + }; + } +); +define( + 'ephox.boulder.core.SchemaError', + + [ + 'ephox.boulder.format.PrettyPrinter', + 'ephox.katamari.api.Result' + ], + + function (PrettyPrinter, Result) { + var nu = function (path, getErrorInfo) { + return Result.error([{ + path: path, + // This is lazy so that it isn't calculated unnecessarily + getErrorInfo: getErrorInfo + }]); + }; + + var missingStrict = function (path, key, obj) { + return nu(path, function () { + return 'Could not find valid *strict* value for "' + key + '" in ' + PrettyPrinter.formatObj(obj); + }); + }; + + var missingKey = function (path, key) { + return nu(path, function () { + return 'Choice schema did not contain choice key: "' + key + '"'; + }); + }; + + var missingBranch = function (path, branches, branch) { + return nu(path, function () { + return 'The chosen schema: "' + branch + '" did not exist in branches: ' + PrettyPrinter.formatObj(branches); + }); + }; + + var unsupportedFields = function (path, unsupported) { + return nu(path, function () { + return 'There are unsupported fields: [' + unsupported.join(', ') + '] specified'; + }); + }; + + var custom = function (path, err) { + return nu(path, function () { return err; }); + }; + + var toString = function (error) { + return 'Failed path: (' + error.path.join(' > ') + ')\n' + error.getErrorInfo(); + }; + + return { + missingStrict: missingStrict, + missingKey: missingKey, + missingBranch: missingBranch, + unsupportedFields: unsupportedFields, + custom: custom, + toString: toString + }; + } +); +define( + 'ephox.boulder.format.TypeTokens', + + [ + 'ephox.katamari.api.Adt' + ], + + function (Adt) { + var typeAdt = Adt.generate([ + { setOf: [ 'validator', 'valueType' ] }, + { arrOf: [ 'valueType' ] }, + { objOf: [ 'fields' ] }, + { itemOf: [ 'validator' ] }, + { choiceOf: [ 'key', 'branches' ] } + + ]); + + var fieldAdt = Adt.generate([ + { field: [ 'name', 'presence', 'type' ] }, + { state: [ 'name' ] } + ]); + + return { + typeAdt: typeAdt, + fieldAdt: fieldAdt + }; + } +); +define( + 'ephox.boulder.core.ValueProcessor', + + [ + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.api.Objects', + 'ephox.boulder.combine.ResultCombine', + 'ephox.boulder.core.ObjReader', + 'ephox.boulder.core.ObjWriter', + 'ephox.boulder.core.SchemaError', + 'ephox.boulder.format.TypeTokens', + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Result', + 'ephox.katamari.api.Type' + ], + + function (FieldPresence, Objects, ResultCombine, ObjReader, ObjWriter, SchemaError, TypeTokens, Adt, Arr, Fun, Merger, Obj, Option, Result, Type) { + var adt = Adt.generate([ + { field: [ 'key', 'okey', 'presence', 'prop' ] }, + { state: [ 'okey', 'instantiator' ] } + ]); + + var output = function (okey, value) { + return adt.state(okey, Fun.constant(value)); + }; + + var snapshot = function (okey) { + return adt.state(okey, Fun.identity); + }; + + + var strictAccess = function (path, obj, key) { + // In strict mode, if it undefined, it is an error. + return ObjReader.readOptFrom(obj, key).fold(function () { + return SchemaError.missingStrict(path, key, obj); + }, Result.value); + }; + + var fallbackAccess = function (obj, key, fallbackThunk) { + var v = ObjReader.readOptFrom(obj, key).fold(function () { + return fallbackThunk(obj); + }, Fun.identity); + return Result.value(v); + }; + + var optionAccess = function (obj, key) { + return Result.value(ObjReader.readOptFrom(obj, key)); + }; + + var optionDefaultedAccess = function (obj, key, fallback) { + var opt = ObjReader.readOptFrom(obj, key).map(function (val) { + return val === true ? fallback(obj) : val; + }); + return Result.value(opt); + }; + + var cExtractOne = function (path, obj, field, strength) { + return field.fold( + function (key, okey, presence, prop) { + var bundle = function (av) { + return prop.extract(path.concat([ key ]), strength, av).map(function (res) { + return ObjWriter.wrap(okey, strength(res)); + }); + }; + + var bundleAsOption = function (optValue) { + return optValue.fold(function () { + var outcome = ObjWriter.wrap(okey, strength(Option.none())); + return Result.value(outcome); + }, function (ov) { + return prop.extract(path.concat([ key ]), strength, ov).map(function (res) { + return ObjWriter.wrap(okey, strength(Option.some(res))); + }); + }); + }; + + return (function () { + return presence.fold(function () { + return strictAccess(path, obj, key).bind(bundle); + }, function (fallbackThunk) { + return fallbackAccess(obj, key, fallbackThunk).bind(bundle); + }, function () { + return optionAccess(obj, key).bind(bundleAsOption); + }, function (fallbackThunk) { + // Defaulted option access + return optionDefaultedAccess(obj, key, fallbackThunk).bind(bundleAsOption); + }, function (baseThunk) { + var base = baseThunk(obj); + return fallbackAccess(obj, key, Fun.constant({})).map(function (v) { + return Merger.deepMerge(base, v); + }).bind(bundle); + }); + })(); + }, + function (okey, instantiator) { + var state = instantiator(obj); + return Result.value(ObjWriter.wrap(okey, strength(state))); + } + ); + }; + + var cExtract = function (path, obj, fields, strength) { + var results = Arr.map(fields, function (field) { + return cExtractOne(path, obj, field, strength); + }); + + return ResultCombine.consolidateObj(results, {}); + }; + + var value = function (validator) { + var extract = function (path, strength, val) { + return validator(val).fold(function (err) { + return SchemaError.custom(path, err); + }, Result.value); // ignore strength + }; + + var toString = function () { + return 'val'; + }; + + var toDsl = function () { + return TypeTokens.typeAdt.itemOf(validator); + }; + + return { + extract: extract, + toString: toString, + toDsl: toDsl + }; + }; + + // This is because Obj.keys can return things where the key is set to undefined. + var getSetKeys = function (obj) { + var keys = Obj.keys(obj); + return Arr.filter(keys, function (k) { + return Objects.hasKey(obj, k); + }); + }; + + var objOnly = function (fields) { + var delegate = obj(fields); + + var fieldNames = Arr.foldr(fields, function (acc, f) { + return f.fold(function (key) { + return Merger.deepMerge(acc, Objects.wrap(key, true)); + }, Fun.constant(acc)); + }, { }); + + var extract = function (path, strength, o) { + var keys = Type.isBoolean(o) ? [ ] : getSetKeys(o); + var extra = Arr.filter(keys, function (k) { + return !Objects.hasKey(fieldNames, k); + }); + + return extra.length === 0 ? delegate.extract(path, strength, o) : + SchemaError.unsupportedFields(path, extra); + }; + + return { + extract: extract, + toString: delegate.toString, + toDsl: delegate.toDsl + }; + }; + + var obj = function (fields) { + var extract = function (path, strength, o) { + return cExtract(path, o, fields, strength); + }; + + var toString = function () { + var fieldStrings = Arr.map(fields, function (field) { + return field.fold(function (key, okey, presence, prop) { + return key + ' -> ' + prop.toString(); + }, function (okey, instantiator) { + return 'state(' + okey + ')'; + }); + }); + return 'obj{\n' + fieldStrings.join('\n') + '}'; + }; + + var toDsl = function () { + return TypeTokens.typeAdt.objOf( + Arr.map(fields, function (f) { + return f.fold(function (key, okey, presence, prop) { + return TypeTokens.fieldAdt.field(key, presence, prop); + }, function (okey, instantiator) { + return TypeTokens.fieldAdt.state(okey); + }); + }) + ); + }; + + return { + extract: extract, + toString: toString, + toDsl: toDsl + }; + }; + + var arr = function (prop) { + var extract = function (path, strength, array) { + var results = Arr.map(array, function (a, i) { + return prop.extract(path.concat(['[' + i + ']' ]), strength, a); + }); + return ResultCombine.consolidateArr(results); + }; + + var toString = function () { + return 'array(' + prop.toString() + ')'; + }; + + var toDsl = function () { + return TypeTokens.typeAdt.arrOf(prop); + }; + + return { + extract: extract, + toString: toString, + toDsl: toDsl + }; + }; + + var setOf = function (validator, prop) { + var validateKeys = function (path, keys) { + return arr(value(validator)).extract(path, Fun.identity, keys); + }; + var extract = function (path, strength, o) { + // + var keys = Obj.keys(o); + return validateKeys(path, keys).bind(function (validKeys) { + var schema = Arr.map(validKeys, function (vk) { + return adt.field(vk, vk, FieldPresence.strict(), prop); + }); + + return obj(schema).extract(path, strength, o); + }); + }; + + var toString = function () { + return 'setOf(' + prop.toString() + ')'; + }; + + var toDsl = function () { + return TypeTokens.typeAdt.setOf(validator, prop); + } + + return { + extract: extract, + toString: toString, + toDsl: toDsl + }; + }; + + var anyValue = value(Result.value); + + var arrOfObj = Fun.compose(arr, obj); + + return { + anyValue: Fun.constant(anyValue), + + value: value, + obj: obj, + objOnly: objOnly, + arr: arr, + setOf: setOf, + + arrOfObj: arrOfObj, + + state: adt.state, + + field: adt.field, + + output: output, + snapshot: snapshot + }; + } +); + + +define( + 'ephox.boulder.api.FieldSchema', + + [ + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.core.ValueProcessor', + 'ephox.katamari.api.Result', + 'ephox.katamari.api.Type' + ], + + function (FieldPresence, ValueProcessor, Result, Type) { + var strict = function (key) { + return ValueProcessor.field(key, key, FieldPresence.strict(), ValueProcessor.anyValue()); + }; + + var strictOf = function (key, schema) { + return ValueProcessor.field(key, key, FieldPresence.strict(), schema); + }; + + var strictFunction = function (key) { + return ValueProcessor.field(key, key, FieldPresence.strict(), ValueProcessor.value(function (f) { + return Type.isFunction(f) ? Result.value(f) : Result.error('Not a function'); + })); + }; + + var forbid = function (key, message) { + return ValueProcessor.field( + key, + key, + FieldPresence.asOption(), + ValueProcessor.value(function (v) { + return Result.error('The field: ' + key + ' is forbidden. ' + message); + }) + ); + }; + + // TODO: Deprecate + var strictArrayOf = function (key, prop) { + return strictOf(key, prop); + }; + + + var strictObjOf = function (key, objSchema) { + return ValueProcessor.field(key, key, FieldPresence.strict(), ValueProcessor.obj(objSchema)); + }; + + var strictArrayOfObj = function (key, objFields) { + return ValueProcessor.field( + key, + key, + FieldPresence.strict(), + ValueProcessor.arrOfObj(objFields) + ); + }; + + var option = function (key) { + return ValueProcessor.field(key, key, FieldPresence.asOption(), ValueProcessor.anyValue()); + }; + + var optionOf = function (key, schema) { + return ValueProcessor.field(key, key, FieldPresence.asOption(), schema); + }; + + var optionObjOf = function (key, objSchema) { + return ValueProcessor.field(key, key, FieldPresence.asOption(), ValueProcessor.obj(objSchema)); + }; + + var optionObjOfOnly = function (key, objSchema) { + return ValueProcessor.field(key, key, FieldPresence.asOption(), ValueProcessor.objOnly(objSchema)); + }; + + var defaulted = function (key, fallback) { + return ValueProcessor.field(key, key, FieldPresence.defaulted(fallback), ValueProcessor.anyValue()); + }; + + var defaultedOf = function (key, fallback, schema) { + return ValueProcessor.field(key, key, FieldPresence.defaulted(fallback), schema); + }; + + var defaultedObjOf = function (key, fallback, objSchema) { + return ValueProcessor.field(key, key, FieldPresence.defaulted(fallback), ValueProcessor.obj(objSchema)); + }; + + var field = function (key, okey, presence, prop) { + return ValueProcessor.field(key, okey, presence, prop); + }; + + var state = function (okey, instantiator) { + return ValueProcessor.state(okey, instantiator); + }; + + return { + strict: strict, + strictOf: strictOf, + strictObjOf: strictObjOf, + strictArrayOf: strictArrayOf, + strictArrayOfObj: strictArrayOfObj, + strictFunction: strictFunction, + + forbid: forbid, + + option: option, + optionOf: optionOf, + optionObjOf: optionObjOf, + optionObjOfOnly: optionObjOfOnly, + + defaulted: defaulted, + defaultedOf: defaultedOf, + defaultedObjOf: defaultedObjOf, + + field: field, + state: state + }; + } +); +define( + 'ephox.boulder.core.ChoiceProcessor', + + [ + 'ephox.boulder.api.Objects', + 'ephox.boulder.core.SchemaError', + 'ephox.boulder.core.ValueProcessor', + 'ephox.boulder.format.TypeTokens', + 'ephox.katamari.api.Obj' + ], + + function (Objects, SchemaError, ValueProcessor, TypeTokens, Obj) { + var chooseFrom = function (path, strength, input, branches, ch) { + var fields = Objects.readOptFrom(branches, ch); + return fields.fold(function () { + return SchemaError.missingBranch(path, branches, ch); + }, function (fs) { + return ValueProcessor.obj(fs).extract(path.concat([ 'branch: ' + ch ]), strength, input); + }); + }; + + // The purpose of choose is to have a key which picks which of the schemas to follow. + // The key will index into the object of schemas: branches + var choose = function (key, branches) { + var extract = function (path, strength, input) { + var choice = Objects.readOptFrom(input, key); + return choice.fold(function () { + return SchemaError.missingKey(path, key); + }, function (chosen) { + return chooseFrom(path, strength, input, branches, chosen); + }); + }; + + var toString = function () { + return 'chooseOn(' + key + '). Possible values: ' + Obj.keys(branches); + }; + + var toDsl = function () { + return TypeTokens.typeAdt.choiceOf(key, branches); + }; + + return { + extract: extract, + toString: toString, + toDsl: toDsl + }; + }; + + return { + choose: choose + }; + } +); +define( + 'ephox.boulder.api.ValueSchema', + + [ + 'ephox.boulder.core.ChoiceProcessor', + 'ephox.boulder.core.ValueProcessor', + 'ephox.boulder.format.PrettyPrinter', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Result', + 'global!Error' + ], + + function (ChoiceProcessor, ValueProcessor, PrettyPrinter, Fun, Result, Error) { + var anyValue = ValueProcessor.value(Result.value); + + var arrOfObj = function (objFields) { + return ValueProcessor.arrOfObj(objFields); + }; + + var arrOfVal = function () { + return ValueProcessor.arr(anyValue); + }; + + var arrOf = ValueProcessor.arr; + + var objOf = ValueProcessor.obj; + + var objOfOnly = ValueProcessor.objOnly; + + var setOf = ValueProcessor.setOf; + + var valueOf = function (validator) { + return ValueProcessor.value(validator); + }; + + var extract = function (label, prop, strength, obj) { + return prop.extract([ label ], strength, obj).fold(function (errs) { + return Result.error({ + input: obj, + errors: errs + }); + }, Result.value); + }; + + var asStruct = function (label, prop, obj) { + return extract(label, prop, Fun.constant, obj); + }; + + var asRaw = function (label, prop, obj) { + return extract(label, prop, Fun.identity, obj); + }; + + var getOrDie = function (extraction) { + return extraction.fold(function (errInfo) { + // A readable version of the error. + throw new Error( + formatError(errInfo) + ); + }, Fun.identity); + }; + + var asRawOrDie = function (label, prop, obj) { + return getOrDie(asRaw(label, prop, obj)); + }; + + var asStructOrDie = function (label, prop, obj) { + return getOrDie(asStruct(label, prop, obj)); + }; + + var formatError = function (errInfo) { + return 'Errors: \n' + PrettyPrinter.formatErrors(errInfo.errors) + + '\n\nInput object: ' + PrettyPrinter.formatObj(errInfo.input); + }; + + var choose = function (key, branches) { + return ChoiceProcessor.choose(key, branches); + }; + + return { + anyValue: Fun.constant(anyValue), + + arrOfObj: arrOfObj, + arrOf: arrOf, + arrOfVal: arrOfVal, + + valueOf: valueOf, + setOf: setOf, + + objOf: objOf, + objOfOnly: objOfOnly, + + asStruct: asStruct, + asRaw: asRaw, + + asStructOrDie: asStructOrDie, + asRawOrDie: asRawOrDie, + + getOrDie: getOrDie, + formatError: formatError, + + choose: choose + }; + } +); +define( + 'ephox.alloy.construct.EventHandler', + + [ + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.sand.api.JSON', + 'ephox.katamari.api.Fun', + 'global!Array', + 'global!Error' + ], + + function (FieldSchema, Objects, ValueSchema, Type, Arr, Json, Fun, Array, Error) { + var nu = function (parts) { + if (! Objects.hasKey(parts, 'can') && !Objects.hasKey(parts, 'abort') && !Objects.hasKey(parts, 'run')) throw new Error( + 'EventHandler defined by: ' + Json.stringify(parts, null, 2) + ' does not have can, abort, or run!' + ); + return ValueSchema.asRawOrDie('Extracting event.handler', ValueSchema.objOfOnly([ + FieldSchema.defaulted('can', Fun.constant(true)), + FieldSchema.defaulted('abort', Fun.constant(false)), + FieldSchema.defaulted('run', Fun.noop) + ]), parts); + }; + + var all = function (handlers, f) { + return function () { + var args = Array.prototype.slice.call(arguments, 0); + return Arr.foldl(handlers, function (acc, handler) { + return acc && f(handler).apply(undefined, args); + }, true); + }; + }; + + var any = function (handlers, f) { + return function () { + var args = Array.prototype.slice.call(arguments, 0); + return Arr.foldl(handlers, function (acc, handler) { + return acc || f(handler).apply(undefined, args); + }, false); + }; + }; + + var read = function (handler) { + return Type.isFunction(handler) ? { + can: Fun.constant(true), + abort: Fun.constant(false), + run: handler + } : handler; + }; + + var fuse = function (handlers) { + var can = all(handlers, function (handler) { + return handler.can; + }); + + var abort = any(handlers, function (handler) { + return handler.abort; + }); + + var run = function () { + var args = Array.prototype.slice.call(arguments, 0); + Arr.each(handlers, function (handler) { + // ASSUMPTION: Return value is unimportant. + handler.run.apply(undefined, args); + }); + }; + + return nu({ + can: can, + abort: abort, + run: run + }); + }; + + return { + read: read, + fuse: fuse, + nu: nu + }; + } +); +define( + 'ephox.alloy.api.events.AlloyEvents', + + [ + 'ephox.alloy.alien.EventRoot', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.construct.EventHandler', + 'ephox.boulder.api.Objects' + ], + + function (EventRoot, AlloyTriggers, SystemEvents, EventHandler, Objects) { + var derive = Objects.wrapAll; + + var abort = function (name, predicate) { + return { + key: name, + value: EventHandler.nu({ + abort: predicate + }) + }; + }; + + var can = function (name, predicate) { + return { + key: name, + value: EventHandler.nu({ + can: predicate + }) + }; + }; + + var preventDefault = function (name) { + return { + key: name, + value: EventHandler.nu({ + run: function (component, simulatedEvent) { + simulatedEvent.event().prevent(); + } + }) + }; + }; + + var run = function (name, handler) { + return { + key: name, + value: EventHandler.nu({ + run: handler + }) + }; + }; + + var runActionExtra = function (name, action, extra) { + return { + key: name, + value: EventHandler.nu({ + run: function (component) { + action.apply(undefined, [ component ].concat(extra)); + } + }) + }; + }; + + var runOnName = function (name) { + return function (handler) { + return run(name, handler); + }; + }; + + var runOnSourceName = function (name) { + return function (handler) { + return { + key: name, + value: EventHandler.nu({ + run: function (component, simulatedEvent) { + if (EventRoot.isSource(component, simulatedEvent)) handler(component, simulatedEvent); + } + }) + }; + }; + }; + + var redirectToUid = function (name, uid) { + return run(name, function (component, simulatedEvent) { + component.getSystem().getByUid(uid).each(function (redirectee) { + AlloyTriggers.dispatchEvent(redirectee, redirectee.element(), name, simulatedEvent); + }); + }); + }; + + var redirectToPart = function (name, detail, partName) { + var uid = detail.partUids()[partName]; + return redirectToUid(name, uid); + }; + + var runWithTarget = function (name, f) { + return run(name, function (component, simulatedEvent) { + component.getSystem().getByDom(simulatedEvent.event().target()).each(function (target) { + f(component, target, simulatedEvent); + }); + }); + }; + + var cutter = function (name) { + return run(name, function (component, simulatedEvent) { + simulatedEvent.cut(); + }); + }; + + var stopper = function (name) { + return run(name, function (component, simulatedEvent) { + simulatedEvent.stop(); + }); + }; + + return { + derive: derive, + run: run, + preventDefault: preventDefault, + runActionExtra: runActionExtra, + runOnAttached: runOnSourceName(SystemEvents.attachedToDom()), + runOnDetached: runOnSourceName(SystemEvents.detachedFromDom()), + runOnInit: runOnSourceName(SystemEvents.systemInit()), + runOnExecute: runOnName(SystemEvents.execute()), + + redirectToUid: redirectToUid, + redirectToPart: redirectToPart, + runWithTarget: runWithTarget, + abort: abort, + can: can, + cutter: cutter, + stopper: stopper + }; + } +); + +define( + 'ephox.alloy.debugging.FunctionAnnotator', + + [ + 'ephox.katamari.api.Option' + ], + + function (Option) { + var markAsBehaviourApi = function (f, apiName, apiFunction) { + return f; + }; + + var markAsExtraApi = function (f, extraName) { + return f; + }; + + var markAsSketchApi = function (f, apiFunction) { + return f; + }; + + var getAnnotation = Option.none; + + return { + markAsBehaviourApi: markAsBehaviourApi, + markAsExtraApi: markAsExtraApi, + markAsSketchApi: markAsSketchApi, + getAnnotation: getAnnotation + }; + } +); + +define( + 'ephox.katamari.data.Immutable', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'global!Array', + 'global!Error' + ], + + function (Arr, Fun, Array, Error) { + return function () { + var fields = arguments; + return function(/* values */) { + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var values = new Array(arguments.length); + for (var i = 0; i < values.length; i++) values[i] = arguments[i]; + + if (fields.length !== values.length) + throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments'); + + var struct = {}; + Arr.each(fields, function (name, i) { + struct[name] = Fun.constant(values[i]); + }); + return struct; + }; + }; + } +); + +define( + 'ephox.katamari.util.BagUtils', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Type', + 'global!Error' + ], + + function (Arr, Type, Error) { + var sort = function (arr) { + return arr.slice(0).sort(); + }; + + var reqMessage = function (required, keys) { + throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.'); + }; + + var unsuppMessage = function (unsupported) { + throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', ')); + }; + + var validateStrArr = function (label, array) { + if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.'); + Arr.each(array, function (a) { + if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.'); + }); + }; + + var invalidTypeMessage = function (incorrect, type) { + throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.'); + }; + + var checkDupes = function (everything) { + var sorted = sort(everything); + var dupe = Arr.find(sorted, function (s, i) { + return i < sorted.length -1 && s === sorted[i + 1]; + }); + + dupe.each(function (d) { + throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].'); + }); + }; + + return { + sort: sort, + reqMessage: reqMessage, + unsuppMessage: unsuppMessage, + validateStrArr: validateStrArr, + invalidTypeMessage: invalidTypeMessage, + checkDupes: checkDupes + }; + } +); +define( + 'ephox.katamari.data.MixedBag', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.util.BagUtils', + 'global!Error', + 'global!Object' + ], + + function (Arr, Fun, Obj, Option, BagUtils, Error, Object) { + + return function (required, optional) { + var everything = required.concat(optional); + if (everything.length === 0) throw new Error('You must specify at least one required or optional field.'); + + BagUtils.validateStrArr('required', required); + BagUtils.validateStrArr('optional', optional); + + BagUtils.checkDupes(everything); + + return function (obj) { + var keys = Obj.keys(obj); + + // Ensure all required keys are present. + var allReqd = Arr.forall(required, function (req) { + return Arr.contains(keys, req); + }); + + if (! allReqd) BagUtils.reqMessage(required, keys); + + var unsupported = Arr.filter(keys, function (key) { + return !Arr.contains(everything, key); + }); + + if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); + + var r = {}; + Arr.each(required, function (req) { + r[req] = Fun.constant(obj[req]); + }); + + Arr.each(optional, function (opt) { + r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none()); + }); + + return r; + }; + }; + } +); +define( + 'ephox.katamari.api.Struct', + + [ + 'ephox.katamari.data.Immutable', + 'ephox.katamari.data.MixedBag' + ], + + function (Immutable, MixedBag) { + return { + immutable: Immutable, + immutableBag: MixedBag + }; + } +); + +define( + 'ephox.alloy.dom.DomDefinition', + + [ + 'ephox.sand.api.JSON', + 'ephox.katamari.api.Struct', + 'global!String' + ], + + function (Json, Struct, String) { + var nu = Struct.immutableBag([ 'tag' ], [ + 'classes', + 'attributes', + 'styles', + 'value', + 'innerHtml', + 'domChildren', + 'defChildren' + ]); + + var defToStr = function (defn) { + var raw = defToRaw(defn); + return Json.stringify(raw, null, 2); + }; + + var defToRaw = function (defn) { + return { + tag: defn.tag(), + classes: defn.classes().getOr([ ]), + attributes: defn.attributes().getOr({ }), + styles: defn.styles().getOr({ }), + value: defn.value().getOr(''), + innerHtml: defn.innerHtml().getOr(''), + defChildren: defn.defChildren().getOr(''), + domChildren: defn.domChildren().fold(function () { + return ''; + }, function (children) { + return children.length === 0 ? '0 children, but still specified' : String(children.length); + }) + }; + }; + + return { + nu: nu, + defToStr: defToStr, + defToRaw: defToRaw + }; + } +); +define( + 'ephox.alloy.dom.DomModification', + + [ + 'ephox.alloy.dom.DomDefinition', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Merger', + 'ephox.sand.api.JSON', + 'ephox.katamari.api.Struct' + ], + + function (DomDefinition, Objects, Arr, Obj, Merger, Json, Struct) { + var fields = [ + 'classes', + 'attributes', + 'styles', + 'value', + 'innerHtml', + 'defChildren', + 'domChildren' + ]; + // Maybe we'll need to allow add/remove + var nu = Struct.immutableBag([ ], fields); + + + var derive = function (settings) { + var r = { }; + var keys = Obj.keys(settings); + Arr.each(keys, function (key) { + settings[key].each(function (v) { + r[key] = v; + }); + }); + + return nu(r); + }; + + var modToStr = function (mod) { + var raw = modToRaw(mod); + return Json.stringify(raw, null, 2); + }; + + var modToRaw = function (mod) { + return { + classes: mod.classes().getOr(''), + attributes: mod.attributes().getOr(''), + styles: mod.styles().getOr(''), + value: mod.value().getOr(''), + innerHtml: mod.innerHtml().getOr(''), + defChildren: mod.defChildren().getOr(''), + domChildren: mod.domChildren().fold(function () { + return ''; + }, function (children) { + return children.length === 0 ? '0 children, but still specified' : String(children.length); + }) + }; + }; + + var clashingOptArrays = function (key, oArr1, oArr2) { + return oArr1.fold(function () { + return oArr2.fold(function () { + return { }; + }, function (arr2) { + return Objects.wrap(key, arr2); + }); + }, function (arr1) { + return oArr2.fold(function () { + return Objects.wrap(key, arr1); + }, function (arr2) { + return Objects.wrap(key, arr2); + }); + }); + }; + + var merge = function (defnA, mod) { + var raw = Merger.deepMerge( + { + tag: defnA.tag(), + classes: mod.classes().getOr([ ]).concat(defnA.classes().getOr([ ])), + attributes: Merger.merge( + defnA.attributes().getOr({}), + mod.attributes().getOr({}) + ), + styles: Merger.merge( + defnA.styles().getOr({}), + mod.styles().getOr({}) + ) + }, + mod.innerHtml().or(defnA.innerHtml()).map(function (innerHtml) { + return Objects.wrap('innerHtml', innerHtml); + }).getOr({ }), + + clashingOptArrays('domChildren', mod.domChildren(), defnA.domChildren()), + clashingOptArrays('defChildren', mod.defChildren(), defnA.defChildren()), + + mod.value().or(defnA.value()).map(function (value) { + return Objects.wrap('value', value); + }).getOr({ }) + ); + + return DomDefinition.nu(raw); + }; + + return { + nu: nu, + derive: derive, + + merge: merge, + // combine: combine, + modToStr: modToStr, + modToRaw: modToRaw + }; + } +); +define( + 'ephox.alloy.behaviour.common.Behaviour', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.debugging.FunctionAnnotator', + 'ephox.alloy.dom.DomModification', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Thunk', + 'global!Array', + 'global!console', + 'global!Error' + ], + + function (AlloyEvents, FunctionAnnotator, DomModification, FieldSchema, Objects, ValueSchema, Fun, Merger, Obj, Option, Thunk, Array, console, Error) { + var executeEvent = function (bConfig, bState, executor) { + return AlloyEvents.runOnExecute(function (component) { + executor(component, bConfig, bState); + }); + }; + + var loadEvent = function (bConfig, bState, f) { + return AlloyEvents.runOnInit(function (component, simulatedEvent) { + f(component, bConfig, bState); + }); + }; + + var create = function (schema, name, active, apis, extra, state) { + var configSchema = ValueSchema.objOfOnly(schema); + var schemaSchema = FieldSchema.optionObjOf(name, [ + FieldSchema.optionObjOfOnly('config', schema) + ]); + return doCreate(configSchema, schemaSchema, name, active, apis, extra, state); + }; + + var createModes = function (modes, name, active, apis, extra, state) { + var configSchema = modes; + var schemaSchema = FieldSchema.optionObjOf(name, [ + FieldSchema.optionOf('config', modes) + ]); + return doCreate(configSchema, schemaSchema, name, active, apis, extra, state); + }; + + var wrapApi = function (bName, apiFunction, apiName) { + var f = function (component) { + var args = arguments; + return component.config({ + name: Fun.constant(bName) + }).fold( + function () { + throw new Error('We could not find any behaviour configuration for: ' + bName + '. Using API: ' + apiName); + }, + function (info) { + var rest = Array.prototype.slice.call(args, 1); + return apiFunction.apply(undefined, [ component, info.config, info.state ].concat(rest)); + } + ); + }; + return FunctionAnnotator.markAsBehaviourApi(f, apiName, apiFunction); + }; + + // I think the "revoke" idea is fragile at best. + var revokeBehaviour = function (name) { + return { + key: name, + value: undefined + }; + }; + + var doCreate = function (configSchema, schemaSchema, name, active, apis, extra, state) { + var getConfig = function (info) { + return Objects.hasKey(info, name) ? info[name]() : Option.none(); + }; + + var wrappedApis = Obj.map(apis, function (apiF, apiName) { + return wrapApi(name, apiF, apiName); + }); + + var wrappedExtra = Obj.map(extra, function (extraF, extraName) { + return FunctionAnnotator.markAsExtraApi(extraF, extraName); + }); + + var me = Merger.deepMerge( + wrappedExtra, + wrappedApis, + { + revoke: Fun.curry(revokeBehaviour, name), + config: function (spec) { + var prepared = ValueSchema.asStructOrDie(name + '-config', configSchema, spec); + + return { + key: name, + value: { + config: prepared, + me: me, + configAsRaw: Thunk.cached(function () { + return ValueSchema.asRawOrDie(name + '-config', configSchema, spec); + }), + initialConfig: spec, + state: state + } + }; + }, + + schema: function () { + return schemaSchema; + }, + + exhibit: function (info, base) { + return getConfig(info).bind(function (behaviourInfo) { + return Objects.readOptFrom(active, 'exhibit').map(function (exhibitor) { + return exhibitor(base, behaviourInfo.config, behaviourInfo.state); + }); + }).getOr(DomModification.nu({ })); + }, + + name: function () { + return name; + }, + + handlers: function (info) { + return getConfig(info).bind(function (behaviourInfo) { + return Objects.readOptFrom(active, 'events').map(function (events) { + return events(behaviourInfo.config, behaviourInfo.state); + }); + }).getOr({ }); + } + } + ); + + return me; + }; + + return { + executeEvent: executeEvent, + loadEvent: loadEvent, + create: create, + createModes: createModes + }; + } +); +define( + 'ephox.katamari.api.Contracts', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Type', + 'ephox.katamari.util.BagUtils', + 'global!Error' + ], + + function (Arr, Fun, Obj, Type, BagUtils, Error) { + // Ensure that the object has all required fields. They must be functions. + var base = function (handleUnsupported, required) { + return baseWith(handleUnsupported, required, { + validate: Type.isFunction, + label: 'function' + }); + }; + + // Ensure that the object has all required fields. They must satisy predicates. + var baseWith = function (handleUnsupported, required, pred) { + if (required.length === 0) throw new Error('You must specify at least one required field.'); + + BagUtils.validateStrArr('required', required); + + BagUtils.checkDupes(required); + + return function (obj) { + var keys = Obj.keys(obj); + + // Ensure all required keys are present. + var allReqd = Arr.forall(required, function (req) { + return Arr.contains(keys, req); + }); + + if (! allReqd) BagUtils.reqMessage(required, keys); + + handleUnsupported(required, keys); + + var invalidKeys = Arr.filter(required, function (key) { + return !pred.validate(obj[key], key); + }); + + if (invalidKeys.length > 0) BagUtils.invalidTypeMessage(invalidKeys, pred.label); + + return obj; + }; + }; + + var handleExact = function (required, keys) { + var unsupported = Arr.filter(keys, function (key) { + return !Arr.contains(required, key); + }); + + if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); + }; + + var allowExtra = Fun.noop; + + return { + exactly: Fun.curry(base, handleExact), + ensure: Fun.curry(base, allowExtra), + ensureWith: Fun.curry(baseWith, allowExtra) + }; + } +); +define( + 'ephox.alloy.behaviour.common.BehaviourState', + + [ + 'ephox.katamari.api.Contracts' + ], + + function (Contracts) { + return Contracts.ensure([ + 'readState' + ]); + } +); + +defineGlobal("global!Math", Math); +define( + 'ephox.alloy.behaviour.common.NoState', + + [ + 'ephox.alloy.behaviour.common.BehaviourState', + 'global!Math' + ], + + function (BehaviourState, Math) { + var init = function () { + return BehaviourState({ + readState: function () { + return 'No State required'; + } + }); + }; + + return { + init: init + }; + } +); + +define( + 'ephox.alloy.api.behaviour.Behaviour', + + [ + 'ephox.alloy.behaviour.common.Behaviour', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun' + ], + + function (Behaviour, NoState, FieldSchema, Objects, ValueSchema, Fun) { + var derive = function (capabilities) { + return Objects.wrapAll(capabilities); + }; + + var simpleSchema = ValueSchema.objOfOnly([ + FieldSchema.strict('fields'), + FieldSchema.strict('name'), + FieldSchema.defaulted('active', { }), + FieldSchema.defaulted('apis', { }), + FieldSchema.defaulted('extra', { }), + FieldSchema.defaulted('state', NoState) + ]); + + var create = function (data) { + var value = ValueSchema.asRawOrDie('Creating behaviour: ' + data.name, simpleSchema, data); + return Behaviour.create(value.fields, value.name, value.active, value.apis, value.extra, value.state); + }; + + var modeSchema = ValueSchema.objOfOnly([ + FieldSchema.strict('branchKey'), + FieldSchema.strict('branches'), + FieldSchema.strict('name'), + FieldSchema.defaulted('active', { }), + FieldSchema.defaulted('apis', { }), + FieldSchema.defaulted('extra', { }), + FieldSchema.defaulted('state', NoState) + ]); + + var createModes = function (data) { + var value = ValueSchema.asRawOrDie('Creating behaviour: ' + data.name, modeSchema, data); + return Behaviour.createModes( + ValueSchema.choose(value.branchKey, value.branches), + value.name, value.active, value.apis, value.extra, value.state + ); + }; + + return { + derive: derive, + revoke: Fun.constant(undefined), + noActive: Fun.constant({ }), + noApis: Fun.constant({ }), + noExtra: Fun.constant({ }), + noState: Fun.constant(NoState), + create: create, + createModes: createModes + }; + } +); +define( + 'ephox.sugar.api.properties.Toggler', + + [ + ], + + function () { + return function (turnOff, turnOn, initial) { + var active = initial || false; + + var on = function () { + turnOn(); + active = true; + }; + + var off = function () { + turnOff(); + active = false; + }; + + var toggle = function () { + var f = active ? off : on; + f(); + }; + + var isOn = function () { + return active; + }; + + return { + on: on, + off: off, + toggle: toggle, + isOn: isOn + }; + }; + } +); + +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.properties.AttrList', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.properties.Attr' + ], + + function (Arr, Attr) { + // Methods for handling attributes that contain a list of values
    + var read = function (element, attr) { + var value = Attr.get(element, attr); + return value === undefined || value === '' ? [] : value.split(' '); + }; + + var add = function (element, attr, id) { + var old = read(element, attr); + var nu = old.concat([id]); + Attr.set(element, attr, nu.join(' ')); + }; + + var remove = function (element, attr, id) { + var nu = Arr.filter(read(element, attr), function (v) { + return v !== id; + }); + if (nu.length > 0) Attr.set(element, attr, nu.join(' ')); + else Attr.remove(element, attr); + }; + + return { + read: read, + add: add, + remove: remove + }; + } +); +define( + 'ephox.sugar.impl.ClassList', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.properties.AttrList' + ], + + function (Arr, AttrList) { + + var supports = function (element) { + // IE11 Can return undefined for a classList on elements such as math, so we make sure it's not undefined before attempting to use it. + return element.dom().classList !== undefined; + }; + + var get = function (element) { + return AttrList.read(element, 'class'); + }; + + var add = function (element, clazz) { + return AttrList.add(element, 'class', clazz); + }; + + var remove = function (element, clazz) { + return AttrList.remove(element, 'class', clazz); + }; + + var toggle = function (element, clazz) { + if (Arr.contains(get(element), clazz)) { + remove(element, clazz); + } else { + add(element, clazz); + } + }; + + return { + get: get, + add: add, + remove: remove, + toggle: toggle, + supports: supports + }; + } +); +define( + 'ephox.sugar.api.properties.Class', + + [ + 'ephox.sugar.api.properties.Toggler', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.impl.ClassList' + ], + + function (Toggler, Attr, ClassList) { + /* + * ClassList is IE10 minimum: + * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList + * + * Note that IE doesn't support the second argument to toggle (at all). + * If it did, the toggler could be better. + */ + + var add = function (element, clazz) { + if (ClassList.supports(element)) element.dom().classList.add(clazz); + else ClassList.add(element, clazz); + }; + + var cleanClass = function (element) { + var classList = ClassList.supports(element) ? element.dom().classList : ClassList.get(element); + // classList is a "live list", so this is up to date already + if (classList.length === 0) { + // No more classes left, remove the class attribute as well + Attr.remove(element, 'class'); + } + }; + + var remove = function (element, clazz) { + if (ClassList.supports(element)) { + var classList = element.dom().classList; + classList.remove(clazz); + } else + ClassList.remove(element, clazz); + + cleanClass(element); + }; + + var toggle = function (element, clazz) { + return ClassList.supports(element) ? element.dom().classList.toggle(clazz) : + ClassList.toggle(element, clazz); + }; + + var toggler = function (element, clazz) { + var hasClasslist = ClassList.supports(element); + var classList = element.dom().classList; + var off = function () { + if (hasClasslist) classList.remove(clazz); + else ClassList.remove(element, clazz); + }; + var on = function () { + if (hasClasslist) classList.add(clazz); + else ClassList.add(element, clazz); + }; + return Toggler(off, on, has(element, clazz)); + }; + + var has = function (element, clazz) { + // Cereal has a nasty habit of calling this with a text node >.< + return ClassList.supports(element) && element.dom().classList.contains(clazz); + }; + + // set deleted, risks bad performance. Be deterministic. + + return { + add: add, + remove: remove, + toggle: toggle, + toggler: toggler, + has: has + }; + } +); + +define( + 'ephox.alloy.behaviour.swapping.SwapApis', + + [ + 'ephox.sugar.api.properties.Class' + ], + + function (Class) { + var swap = function (element, addCls, removeCls) { + Class.remove(element, removeCls); + Class.add(element, addCls); + }; + + var toAlpha = function (component, swapConfig, swapState) { + swap(component.element(), swapConfig.alpha(), swapConfig.omega()); + }; + + var toOmega = function (component, swapConfig, swapState) { + swap(component.element(), swapConfig.omega(), swapConfig.alpha()); + }; + + var clear = function (component, swapConfig, swapState) { + Class.remove(component.element(), swapConfig.alpha()); + Class.remove(component.element(), swapConfig.omega()); + }; + + var isAlpha = function (component, swapConfig, swapState) { + return Class.has(component.element(), swapConfig.alpha()); + }; + + var isOmega = function (component, swapConfig, swapState) { + return Class.has(component.element(), swapConfig.omega()); + }; + + return { + toAlpha: toAlpha, + toOmega: toOmega, + isAlpha: isAlpha, + isOmega: isOmega, + clear: clear + }; + } +); + +define( + 'ephox.alloy.behaviour.swapping.SwapSchema', + + [ + 'ephox.boulder.api.FieldSchema' + ], + + function (FieldSchema) { + return [ + FieldSchema.strict('alpha'), + FieldSchema.strict('omega') + ]; + } +); + +define( + 'ephox.alloy.api.behaviour.Swapping', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.swapping.SwapApis', + 'ephox.alloy.behaviour.swapping.SwapSchema' + ], + + function (Behaviour, SwapApis, SwapSchema) { + return Behaviour.create({ + fields: SwapSchema, + name: 'swapping', + apis: SwapApis + }); + } +); + +define( + 'ephox.sugar.alien.Recurse', + + [ + + ], + + function () { + /** + * Applies f repeatedly until it completes (by returning Option.none()). + * + * Normally would just use recursion, but JavaScript lacks tail call optimisation. + * + * This is what recursion looks like when manually unravelled :) + */ + var toArray = function (target, f) { + var r = []; + + var recurse = function (e) { + r.push(e); + return f(e); + }; + + var cur = f(target); + do { + cur = cur.bind(recurse); + } while (cur.isSome()); + + return r; + }; + + return { + toArray: toArray + }; + } +); +define( + 'ephox.sugar.api.search.Traverse', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Struct', + 'ephox.sugar.alien.Recurse', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element' + ], + + function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) { + // The document associated with the current element + var owner = function (element) { + return Element.fromDom(element.dom().ownerDocument); + }; + + var documentElement = function (element) { + // TODO: Avoid unnecessary wrap/unwrap here + var doc = owner(element); + return Element.fromDom(doc.dom().documentElement); + }; + + // The window element associated with the element + var defaultView = function (element) { + var el = element.dom(); + var defaultView = el.ownerDocument.defaultView; + return Element.fromDom(defaultView); + }; + + var parent = function (element) { + var dom = element.dom(); + return Option.from(dom.parentNode).map(Element.fromDom); + }; + + var findIndex = function (element) { + return parent(element).bind(function (p) { + // TODO: Refactor out children so we can avoid the constant unwrapping + var kin = children(p); + return Arr.findIndex(kin, function (elem) { + return Compare.eq(element, elem); + }); + }); + }; + + var parents = function (element, isRoot) { + var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); + + // This is used a *lot* so it needs to be performant, not recursive + var dom = element.dom(); + var ret = []; + + while (dom.parentNode !== null && dom.parentNode !== undefined) { + var rawParent = dom.parentNode; + var parent = Element.fromDom(rawParent); + ret.push(parent); + + if (stop(parent) === true) break; + else dom = rawParent; + } + return ret; + }; + + var siblings = function (element) { + // TODO: Refactor out children so we can just not add self instead of filtering afterwards + var filterSelf = function (elements) { + return Arr.filter(elements, function (x) { + return !Compare.eq(element, x); + }); + }; + + return parent(element).map(children).map(filterSelf).getOr([]); + }; + + var offsetParent = function (element) { + var dom = element.dom(); + return Option.from(dom.offsetParent).map(Element.fromDom); + }; + + var prevSibling = function (element) { + var dom = element.dom(); + return Option.from(dom.previousSibling).map(Element.fromDom); + }; + + var nextSibling = function (element) { + var dom = element.dom(); + return Option.from(dom.nextSibling).map(Element.fromDom); + }; + + var prevSiblings = function (element) { + // This one needs to be reversed, so they're still in DOM order + return Arr.reverse(Recurse.toArray(element, prevSibling)); + }; + + var nextSiblings = function (element) { + return Recurse.toArray(element, nextSibling); + }; + + var children = function (element) { + var dom = element.dom(); + return Arr.map(dom.childNodes, Element.fromDom); + }; + + var child = function (element, index) { + var children = element.dom().childNodes; + return Option.from(children[index]).map(Element.fromDom); + }; + + var firstChild = function (element) { + return child(element, 0); + }; + + var lastChild = function (element) { + return child(element, element.dom().childNodes.length - 1); + }; + + var childNodesCount = function (element, index) { + return element.dom().childNodes.length; + }; + + var spot = Struct.immutable('element', 'offset'); + var leaf = function (element, offset) { + var cs = children(element); + return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); + }; + + return { + owner: owner, + defaultView: defaultView, + documentElement: documentElement, + parent: parent, + findIndex: findIndex, + parents: parents, + siblings: siblings, + prevSibling: prevSibling, + offsetParent: offsetParent, + prevSiblings: prevSiblings, + nextSibling: nextSibling, + nextSiblings: nextSiblings, + children: children, + child: child, + firstChild: firstChild, + lastChild: lastChild, + childNodesCount: childNodesCount, + leaf: leaf + }; + } +); + +define( + 'ephox.sugar.api.dom.Insert', + + [ + 'ephox.sugar.api.search.Traverse' + ], + + function (Traverse) { + var before = function (marker, element) { + var parent = Traverse.parent(marker); + parent.each(function (v) { + v.dom().insertBefore(element.dom(), marker.dom()); + }); + }; + + var after = function (marker, element) { + var sibling = Traverse.nextSibling(marker); + sibling.fold(function () { + var parent = Traverse.parent(marker); + parent.each(function (v) { + append(v, element); + }); + }, function (v) { + before(v, element); + }); + }; + + var prepend = function (parent, element) { + var firstChild = Traverse.firstChild(parent); + firstChild.fold(function () { + append(parent, element); + }, function (v) { + parent.dom().insertBefore(element.dom(), v.dom()); + }); + }; + + var append = function (parent, element) { + parent.dom().appendChild(element.dom()); + }; + + var appendAt = function (parent, element, index) { + Traverse.child(parent, index).fold(function () { + append(parent, element); + }, function (v) { + before(v, element); + }); + }; + + var wrap = function (element, wrapper) { + before(element, wrapper); + append(wrapper, element); + }; + + return { + before: before, + after: after, + prepend: prepend, + append: append, + appendAt: appendAt, + wrap: wrap + }; + } +); + +define( + 'ephox.sugar.api.dom.InsertAll', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.Insert' + ], + + function (Arr, Insert) { + var before = function (marker, elements) { + Arr.each(elements, function (x) { + Insert.before(marker, x); + }); + }; + + var after = function (marker, elements) { + Arr.each(elements, function (x, i) { + var e = i === 0 ? marker : elements[i - 1]; + Insert.after(e, x); + }); + }; + + var prepend = function (parent, elements) { + Arr.each(elements.slice().reverse(), function (x) { + Insert.prepend(parent, x); + }); + }; + + var append = function (parent, elements) { + Arr.each(elements, function (x) { + Insert.append(parent, x); + }); + }; + + return { + before: before, + after: after, + prepend: prepend, + append: append + }; + } +); + +define( + 'ephox.sugar.api.dom.Remove', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.search.Traverse' + ], + + function (Arr, InsertAll, Traverse) { + var empty = function (element) { + // shortcut "empty node" trick. Requires IE 9. + element.dom().textContent = ''; + + // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general + // than removing every child node manually. + // The following is (probably) safe for performance as 99.9% of the time the trick works and + // Traverse.children will return an empty array. + Arr.each(Traverse.children(element), function (rogue) { + remove(rogue); + }); + }; + + var remove = function (element) { + var dom = element.dom(); + if (dom.parentNode !== null) + dom.parentNode.removeChild(dom); + }; + + var unwrap = function (wrapper) { + var children = Traverse.children(wrapper); + if (children.length > 0) + InsertAll.before(wrapper, children); + remove(wrapper); + }; + + return { + empty: empty, + remove: remove, + unwrap: unwrap + }; + } +); + +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.alloy.api.system.Attachment', + + [ + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.search.Traverse' + ], + + function (AlloyTriggers, SystemEvents, Arr, Option, Insert, Remove, Body, Traverse) { + var fireDetaching = function (component) { + AlloyTriggers.emit(component, SystemEvents.detachedFromDom()); + var children = component.components(); + Arr.each(children, fireDetaching); + }; + + var fireAttaching = function (component) { + var children = component.components(); + Arr.each(children, fireAttaching); + AlloyTriggers.emit(component, SystemEvents.attachedToDom()); + }; + + var attach = function (parent, child) { + attachWith(parent, child, Insert.append); + }; + + var attachWith = function (parent, child, insertion) { + parent.getSystem().addToWorld(child); + insertion(parent.element(), child.element()); + if (Body.inBody(parent.element())) fireAttaching(child); + parent.syncComponents(); + }; + + var doDetach = function (component) { + fireDetaching(component); + Remove.remove(component.element()); + component.getSystem().removeFromWorld(component); + }; + + var detach = function (component) { + var parent = Traverse.parent(component.element()).bind(function (p) { + return component.getSystem().getByDom(p).fold(Option.none, Option.some); + }); + + doDetach(component); + parent.each(function (p) { + p.syncComponents(); + }); + }; + + var detachChildren = function (component) { + // This will not detach the component, but will detach its children and sync at the end. + var subs = component.components(); + Arr.each(subs, doDetach); + // Clear the component also. + Remove.empty(component.element()); + component.syncComponents(); + }; + + var attachSystem = function (element, guiSystem) { + Insert.append(element, guiSystem.element()); + var children = Traverse.children(guiSystem.element()); + Arr.each(children, function (child) { + guiSystem.getByDom(child).each(fireAttaching); + }); + }; + + var detachSystem = function (guiSystem) { + var children = Traverse.children(guiSystem.element()); + Arr.each(children, function (child) { + guiSystem.getByDom(child).each(fireDetaching); + }); + Remove.remove(guiSystem.element()); + }; + + return { + attach: attach, + attachWith: attachWith, + detach: detach, + detachChildren: detachChildren, + + attachSystem: attachSystem, + detachSystem: detachSystem + }; + } +); + +define( + 'ephox.sugar.api.node.Elements', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse', + 'global!document' + ], + + function (Arr, Element, Traverse, document) { + var fromHtml = function (html, scope) { + var doc = scope || document; + var div = doc.createElement('div'); + div.innerHTML = html; + return Traverse.children(Element.fromDom(div)); + }; + + var fromTags = function (tags, scope) { + return Arr.map(tags, function (x) { + return Element.fromTag(x, scope); + }); + }; + + var fromText = function (texts, scope) { + return Arr.map(texts, function (x) { + return Element.fromText(x, scope); + }); + }; + + var fromDom = function (nodes) { + return Arr.map(nodes, Element.fromDom); + }; + + return { + fromHtml: fromHtml, + fromTags: fromTags, + fromText: fromText, + fromDom: fromDom + }; + } +); + +define( + 'ephox.sugar.api.properties.Html', + + [ + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Elements', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.search.Traverse' + ], + + function (Element, Elements, Insert, InsertAll, Remove, Traverse) { + var get = function (element) { + return element.dom().innerHTML; + }; + + var set = function (element, content) { + var owner = Traverse.owner(element); + var docDom = owner.dom(); + + // FireFox has *terrible* performance when using innerHTML = x + var fragment = Element.fromDom(docDom.createDocumentFragment()); + var contentElements = Elements.fromHtml(content, docDom); + InsertAll.append(fragment, contentElements); + + Remove.empty(element); + Insert.append(element, fragment); + }; + + var getOuter = function (element) { + var container = Element.fromTag('div'); + var clone = Element.fromDom(element.dom().cloneNode(true)); + Insert.append(container, clone); + return get(container); + }; + + return { + get: get, + set: set, + getOuter: getOuter + }; + } +); + +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.alloy.alien.Truncate', + + [ + 'ephox.sugar.api.properties.Html', + 'ephox.sugar.api.dom.Replication' + ], + + function (Html, Replication) { + var getHtml = function (element) { + var clone = Replication.shallow(element); + return Html.getOuter(clone); + }; + + return { + getHtml: getHtml + }; + } +); +define( + 'ephox.alloy.log.AlloyLogger', + + [ + 'ephox.alloy.alien.Truncate' + ], + + function (Truncate) { + var element = function (elem) { + return Truncate.getHtml(elem); + }; + + return { + element: element + }; + } +); +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 + }; + } +); + +define( + 'ephox.alloy.debugging.Debugging', + + [ + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.log.AlloyLogger', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Options', + 'global!console', + 'global!Error' + ], + + + function (SystemEvents, AlloyLogger, Objects, Arr, Fun, Obj, Options, console, Error) { + var unknown = 'unknown'; + var debugging = true; + + var CHROME_INSPECTOR_GLOBAL = '__CHROME_INSPECTOR_CONNECTION_TO_ALLOY__'; + + var eventsMonitored = [ ]; + + // Ignore these files in the error stack + var path = [ + 'alloy/data/Fields', + 'alloy/debugging/Debugging' + ]; + + var getTrace = function () { + if (debugging === false) return unknown; + var err = new Error(); + if (err.stack !== undefined) { + var lines = err.stack.split('\n'); + return Arr.find(lines, function (line) { + return line.indexOf('alloy') > 0 && !Arr.exists(path, function (p) { return line.indexOf(p) > -1; }); + }).getOr(unknown); + } else { + return unknown; + } + }; + + var logHandler = function (label, handlerName, trace) { + // if (debugging) console.log(label + ' [' + handlerName + ']', trace); + }; + + var ignoreEvent = { + logEventCut: Fun.noop, + logEventStopped: Fun.noop, + logNoParent: Fun.noop, + logEventNoHandlers: Fun.noop, + logEventResponse: Fun.noop, + write: Fun.noop + }; + + var monitorEvent = function (eventName, initialTarget, f) { + var logger = debugging && (eventsMonitored === '*' || Arr.contains(eventsMonitored, eventName)) ? (function () { + var sequence = [ ]; + + return { + logEventCut: function (name, target, purpose) { + sequence.push({ outcome: 'cut', target: target, purpose: purpose }); + }, + logEventStopped: function (name, target, purpose) { + sequence.push({ outcome: 'stopped', target: target, purpose: purpose }); + }, + logNoParent: function (name, target, purpose) { + sequence.push({ outcome: 'no-parent', target: target, purpose: purpose }); + }, + logEventNoHandlers: function (name, target) { + sequence.push({ outcome: 'no-handlers-left', target: target }); + }, + logEventResponse: function (name, target, purpose) { + sequence.push({ outcome: 'response', purpose: purpose, target: target }); + }, + write: function () { + if (Arr.contains([ 'mousemove', 'mouseover', 'mouseout', SystemEvents.systemInit() ], eventName)) return; + console.log(eventName, { + event: eventName, + target: initialTarget.dom(), + sequence: Arr.map(sequence, function (s) { + if (! Arr.contains([ 'cut', 'stopped', 'response' ], s.outcome)) return s.outcome; + else return '{' + s.purpose + '} ' + s.outcome + ' at (' + AlloyLogger.element(s.target) + ')'; + }) + }); + } + }; + })() : ignoreEvent; + + var output = f(logger); + logger.write(); + return output; + }; + + var inspectorInfo = function (comp) { + var go = function (c) { + var cSpec = c.spec(); + + return { + '(original.spec)': cSpec, + '(dom.ref)': c.element().dom(), + '(element)': AlloyLogger.element(c.element()), + '(initComponents)': Arr.map(cSpec.components !== undefined ? cSpec.components : [ ], go), + '(components)': Arr.map(c.components(), go), + '(bound.events)': Obj.mapToArray(c.events(), function (v, k) { + return [ k ]; + }).join(', '), + '(behaviours)': cSpec.behaviours !== undefined ? Obj.map(cSpec.behaviours, function (v, k) { + return v === undefined ? '--revoked--' : { + config: v.configAsRaw(), + 'original-config': v.initialConfig, + state: c.readState(k) + }; + }) : 'none' + }; + }; + + return go(comp); + }; + + var getOrInitConnection = function () { + // The format of the global is going to be: + // lookup(uid) -> Option { name => data } + // systems: Set AlloyRoots + if (window[CHROME_INSPECTOR_GLOBAL] !== undefined) return window[CHROME_INSPECTOR_GLOBAL]; + else { + window[CHROME_INSPECTOR_GLOBAL] = { + systems: { }, + lookup: function (uid) { + var systems = window[CHROME_INSPECTOR_GLOBAL].systems; + var connections = Obj.keys(systems); + return Options.findMap(connections, function (conn) { + var connGui = systems[conn]; + return connGui.getByUid(uid).toOption().map(function (comp) { + return Objects.wrap(AlloyLogger.element(comp.element()), inspectorInfo(comp)); + }); + }); + } + }; + return window[CHROME_INSPECTOR_GLOBAL]; + } + }; + + var registerInspector = function (name, gui) { + var connection = getOrInitConnection(); + connection.systems[name] = gui; + }; + + return { + logHandler: logHandler, + noLogger: Fun.constant(ignoreEvent), + getTrace: getTrace, + monitorEvent: monitorEvent, + isDebugging: Fun.constant(debugging), + registerInspector: registerInspector + }; + } +); + +define( + 'ephox.katamari.api.Cell', + + [ + ], + + function () { + var Cell = function (initial) { + var value = initial; + + var get = function () { + return value; + }; + + var set = function (v) { + value = v; + }; + + var clone = function () { + return Cell(get()); + }; + + return { + get: get, + set: set, + clone: clone + }; + }; + + return Cell; + } +); + +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.PredicateExists', + + [ + 'ephox.sugar.api.search.PredicateFind' + ], + + function (PredicateFind) { + var any = function (predicate) { + return PredicateFind.first(predicate).isSome(); + }; + + var ancestor = function (scope, predicate, isRoot) { + return PredicateFind.ancestor(scope, predicate, isRoot).isSome(); + }; + + var closest = function (scope, predicate, isRoot) { + return PredicateFind.closest(scope, predicate, isRoot).isSome(); + }; + + var sibling = function (scope, predicate) { + return PredicateFind.sibling(scope, predicate).isSome(); + }; + + var child = function (scope, predicate) { + return PredicateFind.child(scope, predicate).isSome(); + }; + + var descendant = function (scope, predicate) { + return PredicateFind.descendant(scope, predicate).isSome(); + }; + + return { + any: any, + ancestor: ancestor, + closest: closest, + sibling: sibling, + child: child, + descendant: descendant + }; + } +); + +define( + 'ephox.sugar.api.dom.Focus', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.PredicateExists', + 'ephox.sugar.api.search.Traverse', + 'global!document' + ], + + function (Fun, Option, Compare, Element, PredicateExists, Traverse, document) { + var focus = function (element) { + element.dom().focus(); + }; + + var blur = function (element) { + element.dom().blur(); + }; + + var hasFocus = function (element) { + var doc = Traverse.owner(element).dom(); + return element.dom() === doc.activeElement; + }; + + var active = function (_doc) { + var doc = _doc !== undefined ? _doc.dom() : document; + return Option.from(doc.activeElement).map(Element.fromDom); + }; + + var focusInside = function (element) { + // Only call focus if the focus is not already inside it. + var doc = Traverse.owner(element); + var inside = active(doc).filter(function (a) { + return PredicateExists.closest(a, Fun.curry(Compare.eq, element)); + }); + + inside.fold(function () { + focus(element); + }, Fun.noop); + }; + + /** + * Return the descendant element that has focus. + * Use instead of SelectorFind.descendant(container, ':focus') + * because the :focus selector relies on keyboard focus. + */ + var search = function (element) { + return active(Traverse.owner(element)).filter(function (e) { + return element.dom().contains(e.dom()); + }); + }; + + return { + hasFocus: hasFocus, + focus: focus, + blur: blur, + active: active, + search: search, + focusInside: focusInside + }; + } +); +defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); +/** + * 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.dom.DOMUtils', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.dom.DOMUtils'); + } +); + +/** + * 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.ThemeManager', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.ThemeManager'); + } +); + +define( + 'tinymce.themes.mobile.alien.TinyCodeDupe', + + [ + 'global!document' + ], + + function (document) { + /// TODO this code is from the tinymce link plugin, deduplicate when we decide how to share it + var openLink = function (target) { + var link = document.createElement('a'); + link.target = '_blank'; + link.href = target.href; + link.rel = 'noreferrer noopener'; + + var nuEvt = document.createEvent('MouseEvents'); + nuEvt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + + document.body.appendChild(link); + link.dispatchEvent(nuEvt); + document.body.removeChild(link); + }; + + return { + openLink: openLink + }; + } +); + +define( + 'tinymce.themes.mobile.channels.TinyChannels', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + var formatChanged = 'formatChanged'; + var orientationChanged = 'orientationChanged'; + var dropupDismissed = 'dropupDismissed'; + + return { + formatChanged: Fun.constant(formatChanged), + orientationChanged: Fun.constant(orientationChanged), + dropupDismissed: Fun.constant(dropupDismissed) + }; + } +); + +define( + 'ephox.alloy.behaviour.receiving.ActiveReceiving', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.log.AlloyLogger', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj' + ], + + function (AlloyEvents, SystemEvents, AlloyLogger, ValueSchema, Arr, Obj) { + var chooseChannels = function (channels, message) { + return message.universal() ? channels : Arr.filter(channels, function (ch) { + return Arr.contains(message.channels(), ch); + }); + }; + + var events = function (receiveConfig/*, receiveState */) { + return AlloyEvents.derive([ + AlloyEvents.run(SystemEvents.receive(), function (component, message) { + var channelMap = receiveConfig.channels(); + var channels = Obj.keys(channelMap); + + var targetChannels = chooseChannels(channels, message); + Arr.each(targetChannels, function (ch) { + var channelInfo = channelMap[ch](); + var channelSchema = channelInfo.schema(); + var data = ValueSchema.asStructOrDie( + 'channel[' + ch + '] data\nReceiver: ' + AlloyLogger.element(component.element()), + channelSchema, message.data() + ); + channelInfo.onReceive()(component, data); + }); + }) + ]); + }; + + return { + events: events + }; + } +); +define( + 'ephox.alloy.menu.util.MenuMarkers', + + [ + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun' + ], + + function (FieldSchema, ValueSchema, Fun) { + var menuFields = [ + FieldSchema.strict('menu'), + FieldSchema.strict('selectedMenu') + ]; + + var itemFields = [ + FieldSchema.strict('item'), + FieldSchema.strict('selectedItem') + ]; + + var schema = ValueSchema.objOfOnly( + itemFields.concat(menuFields) + ); + + var itemSchema = ValueSchema.objOfOnly(itemFields); + + return { + menuFields: Fun.constant(menuFields), + itemFields: Fun.constant(itemFields), + schema: Fun.constant(schema), + itemSchema: Fun.constant(itemSchema) + }; + } +); +define( + 'ephox.alloy.data.Fields', + + [ + 'ephox.alloy.debugging.Debugging', + 'ephox.alloy.menu.util.MenuMarkers', + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Result', + 'global!console' + ], + + function (Debugging, MenuMarkers, FieldPresence, FieldSchema, ValueSchema, Arr, Fun, Option, Result, console) { + var initSize = FieldSchema.strictObjOf('initSize', [ + FieldSchema.strict('numColumns'), + FieldSchema.strict('numRows') + ]); + + var itemMarkers = function () { + return FieldSchema.strictOf('markers', MenuMarkers.itemSchema()); + }; + + var menuMarkers = function () { + return FieldSchema.strictOf('markers', MenuMarkers.schema()); + }; + + var tieredMenuMarkers = function () { + return FieldSchema.strictObjOf('markers', [ + FieldSchema.strict('backgroundMenu') + ].concat(MenuMarkers.menuFields()).concat(MenuMarkers.itemFields())); + }; + + var markers = function (required) { + return FieldSchema.strictObjOf('markers', Arr.map(required, FieldSchema.strict)); + }; + + var onPresenceHandler = function (label, fieldName, presence) { + // We care about where the handler was declared (in terms of which schema) + var trace = Debugging.getTrace(); + return FieldSchema.field( + fieldName, + fieldName, + presence, + // Apply some wrapping to their supplied function + ValueSchema.valueOf(function (f) { + return Result.value(function () { + /* + * This line is just for debugging information + */ + Debugging.logHandler(label, fieldName, trace); + return f.apply(undefined, arguments); + }); + }) + ); + }; + + var onHandler = function (fieldName) { + return onPresenceHandler('onHandler', fieldName, FieldPresence.defaulted(Fun.noop)); + }; + + var onKeyboardHandler = function (fieldName) { + return onPresenceHandler('onKeyboardHandler', fieldName, FieldPresence.defaulted(Option.none)); + }; + + var onStrictHandler = function (fieldName) { + return onPresenceHandler('onHandler', fieldName, FieldPresence.strict()); + }; + + var onStrictKeyboardHandler = function (fieldName) { + return onPresenceHandler('onKeyboardHandler', fieldName, FieldPresence.strict()); + }; + + var output = function (name, value) { + return FieldSchema.state(name, Fun.constant(value)); + }; + + var snapshot = function (name) { + return FieldSchema.state(name, Fun.identity); + }; + + return { + initSize: Fun.constant(initSize), + itemMarkers: itemMarkers, + menuMarkers: menuMarkers, + tieredMenuMarkers: tieredMenuMarkers, + markers: markers, + + onHandler: onHandler, + onKeyboardHandler: onKeyboardHandler, + onStrictHandler: onStrictHandler, + onStrictKeyboardHandler: onStrictKeyboardHandler, + + output: output, + snapshot: snapshot + }; + } +); +define( + 'ephox.alloy.behaviour.receiving.ReceivingSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Result' + ], + + function (Fields, FieldSchema, ValueSchema, Result) { + return [ + FieldSchema.strictOf('channels', ValueSchema.setOf( + // Allow any keys. + Result.value, + ValueSchema.objOfOnly([ + Fields.onStrictHandler('onReceive'), + FieldSchema.defaulted('schema', ValueSchema.anyValue()) + ]) + )) + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Receiving', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.receiving.ActiveReceiving', + 'ephox.alloy.behaviour.receiving.ReceivingSchema' + ], + + function (Behaviour, ActiveReceiving, ReceivingSchema) { + return Behaviour.create({ + fields: ReceivingSchema, + name: 'receiving', + active: ActiveReceiving + }); + } +); +define( + 'ephox.alloy.behaviour.toggling.ToggleApis', + + [ + 'ephox.sugar.api.properties.Class' + ], + + function (Class) { + var updateAriaState = function (component, toggleConfig, toggleState) { + var pressed = isOn(component, toggleConfig); + + var ariaInfo = toggleConfig.aria(); + ariaInfo.update()(component, ariaInfo, pressed); + }; + + var toggle = function (component, toggleConfig, toggleState) { + Class.toggle(component.element(), toggleConfig.toggleClass()); + updateAriaState(component, toggleConfig); + }; + + var on = function (component, toggleConfig, toggleState) { + Class.add(component.element(), toggleConfig.toggleClass()); + updateAriaState(component, toggleConfig); + }; + + var off = function (component, toggleConfig, toggleState) { + Class.remove(component.element(), toggleConfig.toggleClass()); + updateAriaState(component, toggleConfig); + }; + + var isOn = function (component, toggleConfig, toggleState) { + return Class.has(component.element(), toggleConfig.toggleClass()); + }; + + var onLoad = function (component, toggleConfig, toggleState) { + // There used to be a bit of code in here that would only overwrite + // the attribute if it didn't have a current value. I can't remember + // what case that was for, so I'm removing it until it is required. + var api = toggleConfig.selected() ? on : off; + api(component, toggleConfig, toggleState); + }; + + return { + onLoad: onLoad, + toggle: toggle, + isOn: isOn, + on: on, + off: off + }; + } +); +define( + 'ephox.alloy.behaviour.toggling.ActiveToggle', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.behaviour.common.Behaviour', + 'ephox.alloy.behaviour.toggling.ToggleApis', + 'ephox.alloy.dom.DomModification', + 'ephox.katamari.api.Arr' + ], + + function (AlloyEvents, Behaviour, ToggleApis, DomModification, Arr) { + var exhibit = function (base, toggleConfig, toggleState) { + return DomModification.nu({ }); + }; + + var events = function (toggleConfig, toggleState) { + var execute = Behaviour.executeEvent(toggleConfig, toggleState, ToggleApis.toggle); + var load = Behaviour.loadEvent(toggleConfig, toggleState, ToggleApis.onLoad); + + return AlloyEvents.derive( + Arr.flatten([ + toggleConfig.toggleOnExecute() ? [ execute ] : [ ], + [ load ] + ]) + ); + }; + + return { + exhibit: exhibit, + events: events + }; + } +); +define( + 'ephox.alloy.behaviour.toggling.ToggleModes', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.properties.Attr' + ], + + function (Objects, Arr, Option, Node, Attr) { + var updatePressed = function (component, ariaInfo, status) { + Attr.set(component.element(), 'aria-pressed', status); + if (ariaInfo.syncWithExpanded()) updateExpanded(component, ariaInfo, status); + }; + + var updateSelected = function (component, ariaInfo, status) { + Attr.set(component.element(), 'aria-selected', status); + }; + + var updateChecked = function (component, ariaInfo, status) { + Attr.set(component.element(), 'aria-checked', status); + }; + + var updateExpanded = function (component, ariaInfo, status) { + Attr.set(component.element(), 'aria-expanded', status); + }; + + // INVESTIGATE: What other things can we derive? + var tagAttributes = { + button: [ 'aria-pressed' ], + 'input:checkbox': [ 'aria-checked' ] + }; + + var roleAttributes = { + 'button': [ 'aria-pressed' ], + 'listbox': [ 'aria-pressed', 'aria-expanded' ], + 'menuitemcheckbox': [ 'aria-checked' ] + }; + + var detectFromTag = function (component) { + var elem = component.element(); + var rawTag = Node.name(elem); + var suffix = rawTag === 'input' && Attr.has(elem, 'type') ? ':' + Attr.get(elem, 'type') : ''; + return Objects.readOptFrom(tagAttributes, rawTag + suffix); + }; + + var detectFromRole = function (component) { + var elem = component.element(); + if (! Attr.has(elem, 'role')) return Option.none(); + else { + var role = Attr.get(elem, 'role'); + return Objects.readOptFrom(roleAttributes, role); + } + }; + + var updateAuto = function (component, ariaInfo, status) { + // Role has priority + var attributes = detectFromRole(component).orThunk(function () { + return detectFromTag(component); + }).getOr([ ]); + Arr.each(attributes, function (attr) { + Attr.set(component.element(), attr, status); + }); + }; + + return { + updatePressed: updatePressed, + updateSelected: updateSelected, + updateChecked: updateChecked, + updateExpanded: updateExpanded, + updateAuto: updateAuto + }; + } +); + +define( + 'ephox.alloy.behaviour.toggling.ToggleSchema', + + [ + 'ephox.alloy.behaviour.toggling.ToggleModes', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun' + ], + + function (ToggleModes, Fields, FieldSchema, ValueSchema, Fun) { + return [ + FieldSchema.defaulted('selected', false), + FieldSchema.strict('toggleClass'), + FieldSchema.defaulted('toggleOnExecute', true), + + FieldSchema.defaultedOf('aria', { + mode: 'none' + }, ValueSchema.choose( + 'mode', { + 'pressed': [ + FieldSchema.defaulted('syncWithExpanded', false), + Fields.output('update', ToggleModes.updatePressed) + ], + 'checked': [ + Fields.output('update', ToggleModes.updateChecked) + ], + 'expanded': [ + Fields.output('update', ToggleModes.updateExpanded) + ], + 'selected': [ + Fields.output('update', ToggleModes.updateSelected) + ], + 'none': [ + Fields.output('update', Fun.noop) + ] + } + )) + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Toggling', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.toggling.ActiveToggle', + 'ephox.alloy.behaviour.toggling.ToggleApis', + 'ephox.alloy.behaviour.toggling.ToggleSchema' + ], + + function (Behaviour, ActiveToggle, ToggleApis, ToggleSchema) { + return Behaviour.create({ + fields: ToggleSchema, + name: 'toggling', + active: ActiveToggle, + apis: ToggleApis + }); + } +); +define( + 'ephox.alloy.ephemera.AlloyTags', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + var prefix = 'alloy-id-'; + var idAttr = 'data-alloy-id'; + + return { + prefix: Fun.constant(prefix), + idAttr: Fun.constant(idAttr) + }; + } +); +defineGlobal("global!Date", Date); +define( + 'ephox.katamari.api.Id', + [ + 'global!Date', + 'global!Math', + 'global!String' + ], + + function (Date, Math, String) { + + /** + * Generate a unique identifier. + * + * The unique portion of the identifier only contains an underscore + * and digits, so that it may safely be used within HTML attributes. + * + * The chance of generating a non-unique identifier has been minimized + * by combining the current time, a random number and a one-up counter. + * + * generate :: String -> String + */ + var unique = 0; + + var generate = function (prefix) { + var date = new Date(); + var time = date.getTime(); + var random = Math.floor(Math.random() * 1000000000); + + unique++; + + return prefix + '_' + random + unique + String(time); + }; + + return { + generate: generate + }; + + } +); + +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); + }; + + // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise + 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.alloy.registry.Tagger', + + [ + 'ephox.alloy.ephemera.AlloyTags', + 'ephox.katamari.api.Id', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.SelectorFind' + ], + + function (AlloyTags, Id, Fun, Option, Attr, Node, SelectorFind) { + var prefix = AlloyTags.prefix(); + var idAttr = AlloyTags.idAttr(); + + var write = function (label, elem) { + var id = Id.generate(prefix + label); + Attr.set(elem, idAttr, id); + return id; + }; + + var writeOnly = function (elem, uid) { + Attr.set(elem, idAttr, uid); + }; + + var read = function (elem) { + var id = Node.isElement(elem) ? Attr.get(elem, idAttr) : null; + return Option.from(id); + }; + + var find = function (container, id) { + return SelectorFind.descendant(container, id); + }; + + var generate = function (prefix) { + return Id.generate(prefix); + }; + + var revoke = function (elem) { + Attr.remove(elem, idAttr); + }; + + return { + revoke: revoke, + write: write, + writeOnly: writeOnly, + read: read, + find: find, + generate: generate, + attribute: Fun.constant(idAttr) + }; + } +); +define( + 'ephox.alloy.api.component.Memento', + + [ + 'ephox.alloy.registry.Tagger', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Option' + ], + + function (Tagger, Objects, Merger, Option) { + var record = function (spec) { + var uid = Objects.hasKey(spec, 'uid') ? spec.uid : Tagger.generate('memento'); + + var get = function (any) { + return any.getSystem().getByUid(uid).getOrDie(); + }; + + var getOpt = function (any) { + return any.getSystem().getByUid(uid).fold(Option.none, Option.some); + }; + + var asSpec = function () { + return Merger.deepMerge(spec, { + uid: uid + }); + }; + + return { + get: get, + getOpt: getOpt, + asSpec: asSpec + }; + }; + + return { + record: record + }; + } +); +defineGlobal("global!setTimeout", setTimeout); +defineGlobal("global!window", window); +define( + 'tinymce.themes.mobile.channels.Receivers', + + [ + 'ephox.alloy.api.behaviour.Receiving', + 'ephox.boulder.api.Objects', + 'tinymce.themes.mobile.channels.TinyChannels' + ], + + function (Receiving, Objects, TinyChannels) { + var format = function (command, update) { + return Receiving.config({ + channels: Objects.wrap( + TinyChannels.formatChanged(), + { + onReceive: function (button, data) { + if (data.command === command) { + update(button, data.state); + } + } + } + ) + }); + }; + + var orientation = function (onReceive) { + return Receiving.config({ + channels: Objects.wrap( + TinyChannels.orientationChanged(), + { + onReceive: onReceive + } + ) + }); + }; + + var receive = function (channel, onReceive) { + return { + key: channel, + value: { + onReceive: onReceive + } + }; + }; + + return { + format: format, + orientation: orientation, + receive: receive + }; + } +); + +define( + 'tinymce.themes.mobile.style.Styles', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + var prefix = 'tinymce-mobile'; + + var resolve = function (p) { + return prefix + '-' + p; + }; + + return { + resolve: resolve, + prefix: Fun.constant(prefix) + }; + } +); + +define( + 'ephox.alloy.behaviour.unselecting.ActiveUnselecting', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.dom.DomModification', + 'ephox.katamari.api.Fun' + ], + + function (AlloyEvents, NativeEvents, DomModification, Fun) { + var exhibit = function (base, unselectConfig) { + return DomModification.nu({ + styles: { + '-webkit-user-select': 'none', + 'user-select': 'none', + '-ms-user-select': 'none', + '-moz-user-select': '-moz-none' + }, + attributes: { + 'unselectable': 'on' + } + }); + }; + + var events = function (unselectConfig) { + return AlloyEvents.derive([ + AlloyEvents.abort(NativeEvents.selectstart(), Fun.constant(true)) + ]); + }; + + return { + events: events, + exhibit: exhibit + }; + } +); +define( + 'ephox.alloy.api.behaviour.Unselecting', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.unselecting.ActiveUnselecting' + ], + + function (Behaviour, ActiveUnselecting) { + return Behaviour.create({ + fields: [ ], + name: 'unselecting', + active: ActiveUnselecting + }); + } +); +define( + 'ephox.alloy.behaviour.focusing.FocusApis', + + [ + 'ephox.sugar.api.dom.Focus' + ], + + function (Focus) { + var focus = function (component, focusConfig) { + if (! focusConfig.ignore()) { + Focus.focus(component.element()); + focusConfig.onFocus()(component); + } + }; + + var blur = function (component, focusConfig) { + if (! focusConfig.ignore()) { + Focus.blur(component.element()); + } + }; + + var isFocused = function (component) { + return Focus.hasFocus(component.element()); + }; + + return { + focus: focus, + blur: blur, + isFocused: isFocused + }; + } +); +define( + 'ephox.alloy.behaviour.focusing.ActiveFocus', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.behaviour.focusing.FocusApis', + 'ephox.alloy.dom.DomModification' + ], + + function (AlloyEvents, SystemEvents, FocusApis, DomModification) { + var exhibit = function (base, focusConfig) { + if (focusConfig.ignore()) return DomModification.nu({ }); + else return DomModification.nu({ + attributes: { + 'tabindex': '-1' + } + }); + }; + + var events = function (focusConfig) { + return AlloyEvents.derive([ + AlloyEvents.run(SystemEvents.focus(), function (component, simulatedEvent) { + FocusApis.focus(component, focusConfig); + simulatedEvent.stop(); + }) + ]); + }; + + return { + exhibit: exhibit, + events: events + }; + } +); +define( + 'ephox.alloy.behaviour.focusing.FocusSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema' + ], + + function (Fields, FieldSchema) { + return [ + // TODO: Work out when we want to call this. Only when it is has changed? + Fields.onHandler('onFocus'), + FieldSchema.defaulted('ignore', false) + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Focusing', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.focusing.ActiveFocus', + 'ephox.alloy.behaviour.focusing.FocusApis', + 'ephox.alloy.behaviour.focusing.FocusSchema' + ], + + function (Behaviour, ActiveFocus, FocusApis, FocusSchema) { + return Behaviour.create({ + fields: FocusSchema, + name: 'focusing', + active: ActiveFocus, + apis: FocusApis + // Consider adding isFocused an an extra + }); + } +); +define( + 'ephox.alloy.alien.Keys', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + return { + BACKSPACE : Fun.constant([8]), + TAB : Fun.constant([9]), + ENTER : Fun.constant([13]), + SHIFT : Fun.constant([16]), + CTRL : Fun.constant([17]), + ALT : Fun.constant([18]), + CAPSLOCK : Fun.constant([20]), + ESCAPE : Fun.constant([27]), + SPACE: Fun.constant([32]), + PAGEUP: Fun.constant([33]), + PAGEDOWN: Fun.constant([34]), + END: Fun.constant([35]), + HOME: Fun.constant([36]), + LEFT: Fun.constant([37]), + UP: Fun.constant([38]), + RIGHT: Fun.constant([39]), + DOWN: Fun.constant([40]), + INSERT: Fun.constant([45]), + DEL: Fun.constant([46]), + META: Fun.constant([91, 93, 224]), + F10: Fun.constant([121]) + }; + } +); +define( + 'ephox.alloy.alien.Cycles', + + [ + + ], + + function () { + var cycleBy = function (value, delta, min, max) { + var r = value + delta; + if (r > max) return min; + else return r < min ? max : r; + }; + + var cap = function (value, min, max) { + if (value <= min) return min; + else return value >= max ? max : value; + }; + + return { + cycleBy: cycleBy, + cap: cap + }; + } +); +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 + }; + } +); + +define( + 'ephox.alloy.behaviour.highlighting.HighlightApis', + + [ + 'ephox.alloy.alien.Cycles', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Result', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.SelectorFind', + 'global!Error' + ], + + function (Cycles, Arr, Option, Result, Class, SelectorFilter, SelectorFind, Error) { + var dehighlightAll = function (component, hConfig, hState) { + var highlighted = SelectorFilter.descendants(component.element(), '.' + hConfig.highlightClass()); + Arr.each(highlighted, function (h) { + Class.remove(h, hConfig.highlightClass()); + component.getSystem().getByDom(h).each(function (target) { + hConfig.onDehighlight()(component, target); + }); + }); + }; + + var dehighlight = function (component, hConfig, hState, target) { + var wasHighlighted = isHighlighted(component, hConfig, hState, target); + Class.remove(target.element(), hConfig.highlightClass()); + + // Only fire the event if it was highlighted. + if (wasHighlighted) hConfig.onDehighlight()(component, target); + }; + + var highlight = function (component, hConfig, hState, target) { + var wasHighlighted = isHighlighted(component, hConfig, hState, target); + dehighlightAll(component, hConfig, hState); + Class.add(target.element(), hConfig.highlightClass()); + + // TODO: Check whether this should always fire + if (! wasHighlighted) hConfig.onHighlight()(component, target); + }; + + var highlightFirst = function (component, hConfig, hState) { + getFirst(component, hConfig, hState).each(function (firstComp) { + highlight(component, hConfig, hState, firstComp); + }); + }; + + var highlightLast = function (component, hConfig, hState) { + getLast(component, hConfig, hState).each(function (lastComp) { + highlight(component, hConfig, hState, lastComp); + }); + }; + + var highlightAt = function (component, hConfig, hState, index) { + getByIndex(component, hConfig, hState, index).fold(function (err) { + throw new Error(err); + }, function (firstComp) { + highlight(component, hConfig, hState, firstComp); + }); + }; + + var isHighlighted = function (component, hConfig, hState, queryTarget) { + return Class.has(queryTarget.element(), hConfig.highlightClass()); + }; + + var getHighlighted = function (component, hConfig, hState) { + // FIX: Wrong return type (probably) + return SelectorFind.descendant(component.element(), '.' + hConfig.highlightClass()).bind(component.getSystem().getByDom); + }; + + var getByIndex = function (component, hConfig, hState, index) { + var items = SelectorFilter.descendants(component.element(), '.' + hConfig.itemClass()); + + return Option.from(items[index]).fold(function () { + return Result.error('No element found with index ' + index); + }, component.getSystem().getByDom); + }; + + var getFirst = function (component, hConfig, hState) { + // FIX: Wrong return type (probably) + return SelectorFind.descendant(component.element(), '.' + hConfig.itemClass()).bind(component.getSystem().getByDom); + }; + + var getLast = function (component, hConfig, hState) { + var items = SelectorFilter.descendants(component.element(), '.' + hConfig.itemClass()); + var last = items.length > 0 ? Option.some(items[items.length - 1]) : Option.none(); + return last.bind(component.getSystem().getByDom); + }; + + var getDelta = function (component, hConfig, hState, delta) { + var items = SelectorFilter.descendants(component.element(), '.' + hConfig.itemClass()); + var current = Arr.findIndex(items, function (item) { + return Class.has(item, hConfig.highlightClass()); + }); + + return current.bind(function (selected) { + var dest = Cycles.cycleBy(selected, delta, 0, items.length - 1); + // INVESTIGATE: Are these consistent return types? (Option vs Result) + return component.getSystem().getByDom(items[dest]); + }); + }; + + var getPrevious = function (component, hConfig, hState) { + return getDelta(component, hConfig, hState, -1); + }; + + var getNext = function (component, hConfig, hState) { + return getDelta(component, hConfig, hState, +1); + }; + + return { + dehighlightAll: dehighlightAll, + dehighlight: dehighlight, + highlight: highlight, + highlightFirst: highlightFirst, + highlightLast: highlightLast, + highlightAt: highlightAt, + isHighlighted: isHighlighted, + getHighlighted: getHighlighted, + getFirst: getFirst, + getLast: getLast, + getPrevious: getPrevious, + getNext: getNext + }; + } +); +define( + 'ephox.alloy.behaviour.highlighting.HighlightSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema' + ], + + function (Fields, FieldSchema) { + return [ + FieldSchema.strict('highlightClass'), + FieldSchema.strict('itemClass'), + + Fields.onHandler('onHighlight'), + Fields.onHandler('onDehighlight') + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Highlighting', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.highlighting.HighlightApis', + 'ephox.alloy.behaviour.highlighting.HighlightSchema', + 'global!Array' + ], + + function (Behaviour, HighlightApis, HighlightSchema, Array) { + return Behaviour.create({ + fields: HighlightSchema, + name: 'highlighting', + apis: HighlightApis + }); + } +); +define( + 'ephox.alloy.api.focus.FocusManagers', + + [ + 'ephox.alloy.api.behaviour.Highlighting', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Focus' + ], + + function (Highlighting, Fun, Focus) { + + var dom = function () { + var get = function (component) { + return Focus.search(component.element()); + }; + + var set = function (component, focusee) { + component.getSystem().triggerFocus(focusee, component.element()); + }; + + return { + get: get, + set: set + }; + }; + + var highlights = function () { + var get = function (component) { + return Highlighting.getHighlighted(component).map(function (item) { + return item.element(); + }); + }; + + var set = function (component, element) { + component.getSystem().getByDom(element).fold(Fun.noop, function (item) { + Highlighting.highlight(component, item); + }); + }; + + return { + get: get, + set: set + }; + }; + + return { + dom: dom, + highlights: highlights + }; + } +); +define( + 'ephox.alloy.navigation.KeyMatch', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun' + ], + + function (Arr, Fun) { + var inSet = function (keys) { + return function (event) { + return Arr.contains(keys, event.raw().which); + }; + }; + + var and = function (preds) { + return function (event) { + return Arr.forall(preds, function (pred) { + return pred(event); + }); + }; + }; + + var is = function (key) { + return function (event) { + return event.raw().which === key; + }; + }; + + var isShift = function (event) { + return event.raw().shiftKey === true; + }; + + return { + inSet: inSet, + and: and, + is: is, + isShift: isShift, + isNotShift: Fun.not(isShift) + + }; + } +); +define( + 'ephox.alloy.navigation.KeyRules', + + [ + 'ephox.alloy.navigation.KeyMatch', + 'ephox.katamari.api.Arr' + ], + + function (KeyMatch, Arr) { + var basic = function (key, action) { + return { + matches: KeyMatch.is(key), + classification: action + }; + }; + + var rule = function (matches, action) { + return { + matches: matches, + classification: action + }; + }; + + var choose = function (transitions, event) { + var transition = Arr.find(transitions, function (t) { + return t.matches(event); + }); + + return transition.map(function (t) { + return t.classification; + }); + }; + + return { + basic: basic, + rule: rule, + choose: choose + }; + } +); +define( + 'ephox.alloy.keying.KeyingType', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.api.focus.FocusManagers', + 'ephox.alloy.data.Fields', + 'ephox.alloy.navigation.KeyRules', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Merger' + ], + + function (AlloyEvents, NativeEvents, SystemEvents, FocusManagers, Fields, KeyRules, FieldSchema, Merger) { + var typical = function (infoSchema, stateInit, getRules, getEvents, getApis, optFocusIn) { + var schema = function () { + return infoSchema.concat([ + FieldSchema.defaulted('focusManager', FocusManagers.dom()), + Fields.output('handler', me), + Fields.output('state', stateInit) + ]); + }; + + var processKey = function (component, simulatedEvent, keyingConfig, keyingState) { + var rules = getRules(component, simulatedEvent, keyingConfig, keyingState); + + return KeyRules.choose(rules, simulatedEvent.event()).bind(function (rule) { + return rule(component, simulatedEvent, keyingConfig, keyingState); + }); + }; + + var toEvents = function (keyingConfig, keyingState) { + var otherEvents = getEvents(keyingConfig, keyingState); + var keyEvents = AlloyEvents.derive( + optFocusIn.map(function (focusIn) { + return AlloyEvents.run(SystemEvents.focus(), function (component, simulatedEvent) { + focusIn(component, keyingConfig, keyingState, simulatedEvent); + simulatedEvent.stop(); + }); + }).toArray().concat([ + AlloyEvents.run(NativeEvents.keydown(), function (component, simulatedEvent) { + processKey(component, simulatedEvent, keyingConfig, keyingState).each(function (_) { + simulatedEvent.stop(); + }); + }) + ]) + ); + return Merger.deepMerge(otherEvents, keyEvents); + }; + + var me = { + schema: schema, + processKey: processKey, + toEvents: toEvents, + toApis: getApis + }; + + return me; + }; + + return { + typical: typical + }; + } +); +define( + 'ephox.alloy.navigation.ArrNavigation', + + [ + 'ephox.katamari.api.Arr', + 'global!Math' + ], + + function (Arr, Math) { + var cyclePrev = function (values, index, predicate) { + var before = Arr.reverse(values.slice(0, index)); + var after = Arr.reverse(values.slice(index + 1)); + return Arr.find(before.concat(after), predicate); + }; + + var tryPrev = function (values, index, predicate) { + var before = Arr.reverse(values.slice(0, index)); + return Arr.find(before, predicate); + }; + + var cycleNext = function (values, index, predicate) { + var before = values.slice(0, index); + var after = values.slice(index + 1); + return Arr.find(after.concat(before), predicate); + }; + + var tryNext = function (values, index, predicate) { + var after = values.slice(index + 1); + return Arr.find(after, predicate); + }; + + return { + cyclePrev: cyclePrev, + cycleNext: cycleNext, + tryPrev: tryPrev, + tryNext: tryNext + }; + } +); +define( + 'ephox.sugar.impl.Style', + + [ + + ], + + function () { + // some elements, such as mathml, don't have style attributes + var isSupported = function (dom) { + return dom.style !== undefined; + }; + + return { + isSupported: isSupported + }; + } +); +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 + }; + } +); + +define( + 'ephox.sugar.impl.Dimension', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.impl.Style' + ], + + function (Type, Arr, Css, Style) { + return function (name, getOffset) { + var set = function (element, h) { + if (!Type.isNumber(h) && !h.match(/^[0-9]+$/)) throw name + '.set accepts only positive integer values. Value was ' + h; + var dom = element.dom(); + if (Style.isSupported(dom)) dom.style[name] = h + 'px'; + }; + + /* + * jQuery supports querying width and height on the document and window objects. + * + * TBIO doesn't do this, so the code is removed to save space, but left here just in case. + */ + /* + var getDocumentWidth = function (element) { + var dom = element.dom(); + if (Node.isDocument(element)) { + var body = dom.body; + var doc = dom.documentElement; + return Math.max( + body.scrollHeight, + doc.scrollHeight, + body.offsetHeight, + doc.offsetHeight, + doc.clientHeight + ); + } + }; + + var getWindowWidth = function (element) { + var dom = element.dom(); + if (dom.window === dom) { + // There is no offsetHeight on a window, so use the clientHeight of the document + return dom.document.documentElement.clientHeight; + } + }; + */ + + + var get = function (element) { + var r = getOffset(element); + + // zero or null means non-standard or disconnected, fall back to CSS + if ( r <= 0 || r === null ) { + var css = Css.get(element, name); + // ugh this feels dirty, but it saves cycles + return parseFloat(css) || 0; + } + return r; + }; + + // in jQuery, getOuter replicates (or uses) box-sizing: border-box calculations + // although these calculations only seem relevant for quirks mode, and edge cases TBIO doesn't rely on + var getOuter = get; + + var aggregate = function (element, properties) { + return Arr.foldl(properties, function (acc, property) { + var val = Css.get(element, property); + var value = val === undefined ? 0: parseInt(val, 10); + return isNaN(value) ? acc : acc + value; + }, 0); + }; + + var max = function (element, value, properties) { + var cumulativeInclusions = aggregate(element, properties); + // if max-height is 100px and your cumulativeInclusions is 150px, there is no way max-height can be 100px, so we return 0. + var absoluteMax = value > cumulativeInclusions ? value - cumulativeInclusions : 0; + return absoluteMax; + }; + + return { + set: set, + get: get, + getOuter: getOuter, + aggregate: aggregate, + max: max + }; + }; + } +); +define( + 'ephox.sugar.api.view.Height', + + [ + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.impl.Dimension' + ], + + function (Css, Dimension) { + var api = Dimension('height', function (element) { + // IMO passing this function is better than using dom['offset' + 'height'] + return element.dom().offsetHeight; + }); + + var set = function (element, h) { + api.set(element, h); + }; + + var get = function (element) { + return api.get(element); + }; + + var getOuter = function (element) { + return api.getOuter(element); + }; + + var setMax = function (element, value) { + // These properties affect the absolute max-height, they are not counted natively, we want to include these properties. + var inclusions = [ 'margin-top', 'border-top-width', 'padding-top', 'padding-bottom', 'border-bottom-width', 'margin-bottom' ]; + var absMax = api.max(element, value, inclusions); + Css.set(element, 'max-height', absMax + 'px'); + }; + + return { + set: set, + get: get, + getOuter: getOuter, + setMax: setMax + }; + } +); + +define( + 'ephox.alloy.keying.CyclicType', + + [ + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.data.Fields', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.log.AlloyLogger', + 'ephox.alloy.navigation.ArrNavigation', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.SelectorFind', + 'ephox.sugar.api.view.Height' + ], + + function ( + Keys, NoState, Fields, KeyingType, AlloyLogger, ArrNavigation, KeyMatch, KeyRules, FieldSchema, Arr, Fun, Option, Compare, Focus, SelectorFilter, SelectorFind, + Height + ) { + var schema = [ + FieldSchema.defaulted('selector', '[data-alloy-tabstop="true"]'), + FieldSchema.option('onEscape'), + FieldSchema.option('onEnter'), + FieldSchema.defaulted('firstTabstop', 0), + FieldSchema.defaulted('useTabstopAt', Fun.constant(true)), + // Maybe later we should just expose isVisible + FieldSchema.option('visibilitySelector') + ]; + + // Fire an alloy focus on the first visible element that matches the selector + var focusIn = function (component, cyclicConfig, cyclicState) { + var tabstops = SelectorFilter.descendants(component.element(), cyclicConfig.selector()); + var visibles = Arr.filter(tabstops, function (elem) { + return isVisible(cyclicConfig, elem); + }); + + var visibleOpt = Option.from(visibles[cyclicConfig.firstTabstop()]); + + visibleOpt.each(function (target) { + var originator = component.element(); + component.getSystem().triggerFocus(target, originator); + }); + }; + + // TODO: Test this + var isVisible = function (cyclicConfig, element) { + var target = cyclicConfig.visibilitySelector().bind(function (sel) { + return SelectorFind.closest(element, sel); + }).getOr(element); + + // NOTE: We can't use Visibility.isVisible, because the toolbar has width when it has closed, just not height. + return Height.get(target) > 0; + }; + + var findTabstop = function (component, cyclicConfig) { + return Focus.search(component.element()).bind(function (elem) { + return SelectorFind.closest(elem, cyclicConfig.selector()); + }); + }; + + var goFromTabstop = function (component, tabstops, stopIndex, cyclicConfig, cycle) { + return cycle(tabstops, stopIndex, function (elem) { + return isVisible(cyclicConfig, elem) && cyclicConfig.useTabstopAt(elem); + }).fold(function () { + // Even if there is only one, still capture the event. + // logFailed(index, tabstops); + return Option.some(true); + }, function (outcome) { + // logSuccess(cyclicInfo, index, tabstops, component.element(), outcome); + var system = component.getSystem(); + var originator = component.element(); + system.triggerFocus(outcome, originator); + // Kill the event + return Option.some(true); + }); + }; + + var go = function (component, simulatedEvent, cyclicConfig, cycle) { + // 1. Find our current tabstop + // 2. Find the index of that tabstop + // 3. Cycle the tabstop + // 4. Fire alloy focus on the resultant tabstop + var tabstops = SelectorFilter.descendants(component.element(), cyclicConfig.selector()); + return findTabstop(component, cyclicConfig).bind(function (tabstop) { + // focused component + var optStopIndex = Arr.findIndex(tabstops, Fun.curry(Compare.eq, tabstop)); + + return optStopIndex.bind(function (stopIndex) { + return goFromTabstop(component, tabstops, stopIndex, cyclicConfig, cycle); + }); + }); + }; + + var goBackwards = function (component, simulatedEvent, cyclicConfig, cyclicState) { + return go(component, simulatedEvent, cyclicConfig, ArrNavigation.cyclePrev); + }; + + var goForwards = function (component, simulatedEvent, cyclicConfig, cyclicState) { + return go(component, simulatedEvent, cyclicConfig, ArrNavigation.cycleNext); + }; + + var execute = function (component, simulatedEvent, cyclicConfig, cyclicState) { + return cyclicConfig.onEnter().bind(function (f) { + return f(component, simulatedEvent); + }); + }; + + var exit = function (component, simulatedEvent, cyclicConfig, cyclicState) { + return cyclicConfig.onEscape().bind(function (f) { + return f(component, simulatedEvent); + }); + }; + + var getRules = Fun.constant([ + KeyRules.rule(KeyMatch.and([ KeyMatch.isShift, KeyMatch.inSet(Keys.TAB()) ]), goBackwards), + KeyRules.rule(KeyMatch.inSet(Keys.TAB()), goForwards), + KeyRules.rule(KeyMatch.inSet(Keys.ESCAPE()), exit), + KeyRules.rule(KeyMatch.and([ KeyMatch.isNotShift, KeyMatch.inSet(Keys.ENTER()) ]), execute) + ]); + + var getEvents = Fun.constant({ }); + var getApis = Fun.constant({ }); + + return KeyingType.typical(schema, NoState.init, getRules, getEvents, getApis, Option.some(focusIn)); + } +); +define( + 'ephox.alloy.alien.EditableFields', + + [ + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.node.Node' + ], + + function (Attr, Node) { + var inside = function (target) { + return ( + (Node.name(target) === 'input' && Attr.get(target, 'type') !== 'radio') || + Node.name(target) === 'textarea' + ); + }; + + return { + inside: inside + }; + } +); +define( + 'ephox.alloy.keying.KeyingTypes', + + [ + 'ephox.alloy.alien.EditableFields', + 'ephox.alloy.alien.Keys', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.katamari.api.Option' + ], + + function (EditableFields, Keys, AlloyTriggers, SystemEvents, KeyMatch, Option) { + + var doDefaultExecute = function (component, simulatedEvent, focused) { + // Note, we use to pass through simulatedEvent here and make target: component. This simplification + // may be a problem + AlloyTriggers.dispatch(component, focused, SystemEvents.execute()); + return Option.some(true); + }; + + var defaultExecute = function (component, simulatedEvent, focused) { + return EditableFields.inside(focused) && KeyMatch.inSet(Keys.SPACE())(simulatedEvent.event()) ? Option.none() : doDefaultExecute(component, simulatedEvent, focused); + }; + + return { + defaultExecute: defaultExecute + }; + } +); +define( + 'ephox.alloy.keying.ExecutionType', + + [ + 'ephox.alloy.alien.EditableFields', + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.keying.KeyingTypes', + 'ephox.alloy.log.AlloyLogger', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option' + ], + + function (EditableFields, Keys, NoState, KeyingType, KeyingTypes, AlloyLogger, KeyMatch, KeyRules, FieldSchema, Fun, Option) { + var schema = [ + FieldSchema.defaulted('execute', KeyingTypes.defaultExecute), + FieldSchema.defaulted('useSpace', false), + FieldSchema.defaulted('useEnter', true), + FieldSchema.defaulted('useDown', false) + ]; + + var execute = function (component, simulatedEvent, executeConfig, executeState) { + return executeConfig.execute()(component, simulatedEvent, component.element()); + }; + + var getRules = function (component, simulatedEvent, executeConfig, executeState) { + var spaceExec = executeConfig.useSpace() && !EditableFields.inside(component.element()) ? Keys.SPACE() : [ ]; + var enterExec = executeConfig.useEnter() ? Keys.ENTER() : [ ]; + var downExec = executeConfig.useDown() ? Keys.DOWN() : [ ]; + var execKeys = spaceExec.concat(enterExec).concat(downExec); + + return [ + KeyRules.rule(KeyMatch.inSet(execKeys), execute) + ]; + }; + + var getEvents = Fun.constant({ }); + var getApis = Fun.constant({ }); + + return KeyingType.typical(schema, NoState.init, getRules, getEvents, getApis, Option.none()); + } +); +define( + 'ephox.alloy.behaviour.keyboard.KeyingState', + + [ + 'ephox.alloy.behaviour.common.BehaviourState', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option' + ], + + function (BehaviourState, Cell, Fun, Option) { + var flatgrid = function (spec) { + var dimensions = Cell(Option.none()); + + var setGridSize = function (numRows, numColumns) { + dimensions.set( + Option.some({ + numRows: Fun.constant(numRows), + numColumns: Fun.constant(numColumns) + }) + ); + }; + + var getNumRows = function () { + return dimensions.get().map(function (d) { + return d.numRows(); + }); + }; + + var getNumColumns = function () { + return dimensions.get().map(function (d) { + return d.numColumns(); + }); + }; + + return BehaviourState({ + readState: Fun.constant({ }), + setGridSize: setGridSize, + getNumRows: getNumRows, + getNumColumns: getNumColumns + }); + }; + + var init = function (spec) { + return spec.state()(spec); + }; + + return { + flatgrid: flatgrid, + init: init + }; + } +); + +define( + 'ephox.sugar.api.properties.Direction', + + [ + 'ephox.sugar.api.properties.Css' + ], + + function (Css) { + var onDirection = function (isLtr, isRtl) { + return function (element) { + return getDirection(element) === 'rtl' ? isRtl : isLtr; + }; + }; + + var getDirection = function (element) { + return Css.get(element, 'direction') === 'rtl' ? 'rtl' : 'ltr'; + }; + + return { + onDirection: onDirection, + getDirection: getDirection + }; + } +); +define( + 'ephox.alloy.navigation.DomMovement', + + [ + 'ephox.sugar.api.properties.Direction' + ], + + function (Direction) { + // Looks up direction (considering LTR and RTL), finds the focused element, + // and tries to move. If it succeeds, triggers focus and kills the event. + var useH = function (movement) { + return function (component, simulatedEvent, config, state) { + var move = movement(component.element()); + return use(move, component, simulatedEvent, config, state); + }; + }; + + var west = function (moveLeft, moveRight) { + var movement = Direction.onDirection(moveLeft, moveRight); + return useH(movement); + }; + + var east = function (moveLeft, moveRight) { + var movement = Direction.onDirection(moveRight, moveLeft); + return useH(movement); + }; + + var useV = function (move) { + return function (component, simulatedEvent, config, state) { + return use(move, component, simulatedEvent, config, state); + }; + }; + + var use = function (move, component, simulatedEvent, config, state) { + var outcome = config.focusManager().get(component).bind(function (focused) { + return move(component.element(), focused, config, state); + }); + + return outcome.map(function (newFocus) { + config.focusManager().set(component, newFocus); + return true; + }); + }; + + return { + east: east, + west: west, + north: useV, + south: useV, + move: useV + }; + } +); +define( + 'ephox.alloy.navigation.ArrPinpoint', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Struct' + ], + + function (Arr, Struct) { + var indexInfo = Struct.immutableBag([ 'index', 'candidates' ], [ ]); + + var locate = function (candidates, predicate) { + return Arr.findIndex(candidates, predicate).map(function (index) { + return indexInfo({ + index: index, + candidates: candidates + }); + }); + }; + + return { + locate: locate + }; + } +); +define( + 'ephox.sugar.api.view.Visibility', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.properties.Toggler', + 'ephox.sugar.api.properties.Css' + ], + + function (Fun, Toggler, Css) { + // This function is dangerous. Toggle behaviour is different depending on whether the element is in the DOM or not when it's created. + var visibilityToggler = function (element, property, hiddenValue, visibleValue) { + var initial = Css.get(element, property); + // old jquery-ism that this function depends on + if (initial === undefined) initial = ''; + + var value = initial === hiddenValue ? visibleValue : hiddenValue; + + var off = Fun.curry(Css.set, element, property, initial); + var on = Fun.curry(Css.set, element, property, value); + return Toggler(off, on, false); + }; + + var toggler = function (element) { + return visibilityToggler(element, 'visibility', 'hidden', 'visible'); + }; + + var displayToggler = function (element, value) { + return visibilityToggler(element, 'display', 'none', value); + }; + + var isHidden = function (dom) { + return dom.offsetWidth <= 0 && dom.offsetHeight <= 0; + }; + + var isVisible = function (element) { + var dom = element.dom(); + return !isHidden(dom); + }; + + return { + toggler: toggler, + displayToggler: displayToggler, + isVisible: isVisible + }; + } +); + +define( + 'ephox.alloy.navigation.DomPinpoint', + + [ + 'ephox.alloy.navigation.ArrPinpoint', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.view.Visibility' + ], + + function (ArrPinpoint, Arr, Fun, Compare, SelectorFilter, Visibility) { + var locateVisible = function (container, current, selector) { + var filter = Visibility.isVisible; + return locateIn(container, current, selector, filter); + }; + + var locateIn = function (container, current, selector, filter) { + var predicate = Fun.curry(Compare.eq, current); + var candidates = SelectorFilter.descendants(container, selector); + var visible = Arr.filter(candidates, Visibility.isVisible); + return ArrPinpoint.locate(visible, predicate); + }; + + var findIndex = function (elements, target) { + return Arr.findIndex(elements, function (elem) { + return Compare.eq(target, elem); + }); + }; + + return { + locateVisible: locateVisible, + locateIn: locateIn, + findIndex: findIndex + }; + } +); +define( + 'ephox.alloy.navigation.WrapArrNavigation', + + [ + 'ephox.alloy.alien.Cycles', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'global!Math' + ], + + function (Cycles, Fun, Option, Math) { + var withGrid = function (values, index, numCols, f) { + var oldRow = Math.floor(index / numCols); + var oldColumn = index % numCols; + + return f(oldRow, oldColumn).bind(function (address) { + var newIndex = address.row() * numCols + address.column(); + return newIndex >= 0 && newIndex < values.length ? Option.some(values[newIndex]) : Option.none(); + }); + }; + + var cycleHorizontal = function (values, index, numRows, numCols, delta) { + return withGrid(values, index, numCols, function (oldRow, oldColumn) { + var onLastRow = oldRow === numRows - 1; + var colsInRow = onLastRow ? values.length - (oldRow * numCols) : numCols; + var newColumn = Cycles.cycleBy(oldColumn, delta, 0, colsInRow - 1); + return Option.some({ + row: Fun.constant(oldRow), + column: Fun.constant(newColumn) + }); + }); + }; + + var cycleVertical = function (values, index, numRows, numCols, delta) { + return withGrid(values, index, numCols, function (oldRow, oldColumn) { + var newRow = Cycles.cycleBy(oldRow, delta, 0, numRows - 1); + var onLastRow = newRow === numRows - 1; + var colsInRow = onLastRow ? values.length - (newRow * numCols) : numCols; + var newCol = Cycles.cap(oldColumn, 0, colsInRow - 1); + return Option.some({ + row: Fun.constant(newRow), + column: Fun.constant(newCol) + }); + }); + }; + + var cycleRight = function (values, index, numRows, numCols) { + return cycleHorizontal(values, index, numRows, numCols, +1); + }; + + var cycleLeft = function (values, index, numRows, numCols) { + return cycleHorizontal(values, index, numRows, numCols, -1); + }; + + var cycleUp = function (values, index, numRows, numCols) { + return cycleVertical(values, index, numRows, numCols, -1); + }; + + var cycleDown = function (values, index, numRows, numCols) { + return cycleVertical(values, index, numRows, numCols, +1); + }; + + return { + cycleDown: cycleDown, + cycleUp: cycleUp, + cycleLeft: cycleLeft, + cycleRight: cycleRight + }; + } +); +define( + 'ephox.alloy.keying.FlatgridType', + + [ + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.keyboard.KeyingState', + 'ephox.alloy.data.Fields', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.keying.KeyingTypes', + 'ephox.alloy.navigation.DomMovement', + 'ephox.alloy.navigation.DomPinpoint', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.alloy.navigation.WrapArrNavigation', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.search.SelectorFind' + ], + + function ( + Keys, KeyingState, Fields, KeyingType, KeyingTypes, DomMovement, DomPinpoint, KeyMatch, KeyRules, WrapArrNavigation, FieldSchema, Cell, Fun, Option, Focus, + SelectorFind + ) { + var schema = [ + FieldSchema.strict('selector'), + FieldSchema.defaulted('execute', KeyingTypes.defaultExecute), + Fields.onKeyboardHandler('onEscape'), + FieldSchema.defaulted('captureTab', false), + Fields.initSize() + ]; + + var focusIn = function (component, gridConfig, gridState) { + SelectorFind.descendant(component.element(), gridConfig.selector()).each(function (first) { + component.getSystem().triggerFocus(first, component.element()); + }); + }; + + var execute = function (component, simulatedEvent, gridConfig, gridState) { + return Focus.search(component.element(), gridConfig.selector()).bind(function (focused) { + return gridConfig.execute()(component, simulatedEvent, focused); + }); + }; + + var doMove = function (cycle) { + return function (element, focused, gridConfig, gridState) { + return DomPinpoint.locateVisible(element, focused, gridConfig.selector()).bind(function (identified) { + return cycle( + identified.candidates(), + identified.index(), + gridState.getNumRows().getOr(gridConfig.initSize().numRows()), + gridState.getNumColumns().getOr(gridConfig.initSize().numColumns()) + ); + }); + }; + }; + + var handleTab = function (component, simulatedEvent, gridConfig, gridState) { + return gridConfig.captureTab() ? Option.some(true) : Option.none(); + }; + + var doEscape = function (component, simulatedEvent, gridConfig, gridState) { + return gridConfig.onEscape()(component, simulatedEvent); + }; + + var moveLeft = doMove(WrapArrNavigation.cycleLeft); + var moveRight = doMove(WrapArrNavigation.cycleRight); + + var moveNorth = doMove(WrapArrNavigation.cycleUp); + var moveSouth = doMove(WrapArrNavigation.cycleDown); + + var getRules = Fun.constant([ + KeyRules.rule(KeyMatch.inSet(Keys.LEFT()), DomMovement.west(moveLeft, moveRight)), + KeyRules.rule(KeyMatch.inSet(Keys.RIGHT()), DomMovement.east(moveLeft, moveRight)), + KeyRules.rule(KeyMatch.inSet(Keys.UP()), DomMovement.north(moveNorth)), + KeyRules.rule(KeyMatch.inSet(Keys.DOWN()), DomMovement.south(moveSouth)), + KeyRules.rule(KeyMatch.and([ KeyMatch.isShift, KeyMatch.inSet(Keys.TAB()) ]), handleTab), + KeyRules.rule(KeyMatch.and([ KeyMatch.isNotShift, KeyMatch.inSet(Keys.TAB()) ]), handleTab), + KeyRules.rule(KeyMatch.inSet(Keys.ESCAPE()), doEscape), + + KeyRules.rule(KeyMatch.inSet(Keys.SPACE().concat(Keys.ENTER())), execute) + ]); + + var getEvents = Fun.constant({ }); + + var getApis = {}; + + return KeyingType.typical(schema, KeyingState.flatgrid, getRules, getEvents, getApis, Option.some(focusIn)); + } +); +define( + 'ephox.alloy.navigation.DomNavigation', + + [ + 'ephox.alloy.alien.Cycles', + 'ephox.alloy.navigation.DomPinpoint', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option' + ], + + function (Cycles, DomPinpoint, Fun, Option) { + var horizontal = function (container, selector, current, delta) { + // I wonder if this will be a problem when the focused element is invisible (shouldn't happen) + return DomPinpoint.locateVisible(container, current, selector, Fun.constant(true)).bind(function (identified) { + var index = identified.index(); + var candidates = identified.candidates(); + var newIndex = Cycles.cycleBy(index, delta, 0, candidates.length - 1); + return Option.from(candidates[newIndex]); + }); + }; + + return { + horizontal: horizontal + }; + } +); +define( + 'ephox.alloy.keying.FlowType', + + [ + 'ephox.alloy.alien.EditableFields', + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.keying.KeyingTypes', + 'ephox.alloy.navigation.DomMovement', + 'ephox.alloy.navigation.DomNavigation', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.search.SelectorFind' + ], + + function (EditableFields, Keys, NoState, KeyingType, KeyingTypes, DomMovement, DomNavigation, KeyMatch, KeyRules, FieldSchema, Fun, Option, Focus, SelectorFind) { + var schema = [ + FieldSchema.strict('selector'), + FieldSchema.defaulted('getInitial', Option.none), + FieldSchema.defaulted('execute', KeyingTypes.defaultExecute), + FieldSchema.defaulted('executeOnMove', false) + ]; + + var execute = function (component, simulatedEvent, flowConfig) { + return Focus.search(component.element()).bind(function (focused) { + return flowConfig.execute()(component, simulatedEvent, focused); + }); + }; + + var focusIn = function (component, flowConfig) { + flowConfig.getInitial()(component).or(SelectorFind.descendant(component.element(), flowConfig.selector())).each(function (first) { + component.getSystem().triggerFocus(first, component.element()); + }); + }; + + var moveLeft = function (element, focused, info) { + return DomNavigation.horizontal(element, info.selector(), focused, -1); + }; + + var moveRight = function (element, focused, info) { + return DomNavigation.horizontal(element, info.selector(), focused, +1); + }; + + var doMove = function (movement) { + return function (component, simulatedEvent, flowConfig) { + return movement(component, simulatedEvent, flowConfig).bind(function () { + return flowConfig.executeOnMove() ? execute(component, simulatedEvent, flowConfig) : Option.some(true); + }); + }; + }; + + var getRules = function (_) { + return [ + KeyRules.rule(KeyMatch.inSet(Keys.LEFT().concat(Keys.UP())), doMove(DomMovement.west(moveLeft, moveRight))), + KeyRules.rule(KeyMatch.inSet(Keys.RIGHT().concat(Keys.DOWN())), doMove(DomMovement.east(moveLeft, moveRight))), + KeyRules.rule(KeyMatch.inSet(Keys.ENTER()), execute), + KeyRules.rule(KeyMatch.inSet(Keys.SPACE()), execute) + ]; + }; + + var getEvents = Fun.constant({ }); + + var getApis = Fun.constant({ }); + return KeyingType.typical(schema, NoState.init, getRules, getEvents, getApis, Option.some(focusIn)); + } +); +define( + 'ephox.alloy.navigation.MatrixNavigation', + + [ + 'ephox.alloy.alien.Cycles', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Struct', + 'global!Math' + ], + + function (Cycles, Option, Struct, Math) { + var outcome = Struct.immutableBag([ 'rowIndex', 'columnIndex', 'cell' ], [ ]); + + var toCell = function (matrix, rowIndex, columnIndex) { + return Option.from(matrix[rowIndex]).bind(function (row) { + return Option.from(row[columnIndex]).map(function (cell) { + return outcome({ + rowIndex: rowIndex, + columnIndex: columnIndex, + cell: cell + }); + }); + }); + }; + + var cycleHorizontal = function (matrix, rowIndex, startCol, deltaCol) { + var row = matrix[rowIndex]; + var colsInRow = row.length; + var newColIndex = Cycles.cycleBy(startCol, deltaCol, 0, colsInRow - 1); + return toCell(matrix, rowIndex, newColIndex); + }; + + var cycleVertical = function (matrix, colIndex, startRow, deltaRow) { + var nextRowIndex = Cycles.cycleBy(startRow, deltaRow, 0, matrix.length - 1); + var colsInNextRow = matrix[nextRowIndex].length; + var nextColIndex = Cycles.cap(colIndex, 0, colsInNextRow - 1); + return toCell(matrix, nextRowIndex, nextColIndex); + }; + + var moveHorizontal = function (matrix, rowIndex, startCol, deltaCol) { + var row = matrix[rowIndex]; + var colsInRow = row.length; + var newColIndex = Cycles.cap(startCol + deltaCol, 0, colsInRow - 1); + return toCell(matrix, rowIndex, newColIndex); + }; + + var moveVertical = function (matrix, colIndex, startRow, deltaRow) { + var nextRowIndex = Cycles.cap(startRow + deltaRow, 0, matrix.length - 1); + var colsInNextRow = matrix[nextRowIndex].length; + var nextColIndex = Cycles.cap(colIndex, 0, colsInNextRow - 1); + return toCell(matrix, nextRowIndex, nextColIndex); + }; + + // return address(Math.floor(index / columns), index % columns); + var cycleRight = function (matrix, startRow, startCol) { + return cycleHorizontal(matrix, startRow, startCol, +1); + }; + + var cycleLeft = function (matrix, startRow, startCol) { + return cycleHorizontal(matrix, startRow, startCol, -1); + }; + + var cycleUp = function (matrix, startRow, startCol) { + return cycleVertical(matrix, startCol, startRow, -1); + }; + + var cycleDown = function (matrix, startRow, startCol) { + return cycleVertical(matrix, startCol, startRow, +1); + }; + + var moveLeft = function (matrix, startRow, startCol) { + return moveHorizontal(matrix, startRow, startCol, -1); + }; + + var moveRight = function (matrix, startRow, startCol) { + return moveHorizontal(matrix, startRow, startCol, +1); + }; + + var moveUp = function (matrix, startRow, startCol) { + return moveVertical(matrix, startCol, startRow, -1); + }; + + var moveDown = function (matrix, startRow, startCol) { + return moveVertical(matrix, startCol, startRow, +1); + }; + + return { + cycleRight: cycleRight, + cycleLeft: cycleLeft, + cycleUp: cycleUp, + cycleDown: cycleDown, + + moveLeft: moveLeft, + moveRight: moveRight, + moveUp: moveUp, + moveDown: moveDown + }; + } +); +define( + 'ephox.alloy.keying.MatrixType', + + [ + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.keying.KeyingTypes', + 'ephox.alloy.navigation.DomMovement', + 'ephox.alloy.navigation.DomPinpoint', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.alloy.navigation.MatrixNavigation', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.SelectorFind' + ], + + function ( + Keys, NoState, KeyingType, KeyingTypes, DomMovement, DomPinpoint, KeyMatch, KeyRules, MatrixNavigation, FieldSchema, Arr, Fun, Option, Focus, SelectorFilter, + SelectorFind + ) { + var schema = [ + FieldSchema.strictObjOf('selectors', [ + FieldSchema.strict('row'), + FieldSchema.strict('cell') + ]), + + // Used to determine whether pressing right/down at the end cycles back to the start/top + FieldSchema.defaulted('cycles', true), + FieldSchema.defaulted('previousSelector', Option.none), + FieldSchema.defaulted('execute', KeyingTypes.defaultExecute) + ]; + + var focusIn = function (component, matrixConfig) { + var focused = matrixConfig.previousSelector()(component).orThunk(function () { + var selectors = matrixConfig.selectors(); + return SelectorFind.descendant(component.element(), selectors.cell()); + }); + + focused.each(function (cell) { + component.getSystem().triggerFocus(cell, component.element()); + }); + }; + + var execute = function (component, simulatedEvent, matrixConfig) { + return Focus.search(component.element()).bind(function (focused) { + return matrixConfig.execute()(component, simulatedEvent, focused); + }); + }; + + var toMatrix = function (rows, matrixConfig) { + return Arr.map(rows, function (row) { + return SelectorFilter.descendants(row, matrixConfig.selectors().cell()); + }); + }; + + var doMove = function (ifCycle, ifMove) { + return function (element, focused, matrixConfig) { + var move = matrixConfig.cycles() ? ifCycle : ifMove; + return SelectorFind.closest(focused, matrixConfig.selectors().row()).bind(function (inRow) { + var cellsInRow = SelectorFilter.descendants(inRow, matrixConfig.selectors().cell()); + + return DomPinpoint.findIndex(cellsInRow, focused).bind(function (colIndex) { + var allRows = SelectorFilter.descendants(element, matrixConfig.selectors().row()); + return DomPinpoint.findIndex(allRows, inRow).bind(function (rowIndex) { + // Now, make the matrix. + var matrix = toMatrix(allRows, matrixConfig); + return move(matrix, rowIndex, colIndex).map(function (next) { + return next.cell(); + }); + }); + }); + }); + }; + }; + + var moveLeft = doMove(MatrixNavigation.cycleLeft, MatrixNavigation.moveLeft); + var moveRight = doMove(MatrixNavigation.cycleRight, MatrixNavigation.moveRight); + + var moveNorth = doMove(MatrixNavigation.cycleUp, MatrixNavigation.moveUp); + var moveSouth = doMove(MatrixNavigation.cycleDown, MatrixNavigation.moveDown); + + var getRules = Fun.constant([ + KeyRules.rule(KeyMatch.inSet(Keys.LEFT()), DomMovement.west(moveLeft, moveRight)), + KeyRules.rule(KeyMatch.inSet(Keys.RIGHT()), DomMovement.east(moveLeft, moveRight)), + KeyRules.rule(KeyMatch.inSet(Keys.UP()), DomMovement.north(moveNorth)), + KeyRules.rule(KeyMatch.inSet(Keys.DOWN()), DomMovement.south(moveSouth)), + KeyRules.rule(KeyMatch.inSet(Keys.SPACE().concat(Keys.ENTER())), execute) + ]); + + var getEvents = Fun.constant({ }); + + var getApis = Fun.constant({ }); + return KeyingType.typical(schema, NoState.init, getRules, getEvents, getApis, Option.some(focusIn)); + } +); +define( + 'ephox.alloy.keying.MenuType', + + [ + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.keying.KeyingTypes', + 'ephox.alloy.navigation.DomMovement', + 'ephox.alloy.navigation.DomNavigation', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.search.SelectorFind' + ], + + function (Keys, NoState, KeyingType, KeyingTypes, DomMovement, DomNavigation, KeyMatch, KeyRules, FieldSchema, Fun, Option, Focus, SelectorFind) { + var schema = [ + FieldSchema.strict('selector'), + FieldSchema.defaulted('execute', KeyingTypes.defaultExecute), + FieldSchema.defaulted('moveOnTab', false) + ]; + + var execute = function (component, simulatedEvent, menuConfig) { + return menuConfig.focusManager().get(component).bind(function (focused) { + return menuConfig.execute()(component, simulatedEvent, focused); + }); + }; + + var focusIn = function (component, menuConfig, simulatedEvent) { + // Maybe keep selection if it was there before + SelectorFind.descendant(component.element(), menuConfig.selector()).each(function (first) { + menuConfig.focusManager().set(component, first); + }); + }; + + var moveUp = function (element, focused, info) { + return DomNavigation.horizontal(element, info.selector(), focused, -1); + }; + + var moveDown = function (element, focused, info) { + return DomNavigation.horizontal(element, info.selector(), focused, +1); + }; + + var fireShiftTab = function (component, simulatedEvent, menuConfig) { + return menuConfig.moveOnTab() ? DomMovement.move(moveUp)(component, simulatedEvent, menuConfig) : Option.none(); + }; + + var fireTab = function (component, simulatedEvent, menuConfig) { + return menuConfig.moveOnTab() ? DomMovement.move(moveDown)(component, simulatedEvent, menuConfig) : Option.none(); + }; + + var getRules = Fun.constant([ + KeyRules.rule(KeyMatch.inSet(Keys.UP()), DomMovement.move(moveUp)), + KeyRules.rule(KeyMatch.inSet(Keys.DOWN()), DomMovement.move(moveDown)), + KeyRules.rule(KeyMatch.and([ KeyMatch.isShift, KeyMatch.inSet(Keys.TAB()) ]), fireShiftTab), + KeyRules.rule(KeyMatch.and([ KeyMatch.isNotShift, KeyMatch.inSet(Keys.TAB()) ]), fireTab), + KeyRules.rule(KeyMatch.inSet(Keys.ENTER()), execute), + KeyRules.rule(KeyMatch.inSet(Keys.SPACE()), execute) + ]); + + var getEvents = Fun.constant({ }); + + var getApis = Fun.constant({ }); + + return KeyingType.typical(schema, NoState.init, getRules, getEvents, getApis, Option.some(focusIn)); + } +); +define( + 'ephox.alloy.keying.SpecialType', + + [ + 'ephox.alloy.alien.Keys', + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.data.Fields', + 'ephox.alloy.keying.KeyingType', + 'ephox.alloy.navigation.KeyMatch', + 'ephox.alloy.navigation.KeyRules', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option' + ], + + function (Keys, NoState, Fields, KeyingType, KeyMatch, KeyRules, FieldSchema, Fun, Option) { + var schema = [ + Fields.onKeyboardHandler('onSpace'), + Fields.onKeyboardHandler('onEnter'), + Fields.onKeyboardHandler('onShiftEnter'), + Fields.onKeyboardHandler('onLeft'), + Fields.onKeyboardHandler('onRight'), + Fields.onKeyboardHandler('onTab'), + Fields.onKeyboardHandler('onShiftTab'), + Fields.onKeyboardHandler('onUp'), + Fields.onKeyboardHandler('onDown'), + Fields.onKeyboardHandler('onEscape'), + FieldSchema.option('focusIn') + ]; + + var getRules = function (component, simulatedEvent, executeInfo) { + return [ + KeyRules.rule(KeyMatch.inSet(Keys.SPACE()), executeInfo.onSpace()), + KeyRules.rule( + KeyMatch.and([ KeyMatch.isNotShift, KeyMatch.inSet(Keys.ENTER()) ]), executeInfo.onEnter() + ), + KeyRules.rule( + KeyMatch.and([ KeyMatch.isShift, KeyMatch.inSet(Keys.ENTER()) ]), executeInfo.onShiftEnter() + ), + KeyRules.rule( + KeyMatch.and([ KeyMatch.isShift, KeyMatch.inSet(Keys.TAB()) ]), executeInfo.onShiftTab() + ), + KeyRules.rule( + KeyMatch.and([ KeyMatch.isNotShift, KeyMatch.inSet(Keys.TAB()) ]), executeInfo.onTab() + ), + + KeyRules.rule(KeyMatch.inSet(Keys.UP()), executeInfo.onUp()), + KeyRules.rule(KeyMatch.inSet(Keys.DOWN()), executeInfo.onDown()), + KeyRules.rule(KeyMatch.inSet(Keys.LEFT()), executeInfo.onLeft()), + KeyRules.rule(KeyMatch.inSet(Keys.RIGHT()), executeInfo.onRight()), + KeyRules.rule(KeyMatch.inSet(Keys.SPACE()), executeInfo.onSpace()), + KeyRules.rule(KeyMatch.inSet(Keys.ESCAPE()), executeInfo.onEscape()) + ]; + }; + + var focusIn = function (component, executeInfo) { + return executeInfo.focusIn().bind(function (f) { + return f(component, executeInfo); + }); + }; + + var getEvents = Fun.constant({ }); + var getApis = Fun.constant({ }); + + return KeyingType.typical(schema, NoState.init, getRules, getEvents, getApis, Option.some(focusIn)); + } +); +define( + 'ephox.alloy.behaviour.keyboard.KeyboardBranches', + + [ + 'ephox.alloy.keying.CyclicType', + 'ephox.alloy.keying.ExecutionType', + 'ephox.alloy.keying.FlatgridType', + 'ephox.alloy.keying.FlowType', + 'ephox.alloy.keying.MatrixType', + 'ephox.alloy.keying.MenuType', + 'ephox.alloy.keying.SpecialType' + ], + + function (CyclicType, ExecutionType, FlatgridType, FlowType, MatrixType, MenuType, SpecialType) { + return { + cyclic: CyclicType.schema(), + flow: FlowType.schema(), + flatgrid: FlatgridType.schema(), + matrix: MatrixType.schema(), + execution: ExecutionType.schema(), + menu: MenuType.schema(), + special: SpecialType.schema() + }; + } +); +define( + 'ephox.alloy.api.behaviour.Keying', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.keyboard.KeyboardBranches', + 'ephox.alloy.behaviour.keyboard.KeyingState', + 'ephox.boulder.api.Objects', + 'global!console' + ], + + function (Behaviour, KeyboardBranches, KeyingState, Objects, console) { + // These APIs are going to be interesting because they are not + // available for all keying modes + return Behaviour.createModes({ + branchKey: 'mode', + branches: KeyboardBranches, + name: 'keying', + active: { + events: function (keyingConfig, keyingState) { + var handler = keyingConfig.handler(); + return handler.toEvents(keyingConfig, keyingState); + } + }, + apis: { + focusIn: function (component/*, keyConfig, keyState */) { + component.getSystem().triggerFocus(component.element(), component.element()); + }, + + // These APIs are going to be interesting because they are not + // available for all keying modes + setGridSize: function (component, keyConfig, keyState, numRows, numColumns) { + if (! Objects.hasKey(keyState, 'setGridSize')) { + console.error('Layout does not support setGridSize'); + } else { + keyState.setGridSize(numRows, numColumns); + } + } + }, + state: KeyingState + }); + } +); +define( + 'ephox.alloy.api.ui.GuiTypes', + + [ + 'ephox.alloy.debugging.FunctionAnnotator', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Id', + 'global!Array' + ], + + function (FunctionAnnotator, Objects, Arr, Fun, Id, Array) { + var premadeTag = Id.generate('alloy-premade'); + var apiConfig = Id.generate('api'); + + + var premade = function (comp) { + return Objects.wrap(premadeTag, comp); + }; + + var getPremade = function (spec) { + return Objects.readOptFrom(spec, premadeTag); + }; + + var makeApi = function (f) { + return FunctionAnnotator.markAsSketchApi(function (component/*, ... */) { + var args = Array.prototype.slice.call(arguments, 0); + var spi = component.config(apiConfig); + return f.apply(undefined, [ spi ].concat(args)); + }, f); + }; + + return { + apiConfig: Fun.constant(apiConfig), + makeApi: makeApi, + premade: premade, + getPremade: getPremade + }; + } +); +define( + 'ephox.alloy.parts.PartType', + + [ + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Id', + 'ephox.katamari.api.Option' + ], + + function (FieldPresence, FieldSchema, ValueSchema, Adt, Fun, Id, Option) { + var adt = Adt.generate([ + { required: [ 'data' ] }, + { external: [ 'data' ] }, + { optional: [ 'data' ] }, + { group: [ 'data' ] } + ]); + + var fFactory = FieldSchema.defaulted('factory', { sketch: Fun.identity }); + var fSchema = FieldSchema.defaulted('schema', [ ]); + var fName = FieldSchema.strict('name'); + var fPname = FieldSchema.field( + 'pname', + 'pname', + FieldPresence.defaultedThunk(function (typeSpec) { + return ''; + }), + ValueSchema.anyValue() + ); + + var fDefaults = FieldSchema.defaulted('defaults', Fun.constant({ })); + var fOverrides = FieldSchema.defaulted('overrides', Fun.constant({ })); + + var requiredSpec = ValueSchema.objOf([ + fFactory, fSchema, fName, fPname, fDefaults, fOverrides + ]); + + var externalSpec = ValueSchema.objOf([ + fFactory, fSchema, fName, fDefaults, fOverrides + ]); + + var optionalSpec = ValueSchema.objOf([ + fFactory, fSchema, fName, fPname, fDefaults, fOverrides + ]); + + var groupSpec = ValueSchema.objOf([ + fFactory, fSchema, fName, + FieldSchema.strict('unit'), + fPname, fDefaults, fOverrides + ]); + + var asNamedPart = function (part) { + return part.fold(Option.some, Option.none, Option.some, Option.some); + }; + + var name = function (part) { + var get = function (data) { + return data.name(); + }; + + return part.fold(get, get, get, get); + }; + + var asCommon = function (part) { + return part.fold(Fun.identity, Fun.identity, Fun.identity, Fun.identity); + }; + + var convert = function (adtConstructor, partSpec) { + return function (spec) { + var data = ValueSchema.asStructOrDie('Converting part type', partSpec, spec); + return adtConstructor(data); + }; + }; + + return { + required: convert(adt.required, requiredSpec), + external: convert(adt.external, externalSpec), + optional: convert(adt.optional, optionalSpec), + group: convert(adt.group, groupSpec), + asNamedPart: asNamedPart, + name: name, + asCommon: asCommon, + + original: Fun.constant('entirety') + }; + } +); +define( + 'ephox.alloy.spec.UiSubstitutes', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Merger', + 'ephox.sand.api.JSON', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Adt', + 'global!Error' + ], + + function (Objects, Arr, Obj, Merger, Json, Fun, Adt, Error) { + var placeholder = 'placeholder'; + + var adt = Adt.generate([ + { single: [ 'required', 'valueThunk' ] }, + { multiple: [ 'required', 'valueThunks' ] } + ]); + + var isSubstitute = function (uiType) { + return Arr.contains([ + placeholder + ], uiType); + }; + + var subPlaceholder = function (owner, detail, compSpec, placeholders) { + if (owner.exists(function (o) { return o !== compSpec.owner; })) return adt.single(true, Fun.constant(compSpec)); + // Ignore having to find something for the time being. + return Objects.readOptFrom(placeholders, compSpec.name).fold(function () { + throw new Error('Unknown placeholder component: ' + compSpec.name + '\nKnown: [' + + Obj.keys(placeholders) + ']\nNamespace: ' + owner.getOr('none') + '\nSpec: ' + Json.stringify(compSpec, null, 2) + ); + }, function (newSpec) { + // Must return a single/multiple type + return newSpec.replace(); + }); + }; + + var scan = function (owner, detail, compSpec, placeholders) { + if (compSpec.uiType === placeholder) return subPlaceholder(owner, detail, compSpec, placeholders); + else return adt.single(false, Fun.constant(compSpec)); + }; + + var substitute = function (owner, detail, compSpec, placeholders) { + var base = scan(owner, detail, compSpec, placeholders); + + return base.fold( + function (req, valueThunk) { + var value = valueThunk(detail, compSpec.config, compSpec.validated); + var childSpecs = Objects.readOptFrom(value, 'components').getOr([ ]); + var substituted = Arr.bind(childSpecs, function (c) { + return substitute(owner, detail, c, placeholders); + }); + return [ + Merger.deepMerge(value, { + components: substituted + }) + ]; + }, + function (req, valuesThunk) { + var values = valuesThunk(detail, compSpec.config, compSpec.validated); + return values; + } + ); + }; + + var substituteAll = function (owner, detail, components, placeholders) { + return Arr.bind(components, function (c) { + return substitute(owner, detail, c, placeholders); + }); + }; + + var oneReplace = function (label, replacements) { + var called = false; + + var used = function () { + return called; + }; + + var replace = function () { + if (called === true) throw new Error( + 'Trying to use the same placeholder more than once: ' + label + ); + called = true; + return replacements; + }; + + var required = function () { + return replacements.fold(function (req, _) { + return req; + }, function (req, _) { + return req; + }); + }; + + return { + name: Fun.constant(label), + required: required, + used: used, + replace: replace + }; + }; + + var substitutePlaces = function (owner, detail, components, placeholders) { + var ps = Obj.map(placeholders, function (ph, name) { + return oneReplace(name, ph); + }); + + var outcome = substituteAll(owner, detail, components, ps); + + Obj.each(ps, function (p) { + if (p.used() === false && p.required()) { + throw new Error( + 'Placeholder: ' + p.name() + ' was not found in components list\nNamespace: ' + owner.getOr('none') + '\nComponents: ' + + Json.stringify(detail.components(), null, 2) + ); + } + }); + + return outcome; + }; + + var singleReplace = function (detail, p) { + var replacement = p; + return replacement.fold(function (req, valueThunk) { + return [ valueThunk(detail) ]; + }, function (req, valuesThunk) { + return valuesThunk(detail); + }); + }; + + return { + single: adt.single, + multiple: adt.multiple, + isSubstitute: isSubstitute, + placeholder: Fun.constant(placeholder), + substituteAll: substituteAll, + substitutePlaces: substitutePlaces, + + singleReplace: singleReplace + }; + } +); +define( + 'ephox.alloy.parts.PartSubstitutes', + + [ + 'ephox.alloy.parts.PartType', + 'ephox.alloy.spec.UiSubstitutes', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger' + ], + + function (PartType, UiSubstitutes, Objects, Arr, Fun, Merger) { + var combine = function (detail, data, partSpec, partValidated) { + var spec = partSpec; + + return Merger.deepMerge( + data.defaults()(detail, partSpec, partValidated), + partSpec, + { uid: detail.partUids()[data.name()] }, + data.overrides()(detail, partSpec, partValidated), + { + 'debug.sketcher': Objects.wrap('part-' + data.name(), spec) + } + ); + }; + + var subs = function (owner, detail, parts) { + var internals = { }; + var externals = { }; + + Arr.each(parts, function (part) { + part.fold( + // Internal + function (data) { + internals[data.pname()] = UiSubstitutes.single(true, function (detail, partSpec, partValidated) { + return data.factory().sketch( + combine(detail, data, partSpec, partValidated) + ); + }); + }, + + // External + function (data) { + var partSpec = detail.parts()[data.name()](); + externals[data.name()] = Fun.constant( + combine(detail, data, partSpec[PartType.original()]()) + ); + // no placeholders + }, + + // Optional + function (data) { + internals[data.pname()] = UiSubstitutes.single(false, function (detail, partSpec, partValidated) { + return data.factory().sketch( + combine(detail, data, partSpec, partValidated) + ); + }); + }, + + // Group + function (data) { + internals[data.pname()] = UiSubstitutes.multiple(true, function (detail, _partSpec, _partValidated) { + var units = detail[data.name()](); + return Arr.map(units, function (u) { + // Group multiples do not take the uid because there is more than one. + return data.factory().sketch( + Merger.deepMerge( + data.defaults()(detail, u), + u, + data.overrides()(detail, u) + ) + ); + }); + }); + } + ); + }); + + return { + internals: Fun.constant(internals), + externals: Fun.constant(externals) + }; + }; + + return { + subs: subs + }; + } +); + +define( + 'ephox.alloy.parts.AlloyParts', + + [ + 'ephox.alloy.data.Fields', + 'ephox.alloy.parts.PartSubstitutes', + 'ephox.alloy.parts.PartType', + 'ephox.alloy.spec.UiSubstitutes', + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option' + ], + + function (Fields, PartSubstitutes, PartType, UiSubstitutes, FieldPresence, FieldSchema, Objects, ValueSchema, Arr, Fun, Merger, Obj, Option) { + // TODO: Make more functional if performance isn't an issue. + var generate = function (owner, parts) { + var r = { }; + + Arr.each(parts, function (part) { + PartType.asNamedPart(part).each(function (np) { + var g = doGenerateOne(owner, np.pname()); + r[np.name()] = function (config) { + var validated = ValueSchema.asRawOrDie('Part: ' + np.name() + ' in ' + owner, ValueSchema.objOf(np.schema()), config); + return Merger.deepMerge(g, { + config: config, + validated: validated + }); + }; + }); + }); + + return r; + }; + + // Does not have the config. + var doGenerateOne = function (owner, pname) { + return { + uiType: UiSubstitutes.placeholder(), + owner: owner, + name: pname + }; + }; + + var generateOne = function (owner, pname, config) { + return { + uiType: UiSubstitutes.placeholder(), + owner: owner, + name: pname, + config: config, + validated: { } + }; + }; + + var schemas = function (parts) { + // This actually has to change. It needs to return the schemas for things that will + // not appear in the components list, which is only externals + return Arr.bind(parts, function (part) { + return part.fold( + Option.none, + Option.some, + Option.none, + Option.none + ).map(function (data) { + return FieldSchema.strictObjOf(data.name(), data.schema().concat([ + Fields.snapshot(PartType.original()) + ])); + }).toArray(); + }); + }; + + var names = function (parts) { + return Arr.map(parts, PartType.name); + }; + + var substitutes = function (owner, detail, parts) { + return PartSubstitutes.subs(owner, detail, parts); + }; + + var components = function (owner, detail, internals) { + return UiSubstitutes.substitutePlaces(Option.some(owner), detail, detail.components(), internals); + }; + + var getPart = function (component, detail, partKey) { + var uid = detail.partUids()[partKey]; + return component.getSystem().getByUid(uid).toOption(); + }; + + var getPartOrDie = function (component, detail, partKey) { + return getPart(component, detail, partKey).getOrDie('Could not find part: ' + partKey); + }; + + var getParts = function (component, detail, partKeys) { + var r = { }; + var uids = detail.partUids(); + + var system = component.getSystem(); + Arr.each(partKeys, function (pk) { + r[pk] = system.getByUid(uids[pk]); + }); + + // Structing + return Obj.map(r, Fun.constant); + }; + + var getAllParts = function (component, detail) { + var system = component.getSystem(); + return Obj.map(detail.partUids(), function (pUid, k) { + return Fun.constant(system.getByUid(pUid)); + }); + }; + + var getPartsOrDie = function (component, detail, partKeys) { + var r = { }; + var uids = detail.partUids(); + + var system = component.getSystem(); + Arr.each(partKeys, function (pk) { + r[pk] = system.getByUid(uids[pk]).getOrDie(); + }); + + // Structing + return Obj.map(r, Fun.constant); + }; + + var defaultUids = function (baseUid, partTypes) { + var partNames = names(partTypes); + + return Objects.wrapAll( + Arr.map(partNames, function (pn) { + return { key: pn, value: baseUid + '-' + pn }; + }) + ); + }; + + var defaultUidsSchema = function (partTypes) { + return FieldSchema.field( + 'partUids', + 'partUids', + FieldPresence.mergeWithThunk(function (spec) { + return defaultUids(spec.uid, partTypes); + }), + ValueSchema.anyValue() + ); + }; + + return { + generate: generate, + generateOne: generateOne, + schemas: schemas, + names: names, + substitutes: substitutes, + components: components, + + defaultUids: defaultUids, + defaultUidsSchema: defaultUidsSchema, + + getAllParts: getAllParts, + getPart: getPart, + getPartOrDie: getPartOrDie, + getParts: getParts, + getPartsOrDie: getPartsOrDie + }; + } +); + +define( + 'ephox.alloy.spec.SpecSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.alloy.spec.UiSubstitutes', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.sand.api.JSON', + 'global!Error' + ], + + function (Fields, UiSubstitutes, FieldSchema, Objects, ValueSchema, Arr, Fun, Merger, Obj, Json, Error) { + var getPartsSchema = function (partNames, _optPartNames, _owner) { + var owner = _owner !== undefined ? _owner : 'Unknown owner'; + var fallbackThunk = function () { + return [ + Fields.output('partUids', { }) + ]; + }; + + var optPartNames = _optPartNames !== undefined ? _optPartNames : fallbackThunk(); + if (partNames.length === 0 && optPartNames.length === 0) return fallbackThunk(); + + // temporary hacking + var partsSchema = FieldSchema.strictObjOf( + 'parts', + Arr.flatten([ + Arr.map(partNames, FieldSchema.strict), + Arr.map(optPartNames, function (optPart) { + return FieldSchema.defaulted(optPart, UiSubstitutes.single(false, function () { + throw new Error('The optional part: ' + optPart + ' was not specified in the config, but it was used in components'); + })); + }) + ]) + ); + + var partUidsSchema = FieldSchema.state( + 'partUids', + function (spec) { + if (! Objects.hasKey(spec, 'parts')) { + throw new Error( + 'Part uid definition for owner: ' + owner + ' requires "parts"\nExpected parts: ' + partNames.join(', ') + '\nSpec: ' + + Json.stringify(spec, null, 2) + ); + } + var uids = Obj.map(spec.parts, function (v, k) { + return Objects.readOptFrom(v, 'uid').getOrThunk(function () { + return spec.uid + '-' + k; + }); + }); + return uids; + } + ); + + return [ partsSchema, partUidsSchema ]; + }; + + var getPartUidsSchema = function (label, spec) { + return FieldSchema.state( + 'partUids', + function (spec) { + if (! Objects.hasKey(spec, 'parts')) { + throw new Error( + 'Part uid definition for owner: ' + label + ' requires "parts\nSpec: ' + + Json.stringify(spec, null, 2) + ); + } + var uids = Obj.map(spec.parts, function (v, k) { + return Objects.readOptFrom(v, 'uid').getOrThunk(function () { + return spec.uid + '-' + k; + }); + }); + return uids; + } + ); + }; + + var base = function (label, partSchemas, partUidsSchemas, spec) { + var ps = partSchemas.length > 0 ? [ + FieldSchema.strictObjOf('parts', partSchemas) + ] : [ ]; + + return ps.concat([ + FieldSchema.strict('uid'), + FieldSchema.defaulted('dom', { }), // Maybe get rid of. + FieldSchema.defaulted('components', [ ]), + Fields.snapshot('originalSpec'), + FieldSchema.defaulted('debug.sketcher', { }) + ]).concat(partUidsSchemas); + }; + + + var asRawOrDie = function (label, schema, spec, partSchemas) { + + var baseS = base(label, partSchemas, spec); + return ValueSchema.asRawOrDie(label + ' [SpecSchema]', ValueSchema.objOfOnly(baseS.concat(schema)), spec); + }; + + var asStructOrDie = function (label, schema, spec, partSchemas, partUidsSchemas) { + var baseS = base(label, partSchemas, partUidsSchemas, spec); + return ValueSchema.asStructOrDie(label + ' [SpecSchema]', ValueSchema.objOfOnly(baseS.concat(schema)), spec); + }; + + var extend = function (builder, original, nu) { + // Merge all at the moment. + var newSpec = Merger.deepMerge(original, nu); + return builder(newSpec); + }; + + var addBehaviours = function (original, behaviours) { + return Merger.deepMerge(original, behaviours); + }; + + + return { + asRawOrDie: asRawOrDie, + asStructOrDie: asStructOrDie, + addBehaviours: addBehaviours, + + getPartsSchema: getPartsSchema, + extend: extend + }; + } +); +define( + 'ephox.alloy.api.ui.UiSketcher', + + [ + 'ephox.alloy.parts.AlloyParts', + 'ephox.alloy.registry.Tagger', + 'ephox.alloy.spec.SpecSchema', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Merger' + ], + + function (AlloyParts, Tagger, SpecSchema, Objects, Merger) { + var single = function (owner, schema, factory, spec) { + var specWithUid = supplyUid(spec); + var detail = SpecSchema.asStructOrDie(owner, schema, specWithUid, [ ], [ ]); + return Merger.deepMerge( + factory(detail, specWithUid), + { 'debug.sketcher': Objects.wrap(owner, spec) } + ); + }; + + var composite = function (owner, schema, partTypes, factory, spec) { + var specWithUid = supplyUid(spec); + + // Identify any information required for external parts + var partSchemas = AlloyParts.schemas(partTypes); + + // Generate partUids for all parts (external and otherwise) + var partUidsSchema = AlloyParts.defaultUidsSchema(partTypes); + + var detail = SpecSchema.asStructOrDie(owner, schema, specWithUid, partSchemas, [ partUidsSchema ]); + + // Create (internals, externals) substitutions + var subs = AlloyParts.substitutes(owner, detail, partTypes); + + // Work out the components by substituting internals + var components = AlloyParts.components(owner, detail, subs.internals()); + + return Merger.deepMerge( + // Pass through the substituted components and the externals + factory(detail, components, specWithUid, subs.externals()), + { 'debug.sketcher': Objects.wrap(owner, spec) } + ); + }; + + + var supplyUid = function (spec) { + return Merger.deepMerge( + { + uid: Tagger.generate('uid') + }, spec + ); + }; + + return { + supplyUid: supplyUid, + single: single, + composite: composite + }; + } +); +define( + 'ephox.alloy.api.ui.Sketcher', + + [ + 'ephox.alloy.api.ui.GuiTypes', + 'ephox.alloy.api.ui.UiSketcher', + 'ephox.alloy.debugging.FunctionAnnotator', + 'ephox.alloy.parts.AlloyParts', + 'ephox.alloy.parts.PartType', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj' + ], + + function (GuiTypes, UiSketcher, FunctionAnnotator, AlloyParts, PartType, FieldSchema, ValueSchema, Fun, Merger, Obj) { + var singleSchema = ValueSchema.objOfOnly([ + FieldSchema.strict('name'), + FieldSchema.strict('factory'), + FieldSchema.strict('configFields'), + FieldSchema.defaulted('apis', { }), + FieldSchema.defaulted('extraApis', { }) + ]); + + var compositeSchema = ValueSchema.objOfOnly([ + FieldSchema.strict('name'), + FieldSchema.strict('factory'), + FieldSchema.strict('configFields'), + FieldSchema.strict('partFields'), + FieldSchema.defaulted('apis', { }), + FieldSchema.defaulted('extraApis', { }) + ]); + + var single = function (rawConfig) { + var config = ValueSchema.asRawOrDie('Sketcher for ' + rawConfig.name, singleSchema, rawConfig); + + var sketch = function (spec) { + return UiSketcher.single(config.name, config.configFields, config.factory, spec); + }; + + var apis = Obj.map(config.apis, GuiTypes.makeApi); + var extraApis = Obj.map(config.extraApis, function (f, k) { + return FunctionAnnotator.markAsExtraApi(f, k); + }); + + return Merger.deepMerge( + { + name: Fun.constant(config.name), + partFields: Fun.constant([ ]), + configFields: Fun.constant(config.configFields), + + sketch: sketch + }, + apis, + extraApis + ); + }; + + var composite = function (rawConfig) { + var config = ValueSchema.asRawOrDie('Sketcher for ' + rawConfig.name, compositeSchema, rawConfig); + + var sketch = function (spec) { + return UiSketcher.composite(config.name, config.configFields, config.partFields, config.factory, spec); + }; + + // These are constructors that will store their configuration. + var parts = AlloyParts.generate(config.name, config.partFields); + + var apis = Obj.map(config.apis, GuiTypes.makeApi); + var extraApis = Obj.map(config.extraApis, function (f, k) { + return FunctionAnnotator.markAsExtraApi(f, k); + }); + + return Merger.deepMerge( + { + name: Fun.constant(config.name), + partFields: Fun.constant(config.partFields), + configFields: Fun.constant(config.configFields), + sketch: sketch, + parts: Fun.constant(parts) + }, + apis, + extraApis + ); + }; + + return { + single: single, + composite: composite + }; + } +); + +define( + 'ephox.alloy.ui.common.ButtonBase', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.katamari.api.Arr', + 'ephox.sand.api.PlatformDetection' + ], + + function (AlloyEvents, AlloyTriggers, NativeEvents, SystemEvents, Arr, PlatformDetection) { + var events = function (optAction) { + var executeHandler = function (action) { + return AlloyEvents.run(SystemEvents.execute(), function (component, simulatedEvent) { + action(component); + simulatedEvent.stop(); + }); + }; + + var onClick = function (component, simulatedEvent) { + simulatedEvent.stop(); + AlloyTriggers.emitExecute(component); + }; + + // Other mouse down listeners above this one should not get mousedown behaviour (like dragging) + var onMousedown = function (component, simulatedEvent) { + simulatedEvent.cut(); + }; + + var pointerEvents = PlatformDetection.detect().deviceType.isTouch() ? [ + AlloyEvents.run(SystemEvents.tap(), onClick) + ] : [ + AlloyEvents.run(NativeEvents.click(), onClick), + AlloyEvents.run(NativeEvents.mousedown(), onMousedown) + ]; + + return AlloyEvents.derive( + Arr.flatten([ + // Only listen to execute if it is supplied + optAction.map(executeHandler).toArray(), + pointerEvents + ]) + ); + }; + + return { + events: events + }; + } +); +define( + 'ephox.alloy.api.ui.Button', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Focusing', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.ui.common.ButtonBase', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Merger' + ], + + function (Behaviour, Focusing, Keying, Sketcher, ButtonBase, FieldSchema, Merger) { + var factory = function (detail, spec) { + var events = ButtonBase.events(detail.action()); + + return { + uid: detail.uid(), + dom: detail.dom(), + components: detail.components(), + events: events, + behaviours: Merger.deepMerge( + Behaviour.derive([ + Focusing.config({ }), + Keying.config({ + mode: 'execution', + useSpace: true, + useEnter: true + }) + ]), + detail.buttonBehaviours() + ), + domModification: { + attributes: { + type: 'button', + role: detail.role().getOr('button') + } + }, + eventOrder: detail.eventOrder() + }; + }; + + return Sketcher.single({ + name: 'Button', + factory: factory, + configFields: [ + FieldSchema.defaulted('uid', undefined), + FieldSchema.strict('dom'), + FieldSchema.defaulted('components', [ ]), + FieldSchema.defaulted('buttonBehaviours', { }), + FieldSchema.option('action'), + FieldSchema.option('role'), + FieldSchema.defaulted('eventOrder', { }) + ] + }); + } +); +define( + 'ephox.alloy.api.component.DomFactory', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Merger', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.properties.Html', + 'ephox.sugar.api.search.Traverse', + 'global!Array' + ], + + function (Objects, Arr, Merger, Element, Node, Html, Traverse, Array) { + var getAttrs = function (elem) { + var attributes = elem.dom().attributes !== undefined ? elem.dom().attributes : [ ]; + return Arr.foldl(attributes, function (b, attr) { + // Make class go through the class path. Do not list it as an attribute. + if (attr.name === 'class') return b; + else return Merger.deepMerge(b, Objects.wrap(attr.name, attr.value)); + }, {}); + }; + + var getClasses = function (elem) { + return Array.prototype.slice.call(elem.dom().classList, 0); + }; + + var fromHtml = function (html) { + var elem = Element.fromHtml(html); + + var children = Traverse.children(elem); + var attrs = getAttrs(elem); + var classes = getClasses(elem); + var contents = children.length === 0 ? { } : { innerHtml: Html.get(elem) }; + + return Merger.deepMerge({ + tag: Node.name(elem), + classes: classes, + attributes: attrs + }, contents); + }; + + var sketch = function (sketcher, html, config) { + return sketcher.sketch( + Merger.deepMerge({ + dom: fromHtml(html) + }, config) + ); + }; + + return { + fromHtml: fromHtml, + sketch: sketch + }; + } +); + +define( + 'tinymce.themes.mobile.util.UiDomFactory', + + [ + 'ephox.alloy.api.component.DomFactory', + 'ephox.katamari.api.Strings', + 'tinymce.themes.mobile.style.Styles' + ], + + function (DomFactory, Strings, Styles) { + var dom = function (rawHtml) { + var html = Strings.supplant(rawHtml, { + 'prefix': Styles.prefix() + }); + return DomFactory.fromHtml(html); + }; + + var spec = function (rawHtml) { + var sDom = dom(rawHtml); + return { + dom: sDom + }; + }; + + return { + dom: dom, + spec: spec + }; + } +); + +define( + 'tinymce.themes.mobile.ui.Buttons', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.behaviour.Unselecting', + 'ephox.alloy.api.ui.Button', + 'ephox.katamari.api.Merger', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function (Behaviour, Toggling, Unselecting, Button, Merger, Receivers, Styles, UiDomFactory) { + var forToolbarCommand = function (editor, command) { + return forToolbar(command, function () { + editor.execCommand(command); + }, { }); + }; + + var getToggleBehaviours = function (command) { + return Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('toolbar-button-selected'), + toggleOnExecute: false, + aria: { + mode: 'pressed' + } + }), + Receivers.format(command, function (button, status) { + var toggle = status ? Toggling.on : Toggling.off; + toggle(button); + }) + ]); + }; + + var forToolbarStateCommand = function (editor, command) { + var extraBehaviours = getToggleBehaviours(command); + + return forToolbar(command, function () { + editor.execCommand(command); + }, extraBehaviours); + }; + + // The action is not just executing the same command + var forToolbarStateAction = function (editor, clazz, command, action) { + var extraBehaviours = getToggleBehaviours(command); + return forToolbar(clazz, action, extraBehaviours); + }; + + var forToolbar = function (clazz, action, extraBehaviours) { + return Button.sketch({ + dom: UiDomFactory.dom(''), + action: action, + + buttonBehaviours: Merger.deepMerge( + Behaviour.derive([ + Unselecting.config({ }) + ]), + extraBehaviours + ) + }); + }; + + return { + forToolbar: forToolbar, + forToolbarCommand: forToolbarCommand, + forToolbarStateAction: forToolbarStateAction, + forToolbarStateCommand: forToolbarStateCommand + }; + } +); +define( + 'ephox.alloy.ui.slider.SliderModel', + + [ + 'global!Math' + ], + + function (Math) { + var reduceBy = function (value, min, max, step) { + if (value < min) return value; + else if (value > max) return max; + else if (value === min) return min - 1; + else return Math.max(min, value - step); + }; + + var increaseBy = function (value, min, max, step) { + if (value > max) return value; + else if (value < min) return min; + else if (value === max) return max + 1; + else return Math.min(max, value + step); + }; + + var capValue = function (value, min, max) { + return Math.max( + min, + Math.min(max, value) + ); + }; + + var snapValueOfX = function (bounds, value, min, max, step, snapStart) { + // We are snapping by the step size. Therefore, find the nearest multiple of + // the step + return snapStart.fold(function () { + // There is no initial snapping start, so just go from the minimum + var initValue = value - min; + var extraValue = Math.round(initValue / step) * step; + return capValue(min + extraValue, min - 1, max + 1); + }, function (start) { + // There is an initial snapping start, so using that as the starting point, + // calculate the nearest snap position based on the value + var remainder = (value - start) % step; + var adjustment = Math.round(remainder / step); + + + var rawSteps = Math.floor((value - start) / step); + var maxSteps = Math.floor((max - start) / step); + + var numSteps = Math.min(maxSteps, rawSteps + adjustment); + var r = start + (numSteps * step); + return Math.max(start, r); + }); + }; + + var findValueOfX = function (bounds, min, max, xValue, step, snapToGrid, snapStart) { + var range = max - min; + // TODO: TM-26 Make this bounding of edges work only occur if there are edges (and work with snapping) + if (xValue < bounds.left) return min - 1; + else if (xValue > bounds.right) return max + 1; + else { + var xOffset = Math.min(bounds.right, Math.max(xValue, bounds.left)) - bounds.left; + var newValue = capValue(((xOffset / bounds.width) * range) + min, min - 1, max + 1); + var roundedValue = Math.round(newValue); + return snapToGrid && newValue >= min && newValue <= max ? snapValueOfX(bounds, newValue, min, max, step, snapStart) : roundedValue; + } + }; + + + return { + reduceBy: reduceBy, + increaseBy: increaseBy, + findValueOfX: findValueOfX + }; + } +); +define( + 'ephox.alloy.ui.slider.SliderActions', + + [ + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.ui.slider.SliderModel', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sand.api.PlatformDetection', + 'global!Math' + ], + + function (AlloyTriggers, SliderModel, Fun, Option, PlatformDetection, Math) { + var changeEvent = 'slider.change.value'; + + var isTouch = PlatformDetection.detect().deviceType.isTouch(); + + var getEventSource = function (simulatedEvent) { + var evt = simulatedEvent.event().raw(); + if (isTouch && evt.touches !== undefined && evt.touches.length === 1) return Option.some(evt.touches[0]); + else if (isTouch && evt.touches !== undefined) return Option.none(); + else if (! isTouch && evt.clientX !== undefined) return Option.some(evt); + else return Option.none(); + }; + + var getEventX = function (simulatedEvent) { + var spot = getEventSource(simulatedEvent); + return spot.map(function (s) { + return s.clientX; + }); + }; + + var fireChange = function (component, value) { + AlloyTriggers.emitWith(component, changeEvent, { value: value }); + }; + + var moveRightFromLedge = function (ledge, detail) { + fireChange(ledge, detail.min(), Option.none()); + }; + + var moveLeftFromRedge = function (redge, detail) { + fireChange(redge, detail.max(), Option.none()); + }; + + var setToRedge = function (redge, detail) { + fireChange(redge, detail.max() + 1, Option.none()); + }; + + var setToLedge = function (ledge, detail) { + fireChange(ledge, detail.min() - 1, Option.none()); + }; + + var setToX = function (spectrum, spectrumBounds, detail, xValue) { + var value = SliderModel.findValueOfX( + spectrumBounds, detail.min(), detail.max(), + xValue, detail.stepSize(), detail.snapToGrid(), detail.snapStart() + ); + + fireChange(spectrum, value); + }; + + var setXFromEvent = function (spectrum, detail, spectrumBounds, simulatedEvent) { + return getEventX(simulatedEvent).map(function (xValue) { + setToX(spectrum, spectrumBounds, detail, xValue); + return xValue; + }); + }; + + var moveLeft = function (spectrum, detail) { + var newValue = SliderModel.reduceBy(detail.value().get(), detail.min(), detail.max(), detail.stepSize()); + fireChange(spectrum, newValue, Option.none()); + }; + + var moveRight = function (spectrum, detail) { + var newValue = SliderModel.increaseBy(detail.value().get(), detail.min(), detail.max(), detail.stepSize()); + fireChange(spectrum, newValue, Option.none()); + }; + + + return { + setXFromEvent: setXFromEvent, + setToLedge: setToLedge, + setToRedge: setToRedge, + moveLeftFromRedge: moveLeftFromRedge, + moveRightFromLedge: moveRightFromLedge, + moveLeft: moveLeft, + moveRight: moveRight, + + changeEvent: Fun.constant(changeEvent) + }; + } +); +define( + 'ephox.alloy.ui.slider.SliderParts', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Focusing', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.parts.PartType', + 'ephox.alloy.ui.slider.SliderActions', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sand.api.PlatformDetection' + ], + + function (Behaviour, Focusing, Keying, AlloyEvents, NativeEvents, PartType, SliderActions, FieldSchema, Cell, Fun, Option, PlatformDetection) { + var platform = PlatformDetection.detect(); + var isTouch = platform.deviceType.isTouch(); + + var edgePart = function (name, action) { + return PartType.optional({ + name: '' + name + '-edge', + overrides: function (detail) { + var touchEvents = AlloyEvents.derive([ + AlloyEvents.runActionExtra(NativeEvents.touchstart(), action, [ detail ]) + ]); + + var mouseEvents = AlloyEvents.derive([ + AlloyEvents.runActionExtra(NativeEvents.mousedown(), action, [ detail ]), + AlloyEvents.runActionExtra(NativeEvents.mousemove(), function (l, det) { + if (det.mouseIsDown().get()) action (l, det); + }, [ detail ]) + ]); + + return { + events: isTouch ? touchEvents : mouseEvents + }; + } + }); + }; + + // When the user touches the left edge, it should move the thumb + var ledgePart = edgePart('left', SliderActions.setToLedge); + + // When the user touches the right edge, it should move the thumb + var redgePart = edgePart('right', SliderActions.setToRedge); + + // The thumb part needs to have position absolute to be positioned correctly + var thumbPart = PartType.required({ + name: 'thumb', + defaults: Fun.constant({ + dom: { + styles: { position: 'absolute' } + } + }), + overrides: function (detail) { + return { + events: AlloyEvents.derive([ + // If the user touches the thumb itself, pretend they touched the spectrum instead. This + // allows sliding even when they touchstart the current value + AlloyEvents.redirectToPart(NativeEvents.touchstart(), detail, 'spectrum'), + AlloyEvents.redirectToPart(NativeEvents.touchmove(), detail, 'spectrum'), + AlloyEvents.redirectToPart(NativeEvents.touchend(), detail, 'spectrum') + ]) + }; + } + }); + + var spectrumPart = PartType.required({ + schema: [ + FieldSchema.state('mouseIsDown', function () { return Cell(false); }) + ], + name: 'spectrum', + overrides: function (detail) { + + var moveToX = function (spectrum, simulatedEvent) { + var spectrumBounds = spectrum.element().dom().getBoundingClientRect(); + SliderActions.setXFromEvent(spectrum, detail, spectrumBounds, simulatedEvent); + }; + + var touchEvents = AlloyEvents.derive([ + AlloyEvents.run(NativeEvents.touchstart(), moveToX), + AlloyEvents.run(NativeEvents.touchmove(), moveToX) + ]); + + var mouseEvents = AlloyEvents.derive([ + AlloyEvents.run(NativeEvents.mousedown(), moveToX), + AlloyEvents.run(NativeEvents.mousemove(), function (spectrum, se) { + if (detail.mouseIsDown().get()) moveToX(spectrum, se); + }) + ]); + + + return { + behaviours: Behaviour.derive(isTouch ? [ ] : [ + // Move left and right along the spectrum + Keying.config({ + mode: 'special', + onLeft: function (spectrum) { + SliderActions.moveLeft(spectrum, detail); + return Option.some(true); + }, + onRight: function (spectrum) { + SliderActions.moveRight(spectrum, detail); + return Option.some(true); + } + }), + Focusing.config({ }) + ]), + + events: isTouch ? touchEvents : mouseEvents + }; + } + }); + + return [ + ledgePart, + redgePart, + thumbPart, + spectrumPart + ]; + } +); +define( + 'ephox.alloy.ui.slider.SliderSchema', + + [ + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.PlatformDetection' + ], + + function (FieldSchema, Cell, Fun, PlatformDetection) { + var isTouch = PlatformDetection.detect().deviceType.isTouch(); + + return [ + FieldSchema.strict('min'), + FieldSchema.strict('max'), + FieldSchema.defaulted('stepSize', 1), + FieldSchema.defaulted('onChange', Fun.noop), + FieldSchema.defaulted('onInit', Fun.noop), + FieldSchema.defaulted('onDragStart', Fun.noop), + FieldSchema.defaulted('onDragEnd', Fun.noop), + FieldSchema.defaulted('snapToGrid', false), + FieldSchema.option('snapStart'), + FieldSchema.strict('getInitialValue'), + FieldSchema.defaulted('sliderBehaviours', { }), + + FieldSchema.state('value', function (spec) { return Cell(spec.min); }) + ].concat(! isTouch ? [ + // Only add if not on a touch device + FieldSchema.state('mouseIsDown', function () { return Cell(false); }) + ] : [ ]); + } +); +define( + 'ephox.alloy.behaviour.representing.RepresentApis', + + [ + + ], + + function () { + var onLoad = function (component, repConfig, repState) { + repConfig.store().manager().onLoad(component, repConfig, repState); + }; + + var onUnload = function (component, repConfig, repState) { + repConfig.store().manager().onUnload(component, repConfig, repState); + }; + + var setValue = function (component, repConfig, repState, data) { + repConfig.store().manager().setValue(component, repConfig, repState, data); + }; + + var getValue = function (component, repConfig, repState) { + return repConfig.store().manager().getValue(component, repConfig, repState); + }; + + return { + onLoad: onLoad, + onUnload: onUnload, + setValue: setValue, + getValue: getValue + }; + } +); +define( + 'ephox.alloy.behaviour.representing.ActiveRepresenting', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.behaviour.common.Behaviour', + 'ephox.alloy.behaviour.representing.RepresentApis' + ], + + function (AlloyEvents, Behaviour, RepresentApis) { + var events = function (repConfig, repState) { + var es = repConfig.resetOnDom() ? [ + AlloyEvents.runOnAttached(function (comp, se) { + RepresentApis.onLoad(comp, repConfig, repState); + }), + AlloyEvents.runOnDetached(function (comp, se) { + RepresentApis.onUnload(comp, repConfig, repState); + }) + ] : [ + Behaviour.loadEvent(repConfig, repState, RepresentApis.onLoad) + ]; + + return AlloyEvents.derive(es); + }; + + return { + events: events + }; + } +); +define( + 'ephox.alloy.behaviour.representing.RepresentState', + + [ + 'ephox.alloy.behaviour.common.BehaviourState', + 'ephox.katamari.api.Cell' + ], + + function (BehaviourState, Cell) { + var memory = function () { + var data = Cell(null); + + var readState = function () { + return { + mode: 'memory', + value: data.get() + }; + }; + + var isNotSet = function () { + return data.get() === null; + }; + + var clear = function () { + data.set(null); + }; + + return BehaviourState({ + set: data.set, + get: data.get, + isNotSet: isNotSet, + clear: clear, + readState: readState + }); + }; + + var manual = function () { + var readState = function () { + + }; + + return BehaviourState({ + readState: readState + }); + }; + + var dataset = function () { + var data = Cell({ }); + + var readState = function () { + return { + mode: 'dataset', + dataset: data.get() + }; + }; + + return BehaviourState({ + readState: readState, + set: data.set, + get: data.get + }); + }; + + var init = function (spec) { + return spec.store().manager().state(spec); + }; + + return { + memory: memory, + dataset: dataset, + manual: manual, + + init: init + }; + } +); + +define( + 'ephox.alloy.behaviour.representing.DatasetStore', + + [ + 'ephox.alloy.behaviour.representing.RepresentState', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Fun' + ], + + function (RepresentState, Fields, FieldSchema, Objects, Fun) { + var setValue = function (component, repConfig, repState, data) { + // TODO: Really rethink this mode. + var dataKey = repConfig.store().getDataKey(); + repState.set({ }); + repConfig.store().setData()(component, data); + repConfig.onSetValue()(component, data); + }; + + var getValue = function (component, repConfig, repState) { + var key = repConfig.store().getDataKey()(component); + var dataset = repState.get(); + return Objects.readOptFrom(dataset, key).fold(function () { + return repConfig.store().getFallbackEntry()(key); + }, function (data) { + return data; + }); + }; + + var onLoad = function (component, repConfig, repState) { + repConfig.store().initialValue().each(function (data) { + setValue(component, repConfig, repState, data); + }); + }; + + var onUnload = function (component, repConfig, repState) { + repState.set({ }); + }; + + return [ + FieldSchema.option('initialValue'), + FieldSchema.strict('getFallbackEntry'), + FieldSchema.strict('getDataKey'), + FieldSchema.strict('setData'), + Fields.output('manager', { + setValue: setValue, + getValue: getValue, + onLoad: onLoad, + onUnload: onUnload, + state: RepresentState.dataset + }) + ]; + } +); + +define( + 'ephox.alloy.behaviour.representing.ManualStore', + + [ + 'ephox.alloy.behaviour.common.NoState', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun' + ], + + function (NoState, Fields, FieldSchema, Fun) { + var getValue = function (component, repConfig, repState) { + return repConfig.store().getValue()(component); + }; + + var setValue = function (component, repConfig, repState, data) { + repConfig.store().setValue()(component, data); + repConfig.onSetValue()(component, data); + }; + + var onLoad = function (component, repConfig, repState) { + repConfig.store().initialValue().each(function (data) { + repConfig.store().setValue()(component, data); + }); + }; + + return [ + FieldSchema.strict('getValue'), + FieldSchema.defaulted('setValue', Fun.noop), + FieldSchema.option('initialValue'), + Fields.output('manager', { + setValue: setValue, + getValue: getValue, + onLoad: onLoad, + onUnload: Fun.noop, + state: NoState.init + }) + ]; + } +); + +define( + 'ephox.alloy.behaviour.representing.MemoryStore', + + [ + 'ephox.alloy.behaviour.representing.RepresentState', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema' + ], + + function (RepresentState, Fields, FieldSchema) { + var setValue = function (component, repConfig, repState, data) { + repState.set(data); + repConfig.onSetValue()(component, data); + }; + + var getValue = function (component, repConfig, repState) { + return repState.get(); + }; + + var onLoad = function (component, repConfig, repState) { + repConfig.store().initialValue().each(function (initVal) { + if (repState.isNotSet()) repState.set(initVal); + }); + }; + + var onUnload = function (component, repConfig, repState) { + repState.clear(); + }; + + return [ + FieldSchema.option('initialValue'), + Fields.output('manager', { + setValue: setValue, + getValue: getValue, + onLoad: onLoad, + onUnload: onUnload, + state: RepresentState.memory + }) + ]; + } +); + +define( + 'ephox.alloy.behaviour.representing.RepresentSchema', + + [ + 'ephox.alloy.behaviour.representing.DatasetStore', + 'ephox.alloy.behaviour.representing.ManualStore', + 'ephox.alloy.behaviour.representing.MemoryStore', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema' + ], + + function (DatasetStore, ManualStore, MemoryStore, Fields, FieldSchema, ValueSchema) { + return [ + FieldSchema.defaultedOf('store', { mode: 'memory' }, ValueSchema.choose('mode', { + memory: MemoryStore, + manual: ManualStore, + dataset: DatasetStore + })), + Fields.onHandler('onSetValue'), + FieldSchema.defaulted('resetOnDom', false) + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Representing', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.representing.ActiveRepresenting', + 'ephox.alloy.behaviour.representing.RepresentApis', + 'ephox.alloy.behaviour.representing.RepresentSchema', + 'ephox.alloy.behaviour.representing.RepresentState' + ], + + function (Behaviour, ActiveRepresenting, RepresentApis, RepresentSchema, RepresentState) { + // The self-reference is clumsy. + var me = Behaviour.create({ + fields: RepresentSchema, + name: 'representing', + active: ActiveRepresenting, + apis: RepresentApis, + extra: { + setValueFrom: function (component, source) { + var value = me.getValue(source); + me.setValue(component, value); + } + }, + state: RepresentState + }); + + return me; + } +); +define( + 'ephox.sugar.api.view.Width', + + [ + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.impl.Dimension' + ], + + function (Css, Dimension) { + var api = Dimension('width', function (element) { + // IMO passing this function is better than using dom['offset' + 'width'] + return element.dom().offsetWidth; + }); + + var set = function (element, h) { + api.set(element, h); + }; + + var get = function (element) { + return api.get(element); + }; + + var getOuter = function (element) { + return api.getOuter(element); + }; + + var setMax = function (element, value) { + // These properties affect the absolute max-height, they are not counted natively, we want to include these properties. + var inclusions = [ 'margin-left', 'border-left-width', 'padding-left', 'padding-right', 'border-right-width', 'margin-right' ]; + var absMax = api.max(element, value, inclusions); + Css.set(element, 'max-width', absMax + 'px'); + }; + + return { + set: set, + get: get, + getOuter: getOuter, + setMax: setMax + }; + } +); + +define( + 'ephox.alloy.ui.slider.SliderUi', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.parts.AlloyParts', + 'ephox.alloy.ui.slider.SliderActions', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Option', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.view.Width' + ], + + function (Behaviour, Keying, Representing, AlloyEvents, NativeEvents, AlloyParts, SliderActions, Arr, Fun, Merger, Option, PlatformDetection, Css, Width) { + var isTouch = PlatformDetection.detect().deviceType.isTouch(); + + var sketch = function (detail, components, spec, externals) { + var range = detail.max() - detail.min(); + + var getXCentre = function (component) { + var rect = component.element().dom().getBoundingClientRect(); + return (rect.left + rect.right) / 2; + }; + + var getThumb = function (component) { + return AlloyParts.getPartOrDie(component, detail, 'thumb'); + }; + + var getXOffset = function (slider, spectrumBounds, detail) { + var v = detail.value().get(); + if (v < detail.min()) { + return AlloyParts.getPart(slider, detail, 'left-edge').fold(function () { + return 0; + }, function (ledge) { + return getXCentre(ledge) - spectrumBounds.left; + }); + } else if (v > detail.max()) { + // position at right edge + return AlloyParts.getPart(slider, detail, 'right-edge').fold(function () { + return spectrumBounds.width; + }, function (redge) { + return getXCentre(redge) - spectrumBounds.left; + }); + } else { + // position along the slider + return (detail.value().get() - detail.min()) / range * spectrumBounds.width; + } + }; + + var getXPos = function (slider) { + var spectrum = AlloyParts.getPartOrDie(slider, detail, 'spectrum'); + var spectrumBounds = spectrum.element().dom().getBoundingClientRect(); + var sliderBounds = slider.element().dom().getBoundingClientRect(); + + var xOffset = getXOffset(slider, spectrumBounds, detail); + return (spectrumBounds.left - sliderBounds.left) + xOffset; + }; + + var refresh = function (component) { + var pos = getXPos(component); + var thumb = getThumb(component); + var thumbRadius = Width.get(thumb.element()) / 2; + Css.set(thumb.element(), 'left', (pos - thumbRadius) + 'px'); + }; + + var changeValue = function (component, newValue) { + var oldValue = detail.value().get(); + var thumb = getThumb(component); + // The left check is used so that the first click calls refresh + if (oldValue !== newValue || Css.getRaw(thumb.element(), 'left').isNone()) { + detail.value().set(newValue); + refresh(component); + detail.onChange()(component, thumb, newValue); + return Option.some(true); + } else { + return Option.none(); + } + }; + + var resetToMin = function (slider) { + changeValue(slider, detail.min(), Option.none()); + }; + + var resetToMax = function (slider) { + changeValue(slider, detail.max(), Option.none()); + }; + + var uiEventsArr = isTouch ? [ + AlloyEvents.run(NativeEvents.touchstart(), function (slider, simulatedEvent) { + detail.onDragStart()(slider, getThumb(slider)); + }), + AlloyEvents.run(NativeEvents.touchend(), function (slider, simulatedEvent) { + detail.onDragEnd()(slider, getThumb(slider)); + }) + ] : [ + AlloyEvents.run(NativeEvents.mousedown(), function (slider, simulatedEvent) { + simulatedEvent.stop(); + detail.onDragStart()(slider, getThumb(slider)); + detail.mouseIsDown().set(true); + }), + AlloyEvents.run(NativeEvents.mouseup(), function (slider, simulatedEvent) { + detail.onDragEnd()(slider, getThumb(slider)); + detail.mouseIsDown().set(false); + }) + ]; + + return { + uid: detail.uid(), + dom: detail.dom(), + components: components, + + behaviours: Merger.deepMerge( + Behaviour.derive( + Arr.flatten([ + !isTouch ? [ + Keying.config({ + mode: 'special', + focusIn: function (slider) { + return AlloyParts.getPart(slider, detail, 'spectrum').map(Keying.focusIn).map(Fun.constant(true)); + } + }) + ] : [], + [ + Representing.config({ + store: { + mode: 'manual', + getValue: function (_) { + return detail.value().get(); + } + } + }) + ] + ]) + ), + detail.sliderBehaviours() + ), + + events: AlloyEvents.derive( + [ + AlloyEvents.run(SliderActions.changeEvent(), function (slider, simulatedEvent) { + changeValue(slider, simulatedEvent.event().value()); + }), + AlloyEvents.runOnAttached(function (slider, simulatedEvent) { + detail.value().set(detail.getInitialValue()()); + var thumb = getThumb(slider); + // Call onInit instead of onChange for the first value. + refresh(slider); + detail.onInit()(slider, thumb, detail.value().get()); + }) + ].concat(uiEventsArr) + ), + + apis: { + resetToMin: resetToMin, + resetToMax: resetToMax, + refresh: refresh + }, + + domModification: { + styles: { + position: 'relative' + } + } + }; + }; + + return { + sketch: sketch + }; + } +); +define( + 'ephox.alloy.api.ui.Slider', + + [ + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.ui.slider.SliderParts', + 'ephox.alloy.ui.slider.SliderSchema', + 'ephox.alloy.ui.slider.SliderUi', + 'global!Math' + ], + + function (Sketcher, SliderParts, SliderSchema, SliderUi, Math) { + return Sketcher.composite({ + name: 'Slider', + configFields: SliderSchema, + partFields: SliderParts, + factory: SliderUi.sketch, + apis: { + resetToMin: function (apis, slider) { + apis.resetToMin(slider); + }, + resetToMax: function (apis, slider) { + apis.resetToMax(slider); + }, + refresh: function (apis, slider) { + apis.refresh(slider); + } + } + }); + } +); +define( + 'tinymce.themes.mobile.ui.ToolbarWidgets', + + [ + 'tinymce.themes.mobile.ui.Buttons' + ], + + function (Buttons) { + var button = function (realm, clazz, makeItems) { + return Buttons.forToolbar(clazz, function () { + var items = makeItems(); + realm.setContextToolbar([ + { + // FIX: I18n + label: clazz + ' group', + items: items + } + ]); + }, { }); + }; + + return { + button: button + }; + } +); + +define( + 'tinymce.themes.mobile.ui.ColorSlider', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Receiving', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.ui.Slider', + 'ephox.sugar.api.properties.Css', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.ui.ToolbarWidgets', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function (Behaviour, Receiving, Toggling, Slider, Css, Receivers, Styles, ToolbarWidgets, UiDomFactory) { + var BLACK = -1; + + var makeSlider = function (spec) { + var getColor = function (hue) { + // Handle edges. + if (hue < 0) { + return 'black'; + } else if (hue > 360) { + return 'white'; + } else { + return 'hsl(' + hue + ', 100%, 50%)'; + } + }; + + // Does not fire change intentionally. + var onInit = function (slider, thumb, value) { + var color = getColor(value); + Css.set(thumb.element(), 'background-color', color); + }; + + var onChange = function (slider, thumb, value) { + var color = getColor(value); + Css.set(thumb.element(), 'background-color', color); + spec.onChange(slider, thumb, color); + }; + + return Slider.sketch({ + dom: UiDomFactory.dom('
    '), + components: [ + Slider.parts()['left-edge'](UiDomFactory.spec('
    ')), + Slider.parts().spectrum({ + dom: UiDomFactory.dom('
    '), + components: [ + UiDomFactory.spec('
    ') + ], + behaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('thumb-active') + }) + ]) + }), + Slider.parts()['right-edge'](UiDomFactory.spec('
    ')), + Slider.parts().thumb({ + dom: UiDomFactory.dom('
    '), + behaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('thumb-active') + }) + ]) + }) + ], + + onChange: onChange, + onDragStart: function (slider, thumb) { + Toggling.on(thumb); + }, + onDragEnd: function (slider, thumb) { + Toggling.off(thumb); + }, + onInit: onInit, + stepSize: 10, + min: 0, + max: 360, + getInitialValue: spec.getInitialValue, + + sliderBehaviours: Behaviour.derive([ + Receivers.orientation(Slider.refresh) + ]) + }); + }; + + var makeItems = function (spec) { + return [ + makeSlider(spec) + ]; + }; + + var sketch = function (realm, editor) { + var spec = { + onChange: function (slider, thumb, color) { + editor.undoManager.transact(function () { + editor.formatter.apply('forecolor', { value: color }); + editor.nodeChanged(); + }); + }, + getInitialValue: function (/* slider */) { + // Return black + return BLACK; + } + }; + + return ToolbarWidgets.button(realm, 'color', function () { + return makeItems(spec); + }); + }; + + return { + makeItems: makeItems, + sketch: sketch + }; + } +); +define( + 'tinymce.themes.mobile.ui.SizeSlider', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Receiving', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.ui.Slider', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function (Behaviour, Receiving, Toggling, Slider, FieldSchema, ValueSchema, Receivers, Styles, UiDomFactory) { + var schema = ValueSchema.objOfOnly([ + FieldSchema.strict('getInitialValue'), + FieldSchema.strict('onChange'), + FieldSchema.strict('category'), + FieldSchema.strict('sizes') + ]); + + var sketch = function (rawSpec) { + var spec = ValueSchema.asRawOrDie('SizeSlider', schema, rawSpec); + + var isValidValue = function (valueIndex) { + return valueIndex >= 0 && valueIndex < spec.sizes.length; + }; + + var onChange = function (slider, thumb, valueIndex) { + if (isValidValue(valueIndex)) { + spec.onChange(valueIndex); + } + }; + + return Slider.sketch({ + dom: { + tag: 'div', + classes: [ + Styles.resolve('slider-' + spec.category + '-size-container'), + Styles.resolve('slider'), + Styles.resolve('slider-size-container') ] + }, + onChange: onChange, + onDragStart: function (slider, thumb) { + Toggling.on(thumb); + }, + onDragEnd: function (slider, thumb) { + Toggling.off(thumb); + }, + min: 0, + max: spec.sizes.length - 1, + stepSize: 1, + getInitialValue: spec.getInitialValue, + snapToGrid: true, + + sliderBehaviours: Behaviour.derive([ + Receivers.orientation(Slider.refresh) + ]), + + components: [ + Slider.parts().spectrum({ + dom: UiDomFactory.dom('
    '), + components: [ + UiDomFactory.spec('
    ') + ] + }), + + Slider.parts().thumb({ + dom: UiDomFactory.dom('
    '), + behaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('thumb-active') + }) + ]) + }) + ] + }); + }; + + return { + sketch: sketch + }; + } +); + +define( + 'ephox.sugar.api.search.TransformFind', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element' + ], + + function (Type, Fun, Option, Element) { + var ancestor = function (scope, transform, 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); + + var transformed = transform(el); + if (transformed.isSome()) return transformed; + else if (stop(el)) break; + } + return Option.none(); + }; + + var closest = function (scope, transform, isRoot) { + var current = transform(scope); + return current.orThunk(function () { + return isRoot(scope) ? Option.none() : ancestor(scope, transform, isRoot); + }); + }; + + return { + ancestor: ancestor, + closest: closest + }; + } +); +define( + 'tinymce.themes.mobile.util.FontSizes', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.TransformFind', + 'ephox.sugar.api.search.Traverse' + ], + + function (Arr, Fun, Option, Compare, Element, Node, Css, TransformFind, Traverse) { + var candidates = [ '9px', '10px', '11px', '12px', '14px', '16px', '18px', '20px', '24px', '32px', '36px' ]; + + var defaultSize = 'medium'; + var defaultIndex = 2; + + var indexToSize = function (index) { + return Option.from(candidates[index]); + }; + + var sizeToIndex = function (size) { + return Arr.findIndex(candidates, function (v) { + return v === size; + }); + }; + + var getRawOrComputed = function (isRoot, rawStart) { + var optStart = Node.isElement(rawStart) ? Option.some(rawStart) : Traverse.parent(rawStart); + return optStart.map(function (start) { + var inline = TransformFind.closest(start, function (elem) { + return Css.getRaw(elem, 'font-size'); + }, isRoot); + + return inline.getOrThunk(function () { + return Css.get(start, 'font-size'); + }); + }).getOr(''); + }; + + var getSize = function (editor) { + // This was taken from the tinymce approach (FontInfo is unlikely to be global) + var node = editor.selection.getStart(); + var elem = Element.fromDom(node); + var root = Element.fromDom(editor.getBody()); + + var isRoot = function (e) { + return Compare.eq(root, e); + }; + + var elemSize = getRawOrComputed(isRoot, elem); + return Arr.find(candidates, function (size) { + return elemSize === size; + }).getOr(defaultSize); + }; + + var applySize = function (editor, value) { + var currentValue = getSize(editor); + if (currentValue !== value) { + editor.execCommand('fontSize', false, value); + } + }; + + var get = function (editor) { + var size = getSize(editor); + return sizeToIndex(size).getOr(defaultIndex); + }; + + var apply = function (editor, index) { + indexToSize(index).each(function (size) { + applySize(editor, size); + }); + }; + + return { + candidates: Fun.constant(candidates), + get: get, + apply: apply + }; + } +); + +define( + 'tinymce.themes.mobile.ui.FontSizeSlider', + + [ + 'tinymce.themes.mobile.ui.SizeSlider', + 'tinymce.themes.mobile.ui.ToolbarWidgets', + 'tinymce.themes.mobile.util.FontSizes', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + + function (SizeSlider, ToolbarWidgets, FontSizes, UiDomFactory) { + var sizes = FontSizes.candidates(); + + var makeSlider = function (spec) { + return SizeSlider.sketch({ + onChange: spec.onChange, + sizes: sizes, + category: 'font', + getInitialValue: spec.getInitialValue + }); + }; + + var makeItems = function (spec) { + return [ + UiDomFactory.spec(''), + makeSlider(spec), + UiDomFactory.spec('') + ]; + }; + + var sketch = function (realm, editor) { + var spec = { + onChange: function (value) { + FontSizes.apply(editor, value); + }, + getInitialValue: function (/* slider */) { + return FontSizes.get(editor); + } + }; + + return ToolbarWidgets.button(realm, 'font-size', function () { + return makeItems(spec); + }); + }; + + return { + makeItems: makeItems, + sketch: sketch + }; + } +); + +/* eslint-disable */ +/* jshint ignore:start */ + +/** + * Modifed to be a feature fill and wrapped as tinymce module. + * + * Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill + */ +define( + 'ephox.imagetools.util.Promise', + [ + ], + function () { + if (window.Promise) { + return window.Promise; + } + + // Use polyfill for setImmediate for performance gains + var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) || + function (fn) { setTimeout(fn, 1); }; + + // Polyfill for Function.prototype.bind + function bind(fn, thisArg) { + return function () { + fn.apply(thisArg, arguments); + }; + } + + var isArray = Array.isArray || function (value) { return Object.prototype.toString.call(value) === "[object Array]"; }; + + function Promise(fn) { + if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new'); + if (typeof fn !== 'function') throw new TypeError('not a function'); + this._state = null; + this._value = null; + this._deferreds = []; + + doResolve(fn, bind(resolve, this), bind(reject, this)); + } + + function handle(deferred) { + var me = this; + if (this._state === null) { + this._deferreds.push(deferred); + return; + } + asap(function () { + var cb = me._state ? deferred.onFulfilled : deferred.onRejected; + if (cb === null) { + (me._state ? deferred.resolve : deferred.reject)(me._value); + return; + } + var ret; + try { + ret = cb(me._value); + } + catch (e) { + deferred.reject(e); + return; + } + deferred.resolve(ret); + }); + } + + function resolve(newValue) { + try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure + if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.'); + if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { + var then = newValue.then; + if (typeof then === 'function') { + doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this)); + return; + } + } + this._state = true; + this._value = newValue; + finale.call(this); + } catch (e) { reject.call(this, e); } + } + + function reject(newValue) { + this._state = false; + this._value = newValue; + finale.call(this); + } + + function finale() { + for (var i = 0, len = this._deferreds.length; i < len; i++) { + handle.call(this, this._deferreds[i]); + } + this._deferreds = null; + } + + function Handler(onFulfilled, onRejected, resolve, reject) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.resolve = resolve; + this.reject = reject; + } + + /** + * Take a potentially misbehaving resolver function and make sure + * onFulfilled and onRejected are only called once. + * + * Makes no guarantees about asynchrony. + */ + function doResolve(fn, onFulfilled, onRejected) { + var done = false; + try { + fn(function (value) { + if (done) return; + done = true; + onFulfilled(value); + }, function (reason) { + if (done) return; + done = true; + onRejected(reason); + }); + } catch (ex) { + if (done) return; + done = true; + onRejected(ex); + } + } + + Promise.prototype['catch'] = function (onRejected) { + return this.then(null, onRejected); + }; + + Promise.prototype.then = function (onFulfilled, onRejected) { + var me = this; + return new Promise(function (resolve, reject) { + handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject)); + }); + }; + + Promise.all = function () { + var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments); + + return new Promise(function (resolve, reject) { + if (args.length === 0) return resolve([]); + var remaining = args.length; + function res(i, val) { + try { + if (val && (typeof val === 'object' || typeof val === 'function')) { + var then = val.then; + if (typeof then === 'function') { + then.call(val, function (val) { res(i, val); }, reject); + return; + } + } + args[i] = val; + if (--remaining === 0) { + resolve(args); + } + } catch (ex) { + reject(ex); + } + } + for (var i = 0; i < args.length; i++) { + res(i, args[i]); + } + }); + }; + + Promise.resolve = function (value) { + if (value && typeof value === 'object' && value.constructor === Promise) { + return value; + } + + return new Promise(function (resolve) { + resolve(value); + }); + }; + + Promise.reject = function (value) { + return new Promise(function (resolve, reject) { + reject(value); + }); + }; + + Promise.race = function (values) { + return new Promise(function (resolve, reject) { + for (var i = 0, len = values.length; i < len; i++) { + values[i].then(resolve, reject); + } + }); + }; + + return Promise; + }); + +/* jshint ignore:end */ +/* eslint-enable */ + +define( + 'ephox.imagetools.util.Canvas', + [ + ], + function () { + function create(width, height) { + return resize(document.createElement('canvas'), width, height); + } + + function clone(canvas) { + var tCanvas, ctx; + tCanvas = create(canvas.width, canvas.height); + ctx = get2dContext(tCanvas); + ctx.drawImage(canvas, 0, 0); + return tCanvas; + } + + function get2dContext(canvas) { + return canvas.getContext("2d"); + } + + function get3dContext(canvas) { + var gl = null; + try { + gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); + } + catch (e) { } + + if (!gl) { // it seems that sometimes it doesn't throw exception, but still fails to get context + gl = null; + } + return gl; + } + + function resize(canvas, width, height) { + canvas.width = width; + canvas.height = height; + + return canvas; + } + + return { + create: create, + clone: clone, + resize: resize, + get2dContext: get2dContext, + get3dContext: get3dContext + }; + }); +define( + 'ephox.imagetools.util.Mime', + [ + ], + function () { + function getUriPathName(uri) { + var a = document.createElement('a'); + a.href = uri; + return a.pathname; + } + + function guessMimeType(uri) { + var parts, ext, mimes, matches; + + if (uri.indexOf('data:') === 0) { + uri = uri.split(','); + matches = /data:([^;]+)/.exec(uri[0]); + return matches ? matches[1] : ''; + } else { + mimes = { + 'jpg': 'image/jpeg', + 'jpeg': 'image/jpeg', + 'png': 'image/png' + }; + + parts = getUriPathName(uri).split('.'); + ext = parts[parts.length - 1]; + + if (ext) { + ext = ext.toLowerCase(); + } + return mimes[ext]; + } + } + + + return { + guessMimeType: guessMimeType + }; + }); +define( + 'ephox.imagetools.util.ImageSize', + [ + ], + function() { + function getWidth(image) { + return image.naturalWidth || image.width; + } + + function getHeight(image) { + return image.naturalHeight || image.height; + } + + return { + getWidth: getWidth, + getHeight: getHeight + }; +}); +define( + 'ephox.imagetools.util.Conversions', + [ + 'ephox.imagetools.util.Promise', + 'ephox.imagetools.util.Canvas', + 'ephox.imagetools.util.Mime', + 'ephox.imagetools.util.ImageSize' + ], + function (Promise, Canvas, Mime, ImageSize) { + function loadImage(image) { + return new Promise(function (resolve) { + function loaded() { + image.removeEventListener('load', loaded); + resolve(image); + } + + if (image.complete) { + resolve(image); + } else { + image.addEventListener('load', loaded); + } + }); + } + + function imageToCanvas(image) { + return loadImage(image).then(function (image) { + var context, canvas; + + canvas = Canvas.create(ImageSize.getWidth(image), ImageSize.getHeight(image)); + context = Canvas.get2dContext(canvas); + context.drawImage(image, 0, 0); + + return canvas; + }); + } + + function imageToBlob(image) { + return loadImage(image).then(function (image) { + var src = image.src; + + if (src.indexOf('blob:') === 0) { + return blobUriToBlob(src); + } + + if (src.indexOf('data:') === 0) { + return dataUriToBlob(src); + } + + return imageToCanvas(image).then(function (canvas) { + return dataUriToBlob(canvas.toDataURL(Mime.guessMimeType(src))); + }); + }); + } + + function blobToImage(blob) { + return new Promise(function (resolve) { + var image = new Image(); + + function loaded() { + image.removeEventListener('load', loaded); + resolve(image); + } + + image.addEventListener('load', loaded); + image.src = URL.createObjectURL(blob); + + if (image.complete) { + loaded(); + } + }); + } + + function blobUriToBlob(url) { + return new Promise(function (resolve) { + var xhr = new XMLHttpRequest(); + + xhr.open('GET', url, true); + xhr.responseType = 'blob'; + + xhr.onload = function () { + if (this.status == 200) { + resolve(this.response); + } + }; + + xhr.send(); + }); + } + + function dataUriToBlobSync(uri) { + var str, arr, i, matches, type, blobBuilder; + + uri = uri.split(','); + + matches = /data:([^;]+)/.exec(uri[0]); + if (matches) { + type = matches[1]; + } + + str = atob(uri[1]); + + if (window.WebKitBlobBuilder) { + /*globals WebKitBlobBuilder:false */ + blobBuilder = new WebKitBlobBuilder(); + + arr = new ArrayBuffer(str.length); + for (i = 0; i < arr.length; i++) { + arr[i] = str.charCodeAt(i); + } + + blobBuilder.append(arr); + return blobBuilder.getBlob(type); + } + + arr = new Uint8Array(str.length); + for (i = 0; i < arr.length; i++) { + arr[i] = str.charCodeAt(i); + } + return new Blob([arr], { type: type }); + } + + function dataUriToBlob(uri) { + return new Promise(function (resolve) { + resolve(dataUriToBlobSync(uri)); + }); + } + + function uriToBlob(url) { + if (url.indexOf('blob:') === 0) { + return blobUriToBlob(url); + } + + if (url.indexOf('data:') === 0) { + return dataUriToBlob(url); + } + + return null; + } + + function canvasToBlob(canvas, type, quality) { + type = type || 'image/png'; + + if (HTMLCanvasElement.prototype.toBlob) { + return new Promise(function (resolve) { + canvas.toBlob(function (blob) { + resolve(blob); + }, type, quality); + }); + } else { + return dataUriToBlob(canvas.toDataURL(type, quality)); + } + } + + function blobToDataUri(blob) { + return new Promise(function (resolve) { + var reader = new FileReader(); + + reader.onloadend = function () { + resolve(reader.result); + }; + + reader.readAsDataURL(blob); + }); + } + + function blobToBase64(blob) { + return blobToDataUri(blob).then(function (dataUri) { + return dataUri.split(',')[1]; + }); + } + + function revokeImageUrl(image) { + URL.revokeObjectURL(image.src); + } + + return { + // used outside + blobToImage: blobToImage, + // used outside + imageToBlob: imageToBlob, + // used outside + blobToDataUri: blobToDataUri, + // used outside + blobToBase64: blobToBase64, + + // helper method + imageToCanvas: imageToCanvas, + // helper method + canvasToBlob: canvasToBlob, + // helper method + revokeImageUrl: revokeImageUrl, + // helper method + uriToBlob: uriToBlob, + // helper method + dataUriToBlobSync: dataUriToBlobSync + }; + }); +define( + 'ephox.imagetools.util.ImageResult', + [ + 'ephox.imagetools.util.Promise', + 'ephox.imagetools.util.Conversions', + 'ephox.imagetools.util.Mime', + 'ephox.imagetools.util.Canvas' + ], + function (Promise, Conversions, Mime, Canvas) { + function create(canvas, initialType) { + function getType() { + return initialType; + } + + function toBlob(type, quality) { + return Conversions.canvasToBlob(canvas, type || initialType, quality); + } + + function toDataURL(type, quality) { + return canvas.toDataURL(type || initialType, quality); + } + + function toBase64(type, quality) { + return toDataURL(type, quality).split(',')[1]; + } + + function toCanvas() { + return Canvas.clone(canvas); + } + + return { + getType: getType, + toBlob: toBlob, + toDataURL: toDataURL, + toBase64: toBase64, + toCanvas: toCanvas + }; + } + + function fromBlob(blob) { + return Conversions.blobToImage(blob) + .then(function (image) { + var result = Conversions.imageToCanvas(image); + Conversions.revokeImageUrl(image); + return result; + }) + .then(function (canvas) { + return create(canvas, blob.type); + }); + } + + function fromCanvas(canvas, type) { + return new Promise(function (resolve) { + resolve(create(canvas, type)); + }); + } + + function fromImage(image) { + var type = Mime.guessMimeType(image.src); + return Conversions.imageToCanvas(image).then(function (canvas) { + return create(canvas, type); + }); + } + + return { + fromBlob: fromBlob, + fromCanvas: fromCanvas, + fromImage: fromImage + }; + }); + +define( + 'ephox.imagetools.api.BlobConversions', + [ + 'ephox.imagetools.util.Conversions', + 'ephox.imagetools.util.ImageResult' + ], + function (Conversions, ImageResult) { + var blobToImage = function (image) { + return Conversions.blobToImage(image); + }; + + var imageToBlob = function (blob) { + return Conversions.imageToBlob(blob); + }; + + var blobToDataUri = function (blob) { + return Conversions.blobToDataUri(blob); + }; + + var blobToBase64 = function (blob) { + return Conversions.blobToBase64(blob); + }; + + var blobToImageResult = function(blob) { + return ImageResult.fromBlob(blob); + }; + + var dataUriToImageResult = function(uri) { + return Conversions.uriToBlob(uri).then(ImageResult.fromBlob); + }; + + var imageToImageResult = function(image) { + return ImageResult.fromImage(image); + }; + + var imageResultToBlob = function(ir, type, quality) { + return ir.toBlob(type, quality); + }; + + var imageResultToBlobSync = function(ir, type, quality) { + return Conversions.dataUriToBlobSync(ir.toDataURL(type, quality)); + }; + + return { + // used outside + blobToImage: blobToImage, + // used outside + imageToBlob: imageToBlob, + // used outside + blobToDataUri: blobToDataUri, + // used outside + blobToBase64: blobToBase64, + // used outside + blobToImageResult: blobToImageResult, + // used outside + dataUriToImageResult: dataUriToImageResult, + // used outside + imageToImageResult: imageToImageResult, + // used outside + imageResultToBlob: imageResultToBlob, + // just in case + imageResultToBlobSync: imageResultToBlobSync + }; + } +); +define( + 'tinymce.themes.mobile.ui.ImagePicker', + + [ + 'ephox.alloy.api.component.Memento', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.ui.Button', + 'ephox.imagetools.api.BlobConversions', + 'ephox.katamari.api.Id', + 'ephox.katamari.api.Option', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function (Memento, AlloyEvents, NativeEvents, Button, BlobConversions, Id, Option, UiDomFactory) { + var addImage = function (editor, blob) { + BlobConversions.blobToBase64(blob).then(function (base64) { + editor.undoManager.transact(function () { + var cache = editor.editorUpload.blobCache; + var info = cache.create( + Id.generate('mceu'), blob, base64 + ); + cache.add(info); + var img = editor.dom.createHTML('img', { + src: info.blobUri() + }); + editor.insertContent(img); + }); + }); + }; + + var extractBlob = function (simulatedEvent) { + var event = simulatedEvent.event(); + var files = event.raw().target.files || event.raw().dataTransfer.files; + return Option.from(files[0]); + }; + + var sketch = function (editor) { + var pickerDom = { + tag: 'input', + attributes: { accept: 'image/*', type: 'file', title: '' }, + // Visibility hidden so that it cannot be seen, and position absolute so that it doesn't + // disrupt the layout + styles: { visibility: 'hidden', position: 'absolute' } + }; + + var memPicker = Memento.record({ + dom: pickerDom, + events: AlloyEvents.derive([ + // Stop the event firing again at the button level + AlloyEvents.cutter(NativeEvents.click()), + + AlloyEvents.run(NativeEvents.change(), function (picker, simulatedEvent) { + extractBlob(simulatedEvent).each(function (blob) { + addImage(editor, blob); + }); + }) + ]) + }); + + return Button.sketch({ + dom: UiDomFactory.dom(''), + components: [ + memPicker.asSpec() + ], + action: function (button) { + var picker = memPicker.get(button); + // Trigger a dom click for the file input + picker.element().dom().click(); + } + }); + }; + + return { + sketch: sketch + }; + } +); + +define( + 'ephox.sugar.api.properties.TextContent', + + [ + + ], + + function () { + // REQUIRES IE9 + var get = function (element) { + return element.dom().textContent; + }; + + var set = function (element, value) { + element.dom().textContent = value; + }; + + return { + get: get, + set: set + }; + } +); + +define( + 'tinymce.themes.mobile.bridge.LinkBridge', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.TextContent', + 'ephox.sugar.api.search.SelectorFind' + ], + + function (Fun, Option, Element, Attr, TextContent, SelectorFind) { + var isNotEmpty = function (val) { + return val.length > 0; + }; + + var defaultToEmpty = function (str) { + return str === undefined || str === null ? '' : str; + }; + + var noLink = function (editor) { + var text = editor.selection.getContent({ format: 'text' }); + return { + url: '', + text: text, + title: '', + target: '', + link: Option.none() + }; + }; + + var fromLink = function (link) { + var text = TextContent.get(link); + var url = Attr.get(link, 'href'); + var title = Attr.get(link, 'title'); + var target = Attr.get(link, 'target'); + return { + url: defaultToEmpty(url), + text: text !== url ? defaultToEmpty(text) : '', + title: defaultToEmpty(title), + target: defaultToEmpty(target), + link: Option.some(link) + }; + }; + + var getInfo = function (editor) { + // TODO: Improve with more of tiny's link logic? + return query(editor).fold( + function () { + return noLink(editor); + }, + function (link) { + return fromLink(link); + } + ); + }; + + var wasSimple = function (link) { + var prevHref = Attr.get(link, 'href'); + var prevText = TextContent.get(link); + return prevHref === prevText; + }; + + var getTextToApply = function (link, url, info) { + return info.text.filter(isNotEmpty).fold(function () { + return wasSimple(link) ? Option.some(url) : Option.none(); + }, Option.some); + }; + + var unlinkIfRequired = function (editor, info) { + var activeLink = info.link.bind(Fun.identity); + activeLink.each(function (link) { + editor.execCommand('unlink'); + }); + }; + + var getAttrs = function (url, info) { + var attrs = { }; + attrs.href = url; + + info.title.filter(isNotEmpty).each(function (title) { + attrs.title = title; + }); + info.target.filter(isNotEmpty).each(function (target) { + attrs.target = target; + }); + return attrs; + }; + + var applyInfo = function (editor, info) { + info.url.filter(isNotEmpty).fold(function () { + // Unlink if there is something to unlink + unlinkIfRequired(editor, info); + }, function (url) { + // We must have a non-empty URL to insert a link + var attrs = getAttrs(url, info); + + var activeLink = info.link.bind(Fun.identity); + activeLink.fold(function () { + var text = info.text.filter(isNotEmpty).getOr(url); + editor.insertContent(editor.dom.createHTML('a', attrs, editor.dom.encode(text))); + }, function (link) { + var text = getTextToApply(link, url, info); + Attr.setAll(link, attrs); + text.each(function (newText) { + TextContent.set(link, newText); + }); + }); + }); + }; + + var query = function (editor) { + var start = Element.fromDom(editor.selection.getStart()); + return SelectorFind.closest(start, 'a'); + }; + + return { + getInfo: getInfo, + applyInfo: applyInfo, + query: query + }; + } +); + +define( + 'ephox.alloy.api.behaviour.AddEventsBehaviour', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun' + ], + + function (Behaviour, AlloyEvents, FieldSchema, Fun) { + var events = function (name, eventHandlers) { + var events = AlloyEvents.derive(eventHandlers); + + return Behaviour.create({ + fields: [ + FieldSchema.strict('enabled') + ], + name: name, + active: { + events: Fun.constant(events) + } + }); + }; + + var config = function (name, eventHandlers) { + var me = events(name, eventHandlers); + + return { + key: name, + value: { + config: { }, + me: me, + configAsRaw: Fun.constant({ }), + initialConfig: { }, + state: Behaviour.noState() + } + }; + }; + + return { + events: events, + config: config + }; + } +); +define( + 'ephox.alloy.behaviour.composing.ComposeApis', + + [ + + ], + + function () { + var getCurrent = function (component, composeConfig, composeState) { + return composeConfig.find()(component); + }; + + return { + getCurrent: getCurrent + }; + } +); +define( + 'ephox.alloy.behaviour.composing.ComposeSchema', + + [ + 'ephox.boulder.api.FieldSchema' + ], + + function (FieldSchema) { + return [ + FieldSchema.strict('find') + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Composing', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.composing.ComposeApis', + 'ephox.alloy.behaviour.composing.ComposeSchema' + ], + + function (Behaviour, ComposeApis, ComposeSchema) { + return Behaviour.create({ + fields: ComposeSchema, + name: 'composing', + apis: ComposeApis + }); + } +); +define( + 'ephox.alloy.api.ui.Container', + + [ + 'ephox.alloy.api.ui.Sketcher', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Merger' + ], + + function (Sketcher, FieldSchema, Merger) { + var factory = function (detail, spec) { + return { + uid: detail.uid(), + dom: Merger.deepMerge( + { + tag: 'div', + attributes: { + role: 'presentation' + } + }, + detail.dom() + ), + components: detail.components(), + behaviours: detail.containerBehaviours(), + events: detail.events(), + domModification: detail.domModification(), + eventOrder: detail.eventOrder() + }; + }; + + return Sketcher.single({ + name: 'Container', + factory: factory, + configFields: [ + FieldSchema.defaulted('components', [ ]), + FieldSchema.defaulted('containerBehaviours', { }), + FieldSchema.defaulted('events', { }), + FieldSchema.defaulted('domModification', { }), + FieldSchema.defaulted('eventOrder', { }) + ] + }); + } +); +define( + 'ephox.alloy.api.ui.DataField', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Composing', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.ui.Sketcher', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Option' + ], + + function (Behaviour, Composing, Representing, AlloyEvents, Sketcher, FieldSchema, Option) { + var factory = function (detail, spec) { + return { + uid: detail.uid(), + dom: detail.dom(), + behaviours: Behaviour.derive([ + Representing.config({ + store: { + mode: 'memory', + initialValue: detail.getInitialValue()() + } + }), + Composing.config({ + find: Option.some + }) + ]), + events: AlloyEvents.derive([ + AlloyEvents.runOnAttached(function (component, simulatedEvent) { + Representing.setValue(component, detail.getInitialValue()()); + }) + ]) + }; + }; + + return Sketcher.single({ + name: 'DataField', + factory: factory, + configFields: [ + FieldSchema.strict('uid'), + FieldSchema.strict('dom'), + FieldSchema.strict('getInitialValue') + ] + }); + } +); +define( + 'ephox.alloy.behaviour.tabstopping.ActiveTabstopping', + + [ + 'ephox.alloy.dom.DomModification', + 'ephox.boulder.api.Objects' + ], + + function (DomModification, Objects) { + var exhibit = function (base, tabConfig) { + return DomModification.nu({ + attributes: Objects.wrapAll([ + { key: tabConfig.tabAttr(), value: 'true' } + ]) + }); + }; + + return { + exhibit: exhibit + }; + } +); +define( + 'ephox.alloy.behaviour.tabstopping.TabstopSchema', + + [ + 'ephox.boulder.api.FieldSchema' + ], + + function (FieldSchema) { + return [ + FieldSchema.defaulted('tabAttr', 'data-alloy-tabstop') + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Tabstopping', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.tabstopping.ActiveTabstopping', + 'ephox.alloy.behaviour.tabstopping.TabstopSchema' + ], + + function (Behaviour, ActiveTabstopping, TabstopSchema) { + return Behaviour.create({ + fields: TabstopSchema, + name: 'tabstopping', + active: ActiveTabstopping + }); + } +); +define( + 'ephox.sugar.api.properties.Value', + + [ + 'global!Error' + ], + + function (Error) { + var get = function (element) { + return element.dom().value; + }; + + var set = function (element, value) { + if (value === undefined) throw new Error('Value.set was undefined'); + element.dom().value = value; + }; + + return { + set: set, + get: get + }; + } +); + +define( + 'ephox.alloy.ui.common.InputBase', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Focusing', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.behaviour.Tabstopping', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.sugar.api.properties.Value' + ], + + function (Behaviour, Focusing, Representing, Tabstopping, Fields, FieldSchema, Objects, Fun, Merger, Value) { + + var schema = [ + FieldSchema.option('data'), + FieldSchema.defaulted('inputAttributes', { }), + FieldSchema.defaulted('inputStyles', { }), + FieldSchema.defaulted('type', 'input'), + FieldSchema.defaulted('tag', 'input'), + Fields.onHandler('onSetValue'), + FieldSchema.defaulted('styles', { }), + FieldSchema.option('placeholder'), + FieldSchema.defaulted('eventOrder', { }), + FieldSchema.defaulted('hasTabstop', true), + FieldSchema.defaulted('inputBehaviours', { }), + FieldSchema.defaulted('selectOnFocus', true) + ]; + + var behaviours = function (detail) { + return Merger.deepMerge( + Behaviour.derive([ + Representing.config({ + store: { + mode: 'manual', + // Propagating its Option + initialValue: detail.data().getOr(undefined), + getValue: function (input) { + return Value.get(input.element()); + }, + setValue: function (input, data) { + var current = Value.get(input.element()); + // Only set it if it has changed ... otherwise the cursor goes to the end. + if (current !== data) { + Value.set(input.element(), data); + } + } + }, + onSetValue: detail.onSetValue() + }), + Focusing.config({ + onFocus: detail.selectOnFocus() === false ? Fun.noop : function (component) { + var input = component.element(); + var value = Value.get(input); + input.dom().setSelectionRange(0, value.length); + } + }), + detail.hasTabstop() ? Tabstopping.config({ }) : Tabstopping.revoke() + ]), + detail.inputBehaviours() + ); + }; + + var dom = function (detail) { + return { + tag: detail.tag(), + attributes: Merger.deepMerge( + Objects.wrapAll([ + { + key: 'type', + value: detail.type() + } + ].concat(detail.placeholder().map(function (pc) { + return { + key: 'placeholder', + value: pc + }; + }).toArray())), + detail.inputAttributes() + ), + styles: detail.inputStyles() + }; + }; + + return { + schema: Fun.constant(schema), + behaviours: behaviours, + dom: dom + }; + } +); +define( + 'ephox.alloy.api.ui.Input', + + [ + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.ui.common.InputBase' + ], + + function (Sketcher, InputBase) { + var factory = function (detail, spec) { + return { + uid: detail.uid(), + dom: InputBase.dom(detail), + // No children. + components: [ ], + behaviours: InputBase.behaviours(detail), + eventOrder: detail.eventOrder() + }; + }; + + return Sketcher.single({ + name: 'Input', + configFields: InputBase.schema(), + factory: factory + }); + } +); +define( + 'tinymce.themes.mobile.ui.Inputs', + + [ + 'ephox.alloy.api.behaviour.AddEventsBehaviour', + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Composing', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.component.Memento', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.ui.Button', + 'ephox.alloy.api.ui.Container', + 'ephox.alloy.api.ui.DataField', + 'ephox.alloy.api.ui.Input', + 'ephox.katamari.api.Option', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function ( + AddEventsBehaviour, Behaviour, Composing, Representing, Toggling, Memento, AlloyEvents, + AlloyTriggers, NativeEvents, Button, Container, DataField, Input, Option, Styles, UiDomFactory + ) { + var clearInputBehaviour = 'input-clearing'; + + var field = function (name, placeholder) { + var inputSpec = Memento.record(Input.sketch({ + placeholder: placeholder, + onSetValue: function (input, data) { + // If the value changes, inform the container so that it can update whether the "x" is visible + AlloyTriggers.emit(input, NativeEvents.input()); + }, + inputBehaviours: Behaviour.derive([ + Composing.config({ + find: Option.some + }) + ]), + selectOnFocus: false + })); + + var buttonSpec = Memento.record( + Button.sketch({ + dom: UiDomFactory.dom(''), + action: function (button) { + var input = inputSpec.get(button); + Representing.setValue(input, ''); + } + }) + ); + + return { + name: name, + spec: Container.sketch({ + dom: UiDomFactory.dom('
    '), + components: [ + inputSpec.asSpec(), + buttonSpec.asSpec() + ], + containerBehaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('input-container-empty') + }), + Composing.config({ + find: function (comp) { + return Option.some(inputSpec.get(comp)); + } + }), + AddEventsBehaviour.config(clearInputBehaviour, [ + // INVESTIGATE: Because this only happens on input, + // it won't reset unless it has an initial value + AlloyEvents.run(NativeEvents.input(), function (iContainer) { + var input = inputSpec.get(iContainer); + var val = Representing.getValue(input); + var f = val.length > 0 ? Toggling.off : Toggling.on; + f(iContainer); + }) + ]) + ]) + }) + }; + }; + + var hidden = function (name) { + return { + name: name, + spec: DataField.sketch({ + dom: { + tag: 'span', + styles: { + display: 'none' + } + }, + getInitialValue: function () { + return Option.none(); + } + }) + }; + }; + + return { + field: field, + hidden: hidden + }; + } +); +define( + 'ephox.alloy.behaviour.disabling.DisableApis', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.node.Node' + ], + + function (Arr, Attr, Class, Node) { + // Just use "disabled" attribute for these, not "aria-disabled" + var nativeDisabled = [ + 'input', + 'button', + 'textarea' + ]; + + var onLoad = function (component, disableConfig, disableState) { + if (disableConfig.disabled()) disable(component, disableConfig, disableState); + }; + + var hasNative = function (component) { + return Arr.contains(nativeDisabled, Node.name(component.element())); + }; + + var nativeIsDisabled = function (component) { + return Attr.has(component.element(), 'disabled'); + }; + + var nativeDisable = function (component) { + Attr.set(component.element(), 'disabled', 'disabled'); + }; + + var nativeEnable = function (component) { + Attr.remove(component.element(), 'disabled'); + }; + + var ariaIsDisabled = function (component) { + return Attr.get(component.element(), 'aria-disabled') === 'true'; + }; + + var ariaDisable = function (component) { + Attr.set(component.element(), 'aria-disabled', 'true'); + }; + + var ariaEnable = function (component) { + Attr.set(component.element(), 'aria-disabled', 'false'); + }; + + var disable = function (component, disableConfig, disableState) { + disableConfig.disableClass().each(function (disableClass) { + Class.add(component.element(), disableClass); + }); + var f = hasNative(component) ? nativeDisable : ariaDisable; + f(component); + }; + + var enable = function (component, disableConfig, disableState) { + disableConfig.disableClass().each(function (disableClass) { + Class.remove(component.element(), disableClass); + }); + var f = hasNative(component) ? nativeEnable : ariaEnable; + f(component); + }; + + var isDisabled = function (component) { + return hasNative(component) ? nativeIsDisabled(component) : ariaIsDisabled(component); + }; + + return { + enable: enable, + disable: disable, + isDisabled: isDisabled, + onLoad: onLoad + }; + } +); +define( + 'ephox.alloy.behaviour.disabling.ActiveDisable', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.behaviour.common.Behaviour', + 'ephox.alloy.behaviour.disabling.DisableApis', + 'ephox.alloy.dom.DomModification', + 'ephox.katamari.api.Arr' + ], + + function (AlloyEvents, SystemEvents, Behaviour, DisableApis, DomModification, Arr) { + var exhibit = function (base, disableConfig, disableState) { + return DomModification.nu({ + // Do not add the attribute yet, because it will depend on the node name + // if we use "aria-disabled" or just "disabled" + classes: disableConfig.disabled() ? disableConfig.disableClass().map(Arr.pure).getOr([ ]) : [ ] + }); + }; + + var events = function (disableConfig, disableState) { + return AlloyEvents.derive([ + AlloyEvents.abort(SystemEvents.execute(), function (component, simulatedEvent) { + return DisableApis.isDisabled(component, disableConfig, disableState); + }), + Behaviour.loadEvent(disableConfig, disableState, DisableApis.onLoad) + ]); + }; + + return { + exhibit: exhibit, + events: events + }; + } +); +define( + 'ephox.alloy.behaviour.disabling.DisableSchema', + + [ + 'ephox.boulder.api.FieldSchema' + ], + + function (FieldSchema) { + return [ + FieldSchema.defaulted('disabled', false), + FieldSchema.option('disableClass') + ]; + } +); +define( + 'ephox.alloy.api.behaviour.Disabling', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.disabling.ActiveDisable', + 'ephox.alloy.behaviour.disabling.DisableApis', + 'ephox.alloy.behaviour.disabling.DisableSchema' + ], + + function (Behaviour, ActiveDisable, DisableApis, DisableSchema) { + return Behaviour.create({ + fields: DisableSchema, + name: 'disabling', + active: ActiveDisable, + apis: DisableApis + }); + } +); +define( + 'ephox.alloy.api.ui.Form', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Composing', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.ui.UiSketcher', + 'ephox.alloy.parts.AlloyParts', + 'ephox.alloy.parts.PartType', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj' + ], + + function (Behaviour, Composing, Representing, UiSketcher, AlloyParts, PartType, FieldSchema, Arr, Merger, Obj) { + var owner = 'form'; + + var schema = [ + FieldSchema.defaulted('formBehaviours', { }) + ]; + + var getPartName = function (name) { + return ''; + }; + + var sketch = function (fSpec) { + var parts = (function () { + var record = [ ]; + + var field = function (name, config) { + record.push(name); + return AlloyParts.generateOne(owner, getPartName(name), config); + }; + + return { + field: field, + record: function () { return record; } + }; + })(); + + var spec = fSpec(parts); + + var partNames = parts.record(); + // Unlike other sketches, a form does not know its parts in advance (as they represent each field + // in a particular form). Therefore, it needs to calculate the part names on the fly + var fieldParts = Arr.map(partNames, function (n) { + return PartType.required({ name: n, pname: getPartName(n) }); + }); + + return UiSketcher.composite(owner, schema, fieldParts, make, spec); + }; + + var make = function (detail, components, spec) { + return Merger.deepMerge( + { + 'debug.sketcher': { + 'Form': spec + }, + uid: detail.uid(), + dom: detail.dom(), + components: components, + + // Form has an assumption that every field must have composing, and that the composed element has representing. + behaviours: Merger.deepMerge( + Behaviour.derive([ + Representing.config({ + store: { + mode: 'manual', + getValue: function (form) { + var optPs = AlloyParts.getAllParts(form, detail); + return Obj.map(optPs, function (optPThunk, pName) { + return optPThunk().bind(Composing.getCurrent).map(Representing.getValue); + }); + }, + setValue: function (form, values) { + Obj.each(values, function (newValue, key) { + AlloyParts.getPart(form, detail, key).each(function (wrapper) { + Composing.getCurrent(wrapper).each(function (field) { + Representing.setValue(field, newValue); + }); + }); + }); + } + } + }) + ]), + detail.formBehaviours() + ) + } + ); + }; + + return { + sketch: sketch + }; + } +); +define( + 'ephox.katamari.api.Singleton', + + [ + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Cell' + ], + + function (Option, Cell) { + var revocable = function (doRevoke) { + var subject = Cell(Option.none()); + + var revoke = function () { + subject.get().each(doRevoke); + }; + + var clear = function () { + revoke(); + subject.set(Option.none()); + }; + + var set = function (s) { + revoke(); + subject.set(Option.some(s)); + }; + + var isSet = function () { + return subject.get().isSome(); + }; + + return { + clear: clear, + isSet: isSet, + set: set + }; + }; + + var destroyable = function () { + return revocable(function (s) { + s.destroy(); + }); + }; + + var unbindable = function () { + return revocable(function (s) { + s.unbind(); + }); + }; + + var api = function () { + var subject = Cell(Option.none()); + + var revoke = function () { + subject.get().each(function (s) { + s.destroy(); + }); + }; + + var clear = function () { + revoke(); + subject.set(Option.none()); + }; + + var set = function (s) { + revoke(); + subject.set(Option.some(s)); + }; + + var run = function (f) { + subject.get().each(f); + }; + + var isSet = function () { + return subject.get().isSome(); + }; + + return { + clear: clear, + isSet: isSet, + set: set, + run: run + }; + }; + + var value = function () { + var subject = Cell(Option.none()); + + var clear = function () { + subject.set(Option.none()); + }; + + var set = function (s) { + subject.set(Option.some(s)); + }; + + var on = function (f) { + subject.get().each(f); + }; + + var isSet = function () { + return subject.get().isSome(); + }; + + return { + clear: clear, + set: set, + isSet: isSet, + on: on + }; + }; + + return { + destroyable: destroyable, + unbindable: unbindable, + api: api, + value: value + }; + } +); +define( + 'tinymce.themes.mobile.model.SwipingModel', + + [ + + ], + + function () { + var SWIPING_LEFT = 1; + var SWIPING_RIGHT = -1; + var SWIPING_NONE = 0; + + /* The state is going to record the edge points before the direction changed. We can then use + * these points to identify whether or not the swipe was *consistent enough* + */ + + var init = function (xValue) { + return { + xValue: xValue, + points: [ ] + }; + }; + + var move = function (model, xValue) { + if (xValue === model.xValue) { + return model; // do nothing. + } + + // If the direction is the same as the previous direction, the change the last point + // in the points array (because we have a new edge point). If the direction is different, + // add a new point to the points array (because we have changed direction) + var currentDirection = xValue - model.xValue > 0 ? SWIPING_LEFT : SWIPING_RIGHT; + + var newPoint = { direction: currentDirection, xValue: xValue }; + + var priorPoints = (function () { + if (model.points.length === 0) { + return [ ]; + } else { + var prev = model.points[model.points.length - 1]; + return prev.direction === currentDirection ? model.points.slice(0, model.points.length - 1) : model.points; + } + })(); + + return { + xValue: xValue, + points: priorPoints.concat([ newPoint ]) + }; + }; + + var complete = function (model/*, snaps*/) { + if (model.points.length === 0) { + return SWIPING_NONE; + } else { + // Preserving original intention + var firstDirection = model.points[0].direction; + var lastDirection = model.points[model.points.length - 1].direction; + // eslint-disable-next-line no-nested-ternary + return firstDirection === SWIPING_RIGHT && lastDirection === SWIPING_RIGHT ? SWIPING_RIGHT : + firstDirection === SWIPING_LEFT && lastDirection == SWIPING_LEFT ? SWIPING_LEFT : SWIPING_NONE; + } + }; + + return { + init: init, + move: move, + complete: complete + }; + } +); +define( + 'tinymce.themes.mobile.ui.SerialisedDialog', + + [ + 'ephox.alloy.api.behaviour.AddEventsBehaviour', + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Disabling', + 'ephox.alloy.api.behaviour.Highlighting', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Receiving', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.component.Memento', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.ui.Button', + 'ephox.alloy.api.ui.Container', + 'ephox.alloy.api.ui.Form', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Singleton', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.SelectorFind', + 'ephox.sugar.api.view.Width', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.model.SwipingModel', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function ( + AddEventsBehaviour, Behaviour, Disabling, Highlighting, Keying, Receiving, Representing, Memento, AlloyEvents, AlloyTriggers, NativeEvents, Button, Container, + Form, FieldSchema, ValueSchema, Arr, Cell, Option, Singleton, Css, SelectorFilter, SelectorFind, Width, Receivers, SwipingModel, Styles, UiDomFactory + ) { + var sketch = function (rawSpec) { + var navigateEvent = 'navigateEvent'; + + var wrapperAdhocEvents = 'serializer-wrapper-events'; + var formAdhocEvents = 'form-events'; + + var schema = ValueSchema.objOf([ + FieldSchema.strict('fields'), + // Used for when datafields are present. + FieldSchema.defaulted('maxFieldIndex', rawSpec.fields.length - 1), + FieldSchema.strict('onExecute'), + FieldSchema.strict('getInitialValue'), + FieldSchema.state('state', function () { + return { + dialogSwipeState: Singleton.value(), + currentScreen: Cell(0) + }; + }) + ]); + + var spec = ValueSchema.asRawOrDie('SerialisedDialog', schema, rawSpec); + + var navigationButton = function (direction, directionName, enabled) { + return Button.sketch({ + dom: UiDomFactory.dom(''), + action: function (button) { + AlloyTriggers.emitWith(button, navigateEvent, { direction: direction }); + }, + buttonBehaviours: Behaviour.derive([ + Disabling.config({ + disableClass: Styles.resolve('toolbar-navigation-disabled'), + disabled: !enabled + }) + ]) + }); + }; + + var reposition = function (dialog, message) { + SelectorFind.descendant(dialog.element(), '.' + Styles.resolve('serialised-dialog-chain')).each(function (parent) { + Css.set(parent, 'left', (-spec.state.currentScreen.get() * message.width) + 'px'); + }); + }; + + var navigate = function (dialog, direction) { + var screens = SelectorFilter.descendants(dialog.element(), '.' + Styles.resolve('serialised-dialog-screen')); + SelectorFind.descendant(dialog.element(), '.' + Styles.resolve('serialised-dialog-chain')).each(function (parent) { + if ((spec.state.currentScreen.get() + direction) >= 0 && (spec.state.currentScreen.get() + direction) < screens.length) { + Css.getRaw(parent, 'left').each(function (left) { + var currentLeft = parseInt(left, 10); + var w = Width.get(screens[0]); + Css.set(parent, 'left', (currentLeft - (direction * w)) + 'px'); + }); + spec.state.currentScreen.set(spec.state.currentScreen.get() + direction); + } + }); + }; + + // Unfortunately we need to inspect the DOM to find the input that is currently on screen + var focusInput = function (dialog) { + var inputs = SelectorFilter.descendants(dialog.element(), 'input'); + var optInput = Option.from(inputs[spec.state.currentScreen.get()]); + optInput.each(function (input) { + dialog.getSystem().getByDom(input).each(function (inputComp) { + AlloyTriggers.dispatchFocus(dialog, inputComp.element()); + }); + }); + var dotitems = memDots.get(dialog); + Highlighting.highlightAt(dotitems, spec.state.currentScreen.get()); + }; + + var resetState = function () { + spec.state.currentScreen.set(0); + spec.state.dialogSwipeState.clear(); + }; + + var memForm = Memento.record( + Form.sketch(function (parts) { + return { + dom: UiDomFactory.dom('
    '), + components: [ + Container.sketch({ + dom: UiDomFactory.dom('
    '), + components: Arr.map(spec.fields, function (field, i) { + return i <= spec.maxFieldIndex ? Container.sketch({ + dom: UiDomFactory.dom('
    '), + components: Arr.flatten([ + [ navigationButton(-1, 'previous', (i > 0)) ], + [ parts.field(field.name, field.spec) ], + [ navigationButton(+1, 'next', (i < spec.maxFieldIndex)) ] + ]) + }) : parts.field(field.name, field.spec); + }) + }) + ], + + formBehaviours: Behaviour.derive([ + Receivers.orientation(function (dialog, message) { + reposition(dialog, message); + }), + Keying.config({ + mode: 'special', + focusIn: function (dialog/*, specialInfo */) { + focusInput(dialog); + }, + onTab: function (dialog/*, specialInfo */) { + navigate(dialog, +1); + return Option.some(true); + }, + onShiftTab: function (dialog/*, specialInfo */) { + navigate(dialog, -1); + return Option.some(true); + } + }), + + AddEventsBehaviour.config(formAdhocEvents, [ + AlloyEvents.runOnAttached(function (dialog, simulatedEvent) { + // Reset state to first screen. + resetState(); + var dotitems = memDots.get(dialog); + Highlighting.highlightFirst(dotitems); + spec.getInitialValue(dialog).each(function (v) { + Representing.setValue(dialog, v); + }); + }), + + AlloyEvents.runOnExecute(spec.onExecute), + + AlloyEvents.run(NativeEvents.transitionend(), function (dialog, simulatedEvent) { + if (simulatedEvent.event().raw().propertyName === 'left') { + focusInput(dialog); + } + }), + + AlloyEvents.run(navigateEvent, function (dialog, simulatedEvent) { + var direction = simulatedEvent.event().direction(); + navigate(dialog, direction); + }) + ]) + ]) + }; + }) + ); + + var memDots = Memento.record({ + dom: UiDomFactory.dom('
    '), + behaviours: Behaviour.derive([ + Highlighting.config({ + highlightClass: Styles.resolve('dot-active'), + itemClass: Styles.resolve('dot-item') + }) + ]), + components: Arr.bind(spec.fields, function (_f, i) { + return i <= spec.maxFieldIndex ? [ + UiDomFactory.spec('
    ') + ] : []; + }) + }); + + return { + dom: UiDomFactory.dom('
    '), + components: [ + memForm.asSpec(), + memDots.asSpec() + ], + + behaviours: Behaviour.derive([ + Keying.config({ + mode: 'special', + focusIn: function (wrapper) { + var form = memForm.get(wrapper); + Keying.focusIn(form); + } + }), + + AddEventsBehaviour.config(wrapperAdhocEvents, [ + AlloyEvents.run(NativeEvents.touchstart(), function (wrapper, simulatedEvent) { + spec.state.dialogSwipeState.set( + SwipingModel.init(simulatedEvent.event().raw().touches[0].clientX) + ); + }), + AlloyEvents.run(NativeEvents.touchmove(), function (wrapper, simulatedEvent) { + spec.state.dialogSwipeState.on(function (state) { + simulatedEvent.event().prevent(); + spec.state.dialogSwipeState.set( + SwipingModel.move(state, simulatedEvent.event().raw().touches[0].clientX) + ); + }); + }), + AlloyEvents.run(NativeEvents.touchend(), function (wrapper/*, simulatedEvent */) { + spec.state.dialogSwipeState.on(function (state) { + var dialog = memForm.get(wrapper); + // Confusing + var direction = -1 * SwipingModel.complete(state); + navigate(dialog, direction); + }); + }) + ]) + ]) + }; + }; + + return { + sketch: sketch + }; + } +); +define( + 'tinymce.themes.mobile.util.RangePreserver', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sand.api.PlatformDetection' + ], + + function (Fun, PlatformDetection) { + var platform = PlatformDetection.detect(); + /* At the moment, this is only going to be used for Android. The Google keyboard + * that comes with Android seems to shift the selection when the editor gets blurred + * to the end of the word. This function rectifies that behaviour + * + * See fiddle: http://fiddle.tinymce.com/xNfaab/3 or http://fiddle.tinymce.com/xNfaab/4 + */ + var preserve = function (f, editor) { + var rng = editor.selection.getRng(); + f(); + editor.selection.setRng(rng); + }; + + var forAndroid = function (editor, f) { + var wrapper = platform.os.isAndroid() ? preserve : Fun.apply; + wrapper(f, editor); + }; + + return { + forAndroid: forAndroid + }; + } +); + +define( + 'tinymce.themes.mobile.ui.LinkButton', + + [ + 'ephox.alloy.api.behaviour.Representing', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Thunk', + 'tinymce.themes.mobile.bridge.LinkBridge', + 'tinymce.themes.mobile.ui.Buttons', + 'tinymce.themes.mobile.ui.Inputs', + 'tinymce.themes.mobile.ui.SerialisedDialog', + 'tinymce.themes.mobile.util.RangePreserver' + ], + + function (Representing, Option, Thunk, LinkBridge, Buttons, Inputs, SerialisedDialog, RangePreserver) { + var getGroups = Thunk.cached(function (realm, editor) { + return [ + { + label: 'the link group', + items: [ + SerialisedDialog.sketch({ + fields: [ + Inputs.field('url', 'Type or paste URL'), + Inputs.field('text', 'Link text'), + Inputs.field('title', 'Link title'), + Inputs.field('target', 'Link target'), + Inputs.hidden('link') + ], + + // Do not include link + maxFieldIndex: [ 'url', 'text', 'title', 'target' ].length - 1, + getInitialValue: function (/* dialog */) { + return Option.some( + LinkBridge.getInfo(editor) + ); + }, + + onExecute: function (dialog/*, simulatedEvent */) { + var info = Representing.getValue(dialog); + LinkBridge.applyInfo(editor, info); + realm.restoreToolbar(); + editor.focus(); + } + }) + ] + } + ]; + }); + + var sketch = function (realm, editor) { + return Buttons.forToolbarStateAction(editor, 'link', 'link', function () { + var groups = getGroups(realm, editor); + + realm.setContextToolbar(groups); + // Focus inside + // On Android, there is a bug where if you position the cursor (collapsed) within a + // word, and you blur the editor (by focusing an input), the selection moves to the + // end of the word (http://fiddle.tinymce.com/xNfaab/3 or 4). This is actually dependent + // on your keyboard (Google Keyboard) and is probably considered a feature. It does + // not happen on Samsung (for example). + RangePreserver.forAndroid(editor, function () { + realm.focusToolbar(); + }); + + LinkBridge.query(editor).each(function (link) { + editor.selection.select(link.dom()); + }); + }); + }; + + return { + sketch: sketch + }; + } +); +define( + 'tinymce.themes.mobile.features.DefaultStyleFormats', + + [ + + ], + + function () { + return [ + { + title: 'Headings', items: [ + { title: 'Heading 1', format: 'h1' }, + { title: 'Heading 2', format: 'h2' }, + { title: 'Heading 3', format: 'h3' }, + { title: 'Heading 4', format: 'h4' }, + { title: 'Heading 5', format: 'h5' }, + { title: 'Heading 6', format: 'h6' } + ] + }, + + { + title: 'Inline', items: [ + { title: 'Bold', icon: 'bold', format: 'bold' }, + { title: 'Italic', icon: 'italic', format: 'italic' }, + { title: 'Underline', icon: 'underline', format: 'underline' }, + { title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough' }, + { title: 'Superscript', icon: 'superscript', format: 'superscript' }, + { title: 'Subscript', icon: 'subscript', format: 'subscript' }, + { title: 'Code', icon: 'code', format: 'code' } + ] + }, + + { + title: 'Blocks', items: [ + { title: 'Paragraph', format: 'p' }, + { title: 'Blockquote', format: 'blockquote' }, + { title: 'Div', format: 'div' }, + { title: 'Pre', format: 'pre' } + ] + }, + + { + title: 'Alignment', items: [ + { title: 'Left', icon: 'alignleft', format: 'alignleft' }, + { title: 'Center', icon: 'aligncenter', format: 'aligncenter' }, + { title: 'Right', icon: 'alignright', format: 'alignright' }, + { title: 'Justify', icon: 'alignjustify', format: 'alignjustify' } + ] + } + ]; + } +); + +define( + 'ephox.alloy.behaviour.transitioning.TransitionApis', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.Class' + ], + + function (Objects, Fun, Option, Attr, Class) { + var findRoute = function (component, transConfig, transState, route) { + return Objects.readOptFrom(transConfig.routes(), route.start()).map(Fun.apply).bind(function (sConfig) { + return Objects.readOptFrom(sConfig, route.destination()).map(Fun.apply); + }); + }; + + var getTransition = function (comp, transConfig, transState) { + var route = getCurrentRoute(comp, transConfig, transState); + return route.bind(function (r) { + return getTransitionOf(comp, transConfig, transState, r); + }); + }; + + var getTransitionOf = function (comp, transConfig, transState, route) { + return findRoute(comp, transConfig, transState, route).bind(function (r) { + return r.transition().map(function (t) { + return { + transition: Fun.constant(t), + route: Fun.constant(r) + }; + }); + }); + }; + + var disableTransition = function (comp, transConfig, transState) { + // Disable the current transition + getTransition(comp, transConfig, transState).each(function (routeTransition) { + var t = routeTransition.transition(); + Class.remove(comp.element(), t.transitionClass()); + Attr.remove(comp.element(), transConfig.destinationAttr()); + }); + }; + + var getNewRoute = function (comp, transConfig, transState, destination) { + return { + start: Fun.constant(Attr.get(comp.element(), transConfig.stateAttr())), + destination: Fun.constant(destination) + }; + }; + + var getCurrentRoute = function (comp, transConfig, transState) { + var el = comp.element(); + return Attr.has(el, transConfig.destinationAttr()) ? Option.some({ + start: Fun.constant(Attr.get(comp.element(), transConfig.stateAttr())), + destination: Fun.constant(Attr.get(comp.element(), transConfig.destinationAttr())) + }) : Option.none(); + }; + + var jumpTo = function (comp, transConfig, transState, destination) { + // Remove the previous transition + disableTransition(comp, transConfig, transState); + // Only call finish if there was an original state + if (Attr.has(comp.element(), transConfig.stateAttr()) && Attr.get(comp.element(), transConfig.stateAttr()) !== destination) transConfig.onFinish()(comp, destination); + Attr.set(comp.element(), transConfig.stateAttr(), destination); + }; + + var fasttrack = function (comp, transConfig, transState, destination) { + if (Attr.has(comp.element(), transConfig.destinationAttr())) { + Attr.set(comp.element(), transConfig.stateAttr(), Attr.get(comp.element(), transConfig.destinationAttr())); + Attr.remove(comp.element(), transConfig.destinationAttr()); + } + }; + + var progressTo = function (comp, transConfig, transState, destination) { + fasttrack(comp, transConfig, transState, destination); + var route = getNewRoute(comp, transConfig, transState, destination); + getTransitionOf(comp, transConfig, transState, route).fold(function () { + jumpTo(comp, transConfig, transState, destination); + }, function (routeTransition) { + disableTransition(comp, transConfig, transState); + var t = routeTransition.transition(); + Class.add(comp.element(), t.transitionClass()); + Attr.set(comp.element(), transConfig.destinationAttr(), destination); + }); + }; + + var getState = function (comp, transConfig, transState) { + var e = comp.element(); + return Attr.has(e, transConfig.stateAttr()) ? Option.some( + Attr.get(e, transConfig.stateAttr()) + ) : Option.none(); + }; + + return { + findRoute: findRoute, + disableTransition: disableTransition, + getCurrentRoute: getCurrentRoute, + jumpTo: jumpTo, + progressTo: progressTo, + getState: getState + }; + } +); + +define( + 'ephox.alloy.behaviour.transitioning.ActiveTransitioning', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.behaviour.transitioning.TransitionApis' + ], + + function (AlloyEvents, NativeEvents, TransitionApis) { + var events = function (transConfig, transState) { + return AlloyEvents.derive([ + AlloyEvents.run(NativeEvents.transitionend(), function (component, simulatedEvent) { + var raw = simulatedEvent.event().raw(); + TransitionApis.getCurrentRoute(component, transConfig, transState).each(function (route) { + TransitionApis.findRoute(component, transConfig, transState, route).each(function (rInfo) { + rInfo.transition().each(function (rTransition) { + if (raw.propertyName === rTransition.property()) { + TransitionApis.jumpTo(component, transConfig, transState, route.destination()); + transConfig.onTransition()(component, route); + } + }); + }); + }); + }), + + AlloyEvents.runOnAttached(function (comp, se) { + TransitionApis.jumpTo(comp, transConfig, transState, transConfig.initialState()); + }) + ]); + }; + + return { + events: events + }; + } +); + +define( + 'ephox.alloy.behaviour.transitioning.TransitionSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Result' + ], + + function (Fields, FieldSchema, ValueSchema, Result) { + return [ + FieldSchema.defaulted('destinationAttr', 'data-transitioning-destination'), + FieldSchema.defaulted('stateAttr', 'data-transitioning-state'), + FieldSchema.strict('initialState'), + Fields.onHandler('onTransition'), + Fields.onHandler('onFinish'), + FieldSchema.strictOf( + 'routes', + ValueSchema.setOf( + Result.value, + ValueSchema.setOf( + Result.value, + ValueSchema.objOfOnly([ + FieldSchema.optionObjOfOnly('transition', [ + FieldSchema.strict('property'), + FieldSchema.strict('transitionClass') + ]) + ]) + ) + ) + ) + ]; + } +); + +define( + 'ephox.alloy.api.behaviour.Transitioning', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.transitioning.ActiveTransitioning', + 'ephox.alloy.behaviour.transitioning.TransitionApis', + 'ephox.alloy.behaviour.transitioning.TransitionSchema', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Obj' + ], + + function (Behaviour, ActiveTransitioning, TransitionApis, TransitionSchema, Objects, Obj) { + var createRoutes = function (routes) { + var r = { }; + Obj.each(routes, function (v, k) { + var waypoints = k.split('<->'); + r[waypoints[0]] = Objects.wrap(waypoints[1], v); + r[waypoints[1]] = Objects.wrap(waypoints[0], v); + }); + return r; + }; + + var createBistate = function (first, second, transitions) { + return Objects.wrapAll([ + { key: first, value: Objects.wrap(second, transitions) }, + { key: second, value: Objects.wrap(first, transitions) } + ]); + }; + + var createTristate = function (first, second, third, transitions) { + return Objects.wrapAll([ + { + key: first, + value: Objects.wrapAll([ + { key: second, value: transitions }, + { key: third, value: transitions } + ]) + }, + { + key: second, + value: Objects.wrapAll([ + { key: first, value: transitions }, + { key: third, value: transitions } + ]) + }, + { + key: third, + value: Objects.wrapAll([ + { key: first, value: transitions }, + { key: second, value: transitions } + ]) + } + ]); + }; + + return Behaviour.create({ + fields: TransitionSchema, + name: 'transitioning', + active: ActiveTransitioning, + apis: TransitionApis, + extra: { + createRoutes: createRoutes, + createBistate: createBistate, + createTristate: createTristate + } + }); + } +); + +define( + 'ephox.alloy.behaviour.common.BehaviourBlob', + + [ + 'ephox.alloy.behaviour.common.NoState', + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.sand.api.JSON', + 'global!Error' + ], + + function (NoState, FieldPresence, FieldSchema, ValueSchema, Arr, Fun, Obj, JSON, Error) { + var generateFrom = function (spec, all) { + var schema = Arr.map(all, function (a) { + return FieldSchema.field(a.name(), a.name(), FieldPresence.asOption(), ValueSchema.objOf([ + FieldSchema.strict('config'), + FieldSchema.defaulted('state', NoState) + ])); + }); + + var validated = ValueSchema.asStruct('component.behaviours', ValueSchema.objOf(schema), spec.behaviours).fold(function (errInfo) { + throw new Error( + ValueSchema.formatError(errInfo) + '\nComplete spec:\n' + + JSON.stringify(spec, null, 2) + ); + }, Fun.identity); + + return { + list: all, + data: Obj.map(validated, function (blobOptionThunk/*, rawK */) { + var blobOption = blobOptionThunk(); + return Fun.constant(blobOption.map(function (blob) { + return { + config: blob.config(), + state: blob.state().init(blob.config()) + }; + })); + }) + }; + }; + + var getBehaviours = function (bData) { + return bData.list; + }; + + var getData = function (bData) { + return bData.data; + }; + + return { + generateFrom: generateFrom, + getBehaviours: getBehaviours, + getData: getData + }; + } +); + +define( + 'ephox.alloy.api.component.CompBehaviours', + + [ + 'ephox.alloy.behaviour.common.BehaviourBlob', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'global!Error' + ], + + function (BehaviourBlob, Objects, Arr, Obj, Error) { + var getBehaviours = function (spec) { + var behaviours = Objects.readOptFrom(spec, 'behaviours').getOr({ }); + var keys = Arr.filter( + Obj.keys(behaviours), + function (k) { return behaviours[k] !== undefined; } + ); + return Arr.map(keys, function (k) { + return spec.behaviours[k].me; + }); + }; + + var generateFrom = function (spec, all) { + return BehaviourBlob.generateFrom(spec, all); + }; + + var generate = function (spec) { + var all = getBehaviours(spec); + return generateFrom(spec, all); + }; + + return { + generate: generate, + generateFrom: generateFrom + }; + } +); + +define( + 'ephox.alloy.api.component.ComponentApi', + + [ + 'ephox.katamari.api.Contracts' + ], + + function (Contracts) { + return Contracts.exactly([ + 'getSystem', + 'config', + 'spec', + 'connect', + 'disconnect', + 'element', + 'syncComponents', + 'readState', + 'components', + 'events' + ]); + } +); + +define( + 'ephox.alloy.api.system.SystemApi', + + [ + 'ephox.katamari.api.Contracts' + ], + + function (Contracts) { + return Contracts.exactly([ + 'debugInfo', + 'triggerFocus', + 'triggerEvent', + 'triggerEscape', + // TODO: Implement later. See lab for details. + // 'openPopup', + // 'closePopup', + 'addToWorld', + 'removeFromWorld', + 'addToGui', + 'removeFromGui', + 'build', + 'getByUid', + 'getByDom', + + 'broadcast', + 'broadcastOn' + ]); + } +); +define( + 'ephox.alloy.api.system.NoContextApi', + + [ + 'ephox.alloy.api.system.SystemApi', + 'ephox.alloy.log.AlloyLogger', + 'ephox.katamari.api.Fun', + 'global!Error' + ], + + function (SystemApi, AlloyLogger, Fun, Error) { + return function (getComp) { + var fail = function (event) { + return function () { + throw new Error('The component must be in a context to send: ' + event + '\n' + + AlloyLogger.element(getComp().element()) + ' is not in context.' + ); + }; + }; + + return SystemApi({ + debugInfo: Fun.constant('fake'), + triggerEvent: fail('triggerEvent'), + triggerFocus: fail('triggerFocus'), + triggerEscape: fail('triggerEscape'), + build: fail('build'), + addToWorld: fail('addToWorld'), + removeFromWorld: fail('removeFromWorld'), + addToGui: fail('addToGui'), + removeFromGui: fail('removeFromGui'), + getByUid: fail('getByUid'), + getByDom: fail('getByDom'), + broadcast: fail('broadcast'), + broadcastOn: fail('broadcastOn') + }); + }; + } +); +define( + 'ephox.alloy.alien.ObjIndex', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Obj' + ], + + function (Objects, Obj) { + + /* + * This is used to take something like: + * + * { + * behaviour: { + * event1: listener + * }, + * behaviour2: { + * event1: listener + * } + * } + * + * And turn it into something like: + * + * { + * event1: [ { b: behaviour1, l: listener }, { b: behaviour2, l: listener } ] + * } + */ + + + var byInnerKey = function (data, tuple) { + var r = {}; + Obj.each(data, function (detail, key) { + Obj.each(detail, function (value, indexKey) { + var chain = Objects.readOr(indexKey, [ ])(r); + r[indexKey] = chain.concat([ + tuple(key, value) + ]); + }); + }); + return r; + }; + + return { + byInnerKey: byInnerKey + }; + + } +); +define( + 'ephox.alloy.construct.ComponentDom', + + [ + 'ephox.alloy.alien.ObjIndex', + 'ephox.alloy.dom.DomModification', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Merger', + 'ephox.sand.api.JSON', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Result' + ], + + function (ObjIndex, DomModification, Objects, Arr, Obj, Merger, Json, Fun, Result) { + var behaviourDom = function (name, modification) { + return { + name: Fun.constant(name), + modification: modification + }; + }; + + var concat = function (chain, aspect) { + var values = Arr.bind(chain, function (c) { + return c.modification().getOr([ ]); + }); + return Result.value( + Objects.wrap(aspect, values) + ); + }; + + var onlyOne = function (chain, aspect, order) { + if (chain.length > 1) return Result.error( + 'Multiple behaviours have tried to change DOM "' + aspect + '". The guilty behaviours are: ' + + Json.stringify(Arr.map(chain, function (b) { return b.name(); })) + '. At this stage, this ' + + 'is not supported. Future releases might provide strategies for resolving this.' + ); + else if (chain.length === 0) return Result.value({ }); + else return Result.value( + chain[0].modification().fold(function () { + return { }; + }, function (m) { + return Objects.wrap(aspect, m); + }) + ); + }; + + var duplicate = function (aspect, k, obj, behaviours) { + return Result.error('Mulitple behaviours have tried to change the _' + k + '_ "' + aspect + '"' + + '. The guilty behaviours are: ' + Json.stringify(Arr.bind(behaviours, function (b) { + return b.modification().getOr({})[k] !== undefined ? [ b.name() ] : [ ]; + }), null, 2) + '. This is not currently supported.' + ); + }; + + var safeMerge = function (chain, aspect) { + // return unsafeMerge(chain, aspect); + var y = Arr.foldl(chain, function (acc, c) { + var obj = c.modification().getOr({}); + return acc.bind(function (accRest) { + var parts = Obj.mapToArray(obj, function (v, k) { + return accRest[k] !== undefined ? duplicate(aspect, k, obj, chain) : + Result.value(Objects.wrap(k, v)); + }); + return Objects.consolidate(parts, accRest); + }); + }, Result.value({})); + + return y.map(function (yValue) { + return Objects.wrap(aspect, yValue); + }); + }; + + var mergeTypes = { + classes: concat, + attributes: safeMerge, + styles: safeMerge, + + // Group these together somehow + domChildren: onlyOne, + defChildren: onlyOne, + innerHtml: onlyOne, + + value: onlyOne + }; + + var combine = function (info, baseMod, behaviours, base) { + // Get the Behaviour DOM modifications + var behaviourDoms = Merger.deepMerge({ }, baseMod); + Arr.each(behaviours, function (behaviour) { + behaviourDoms[behaviour.name()] = behaviour.exhibit(info, base); + }); + + var byAspect = ObjIndex.byInnerKey(behaviourDoms, behaviourDom); + // byAspect format: { classes: [ { name: Toggling, modification: [ 'selected' ] } ] } + + var usedAspect = Obj.map(byAspect, function (values, aspect) { + return Arr.bind(values, function (value) { + return value.modification().fold(function () { + return [ ]; + }, function (v) { + return [ value ]; + }); + }); + }); + + var modifications = Obj.mapToArray(usedAspect, function (values, aspect) { + return Objects.readOptFrom(mergeTypes, aspect).fold(function () { + return Result.error('Unknown field type: ' + aspect); + }, function (merger) { + return merger(values, aspect); + }); + }); + + var consolidated = Objects.consolidate(modifications, {}); + + return consolidated.map(DomModification.nu); + }; + + return { + combine: combine + }; + } +); +define( + 'ephox.alloy.alien.PrioritySort', + + [ + 'ephox.sand.api.JSON', + 'ephox.katamari.api.Result', + 'global!Error' + ], + + function (Json, Result, Error) { + var sortKeys = function (label, keyName, array, order) { + var sliced = array.slice(0); + try { + var sorted = sliced.sort(function (a, b) { + var aKey = a[keyName](); + var bKey = b[keyName](); + var aIndex = order.indexOf(aKey); + var bIndex = order.indexOf(bKey); + if (aIndex === -1) throw new Error('The ordering for ' + label + ' does not have an entry for ' + aKey + + '.\nOrder specified: ' + Json.stringify(order, null, 2)); + if (bIndex === -1) throw new Error('The ordering for ' + label + ' does not have an entry for ' + bKey + + '.\nOrder specified: ' + Json.stringify(order, null, 2)); + if (aIndex < bIndex) return -1; + else if (bIndex < aIndex) return 1; + else return 0; + }); + return Result.value(sorted); + } catch (err) { + return Result.error([ err ]); + } + }; + + return { + sortKeys: sortKeys + }; + } +); +define( + 'ephox.alloy.events.DescribedHandler', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + var nu = function (handler, purpose) { + return { + handler: handler, + purpose: Fun.constant(purpose) + }; + }; + + var curryArgs = function (descHandler, extraArgs) { + return { + handler: Fun.curry.apply(undefined, [ descHandler.handler ].concat(extraArgs)), + purpose: descHandler.purpose + }; + }; + + var getHandler = function (descHandler) { + return descHandler.handler; + }; + + return { + nu: nu, + curryArgs: curryArgs, + getHandler: getHandler + }; + } +); + +define( + 'ephox.alloy.construct.ComponentEvents', + + [ + 'ephox.alloy.alien.ObjIndex', + 'ephox.alloy.alien.PrioritySort', + 'ephox.alloy.construct.EventHandler', + 'ephox.alloy.events.DescribedHandler', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Result', + 'ephox.sand.api.JSON', + 'global!Array', + 'global!Error' + ], + + function (ObjIndex, PrioritySort, EventHandler, DescribedHandler, Objects, Arr, Fun, Merger, Obj, Result, Json, Array, Error) { + /* + * The process of combining a component's events + * + * - Generate all the handlers based on the behaviour and the base events + * - Create an index (eventName -> [tuples(behaviourName, handler)]) + * - Map over this index: + * - if the list == length 1, then collapse it to the head value + * - if the list > length 1, then: + * - sort the tuples using the behavour name ordering specified using + eventOrder[event]. Return error if insufficient + * - generate a can, run, and abort that combines the handlers of the + tuples in the sorted order + * + * So at the end, you should have Result(eventName -> single function) + */ + var behaviourTuple = function (name, handler) { + return { + name: Fun.constant(name), + handler: Fun.constant(handler) + }; + }; + + var nameToHandlers = function (behaviours, info) { + var r = {}; + Arr.each(behaviours, function (behaviour) { + r[behaviour.name()] = behaviour.handlers(info); + }); + return r; + }; + + var groupByEvents = function (info, behaviours, base) { + var behaviourEvents = Merger.deepMerge(base, nameToHandlers(behaviours, info)); + // Now, with all of these events, we need to index by event name + return ObjIndex.byInnerKey(behaviourEvents, behaviourTuple); + }; + + var combine = function (info, eventOrder, behaviours, base) { + var byEventName = groupByEvents(info, behaviours, base); + return combineGroups(byEventName, eventOrder); + }; + + var assemble = function (rawHandler) { + var handler = EventHandler.read(rawHandler); + return function (component, simulatedEvent/*, others */) { + var args = Array.prototype.slice.call(arguments, 0); + if (handler.abort.apply(undefined, args)) { + simulatedEvent.stop(); + } else if (handler.can.apply(undefined, args)) { + handler.run.apply(undefined, args); + } + }; + }; + + var missingOrderError = function (eventName, tuples) { + return new Result.error([ + 'The event (' + eventName + ') has more than one behaviour that listens to it.\nWhen this occurs, you must ' + + 'specify an event ordering for the behaviours in your spec (e.g. [ "listing", "toggling" ]).\nThe behaviours that ' + + 'can trigger it are: ' + Json.stringify(Arr.map(tuples, function (c) { return c.name(); }), null, 2) + ]); + }; + + var fuse = function (tuples, eventOrder, eventName) { + // ASSUMPTION: tuples.length will never be 0, because it wouldn't have an entry if it was 0 + var order = eventOrder[eventName]; + if (! order) return missingOrderError(eventName, tuples); + else return PrioritySort.sortKeys('Event: ' + eventName, 'name', tuples, order).map(function (sortedTuples) { + var handlers = Arr.map(sortedTuples, function (tuple) { return tuple.handler(); }); + return EventHandler.fuse(handlers); + }); + }; + + var combineGroups = function (byEventName, eventOrder) { + var r = Obj.mapToArray(byEventName, function (tuples, eventName) { + var combined = tuples.length === 1 ? Result.value(tuples[0].handler()) : fuse(tuples, eventOrder, eventName); + return combined.map(function (handler) { + var assembled = assemble(handler); + var purpose = tuples.length > 1 ? Arr.filter(eventOrder, function (o) { + return Arr.contains(tuples, function (t) { return t.name() === o; }); + }).join(' > ') : tuples[0].name(); + return Objects.wrap(eventName, DescribedHandler.nu(assembled, purpose)); + }); + }); + + return Objects.consolidate(r, {}); + }; + + return { + combine: combine + }; + } +); +define( + 'ephox.alloy.construct.CustomDefinition', + + [ + 'ephox.alloy.data.Fields', + 'ephox.alloy.dom.DomDefinition', + 'ephox.alloy.dom.DomModification', + 'ephox.alloy.ephemera.AlloyTags', + 'ephox.boulder.api.FieldPresence', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'global!Error' + ], + + function (Fields, DomDefinition, DomModification, AlloyTags, FieldPresence, FieldSchema, Objects, ValueSchema, Arr, Fun, Merger, Error) { + + var toInfo = function (spec) { + return ValueSchema.asStruct('custom.definition', ValueSchema.objOfOnly([ + FieldSchema.field('dom', 'dom', FieldPresence.strict(), ValueSchema.objOfOnly([ + // Note, no children. + FieldSchema.strict('tag'), + FieldSchema.defaulted('styles', {}), + FieldSchema.defaulted('classes', []), + FieldSchema.defaulted('attributes', {}), + FieldSchema.option('value'), + FieldSchema.option('innerHtml') + ])), + FieldSchema.strict('components'), + FieldSchema.strict('uid'), + + FieldSchema.defaulted('events', {}), + FieldSchema.defaulted('apis', Fun.constant({})), + + // Use mergeWith in the future when pre-built behaviours conflict + FieldSchema.field( + 'eventOrder', + 'eventOrder', + FieldPresence.mergeWith({ + 'alloy.execute': [ 'disabling', 'alloy.base.behaviour', 'toggling' ], + 'alloy.focus': [ 'alloy.base.behaviour', 'keying', 'focusing' ], + 'alloy.system.init': [ 'alloy.base.behaviour', 'disabling', 'toggling', 'representing' ], + 'input': [ 'alloy.base.behaviour', 'representing', 'streaming', 'invalidating' ], + 'alloy.system.detached': [ 'alloy.base.behaviour', 'representing' ] + }), + ValueSchema.anyValue() + ), + + FieldSchema.option('domModification'), + Fields.snapshot('originalSpec'), + + // Need to have this initially + FieldSchema.defaulted('debug.sketcher', 'unknown') + ]), spec); + }; + + var getUid = function (info) { + return Objects.wrap(AlloyTags.idAttr(), info.uid()); + }; + + var toDefinition = function (info) { + var base = { + tag: info.dom().tag(), + classes: info.dom().classes(), + attributes: Merger.deepMerge( + getUid(info), + info.dom().attributes() + ), + styles: info.dom().styles(), + domChildren: Arr.map(info.components(), function (comp) { return comp.element(); }) + }; + + return DomDefinition.nu(Merger.deepMerge(base, + info.dom().innerHtml().map(function (h) { return Objects.wrap('innerHtml', h); }).getOr({ }), + info.dom().value().map(function (h) { return Objects.wrap('value', h); }).getOr({ }) + )); + }; + + var toModification = function (info) { + return info.domModification().fold(function () { + return DomModification.nu({ }); + }, DomModification.nu); + }; + + // Probably want to pass info to these at some point. + var toApis = function (info) { + return info.apis(); + }; + + var toEvents = function (info) { + return info.events(); + }; + + return { + toInfo: toInfo, + toDefinition: toDefinition, + toModification: toModification, + toApis: toApis, + toEvents: toEvents + }; + } +); +define( + 'ephox.sugar.api.properties.Classes', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.impl.ClassList', + 'global!Array' + ], + + function (Arr, Class, ClassList, Array) { + /* + * ClassList is IE10 minimum: + * https://developer.mozilla.org/en-US/docs/Web/API/Element.classList + */ + var add = function (element, classes) { + Arr.each(classes, function (x) { + Class.add(element, x); + }); + }; + + var remove = function (element, classes) { + Arr.each(classes, function (x) { + Class.remove(element, x); + }); + }; + + var toggle = function (element, classes) { + Arr.each(classes, function (x) { + Class.toggle(element, x); + }); + }; + + var hasAll = function (element, classes) { + return Arr.forall(classes, function (clazz) { + return Class.has(element, clazz); + }); + }; + + var hasAny = function (element, classes) { + return Arr.exists(classes, function (clazz) { + return Class.has(element, clazz); + }); + }; + + var getNative = function (element) { + var classList = element.dom().classList; + var r = new Array(classList.length); + for (var i = 0; i < classList.length; i++) { + r[i] = classList.item(i); + } + return r; + }; + + var get = function (element) { + return ClassList.supports(element) ? getNative(element) : ClassList.get(element); + }; + + // set deleted, risks bad performance. Be deterministic. + + return { + add: add, + remove: remove, + toggle: toggle, + hasAll: hasAll, + hasAny: hasAny, + get: get + }; + } +); + +define( + 'ephox.alloy.dom.DomRender', + + [ + 'ephox.alloy.dom.DomDefinition', + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.Classes', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Html', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.properties.Value', + 'global!Error' + ], + + function (DomDefinition, Arr, Attr, Classes, Css, Element, Html, InsertAll, Value, Error) { + var getChildren = function (definition) { + if (definition.domChildren().isSome() && definition.defChildren().isSome()) { + throw new Error('Cannot specify children and child specs! Must be one or the other.\nDef: ' + DomDefinition.defToStr(definition)); + } else { + return definition.domChildren().fold(function () { + var defChildren = definition.defChildren().getOr([ ]); + return Arr.map(defChildren, renderDef); + }, function (domChildren) { + return domChildren; + }); + } + }; + + var renderToDom = function (definition) { + var subject = Element.fromTag(definition.tag()); + Attr.setAll(subject, definition.attributes().getOr({ })); + Classes.add(subject, definition.classes().getOr([ ])); + Css.setAll(subject, definition.styles().getOr({ })); + // Remember: Order of innerHtml vs children is important. + Html.set(subject, definition.innerHtml().getOr('')); + + // Children are already elements. + var children = getChildren(definition); + InsertAll.append(subject, children); + + definition.value().each(function (value) { + Value.set(subject, value); + }); + + return subject; + }; + + var renderDef = function (spec) { + var definition = DomDefinition.nu(spec); + return renderToDom(definition); + }; + + return { + renderToDom: renderToDom + }; + } +); +define( + 'ephox.alloy.api.component.Component', + + [ + 'ephox.alloy.api.component.CompBehaviours', + 'ephox.alloy.api.component.ComponentApi', + 'ephox.alloy.api.system.NoContextApi', + 'ephox.alloy.api.ui.GuiTypes', + 'ephox.alloy.behaviour.common.BehaviourBlob', + 'ephox.alloy.construct.ComponentDom', + 'ephox.alloy.construct.ComponentEvents', + 'ephox.alloy.construct.CustomDefinition', + 'ephox.alloy.dom.DomModification', + 'ephox.alloy.dom.DomRender', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Type', + 'ephox.sand.api.JSON', + 'ephox.sugar.api.search.Traverse', + 'global!Error' + ], + + function ( + CompBehaviours, ComponentApi, NoContextApi, GuiTypes, BehaviourBlob, ComponentDom, ComponentEvents, CustomDefinition, DomModification, DomRender, ValueSchema, + Arr, Cell, Fun, Merger, Type, Json, Traverse, Error + ) { + var build = function (spec) { + var getMe = function () { + return me; + }; + + var systemApi = Cell(NoContextApi(getMe)); + + + var info = ValueSchema.getOrDie(CustomDefinition.toInfo(Merger.deepMerge( + spec, + {behaviours: undefined} + ))); + + // The behaviour configuration is put into info.behaviours(). For everything else, + // we just need the list of static behaviours that this component cares about. The behaviour info + // to pass through will come from the info.behaviours() obj. + var bBlob = CompBehaviours.generate(spec); + var bList = BehaviourBlob.getBehaviours(bBlob); + var bData = BehaviourBlob.getData(bBlob); + + var definition = CustomDefinition.toDefinition(info); + + var baseModification = { + 'alloy.base.modification': CustomDefinition.toModification(info) + }; + + var modification = ComponentDom.combine(bData, baseModification, bList, definition).getOrDie(); + + var modDefinition = DomModification.merge(definition, modification); + + var item = DomRender.renderToDom(modDefinition); + + var baseEvents = { + 'alloy.base.behaviour': CustomDefinition.toEvents(info) + }; + + var events = ComponentEvents.combine(bData, info.eventOrder(), bList, baseEvents).getOrDie(); + + var subcomponents = Cell(info.components()); + + var connect = function (newApi) { + systemApi.set(newApi); + }; + + var disconnect = function () { + systemApi.set(NoContextApi(getMe)); + }; + + var syncComponents = function () { + // Update the component list with the current children + var children = Traverse.children(item); + var subs = Arr.bind(children, function (child) { + + return systemApi.get().getByDom(child).fold(function () { + // INVESTIGATE: Not sure about how to handle text nodes here. + return [ ]; + }, function (c) { + return [ c ]; + }); + }); + subcomponents.set(subs); + }; + + var config = function (behaviour) { + if (behaviour === GuiTypes.apiConfig()) return info.apis(); + var b = bData; + var f = Type.isFunction(b[behaviour.name()]) ? b[behaviour.name()] : function () { + throw new Error('Could not find ' + behaviour.name() + ' in ' + Json.stringify(spec, null, 2)); + }; + return f(); + // }); + }; + + var readState = function (behaviourName) { + return bData[behaviourName]().map(function (b) { + return b.state.readState(); + }).getOr('not enabled'); + }; + + var me = ComponentApi({ + getSystem: systemApi.get, + config: config, + spec: Fun.constant(spec), + readState: readState, + + connect: connect, + disconnect: disconnect, + element: Fun.constant(item), + syncComponents: syncComponents, + components: subcomponents.get, + events: Fun.constant(events) + }); + + return me; + }; + + return { + build: build + }; + } +); +define( + 'ephox.alloy.events.DefaultEvents', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.log.AlloyLogger', + 'ephox.sugar.api.dom.Compare', + 'global!console' + ], + + function (AlloyEvents, SystemEvents, AlloyLogger, Compare, console) { + // The purpose of this check is to ensure that a simulated focus call is not going + // to recurse infinitely. Essentially, if the originator of the focus call is the same + // as the element receiving it, and it wasn't its own target, then stop the focus call + // and log a warning. + var isRecursive = function (component, originator, target) { + return Compare.eq(originator, component.element()) && + !Compare.eq(originator, target); + }; + + return { + events: AlloyEvents.derive([ + AlloyEvents.can(SystemEvents.focus(), function (component, simulatedEvent) { + // originator may not always be there. Will need to check this. + var originator = simulatedEvent.event().originator(); + var target = simulatedEvent.event().target(); + if (isRecursive(component, originator, target)) { + console.warn( + SystemEvents.focus() + ' did not get interpreted by the desired target. ' + + '\nOriginator: ' + AlloyLogger.element(originator) + + '\nTarget: ' + AlloyLogger.element(target) + + '\nCheck the ' + SystemEvents.focus() + ' event handlers' + ); + return false; + } else { + return true; + } + }) + ]) + }; + } +); +define( + 'ephox.alloy.spec.CustomSpec', + + [ + + ], + + function () { + var make = function (spec) { + // Maybe default some arguments here + return spec; + }; + + return { + make: make + }; + } +); +define( + 'ephox.alloy.api.component.GuiFactory', + + [ + 'ephox.alloy.api.component.Component', + 'ephox.alloy.api.component.ComponentApi', + 'ephox.alloy.api.system.NoContextApi', + 'ephox.alloy.api.ui.GuiTypes', + 'ephox.alloy.events.DefaultEvents', + 'ephox.alloy.registry.Tagger', + 'ephox.alloy.spec.CustomSpec', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Result', + 'ephox.sugar.api.node.Element', + 'global!Error' + ], + + function ( + Component, ComponentApi, NoContextApi, GuiTypes, DefaultEvents, Tagger, CustomSpec, FieldSchema, Objects, ValueSchema, Arr, Cell, Fun, Merger, Option, Result, + Element, Error + ) { + var buildSubcomponents = function (spec) { + var components = Objects.readOr('components', [ ])(spec); + return Arr.map(components, build); + }; + + var buildFromSpec = function (userSpec) { + var spec = CustomSpec.make(userSpec); + + // Build the subcomponents + var components = buildSubcomponents(spec); + + var completeSpec = Merger.deepMerge( + DefaultEvents, + spec, + Objects.wrap('components', components) + ); + + return Result.value( + Component.build(completeSpec) + ); + }; + + var text = function (textContent) { + var element = Element.fromText(textContent); + + return external({ + element: element + }); + }; + + var external = function (spec) { + var extSpec = ValueSchema.asStructOrDie('external.component', ValueSchema.objOfOnly([ + FieldSchema.strict('element'), + FieldSchema.option('uid') + ]), spec); + + var systemApi = Cell(NoContextApi()); + + var connect = function (newApi) { + systemApi.set(newApi); + }; + + var disconnect = function () { + systemApi.set(NoContextApi(function () { + return me; + })); + }; + + extSpec.uid().each(function (uid) { + Tagger.writeOnly(extSpec.element(), uid); + }); + + var me = ComponentApi({ + getSystem: systemApi.get, + config: Option.none, + connect: connect, + disconnect: disconnect, + element: Fun.constant(extSpec.element()), + spec: Fun.constant(spec), + readState: Fun.constant('No state'), + syncComponents: Fun.noop, + components: Fun.constant([ ]), + events: Fun.constant({ }) + }); + + return GuiTypes.premade(me); + }; + + // INVESTIGATE: A better way to provide 'meta-specs' + var build = function (rawUserSpec) { + + return GuiTypes.getPremade(rawUserSpec).fold(function () { + var userSpecWithUid = Merger.deepMerge({ uid: Tagger.generate('') }, rawUserSpec); + return buildFromSpec(userSpecWithUid).getOrDie(); + }, function (prebuilt) { + return prebuilt; + }); + }; + + return { + build: build, + premade: GuiTypes.premade, + external: external, + text: text + }; + } +); +define( + 'ephox.alloy.menu.util.ItemEvents', + + [ + 'ephox.alloy.api.behaviour.Focusing', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Focus' + ], + + function (Focusing, AlloyTriggers, Fun, Focus) { + var hoverEvent = 'alloy.item-hover'; + var focusEvent = 'alloy.item-focus'; + + var onHover = function (item) { + // Firstly, check that the focus isn't already inside the item. This + // is to handle situations like widgets where the widget is inside the item + // and it has the focus, so as you slightly adjust the mouse, you don't + // want to lose focus on the widget. Note, that because this isn't API based + // (i.e. we are manually searching for focus), it may not be that flexible. + if (Focus.search(item.element()).isNone() || Focusing.isFocused(item)) { + if (! Focusing.isFocused(item)) Focusing.focus(item); + AlloyTriggers.emitWith(item, hoverEvent, { item: item }); + } + }; + + var onFocus = function (item) { + AlloyTriggers.emitWith(item, focusEvent, { item: item }); + }; + + return { + hover: Fun.constant(hoverEvent), + focus: Fun.constant(focusEvent), + + onHover: onHover, + onFocus: onFocus + }; + } +); +define( + 'ephox.alloy.menu.build.ItemType', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Focusing', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.data.Fields', + 'ephox.alloy.menu.util.ItemEvents', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Merger' + ], + + function (Behaviour, Focusing, Keying, Representing, Toggling, AlloyEvents, AlloyTriggers, NativeEvents, SystemEvents, Fields, ItemEvents, FieldSchema, Merger) { + var builder = function (info) { + return { + dom: Merger.deepMerge( + info.dom(), + { + attributes: { + role: info.toggling().isSome() ? 'menuitemcheckbox' : 'menuitem' + } + } + ), + behaviours: Merger.deepMerge( + Behaviour.derive([ + info.toggling().fold(Toggling.revoke, function (tConfig) { + return Toggling.config( + Merger.deepMerge({ + aria: { + mode: 'checked' + } + }, tConfig) + ); + }), + Focusing.config({ + ignore: info.ignoreFocus(), + onFocus: function (component) { + ItemEvents.onFocus(component); + } + }), + Keying.config({ + mode: 'execution' + }), + Representing.config({ + store: { + mode: 'memory', + initialValue: info.data() + } + }) + ]), + info.itemBehaviours() + ), + events: AlloyEvents.derive([ + // Trigger execute when clicked + AlloyEvents.runWithTarget(SystemEvents.tapOrClick(), AlloyTriggers.emitExecute), + + // Like button, stop mousedown propagating up the DOM tree. + AlloyEvents.cutter(NativeEvents.mousedown()), + + AlloyEvents.run(NativeEvents.mouseover(), ItemEvents.onHover), + + AlloyEvents.run(SystemEvents.focusItem(), Focusing.focus) + ]), + components: info.components(), + + domModification: info.domModification() + }; + }; + + var schema = [ + FieldSchema.strict('data'), + FieldSchema.strict('components'), + FieldSchema.strict('dom'), + + FieldSchema.option('toggling'), + + // Maybe this needs to have fewer behaviours + FieldSchema.defaulted('itemBehaviours', { }), + + FieldSchema.defaulted('ignoreFocus', false), + FieldSchema.defaulted('domModification', { }), + Fields.output('builder', builder) + ]; + + + + return schema; + } +); +define( + 'ephox.alloy.menu.build.SeparatorType', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema' + ], + + function (AlloyEvents, SystemEvents, Fields, FieldSchema) { + var builder = function (detail) { + return { + dom: detail.dom(), + components: detail.components(), + events: AlloyEvents.derive([ + AlloyEvents.stopper(SystemEvents.focusItem()) + ]) + }; + }; + + var schema = [ + FieldSchema.strict('dom'), + FieldSchema.strict('components'), + Fields.output('builder', builder) + ]; + + return schema; + } +); +define( + 'ephox.alloy.menu.build.WidgetParts', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.parts.PartType', + 'ephox.katamari.api.Fun' + ], + + function (Behaviour, Representing, PartType, Fun) { + var owner = 'item-widget'; + + var partTypes = [ + PartType.required({ + name: 'widget', + overrides: function (detail) { + return { + behaviours: Behaviour.derive([ + Representing.config({ + store: { + mode: 'manual', + getValue: function (component) { + return detail.data(); + }, + setValue: function () { } + } + }) + ]) + }; + } + }) + ]; + + return { + owner: Fun.constant(owner), + parts: Fun.constant(partTypes) + }; + } +); + +define( + 'ephox.alloy.menu.build.WidgetType', + + [ + 'ephox.alloy.alien.EditableFields', + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Focusing', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.data.Fields', + 'ephox.alloy.menu.build.WidgetParts', + 'ephox.alloy.menu.util.ItemEvents', + 'ephox.alloy.parts.AlloyParts', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Option' + ], + + function ( + EditableFields, Behaviour, Focusing, Keying, Representing, AlloyEvents, NativeEvents, SystemEvents, Fields, WidgetParts, ItemEvents, AlloyParts, FieldSchema, + Merger, Option + ) { + var builder = function (info) { + var subs = AlloyParts.substitutes(WidgetParts.owner(), info, WidgetParts.parts()); + var components = AlloyParts.components(WidgetParts.owner(), info, subs.internals()); + + var focusWidget = function (component) { + return AlloyParts.getPart(component, info, 'widget').map(function (widget) { + Keying.focusIn(widget); + return widget; + }); + }; + + var onHorizontalArrow = function (component, simulatedEvent) { + return EditableFields.inside(simulatedEvent.event().target()) ? Option.none() : (function () { + if (info.autofocus()) { + simulatedEvent.setSource(component.element()); + return Option.none(); + } else { + return Option.none(); + } + })(); + }; + + return Merger.deepMerge({ + dom: info.dom(), + components: components, + domModification: info.domModification(), + events: AlloyEvents.derive([ + AlloyEvents.runOnExecute(function (component, simulatedEvent) { + focusWidget(component).each(function (widget) { + simulatedEvent.stop(); + }); + }), + + AlloyEvents.run(NativeEvents.mouseover(), ItemEvents.onHover), + + AlloyEvents.run(SystemEvents.focusItem(), function (component, simulatedEvent) { + if (info.autofocus()) focusWidget(component); + else Focusing.focus(component); + }) + ]), + behaviours: Behaviour.derive([ + Representing.config({ + store: { + mode: 'memory', + initialValue: info.data() + } + }), + Focusing.config({ + onFocus: function (component) { + ItemEvents.onFocus(component); + } + }), + Keying.config({ + mode: 'special', + // focusIn: info.autofocus() ? function (component) { + // focusWidget(component); + // } : Behaviour.revoke(), + onLeft: onHorizontalArrow, + onRight: onHorizontalArrow, + onEscape: function (component, simulatedEvent) { + // If the outer list item didn't have focus, + // then focus it (i.e. escape the inner widget). Only do if not autofocusing + // Autofocusing should treat the widget like it is the only item, so it should + // let its outer menu handle escape + if (! Focusing.isFocused(component) && !info.autofocus()) { + Focusing.focus(component); + return Option.some(true); + } else if (info.autofocus()) { + simulatedEvent.setSource(component.element()); + return Option.none(); + } else { + return Option.none(); + } + } + }) + ]) + }); + }; + + var schema = [ + FieldSchema.strict('uid'), + FieldSchema.strict('data'), + FieldSchema.strict('components'), + FieldSchema.strict('dom'), + FieldSchema.defaulted('autofocus', false), + FieldSchema.defaulted('domModification', { }), + // We don't have the uid at this point + AlloyParts.defaultUidsSchema(WidgetParts.parts()), + Fields.output('builder', builder) + ]; + + + return schema; + } +); +define( + 'ephox.alloy.ui.schema.MenuSchema', + + [ + 'ephox.alloy.api.focus.FocusManagers', + 'ephox.alloy.data.Fields', + 'ephox.alloy.menu.build.ItemType', + 'ephox.alloy.menu.build.SeparatorType', + 'ephox.alloy.menu.build.WidgetType', + 'ephox.alloy.parts.PartType', + 'ephox.alloy.registry.Tagger', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger' + ], + + function (FocusManagers, Fields, ItemType, SeparatorType, WidgetType, PartType, Tagger, FieldSchema, ValueSchema, Fun, Merger) { + var itemSchema = ValueSchema.choose( + 'type', + { + widget: WidgetType, + item: ItemType, + separator: SeparatorType + } + ); + + var configureGrid = function (detail, movementInfo) { + return { + mode: 'flatgrid', + selector: '.' + detail.markers().item(), + initSize: { + numColumns: movementInfo.initSize().numColumns(), + numRows: movementInfo.initSize().numRows() + }, + focusManager: detail.focusManager() + }; + }; + + var configureMenu = function (detail, movementInfo) { + return { + mode: 'menu', + selector: '.' + detail.markers().item(), + moveOnTab: movementInfo.moveOnTab(), + focusManager: detail.focusManager() + }; + }; + + var parts = [ + PartType.group({ + factory: { + sketch: function (spec) { + var itemInfo = ValueSchema.asStructOrDie('menu.spec item', itemSchema, spec); + return itemInfo.builder()(itemInfo); + } + }, + name: 'items', + unit: 'item', + defaults: function (detail, u) { + var fallbackUid = Tagger.generate(''); + return Merger.deepMerge( + { + uid: fallbackUid + }, + u + ); + }, + overrides: function (detail, u) { + return { + type: u.type, + ignoreFocus: detail.fakeFocus(), + domModification: { + classes: [ detail.markers().item() ] + } + }; + } + }) + ]; + + var schema = [ + FieldSchema.strict('value'), + FieldSchema.strict('items'), + FieldSchema.strict('dom'), + FieldSchema.strict('components'), + FieldSchema.defaulted('eventOrder', { }), + FieldSchema.defaulted('menuBehaviours', { }), + + + FieldSchema.defaultedOf('movement', { + mode: 'menu', + moveOnTab: true + }, ValueSchema.choose( + 'mode', + { + grid: [ + Fields.initSize(), + Fields.output('config', configureGrid) + ], + menu: [ + FieldSchema.defaulted('moveOnTab', true), + Fields.output('config', configureMenu) + ] + } + )), + + Fields.itemMarkers(), + + FieldSchema.defaulted('fakeFocus', false), + FieldSchema.defaulted('focusManager', FocusManagers.dom()), + Fields.onHandler('onHighlight') + ]; + + return { + name: Fun.constant('Menu'), + schema: Fun.constant(schema), + parts: Fun.constant(parts) + }; + } +); +define( + 'ephox.alloy.menu.util.MenuEvents', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + var focusEvent = 'alloy.menu-focus'; + + return { + focus: Fun.constant(focusEvent) + }; + } +); +define( + 'ephox.alloy.ui.single.MenuSpec', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Composing', + 'ephox.alloy.api.behaviour.Highlighting', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.menu.util.ItemEvents', + 'ephox.alloy.menu.util.MenuEvents', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'global!Error' + ], + + function (Behaviour, Composing, Highlighting, Keying, Representing, AlloyEvents, AlloyTriggers, ItemEvents, MenuEvents, Fun, Merger, Error) { + var make = function (detail, components, spec, externals) { + return Merger.deepMerge( + { + dom: Merger.deepMerge( + detail.dom(), + { + attributes: { + role: 'menu' + } + } + ), + uid: detail.uid(), + + behaviours: Merger.deepMerge( + Behaviour.derive([ + Highlighting.config({ + // Highlighting for a menu is selecting items inside the menu + highlightClass: detail.markers().selectedItem(), + itemClass: detail.markers().item(), + onHighlight: detail.onHighlight() + }), + Representing.config({ + store: { + mode: 'memory', + initialValue: detail.value() + } + }), + // FIX: Is this used? It has the wrong return type. + Composing.config({ + find: Fun.identity + }), + Keying.config(detail.movement().config()(detail, detail.movement())) + ]), + detail.menuBehaviours() + ), + events: AlloyEvents.derive([ + // This is dispatched from a menu to tell an item to be highlighted. + AlloyEvents.run(ItemEvents.focus(), function (menu, simulatedEvent) { + // Highlight the item + var event = simulatedEvent.event(); + menu.getSystem().getByDom(event.target()).each(function (item) { + Highlighting.highlight(menu, item); + + simulatedEvent.stop(); + + // Trigger the focus event on the menu. + AlloyTriggers.emitWith(menu, MenuEvents.focus(), { menu: menu, item: item }); + }); + }), + + // Highlight the item that the cursor is over. The onHighlight + // code needs to handle updating focus if required + AlloyEvents.run(ItemEvents.hover(), function (menu, simulatedEvent) { + var item = simulatedEvent.event().item(); + Highlighting.highlight(menu, item); + }) + ]), + components: components, + eventOrder: detail.eventOrder() + } + ); + }; + + return { + make: make + }; + } +); +define( + 'ephox.alloy.api.ui.Menu', + + [ + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.ui.schema.MenuSchema', + 'ephox.alloy.ui.single.MenuSpec' + ], + + function (Sketcher, MenuSchema, MenuSpec) { + return Sketcher.composite({ + name: 'Menu', + configFields: MenuSchema.schema(), + partFields: MenuSchema.parts(), + factory: MenuSpec.make + }); + } +); +define( + 'ephox.alloy.alien.AriaFocus', + + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.search.PredicateFind', + 'ephox.sugar.api.search.Traverse' + ], + + function (Option, Compare, Focus, PredicateFind, Traverse) { + var preserve = function (f, container) { + var ownerDoc = Traverse.owner(container); + + var refocus = Focus.active(ownerDoc).bind(function (focused) { + var hasFocus = function (elem) { + return Compare.eq(focused, elem); + }; + return hasFocus(container) ? Option.some(container) : PredicateFind.descendant(container, hasFocus); + }); + + var result = f(container); + + // If there is a focussed element, the F function may cause focus to be lost (such as by hiding elements). Restore it afterwards. + refocus.each(function (oldFocus) { + Focus.active(ownerDoc).filter(function (newFocus) { + return Compare.eq(newFocus, oldFocus); + }).orThunk(function () { + // Only refocus if the focus has changed, otherwise we break IE + Focus.focus(oldFocus); + }); + }); + return result; + }; + + return { + preserve: preserve + }; + } +); +define( + 'ephox.alloy.behaviour.replacing.ReplaceApis', + + [ + 'ephox.alloy.alien.AriaFocus', + 'ephox.alloy.api.system.Attachment', + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Insert' + ], + + function (AriaFocus, Attachment, Arr, Compare, Insert) { + var set = function (component, replaceConfig, replaceState, data) { + Attachment.detachChildren(component); + + // NOTE: we may want to create a behaviour which allows you to switch + // between predefined layouts, which would make a noop detection easier. + // Until then, we'll just use AriaFocus like redesigning does. + AriaFocus.preserve(function () { + var children = Arr.map(data, component.getSystem().build); + + Arr.each(children, function (l) { + Attachment.attach(component, l); + }); + }, component.element()); + }; + + var insert = function (component, replaceConfig, insertion, childSpec) { + var child = component.getSystem().build(childSpec); + Attachment.attachWith(component, child, insertion); + }; + + var append = function (component, replaceConfig, replaceState, appendee) { + insert(component, replaceConfig, Insert.append, appendee); + }; + + var prepend = function (component, replaceConfig, replaceState, prependee) { + insert(component, replaceConfig, Insert.prepend, prependee); + }; + + // NOTE: Removee is going to be a component, not a spec. + var remove = function (component, replaceConfig, replaceState, removee) { + var children = contents(component, replaceConfig); + var foundChild = Arr.find(children, function (child) { + return Compare.eq(removee.element(), child.element()); + }); + + foundChild.each(Attachment.detach); + }; + + // TODO: Rename + var contents = function (component, replaceConfig/*, replaceState */) { + return component.components(); + }; + + return { + append: append, + prepend: prepend, + remove: remove, + set: set, + contents: contents + }; + } +); +define( + 'ephox.alloy.api.behaviour.Replacing', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.replacing.ReplaceApis' + ], + + function (Behaviour, ReplaceApis) { + return Behaviour.create({ + fields: [ ], + name: 'replacing', + apis: ReplaceApis + }); + } +); +define( + 'ephox.alloy.menu.layered.MenuPathing', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option' + ], + + function (Objects, Arr, Obj, Option) { + var transpose = function (obj) { + // Assumes no duplicate fields. + return Obj.tupleMap(obj, function (v, k) { + return { k: v, v: k }; + }); + }; + var trace = function (items, byItem, byMenu, finish) { + // Given a finishing submenu (which will be the value of expansions), + // find the triggering item, find its menu, and repeat the process. If there + // is no triggering item, we are done. + return Objects.readOptFrom(byMenu, finish).bind(function (triggerItem) { + return Objects.readOptFrom(items, triggerItem).bind(function (triggerMenu) { + var rest = trace(items, byItem, byMenu, triggerMenu); + return Option.some([ triggerMenu ].concat(rest)); + }); + }).getOr([ ]); + }; + + var generate = function (menus, expansions) { + var items = { }; + Obj.each(menus, function (menuItems, menu) { + Arr.each(menuItems, function (item) { + items[item] = menu; + }); + }); + + var byItem = expansions; + var byMenu = transpose(expansions); + + var menuPaths = Obj.map(byMenu, function (triggerItem, submenu) { + return [ submenu ].concat(trace(items, byItem, byMenu, submenu)); + }); + + return Obj.map(items, function (path) { + return Objects.readOptFrom(menuPaths, path).getOr([ path ]); + }); + }; + + return { + generate: generate + }; + } +); +define( + 'ephox.alloy.menu.layered.LayeredState', + + [ + 'ephox.alloy.menu.layered.MenuPathing', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Cell' + ], + + function (MenuPathing, Objects, Arr, Obj, Fun, Option, Cell) { + return function () { + var expansions = Cell({ }); + var menus = Cell({ }); + var paths = Cell({ }); + var primary = Cell(Option.none()); + + // Probably think of a better way to store this information. + var toItemValues = Cell( + Fun.constant([ ]) + ); + + var clear = function () { + expansions.set({}); + menus.set({}); + paths.set({}); + primary.set(Option.none()); + }; + + var isClear = function () { + return primary.get().isNone(); + }; + + var setContents = function (sPrimary, sMenus, sExpansions, sToItemValues) { + primary.set(Option.some(sPrimary)); + expansions.set(sExpansions); + menus.set(sMenus); + toItemValues.set(sToItemValues); + var menuValues = sToItemValues(sMenus); + var sPaths = MenuPathing.generate(menuValues, sExpansions); + paths.set(sPaths); + }; + + var expand = function (itemValue) { + return Objects.readOptFrom(expansions.get(), itemValue).map(function (menu) { + var current = Objects.readOptFrom(paths.get(), itemValue).getOr([ ]); + return [ menu ].concat(current); + }); + }; + + var collapse = function (itemValue) { + // Look up which key has the itemValue + return Objects.readOptFrom(paths.get(), itemValue).bind(function (path) { + return path.length > 1 ? Option.some(path.slice(1)) : Option.none(); + }); + }; + + var refresh = function (itemValue) { + return Objects.readOptFrom(paths.get(), itemValue); + }; + + var lookupMenu = function (menuValue) { + return Objects.readOptFrom( + menus.get(), + menuValue + ); + }; + + var otherMenus = function (path) { + var menuValues = toItemValues.get()(menus.get()); + return Arr.difference(Obj.keys(menuValues), path); + }; + + var getPrimary = function () { + return primary.get().bind(lookupMenu); + }; + + var getMenus = function () { + return menus.get(); + }; + + return { + setContents: setContents, + expand: expand, + refresh: refresh, + collapse: collapse, + lookupMenu: lookupMenu, + otherMenus: otherMenus, + getPrimary: getPrimary, + getMenus: getMenus, + clear: clear, + isClear: isClear + }; + }; + } +); +define( + 'ephox.alloy.ui.single.TieredMenuSpec', + + [ + 'ephox.alloy.alien.EditableFields', + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Composing', + 'ephox.alloy.api.behaviour.Highlighting', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.api.focus.FocusManagers', + 'ephox.alloy.api.ui.Menu', + 'ephox.alloy.menu.layered.LayeredState', + 'ephox.alloy.menu.util.ItemEvents', + 'ephox.alloy.menu.util.MenuEvents', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.properties.Classes', + 'ephox.sugar.api.search.SelectorFind' + ], + + function ( + EditableFields, Behaviour, Composing, Highlighting, Keying, Replacing, Representing, GuiFactory, AlloyEvents, AlloyTriggers, SystemEvents, FocusManagers, + Menu, LayeredState, ItemEvents, MenuEvents, Objects, Arr, Fun, Merger, Obj, Option, Options, Body, Class, Classes, SelectorFind + ) { + var make = function (detail, rawUiSpec) { + var buildMenus = function (container, menus) { + return Obj.map(menus, function (spec, name) { + var data = Menu.sketch( + Merger.deepMerge( + spec, + { + value: name, + items: spec.items, + markers: Objects.narrow(rawUiSpec.markers, [ 'item', 'selectedItem' ]), + + // Fake focus. + fakeFocus: detail.fakeFocus(), + onHighlight: detail.onHighlight(), + + + focusManager: detail.fakeFocus() ? FocusManagers.highlights() : FocusManagers.dom() + } + ) + ); + + return container.getSystem().build(data); + }); + }; + + var state = LayeredState(); + + var setup = function (container) { + var componentMap = buildMenus(container, detail.data().menus()); + state.setContents(detail.data().primary(), componentMap, detail.data().expansions(), function (sMenus) { + return toMenuValues(container, sMenus); + }); + + return state.getPrimary(); + }; + + var getItemValue = function (item) { + return Representing.getValue(item).value; + }; + + var toMenuValues = function (container, sMenus) { + return Obj.map(detail.data().menus(), function (data, menuName) { + return Arr.bind(data.items, function (item) { + return item.type === 'separator' ? [ ] : [ item.data.value ]; + }); + }); + }; + + var setActiveMenu = function (container, menu) { + Highlighting.highlight(container, menu); + Highlighting.getHighlighted(menu).orThunk(function () { + return Highlighting.getFirst(menu); + }).each(function (item) { + AlloyTriggers.dispatch(container, item.element(), SystemEvents.focusItem()); + }); + }; + + var getMenus = function (state, menuValues) { + return Options.cat( + Arr.map(menuValues, state.lookupMenu) + ); + }; + + var updateMenuPath = function (container, state, path) { + return Option.from(path[0]).bind(state.lookupMenu).map(function (activeMenu) { + var rest = getMenus(state, path.slice(1)); + Arr.each(rest, function (r) { + Class.add(r.element(), detail.markers().backgroundMenu()); + }); + + if (! Body.inBody(activeMenu.element())) { + Replacing.append(container, GuiFactory.premade(activeMenu)); + } + + // Remove the background-menu class from the active menu + Classes.remove(activeMenu.element(), [ detail.markers().backgroundMenu() ]); + setActiveMenu(container, activeMenu); + var others = getMenus(state, state.otherMenus(path)); + Arr.each(others, function (o) { + // May not need to do the active menu thing. + Classes.remove(o.element(), [ detail.markers().backgroundMenu() ]); + if (! detail.stayInDom()) Replacing.remove(container, o); + }); + + return activeMenu; + }); + + }; + + var expandRight = function (container, item) { + var value = getItemValue(item); + return state.expand(value).bind(function (path) { + // When expanding, always select the first. + Option.from(path[0]).bind(state.lookupMenu).each(function (activeMenu) { + // DUPE with above. Fix later. + if (! Body.inBody(activeMenu.element())) { + Replacing.append(container, GuiFactory.premade(activeMenu)); + } + + detail.onOpenSubmenu()(container, item, activeMenu); + Highlighting.highlightFirst(activeMenu); + }); + + return updateMenuPath(container, state, path); + }); + }; + + var collapseLeft = function (container, item) { + var value = getItemValue(item); + return state.collapse(value).bind(function (path) { + return updateMenuPath(container, state, path).map(function (activeMenu) { + detail.onCollapseMenu()(container, item, activeMenu); + return activeMenu; + }); + }); + }; + + var updateView = function (container, item) { + var value = getItemValue(item); + return state.refresh(value).bind(function (path) { + return updateMenuPath(container, state, path); + }); + }; + + var onRight = function (container, item) { + return EditableFields.inside(item.element()) ? Option.none() : expandRight(container, item); + }; + + var onLeft = function (container, item) { + // Exclude inputs, textareas etc. + return EditableFields.inside(item.element()) ? Option.none() : collapseLeft(container, item); + }; + + var onEscape = function (container, item) { + return collapseLeft(container, item).orThunk(function () { + return detail.onEscape()(container, item); + // This should only fire when the user presses ESC ... not any other close. + // return HotspotViews.onEscape(detail.lazyAnchor()(), container); + }); + }; + + var keyOnItem = function (f) { + return function (container, simulatedEvent) { + return SelectorFind.closest(simulatedEvent.getSource(), '.' + detail.markers().item()).bind(function (target) { + return container.getSystem().getByDom(target).bind(function (item) { + return f(container, item); + }); + }); + }; + }; + + var events = AlloyEvents.derive([ + // Set "active-menu" for the menu with focus + AlloyEvents.run(MenuEvents.focus(), function (sandbox, simulatedEvent) { + var menu = simulatedEvent.event().menu(); + Highlighting.highlight(sandbox, menu); + }), + + + AlloyEvents.runOnExecute(function (sandbox, simulatedEvent) { + // Trigger on execute on the targeted element + // I.e. clicking on menu item + var target = simulatedEvent.event().target(); + return sandbox.getSystem().getByDom(target).bind(function (item) { + var itemValue = getItemValue(item); + if (itemValue.indexOf('collapse-item') === 0) { + return collapseLeft(sandbox, item); + } + + + return expandRight(sandbox, item).orThunk(function () { + return detail.onExecute()(sandbox, item); + }); + }); + }), + + // Open the menu as soon as it is added to the DOM + AlloyEvents.runOnAttached(function (container, simulatedEvent) { + setup(container).each(function (primary) { + Replacing.append(container, GuiFactory.premade(primary)); + + if (detail.openImmediately()) { + setActiveMenu(container, primary); + detail.onOpenMenu()(container, primary); + } + }); + }) + ].concat(detail.navigateOnHover() ? [ + // Hide any irrelevant submenus and expand any submenus based + // on hovered item + AlloyEvents.run(ItemEvents.hover(), function (sandbox, simulatedEvent) { + var item = simulatedEvent.event().item(); + updateView(sandbox, item); + expandRight(sandbox, item); + detail.onHover()(sandbox, item); + }) + ] : [ ])); + + var collapseMenuApi = function (container) { + Highlighting.getHighlighted(container).each(function (currentMenu) { + Highlighting.getHighlighted(currentMenu).each(function (currentItem) { + collapseLeft(container, currentItem); + }); + }); + }; + + return { + uid: detail.uid(), + dom: detail.dom(), + behaviours: Merger.deepMerge( + Behaviour.derive([ + Keying.config({ + mode: 'special', + onRight: keyOnItem(onRight), + onLeft: keyOnItem(onLeft), + onEscape: keyOnItem(onEscape), + focusIn: function (container, keyInfo) { + state.getPrimary().each(function (primary) { + AlloyTriggers.dispatch(container, primary.element(), SystemEvents.focusItem()); + }); + } + }), + // Highlighting is used for highlighting the active menu + Highlighting.config({ + highlightClass: detail.markers().selectedMenu(), + itemClass: detail.markers().menu() + }), + Composing.config({ + find: function (container) { + return Highlighting.getHighlighted(container); + } + }), + Replacing.config({ }) + ]), + detail.tmenuBehaviours() + ), + eventOrder: detail.eventOrder(), + apis: { + collapseMenu: collapseMenuApi + }, + events: events + }; + }; + + return { + make: make, + collapseItem: Fun.constant('collapse-item') + }; + } +); +define( + 'ephox.alloy.api.ui.TieredMenu', + + [ + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.data.Fields', + 'ephox.alloy.ui.single.TieredMenuSpec', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Id' + ], + + function (Sketcher, Fields, TieredMenuSpec, FieldSchema, Objects, Id) { + var tieredData = function (primary, menus, expansions) { + return { + primary: primary, + menus: menus, + expansions: expansions + }; + }; + + var singleData = function (name, menu) { + return { + primary: name, + menus: Objects.wrap(name, menu), + expansions: { } + }; + }; + + var collapseItem = function (text) { + return { + value: Id.generate(TieredMenuSpec.collapseItem()), + text: text + }; + }; + + return Sketcher.single({ + name: 'TieredMenu', + configFields: [ + Fields.onStrictKeyboardHandler('onExecute'), + Fields.onStrictKeyboardHandler('onEscape'), + + Fields.onStrictHandler('onOpenMenu'), + Fields.onStrictHandler('onOpenSubmenu'), + Fields.onHandler('onCollapseMenu'), + + FieldSchema.defaulted('openImmediately', true), + + FieldSchema.strictObjOf('data', [ + FieldSchema.strict('primary'), + FieldSchema.strict('menus'), + FieldSchema.strict('expansions') + ]), + + FieldSchema.defaulted('fakeFocus', false), + Fields.onHandler('onHighlight'), + Fields.onHandler('onHover'), + Fields.tieredMenuMarkers(), + + + FieldSchema.strict('dom'), + + FieldSchema.defaulted('navigateOnHover', true), + FieldSchema.defaulted('stayInDom', false), + + FieldSchema.defaulted('tmenuBehaviours', { }), + FieldSchema.defaulted('eventOrder', { }) + ], + + apis: { + collapseMenu: function (apis, tmenu) { + apis.collapseMenu(tmenu); + } + }, + + factory: TieredMenuSpec.make, + + extraApis: { + tieredData: tieredData, + singleData: singleData, + collapseItem: collapseItem + } + }); + } +); +define( + 'tinymce.themes.mobile.touch.scroll.Scrollable', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.properties.Class', + 'tinymce.themes.mobile.style.Styles' + ], + + function (Fun, Class, Styles) { + var scrollable = Styles.resolve('scrollable'); + + var register = function (element) { + /* + * The reason this function exists is to have a + * central place where to set if an element can be explicitly + * scrolled. This is for mobile devices atm. + */ + Class.add(element, scrollable); + }; + + var deregister = function (element) { + Class.remove(element, scrollable); + }; + + return { + register: register, + deregister: deregister, + scrollable: Fun.constant(scrollable) + }; + } +); + +define( + 'tinymce.themes.mobile.ui.StylesMenu', + + [ + 'ephox.alloy.api.behaviour.AddEventsBehaviour', + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Representing', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.behaviour.Transitioning', + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.component.Memento', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.ui.Button', + 'ephox.alloy.api.ui.Menu', + 'ephox.alloy.api.ui.TieredMenu', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Obj', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.SelectorFind', + 'ephox.sugar.api.view.Width', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.touch.scroll.Scrollable' + ], + + function ( + AddEventsBehaviour, Behaviour, Representing, Toggling, Transitioning, GuiFactory, Memento, + AlloyEvents, Button, Menu, TieredMenu, Objects, Arr, Merger, Obj, Css, SelectorFind, + Width, Receivers, Styles, Scrollable + ) { + + var getValue = function (item) { + return Objects.readOptFrom(item, 'format').getOr(item.title); + }; + + var convert = function (formats, memMenuThunk) { + var mainMenu = makeMenu('Styles', [ + ].concat( + Arr.map(formats.items, function (k) { + return makeItem(getValue(k), k.title, k.isSelected(), k.getPreview(), Objects.hasKey(formats.expansions, getValue(k))); + }) + ), memMenuThunk, false); + + var submenus = Obj.map(formats.menus, function (menuItems, menuName) { + var items = Arr.map(menuItems, function (item) { + return makeItem( + getValue(item), + item.title, + item.isSelected !== undefined ? item.isSelected() : false, + item.getPreview !== undefined ? item.getPreview() : '', + Objects.hasKey(formats.expansions, getValue(item)) + ); + }); + return makeMenu(menuName, items, memMenuThunk, true); + }); + + var menus = Merger.deepMerge(submenus, Objects.wrap('styles', mainMenu)); + + + var tmenu = TieredMenu.tieredData('styles', menus, formats.expansions); + + return { + tmenu: tmenu + }; + }; + + var makeItem = function (value, text, selected, preview, isMenu) { + return { + data: { + value: value, + text: text + }, + type: 'item', + dom: { + tag: 'div', + classes: isMenu ? [ Styles.resolve('styles-item-is-menu') ] : [ ] + }, + toggling: { + toggleOnExecute: false, + toggleClass: Styles.resolve('format-matches'), + selected: selected + }, + itemBehaviours: Behaviour.derive(isMenu ? [ ] : [ + Receivers.format(value, function (comp, status) { + var toggle = status ? Toggling.on : Toggling.off; + toggle(comp); + }) + ]), + components: [ + { + dom: { + tag: 'div', + attributes: { + style: preview + }, + innerHtml: text + } + } + ] + }; + }; + + var makeMenu = function (value, items, memMenuThunk, collapsable) { + return { + value: value, + dom: { + tag: 'div' + }, + components: [ + Button.sketch({ + dom: { + tag: 'div', + classes: [ Styles.resolve('styles-collapser') ] + }, + components: collapsable ? [ + { + dom: { + tag: 'span', + classes: [ Styles.resolve('styles-collapse-icon') ] + } + }, + GuiFactory.text(value) + ] : [ GuiFactory.text(value) ], + action: function (item) { + if (collapsable) { + var comp = memMenuThunk().get(item); + TieredMenu.collapseMenu(comp); + } + } + }), + { + dom: { + tag: 'div', + classes: [ Styles.resolve('styles-menu-items-container') ] + }, + components: [ + Menu.parts().items({ }) + ], + + behaviours: Behaviour.derive([ + AddEventsBehaviour.config('adhoc-scrollable-menu', [ + AlloyEvents.runOnAttached(function (component, simulatedEvent) { + Css.set(component.element(), 'overflow-y', 'auto'); + Css.set(component.element(), '-webkit-overflow-scrolling', 'touch'); + Scrollable.register(component.element()); + }), + + AlloyEvents.runOnDetached(function (component) { + Css.remove(component.element(), 'overflow-y'); + Css.remove(component.element(), '-webkit-overflow-scrolling'); + Scrollable.deregister(component.element()); + }) + ]) + ]) + } + ], + items: items, + menuBehaviours: Behaviour.derive([ + Transitioning.config({ + initialState: 'after', + routes: Transitioning.createTristate('before', 'current', 'after', { + transition: { + property: 'transform', + transitionClass: 'transitioning' + } + }) + }) + ]) + }; + }; + + var sketch = function (settings) { + var dataset = convert(settings.formats, function () { + return memMenu; + }); + // Turn settings into a tiered menu data. + + var memMenu = Memento.record(TieredMenu.sketch({ + dom: { + tag: 'div', + classes: [ Styles.resolve('styles-menu') ] + }, + components: [ ], + + // Focus causes issues when the things being focused are offscreen. + fakeFocus: true, + // For animations, need things to stay around in the DOM (at least until animation is done) + stayInDom: true, + + onExecute: function (tmenu, item) { + var v = Representing.getValue(item); + settings.handle(item, v.value); + }, + onEscape: function () { + }, + onOpenMenu: function (container, menu) { + var w = Width.get(container.element()); + Width.set(menu.element(), w); + Transitioning.jumpTo(menu, 'current'); + }, + onOpenSubmenu: function (container, item, submenu) { + var w = Width.get(container.element()); + var menu = SelectorFind.ancestor(item.element(), '[role="menu"]').getOrDie('hacky'); + var menuComp = container.getSystem().getByDom(menu).getOrDie(); + + Width.set(submenu.element(), w); + + Transitioning.progressTo(menuComp, 'before'); + Transitioning.jumpTo(submenu, 'after'); + Transitioning.progressTo(submenu, 'current'); + }, + + onCollapseMenu: function (container, item, menu) { + var submenu = SelectorFind.ancestor(item.element(), '[role="menu"]').getOrDie('hacky'); + var submenuComp = container.getSystem().getByDom(submenu).getOrDie(); + Transitioning.progressTo(submenuComp, 'after'); + Transitioning.progressTo(menu, 'current'); + }, + + navigateOnHover: false, + + openImmediately: true, + data: dataset.tmenu, + + markers: { + backgroundMenu: Styles.resolve('styles-background-menu'), + menu: Styles.resolve('styles-menu'), + selectedMenu: Styles.resolve('styles-selected-menu'), + item: Styles.resolve('styles-item'), + selectedItem: Styles.resolve('styles-selected-item') + } + })); + + return memMenu.asSpec(); + }; + + return { + sketch: sketch + }; + } +); + +define( + 'tinymce.themes.mobile.util.StyleConversions', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Merger' + ], + + function (Objects, Arr, Merger) { + + var getFromExpandingItem = function (item) { + var newItem = Merger.deepMerge( + Objects.exclude(item, [ 'items' ]), + { + menu: true + } + ); + + var rest = expand(item.items, item.title); + + var newMenus = Merger.deepMerge( + rest.menus, + Objects.wrap( + item.title, + rest.items + ) + ); + var newExpansions = Merger.deepMerge( + rest.expansions, + Objects.wrap(item.title, item.title) + ); + + return { + item: newItem, + menus: newMenus, + expansions: newExpansions + }; + }; + + var getFromItem = function (item) { + return Objects.hasKey(item, 'items') ? getFromExpandingItem(item) : { + item: item, + menus: { }, + expansions: { } + }; + }; + + + // Takes items, and consolidates them into its return value + var expand = function (items) { + return Arr.foldr(items, function (acc, item) { + var newData = getFromItem(item); + return { + menus: Merger.deepMerge(acc.menus, newData.menus), + items: [ newData.item ].concat(acc.items), + expansions: Merger.deepMerge(acc.expansions, newData.expansions) + }; + }, { + menus: { }, + expansions: { }, + items: [ ] + }); + }; + + return { + expand: expand + }; + } +); + +define( + 'tinymce.themes.mobile.util.StyleFormats', + + [ + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Id', + 'ephox.katamari.api.Merger', + 'tinymce.themes.mobile.features.DefaultStyleFormats', + 'tinymce.themes.mobile.ui.StylesMenu', + 'tinymce.themes.mobile.util.StyleConversions' + ], + + function (Toggling, Objects, Arr, Fun, Id, Merger, DefaultStyleFormats, StylesMenu, StyleConversions) { + var register = function (editor, settings) { + + var isSelectedFor = function (format) { + return function () { + return editor.formatter.match(format); + }; + }; + + var getPreview = function (format) { + return function () { + var styles = editor.formatter.getCssText(format); + return styles; + }; + }; + + var enrichSupported = function (item) { + return Merger.deepMerge(item, { + isSelected: isSelectedFor(item.format), + getPreview: getPreview(item.format) + }); + }; + + // Item that triggers a submenu + var enrichMenu = function (item) { + return Merger.deepMerge(item, { + isSelected: Fun.constant(false), + getPreview: Fun.constant('') + }); + }; + + var enrichCustom = function (item) { + var formatName = Id.generate(item.title); + var newItem = Merger.deepMerge(item, { + format: formatName, + isSelected: isSelectedFor(formatName), + getPreview: getPreview(formatName) + }); + editor.formatter.register(formatName, newItem); + return newItem; + }; + + var formats = Objects.readOptFrom(settings, 'style_formats').getOr(DefaultStyleFormats); + + var doEnrich = function (items) { + return Arr.map(items, function (item) { + if (Objects.hasKey(item, 'items')) { + var newItems = doEnrich(item.items); + return Merger.deepMerge( + enrichMenu(item), + { + items: newItems + } + ); + } else if (Objects.hasKey(item, 'format')) { + return enrichSupported(item); + } else { + return enrichCustom(item); + } + }); + }; + + return doEnrich(formats); + }; + + var prune = function (editor, formats) { + + var doPrune = function (items) { + return Arr.bind(items, function (item) { + if (item.items !== undefined) { + var newItems = doPrune(item.items); + return newItems.length > 0 ? [ item ] : [ ]; + } else { + var keep = Objects.hasKey(item, 'format') ? editor.formatter.canApply(item.format) : true; + return keep ? [ item ] : [ ]; + } + }); + }; + + var prunedItems = doPrune(formats); + return StyleConversions.expand(prunedItems); + }; + + + var ui = function (editor, formats, onDone) { + var pruned = prune(editor, formats); + + return StylesMenu.sketch({ + formats: pruned, + handle: function (item, value) { + editor.undoManager.transact(function () { + if (Toggling.isOn(item)) { + editor.formatter.remove(value); + } else { + editor.formatter.apply(value); + } + }); + onDone(); + } + }); + }; + + return { + register: register, + ui: ui + }; + } +); +define( + 'tinymce.themes.mobile.features.Features', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Receiving', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.component.Memento', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Type', + 'global!setTimeout', + 'global!window', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.channels.TinyChannels', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.ui.Buttons', + 'tinymce.themes.mobile.ui.ColorSlider', + 'tinymce.themes.mobile.ui.FontSizeSlider', + 'tinymce.themes.mobile.ui.ImagePicker', + 'tinymce.themes.mobile.ui.LinkButton', + 'tinymce.themes.mobile.util.StyleFormats' + ], + + function ( + Behaviour, Receiving, Toggling, Memento, Objects, Arr, Fun, Option, Type, setTimeout, window, Receivers, TinyChannels, Styles, Buttons, ColorSlider, FontSizeSlider, + ImagePicker, LinkButton, StyleFormats + ) { + var defaults = [ 'undo', 'bold', 'italic', 'link', 'image', 'bullist', 'styleselect' ]; + + var extract = function (rawToolbar) { + // Ignoring groups + var toolbar = rawToolbar.replace(/\|/g, ' ').trim(); + return toolbar.length > 0 ? toolbar.split(/\s+/) : [ ]; + }; + + var identifyFromArray = function (toolbar) { + return Arr.bind(toolbar, function (item) { + return Type.isArray(item) ? identifyFromArray(item) : extract(item); + }); + }; + + var identify = function (settings) { + // Firstly, flatten the toolbar + var toolbar = settings.toolbar !== undefined ? settings.toolbar : defaults; + return Type.isArray(toolbar) ? identifyFromArray(toolbar) : extract(toolbar); + }; + + var setup = function (realm, editor) { + var commandSketch = function (name) { + return function () { + return Buttons.forToolbarCommand(editor, name); + }; + }; + + var stateCommandSketch = function (name) { + return function () { + return Buttons.forToolbarStateCommand(editor, name); + }; + }; + + var actionSketch = function (name, query, action) { + return function () { + return Buttons.forToolbarStateAction(editor, name, query, action); + }; + }; + + var undo = commandSketch('undo'); + var redo = commandSketch('redo'); + var bold = stateCommandSketch('bold'); + var italic = stateCommandSketch('italic'); + var underline = stateCommandSketch('underline'); + var removeformat = commandSketch('removeformat'); + + var link = function () { + return LinkButton.sketch(realm, editor); + }; + + var unlink = actionSketch('unlink', 'link', function () { + editor.execCommand('unlink', null, false); + }); + var image = function () { + return ImagePicker.sketch(editor); + }; + + var bullist = actionSketch('unordered-list', 'ul', function () { + editor.execCommand('InsertUnorderedList', null, false); + }); + + var numlist = actionSketch('ordered-list', 'ol', function () { + editor.execCommand('InsertOrderedList', null, false); + }); + + var fontsizeselect = function () { + return FontSizeSlider.sketch(realm, editor); + }; + + var forecolor = function () { + return ColorSlider.sketch(realm, editor); + }; + + var styleFormats = StyleFormats.register(editor, editor.settings); + + var styleFormatsMenu = function () { + return StyleFormats.ui(editor, styleFormats, function () { + editor.fire('scrollIntoView'); + }); + }; + + var styleselect = function () { + return Buttons.forToolbar('style-formats', function (button) { + editor.fire('toReading'); + realm.dropup().appear(styleFormatsMenu, Toggling.on, button); + }, Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('toolbar-button-selected'), + toggleOnExecute: false, + aria: { + mode: 'pressed' + } + }), + Receiving.config({ + channels: Objects.wrapAll([ + Receivers.receive(TinyChannels.orientationChanged(), Toggling.off), + Receivers.receive(TinyChannels.dropupDismissed(), Toggling.off) + ]) + }) + ])); + }; + + var feature = function (prereq, sketch) { + return { + isSupported: function () { + // NOTE: forall is true for none + return prereq.forall(function (p) { + return Objects.hasKey(editor.buttons, p); + }); + }, + sketch: sketch + }; + }; + + return { + undo: feature(Option.none(), undo), + redo: feature(Option.none(), redo), + bold: feature(Option.none(), bold), + italic: feature(Option.none(), italic), + underline: feature(Option.none(), underline), + removeformat: feature(Option.none(), removeformat), + link: feature(Option.none(), link), + unlink: feature(Option.none(), unlink), + image: feature(Option.none(), image), + // NOTE: Requires "lists" plugin. + bullist: feature(Option.some('bullist'), bullist), + numlist: feature(Option.some('numlist'), numlist), + fontsizeselect: feature(Option.none(), fontsizeselect), + forecolor: feature(Option.none(), forecolor), + styleselect: feature(Option.none(), styleselect) + }; + }; + + var detect = function (settings, features) { + // Firstly, work out which items are in the toolbar + var itemNames = identify(settings); + + // Now, build the list only including supported features and no duplicates. + var present = { }; + return Arr.bind(itemNames, function (iName) { + var r = !Objects.hasKey(present, iName) && Objects.hasKey(features, iName) && features[iName].isSupported() ? [ features[iName].sketch() ] : []; + // NOTE: Could use fold to avoid mutation, but it might be overkill and not performant + present[iName] = true; + return r; + }); + }; + + return { + identify: identify, + setup: setup, + detect: detect + }; + } +); + +define( + 'ephox.sugar.impl.FilteredEvent', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Element' + ], + + function (Fun, Element) { + + var mkEvent = function (target, x, y, stop, prevent, kill, raw) { + // switched from a struct to manual Fun.constant() because we are passing functions now, not just values + return { + 'target': Fun.constant(target), + 'x': Fun.constant(x), + 'y': Fun.constant(y), + 'stop': stop, + 'prevent': prevent, + 'kill': kill, + 'raw': Fun.constant(raw) + }; + }; + + var handle = function (filter, handler) { + return function (rawEvent) { + if (!filter(rawEvent)) return; + + // IE9 minimum + var target = Element.fromDom(rawEvent.target); + + var stop = function () { + rawEvent.stopPropagation(); + }; + + var prevent = function () { + rawEvent.preventDefault(); + }; + + var kill = Fun.compose(prevent, stop); // more of a sequence than a compose, but same effect + + // FIX: Don't just expose the raw event. Need to identify what needs standardisation. + var evt = mkEvent(target, rawEvent.clientX, rawEvent.clientY, stop, prevent, kill, rawEvent); + handler(evt); + }; + }; + + var binder = function (element, event, filter, handler, useCapture) { + var wrapped = handle(filter, handler); + // IE9 minimum + element.dom().addEventListener(event, wrapped, useCapture); + + return { + unbind: Fun.curry(unbind, element, event, wrapped, useCapture) + }; + }; + + var bind = function (element, event, filter, handler) { + return binder(element, event, filter, handler, false); + }; + + var capture = function (element, event, filter, handler) { + return binder(element, event, filter, handler, true); + }; + + var unbind = function (element, event, handler, useCapture) { + // IE9 minimum + element.dom().removeEventListener(event, handler, useCapture); + }; + + return { + bind: bind, + capture: capture + }; + } +); +define( + 'ephox.sugar.api.events.DomEvent', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.impl.FilteredEvent' + ], + + function (Fun, FilteredEvent) { + var filter = Fun.constant(true); // no filter on plain DomEvents + + var bind = function (element, event, handler) { + return FilteredEvent.bind(element, event, filter, handler); + }; + + var capture = function (element, event, handler) { + return FilteredEvent.capture(element, event, filter, handler); + }; + + return { + bind: bind, + capture: capture + }; + } +); + +defineGlobal("global!clearInterval", clearInterval); +defineGlobal("global!setInterval", setInterval); +define( + 'tinymce.themes.mobile.touch.view.Orientation', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Element', + 'global!clearInterval', + 'global!Math', + 'global!setInterval' + ], + + function (Fun, Option, PlatformDetection, DomEvent, Element, clearInterval, Math, setInterval) { + var INTERVAL = 50; + var INSURANCE = 1000 / INTERVAL; + + var get = function (outerWindow) { + // We need to use this because the window shrinks due to an app keyboard, + // width > height is no longer reliable. + var isPortrait = outerWindow.matchMedia('(orientation: portrait)').matches; + return { + isPortrait: Fun.constant(isPortrait) + }; + }; + + + // In iOS the width of the window is not swapped properly when the device is + // rotated causing problems. + // getActualWidth will return the actual width of the window accurated with the + // orientation of the device. + var getActualWidth = function (outerWindow) { + var isIos = PlatformDetection.detect().os.isiOS(); + var isPortrait = get(outerWindow).isPortrait(); + return isIos && !isPortrait ? outerWindow.screen.height : outerWindow.screen.width; + }; + + var onChange = function (outerWindow, listeners) { + var win = Element.fromDom(outerWindow); + var poller = null; + + var change = function () { + // If a developer is spamming orientation events in the simulator, clear our last check + clearInterval(poller); + + var orientation = get(outerWindow); + listeners.onChange(orientation); + + onAdjustment(function () { + // We don't care about whether there was a resize or not. + listeners.onReady(orientation); + }); + }; + + var orientationHandle = DomEvent.bind(win, 'orientationchange', change); + + var onAdjustment = function (f) { + // If a developer is spamming orientation events in the simulator, clear our last check + clearInterval(poller); + + var flag = outerWindow.innerHeight; + var insurance = 0; + poller = setInterval(function () { + if (flag !== outerWindow.innerHeight) { + clearInterval(poller); + f(Option.some(outerWindow.innerHeight)); + } else if (insurance > INSURANCE) { + clearInterval(poller); + f(Option.none()); + } + insurance++; + }, INTERVAL); + }; + + var destroy = function () { + orientationHandle.unbind(); + }; + + return { + onAdjustment: onAdjustment, + destroy: destroy + }; + }; + + return { + get: get, + onChange: onChange, + getActualWidth: getActualWidth + }; + } +); +defineGlobal("global!clearTimeout", clearTimeout); +define( + 'ephox.alloy.alien.DelayedFunction', + + [ + 'global!clearTimeout', + 'global!setTimeout' + ], + + function (clearTimeout, setTimeout) { + return function (fun, delay) { + var ref = null; + + var schedule = function () { + var args = arguments; + ref = setTimeout(function () { + fun.apply(null, args); + ref = null; + }, delay); + }; + + var cancel = function () { + if (ref !== null) { + clearTimeout(ref); + ref = null; + } + }; + + return { + cancel: cancel, + schedule: schedule + }; + }; + } +); + +define( + 'ephox.alloy.events.TapEvent', + + [ + 'ephox.alloy.alien.DelayedFunction', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'global!Math' + ], + + function (DelayedFunction, NativeEvents, SystemEvents, Objects, Cell, Fun, Option, Compare, Math) { + var SIGNIFICANT_MOVE = 5; + + var LONGPRESS_DELAY = 400; + + var getTouch = function (event) { + if (event.raw().touches === undefined || event.raw().touches.length !== 1) return Option.none(); + return Option.some(event.raw().touches[0]); + }; + + // Check to see if the touch has changed a *significant* amount + var isFarEnough = function (touch, data) { + var distX = Math.abs(touch.clientX - data.x()); + var distY = Math.abs(touch.clientY - data.y()); + return distX > SIGNIFICANT_MOVE || distY > SIGNIFICANT_MOVE; + }; + + var monitor = function (settings) { + /* A tap event is a combination of touchstart and touchend on the same element + * without a *significant* touchmove in between. + */ + + // Need a return value, so can't use Singleton.value; + var startData = Cell(Option.none()); + + var longpress = DelayedFunction(function (event) { + // Stop longpress firing a tap + startData.set(Option.none()); + settings.triggerEvent(SystemEvents.longpress(), event); + }, LONGPRESS_DELAY); + + var handleTouchstart = function (event) { + getTouch(event).each(function (touch) { + longpress.cancel(); + + var data = { + x: Fun.constant(touch.clientX), + y: Fun.constant(touch.clientY), + target: event.target + }; + + longpress.schedule(data); + startData.set(Option.some(data)); + }); + return Option.none(); + }; + + var handleTouchmove = function (event) { + longpress.cancel(); + getTouch(event).each(function (touch) { + startData.get().each(function (data) { + if (isFarEnough(touch, data)) startData.set(Option.none()); + }); + }); + return Option.none(); + }; + + var handleTouchend = function (event) { + longpress.cancel(); + + var isSame = function (data) { + return Compare.eq(data.target(), event.target()); + }; + + return startData.get().filter(isSame).map(function (data) { + return settings.triggerEvent(SystemEvents.tap(), event); + }); + }; + + var handlers = Objects.wrapAll([ + { key: NativeEvents.touchstart(), value: handleTouchstart }, + { key: NativeEvents.touchmove(), value: handleTouchmove }, + { key: NativeEvents.touchend(), value: handleTouchend } + ]); + + var fireIfReady = function (event, type) { + return Objects.readOptFrom(handlers, type).bind(function (handler) { + return handler(event); + }); + }; + + return { + fireIfReady: fireIfReady + }; + }; + + return { + monitor: monitor + }; + } +); + +define( + 'tinymce.themes.mobile.util.TappingEvent', + + [ + 'ephox.alloy.events.TapEvent', + 'ephox.sugar.api.events.DomEvent' + ], + + function (TapEvent, DomEvent) { + // TODO: TapEvent needs to be exposed in alloy's API somehow + var monitor = function (editorApi) { + var tapEvent = TapEvent.monitor({ + triggerEvent: function (type, evt) { + editorApi.onTapContent(evt); + } + }); + + // convenience methods + var onTouchend = function () { + return DomEvent.bind(editorApi.body(), 'touchend', function (evt) { + tapEvent.fireIfReady(evt, 'touchend'); + }); + }; + + var onTouchmove = function () { + return DomEvent.bind(editorApi.body(), 'touchmove', function (evt) { + tapEvent.fireIfReady(evt, 'touchmove'); + }); + }; + + var fireTouchstart = function (evt) { + tapEvent.fireIfReady(evt, 'touchstart'); + }; + + return { + fireTouchstart: fireTouchstart, + onTouchend: onTouchend, + onTouchmove: onTouchmove + }; + }; + + return { + monitor: monitor + }; + } +); + +define( + 'tinymce.themes.mobile.android.core.AndroidEvents', + + [ + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.Traverse', + 'tinymce.themes.mobile.util.TappingEvent' + ], + + function (Toggling, Arr, Fun, PlatformDetection, Compare, Focus, DomEvent, Element, Node, Traverse, TappingEvent) { + + var isAndroid6 = PlatformDetection.detect().os.version.major >= 6; + /* + + `selectionchange` on the iframe document. If the selection is *ranged*, then we add the margin, because we + assume that the context menu has appeared. If it is collapsed, then the context menu shouldn't appear + (there is no selected text to format), so we reset the margin to `0px`. Note, when adding a margin, + we add `23px` --- this is most likely based on trial and error. We may need to work out how to get + this value properly. + + 2. `select` on the outer document. This will also need to add the margin if the selection is ranged within + an input or textarea + + */ + var initEvents = function (editorApi, toolstrip, alloy) { + + var tapping = TappingEvent.monitor(editorApi); + var outerDoc = Traverse.owner(toolstrip); + + var isRanged = function (sel) { + return !Compare.eq(sel.start(), sel.finish()) || sel.soffset() !== sel.foffset(); + }; + + var hasRangeInUi = function () { + return Focus.active(outerDoc).filter(function (input) { + return Node.name(input) === 'input'; + }).exists(function (input) { + return input.dom().selectionStart !== input.dom().selectionEnd; + }); + }; + + var updateMargin = function () { + var rangeInContent = editorApi.doc().dom().hasFocus() && editorApi.getSelection().exists(isRanged); + alloy.getByDom(toolstrip).each((rangeInContent || hasRangeInUi()) === true ? Toggling.on : Toggling.off); + }; + + var listeners = [ + DomEvent.bind(editorApi.body(), 'touchstart', function (evt) { + editorApi.onTouchContent(); + tapping.fireTouchstart(evt); + }), + tapping.onTouchmove(), + tapping.onTouchend(), + + DomEvent.bind(toolstrip, 'touchstart', function (evt) { + editorApi.onTouchToolstrip(); + }), + + editorApi.onToReading(function () { + Focus.blur(editorApi.body()); + }), + editorApi.onToEditing(Fun.noop), + + // Scroll to cursor and update the iframe height + editorApi.onScrollToCursor(function (tinyEvent) { + tinyEvent.preventDefault(); + editorApi.getCursorBox().each(function (bounds) { + var cWin = editorApi.win(); + // The goal here is to shift as little as required. + var isOutside = bounds.top() > cWin.innerHeight || bounds.bottom() > cWin.innerHeight; + var cScrollBy = isOutside ? bounds.bottom() - cWin.innerHeight + 50/*EXTRA_SPACING*/ : 0; + if (cScrollBy !== 0) { + cWin.scrollTo(cWin.pageXOffset, cWin.pageYOffset + cScrollBy); + } + }); + }) + ].concat( + isAndroid6 === true ? [ ] : [ + DomEvent.bind(Element.fromDom(editorApi.win()), 'blur', function () { + alloy.getByDom(toolstrip).each(Toggling.off); + }), + DomEvent.bind(outerDoc, 'select', updateMargin), + DomEvent.bind(editorApi.doc(), 'selectionchange', updateMargin) + ] + ); + + var destroy = function () { + Arr.each(listeners, function (l) { + l.unbind(); + }); + }; + + return { + destroy: destroy + }; + }; + + return { + initEvents: initEvents + }; + } +); + +define( + 'tinymce.themes.mobile.android.focus.ResumeEditing', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'global!setTimeout' + ], + + function (Arr, Fun, Focus, Element, Node, setTimeout) { + // There are numerous problems with Google Keyboard when we need to switch focus back from a toolbar item / dialog to + // the content for editing. The major problem is to do with autocomplete. Android Google Keyboard (not Swift) seems to + // remember the things that you've typed into the input, and then adds them to whatever you type in the content once you give it + // focus, as long as the keyboard has stayed up. + + // We tried turning autocomplete off, and then turning it back on again with a setTimeout, and although it fixed the problem, + // the autcomplete on didn't start working immediately. Maurizio also pointed out that it was probably keyboard specific + // (and he was right) autocomplete off, and then turn it back on in an attempt to stop this happening. It works, but the + // autocomplete on part takes a while to start working again. + + // Then we tried everyone's favourite setTimeout solution. This appears to work because it looks like the bug might + // be caused by the fact that the autocomplete cache is maintained while in the same event queue. As soon as we + // disconnect the stack, it looks like it is fixed. That makes some level of sense. + var autocompleteHack = function (/* iBody */) { + return function (f) { + setTimeout(function () { + f(); + }, 0); + }; + }; + + var resume = function (cWin) { + cWin.focus(); + var iBody = Element.fromDom(cWin.document.body); + + var inInput = Focus.active().exists(function (elem) { + return Arr.contains([ 'input', 'textarea' ], Node.name(elem)); + }); + + var transaction = inInput ? autocompleteHack(iBody) : Fun.apply; + + transaction(function () { + // If we don't blur before focusing the content, a previous focus in something like a statebutton + // which represents the chosen font colour can stop the keyboard from appearing. Therefore, we blur + // first. + Focus.active().each(Focus.blur); + Focus.focus(iBody); + }); + }; + + return { + resume: resume + }; + } +); + +defineGlobal("global!isNaN", isNaN); +defineGlobal("global!parseInt", parseInt); +define( + 'tinymce.themes.mobile.util.DataAttributes', + + [ + 'ephox.sugar.api.properties.Attr', + 'global!isNaN', + 'global!parseInt' + ], + + function (Attr, isNaN, parseInt) { + var safeParse = function (element, attribute) { + var parsed = parseInt(Attr.get(element, attribute), 10); + return isNaN(parsed) ? 0 : parsed; + }; + + return { + safeParse: safeParse + }; + } +); +define( + 'ephox.sugar.impl.NodeValue', + + [ + 'ephox.sand.api.PlatformDetection', + 'ephox.katamari.api.Option', + 'global!Error' + ], + + 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 { + get: get, + getOption: getOption, + set: set + }; + } +); + +define( + 'ephox.sugar.api.selection.Awareness', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.node.Text', + 'ephox.sugar.api.search.Traverse' + ], + + function (Arr, Node, Text, Traverse) { + var getEnd = function (element) { + return Node.name(element) === 'img' ? 1 : Text.getOption(element).fold(function () { + return Traverse.children(element).length; + }, function (v) { + return v.length; + }); + }; + + var isEnd = function (element, offset) { + return getEnd(element) === offset; + }; + + var isStart = function (element, offset) { + return offset === 0; + }; + + var NBSP = '\u00A0'; + + var isTextNodeWithCursorPosition = function (el) { + return Text.getOption(el).filter(function (text) { + // For the purposes of finding cursor positions only allow text nodes with content, + // but trim removes   and that's allowed + return text.trim().length !== 0 || text.indexOf(NBSP) > -1; + }).isSome(); + }; + + var elementsWithCursorPosition = [ 'img', 'br' ]; + var isCursorPosition = function (elem) { + var hasCursorPosition = isTextNodeWithCursorPosition(elem); + return hasCursorPosition || Arr.contains(elementsWithCursorPosition, Node.name(elem)); + }; + + return { + getEnd: getEnd, + isEnd: isEnd, + isStart: isStart, + isCursorPosition: isCursorPosition + }; + } +); + +define( + 'ephox.sugar.api.selection.Selection', + + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Struct' + ], + + function (Adt, Struct) { + // Consider adding a type for "element" + var type = Adt.generate([ + { domRange: [ 'rng' ] }, + { relative: [ 'startSitu', 'finishSitu' ] }, + { exact: [ 'start', 'soffset', 'finish', 'foffset' ] } + ]); + + var range = Struct.immutable( + 'start', + 'soffset', + 'finish', + 'foffset' + ); + + var exactFromRange = function (simRange) { + return type.exact(simRange.start(), simRange.soffset(), simRange.finish(), simRange.foffset()); + }; + + return { + domRange: type.domRange, + relative: type.relative, + exact: type.exact, + + exactFromRange: exactFromRange, + range: range + }; + } +); + +define( + 'ephox.sugar.api.dom.DocumentPosition', + + [ + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse' + ], + + function (Compare, Element, Traverse) { + var makeRange = function (start, soffset, finish, foffset) { + var doc = Traverse.owner(start); + + // TODO: We need to think about a better place to put native range creation code. Does it even belong in sugar? + // Could the `Compare` checks (node.compareDocumentPosition) handle these situations better? + var rng = doc.dom().createRange(); + rng.setStart(start.dom(), soffset); + rng.setEnd(finish.dom(), foffset); + return rng; + }; + + // Return the deepest - or furthest down the document tree - Node that contains both boundary points + // of the range (start:soffset, finish:foffset). + var commonAncestorContainer = function (start, soffset, finish, foffset) { + var r = makeRange(start, soffset, finish, foffset); + return Element.fromDom(r.commonAncestorContainer); + }; + + var after = function (start, soffset, finish, foffset) { + var r = makeRange(start, soffset, finish, foffset); + + var same = Compare.eq(start, finish) && soffset === foffset; + return r.collapsed && !same; + }; + + return { + after: after, + commonAncestorContainer: commonAncestorContainer + }; + } +); +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.api.selection.Situ', + + [ + 'ephox.katamari.api.Adt' + ], + + function (Adt) { + var adt = Adt.generate([ + { 'before': [ 'element' ] }, + { 'on': [ 'element', 'offset' ] }, + { after: [ 'element' ] } + ]); + + // Probably don't need this given that we now have "match" + var cata = function (subject, onBefore, onOn, onAfter) { + return subject.fold(onBefore, onOn, onAfter); + }; + + return { + before: adt.before, + on: adt.on, + after: adt.after, + cata: cata + }; + } +); + +define( + 'ephox.sugar.selection.core.NativeRange', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element' + ], + + function (Fun, Option, Compare, Element) { + var selectNodeContents = function (win, element) { + var rng = win.document.createRange(); + selectNodeContentsUsing(rng, element); + return rng; + }; + + var selectNodeContentsUsing = function (rng, element) { + rng.selectNodeContents(element.dom()); + }; + + var isWithin = function (outerRange, innerRange) { + // Adapted from: http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element + return innerRange.compareBoundaryPoints(outerRange.END_TO_START, outerRange) < 1 && + innerRange.compareBoundaryPoints(outerRange.START_TO_END, outerRange) > -1; + }; + + var create = function (win) { + return win.document.createRange(); + }; + + // NOTE: Mutates the range. + var setStart = function (rng, situ) { + situ.fold(function (e) { + rng.setStartBefore(e.dom()); + }, function (e, o) { + rng.setStart(e.dom(), o); + }, function (e) { + rng.setStartAfter(e.dom()); + }); + }; + + var setFinish = function (rng, situ) { + situ.fold(function (e) { + rng.setEndBefore(e.dom()); + }, function (e, o) { + rng.setEnd(e.dom(), o); + }, function (e) { + rng.setEndAfter(e.dom()); + }); + }; + + var replaceWith = function (rng, fragment) { + // Note: this document fragment approach may not work on IE9. + deleteContents(rng); + rng.insertNode(fragment.dom()); + }; + + var isCollapsed = function (start, soffset, finish, foffset) { + return Compare.eq(start, finish) && soffset === foffset; + }; + + var relativeToNative = function (win, startSitu, finishSitu) { + var range = win.document.createRange(); + setStart(range, startSitu); + setFinish(range, finishSitu); + return range; + }; + + var exactToNative = function (win, start, soffset, finish, foffset) { + var rng = win.document.createRange(); + rng.setStart(start.dom(), soffset); + rng.setEnd(finish.dom(), foffset); + return rng; + }; + + var deleteContents = function (rng) { + rng.deleteContents(); + }; + + var cloneFragment = function (rng) { + var fragment = rng.cloneContents(); + return Element.fromDom(fragment); + }; + + var toRect = function (rect) { + return { + left: Fun.constant(rect.left), + top: Fun.constant(rect.top), + right: Fun.constant(rect.right), + bottom: Fun.constant(rect.bottom), + width: Fun.constant(rect.width), + height: Fun.constant(rect.height) + }; + }; + + var getFirstRect = function (rng) { + var rects = rng.getClientRects(); + // ASSUMPTION: The first rectangle is the start of the selection + var rect = rects.length > 0 ? rects[0] : rng.getBoundingClientRect(); + return rect.width > 0 || rect.height > 0 ? Option.some(rect).map(toRect) : Option.none(); + }; + + var getBounds = function (rng) { + var rect = rng.getBoundingClientRect(); + return rect.width > 0 || rect.height > 0 ? Option.some(rect).map(toRect) : Option.none(); + }; + + var toString = function (rng) { + return rng.toString(); + }; + + return { + create: create, + replaceWith: replaceWith, + selectNodeContents: selectNodeContents, + selectNodeContentsUsing: selectNodeContentsUsing, + isCollapsed: isCollapsed, + relativeToNative: relativeToNative, + exactToNative: exactToNative, + deleteContents: deleteContents, + cloneFragment: cloneFragment, + getFirstRect: getFirstRect, + getBounds: getBounds, + isWithin: isWithin, + toString: toString + }; + } +); + +define( + 'ephox.sugar.selection.core.SelectionDirection', + + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Thunk', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.selection.core.NativeRange' + ], + + function (Adt, Fun, Option, Thunk, Element, NativeRange) { + var adt = Adt.generate([ + { ltr: [ 'start', 'soffset', 'finish', 'foffset' ] }, + { rtl: [ 'start', 'soffset', 'finish', 'foffset' ] } + ]); + + var fromRange = function (win, type, range) { + return type(Element.fromDom(range.startContainer), range.startOffset, Element.fromDom(range.endContainer), range.endOffset); + }; + + var getRanges = function (win, selection) { + return selection.match({ + domRange: function (rng) { + return { + ltr: Fun.constant(rng), + rtl: Option.none + }; + }, + relative: function (startSitu, finishSitu) { + return { + ltr: Thunk.cached(function () { + return NativeRange.relativeToNative(win, startSitu, finishSitu); + }), + rtl: Thunk.cached(function () { + return Option.some( + NativeRange.relativeToNative(win, finishSitu, startSitu) + ); + }) + }; + }, + exact: function (start, soffset, finish, foffset) { + return { + ltr: Thunk.cached(function () { + return NativeRange.exactToNative(win, start, soffset, finish, foffset); + }), + rtl: Thunk.cached(function () { + return Option.some( + NativeRange.exactToNative(win, finish, foffset, start, soffset) + ); + }) + }; + } + }); + }; + + var doDiagnose = function (win, ranges) { + // If we cannot create a ranged selection from start > finish, it could be RTL + var rng = ranges.ltr(); + if (rng.collapsed) { + // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed + var reversed = ranges.rtl().filter(function (rev) { + return rev.collapsed === false; + }); + + return reversed.map(function (rev) { + // We need to use "reversed" here, because the original only has one point (collapsed) + return adt.rtl( + Element.fromDom(rev.endContainer), rev.endOffset, + Element.fromDom(rev.startContainer), rev.startOffset + ); + }).getOrThunk(function () { + return fromRange(win, adt.ltr, rng); + }); + } else { + return fromRange(win, adt.ltr, rng); + } + }; + + var diagnose = function (win, selection) { + var ranges = getRanges(win, selection); + return doDiagnose(win, ranges); + }; + + var asLtrRange = function (win, selection) { + var diagnosis = diagnose(win, selection); + return diagnosis.match({ + ltr: function (start, soffset, finish, foffset) { + var rng = win.document.createRange(); + rng.setStart(start.dom(), soffset); + rng.setEnd(finish.dom(), foffset); + return rng; + }, + rtl: function (start, soffset, finish, foffset) { + // NOTE: Reversing start and finish + var rng = win.document.createRange(); + rng.setStart(finish.dom(), foffset); + rng.setEnd(start.dom(), soffset); + return rng; + } + }); + }; + + return { + ltr: adt.ltr, + rtl: adt.rtl, + diagnose: diagnose, + asLtrRange: asLtrRange + }; + } +); + +define( + 'ephox.sugar.selection.alien.Geometry', + + [ + 'global!Math' + ], + + function (Math) { + var searchForPoint = function (rectForOffset, x, y, maxX, length) { + // easy cases + if (length === 0) return 0; + else if (x === maxX) return length - 1; + + var xDelta = maxX; + + // start at 1, zero is the fallback + for (var i = 1; i < length; i++) { + var rect = rectForOffset(i); + var curDeltaX = Math.abs(x - rect.left); + + if (y > rect.bottom) { + // range is too high, above drop point, do nothing + } else if (y < rect.top || curDeltaX > xDelta) { + // if the search winds up on the line below the drop point, + // or we pass the best X offset, + // wind back to the previous (best) delta + return i - 1; + } else { + // update current search delta + xDelta = curDeltaX; + } + } + return 0; // always return something, even if it's not the exact offset it'll be better than nothing + }; + + var inRect = function (rect, x, y) { + return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; + }; + + return { + inRect: inRect, + searchForPoint: searchForPoint + }; + + } +); + +define( + 'ephox.sugar.selection.query.TextPoint', + + [ + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.node.Text', + 'ephox.sugar.selection.alien.Geometry', + 'global!Math' + ], + + function (Option, Options, Text, Geometry, Math) { + var locateOffset = function (doc, textnode, x, y, rect) { + var rangeForOffset = function (offset) { + var r = doc.dom().createRange(); + r.setStart(textnode.dom(), offset); + r.collapse(true); + return r; + }; + + var rectForOffset = function (offset) { + var r = rangeForOffset(offset); + return r.getBoundingClientRect(); + }; + + var length = Text.get(textnode).length; + var offset = Geometry.searchForPoint(rectForOffset, x, y, rect.right, length); + return rangeForOffset(offset); + }; + + var locate = function (doc, node, x, y) { + var r = doc.dom().createRange(); + r.selectNode(node.dom()); + var rects = r.getClientRects(); + var foundRect = Options.findMap(rects, function (rect) { + return Geometry.inRect(rect, x, y) ? Option.some(rect) : Option.none(); + }); + + return foundRect.map(function (rect) { + return locateOffset(doc, node, x, y, rect); + }); + }; + + return { + locate: locate + }; + } +); + +define( + 'ephox.sugar.selection.query.ContainerPoint', + + [ + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.selection.alien.Geometry', + 'ephox.sugar.selection.query.TextPoint', + 'global!Math' + ], + + function (Option, Options, Node, Traverse, Geometry, TextPoint, Math) { + /** + * Future idea: + * + * This code requires the drop point to be contained within the nodes array somewhere. If it isn't, + * we fall back to the extreme start or end of the node array contents. + * This isn't really what the user intended. + * + * In theory, we could just find the range point closest to the boxes representing the node + * (repartee does something similar). + */ + + var searchInChildren = function (doc, node, x, y) { + var r = doc.dom().createRange(); + var nodes = Traverse.children(node); + return Options.findMap(nodes, function (n) { + // slight mutation because we assume creating ranges is expensive + r.selectNode(n.dom()); + return Geometry.inRect(r.getBoundingClientRect(), x, y) ? + locateNode(doc, n, x, y) : + Option.none(); + }); + }; + + var locateNode = function (doc, node, x, y) { + var locator = Node.isText(node) ? TextPoint.locate : searchInChildren; + return locator(doc, node, x, y); + }; + + var locate = function (doc, node, x, y) { + var r = doc.dom().createRange(); + r.selectNode(node.dom()); + var rect = r.getBoundingClientRect(); + // Clamp x,y at the bounds of the node so that the locate function has SOME chance + var boundedX = Math.max(rect.left, Math.min(rect.right, x)); + var boundedY = Math.max(rect.top, Math.min(rect.bottom, y)); + + return locateNode(doc, node, boundedX, boundedY); + }; + + return { + locate: locate + }; + } +); + +define( + 'ephox.sugar.api.selection.CursorPosition', + + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.search.PredicateFind', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.Awareness' + ], + + function (Option, PredicateFind, Traverse, Awareness) { + var first = function (element) { + return PredicateFind.descendant(element, Awareness.isCursorPosition); + }; + + var last = function (element) { + return descendantRtl(element, Awareness.isCursorPosition); + }; + + // Note, sugar probably needs some RTL traversals. + var descendantRtl = function (scope, predicate) { + var descend = function (element) { + var children = Traverse.children(element); + for (var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if (predicate(child)) return Option.some(child); + var res = descend(child); + if (res.isSome()) return res; + } + + return Option.none(); + }; + + return descend(scope); + }; + + return { + first: first, + last: last + }; + } +); + +define( + 'ephox.sugar.selection.query.EdgePoint', + + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.CursorPosition' + ], + + function (Option, Traverse, CursorPosition) { + /* + * When a node has children, we return either the first or the last cursor + * position, whichever is closer horizontally + * + * When a node has no children, we return the start of end of the element, + * depending on which is closer horizontally + * */ + + // TODO: Make this RTL compatible + var COLLAPSE_TO_LEFT = true; + var COLLAPSE_TO_RIGHT = false; + + var getCollapseDirection = function (rect, x) { + return x - rect.left < rect.right - x ? COLLAPSE_TO_LEFT : COLLAPSE_TO_RIGHT; + }; + + var createCollapsedNode = function (doc, target, collapseDirection) { + var r = doc.dom().createRange(); + r.selectNode(target.dom()); + r.collapse(collapseDirection); + return r; + }; + + var locateInElement = function (doc, node, x) { + var cursorRange = doc.dom().createRange(); + cursorRange.selectNode(node.dom()); + var rect = cursorRange.getBoundingClientRect(); + var collapseDirection = getCollapseDirection(rect, x); + + var f = collapseDirection === COLLAPSE_TO_LEFT ? CursorPosition.first : CursorPosition.last; + return f(node).map(function (target) { + return createCollapsedNode(doc, target, collapseDirection); + }); + }; + + var locateInEmpty = function (doc, node, x) { + var rect = node.dom().getBoundingClientRect(); + var collapseDirection = getCollapseDirection(rect, x); + return Option.some(createCollapsedNode(doc, node, collapseDirection)); + }; + + var search = function (doc, node, x) { + var f = Traverse.children(node).length === 0 ? locateInEmpty : locateInElement; + return f(doc, node, x); + }; + + return { + search: search + }; + } +); + +define( + 'ephox.sugar.selection.query.CaretRange', + + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.selection.query.ContainerPoint', + 'ephox.sugar.selection.query.EdgePoint', + 'global!document', + 'global!Math' + ], + + function (Option, Element, Traverse, Selection, ContainerPoint, EdgePoint, document, Math) { + var caretPositionFromPoint = function (doc, x, y) { + return Option.from(doc.dom().caretPositionFromPoint(x, y)).bind(function (pos) { + // It turns out that Firefox can return null for pos.offsetNode + if (pos.offsetNode === null) return Option.none(); + var r = doc.dom().createRange(); + r.setStart(pos.offsetNode, pos.offset); + r.collapse(); + return Option.some(r); + }); + }; + + var caretRangeFromPoint = function (doc, x, y) { + return Option.from(doc.dom().caretRangeFromPoint(x, y)); + }; + + var searchTextNodes = function (doc, node, x, y) { + var r = doc.dom().createRange(); + r.selectNode(node.dom()); + var rect = r.getBoundingClientRect(); + // Clamp x,y at the bounds of the node so that the locate function has SOME chance + var boundedX = Math.max(rect.left, Math.min(rect.right, x)); + var boundedY = Math.max(rect.top, Math.min(rect.bottom, y)); + + return ContainerPoint.locate(doc, node, boundedX, boundedY); + }; + + var searchFromPoint = function (doc, x, y) { + // elementFromPoint is defined to return null when there is no element at the point + // This often happens when using IE10 event.y instead of event.clientY + return Option.from(doc.dom().elementFromPoint(x, y)).map(Element.fromDom).bind(function (elem) { + // used when the x,y position points to an image, or outside the bounds + var fallback = function () { + return EdgePoint.search(doc, elem, x); + }; + + return Traverse.children(elem).length === 0 ? fallback() : + // if we have children, search for the right text node and then get the offset out of it + searchTextNodes(doc, elem, x, y).orThunk(fallback); + }); + }; + + var availableSearch = document.caretPositionFromPoint ? caretPositionFromPoint : // defined standard + document.caretRangeFromPoint ? caretRangeFromPoint : // webkit implementation + searchFromPoint; // fallback + + + var fromPoint = function (win, x, y) { + var doc = Element.fromDom(win.document); + return availableSearch(doc, x, y).map(function (rng) { + return Selection.range( + Element.fromDom(rng.startContainer), + rng.startOffset, + Element.fromDom(rng.endContainer), + rng.endOffset + ); + }); + }; + + return { + fromPoint: fromPoint + }; + } +); + +define( + 'ephox.sugar.selection.query.Within', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.Selectors', + 'ephox.sugar.selection.core.NativeRange', + 'ephox.sugar.selection.core.SelectionDirection' + ], + + function (Arr, Element, Node, SelectorFilter, Selectors, NativeRange, SelectionDirection) { + var withinContainer = function (win, ancestor, outerRange, selector) { + var innerRange = NativeRange.create(win); + var self = Selectors.is(ancestor, selector) ? [ ancestor ] : []; + var elements = self.concat(SelectorFilter.descendants(ancestor, selector)); + return Arr.filter(elements, function (elem) { + // Mutate the selection to save creating new ranges each time + NativeRange.selectNodeContentsUsing(innerRange, elem); + return NativeRange.isWithin(outerRange, innerRange); + }); + }; + + var find = function (win, selection, selector) { + // Reverse the selection if it is RTL when doing the comparison + var outerRange = SelectionDirection.asLtrRange(win, selection); + var ancestor = Element.fromDom(outerRange.commonAncestorContainer); + // Note, this might need to change when we have to start looking for non elements. + return Node.isElement(ancestor) ? + withinContainer(win, ancestor, outerRange, selector) : []; + }; + + return { + find: find + }; + } +); +define( + 'ephox.sugar.selection.quirks.Prefilter', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.api.selection.Situ' + ], + + function (Arr, Node, Selection, Situ) { + var beforeSpecial = function (element, offset) { + // From memory, we don't want to use
    directly on Firefox because it locks the keyboard input. + // It turns out that directly on IE locks the keyboard as well. + // If the offset is 0, use before. If the offset is 1, use after. + // TBIO-3889: Firefox Situ.on results in a child of the ; Situ.before results in platform inconsistencies + var name = Node.name(element); + if ('input' === name) return Situ.after(element); + else if (!Arr.contains([ 'br', 'img' ], name)) return Situ.on(element, offset); + else return offset === 0 ? Situ.before(element) : Situ.after(element); + }; + + var preprocess = function (startSitu, finishSitu) { + var start = startSitu.fold(Situ.before, beforeSpecial, Situ.after); + var finish = finishSitu.fold(Situ.before, beforeSpecial, Situ.after); + return Selection.relative(start, finish); + }; + + return { + beforeSpecial: beforeSpecial, + preprocess: preprocess + }; + } +); + +define( + 'ephox.sugar.api.selection.WindowSelection', + + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.DocumentPosition', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Fragment', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.api.selection.Situ', + 'ephox.sugar.selection.core.NativeRange', + 'ephox.sugar.selection.core.SelectionDirection', + 'ephox.sugar.selection.query.CaretRange', + 'ephox.sugar.selection.query.Within', + 'ephox.sugar.selection.quirks.Prefilter' + ], + + function (Option, DocumentPosition, Element, Fragment, Selection, Situ, NativeRange, SelectionDirection, CaretRange, Within, Prefilter) { + var doSetNativeRange = function (win, rng) { + var selection = win.getSelection(); + selection.removeAllRanges(); + selection.addRange(rng); + }; + + var doSetRange = function (win, start, soffset, finish, foffset) { + var rng = NativeRange.exactToNative(win, start, soffset, finish, foffset); + doSetNativeRange(win, rng); + }; + + var findWithin = function (win, selection, selector) { + return Within.find(win, selection, selector); + }; + + var setExact = function (win, start, soffset, finish, foffset) { + setRelative(win, Situ.on(start, soffset), Situ.on(finish, foffset)); + }; + + var setRelative = function (win, startSitu, finishSitu) { + var relative = Prefilter.preprocess(startSitu, finishSitu); + + return SelectionDirection.diagnose(win, relative).match({ + ltr: function (start, soffset, finish, foffset) { + doSetRange(win, start, soffset, finish, foffset); + }, + rtl: function (start, soffset, finish, foffset) { + var selection = win.getSelection(); + // If this selection is backwards, then we need to use extend. + if (selection.extend) { + selection.collapse(start.dom(), soffset); + selection.extend(finish.dom(), foffset); + } else { + doSetRange(win, finish, foffset, start, soffset); + } + } + }); + }; + + // NOTE: We are still reading the range because it gives subtly different behaviour + // than using the anchorNode and focusNode. I'm not sure if this behaviour is any + // better or worse; it's just different. + var readRange = function (selection) { + var rng = Option.from(selection.getRangeAt(0)); + return rng.map(function (r) { + return Selection.range(Element.fromDom(r.startContainer), r.startOffset, Element.fromDom(r.endContainer), r.endOffset); + }); + }; + + var doGetExact = function (selection) { + var anchorNode = Element.fromDom(selection.anchorNode); + var focusNode = Element.fromDom(selection.focusNode); + return DocumentPosition.after(anchorNode, selection.anchorOffset, focusNode, selection.focusOffset) ? Option.some( + Selection.range( + Element.fromDom(selection.anchorNode), + selection.anchorOffset, + Element.fromDom(selection.focusNode), + selection.focusOffset + ) + ) : readRange(selection); + }; + + var setToElement = function (win, element) { + var rng = NativeRange.selectNodeContents(win, element); + doSetNativeRange(win, rng); + }; + + var forElement = function (win, element) { + var rng = NativeRange.selectNodeContents(win, element); + return Selection.range( + Element.fromDom(rng.startContainer), rng.startOffset, + Element.fromDom(rng.endContainer), rng.endOffset + ); + }; + + var getExact = function (win) { + // We want to retrieve the selection as it is. + var selection = win.getSelection(); + return selection.rangeCount > 0 ? doGetExact(selection) : Option.none(); + }; + + // TODO: Test this. + var get = function (win) { + return getExact(win).map(function (range) { + return Selection.exact(range.start(), range.soffset(), range.finish(), range.foffset()); + }); + }; + + var getFirstRect = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.getFirstRect(rng); + }; + + var getBounds = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.getBounds(rng); + }; + + var getAtPoint = function (win, x, y) { + return CaretRange.fromPoint(win, x, y); + }; + + var getAsString = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.toString(rng); + }; + + var clear = function (win) { + var selection = win.getSelection(); + selection.removeAllRanges(); + }; + + var clone = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.cloneFragment(rng); + }; + + var replace = function (win, selection, elements) { + var rng = SelectionDirection.asLtrRange(win, selection); + var fragment = Fragment.fromElements(elements); + NativeRange.replaceWith(rng, fragment); + }; + + var deleteAt = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + NativeRange.deleteContents(rng); + }; + + return { + setExact: setExact, + getExact: getExact, + get: get, + setRelative: setRelative, + setToElement: setToElement, + clear: clear, + + clone: clone, + replace: replace, + deleteAt: deleteAt, + + forElement: forElement, + + getFirstRect: getFirstRect, + getBounds: getBounds, + getAtPoint: getAtPoint, + + findWithin: findWithin, + getAsString: getAsString + }; + } +); + +define( + 'tinymce.themes.mobile.util.Rectangles', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.Awareness', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.api.selection.WindowSelection' + ], + + function (Arr, Fun, Element, Traverse, Awareness, Selection, WindowSelection) { + var COLLAPSED_WIDTH = 2; + + var collapsedRect = function (rect) { + return { + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + width: Fun.constant(COLLAPSED_WIDTH), + height: rect.height + }; + }; + + var toRect = function (rawRect) { + return { + left: Fun.constant(rawRect.left), + top: Fun.constant(rawRect.top), + right: Fun.constant(rawRect.right), + bottom: Fun.constant(rawRect.bottom), + width: Fun.constant(rawRect.width), + height: Fun.constant(rawRect.height) + }; + }; + + var getRectsFromRange = function (range) { + if (! range.collapsed) { + return Arr.map(range.getClientRects(), toRect); + } else { + var start = Element.fromDom(range.startContainer); + return Traverse.parent(start).bind(function (parent) { + var selection = Selection.exact(start, range.startOffset, parent, Awareness.getEnd(parent)); + var optRect = WindowSelection.getFirstRect(range.startContainer.ownerDocument.defaultView, selection); + return optRect.map(collapsedRect).map(Arr.pure); + }).getOr([ ]); + } + }; + + var getRectangles = function (cWin) { + var sel = cWin.getSelection(); + // In the Android WebView for some reason cWin.getSelection returns undefined. + // The undefined check it is to avoid throwing of a JS error. + return sel !== undefined && sel.rangeCount > 0 ? getRectsFromRange(sel.getRangeAt(0)) : [ ]; + }; + + return { + getRectangles: getRectangles + }; + } +); + +define( + 'tinymce.themes.mobile.android.core.AndroidSetup', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Attr', + 'global!Math', + 'tinymce.themes.mobile.android.focus.ResumeEditing', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.DataAttributes', + 'tinymce.themes.mobile.util.Rectangles' + ], + + function (Fun, Option, DomEvent, Element, Attr, Math, ResumeEditing, Styles, DataAttributes, Rectangles) { + // This amount is added to the minimum scrolling distance when calculating how much to scroll + // because the soft keyboard has appeared. + var EXTRA_SPACING = 50; + + var data = 'data-' + Styles.resolve('last-outer-height'); + + var setLastHeight = function (cBody, value) { + Attr.set(cBody, data, value); + }; + + var getLastHeight = function (cBody) { + return DataAttributes.safeParse(cBody, data); + }; + + var getBoundsFrom = function (rect) { + return { + top: Fun.constant(rect.top()), + bottom: Fun.constant(rect.top() + rect.height()) + }; + }; + + var getBounds = function (cWin) { + var rects = Rectangles.getRectangles(cWin); + return rects.length > 0 ? Option.some(rects[0]).map(getBoundsFrom) : Option.none(); + }; + + var findDelta = function (outerWindow, cBody) { + var last = getLastHeight(cBody); + var current = outerWindow.innerHeight; + return last > current ? Option.some(last - current) : Option.none(); + }; + + var calculate = function (cWin, bounds, delta) { + // The goal here is to shift as little as required. + var isOutside = bounds.top() > cWin.innerHeight || bounds.bottom() > cWin.innerHeight; + return isOutside ? Math.min(delta, bounds.bottom() - cWin.innerHeight + EXTRA_SPACING) : 0; + }; + + var setup = function (outerWindow, cWin) { + var cBody = Element.fromDom(cWin.document.body); + + var toEditing = function () { + // TBIO-3816 throttling the resume was causing keyboard hide/show issues with undo/redo + // throttling was introduced to work around a different keyboard hide/show issue, where + // async uiChanged in Processor in polish was causing keyboard hide, which no longer seems to occur + ResumeEditing.resume(cWin); + }; + + var onResize = DomEvent.bind(Element.fromDom(outerWindow), 'resize', function () { + + findDelta(outerWindow, cBody).each(function (delta) { + getBounds(cWin).each(function (bounds) { + // If the top is offscreen, scroll it into view. + var cScrollBy = calculate(cWin, bounds, delta); + if (cScrollBy !== 0) { + cWin.scrollTo(cWin.pageXOffset, cWin.pageYOffset + cScrollBy); + } + }); + }); + setLastHeight(cBody, outerWindow.innerHeight); + }); + + setLastHeight(cBody, outerWindow.innerHeight); + + var destroy = function () { + onResize.unbind(); + }; + + return { + toEditing: toEditing, + destroy: destroy + }; + }; + + return { + setup: setup + }; + } +); + +define( + 'tinymce.themes.mobile.ios.core.PlatformEditor', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.selection.WindowSelection' + ], + + function (Fun, Option, Compare, DomEvent, Element, WindowSelection) { + var getBodyFromFrame = function (frame) { + return Option.some(Element.fromDom(frame.dom().contentWindow.document.body)); + }; + + var getDocFromFrame = function (frame) { + return Option.some(Element.fromDom(frame.dom().contentWindow.document)); + }; + + var getWinFromFrame = function (frame) { + return Option.from(frame.dom().contentWindow); + }; + + var getSelectionFromFrame = function (frame) { + var optWin = getWinFromFrame(frame); + return optWin.bind(WindowSelection.getExact); + }; + + var getFrame = function (editor) { + return editor.getFrame(); + }; + + var getOrDerive = function (name, f) { + return function (editor) { + var g = editor[name].getOrThunk(function () { + var frame = getFrame(editor); + return function () { + return f(frame); + }; + }); + + return g(); + }; + }; + + var getOrListen = function (editor, doc, name, type) { + return editor[name].getOrThunk(function () { + return function (handler) { + return DomEvent.bind(doc, type, handler); + }; + }); + }; + + var toRect = function (rect) { + return { + left: Fun.constant(rect.left), + top: Fun.constant(rect.top), + right: Fun.constant(rect.right), + bottom: Fun.constant(rect.bottom), + width: Fun.constant(rect.width), + height: Fun.constant(rect.height) + }; + }; + + var getActiveApi = function (editor) { + var frame = getFrame(editor); + + // Empty paragraphs can have no rectangle size, so let's just use the start container + // if it is collapsed; + var tryFallbackBox = function (win) { + var isCollapsed = function (sel) { + return Compare.eq(sel.start(), sel.finish()) && sel.soffset() === sel.foffset(); + }; + + var toStartRect = function (sel) { + var rect = sel.start().dom().getBoundingClientRect(); + return rect.width > 0 || rect.height > 0 ? Option.some(rect).map(toRect) : Option.none(); + }; + + return WindowSelection.getExact(win).filter(isCollapsed).bind(toStartRect); + }; + + return getBodyFromFrame(frame).bind(function (body) { + return getDocFromFrame(frame).bind(function (doc) { + return getWinFromFrame(frame).map(function (win) { + + var html = Element.fromDom(doc.dom().documentElement); + + var getCursorBox = editor.getCursorBox.getOrThunk(function () { + return function () { + return WindowSelection.get(win).bind(function (sel) { + return WindowSelection.getFirstRect(win, sel).orThunk(function () { + return tryFallbackBox(win); + }); + }); + }; + }); + + var setSelection = editor.setSelection.getOrThunk(function () { + return function (start, soffset, finish, foffset) { + WindowSelection.setExact(win, start, soffset, finish, foffset); + }; + }); + + var clearSelection = editor.clearSelection.getOrThunk(function () { + return function () { + WindowSelection.clear(win); + }; + }); + + return { + body: Fun.constant(body), + doc: Fun.constant(doc), + win: Fun.constant(win), + html: Fun.constant(html), + getSelection: Fun.curry(getSelectionFromFrame, frame), + setSelection: setSelection, + clearSelection: clearSelection, + frame: Fun.constant(frame), + + onKeyup: getOrListen(editor, doc, 'onKeyup', 'keyup'), + onNodeChanged: getOrListen(editor, doc, 'onNodeChanged', 'selectionchange'), + onDomChanged: editor.onDomChanged, // consider defaulting with MutationObserver + + onScrollToCursor: editor.onScrollToCursor, + onScrollToElement: editor.onScrollToElement, + onToReading: editor.onToReading, + onToEditing: editor.onToEditing, + + onToolbarScrollStart: editor.onToolbarScrollStart, + onTouchContent: editor.onTouchContent, + onTapContent: editor.onTapContent, + onTouchToolstrip: editor.onTouchToolstrip, + + getCursorBox: getCursorBox + }; + }); + }); + }); + }; + + return { + getBody: getOrDerive('getBody', getBodyFromFrame), + getDoc: getOrDerive('getDoc', getDocFromFrame), + getWin: getOrDerive('getWin', getWinFromFrame), + getSelection: getOrDerive('getSelection', getSelectionFromFrame), + getFrame: getFrame, + getActiveApi: getActiveApi + }; + } +); + +define( + 'tinymce.themes.mobile.util.Thor', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.SelectorFilter' + ], + + function (Arr, PlatformDetection, Attr, Css, SelectorFilter) { + var attr = 'data-ephox-mobile-fullscreen-style'; + var siblingStyles = 'display:none!important;'; + var ancestorPosition = 'position:absolute!important;'; + var ancestorStyles = 'top:0!important;left:0!important;margin:0' + + '!important;padding:0!important;width:100%!important;'; + var bgFallback = 'background-color:rgb(255,255,255)!important;'; + + var isAndroid = PlatformDetection.detect().os.isAndroid(); + + var matchColor = function (editorBody) { + // in iOS you can overscroll, sometimes when you overscroll you can reveal the bgcolor of an element beneath, + // by matching the bg color and clobbering ensures any reveals are 'camouflaged' the same color + var color = Css.get(editorBody, 'background-color'); + return (color !== undefined && color !== '') ? 'background-color:' + color + '!important' : bgFallback; + }; + + // We clobber all tags, direct ancestors to the editorBody get ancestorStyles, everything else gets siblingStyles + var clobberStyles = function (container, editorBody) { + var gatherSibilings = function (element) { + var siblings = SelectorFilter.siblings(element, '*'); + return siblings; + }; + + var clobber = function (clobberStyle) { + return function (element) { + var styles = Attr.get(element, 'style'); + var backup = styles === undefined ? 'no-styles' : styles.trim(); + + if (backup === clobberStyle) { + return; + } else { + Attr.set(element, attr, backup); + Attr.set(element, 'style', clobberStyle); + } + }; + }; + + var ancestors = SelectorFilter.ancestors(container, '*'); + var siblings = Arr.bind(ancestors, gatherSibilings); + var bgColor = matchColor(editorBody); + + /* NOTE: This assumes that container has no siblings itself */ + Arr.each(siblings, clobber(siblingStyles)); + Arr.each(ancestors, clobber(ancestorPosition + ancestorStyles + bgColor)); + // position absolute on the outer-container breaks Android flex layout + var containerStyles = isAndroid === true ? '' : ancestorPosition; + clobber(containerStyles + ancestorStyles + bgColor)(container); + }; + + var restoreStyles = function () { + var clobberedEls = SelectorFilter.all('[' + attr + ']'); + Arr.each(clobberedEls, function (element) { + var restore = Attr.get(element, attr); + if (restore !== 'no-styles') { + Attr.set(element, 'style', restore); + } else { + Attr.remove(element, 'style'); + } + Attr.remove(element, attr); + }); + }; + + return { + clobberStyles: clobberStyles, + restoreStyles: restoreStyles + }; + } +); + +define( + 'tinymce.themes.mobile.touch.view.MetaViewport', + + [ + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.search.SelectorFind' + ], + + function (Insert, Element, Attr, SelectorFind) { + /* + * The purpose of this fix is to toggle the presence of a meta tag which disables scrolling + * for the user + */ + var tag = function () { + var head = SelectorFind.first('head').getOrDie(); + + var nu = function () { + var meta = Element.fromTag('meta'); + Attr.set(meta, 'name', 'viewport'); + Insert.append(head, meta); + return meta; + }; + + var element = SelectorFind.first('meta[name="viewport"]').getOrThunk(nu); + var backup = Attr.get(element, 'content'); + + var maximize = function () { + Attr.set(element, 'content', 'width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0'); + }; + + var restore = function () { + if (backup !== undefined && backup !== null && backup.length > 0) { + Attr.set(element, 'content', backup); + } else { + // TODO: user-scalable is left as disabled when the editor closes + Attr.remove(element, 'content'); + } + }; + + return { + maximize: maximize, + restore: restore + }; + }; + + return { + tag: tag + }; + } +); +define( + 'tinymce.themes.mobile.android.core.AndroidMode', + + [ + 'ephox.katamari.api.Singleton', + 'ephox.sugar.api.properties.Class', + 'tinymce.themes.mobile.android.core.AndroidEvents', + 'tinymce.themes.mobile.android.core.AndroidSetup', + 'tinymce.themes.mobile.ios.core.PlatformEditor', + 'tinymce.themes.mobile.util.Thor', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.touch.view.MetaViewport' + ], + + function (Singleton, Class, AndroidEvents, AndroidSetup, PlatformEditor, Thor, Styles, MetaViewport) { + var create = function (platform, mask) { + + var meta = MetaViewport.tag(); + var androidApi = Singleton.api(); + + var androidEvents = Singleton.api(); + + var enter = function () { + mask.hide(); + + Class.add(platform.container, Styles.resolve('fullscreen-maximized')); + Class.add(platform.container, Styles.resolve('android-maximized')); + meta.maximize(); + + /// TM-48 Prevent browser refresh by swipe/scroll on android devices + Class.add(platform.body, Styles.resolve('android-scroll-reload')); + + androidApi.set( + AndroidSetup.setup(platform.win, PlatformEditor.getWin(platform.editor).getOrDie('no')) + ); + + PlatformEditor.getActiveApi(platform.editor).each(function (editorApi) { + Thor.clobberStyles(platform.container, editorApi.body()); + androidEvents.set( + AndroidEvents.initEvents(editorApi, platform.toolstrip, platform.alloy) + ); + }); + }; + + var exit = function () { + meta.restore(); + mask.show(); + Class.remove(platform.container, Styles.resolve('fullscreen-maximized')); + Class.remove(platform.container, Styles.resolve('android-maximized')); + Thor.restoreStyles(); + + /// TM-48 re-enable swipe/scroll browser refresh on android + Class.remove(platform.body, Styles.resolve('android-scroll-reload')); + + androidEvents.clear(); + + androidApi.clear(); + }; + + return { + enter: enter, + exit: exit + }; + }; + + return { + create: create + }; + } +); + +define( + 'tinymce.themes.mobile.api.MobileSchema', + + [ + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse', + 'global!window' + ], + + function (FieldSchema, ValueSchema, Fun, Element, Traverse, window) { + return ValueSchema.objOf([ + FieldSchema.strictObjOf('editor', [ + // Maybe have frame as a method, but I doubt it ... I think we pretty much need a frame + FieldSchema.strict('getFrame'), + FieldSchema.option('getBody'), + FieldSchema.option('getDoc'), + FieldSchema.option('getWin'), + FieldSchema.option('getSelection'), + FieldSchema.option('setSelection'), + FieldSchema.option('clearSelection'), + + FieldSchema.option('cursorSaver'), + + FieldSchema.option('onKeyup'), + FieldSchema.option('onNodeChanged'), + FieldSchema.option('getCursorBox'), + + FieldSchema.strict('onDomChanged'), + + FieldSchema.defaulted('onTouchContent', Fun.noop), + FieldSchema.defaulted('onTapContent', Fun.noop), + FieldSchema.defaulted('onTouchToolstrip', Fun.noop), + + + FieldSchema.defaulted('onScrollToCursor', Fun.constant({ unbind: Fun.noop })), + FieldSchema.defaulted('onScrollToElement', Fun.constant({ unbind: Fun.noop })), + FieldSchema.defaulted('onToEditing', Fun.constant({ unbind: Fun.noop })), + FieldSchema.defaulted('onToReading', Fun.constant({ unbind: Fun.noop })), + FieldSchema.defaulted('onToolbarScrollStart', Fun.identity) + ]), + + FieldSchema.strict('socket'), + FieldSchema.strict('toolstrip'), + FieldSchema.strict('dropup'), + FieldSchema.strict('toolbar'), + FieldSchema.strict('container'), + FieldSchema.strict('alloy'), + FieldSchema.state('win', function (spec) { + return Traverse.owner(spec.socket).dom().defaultView; + }), + FieldSchema.state('body', function (spec) { + return Element.fromDom( + spec.socket.dom().ownerDocument.body + ); + }), + FieldSchema.defaulted('translate', Fun.identity), + FieldSchema.defaulted('setReadOnly', Fun.noop) + ]); + } +); + +define( + 'ephox.katamari.api.Throttler', + + [ + 'global!clearTimeout', + 'global!setTimeout' + ], + + function (clearTimeout, setTimeout) { + // Run a function fn afer rate ms. If another invocation occurs + // during the time it is waiting, update the arguments f will run + // with (but keep the current schedule) + var adaptable = function (fn, rate) { + var timer = null; + var args = null; + var cancel = function () { + if (timer !== null) { + clearTimeout(timer); + timer = null; + args = null; + } + }; + var throttle = function () { + args = arguments; + if (timer === null) { + timer = setTimeout(function () { + fn.apply(null, args); + timer = null; + args = null; + }, rate); + } + }; + + return { + cancel: cancel, + throttle: throttle + }; + }; + + // Run a function fn after rate ms. If another invocation occurs + // during the time it is waiting, ignore it completely. + var first = function (fn, rate) { + var timer = null; + var cancel = function () { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + }; + var throttle = function () { + var args = arguments; + if (timer === null) { + timer = setTimeout(function () { + fn.apply(null, args); + timer = null; + args = null; + }, rate); + } + }; + + return { + cancel: cancel, + throttle: throttle + }; + }; + + // Run a function fn after rate ms. If another invocation occurs + // during the time it is waiting, reschedule the function again + // with the new arguments. + var last = function (fn, rate) { + var timer = null; + var cancel = function () { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + }; + var throttle = function () { + var args = arguments; + if (timer !== null) clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(null, args); + timer = null; + args = null; + }, rate); + }; + + return { + cancel: cancel, + throttle: throttle + }; + }; + + return { + adaptable: adaptable, + first: first, + last: last + }; + } +); +define( + 'tinymce.themes.mobile.touch.view.TapToEditMask', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.component.Memento', + 'ephox.alloy.api.ui.Button', + 'ephox.alloy.api.ui.Container', + 'ephox.katamari.api.Throttler', + 'global!setTimeout', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function (Behaviour, Toggling, Memento, Button, Container, Throttler, setTimeout, Styles, UiDomFactory) { + var sketch = function (onView, translate) { + + var memIcon = Memento.record( + Container.sketch({ + dom: UiDomFactory.dom(''), + containerBehaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('mask-tap-icon-selected'), + toggleOnExecute: false + }) + ]) + }) + ); + + var onViewThrottle = Throttler.first(onView, 200); + + return Container.sketch({ + dom: UiDomFactory.dom('
    '), + components: [ + Container.sketch({ + dom: UiDomFactory.dom('
    '), + components: [ + Button.sketch({ + dom: UiDomFactory.dom('
    '), + components: [ + memIcon.asSpec() + ], + action: function (button) { + onViewThrottle.throttle(); + }, + + buttonBehaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('mask-tap-icon-selected') + }) + ]) + }) + ] + }) + ] + }); + }; + + return { + sketch: sketch + }; + } +); + +define( + 'tinymce.themes.mobile.api.AndroidWebapp', + + [ + 'ephox.alloy.api.component.GuiFactory', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.properties.Css', + 'tinymce.themes.mobile.android.core.AndroidMode', + 'tinymce.themes.mobile.api.MobileSchema', + 'tinymce.themes.mobile.touch.view.TapToEditMask' + ], + + function (GuiFactory, ValueSchema, Fun, Insert, Css, AndroidMode, MobileSchema, TapToEditMask) { + // TODO: Remove dupe with IosWebapp + var produce = function (raw) { + var mobile = ValueSchema.asRawOrDie( + 'Getting AndroidWebapp schema', + MobileSchema, + raw + ); + + /* Make the toolbar */ + Css.set(mobile.toolstrip, 'width', '100%'); + + // We do not make the Android container relative, because we aren't positioning the toolbar absolutely. + var onTap = function () { + mobile.setReadOnly(true); + mode.enter(); + }; + + var mask = GuiFactory.build( + TapToEditMask.sketch(onTap, mobile.translate) + ); + + mobile.alloy.add(mask); + var maskApi = { + show: function () { + mobile.alloy.add(mask); + }, + hide: function () { + mobile.alloy.remove(mask); + } + }; + + Insert.append(mobile.container, mask.element()); + + var mode = AndroidMode.create(mobile, maskApi); + + return { + setReadOnly: mobile.setReadOnly, + // Not used. + refreshStructure: Fun.noop, + enter: mode.enter, + exit: mode.exit, + destroy: Fun.noop + }; + }; + + return { + produce: produce + }; + } +); + +define( + 'ephox.alloy.ui.schema.ToolbarSchema', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.alloy.data.Fields', + 'ephox.alloy.parts.PartType', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun' + ], + + function (Behaviour, Replacing, Fields, PartType, FieldSchema, Fun) { + var schema = [ + FieldSchema.defaulted('shell', true), + FieldSchema.defaulted('toolbarBehaviours', { }) + ]; + + // TODO: Dupe with Toolbar + var enhanceGroups = function (detail) { + return { + behaviours: Behaviour.derive([ + Replacing.config({ }) + ]) + }; + }; + + var partTypes = [ + // Note, is the container for putting all the groups in, not a group itself. + PartType.optional({ + name: 'groups', + overrides: enhanceGroups + }) + ]; + + return { + name: Fun.constant('Toolbar'), + schema: Fun.constant(schema), + parts: Fun.constant(partTypes) + }; + } +); +define( + 'ephox.alloy.api.ui.Toolbar', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.parts.AlloyParts', + 'ephox.alloy.ui.schema.ToolbarSchema', + 'ephox.katamari.api.Merger', + 'ephox.katamari.api.Option', + 'global!console', + 'global!Error' + ], + + function (Behaviour, Replacing, Sketcher, AlloyParts, ToolbarSchema, Merger, Option, console, Error) { + var factory = function (detail, components, spec, _externals) { + var setGroups = function (toolbar, groups) { + getGroupContainer(toolbar).fold(function () { + // check that the group container existed. It may not have if the components + // did not list anything, and shell was false. + console.error('Toolbar was defined to not be a shell, but no groups container was specified in components'); + throw new Error('Toolbar was defined to not be a shell, but no groups container was specified in components'); + }, function (container) { + Replacing.set(container, groups); + }); + }; + + var getGroupContainer = function (component) { + return detail.shell() ? Option.some(component) : AlloyParts.getPart(component, detail, 'groups'); + }; + + // In shell mode, the group overrides need to be added to the main container, and there can be no children + var extra = detail.shell() ? { behaviours: [ Replacing.config({ }) ], components: [ ] } : + { behaviours: [ ], components: components }; + + return { + uid: detail.uid(), + dom: detail.dom(), + components: extra.components, + + behaviours: Merger.deepMerge( + Behaviour.derive(extra.behaviours), + detail.toolbarBehaviours() + ), + apis: { + setGroups: setGroups + }, + domModification: { + attributes: { + role: 'group' + } + } + }; + }; + + return Sketcher.composite({ + name: 'Toolbar', + configFields: ToolbarSchema.schema(), + partFields: ToolbarSchema.parts(), + factory: factory, + apis: { + setGroups: function (apis, toolbar, groups) { + apis.setGroups(toolbar, groups); + } + } + }); + } +); +define( + 'ephox.alloy.ui.schema.ToolbarGroupSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.alloy.parts.PartType', + 'ephox.boulder.api.FieldSchema', + 'ephox.katamari.api.Fun' + ], + + function (Fields, PartType, FieldSchema, Fun) { + var schema = [ + FieldSchema.strict('items'), + Fields.markers([ 'itemClass' ]), + FieldSchema.defaulted('hasTabstop', true), + FieldSchema.defaulted('tgroupBehaviours', { }) + ]; + + var partTypes = [ + PartType.group({ + name: 'items', + unit: 'item', + overrides: function (detail) { + return { + domModification: { + classes: [ detail.markers().itemClass() ] + } + }; + } + }) + ]; + + return { + name: Fun.constant('ToolbarGroup'), + schema: Fun.constant(schema), + parts: Fun.constant(partTypes) + }; + } +); +define( + 'ephox.alloy.api.ui.ToolbarGroup', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Tabstopping', + 'ephox.alloy.api.ui.Sketcher', + 'ephox.alloy.ui.schema.ToolbarGroupSchema', + 'ephox.katamari.api.Merger', + 'global!Error' + ], + + function (Behaviour, Keying, Tabstopping, Sketcher, ToolbarGroupSchema, Merger, Error) { + var factory = function (detail, components, spec, _externals) { + return Merger.deepMerge( + { + dom: { + attributes: { + role: 'toolbar' + } + } + }, + { + uid: detail.uid(), + dom: detail.dom(), + components: components, + + behaviours: Merger.deepMerge( + Behaviour.derive([ + Keying.config({ + mode: 'flow', + selector: '.' + detail.markers().itemClass() + }), + detail.hasTabstop() ? Tabstopping.config({ }) : Tabstopping.revoke() + ]), + detail.tgroupBehaviours() + ), + + 'debug.sketcher': spec['debug.sketcher'] + } + ); + }; + + return Sketcher.composite({ + name: 'ToolbarGroup', + configFields: ToolbarGroupSchema.schema(), + partFields: ToolbarGroupSchema.parts(), + factory: factory + }); + } +); +define( + 'tinymce.themes.mobile.ios.scroll.Scrollables', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.search.SelectorFind', + 'tinymce.themes.mobile.style.Styles' + ], + + function (Fun, DomEvent, Attr, SelectorFind, Styles) { + var dataHorizontal = 'data-' + Styles.resolve('horizontal-scroll'); + + var canScrollVertically = function (container) { + container.dom().scrollTop = 1; + var result = container.dom().scrollTop !== 0; + container.dom().scrollTop = 0; + return result; + }; + + var canScrollHorizontally = function (container) { + container.dom().scrollLeft = 1; + var result = container.dom().scrollLeft !== 0; + container.dom().scrollLeft = 0; + return result; + }; + + var hasVerticalScroll = function (container) { + return container.dom().scrollTop > 0 || canScrollVertically(container); + }; + + var hasHorizontalScroll = function (container) { + return container.dom().scrollLeft > 0 || canScrollHorizontally(container); + }; + + var markAsHorizontal = function (container) { + Attr.set(container, dataHorizontal, 'true'); + }; + + var hasScroll = function (container) { + return Attr.get(container, dataHorizontal) === 'true' ? hasHorizontalScroll : hasVerticalScroll; + }; + + /* + * Prevents default on touchmove for anything that is not within a scrollable area. The + * scrollable areas are defined by selector. + */ + var exclusive = function (scope, selector) { + return DomEvent.bind(scope, 'touchmove', function (event) { + SelectorFind.closest(event.target(), selector).filter(hasScroll).fold(function () { + event.raw().preventDefault(); + }, Fun.noop); + }); + }; + + return { + exclusive: exclusive, + markAsHorizontal: markAsHorizontal + }; + } +); + +define( + 'tinymce.themes.mobile.toolbar.ScrollingToolbar', + + [ + 'ephox.alloy.api.behaviour.AddEventsBehaviour', + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Keying', + 'ephox.alloy.api.behaviour.Toggling', + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.ui.Container', + 'ephox.alloy.api.ui.Toolbar', + 'ephox.alloy.api.ui.ToolbarGroup', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.properties.Css', + 'tinymce.themes.mobile.ios.scroll.Scrollables', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.touch.scroll.Scrollable', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function ( + AddEventsBehaviour, Behaviour, Keying, Toggling, GuiFactory, AlloyEvents, Container, Toolbar, ToolbarGroup, Arr, Cell, Fun, Css, Scrollables, Styles, Scrollable, + UiDomFactory + ) { + return function () { + var makeGroup = function (gSpec) { + var scrollClass = gSpec.scrollable === true ? '${prefix}-toolbar-scrollable-group' : ''; + return { + dom: UiDomFactory.dom('
    '), + + tgroupBehaviours: Behaviour.derive([ + AddEventsBehaviour.config('adhoc-scrollable-toolbar', gSpec.scrollable === true ? [ + AlloyEvents.runOnInit(function (component, simulatedEvent) { + Css.set(component.element(), 'overflow-x', 'auto'); + Scrollables.markAsHorizontal(component.element()); + Scrollable.register(component.element()); + }) + ] : [ ]) + ]), + + components: [ + Container.sketch({ + components: [ + ToolbarGroup.parts().items({ }) + ] + }) + ], + markers: { + itemClass: Styles.resolve('toolbar-group-item') + }, + + items: gSpec.items + }; + }; + + var toolbar = GuiFactory.build( + Toolbar.sketch( + { + dom: UiDomFactory.dom('
    '), + components: [ + Toolbar.parts().groups({ }) + ], + toolbarBehaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('context-toolbar'), + toggleOnExecute: false, + aria: { + mode: 'none' + } + }), + Keying.config({ + mode: 'cyclic' + }) + ]), + shell: true + } + ) + ); + + var wrapper = GuiFactory.build( + Container.sketch({ + dom: { + classes: [ Styles.resolve('toolstrip') ] + }, + components: [ + GuiFactory.premade(toolbar) + ], + containerBehaviours: Behaviour.derive([ + Toggling.config({ + toggleClass: Styles.resolve('android-selection-context-toolbar'), + toggleOnExecute: false + }) + ]) + }) + ); + + var resetGroups = function () { + Toolbar.setGroups(toolbar, initGroups.get()); + Toggling.off(toolbar); + }; + + var initGroups = Cell([ ]); + + var setGroups = function (gs) { + initGroups.set(gs); + resetGroups(); + }; + + var createGroups = function (gs) { + return Arr.map(gs, Fun.compose(ToolbarGroup.sketch, makeGroup)); + }; + + var refresh = function () { + Toolbar.refresh(toolbar); + }; + + var setContextToolbar = function (gs) { + Toggling.on(toolbar); + Toolbar.setGroups(toolbar, gs); + }; + + var restoreToolbar = function () { + if (Toggling.isOn(toolbar)) { + resetGroups(); + } + }; + + var focus = function () { + Keying.focusIn(toolbar); + }; + + return { + wrapper: Fun.constant(wrapper), + toolbar: Fun.constant(toolbar), + createGroups: createGroups, + setGroups: setGroups, + setContextToolbar: setContextToolbar, + restoreToolbar: restoreToolbar, + refresh: refresh, + focus: focus + }; + }; + + } +); +define( + 'tinymce.themes.mobile.ui.CommonRealm', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.alloy.api.behaviour.Swapping', + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.ui.Button', + 'ephox.alloy.api.ui.Container', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.properties.Class', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.UiDomFactory' + ], + + function ( + Behaviour, Replacing, Swapping, GuiFactory, Button, Container, Fun, Class, Styles, + UiDomFactory + ) { + var makeEditSwitch = function (webapp) { + return GuiFactory.build( + Button.sketch({ + dom: UiDomFactory.dom('
    '), + action: function () { + webapp.run(function (w) { + w.setReadOnly(false); + }); + } + }) + ); + }; + + var makeSocket = function () { + return GuiFactory.build( + Container.sketch({ + dom: UiDomFactory.dom('
    '), + components: [ ], + containerBehaviours: Behaviour.derive([ + Replacing.config({ }) + ]) + }) + ); + }; + + var showEdit = function (socket, switchToEdit) { + Replacing.append(socket, GuiFactory.premade(switchToEdit)); + }; + + var hideEdit = function (socket, switchToEdit) { + Replacing.remove(socket, switchToEdit); + }; + + var updateMode = function (socket, switchToEdit, readOnly, root) { + var swap = (readOnly === true) ? Swapping.toAlpha : Swapping.toOmega; + swap(root); + + var f = readOnly ? showEdit : hideEdit; + f(socket, switchToEdit); + }; + + return { + makeEditSwitch: makeEditSwitch, + makeSocket: makeSocket, + updateMode: updateMode + }; + } +); + +define( + 'ephox.alloy.behaviour.sliding.SlidingApis', + + [ + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.properties.Classes', + 'ephox.sugar.api.properties.Css' + ], + + function (Class, Classes, Css) { + var getAnimationRoot = function (component, slideConfig) { + return slideConfig.getAnimationRoot().fold(function () { + return component.element(); + }, function (get) { + return get(component); + }); + }; + + var getDimensionProperty = function (slideConfig) { + return slideConfig.dimension().property(); + }; + + var getDimension = function (slideConfig, elem) { + return slideConfig.dimension().getDimension()(elem); + }; + + var disableTransitions = function (component, slideConfig) { + var root = getAnimationRoot(component, slideConfig); + Classes.remove(root, [ slideConfig.shrinkingClass(), slideConfig.growingClass() ]); + }; + + var setShrunk = function (component, slideConfig) { + Class.remove(component.element(), slideConfig.openClass()); + Class.add(component.element(), slideConfig.closedClass()); + Css.set(component.element(), getDimensionProperty(slideConfig), '0px'); + Css.reflow(component.element()); + }; + + // Note, this is without transitions, so we can measure the size instantaneously + var measureTargetSize = function (component, slideConfig) { + setGrown(component, slideConfig); + var expanded = getDimension(slideConfig, component.element()); + setShrunk(component, slideConfig); + return expanded; + }; + + var setGrown = function (component, slideConfig) { + Class.remove(component.element(), slideConfig.closedClass()); + Class.add(component.element(), slideConfig.openClass()); + Css.remove(component.element(), getDimensionProperty(slideConfig)); + }; + + var doImmediateShrink = function (component, slideConfig, slideState) { + slideState.setCollapsed(); + + // Force current dimension to begin transition + Css.set(component.element(), getDimensionProperty(slideConfig), getDimension(slideConfig, component.element())); + Css.reflow(component.element()); + + disableTransitions(component, slideConfig); + + setShrunk(component, slideConfig); + slideConfig.onStartShrink()(component); + slideConfig.onShrunk()(component); + }; + + var doStartShrink = function (component, slideConfig, slideState) { + slideState.setCollapsed(); + + // Force current dimension to begin transition + Css.set(component.element(), getDimensionProperty(slideConfig), getDimension(slideConfig, component.element())); + Css.reflow(component.element()); + + var root = getAnimationRoot(component, slideConfig); + Class.add(root, slideConfig.shrinkingClass()); // enable transitions + setShrunk(component, slideConfig); + slideConfig.onStartShrink()(component); + }; + + // Showing is complex due to the inability to transition to "auto". + // We also can't cache the dimension as the parents may have resized since it was last shown. + var doStartGrow = function (component, slideConfig, slideState) { + var fullSize = measureTargetSize(component, slideConfig); + + // Start the growing animation styles + var root = getAnimationRoot(component, slideConfig); + Class.add(root, slideConfig.growingClass()); + + setGrown(component, slideConfig); + + Css.set(component.element(), getDimensionProperty(slideConfig), fullSize); + // We might need to consider having a Css.reflow here. We can't have it in setGrown because + // it stops the transition immediately because it jumps to the final size. + + slideState.setExpanded(); + slideConfig.onStartGrow()(component); + }; + + var grow = function (component, slideConfig, slideState) { + if (! slideState.isExpanded()) doStartGrow(component, slideConfig, slideState); + }; + + var shrink = function (component, slideConfig, slideState) { + if (slideState.isExpanded()) doStartShrink(component, slideConfig, slideState); + }; + + var immediateShrink = function (component, slideConfig, slideState) { + if (slideState.isExpanded()) doImmediateShrink(component, slideConfig, slideState); + }; + + var hasGrown = function (component, slideConfig, slideState) { + return slideState.isExpanded(); + }; + + var hasShrunk = function (component, slideConfig, slideState) { + return slideState.isCollapsed(); + }; + + var isGrowing = function (component, slideConfig, slideState) { + var root = getAnimationRoot(component, slideConfig); + return Class.has(root, slideConfig.growingClass()) === true; + }; + + var isShrinking = function (component, slideConfig, slideState) { + var root = getAnimationRoot(component, slideConfig); + return Class.has(root, slideConfig.shrinkingClass()) === true; + }; + + var isTransitioning = function (component, slideConfig, slideState) { + return isGrowing(component, slideConfig, slideState) === true || isShrinking(component, slideConfig, slideState) === true; + }; + + var toggleGrow = function (component, slideConfig, slideState) { + var f = slideState.isExpanded() ? doStartShrink : doStartGrow; + f(component, slideConfig, slideState); + }; + + return { + grow: grow, + shrink: shrink, + immediateShrink: immediateShrink, + hasGrown: hasGrown, + hasShrunk: hasShrunk, + isGrowing: isGrowing, + isShrinking: isShrinking, + isTransitioning: isTransitioning, + toggleGrow: toggleGrow, + disableTransitions: disableTransitions + }; + } +); +define( + 'ephox.alloy.behaviour.sliding.ActiveSliding', + + [ + 'ephox.alloy.api.events.AlloyEvents', + 'ephox.alloy.api.events.NativeEvents', + 'ephox.alloy.behaviour.sliding.SlidingApis', + 'ephox.alloy.dom.DomModification', + 'ephox.boulder.api.Objects', + 'ephox.sugar.api.properties.Css' + ], + + function (AlloyEvents, NativeEvents, SlidingApis, DomModification, Objects, Css) { + var exhibit = function (base, slideConfig/*, slideState */) { + var expanded = slideConfig.expanded(); + + return expanded ? DomModification.nu({ + classes: [ slideConfig.openClass() ], + styles: { } + }) : DomModification.nu({ + classes: [ slideConfig.closedClass() ], + styles: Objects.wrap(slideConfig.dimension().property(), '0px') + }); + }; + + var events = function (slideConfig, slideState) { + return AlloyEvents.derive([ + AlloyEvents.run(NativeEvents.transitionend(), function (component, simulatedEvent) { + var raw = simulatedEvent.event().raw(); + // This will fire for all transitions, we're only interested in the dimension completion + if (raw.propertyName === slideConfig.dimension().property()) { + SlidingApis.disableTransitions(component, slideConfig, slideState); // disable transitions immediately (Safari animates the dimension removal below) + if (slideState.isExpanded()) Css.remove(component.element(), slideConfig.dimension().property()); // when showing, remove the dimension so it is responsive + var notify = slideState.isExpanded() ? slideConfig.onGrown() : slideConfig.onShrunk(); + notify(component, simulatedEvent); + } + }) + ]); + }; + + return { + exhibit: exhibit, + events: events + }; + } +); +define( + 'ephox.alloy.behaviour.sliding.SlidingSchema', + + [ + 'ephox.alloy.data.Fields', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.sugar.api.view.Height', + 'ephox.sugar.api.view.Width' + ], + + function (Fields, FieldSchema, ValueSchema, Height, Width) { + return [ + FieldSchema.strict('closedClass'), + FieldSchema.strict('openClass'), + FieldSchema.strict('shrinkingClass'), + FieldSchema.strict('growingClass'), + + // Element which shrinking and growing animations + FieldSchema.option('getAnimationRoot'), + + Fields.onHandler('onShrunk'), + Fields.onHandler('onStartShrink'), + Fields.onHandler('onGrown'), + Fields.onHandler('onStartGrow'), + FieldSchema.defaulted('expanded', false), + FieldSchema.strictOf('dimension', ValueSchema.choose( + 'property', { + width: [ + Fields.output('property', 'width'), + Fields.output('getDimension', function (elem) { + return Width.get(elem) + 'px'; + }) + ], + height: [ + Fields.output('property', 'height'), + Fields.output('getDimension', function (elem) { + return Height.get(elem) + 'px'; + }) + ] + } + )) + + ]; + } +); +define( + 'ephox.alloy.behaviour.sliding.SlidingState', + + [ + 'ephox.alloy.behaviour.common.BehaviourState', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun' + ], + + function (BehaviourState, Cell, Fun) { + var init = function (spec) { + var state = Cell(spec.expanded()); + + var readState = function () { + return 'expanded: ' + state.get(); + }; + + return BehaviourState({ + isExpanded: function () { return state.get() === true; }, + isCollapsed: function () { return state.get() === false; }, + setCollapsed: Fun.curry(state.set, false), + setExpanded: Fun.curry(state.set, true), + readState: readState + }); + }; + + return { + init: init + }; + } +); + +define( + 'ephox.alloy.api.behaviour.Sliding', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.behaviour.sliding.ActiveSliding', + 'ephox.alloy.behaviour.sliding.SlidingApis', + 'ephox.alloy.behaviour.sliding.SlidingSchema', + 'ephox.alloy.behaviour.sliding.SlidingState' + ], + + function (Behaviour, ActiveSliding, SlidingApis, SlidingSchema, SlidingState) { + return Behaviour.create({ + fields: SlidingSchema, + name: 'sliding', + active: ActiveSliding, + apis: SlidingApis, + state: SlidingState + }); + } +); +define( + 'tinymce.themes.mobile.ui.Dropup', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.alloy.api.behaviour.Sliding', + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.ui.Container', + 'ephox.katamari.api.Fun', + 'global!window', + 'tinymce.themes.mobile.channels.Receivers', + 'tinymce.themes.mobile.style.Styles' + ], + + function (Behaviour, Replacing, Sliding, GuiFactory, Container, Fun, window, Receivers, Styles) { + var build = function (refresh, scrollIntoView) { + var dropup = GuiFactory.build( + Container.sketch({ + dom: { + tag: 'div', + classes: Styles.resolve('dropup') + }, + components: [ + + ], + containerBehaviours: Behaviour.derive([ + Replacing.config({ }), + Sliding.config({ + closedClass: Styles.resolve('dropup-closed'), + openClass: Styles.resolve('dropup-open'), + shrinkingClass: Styles.resolve('dropup-shrinking'), + growingClass: Styles.resolve('dropup-growing'), + dimension: { + property: 'height' + }, + onShrunk: function (component) { + refresh(); + scrollIntoView(); + + Replacing.set(component, [ ]); + }, + onGrown: function (component) { + refresh(); + scrollIntoView(); + } + }), + Receivers.orientation(function (component, data) { + disappear(Fun.noop); + }) + ]) + }) + ); + + var appear = function (menu, update, component) { + if (Sliding.hasShrunk(dropup) === true && Sliding.isTransitioning(dropup) === false) { + window.requestAnimationFrame(function () { + update(component); + Replacing.set(dropup, [ menu() ]); + Sliding.grow(dropup); + }); + } + }; + + var disappear = function (onReadyToShrink) { + window.requestAnimationFrame(function () { + onReadyToShrink(); + Sliding.shrink(dropup); + }); + }; + + return { + appear: appear, + disappear: disappear, + component: Fun.constant(dropup), + element: dropup.element + }; + }; + + return { + build: build + }; + } +); + +define( + 'ephox.alloy.events.GuiEvents', + + [ + 'ephox.alloy.alien.Keys', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.events.TapEvent', + 'ephox.boulder.api.FieldSchema', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Arr', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.Traverse', + 'global!setTimeout' + ], + + function (Keys, SystemEvents, TapEvent, FieldSchema, ValueSchema, Arr, PlatformDetection, DomEvent, Node, Traverse, setTimeout) { + var isDangerous = function (event) { + // Will trigger the Back button in the browser + return event.raw().which === Keys.BACKSPACE()[0] && !Arr.contains([ 'input', 'textarea' ], Node.name(event.target())); + }; + + var isFirefox = PlatformDetection.detect().browser.isFirefox(); + + var settingsSchema = ValueSchema.objOfOnly([ + // triggerEvent(eventName, event) + FieldSchema.strictFunction('triggerEvent'), + FieldSchema.strictFunction('broadcastEvent'), + FieldSchema.defaulted('stopBackspace', true) + ]); + + var bindFocus = function (container, handler) { + if (isFirefox) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=687787 + return DomEvent.capture(container, 'focus', handler); + } else { + return DomEvent.bind(container, 'focusin', handler); + } + }; + + var bindBlur = function (container, handler) { + if (isFirefox) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=687787 + return DomEvent.capture(container, 'blur', handler); + } else { + return DomEvent.bind(container, 'focusout', handler); + } + }; + + var setup = function (container, rawSettings) { + var settings = ValueSchema.asRawOrDie('Getting GUI events settings', settingsSchema, rawSettings); + + var pointerEvents = PlatformDetection.detect().deviceType.isTouch() ? [ + 'touchstart', + 'touchmove', + 'touchend', + 'gesturestart' + ] : [ + 'mousedown', + 'mouseup', + 'mouseover', + 'mousemove', + 'mouseout', + 'click' + ]; + + var tapEvent = TapEvent.monitor(settings); + + // These events are just passed through ... no additional processing + var simpleEvents = Arr.map( + pointerEvents.concat([ + 'selectstart', + 'input', + 'contextmenu', + 'change', + 'transitionend', + // Test the drag events + 'dragstart', + 'dragover', + 'drop' + ]), + function (type) { + return DomEvent.bind(container, type, function (event) { + tapEvent.fireIfReady(event, type).each(function (tapStopped) { + if (tapStopped) event.kill(); + }); + + var stopped = settings.triggerEvent(type, event); + if (stopped) event.kill(); + }); + } + ); + + var onKeydown = DomEvent.bind(container, 'keydown', function (event) { + // Prevent default of backspace when not in input fields. + var stopped = settings.triggerEvent('keydown', event); + if (stopped) event.kill(); + else if (settings.stopBackspace === true && isDangerous(event)) { event.prevent(); } + }); + + var onFocusIn = bindFocus(container, function (event) { + var stopped = settings.triggerEvent('focusin', event); + if (stopped) event.kill(); + }); + + var onFocusOut = bindBlur(container, function (event) { + var stopped = settings.triggerEvent('focusout', event); + if (stopped) event.kill(); + + // INVESTIGATE: Come up with a better way of doing this. Related target can be used, but not on FF. + // It allows the active element to change before firing the blur that we will listen to + // for things like closing popups + setTimeout(function () { + settings.triggerEvent(SystemEvents.postBlur(), event); + }, 0); + }); + + var defaultView = Traverse.defaultView(container); + var onWindowScroll = DomEvent.bind(defaultView, 'scroll', function (event) { + var stopped = settings.broadcastEvent(SystemEvents.windowScroll(), event); + if (stopped) event.kill(); + }); + + var unbind = function () { + Arr.each(simpleEvents, function (e) { + e.unbind(); + }); + onKeydown.unbind(); + onFocusIn.unbind(); + onFocusOut.unbind(); + onWindowScroll.unbind(); + }; + + return { + unbind: unbind + }; + }; + + return { + setup: setup + }; + } +); +define( + 'ephox.alloy.events.EventSource', + + [ + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Cell' + ], + + function (Objects, Cell) { + var derive = function (rawEvent, rawTarget) { + var source = Objects.readOptFrom(rawEvent, 'target').map(function (getTarget) { + return getTarget(); + }).getOr(rawTarget); + + return Cell(source); + }; + + return { + derive: derive + }; + } +); +define( + 'ephox.alloy.events.SimulatedEvent', + + [ + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'global!Error' + ], + + function (Cell, Fun, Error) { + var fromSource = function (event, source) { + var stopper = Cell(false); + + var cutter = Cell(false); + + var stop = function () { + stopper.set(true); + }; + + var cut = function () { + cutter.set(true); + }; + + return { + stop: stop, + cut: cut, + isStopped: stopper.get, + isCut: cutter.get, + event: Fun.constant(event), + // Used only for tiered menu at the moment. It is an element, not a component + setSource: source.set, + getSource: source.get + }; + }; + + // Events that come from outside of the alloy root (e.g. window scroll) + var fromExternal = function (event) { + var stopper = Cell(false); + + var stop = function () { + stopper.set(true); + }; + + return { + stop: stop, + cut: Fun.noop, // cutting has no meaning for a broadcasted event + isStopped: stopper.get, + isCut: Fun.constant(false), + event: Fun.constant(event), + // Nor do targets really + setTarget: Fun.die( + new Error('Cannot set target of a broadcasted event') + ), + getTarget: Fun.die( + new Error('Cannot get target of a broadcasted event') + ) + }; + }; + + var fromTarget = function (event, target) { + var source = Cell(target); + return fromSource(event, source); + }; + + return { + fromSource: fromSource, + fromExternal: fromExternal, + fromTarget: fromTarget + }; + } +); + +define( + 'ephox.alloy.events.Triggers', + + [ + 'ephox.alloy.events.DescribedHandler', + 'ephox.alloy.events.EventSource', + 'ephox.alloy.events.SimulatedEvent', + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.search.Traverse', + 'global!Error' + ], + + function (DescribedHandler, EventSource, SimulatedEvent, Adt, Arr, Traverse, Error) { + var adt = Adt.generate([ + { stopped: [ ] }, + { resume: [ 'element' ] }, + { complete: [ ] } + ]); + + var doTriggerHandler = function (lookup, eventType, rawEvent, target, source, logger) { + var handler = lookup(eventType, target); + + var simulatedEvent = SimulatedEvent.fromSource(rawEvent, source); + + return handler.fold(function () { + // No handler, so complete. + logger.logEventNoHandlers(eventType, target); + return adt.complete(); + }, function (handlerInfo) { + var descHandler = handlerInfo.descHandler(); + var eventHandler = DescribedHandler.getHandler(descHandler); + eventHandler(simulatedEvent); + + // Now, check if the event was stopped. + if (simulatedEvent.isStopped()) { + logger.logEventStopped(eventType, handlerInfo.element(), descHandler.purpose()); + return adt.stopped(); + } + // Now, check if the event was cut + else if (simulatedEvent.isCut()) { + logger.logEventCut(eventType, handlerInfo.element(), descHandler.purpose()); + return adt.complete(); + } + else return Traverse.parent(handlerInfo.element()).fold(function () { + logger.logNoParent(eventType, handlerInfo.element(), descHandler.purpose()); + // No parent, so complete. + return adt.complete(); + }, function (parent) { + logger.logEventResponse(eventType, handlerInfo.element(), descHandler.purpose()); + // Resume at parent + return adt.resume(parent); + }); + }); + }; + + var doTriggerOnUntilStopped = function (lookup, eventType, rawEvent, rawTarget, source, logger) { + return doTriggerHandler(lookup, eventType, rawEvent, rawTarget, source, logger).fold(function () { + // stopped. + return true; + }, function (parent) { + // Go again. + return doTriggerOnUntilStopped(lookup, eventType, rawEvent, parent, source, logger); + }, function () { + // completed + return false; + }); + }; + + var triggerHandler = function (lookup, eventType, rawEvent, target, logger) { + var source = EventSource.derive(rawEvent, target); + return doTriggerHandler(lookup, eventType, rawEvent, target, source, logger); + }; + + var broadcast = function (listeners, rawEvent, logger) { + var simulatedEvent = SimulatedEvent.fromExternal(rawEvent); + + Arr.each(listeners, function (listener) { + var descHandler = listener.descHandler(); + var handler = DescribedHandler.getHandler(descHandler); + handler(simulatedEvent); + }); + + return simulatedEvent.isStopped(); + }; + + var triggerUntilStopped = function (lookup, eventType, rawEvent, logger) { + var rawTarget = rawEvent.target(); + return triggerOnUntilStopped(lookup, eventType, rawEvent, rawTarget, logger); + }; + + var triggerOnUntilStopped = function (lookup, eventType, rawEvent, rawTarget, logger) { + var source = EventSource.derive(rawEvent, rawTarget); + return doTriggerOnUntilStopped(lookup, eventType, rawEvent, rawTarget, source, logger); + }; + + return { + triggerHandler: triggerHandler, + triggerUntilStopped: triggerUntilStopped, + triggerOnUntilStopped: triggerOnUntilStopped, + broadcast: broadcast + }; + } +); +define( + 'ephox.alloy.alien.TransformFind', + + [ + 'ephox.sugar.api.search.PredicateFind' + ], + + function (PredicateFind) { + var closest = function (target, transform, isRoot) { + // TODO: Sugar method is inefficient ... .need to write something new which allows me to keep the optional + // information, rather than just returning a boolean. Sort of a findMap for Predicate.ancestor. + var delegate = PredicateFind.closest(target, function (elem) { + return transform(elem).isSome(); + }, isRoot); + + return delegate.bind(transform); + }; + + return { + closest: closest + }; + } +); +define( + 'ephox.alloy.events.EventRegistry', + + [ + 'ephox.alloy.alien.TransformFind', + 'ephox.alloy.events.DescribedHandler', + 'ephox.alloy.registry.Tagger', + 'ephox.boulder.api.Objects', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Struct', + 'global!console' + ], + + function (TransformFind, DescribedHandler, Tagger, Objects, Fun, Obj, Option, Struct, console) { + var eventHandler = Struct.immutable('element', 'descHandler'); + + var messageHandler = function (id, handler) { + return { + id: Fun.constant(id), + descHandler: Fun.constant(handler) + }; + }; + + return function () { + var registry = { }; + + var registerId = function (extraArgs, id, events) { + Obj.each(events, function (v, k) { + var handlers = registry[k] !== undefined ? registry[k] : { }; + handlers[id] = DescribedHandler.curryArgs(v, extraArgs); + registry[k] = handlers; + }); + }; + + var findHandler = function (handlers, elem) { + return Tagger.read(elem).fold(function (err) { + return Option.none(); + }, function (id) { + var reader = Objects.readOpt(id); + return handlers.bind(reader).map(function (descHandler) { + return eventHandler(elem, descHandler); + }); + }); + }; + + // Given just the event type, find all handlers regardless of element + var filterByType = function (type) { + return Objects.readOptFrom(registry, type).map(function (handlers) { + return Obj.mapToArray(handlers, function (f, id) { + return messageHandler(id, f); + }); + }).getOr([ ]); + }; + + // Given event type, and element, find the handler. + var find = function (isAboveRoot, type, target) { + var readType = Objects.readOpt(type); + var handlers = readType(registry); + return TransformFind.closest(target, function (elem) { + return findHandler(handlers, elem); + }, isAboveRoot); + }; + + var unregisterId = function (id) { + // INVESTIGATE: Find a better way than mutation if we can. + Obj.each(registry, function (handlersById, eventName) { + if (handlersById.hasOwnProperty(id)) delete handlersById[id]; + }); + }; + + return { + registerId: registerId, + unregisterId: unregisterId, + filterByType: filterByType, + find: find + }; + }; + } +); +define( + 'ephox.alloy.registry.Registry', + + [ + 'ephox.alloy.events.EventRegistry', + 'ephox.alloy.log.AlloyLogger', + 'ephox.alloy.registry.Tagger', + 'ephox.boulder.api.Objects', + 'ephox.sugar.api.node.Body', + 'global!Error' + ], + + function (EventRegistry, AlloyLogger, Tagger, Objects, Body, Error) { + return function () { + var events = EventRegistry(); + + var components = { }; + + var readOrTag = function (component) { + var elem = component.element(); + return Tagger.read(elem).fold(function () { + // No existing tag, so add one. + return Tagger.write('uid-', component.element()); + }, function (uid) { + return uid; + }); + }; + + var failOnDuplicate = function (component, tagId) { + var conflict = components[tagId]; + if (conflict === component) unregister(component); + else throw new Error( + 'The tagId "' + tagId + '" is already used by: ' + AlloyLogger.element(conflict.element()) + '\nCannot use it for: ' + AlloyLogger.element(component.element()) + '\n' + + 'The conflicting element is' + (Body.inBody(conflict.element()) ? ' ' : ' not ') + 'already in the DOM' + ); + }; + + var register = function (component) { + var tagId = readOrTag(component); + if (Objects.hasKey(components, tagId)) failOnDuplicate(component, tagId); + // Component is passed through an an extra argument to all events + var extraArgs = [ component ]; + events.registerId(extraArgs, tagId, component.events()); + components[tagId] = component; + }; + + var unregister = function (component) { + Tagger.read(component.element()).each(function (tagId) { + components[tagId] = undefined; + events.unregisterId(tagId); + }); + }; + + var filter = function (type) { + return events.filterByType(type); + }; + + var find = function (isAboveRoot, type, target) { + return events.find(isAboveRoot, type, target); + }; + + var getById = function (id) { + return Objects.readOpt(id)(components); + }; + + return { + find: find, + filter: filter, + register: register, + unregister: unregister, + getById: getById + }; + }; + } +); +define( + 'ephox.alloy.api.system.Gui', + + [ + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.events.SystemEvents', + 'ephox.alloy.api.system.Attachment', + 'ephox.alloy.api.system.SystemApi', + 'ephox.alloy.api.ui.Container', + 'ephox.alloy.debugging.Debugging', + 'ephox.alloy.events.DescribedHandler', + 'ephox.alloy.events.GuiEvents', + 'ephox.alloy.events.Triggers', + 'ephox.alloy.log.AlloyLogger', + 'ephox.alloy.registry.Registry', + 'ephox.alloy.registry.Tagger', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Result', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.search.Traverse', + 'global!Error' + ], + + function ( + GuiFactory, SystemEvents, Attachment, SystemApi, Container, Debugging, DescribedHandler, GuiEvents, Triggers, AlloyLogger, Registry, Tagger, Arr, Fun, Result, + Compare, Focus, Insert, Remove, Node, Class, Traverse, Error + ) { + var create = function () { + var root = GuiFactory.build( + Container.sketch({ + dom: { + tag: 'div' + } + }) + ); + return takeover(root); + }; + + var takeover = function (root) { + var isAboveRoot = function (el) { + return Traverse.parent(root.element()).fold( + function () { + return true; + }, + function (parent) { + return Compare.eq(el, parent); + } + ); + }; + + var registry = Registry(); + + var lookup = function (eventName, target) { + return registry.find(isAboveRoot, eventName, target); + }; + + var domEvents = GuiEvents.setup(root.element(), { + triggerEvent: function (eventName, event) { + return Debugging.monitorEvent(eventName, event.target(), function (logger) { + return Triggers.triggerUntilStopped(lookup, eventName, event, logger); + }); + }, + + // This doesn't follow usual DOM bubbling. It will just dispatch on all + // targets that have the event. It is the general case of the more specialised + // "message". "messages" may actually just go away. This is used for things + // like window scroll. + broadcastEvent: function (eventName, event) { + var listeners = registry.filter(eventName); + return Triggers.broadcast(listeners, event); + } + }); + + var systemApi = SystemApi({ + // This is a real system + debugInfo: Fun.constant('real'), + triggerEvent: function (customType, target, data) { + Debugging.monitorEvent(customType, target, function (logger) { + // The return value is not used because this is a fake event. + Triggers.triggerOnUntilStopped(lookup, customType, data, target, logger); + }); + }, + triggerFocus: function (target, originator) { + Tagger.read(target).fold(function () { + // When the target is not within the alloy system, dispatch a normal focus event. + Focus.focus(target); + }, function (_alloyId) { + Debugging.monitorEvent(SystemEvents.focus(), target, function (logger) { + Triggers.triggerHandler(lookup, SystemEvents.focus(), { + // originator is used by the default events to ensure that focus doesn't + // get called infinitely + originator: Fun.constant(originator), + target: Fun.constant(target) + }, target, logger); + }); + }); + }, + + triggerEscape: function (comp, simulatedEvent) { + systemApi.triggerEvent('keydown', comp.element(), simulatedEvent.event()); + }, + + getByUid: function (uid) { + return getByUid(uid); + }, + getByDom: function (elem) { + return getByDom(elem); + }, + build: GuiFactory.build, + addToGui: function (c) { add(c); }, + removeFromGui: function (c) { remove(c); }, + addToWorld: function (c) { addToWorld(c); }, + removeFromWorld: function (c) { removeFromWorld(c); }, + broadcast: function (message) { + broadcast(message); + }, + broadcastOn: function (channels, message) { + broadcastOn(channels, message); + } + }); + + var addToWorld = function (component) { + component.connect(systemApi); + if (!Node.isText(component.element())) { + registry.register(component); + Arr.each(component.components(), addToWorld); + systemApi.triggerEvent(SystemEvents.systemInit(), component.element(), { target: Fun.constant(component.element()) }); + } + }; + + var removeFromWorld = function (component) { + if (!Node.isText(component.element())) { + Arr.each(component.components(), removeFromWorld); + registry.unregister(component); + } + component.disconnect(); + }; + + var add = function (component) { + Attachment.attach(root, component); + }; + + var remove = function (component) { + Attachment.detach(component); + }; + + var destroy = function () { + // INVESTIGATE: something with registry? + domEvents.unbind(); + Remove.remove(root.element()); + }; + + var broadcastData = function (data) { + var receivers = registry.filter(SystemEvents.receive()); + Arr.each(receivers, function (receiver) { + var descHandler = receiver.descHandler(); + var handler = DescribedHandler.getHandler(descHandler); + handler(data); + }); + }; + + var broadcast = function (message) { + broadcastData({ + universal: Fun.constant(true), + data: Fun.constant(message) + }); + }; + + var broadcastOn = function (channels, message) { + broadcastData({ + universal: Fun.constant(false), + channels: Fun.constant(channels), + data: Fun.constant(message) + }); + }; + + var getByUid = function (uid) { + return registry.getById(uid).fold(function () { + return Result.error( + new Error('Could not find component with uid: "' + uid + '" in system.') + ); + }, Result.value); + }; + + var getByDom = function (elem) { + return Tagger.read(elem).bind(getByUid); + }; + + addToWorld(root); + + return { + root: Fun.constant(root), + element: root.element, + destroy: destroy, + add: add, + remove: remove, + getByUid: getByUid, + getByDom: getByDom, + + addToWorld: addToWorld, + removeFromWorld: removeFromWorld, + + broadcast: broadcast, + broadcastOn: broadcastOn + }; + }; + + return { + create: create, + takeover: takeover + }; + } +); +define( + 'tinymce.themes.mobile.ui.OuterContainer', + + [ + 'ephox.alloy.api.behaviour.Behaviour', + 'ephox.alloy.api.behaviour.Swapping', + 'ephox.alloy.api.component.GuiFactory', + 'ephox.alloy.api.system.Gui', + 'ephox.alloy.api.ui.Container', + 'ephox.katamari.api.Fun', + 'tinymce.themes.mobile.style.Styles' + ], + + function (Behaviour, Swapping, GuiFactory, Gui, Container, Fun, Styles) { + var READ_ONLY_MODE_CLASS = Fun.constant(Styles.resolve('readonly-mode')); + var EDIT_MODE_CLASS = Fun.constant(Styles.resolve('edit-mode')); + + return function (spec) { + var root = GuiFactory.build( + Container.sketch({ + dom: { + classes: [ Styles.resolve('outer-container') ].concat(spec.classes) + }, + + containerBehaviours: Behaviour.derive([ + Swapping.config({ + alpha: READ_ONLY_MODE_CLASS(), + omega: EDIT_MODE_CLASS() + }) + ]) + }) + ); + + return Gui.takeover(root); + }; + } +); + +define( + 'tinymce.themes.mobile.ui.AndroidRealm', + + [ + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.alloy.api.behaviour.Swapping', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Singleton', + 'tinymce.themes.mobile.api.AndroidWebapp', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.toolbar.ScrollingToolbar', + 'tinymce.themes.mobile.ui.CommonRealm', + 'tinymce.themes.mobile.ui.Dropup', + 'tinymce.themes.mobile.ui.OuterContainer' + ], + + + function (Replacing, Swapping, Fun, Singleton, AndroidWebapp, Styles, ScrollingToolbar, CommonRealm, Dropup, OuterContainer) { + return function (scrollIntoView) { + var alloy = OuterContainer({ + classes: [ Styles.resolve('android-container') ] + }); + + var toolbar = ScrollingToolbar(); + + var webapp = Singleton.api(); + + var switchToEdit = CommonRealm.makeEditSwitch(webapp); + + var socket = CommonRealm.makeSocket(); + + var dropup = Dropup.build(Fun.noop, scrollIntoView); + + alloy.add(toolbar.wrapper()); + alloy.add(socket); + alloy.add(dropup.component()); + + var setToolbarGroups = function (rawGroups) { + var groups = toolbar.createGroups(rawGroups); + toolbar.setGroups(groups); + }; + + var setContextToolbar = function (rawGroups) { + var groups = toolbar.createGroups(rawGroups); + toolbar.setContextToolbar(groups); + }; + + // You do not always want to do this. + var focusToolbar = function () { + toolbar.focus(); + }; + + var restoreToolbar = function () { + toolbar.restoreToolbar(); + }; + + var init = function (spec) { + webapp.set( + AndroidWebapp.produce(spec) + ); + }; + + var exit = function () { + webapp.run(function (w) { + w.exit(); + Replacing.remove(socket, switchToEdit); + }); + }; + + var updateMode = function (readOnly) { + CommonRealm.updateMode(socket, switchToEdit, readOnly, alloy.root()); + }; + + return { + system: Fun.constant(alloy), + element: alloy.element, + init: init, + exit: exit, + setToolbarGroups: setToolbarGroups, + setContextToolbar: setContextToolbar, + focusToolbar: focusToolbar, + restoreToolbar: restoreToolbar, + updateMode: updateMode, + socket: Fun.constant(socket), + dropup: Fun.constant(dropup) + }; + }; + } +); + +define( + 'ephox.sugar.api.view.Position', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + var r = function (left, top) { + var translate = function (x, y) { + return r(left + x, top + y); + }; + + return { + left: Fun.constant(left), + top: Fun.constant(top), + translate: translate + }; + }; + + return r; + } +); + +define( + 'ephox.sugar.api.dom.Dom', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.PredicateFind', + 'global!document' + ], + + function (Fun, Compare, Element, Node, PredicateFind, document) { + // TEST: Is this just Body.inBody which doesn't need scope ?? + var attached = function (element, scope) { + var doc = scope || Element.fromDom(document.documentElement); + return PredicateFind.ancestor(element, Fun.curry(Compare.eq, doc)).isSome(); + }; + + // TEST: Is this just Traverse.defaultView ?? + var windowOf = function (element) { + var dom = element.dom(); + if (dom === dom.window) return element; + return Node.isDocument(element) ? dom.defaultView || dom.parentWindow : null; + }; + + return { + attached: attached, + windowOf: windowOf + }; + } +); + +define( + 'ephox.sugar.api.view.Location', + + [ + 'ephox.sugar.api.view.Position', + 'ephox.sugar.api.dom.Dom', + 'ephox.sugar.api.node.Element' + ], + + function (Position, Dom, Element) { + var boxPosition = function (dom) { + var box = dom.getBoundingClientRect(); + return Position(box.left, box.top); + }; + + // Avoids falsy false fallthrough + var firstDefinedOrZero = function (a, b) { + return a !== undefined ? a : + b !== undefined ? b : + 0; + }; + + var absolute = function (element) { + var doc = element.dom().ownerDocument; + var body = doc.body; + var win = Dom.windowOf(Element.fromDom(doc)); + var html = doc.documentElement; + + + var scrollTop = firstDefinedOrZero(win.pageYOffset, html.scrollTop); + var scrollLeft = firstDefinedOrZero(win.pageXOffset, html.scrollLeft); + + var clientTop = firstDefinedOrZero(html.clientTop, body.clientTop); + var clientLeft = firstDefinedOrZero(html.clientLeft, body.clientLeft); + + return viewport(element).translate( + scrollLeft - clientLeft, + scrollTop - clientTop); + }; + + // This is the old $.position(), but JQuery does nonsense calculations. + // We're only 1 <-> 1 with the old value in the single place we use this function + // (ego.api.Dragging) so the rest can bite me. + var relative = function (element) { + var dom = element.dom(); + // jquery-ism: when style="position: fixed", this === boxPosition() + // but tests reveal it returns the same thing anyway + return Position(dom.offsetLeft, dom.offsetTop); + }; + + var viewport = function (element) { + var dom = element.dom(); + + var doc = dom.ownerDocument; + var body = doc.body; + var html = Element.fromDom(doc.documentElement); + + if (body === dom) + return Position(body.offsetLeft, body.offsetTop); + + if (!Dom.attached(element, html)) + return Position(0, 0); + + return boxPosition(dom); + }; + + return { + absolute: absolute, + relative: relative, + viewport: viewport + }; + } +); + +define( + 'tinymce.themes.mobile.ios.core.IosEvents', + + [ + 'ephox.alloy.events.TapEvent', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Throttler', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.view.Height', + 'ephox.sugar.api.view.Location', + 'tinymce.themes.mobile.util.TappingEvent' + ], + + function (TapEvent, Arr, Throttler, Compare, DomEvent, Height, Location, TappingEvent) { + var initEvents = function (editorApi, iosApi, toolstrip, socket, dropup) { + var saveSelectionFirst = function () { + iosApi.run(function (api) { + api.highlightSelection(); + }); + }; + + var refreshIosSelection = function () { + iosApi.run(function (api) { + api.refreshSelection(); + }); + }; + + var scrollToY = function (yTop, height) { + // Because the iframe has no scroll, and the socket is the part that scrolls, + // anything visible inside the iframe actually has a top value (for bounding + // rectangle) > socket.scrollTop. The rectangle is with respect to the top of + // the iframe, which has scrolled up above the socket viewport. + var y = yTop - socket.dom().scrollTop; + iosApi.run(function (api) { + api.scrollIntoView(y, y + height); + }); + }; + + var scrollToElement = function (target) { + var yTop = Location.absolute(target).top(); + var height = Height.get(target); + scrollToY(iosApi, socket, yTop, height); + }; + + var scrollToCursor = function () { + editorApi.getCursorBox().each(function (box) { + scrollToY(box.top(), box.height()); + }); + }; + + var clearSelection = function () { + // Clear any fake selections visible. + iosApi.run(function (api) { + api.clearSelection(); + }); + }; + + var clearAndRefresh = function () { + clearSelection(); + refreshThrottle.throttle(); + }; + + var refreshView = function () { + scrollToCursor(editorApi, iosApi, socket); + iosApi.run(function (api) { + api.syncHeight(); + }); + }; + + var reposition = function () { + var toolbarHeight = Height.get(toolstrip); + iosApi.run(function (api) { + api.setViewportOffset(toolbarHeight); + }); + + refreshIosSelection(iosApi); + refreshView(editorApi, iosApi, socket); + }; + + var toEditing = function () { + iosApi.run(function (api) { + api.toEditing(); + }); + }; + + var toReading = function () { + iosApi.run(function (api) { + api.toReading(); + }); + }; + + var onToolbarTouch = function (event) { + iosApi.run(function (api) { + api.onToolbarTouch(event); + }); + }; + + var tapping = TappingEvent.monitor(editorApi); + + var refreshThrottle = Throttler.last(refreshView, 300); + var listeners = [ + // Clear any fake selections, scroll to cursor, and update the iframe height + editorApi.onKeyup(clearAndRefresh), + // Update any fake selections that are showing + editorApi.onNodeChanged(refreshIosSelection), + + // Scroll to cursor, and update the iframe height + editorApi.onDomChanged(refreshThrottle.throttle), + // Update any fake selections that are showing + editorApi.onDomChanged(refreshIosSelection), + + // Scroll to cursor and update the iframe height + editorApi.onScrollToCursor(function (tinyEvent) { + tinyEvent.preventDefault(); + refreshThrottle.throttle(); + }), + + // Scroll to element + editorApi.onScrollToElement(function (event) { + scrollToElement(event.element()); + }), + + // Focus the content and show the keyboard + editorApi.onToEditing(toEditing), + + // Dismiss keyboard + editorApi.onToReading(toReading), + + // If the user is touching outside the content, but on the body(?) or html elements, find the nearest selection + // and focus that. + DomEvent.bind(editorApi.doc(), 'touchend', function (touchEvent) { + if (Compare.eq(editorApi.html(), touchEvent.target()) || Compare.eq(editorApi.body(), touchEvent.target())) { + // IosHacks.setSelectionAtTouch(editorApi, touchEvent); + } + }), + + // Listen to the toolstrip growing animation so that we can update the position of the socket once it is done. + DomEvent.bind(toolstrip, 'transitionend', function (transitionEvent) { + if (transitionEvent.raw().propertyName === 'height') { + reposition(); + } + }), + + // Capture the start of interacting with a toolstrip. It is most likely going to lose the selection, so we save it + // before that happens + DomEvent.capture(toolstrip, 'touchstart', function (touchEvent) { + // When touching the toolbar, the first thing that we need to do is 'represent' the selection. We do this with + // a fake selection. As soon as the focus switches away from the content, the real selection will disappear, so + // this lets the user still see their selection. + + saveSelectionFirst(); + + // Then, depending on the keyboard mode, we may need to do something else (like dismiss the keyboard) + onToolbarTouch(touchEvent); + + // Fire the touchstart event to the theme for things like hiding dropups + editorApi.onTouchToolstrip(); + }), + + // When the user clicks back into the content, clear any fake selections + DomEvent.bind(editorApi.body(), 'touchstart', function (evt) { + clearSelection(); + editorApi.onTouchContent(); + tapping.fireTouchstart(evt); + }), + + tapping.onTouchmove(), + tapping.onTouchend(), + + // Stop any "clicks" being processed in the body at alls + DomEvent.bind(editorApi.body(), 'click', function (event) { + event.kill(); + }), + + // Close any menus when scrolling the toolstrip + DomEvent.bind(toolstrip, 'touchmove', function (/* event */) { + editorApi.onToolbarScrollStart(); + }) + ]; + + var destroy = function () { + Arr.each(listeners, function (l) { + l.unbind(); + }); + }; + + return { + destroy: destroy + }; + }; + + + return { + initEvents: initEvents + }; + } +); + +define( + 'tinymce.themes.mobile.touch.focus.CursorRefresh', + + [ + 'ephox.sugar.api.dom.Focus', + 'global!setTimeout' + ], + + function (Focus, setTimeout) { + var refreshInput = function (input) { + // This is magic used to refresh the iOS cursor on an input field when input focus is + // lost and then restored. The setTime out is important for consistency, a lower value + // may not yield a successful reselection when the time out value is 10, 30% success + // on making the blue selection reappear. + var start = input.dom().selectionStart; + var end = input.dom().selectionEnd; + var dir = input.dom().selectionDirection; + setTimeout(function () { + input.dom().setSelectionRange(start, end, dir); + Focus.focus(input); + }, 50); + }; + + var refresh = function (winScope) { + // Sometimes the cursor can get out of sync with the content, it looks weird and importantly + // it causes the code that dismisses the keyboard to fail, Fussy has selection code, but since + // this is fired often and confined to iOS, it's implemented with more native code. Note, you + // can see the need for this if you remove this code, and click near the bottom of the content + // and start typing. The content will scroll up to go into the greenzone, but the cursor will + // still display in the old location. It only updates once you keep typing. However, if we do this + // hack, then the cursor is updated. You'll still have any autocorrect selection boxes, though. + var sel = winScope.getSelection(); + if (sel.rangeCount > 0) { + var br = sel.getRangeAt(0); + var r = winScope.document.createRange(); + r.setStart(br.startContainer, br.startOffset); + r.setEnd(br.endContainer, br.endOffset); + + // Note, if dropdowns appear to flicker, we might want to remove this line. All selections + // (not Firefox) probably just replace the one selection anyway. + sel.removeAllRanges(); + sel.addRange(r); + } + }; + + return { + refreshInput: refreshInput, + refresh: refresh + }; + } +); + +define( + 'tinymce.themes.mobile.ios.focus.ResumeEditing', + + [ + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.node.Element', + 'tinymce.themes.mobile.touch.focus.CursorRefresh' + ], + + function (Compare, Focus, Element, CursorRefresh) { + var resume = function (cWin, frame) { + Focus.active().each(function (active) { + // INVESTIGATE: This predicate may not be required. The purpose of it is to ensure + // that the content window's frame element is not unnecessarily blurred before giving + // it focus. + if (! Compare.eq(active, frame)) { + Focus.blur(active); + } + }); + // Required when transferring from another input area. + cWin.focus(); + + Focus.focus(Element.fromDom(cWin.document.body)); + CursorRefresh.refresh(cWin); + }; + + return { + resume: resume + }; + } +); + +define( + 'tinymce.themes.mobile.ios.focus.FakeSelection', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.properties.Classes', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.Traverse', + 'tinymce.themes.mobile.ios.focus.ResumeEditing', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.Rectangles' + ], + + function (Arr, Insert, InsertAll, Remove, DomEvent, Element, Class, Classes, Css, Traverse, ResumeEditing, Styles, Rectangles) { + return function (win, frame) { + // NOTE: This may be required for android also. + + + /* + * FakeSelection is used to draw rectangles around selection so that when the content loses + * focus, the selection is still visible. The selections should match the current content + * selection, and be removed as soon as the user clicks on them (because the content will + * get focus again) + */ + var doc = win.document; + + var container = Element.fromTag('div'); + Class.add(container, Styles.resolve('unfocused-selections')); + + Insert.append(Element.fromDom(doc.documentElement), container); + + var onTouch = DomEvent.bind(container, 'touchstart', function (event) { + // We preventDefault the event incase the touch is between 2 letters creating a new collapsed selection, + // in this very specific case we just want to turn the fake cursor into a real cursor. Remember that + // touchstart may be used to dimiss popups too, so don't kill it completely, just prevent its + // default native selection + event.prevent(); + ResumeEditing.resume(win, frame); + clear(); + }); + + var make = function (rectangle) { + var span = Element.fromTag('span'); + Classes.add(span, [ Styles.resolve('layer-editor'), Styles.resolve('unfocused-selection') ]); + Css.setAll(span, { + 'left': rectangle.left() + 'px', + 'top': rectangle.top() + 'px', + 'width': rectangle.width() + 'px', + 'height': rectangle.height() + 'px' + }); + return span; + }; + + var update = function () { + clear(); + var rectangles = Rectangles.getRectangles(win); + var spans = Arr.map(rectangles, make); + InsertAll.append(container, spans); + }; + + var clear = function () { + Remove.empty(container); + }; + + var destroy = function () { + onTouch.unbind(); + Remove.remove(container); + }; + + var isActive = function () { + return Traverse.children(container).length > 0; + }; + + return { + update: update, + isActive: isActive, + destroy: destroy, + clear: clear + }; + }; + } +); +define( + 'ephox.katamari.api.LazyValue', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'global!setTimeout' + ], + + function (Arr, Option, setTimeout) { + var nu = function (baseFn) { + var data = Option.none(); + var callbacks = []; + + /** map :: this LazyValue a -> (a -> b) -> LazyValue b */ + var map = function (f) { + return nu(function (nCallback) { + get(function (data) { + nCallback(f(data)); + }); + }); + }; + + var get = function (nCallback) { + if (isReady()) call(nCallback); + else callbacks.push(nCallback); + }; + + var set = function (x) { + data = Option.some(x); + run(callbacks); + callbacks = []; + }; + + var isReady = function () { + return data.isSome(); + }; + + var run = function (cbs) { + Arr.each(cbs, call); + }; + + var call = function(cb) { + data.each(function(x) { + setTimeout(function() { + cb(x); + }, 0); + }); + }; + + // Lazy values cache the value and kick off immediately + baseFn(set); + + return { + get: get, + map: map, + isReady: isReady + }; + }; + + var pure = function (a) { + return nu(function (callback) { + callback(a); + }); + }; + + return { + nu: nu, + pure: pure + }; + } +); +define( + 'ephox.katamari.async.Bounce', + + [ + 'global!Array', + 'global!setTimeout' + ], + + function (Array, setTimeout) { + + var bounce = function(f) { + return function() { + var args = Array.prototype.slice.call(arguments); + var me = this; + setTimeout(function() { + f.apply(me, args); + }, 0); + }; + }; + + return { + bounce: bounce + }; + } +); + +define( + 'ephox.katamari.api.Future', + + [ + 'ephox.katamari.api.LazyValue', + 'ephox.katamari.async.Bounce' + ], + + /** A future value that is evaluated on demand. The base function is re-evaluated each time 'get' is called. */ + function (LazyValue, Bounce) { + var nu = function (baseFn) { + var get = function(callback) { + baseFn(Bounce.bounce(callback)); + }; + + /** map :: this Future a -> (a -> b) -> Future b */ + var map = function (fab) { + return nu(function (callback) { + get(function (a) { + var value = fab(a); + callback(value); + }); + }); + }; + + /** bind :: this Future a -> (a -> Future b) -> Future b */ + var bind = function (aFutureB) { + return nu(function (callback) { + get(function (a) { + aFutureB(a).get(callback); + }); + }); + }; + + /** anonBind :: this Future a -> Future b -> Future b + * Returns a future, which evaluates the first future, ignores the result, then evaluates the second. + */ + var anonBind = function (futureB) { + return nu(function (callback) { + get(function (a) { + futureB.get(callback); + }); + }); + }; + + var toLazy = function () { + return LazyValue.nu(get); + }; + + return { + map: map, + bind: bind, + anonBind: anonBind, + toLazy: toLazy, + get: get + }; + + }; + + /** a -> Future a */ + var pure = function (a) { + return nu(function (callback) { + callback(a); + }); + }; + + return { + nu: nu, + pure: pure + }; + } +); + +define( + 'tinymce.themes.mobile.ios.smooth.SmoothAnimation', + + [ + 'ephox.katamari.api.Option', + 'global!clearInterval', + 'global!Math', + 'global!setInterval' + ], + + function (Option, clearInterval, Math, setInterval) { + var adjust = function (value, destination, amount) { + if (Math.abs(value - destination) <= amount) { + return Option.none(); + } else if (value < destination) { + return Option.some(value + amount); + } else { + return Option.some(value - amount); + } + }; + + var create = function () { + var interval = null; + + var animate = function (getCurrent, destination, amount, increment, doFinish, rate) { + var finished = false; + + var finish = function (v) { + finished = true; + doFinish(v); + }; + + clearInterval(interval); + + var abort = function (v) { + clearInterval(interval); + finish(v); + }; + + interval = setInterval(function () { + var value = getCurrent(); + adjust(value, destination, amount).fold(function () { + clearInterval(interval); + finish(destination); + }, function (s) { + increment(s, abort); + if (! finished) { + var newValue = getCurrent(); + // Jump to the end if the increment is no longer working. + if (newValue !== s || Math.abs(newValue - destination) > Math.abs(value - destination)) { + clearInterval(interval); + finish(destination); + } + } + }); + }, rate); + }; + + return { + animate: animate + }; + }; + + return { + create: create, + adjust: adjust + }; + } +); + +define( + 'tinymce.themes.mobile.ios.view.Devices', + + [ + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options' + ], + + function (Option, Options) { + /* + + DEVICE SCREEN AND KEYBOARD SIZES + + iPhone 4 + 320 x 480 + portrait : 297 + landscape : 237 + + iPhone 5 + 320 x 568 + portrait : 297 + landscape : 237 + + iPhone 6 + 375 x 667 + portrait : 302 + landscape : 237 + + iPhone 6 + + 414 x 736 + portrait : 314 + landscape : 238 + + iPad (mini and full) + 768 x 1024 + portrait : 313 + landscape : 398 + + iPad Pro + 1024 x 1366 + portrait : 371 + landscape : 459 + + */ + + var findDevice = function (deviceWidth, deviceHeight) { + var devices = [ + // iPhone 4 class + { width: 320, height: 480, keyboard: { portrait: 300, landscape: 240 } }, + // iPhone 5 class + { width: 320, height: 568, keyboard: { portrait: 300, landscape: 240 } }, + // iPhone 6 class + { width: 375, height: 667, keyboard: { portrait: 305, landscape: 240 } }, + // iPhone 6+ class + { width: 414, height: 736, keyboard: { portrait: 320, landscape: 240 } }, + // iPad class + { width: 768, height: 1024, keyboard: { portrait: 320, landscape: 400 } }, + // iPad pro class + { width: 1024, height: 1366, keyboard: { portrait: 380, landscape: 460 } } + ]; + + return Options.findMap(devices, function (device) { + return deviceWidth <= device.width && deviceHeight <= device.height ? + Option.some(device.keyboard) : + Option.none(); + }).getOr({ portrait: deviceHeight / 5, landscape: deviceWidth / 4 }); + }; + + return { + findDevice: findDevice + }; + } +); +define( + 'tinymce.themes.mobile.ios.view.DeviceZones', + + [ + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.view.Height', + 'tinymce.themes.mobile.ios.view.Devices', + 'tinymce.themes.mobile.touch.view.Orientation' + ], + + function (Css, Traverse, Height, Devices, Orientation) { + // Green zone is the area below the toolbar and above the keyboard, its considered the viewable + // region that is not obstructed by the keyboard. If the keyboard is down, then the Green Zone is larger. + + /* + _______________________ + | toolbar | + |_____________________| + | | + | | + | greenzone | + |_____________________| + | | + | keyboard | + |_____________________| + + */ + + var softKeyboardLimits = function (outerWindow) { + return Devices.findDevice(outerWindow.screen.width, outerWindow.screen.height); + }; + + var accountableKeyboardHeight = function (outerWindow) { + var portrait = Orientation.get(outerWindow).isPortrait(); + var limits = softKeyboardLimits(outerWindow); + + var keyboard = portrait ? limits.portrait : limits.landscape; + + var visualScreenHeight = portrait ? outerWindow.screen.height : outerWindow.screen.width; + + // This is our attempt to detect when we are in a webview. If the DOM window height is smaller than the + // actual screen height by about the size of a keyboard, we assume that's because a keyboard is + // causing it to be that small. We will improve this at a later date. + return (visualScreenHeight - outerWindow.innerHeight) > keyboard ? 0 : keyboard; + }; + + var getGreenzone = function (socket, dropup) { + var outerWindow = Traverse.owner(socket).dom().defaultView; + // Include the dropup for this calculation because it represents the total viewable height. + var viewportHeight = Height.get(socket) + Height.get(dropup); + var acc = accountableKeyboardHeight(outerWindow); + return viewportHeight - acc; + }; + + var updatePadding = function (contentBody, socket, dropup) { + var greenzoneHeight = getGreenzone(socket, dropup); + var deltaHeight = (Height.get(socket) + Height.get(dropup)) - greenzoneHeight; + // TBIO-3878 Changed the element that was receiving the padding from the iframe to the body of the + // iframe's document. The reasoning for this is that the syncHeight function of IosSetup.js relies on + // the scrollHeight of the body to set the height of the iframe itself. If we don't set the + // padding-bottom on the body, the scrollHeight is too short, effectively disappearing the content from view. + Css.set(contentBody, 'padding-bottom', deltaHeight + 'px'); + }; + + return { + getGreenzone: getGreenzone, + updatePadding: updatePadding + }; + } +); +define( + 'tinymce.themes.mobile.ios.view.IosViewport', + + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.view.Height', + 'tinymce.themes.mobile.ios.view.DeviceZones', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.touch.scroll.Scrollable', + 'tinymce.themes.mobile.util.DataAttributes' + ], + + function (Adt, Arr, Fun, Attr, Css, SelectorFilter, Traverse, Height, DeviceZones, Styles, Scrollable, DataAttributes) { + var fixture = Adt.generate([ + { 'fixed': [ 'element', 'property', 'offsetY' ] }, + // Not supporting property yet + { 'scroller' :[ 'element', 'offsetY' ] } + ]); + + var yFixedData = 'data-' + Styles.resolve('position-y-fixed'); + var yFixedProperty = 'data-' + Styles.resolve('y-property'); + var yScrollingData = 'data-' + Styles.resolve('scrolling'); + var windowSizeData = 'data-' + Styles.resolve('last-window-height'); + + var getYFixedData = function (element) { + return DataAttributes.safeParse(element, yFixedData); + }; + + var getYFixedProperty = function (element) { + return Attr.get(element, yFixedProperty); + }; + + var getLastWindowSize = function (element) { + return DataAttributes.safeParse(element, windowSizeData); + }; + + var classifyFixed = function (element, offsetY) { + var prop = getYFixedProperty(element); + return fixture.fixed(element, prop, offsetY); + }; + + var classifyScrolling = function (element, offsetY) { + return fixture.scroller(element, offsetY); + }; + + var classify = function (element) { + var offsetY = getYFixedData(element); + var classifier = Attr.get(element, yScrollingData) === 'true' ? classifyScrolling : classifyFixed; + return classifier(element, offsetY); + }; + + var findFixtures = function (container) { + var candidates = SelectorFilter.descendants(container, '[' + yFixedData + ']'); + return Arr.map(candidates, classify); + }; + + var takeoverToolbar = function (toolbar) { + var oldToolbarStyle = Attr.get(toolbar, 'style'); + Css.setAll(toolbar, { + position: 'absolute', + top: '0px' + }); + + Attr.set(toolbar, yFixedData, '0px'); + Attr.set(toolbar, yFixedProperty, 'top'); + + var restore = function () { + Attr.set(toolbar, 'style', oldToolbarStyle || ''); + Attr.remove(toolbar, yFixedData); + Attr.remove(toolbar, yFixedProperty); + }; + + return { + restore: restore + }; + }; + + var takeoverViewport = function (toolbarHeight, height, viewport) { + var oldViewportStyle = Attr.get(viewport, 'style'); + + Scrollable.register(viewport); + Css.setAll(viewport, { + 'position': 'absolute', + // I think there a class that does this overflow scrolling touch part + 'height': height + 'px', + 'width': '100%', + 'top': toolbarHeight + 'px' + }); + + Attr.set(viewport, yFixedData, toolbarHeight + 'px'); + Attr.set(viewport, yScrollingData, 'true'); + Attr.set(viewport, yFixedProperty, 'top'); + + var restore = function () { + Scrollable.deregister(viewport); + Attr.set(viewport, 'style', oldViewportStyle || ''); + Attr.remove(viewport, yFixedData); + Attr.remove(viewport, yScrollingData); + Attr.remove(viewport, yFixedProperty); + }; + + return { + restore: restore + }; + }; + + var takeoverDropup = function (dropup, toolbarHeight, viewportHeight) { + var oldDropupStyle = Attr.get(dropup, 'style'); + Css.setAll(dropup, { + position: 'absolute', + bottom: '0px' + }); + + Attr.set(dropup, yFixedData, '0px'); + Attr.set(dropup, yFixedProperty, 'bottom'); + + var restore = function () { + Attr.set(dropup, 'style', oldDropupStyle || ''); + Attr.remove(dropup, yFixedData); + Attr.remove(dropup, yFixedProperty); + }; + + return { + restore: restore + }; + }; + + var deriveViewportHeight = function (viewport, toolbarHeight, dropupHeight) { + // Note, Mike thinks this value changes when the URL address bar grows and shrinks. If this value is too high + // the main problem is that scrolling into the greenzone may not scroll into an area that is viewable. Investigate. + var outerWindow = Traverse.owner(viewport).dom().defaultView; + var winH = outerWindow.innerHeight; + Attr.set(viewport, windowSizeData, winH + 'px'); + return winH - toolbarHeight - dropupHeight; + }; + + var takeover = function (viewport, contentBody, toolbar, dropup) { + var outerWindow = Traverse.owner(viewport).dom().defaultView; + var toolbarSetup = takeoverToolbar(toolbar); + var toolbarHeight = Height.get(toolbar); + var dropupHeight = Height.get(dropup); + var viewportHeight = deriveViewportHeight(viewport, toolbarHeight, dropupHeight); + + var viewportSetup = takeoverViewport(toolbarHeight, viewportHeight, viewport); + + var dropupSetup = takeoverDropup(dropup, toolbarHeight, viewportHeight); + + var isActive = true; + + var restore = function () { + isActive = false; + toolbarSetup.restore(); + viewportSetup.restore(); + dropupSetup.restore(); + }; + + var isExpanding = function () { + var currentWinHeight = outerWindow.innerHeight; + var lastWinHeight = getLastWindowSize(viewport); + return currentWinHeight > lastWinHeight; + }; + + var refresh = function () { + if (isActive) { + var newToolbarHeight = Height.get(toolbar); + var dropupHeight = Height.get(dropup); + var newHeight = deriveViewportHeight(viewport, newToolbarHeight, dropupHeight); + Attr.set(viewport, yFixedData, newToolbarHeight + 'px'); + Css.set(viewport, 'height', newHeight + 'px'); + + Css.set(dropup, 'bottom', -(newToolbarHeight + newHeight + dropupHeight) + 'px'); + DeviceZones.updatePadding(contentBody, viewport, dropup); + } + }; + + var setViewportOffset = function (newYOffset) { + var offsetPx = newYOffset + 'px'; + Attr.set(viewport, yFixedData, offsetPx); + // The toolbar height has probably changed, so recalculate the viewport height. + refresh(); + }; + + DeviceZones.updatePadding(contentBody, viewport, dropup); + + return { + setViewportOffset: setViewportOffset, + isExpanding: isExpanding, + isShrinking: Fun.not(isExpanding), + refresh: refresh, + restore: restore + }; + }; + + return { + findFixtures: findFixtures, + takeover: takeover, + getYFixedData: getYFixedData + }; + } +); + +define( + 'tinymce.themes.mobile.ios.scroll.IosScrolling', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Future', + 'ephox.sugar.api.properties.Attr', + 'ephox.sugar.api.properties.Classes', + 'ephox.sugar.api.properties.Css', + 'ephox.sugar.api.search.Traverse', + 'global!Math', + 'tinymce.themes.mobile.ios.smooth.SmoothAnimation', + 'tinymce.themes.mobile.ios.view.IosViewport', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.util.DataAttributes' + ], + + function (Fun, Future, Attr, Classes, Css, Traverse, Math, SmoothAnimation, IosViewport, Styles, DataAttributes) { + var animator = SmoothAnimation.create(); + + var ANIMATION_STEP = 15; + var NUM_TOP_ANIMATION_FRAMES = 10; + var ANIMATION_RATE = 10; + + var lastScroll = 'data-' + Styles.resolve('last-scroll-top'); + + var getTop = function (element) { + var raw = Css.getRaw(element, 'top').getOr(0); + return parseInt(raw, 10); + }; + + var getScrollTop = function (element) { + return parseInt(element.dom().scrollTop, 10); + }; + + var moveScrollAndTop = function (element, destination, finalTop) { + return Future.nu(function (callback) { + var getCurrent = Fun.curry(getScrollTop, element); + + var update = function (newScroll) { + element.dom().scrollTop = newScroll; + Css.set(element, 'top', (getTop(element) + ANIMATION_STEP) + 'px'); + }; + + var finish = function (/* dest */) { + element.dom().scrollTop = destination; + Css.set(element, 'top', finalTop + 'px'); + callback(destination); + }; + + animator.animate(getCurrent, destination, ANIMATION_STEP, update, finish, ANIMATION_RATE); + }); + }; + + var moveOnlyScroll = function (element, destination) { + return Future.nu(function (callback) { + var getCurrent = Fun.curry(getScrollTop, element); + Attr.set(element, lastScroll, getCurrent()); + + var update = function (newScroll, abort) { + var previous = DataAttributes.safeParse(element, lastScroll); + // As soon as we detect a scroll value that we didn't set, assume the user + // is scrolling, and abort the scrolling. + if (previous !== element.dom().scrollTop) { + abort(element.dom().scrollTop); + } else { + element.dom().scrollTop = newScroll; + Attr.set(element, lastScroll, newScroll); + } + }; + + var finish = function (/* dest */) { + element.dom().scrollTop = destination; + Attr.set(element, lastScroll, destination); + callback(destination); + }; + + // Identify the number of steps based on distance (consistent time) + var distance = Math.abs(destination - getCurrent()); + var step = Math.ceil(distance / NUM_TOP_ANIMATION_FRAMES); + animator.animate(getCurrent, destination, step, update, finish, ANIMATION_RATE); + }); + }; + + var moveOnlyTop = function (element, destination) { + return Future.nu(function (callback) { + var getCurrent = Fun.curry(getTop, element); + + var update = function (newTop) { + Css.set(element, 'top', newTop + 'px'); + }; + + var finish = function (/* dest */) { + update(destination); + callback(destination); + }; + + var distance = Math.abs(destination - getCurrent()); + var step = Math.ceil(distance / NUM_TOP_ANIMATION_FRAMES); + animator.animate(getCurrent, destination, step, update, finish, ANIMATION_RATE); + }); + }; + + var updateTop = function (element, amount) { + var newTop = (amount + IosViewport.getYFixedData(element)) + 'px'; + Css.set(element, 'top', newTop); + }; + + // Previously, we moved the window scroll back smoothly with the SmoothAnimation concept. + // However, on tinyMCE, we seemed to get a lot more cursor flickering as the window scroll + // was changing. Therefore, until tests prove otherwise, we are just going to jump to the + // destination in one go. + var moveWindowScroll = function (toolbar, viewport, destY) { + var outerWindow = Traverse.owner(toolbar).dom().defaultView; + return Future.nu(function (callback) { + updateTop(toolbar, destY); + updateTop(viewport, destY); + outerWindow.scrollTo(0, destY); + callback(destY); + }); + }; + + return { + moveScrollAndTop: moveScrollAndTop, + moveOnlyScroll: moveOnlyScroll, + moveOnlyTop: moveOnlyTop, + moveWindowScroll: moveWindowScroll + }; + } +); + +define( + 'tinymce.themes.mobile.ios.smooth.BackgroundActivity', + + [ + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.LazyValue' + ], + + function (Cell, LazyValue) { + return function (doAction) { + // Start the activity in idle state. + var action = Cell( + LazyValue.pure({}) + ); + + var start = function (value) { + var future = LazyValue.nu(function (callback) { + return doAction(value).get(callback); + }); + + // Note: LazyValue kicks off immediately + action.set(future); + }; + + // Idle will fire g once the current action is complete. + var idle = function (g) { + action.get().get(function () { + g(); + }); + }; + + return { + start: start, + idle: idle + }; + }; + } +); + +define( + 'tinymce.themes.mobile.ios.view.Greenzone', + + [ + 'ephox.katamari.api.Fun', + 'global!parseInt', + 'tinymce.themes.mobile.ios.scroll.IosScrolling', + 'tinymce.themes.mobile.ios.view.DeviceZones', + 'tinymce.themes.mobile.touch.focus.CursorRefresh' + ], + + function (Fun, parseInt, IosScrolling, DeviceZones, CursorRefresh) { + var scrollIntoView = function (cWin, socket, dropup, top, bottom) { + var greenzone = DeviceZones.getGreenzone(socket, dropup); + var refreshCursor = Fun.curry(CursorRefresh.refresh, cWin); + + if (top > greenzone || bottom > greenzone) { + IosScrolling.moveOnlyScroll(socket, socket.dom().scrollTop - greenzone + bottom).get(refreshCursor); + } else if (top < 0) { + IosScrolling.moveOnlyScroll(socket, socket.dom().scrollTop + top).get(refreshCursor); + } else { + // do nothing + } + }; + + return { + scrollIntoView: scrollIntoView + }; + } +); +define( + 'ephox.katamari.async.AsyncValues', + + [ + 'ephox.katamari.api.Arr' + ], + + function (Arr) { + /* + * NOTE: an `asyncValue` must have a `get` function which gets given a callback and calls + * that callback with a value once it is ready + * + * e.g + * { + * get: function (callback) { callback(10); } + * } + */ + var par = function (asyncValues, nu) { + return nu(function(callback) { + var r = []; + var count = 0; + + var cb = function(i) { + return function(value) { + r[i] = value; + count++; + if (count >= asyncValues.length) { + callback(r); + } + }; + }; + + if (asyncValues.length === 0) { + callback([]); + } else { + Arr.each(asyncValues, function(asyncValue, i) { + asyncValue.get(cb(i)); + }); + } + }); + }; + + return { + par: par + }; + } +); +define( + 'ephox.katamari.api.Futures', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Future', + 'ephox.katamari.async.AsyncValues' + ], + + function (Arr, Future, AsyncValues) { + /** par :: [Future a] -> Future [a] */ + var par = function(futures) { + return AsyncValues.par(futures, Future.nu); + }; + + /** mapM :: [a] -> (a -> Future b) -> Future [b] */ + var mapM = function(array, fn) { + var futures = Arr.map(array, fn); + return par(futures); + }; + + /** Kleisli composition of two functions: a -> Future b. + * Note the order of arguments: g is invoked first, then the result passed to f. + * This is in line with f . g = \x -> f (g a) + * + * compose :: ((b -> Future c), (a -> Future b)) -> a -> Future c + */ + var compose = function (f, g) { + return function (a) { + return g(a).bind(f); + }; + }; + + return { + par: par, + mapM: mapM, + compose: compose + }; + } +); +define( + 'tinymce.themes.mobile.ios.view.IosUpdates', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Future', + 'ephox.katamari.api.Futures', + 'ephox.sugar.api.properties.Css', + 'tinymce.themes.mobile.ios.scroll.IosScrolling', + 'tinymce.themes.mobile.ios.view.IosViewport' + ], + + function (Arr, Future, Futures, Css, IosScrolling, IosViewport) { + var updateFixed = function (element, property, winY, offsetY) { + var destination = winY + offsetY; + Css.set(element, property, destination + 'px'); + return Future.pure(offsetY); + }; + + var updateScrollingFixed = function (element, winY, offsetY) { + var destTop = winY + offsetY; + var oldProp = Css.getRaw(element, 'top').getOr(offsetY); + // While we are changing top, aim to scroll by the same amount to keep the cursor in the same location. + var delta = destTop - parseInt(oldProp, 10); + var destScroll = element.dom().scrollTop + delta; + return IosScrolling.moveScrollAndTop(element, destScroll, destTop); + }; + + var updateFixture = function (fixture, winY) { + return fixture.fold(function (element, property, offsetY) { + return updateFixed(element, property, winY, offsetY); + }, function (element, offsetY) { + return updateScrollingFixed(element, winY, offsetY); + }); + }; + + var updatePositions = function (container, winY) { + var fixtures = IosViewport.findFixtures(container); + var updates = Arr.map(fixtures, function (fixture) { + return updateFixture(fixture, winY); + }); + return Futures.par(updates); + }; + + return { + updatePositions: updatePositions + }; + } +); + +define( + 'tinymce.themes.mobile.util.CaptureBin', + + [ + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Css' + ], + + function (Focus, Insert, Remove, Element, Css) { + var input = function (parent, operation) { + // to capture focus allowing the keyboard to remain open with no 'real' selection + var input = Element.fromTag('input'); + Css.setAll(input, { + 'opacity': '0', + 'position': 'absolute', + 'top': '-1000px', + 'left': '-1000px' + }); + Insert.append(parent, input); + + Focus.focus(input); + operation(input); + Remove.remove(input); + }; + + return { + input: input + }; + } +); +define( + 'tinymce.themes.mobile.ios.core.IosSetup', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Throttler', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Css', + 'global!clearInterval', + 'global!clearTimeout', + 'global!console', + 'global!Math', + 'global!parseInt', + 'global!setInterval', + 'global!setTimeout', + 'tinymce.themes.mobile.ios.focus.FakeSelection', + 'tinymce.themes.mobile.ios.scroll.IosScrolling', + 'tinymce.themes.mobile.ios.smooth.BackgroundActivity', + 'tinymce.themes.mobile.ios.view.Greenzone', + 'tinymce.themes.mobile.ios.view.IosUpdates', + 'tinymce.themes.mobile.ios.view.IosViewport', + 'tinymce.themes.mobile.touch.view.Orientation', + 'tinymce.themes.mobile.util.CaptureBin', + 'tinymce.themes.mobile.util.Rectangles' + ], + + function ( + Fun, Option, Throttler, Focus, DomEvent, Body, Element, Css, clearInterval, clearTimeout, console, Math, parseInt, setInterval, setTimeout, FakeSelection, + IosScrolling, BackgroundActivity, Greenzone, IosUpdates, IosViewport, Orientation, CaptureBin, Rectangles + ) { + var VIEW_MARGIN = 5; + + var register = function (toolstrip, socket, container, outerWindow, structure, cWin) { + var scroller = BackgroundActivity(function (y) { + return IosScrolling.moveWindowScroll(toolstrip, socket, y); + }); + + // NOTE: This is a WebView specific way of scrolling when out of bounds. When we need to make + // the webapp work again, we'll have to adjust this function. Essentially, it just jumps the scroll + // back to show the current selection rectangle. + var scrollBounds = function () { + var rects = Rectangles.getRectangles(cWin); + return Option.from(rects[0]).bind(function (rect) { + var viewTop = rect.top() - socket.dom().scrollTop; + var outside = viewTop > outerWindow.innerHeight + VIEW_MARGIN || viewTop < -VIEW_MARGIN; + return outside ? Option.some({ + top: Fun.constant(viewTop), + bottom: Fun.constant(viewTop + rect.height()) + }) : Option.none(); + }); + }; + + var scrollThrottle = Throttler.last(function () { + /* + * As soon as the window is back to 0 (idle), scroll the toolbar and socket back into place on scroll. + */ + scroller.idle(function () { + IosUpdates.updatePositions(container, outerWindow.pageYOffset).get(function (/* _ */) { + var extraScroll = scrollBounds(); + extraScroll.each(function (extra) { + // TODO: Smoothly animate this in a way that doesn't conflict with anything else. + socket.dom().scrollTop = socket.dom().scrollTop + extra.top(); + }); + scroller.start(0); + structure.refresh(); + }); + }); + }, 1000); + + var onScroll = DomEvent.bind(Element.fromDom(outerWindow), 'scroll', function () { + if (outerWindow.pageYOffset < 0) { + return; + } + + /* + We've experimented with trying to set the socket scroll (hidden vs. scroll) based on whether the outer body + has scrolled. When the window starts scrolling, we would lock the socket scroll, and we would + unlock it when the window stopped scrolling. This would give a nice rubber-band effect at the end + of the content, but would break the code that tried to position the text in the viewable area + (more details below). Also, as soon as you flicked to outer scroll, if you started scrolling up again, + you would drag the whole window down, because you would still be in outerscroll mode. That's hardly + much of a problem, but it is a minor issue. It also didn't play nicely with keeping the toolbar on the screen. + + The big problem was that this was incompatible with the toolbar and scrolling code. We need a padding inside + the socket so that the bottom of the content can be scrolled into the viewable greenzone. If it doesn't + have padding, then unless we move the socket top to some negative value as well, then we can't get + a scrollTop high enough to get the selection into the viewable greenzone. This is the purpose of the + padding at the bottom of the iframe. Without it, the scroll consistently jumps back to its + max scroll value, and you can't keep the last line on screen when the keyboard is up. + + However, if the padding is too large, then the content can be 'effectively' scrolled off the screen + (the iframe anyway), and the user can get lost about where they are. Our short-term fix is just to + make the padding at the end the height - the greenzone height so that content should always be + visible on the screen, even if they've scrolled to the end. + */ + + scrollThrottle.throttle(); + }); + + IosUpdates.updatePositions(container, outerWindow.pageYOffset).get(Fun.identity); + + return { + unbind: onScroll.unbind + }; + }; + + var setup = function (bag) { + var cWin = bag.cWin(); + var ceBody = bag.ceBody(); + var socket = bag.socket(); + var toolstrip = bag.toolstrip(); + var toolbar = bag.toolbar(); + var contentElement = bag.contentElement(); + var keyboardType = bag.keyboardType(); + var outerWindow = bag.outerWindow(); + var dropup = bag.dropup(); + + var structure = IosViewport.takeover(socket, ceBody, toolstrip, dropup); + var keyboardModel = keyboardType(bag.outerBody(), cWin, Body.body(), contentElement, toolstrip, toolbar); + + var toEditing = function () { + // Consider inlining, though it will make it harder to follow the API + keyboardModel.toEditing(); + clearSelection(); + }; + + var toReading = function () { + keyboardModel.toReading(); + }; + + var onToolbarTouch = function (event) { + keyboardModel.onToolbarTouch(event); + }; + + var onOrientation = Orientation.onChange(outerWindow, { + onChange: Fun.noop, + onReady: structure.refresh + }); + + // NOTE: When the window is resizing (probably due to meta tags and viewport definitions), we are not receiving a window resize event. + // However, it happens shortly after we start Ios mode, so here we just wait for the first window size event that we get. This code + // is also the same code that is used for the Orientation ready event. + onOrientation.onAdjustment(function () { + structure.refresh(); + }); + + var onResize = DomEvent.bind(Element.fromDom(outerWindow), 'resize', function () { + if (structure.isExpanding()) { + structure.refresh(); + } + }); + + var onScroll = register(toolstrip, socket, bag.outerBody(), outerWindow, structure, cWin); + + var unfocusedSelection = FakeSelection(cWin, contentElement); + + var refreshSelection = function () { + if (unfocusedSelection.isActive()) { + unfocusedSelection.update(); + } + }; + + var highlightSelection = function () { + unfocusedSelection.update(); + }; + + var clearSelection = function () { + unfocusedSelection.clear(); + }; + + var scrollIntoView = function (top, bottom) { + Greenzone.scrollIntoView(cWin, socket, dropup, top, bottom); + }; + + var syncHeight = function () { + Css.set(contentElement, 'height', contentElement.dom().contentWindow.document.body.scrollHeight + 'px'); + }; + + var setViewportOffset = function (newYOffset) { + structure.setViewportOffset(newYOffset); + IosScrolling.moveOnlyTop(socket, newYOffset).get(Fun.identity); + }; + + var destroy = function () { + structure.restore(); + onOrientation.destroy(); + onScroll.unbind(); + onResize.unbind(); + keyboardModel.destroy(); + + unfocusedSelection.destroy(); + + // Try and dismiss the keyboard on close, as they have no input focus. + CaptureBin.input(Body.body(), Focus.blur); + }; + + return { + toEditing: toEditing, + toReading: toReading, + onToolbarTouch: onToolbarTouch, + refreshSelection: refreshSelection, + clearSelection: clearSelection, + highlightSelection: highlightSelection, + scrollIntoView: scrollIntoView, + updateToolbarPadding: Fun.noop, + setViewportOffset: setViewportOffset, + syncHeight: syncHeight, + refreshStructure: structure.refresh, + destroy: destroy + }; + }; + + return { + setup: setup + }; + } +); +define( + 'tinymce.themes.mobile.ios.view.IosKeyboard', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.events.DomEvent', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.node.Node', + 'tinymce.themes.mobile.ios.focus.ResumeEditing', + 'tinymce.themes.mobile.util.CaptureBin' + ], + + function (Arr, Fun, Focus, DomEvent, Body, Node, ResumeEditing, CaptureBin) { + /* + * Stubborn IOS Keyboard mode: + * + * The keyboard will stubbornly refuse to go away. The only time it will go away is when toReading + * is called (which is currently only used by the insert image button). It will probably go away + * at other times, but we never explicitly try to make it go away. + * + * The major problem with not dismissing the keyboard when the user presses the toolbar is that + * the input focus can be put in some very interesting things. Once the input focus is in something + * that is not the content or an input that the user can clearly see, behaviour gets very strange + * very quickly. The Stubborn keyboard tries to resolve this issue in a few ways: + * + * 1. After scrolling the toolbar, it resumes editing of the content. This has the built-in assumption + * that there are no slick toolbars that require scrolling AND input focus + * 2. Any time a keydown is received on the outer page, we resume editing of the content. What this means + * is that in situations where the user has still managed to get into the toolbar (e.g. they typed while + * the dropdown was visible, or the insert image toReading didn't quite work etc.), then the first keystroke + * sends the input back to the content, and then subsequent keystrokes appear in the content. Although + * this means that their first keystroke is lost, it is a reasonable way of ensuring that they don't + * get stuck in some weird input somewhere. The goal of the stubborn keyboard is to view this as a + * fallback ... we want to prevent it getting to this state wherever possible. However, there are just + * some situations where we really don't know what typing on the keyboard should do (e.g. a dropdown is open). + * Note, when we transfer the focus back to the content, we also close any menus that are still visible. + * + * Now, because in WebView mode, the actual window is shrunk when the keyboard appears, the dropdown vertical + * scrolling is set to the right height. However, when running as a webapp, this won't be the case. To use + * the stubborn keyboard in webapp mode, we will need to find some way to let repartee know the MaxHeight + * needs to exclude the keyboard. This isn't a problem with timid, because the keyboard is dismissed. + */ + var stubborn = function (outerBody, cWin, page, frame/*, toolstrip, toolbar*/) { + var toEditing = function () { + ResumeEditing.resume(cWin, frame); + }; + + var toReading = function () { + CaptureBin.input(outerBody, Focus.blur); + }; + + var captureInput = DomEvent.bind(page, 'keydown', function (evt) { + // Think about killing the event. + if (! Arr.contains([ 'input', 'textarea' ], Node.name(evt.target()))) { + + // FIX: Close the menus + // closeMenus() + + toEditing(); + } + }); + + var onToolbarTouch = function (/* event */) { + // Do nothing + }; + + var destroy = function () { + captureInput.unbind(); + }; + + return { + toReading: toReading, + toEditing: toEditing, + onToolbarTouch: onToolbarTouch, + destroy: destroy + }; + }; + + /* + * Timid IOS Keyboard mode: + * + * In timid mode, the keyboard will be dismissed as soon as the toolbar is clicked. In lot of + * situations, it will then quickly reappear is toEditing is called. The timid mode is safe, + * but can be very jarring. + * + * One situation that the timid mode does not handle is when in a WebView, if the user has + * scrolled to the bottom of the content and is editing it, as soon as they click on a formatting + * operation, the keyboard will be dismissed, and the content will visibly jump back down to + * the bottom of the screen (because the increased window size has decreased the amount of + * scrolling available). As soon as the formatting operation is completed (which can be + * instantaneously for something like bold), then the keyboard reappears and the content + * jumps again. It's very jarring and there's not much we can do (I think). + * + * However, the timid keyboard mode will seamlessly integrate with dropdowns max-height, because + * dropdowns dismiss the keyboard, so they have all the height they require. + */ + var timid = function (outerBody, cWin, page, frame/*, toolstrip, toolbar*/) { + var dismissKeyboard = function () { + Focus.blur(frame); + }; + + var onToolbarTouch = function () { + dismissKeyboard(); + }; + + var toReading = function () { + dismissKeyboard(); + }; + + var toEditing = function () { + ResumeEditing.resume(cWin, frame); + }; + + return { + toReading: toReading, + toEditing: toEditing, + onToolbarTouch: onToolbarTouch, + destroy: Fun.noop + }; + }; + + return { + stubborn: stubborn, + timid: timid + }; + } +); +define( + 'tinymce.themes.mobile.ios.core.IosMode', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Singleton', + 'ephox.katamari.api.Struct', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.properties.Class', + 'ephox.sugar.api.properties.Css', + 'global!document', + 'tinymce.themes.mobile.ios.core.IosEvents', + 'tinymce.themes.mobile.ios.core.IosSetup', + 'tinymce.themes.mobile.ios.core.PlatformEditor', + 'tinymce.themes.mobile.ios.scroll.Scrollables', + 'tinymce.themes.mobile.ios.view.IosKeyboard', + 'tinymce.themes.mobile.util.Thor', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.touch.scroll.Scrollable', + 'tinymce.themes.mobile.touch.view.MetaViewport' + ], + + function ( + Fun, Singleton, Struct, Focus, Element, Class, Css, document, IosEvents, IosSetup, + PlatformEditor, Scrollables, IosKeyboard, Thor, Styles, Scrollable, MetaViewport + ) { + var create = function (platform, mask) { + var meta = MetaViewport.tag(); + + var priorState = Singleton.value(); + var scrollEvents = Singleton.value(); + + var iosApi = Singleton.api(); + var iosEvents = Singleton.api(); + + var enter = function () { + mask.hide(); + var doc = Element.fromDom(document); + PlatformEditor.getActiveApi(platform.editor).each(function (editorApi) { + // TODO: Orientation changes. + // orientation = Orientation.onChange(); + + priorState.set({ + socketHeight: Css.getRaw(platform.socket, 'height'), + iframeHeight: Css.getRaw(editorApi.frame(), 'height'), + outerScroll: document.body.scrollTop + }); + + scrollEvents.set({ + // Allow only things that have scrollable class to be scrollable. Without this, + // the toolbar scrolling gets prevented + exclusives: Scrollables.exclusive(doc, '.' + Scrollable.scrollable()) + }); + + Class.add(platform.container, Styles.resolve('fullscreen-maximized')); + Thor.clobberStyles(platform.container, editorApi.body()); + meta.maximize(); + + /* NOTE: Making the toolbar scrollable is now done when the middle group is created */ + + Css.set(platform.socket, 'overflow', 'scroll'); + Css.set(platform.socket, '-webkit-overflow-scrolling', 'touch'); + + Focus.focus(editorApi.body()); + + var setupBag = Struct.immutableBag([ + 'cWin', + 'ceBody', + 'socket', + 'toolstrip', + 'toolbar', + 'dropup', + 'contentElement', + 'cursor', + 'keyboardType', + 'isScrolling', + 'outerWindow', + 'outerBody' + ], []); + + iosApi.set( + IosSetup.setup(setupBag({ + 'cWin': editorApi.win(), + 'ceBody': editorApi.body(), + 'socket': platform.socket, + 'toolstrip': platform.toolstrip, + 'toolbar': platform.toolbar, + 'dropup': platform.dropup.element(), + 'contentElement': editorApi.frame(), + 'cursor': Fun.noop, + 'outerBody': platform.body, + 'outerWindow': platform.win, + 'keyboardType': IosKeyboard.stubborn, + 'isScrolling': function () { + return scrollEvents.get().exists(function (s) { + return s.socket.isScrolling(); + }); + } + })) + ); + + iosApi.run(function (api) { + api.syncHeight(); + }); + + + iosEvents.set( + IosEvents.initEvents(editorApi, iosApi, platform.toolstrip, platform.socket, platform.dropup) + ); + }); + }; + + var exit = function () { + meta.restore(); + iosEvents.clear(); + iosApi.clear(); + + mask.show(); + + priorState.on(function (s) { + s.socketHeight.each(function (h) { + Css.set(platform.socket, 'height', h); + }); + s.iframeHeight.each(function (h) { + Css.set(platform.editor.getFrame(), 'height', h); + }); + document.body.scrollTop = s.scrollTop; + }); + priorState.clear(); + + scrollEvents.on(function (s) { + s.exclusives.unbind(); + }); + scrollEvents.clear(); + + Class.remove(platform.container, Styles.resolve('fullscreen-maximized')); + Thor.restoreStyles(); + Scrollable.deregister(platform.toolbar); + + Css.remove(platform.socket, 'overflow'/*, 'scroll'*/); + Css.remove(platform.socket, '-webkit-overflow-scrolling'/*, 'touch'*/); + + // Hide the keyboard and remove the selection so there isn't a blue cursor in the content + // still even once exited. + Focus.blur(platform.editor.getFrame()); + + PlatformEditor.getActiveApi(platform.editor).each(function (editorApi) { + editorApi.clearSelection(); + }); + }; + + // dropup + var refreshStructure = function () { + iosApi.run(function (api) { + api.refreshStructure(); + }); + }; + + return { + enter: enter, + refreshStructure: refreshStructure, + exit: exit + }; + }; + + return { + create: create + }; + } +); + +define( + 'tinymce.themes.mobile.api.IosWebapp', + + [ + 'ephox.alloy.api.component.GuiFactory', + 'ephox.boulder.api.ValueSchema', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.properties.Css', + 'tinymce.themes.mobile.api.MobileSchema', + 'tinymce.themes.mobile.ios.core.IosMode', + 'tinymce.themes.mobile.touch.view.TapToEditMask' + ], + + function (GuiFactory, ValueSchema, Fun, Css, MobileSchema, IosMode, TapToEditMask) { + var produce = function (raw) { + var mobile = ValueSchema.asRawOrDie( + 'Getting IosWebapp schema', + MobileSchema, + raw + ); + + /* Make the toolbar */ + Css.set(mobile.toolstrip, 'width', '100%'); + + Css.set(mobile.container, 'position', 'relative'); + var onView = function () { + mobile.setReadOnly(true); + mode.enter(); + }; + + var mask = GuiFactory.build( + TapToEditMask.sketch(onView, mobile.translate) + ); + + mobile.alloy.add(mask); + var maskApi = { + show: function () { + mobile.alloy.add(mask); + }, + hide: function () { + mobile.alloy.remove(mask); + } + }; + + var mode = IosMode.create(mobile, maskApi); + + return { + setReadOnly: mobile.setReadOnly, + refreshStructure: mode.refreshStructure, + enter: mode.enter, + exit: mode.exit, + destroy: Fun.noop + }; + }; + + return { + produce: produce + }; + } +); + +define( + 'tinymce.themes.mobile.ui.IosRealm', + + [ + 'ephox.alloy.api.behaviour.Replacing', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Singleton', + 'tinymce.themes.mobile.api.IosWebapp', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.toolbar.ScrollingToolbar', + 'tinymce.themes.mobile.ui.CommonRealm', + 'tinymce.themes.mobile.ui.Dropup', + 'tinymce.themes.mobile.ui.OuterContainer' + ], + + function (Replacing, Fun, Singleton, IosWebapp, Styles, ScrollingToolbar, CommonRealm, Dropup, OuterContainer) { + return function (scrollIntoView) { + var alloy = OuterContainer({ + classes: [ Styles.resolve('ios-container') ] + }); + + var toolbar = ScrollingToolbar(); + + var webapp = Singleton.api(); + + var switchToEdit = CommonRealm.makeEditSwitch(webapp); + + var socket = CommonRealm.makeSocket(); + + var dropup = Dropup.build(function () { + webapp.run(function (w) { + w.refreshStructure(); + }); + }, scrollIntoView); + + alloy.add(toolbar.wrapper()); + alloy.add(socket); + alloy.add(dropup.component()); + + var setToolbarGroups = function (rawGroups) { + var groups = toolbar.createGroups(rawGroups); + toolbar.setGroups(groups); + }; + + var setContextToolbar = function (rawGroups) { + var groups = toolbar.createGroups(rawGroups); + toolbar.setContextToolbar(groups); + }; + + var focusToolbar = function () { + toolbar.focus(); + }; + + var restoreToolbar = function () { + toolbar.restoreToolbar(); + }; + + var init = function (spec) { + webapp.set( + IosWebapp.produce(spec) + ); + }; + + var exit = function () { + webapp.run(function (w) { + Replacing.remove(socket, switchToEdit); + w.exit(); + }); + }; + + var updateMode = function (readOnly) { + CommonRealm.updateMode(socket, switchToEdit, readOnly, alloy.root()); + }; + + return { + system: Fun.constant(alloy), + element: alloy.element, + init: init, + exit: exit, + setToolbarGroups: setToolbarGroups, + setContextToolbar: setContextToolbar, + focusToolbar: focusToolbar, + restoreToolbar: restoreToolbar, + updateMode: updateMode, + socket: Fun.constant(socket), + dropup: Fun.constant(dropup) + }; + }; + } +); + +/** + * 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.EditorManager', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.EditorManager'); + } +); + +define( + 'tinymce.themes.mobile.util.CssUrls', + + [ + 'ephox.boulder.api.Objects', + 'tinymce.core.EditorManager' + ], + + function (Objects, EditorManager) { + var derive = function (editor) { + var base = Objects.readOptFrom(editor.settings, 'skin_url').fold(function () { + return EditorManager.baseURL + '/skins/' + 'lightgray'; + }, function (url) { + return url; + }); + + return { + content: base + '/content.mobile.min.css', + ui: base + '/skin.mobile.min.css' + }; + }; + + return { + derive: derive + }; + } +); + +define( + 'tinymce.themes.mobile.util.FormatChangers', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'tinymce.themes.mobile.channels.TinyChannels' + ], + + function (Arr, Fun, Obj, TinyChannels) { + var fontSizes = [ 'x-small', 'small', 'medium', 'large', 'x-large' ]; + + var fireChange = function (realm, command, state) { + realm.system().broadcastOn([ TinyChannels.formatChanged() ], { + command: command, + state: state + }); + }; + + var init = function (realm, editor) { + var allFormats = Obj.keys(editor.formatter.get()); + Arr.each(allFormats, function (command) { + editor.formatter.formatChanged(command, function (state) { + fireChange(realm, command, state); + }); + }); + + Arr.each([ 'ul', 'ol' ], function (command) { + editor.selection.selectorChanged(command, function (state, data) { + fireChange(realm, command, state); + }); + }); + }; + + return { + init: init, + fontSizes: Fun.constant(fontSizes) + }; + } +); + +define( + 'tinymce.themes.mobile.util.SkinLoaded', + + [ + + ], + + function () { + var fireSkinLoaded = function (editor) { + var done = function () { + editor._skinLoaded = true; + editor.fire('SkinLoaded'); + }; + + return function () { + if (editor.initialized) { + done(); + } else { + editor.on('init', done); + } + }; + }; + + return { + fireSkinLoaded: fireSkinLoaded + }; + } +); + +define( + 'tinymce.themes.mobile.Theme', + + [ + 'ephox.alloy.api.behaviour.Swapping', + 'ephox.alloy.api.events.AlloyTriggers', + 'ephox.alloy.api.system.Attachment', + 'ephox.alloy.debugging.Debugging', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.ThemeManager', + 'tinymce.themes.mobile.alien.TinyCodeDupe', + 'tinymce.themes.mobile.channels.TinyChannels', + 'tinymce.themes.mobile.features.Features', + 'tinymce.themes.mobile.style.Styles', + 'tinymce.themes.mobile.touch.view.Orientation', + 'tinymce.themes.mobile.ui.AndroidRealm', + 'tinymce.themes.mobile.ui.Buttons', + 'tinymce.themes.mobile.ui.IosRealm', + 'tinymce.themes.mobile.util.CssUrls', + 'tinymce.themes.mobile.util.FormatChangers', + 'tinymce.themes.mobile.util.SkinLoaded' + ], + + + function ( + Swapping, AlloyTriggers, Attachment, Debugging, Cell, Fun, PlatformDetection, Focus, Insert, Element, Node, DOMUtils, ThemeManager, TinyCodeDupe, TinyChannels, + Features, Styles, Orientation, AndroidRealm, Buttons, IosRealm, CssUrls, FormatChangers, SkinLoaded + ) { + /// not to be confused with editor mode + var READING = Fun.constant('toReading'); /// 'hide the keyboard' + var EDITING = Fun.constant('toEditing'); /// 'show the keyboard' + + ThemeManager.add('mobile', function (editor) { + var renderUI = function (args) { + var cssUrls = CssUrls.derive(editor); + + editor.contentCSS.push(cssUrls.content); + DOMUtils.DOM.styleSheetLoader.load(cssUrls.ui, SkinLoaded.fireSkinLoaded(editor)); + + var doScrollIntoView = function () { + editor.fire('scrollIntoView'); + }; + + var wrapper = Element.fromTag('div'); + var realm = PlatformDetection.detect().os.isAndroid() ? AndroidRealm(doScrollIntoView) : IosRealm(doScrollIntoView); + var original = Element.fromDom(args.targetNode); + Insert.after(original, wrapper); + Attachment.attachSystem(wrapper, realm.system()); + + var findFocusIn = function (elem) { + return Focus.search(elem).bind(function (focused) { + return realm.system().getByDom(focused).toOption(); + }); + }; + var outerWindow = args.targetNode.ownerDocument.defaultView; + var orientation = Orientation.onChange(outerWindow, { + onChange: function () { + var alloy = realm.system(); + alloy.broadcastOn([ TinyChannels.orientationChanged() ], { width: Orientation.getActualWidth(outerWindow) }); + }, + onReady: Fun.noop + }); + + var setReadOnly = function (readOnlyGroups, mainGroups, ro) { + if (ro === false) editor.selection.collapse(); + realm.setToolbarGroups(ro ? readOnlyGroups.get() : mainGroups.get()); + editor.setMode(ro === true ? 'readonly' : 'design'); + editor.fire(ro === true ? READING() : EDITING()); + realm.updateMode(ro); + }; + + var bindHandler = function (label, handler) { + editor.on(label, handler); + return { + unbind: function () { + editor.off(label); + } + }; + }; + + editor.on('init', function () { + realm.init({ + editor: { + getFrame: function () { + return Element.fromDom(editor.contentAreaContainer.querySelector('iframe')); + }, + + onDomChanged: function () { + return { + unbind: Fun.noop + }; + }, + + onToReading: function (handler) { + return bindHandler(READING(), handler); + }, + + onToEditing: function (handler) { + return bindHandler(EDITING(), handler); + }, + + onScrollToCursor: function (handler) { + editor.on('scrollIntoView', function (tinyEvent) { + handler(tinyEvent); + }); + + var unbind = function () { + editor.off('scrollIntoView'); + orientation.destroy(); + }; + + return { + unbind: unbind + }; + }, + + onTouchToolstrip: function () { + hideDropup(); + }, + + onTouchContent: function () { + var toolbar = Element.fromDom(editor.editorContainer.querySelector('.' + Styles.resolve('toolbar'))); + // If something in the toolbar had focus, fire an execute on it (execute on tap away) + // Perhaps it will be clearer later what is a better way of doing this. + findFocusIn(toolbar).each(AlloyTriggers.emitExecute); + realm.restoreToolbar(); + hideDropup(); + }, + + onTapContent: function (evt) { + var target = evt.target(); + // If the user has tapped (touchstart, touchend without movement) on an image, select it. + if (Node.name(target) === 'img') { + editor.selection.select(target.dom()); + // Prevent the default behaviour from firing so that the image stays selected + evt.kill(); + } else if (Node.name(target) === 'a') { + var component = realm.system().getByDom(Element.fromDom(editor.editorContainer)); + component.each(function (container) { + /// view mode + if (Swapping.isAlpha(container)) { + TinyCodeDupe.openLink(target.dom()); + } + }); + } + } + }, + container: Element.fromDom(editor.editorContainer), + socket: Element.fromDom(editor.contentAreaContainer), + toolstrip: Element.fromDom(editor.editorContainer.querySelector('.' + Styles.resolve('toolstrip'))), + toolbar: Element.fromDom(editor.editorContainer.querySelector('.' + Styles.resolve('toolbar'))), + dropup: realm.dropup(), + alloy: realm.system(), + translate: Fun.noop, + + setReadOnly: function (ro) { + setReadOnly(readOnlyGroups, mainGroups, ro); + } + }); + + var hideDropup = function () { + realm.dropup().disappear(function () { + realm.system().broadcastOn([ TinyChannels.dropupDismissed() ], { }); + }); + }; + + Debugging.registerInspector('remove this', realm.system()); + + var backToMaskGroup = { + label: 'The first group', + scrollable: false, + items: [ + Buttons.forToolbar('back', function (/* btn */) { + editor.selection.collapse(); + realm.exit(); + }, { }) + ] + }; + + var backToReadOnlyGroup = { + label: 'Back to read only', + scrollable: false, + items: [ + Buttons.forToolbar('readonly-back', function (/* btn */) { + setReadOnly(readOnlyGroups, mainGroups, true); + }, {}) + ] + }; + + var readOnlyGroup = { + label: 'The read only mode group', + scrollable: true, + items: [] + }; + + var features = Features.setup(realm, editor); + var items = Features.detect(editor.settings, features); + + var actionGroup = { + label: 'the action group', + scrollable: true, + items: items + }; + + var extraGroup = { + label: 'The extra group', + scrollable: false, + items: [ + // This is where the "add button" button goes. + ] + }; + + var mainGroups = Cell([ backToReadOnlyGroup, actionGroup, extraGroup ]); + var readOnlyGroups = Cell([ backToMaskGroup, readOnlyGroup, extraGroup ]); + + // Investigate ways to keep in sync with the ui + FormatChangers.init(realm, editor); + }); + + return { + iframeContainer: realm.socket().element().dom(), + editorContainer: realm.element().dom() + }; + }; + + return { + getNotificationManagerImpl: function () { + return { + open: Fun.identity, + close: Fun.noop, + reposition: Fun.noop, + getArgs: Fun.identity + }; + }, + renderUI: renderUI + }; + }); + + return function () { }; + + } +); + +dem('tinymce.themes.mobile.Theme')(); +})(); diff --git a/media/vendor/tinymce/themes/mobile/theme.min.js b/media/vendor/tinymce/themes/mobile/theme.min.js new file mode 100644 index 0000000000000..cbfa1609fb8cd --- /dev/null +++ b/media/vendor/tinymce/themes/mobile/theme.min.js @@ -0,0 +1,8 @@ +!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-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e1)throw c.error("HTML does not have a single root node",a),"HTML must have a single root node";return h(f.childNodes[0])},f=function(a,b){var c=b||d,e=c.createElement(a);return h(e)},g=function(a,b){var c=b||d,e=c.createTextNode(a);return h(e)},h=function(c){if(null===c||void 0===c)throw new b("Node cannot be null or undefined");return{dom:a.constant(c)}};return{fromHtml:e,fromTag:f,fromText:g,fromDom:h}}),g("1c",[],function(){return{ATTRIBUTE:2,CDATA_SECTION:4,COMMENT:8,DOCUMENT:9,DOCUMENT_TYPE:10,DOCUMENT_FRAGMENT:11,ELEMENT:1,TEXT:3,PROCESSING_INSTRUCTION:7,ENTITY_REFERENCE:5,ENTITY:6,NOTATION:12}}),g("2q",["x","y","a","1c","u","1a"],function(a,b,c,d,e,f){var g=0,h=1,i=2,j=3,k=function(){var a=f.createElement("span");return void 0!==a.matches?g:void 0!==a.msMatchesSelector?h:void 0!==a.webkitMatchesSelector?i:void 0!==a.mozMatchesSelector?j:-1}(),l=d.ELEMENT,m=d.DOCUMENT,n=function(a,b){var c=a.dom();if(c.nodeType!==l)return!1;if(k===g)return c.matches(b);if(k===h)return c.msMatchesSelector(b);if(k===i)return c.webkitMatchesSelector(b);if(k===j)return c.mozMatchesSelector(b);throw new e("Browser lacks native selectors")},o=function(a){return a.nodeType!==l&&a.nodeType!==m||0===a.childElementCount},p=function(b,d){var e=void 0===d?f:d.dom();return o(e)?[]:a.map(e.querySelectorAll(b),c.fromDom)},q=function(a,d){var e=void 0===d?f:d.dom();return o(e)?b.none():b.from(e.querySelector(a)).map(c.fromDom)};return{all:p,is:n,one:q}}),g("19",["x","6","2p","7","2q"],function(a,b,c,d,e){var f=function(a,b){return a.dom()===b.dom()},g=function(a,b){return a.dom().isEqualNode(b.dom())},h=function(c,d){return a.exists(d,b.curry(f,c))},i=function(a,b){var c=a.dom(),d=b.dom();return c!==d&&c.contains(d)},j=function(a,b){return c.documentPositionContainedBy(a.dom(),b.dom())},k=d.detect().browser,l=k.isIE()?j:i;return{eq:f,isEqualNode:g,member:h,contains:l,is:e.is}}),g("42",["19"],function(a){var b=function(b,c){return a.eq(b.element(),c.event().target())};return{isSource:b}}),g("2f",["6"],function(a){return{contextmenu:a.constant("contextmenu"),touchstart:a.constant("touchstart"),touchmove:a.constant("touchmove"),touchend:a.constant("touchend"),gesturestart:a.constant("gesturestart"),mousedown:a.constant("mousedown"),mousemove:a.constant("mousemove"),mouseout:a.constant("mouseout"),mouseup:a.constant("mouseup"),mouseover:a.constant("mouseover"),focusin:a.constant("focusin"),keydown:a.constant("keydown"),input:a.constant("input"),change:a.constant("change"),focus:a.constant("focus"),click:a.constant("click"),transitionend:a.constant("transitionend"),selectstart:a.constant("selectstart")}}),g("s",["2f","6","7"],function(a,b,c){var d={tap:b.constant("alloy.tap")};return{focus:b.constant("alloy.focus"),postBlur:b.constant("alloy.blur.post"),receive:b.constant("alloy.receive"),execute:b.constant("alloy.execute"),focusItem:b.constant("alloy.focus.item"),tap:d.tap,tapOrClick:c.detect().deviceType.isTouch()?d.tap:a.click,longpress:b.constant("alloy.longpress"),sandboxClose:b.constant("alloy.sandbox.close"),systemInit:b.constant("alloy.system.init"),windowScroll:b.constant("alloy.system.scroll"),attachedToDom:b.constant("alloy.system.attached"),detachedFromDom:b.constant("alloy.system.detached"),changeTab:b.constant("alloy.change.tab"),dismissTab:b.constant("alloy.dismiss.tab")}}),g("1h",["t","2l"],function(a,b){var c=function(c){if(null===c)return"null";var d=typeof c;return"object"===d&&a.prototype.isPrototypeOf(c)?"array":"object"===d&&b.prototype.isPrototypeOf(c)?"string":d},d=function(a){return function(b){return c(b)===a}};return{isString:d("string"),isObject:d("object"),isArray:d("array"),isNull:d("null"),isBoolean:d("boolean"),isUndefined:d("undefined"),isFunction:d("function"),isNumber:d("number")}}),g("v",["1h","t","u"],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;e0?g(c.errors):f(c.values,b)},i=function(a){var b=e.partition(a);return b.errors.length>0?g(b.errors):d.value(b.values)};return{consolidateObj:h,consolidateArr:i}}),g("2a",["x","w"],function(a,b){var c=function(b,c){var d={};return a.each(c,function(a){void 0!==b[a]&&b.hasOwnProperty(a)&&(d[a]=b[a])}),d},d=function(b,c){var d={};return a.each(b,function(a){var b=a[c];d[b]=a}),d},e=function(c,d){var e={};return b.each(c,function(b,c){a.contains(d,c)||(e[c]=b)}),e};return{narrow:c,exclude:e,indexOnKey:d}}),g("2b",["y"],function(a){var b=function(b){return function(c){return c.hasOwnProperty(b)?a.from(c[b]):a.none()}},c=function(a,c){return function(d){return b(a)(d).getOr(c)}},d=function(a,c){return b(c)(a)},e=function(a,b){return a.hasOwnProperty(b)&&void 0!==a[b]&&null!==a[b]};return{readOpt:b,readOr:c,readOptFrom:d,hasKey:e}}),g("2c",["x"],function(a){var b=function(a,b){var c={};return c[a]=b,c},c=function(b){var c={};return a.each(b,function(a){c[a.key]=a.value}),c};return{wrap:b,wrapAll:c}}),g("13",["29","2a","2b","2c"],function(a,b,c,d){var e=function(a,c){return b.narrow(a,c)},f=function(a,c){return b.exclude(a,c)},g=function(a){return c.readOpt(a)},h=function(a,b){return c.readOr(a,b)},i=function(a,b){return c.readOptFrom(a,b)},j=function(a,b){return d.wrap(a,b)},k=function(a){return d.wrapAll(a)},l=function(a,c){return b.indexOnKey(a,c)},m=function(b,c){return a.consolidateObj(b,c)},n=function(a,b){return c.hasKey(a,b)};return{narrow:e,exclude:f,readOpt:g,readOr:h,readOptFrom:i,wrap:j,wrapAll:k,indexOnKey:l,hasKey:n,consolidate:m}}),g("6n",["4l"],function(a){var b=function(){return a.getOrDie("JSON")},c=function(a){return b().parse(a)},d=function(a,c,d){return b().stringify(a,c,d)};return{parse:c,stringify:d}}),g("4e",["x","w","1h","6n"],function(a,b,c,d){var e=function(a){return c.isObject(a)&&b.keys(a).length>100?" removed due to size":d.stringify(a,null,2)},f=function(b){var c=b.length>10?b.slice(0,10).concat([{path:[],getErrorInfo:function(){return"... (only showing first ten failures)"}}]):b;return a.map(c,function(a){return"Failed path: ("+a.path.join(" > ")+")\n"+a.getErrorInfo()})};return{formatObj:e,formatErrors:f}}),g("6l",["4e","47"],function(a,b){var c=function(a,c){return b.error([{path:a,getErrorInfo:c}])},d=function(b,d,e){return c(b,function(){return'Could not find valid *strict* value for "'+d+'" in '+a.formatObj(e)})},e=function(a,b){return c(a,function(){return'Choice schema did not contain choice key: "'+b+'"'})},f=function(b,d,e){return c(b,function(){return'The chosen schema: "'+e+'" did not exist in branches: '+a.formatObj(d)})},g=function(a,b){return c(a,function(){return"There are unsupported fields: ["+b.join(", ")+"] specified"})},h=function(a,b){return c(a,function(){return b})},i=function(a){return"Failed path: ("+a.path.join(" > ")+")\n"+a.getErrorInfo()};return{missingStrict:d,missingKey:e,missingBranch:f,unsupportedFields:g,custom:h,toString:i}}),g("6m",["6k"],function(a){var b=a.generate([{setOf:["validator","valueType"]},{arrOf:["valueType"]},{objOf:["fields"]},{itemOf:["validator"]},{choiceOf:["key","branches"]}]),c=a.generate([{field:["name","presence","type"]},{state:["name"]}]);return{typeAdt:b,fieldAdt:c}}),g("4c",["4b","13","29","2b","2c","6l","6m","6k","x","6","v","w","y","47","1h"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p=h.generate([{field:["key","okey","presence","prop"]},{state:["okey","instantiator"]}]),q=function(a,b){return p.state(a,j.constant(b))},r=function(a){return p.state(a,j.identity)},s=function(a,b,c){return d.readOptFrom(b,c).fold(function(){return f.missingStrict(a,c,b)},n.value)},t=function(a,b,c){var e=d.readOptFrom(a,b).fold(function(){return c(a)},j.identity);return n.value(e)},u=function(a,b){return n.value(d.readOptFrom(a,b))},v=function(a,b,c){var e=d.readOptFrom(a,b).map(function(b){return b===!0?c(a):b});return n.value(e)},w=function(a,b,c,d){return c.fold(function(c,f,g,h){var i=function(b){return h.extract(a.concat([c]),d,b).map(function(a){return e.wrap(f,d(a))})},l=function(b){return b.fold(function(){var a=e.wrap(f,d(m.none()));return n.value(a)},function(b){return h.extract(a.concat([c]),d,b).map(function(a){return e.wrap(f,d(m.some(a)))})})};return function(){return g.fold(function(){return s(a,b,c).bind(i)},function(a){return t(b,c,a).bind(i)},function(){return u(b,c).bind(l)},function(a){return v(b,c,a).bind(l)},function(a){var d=a(b);return t(b,c,j.constant({})).map(function(a){return k.deepMerge(d,a)}).bind(i)})}()},function(a,c){var f=c(b);return n.value(e.wrap(a,d(f)))})},x=function(a,b,d,e){var f=i.map(d,function(c){return w(a,b,c,e)});return c.consolidateObj(f,{})},y=function(a){var b=function(b,c,d){return a(d).fold(function(a){return f.custom(b,a)},n.value)},c=function(){return"val"},d=function(){return g.typeAdt.itemOf(a)};return{extract:b,toString:c,toDsl:d}},z=function(a){var c=l.keys(a);return i.filter(c,function(c){return b.hasKey(a,c)})},A=function(a){var c=B(a),d=i.foldr(a,function(a,c){return c.fold(function(c){return k.deepMerge(a,b.wrap(c,!0))},j.constant(a))},{}),e=function(a,e,g){var h=o.isBoolean(g)?[]:z(g),j=i.filter(h,function(a){return!b.hasKey(d,a)});return 0===j.length?c.extract(a,e,g):f.unsupportedFields(a,j)};return{extract:e,toString:c.toString,toDsl:c.toDsl}},B=function(a){var b=function(b,c,d){return x(b,d,a,c)},c=function(){var b=i.map(a,function(a){return a.fold(function(a,b,c,d){return a+" -> "+d.toString()},function(a,b){return"state("+a+")"})});return"obj{\n"+b.join("\n")+"}"},d=function(){return g.typeAdt.objOf(i.map(a,function(a){return a.fold(function(a,b,c,d){return g.fieldAdt.field(a,c,d)},function(a,b){return g.fieldAdt.state(a)})}))};return{extract:b,toString:c,toDsl:d}},C=function(a){var b=function(b,d,e){var f=i.map(e,function(c,e){return a.extract(b.concat(["["+e+"]"]),d,c)});return c.consolidateArr(f)},d=function(){return"array("+a.toString()+")"},e=function(){return g.typeAdt.arrOf(a)};return{extract:b,toString:d,toDsl:e}},D=function(b,c){var d=function(a,c){return C(y(b)).extract(a,j.identity,c)},e=function(b,e,f){var g=l.keys(f);return d(b,g).bind(function(d){var g=i.map(d,function(b){return p.field(b,b,a.strict(),c)});return B(g).extract(b,e,f)})},f=function(){return"setOf("+c.toString()+")"},h=function(){return g.typeAdt.setOf(b,c)};return{extract:e,toString:f,toDsl:h}},E=y(n.value),F=j.compose(C,B);return{anyValue:j.constant(E),value:y,obj:B,objOnly:A,arr:C,setOf:D,arrOfObj:F,state:p.state,field:p.field,output:q,snapshot:r}}),g("28",["4b","4c","47","1h"],function(a,b,c,d){var e=function(c){return b.field(c,c,a.strict(),b.anyValue())},f=function(c,d){return b.field(c,c,a.strict(),d)},g=function(e){return b.field(e,e,a.strict(),b.value(function(a){return d.isFunction(a)?c.value(a):c.error("Not a function")}))},h=function(d,e){return b.field(d,d,a.asOption(),b.value(function(a){return c.error("The field: "+d+" is forbidden. "+e)}))},i=function(a,b){return f(a,b)},j=function(c,d){return b.field(c,c,a.strict(),b.obj(d))},k=function(c,d){return b.field(c,c,a.strict(),b.arrOfObj(d))},l=function(c){return b.field(c,c,a.asOption(),b.anyValue())},m=function(c,d){return b.field(c,c,a.asOption(),d)},n=function(c,d){return b.field(c,c,a.asOption(),b.obj(d))},o=function(c,d){return b.field(c,c,a.asOption(),b.objOnly(d))},p=function(c,d){return b.field(c,c,a.defaulted(d),b.anyValue())},q=function(c,d,e){return b.field(c,c,a.defaulted(d),e)},r=function(c,d,e){return b.field(c,c,a.defaulted(d),b.obj(e))},s=function(a,c,d,e){return b.field(a,c,d,e)},t=function(a,c){return b.state(a,c)};return{strict:e,strictOf:f,strictObjOf:j,strictArrayOf:i,strictArrayOfObj:k,strictFunction:g,forbid:h,option:l,optionOf:m,optionObjOf:n,optionObjOfOnly:o,defaulted:p,defaultedOf:q,defaultedObjOf:r,field:s,state:t}}),g("4d",["13","6l","4c","6m","w"],function(a,b,c,d,e){var f=function(d,e,f,g,h){var i=a.readOptFrom(g,h);return i.fold(function(){return b.missingBranch(d,g,h)},function(a){return c.obj(a).extract(d.concat(["branch: "+h]),e,f)})},g=function(c,g){var h=function(d,e,h){var i=a.readOptFrom(h,c);return i.fold(function(){return b.missingKey(d,c)},function(a){return f(d,e,h,g,a)})},i=function(){return"chooseOn("+c+"). Possible values: "+e.keys(g)},j=function(){return d.typeAdt.choiceOf(c,g)};return{extract:h,toString:i,toDsl:j}};return{choose:g}}),g("2d",["4d","4c","4e","6","47","u"],function(a,b,c,d,e,f){var g=b.value(e.value),h=function(a){return b.arrOfObj(a)},i=function(){return b.arr(g)},j=b.arr,k=b.obj,l=b.objOnly,m=b.setOf,n=function(a){return b.value(a)},o=function(a,b,c,d){return b.extract([a],c,d).fold(function(a){return e.error({input:d,errors:a})},e.value)},p=function(a,b,c){return o(a,b,d.constant,c)},q=function(a,b,c){return o(a,b,d.identity,c)},r=function(a){return a.fold(function(a){throw new f(u(a))},d.identity)},s=function(a,b,c){return r(q(a,b,c))},t=function(a,b,c){return r(p(a,b,c))},u=function(a){return"Errors: \n"+c.formatErrors(a.errors)+"\n\nInput object: "+c.formatObj(a.input)},v=function(b,c){return a.choose(b,c)};return{anyValue:d.constant(g),arrOfObj:h,arrOf:j,arrOfVal:i,valueOf:n,setOf:m,objOf:k,objOfOnly:l,asStruct:p,asRaw:q,asStructOrDie:t,asRawOrDie:s,getOrDie:r,formatError:u,choose:v}}),g("46",["28","13","2d","1h","x","6n","6","t","u"],function(a,b,c,d,e,f,g,h,i){var j=function(d){if(!b.hasKey(d,"can")&&!b.hasKey(d,"abort")&&!b.hasKey(d,"run"))throw new i("EventHandler defined by: "+f.stringify(d,null,2)+" does not have can, abort, or run!");return c.asRawOrDie("Extracting event.handler",c.objOfOnly([a.defaulted("can",g.constant(!0)),a.defaulted("abort",g.constant(!1)),a.defaulted("run",g.noop)]),d)},k=function(a,b){return function(){var c=h.prototype.slice.call(arguments,0);return e.foldl(a,function(a,d){return a&&b(d).apply(void 0,c)},!0)}},l=function(a,b){return function(){var c=h.prototype.slice.call(arguments,0); +return e.foldl(a,function(a,d){return a||b(d).apply(void 0,c)},!1)}},m=function(a){return d.isFunction(a)?{can:g.constant(!0),abort:g.constant(!1),run:a}:a},n=function(a){var b=k(a,function(a){return a.can}),c=l(a,function(a){return a.abort}),d=function(){var b=h.prototype.slice.call(arguments,0);e.each(a,function(a){a.run.apply(void 0,b)})};return j({can:b,abort:c,run:d})};return{read:m,fuse:n,nu:j}}),g("3c",["42","2","s","46","13"],function(a,b,c,d,e){var f=e.wrapAll,g=function(a,b){return{key:a,value:d.nu({abort:b})}},h=function(a,b){return{key:a,value:d.nu({can:b})}},i=function(a){return{key:a,value:d.nu({run:function(a,b){b.event().prevent()}})}},j=function(a,b){return{key:a,value:d.nu({run:b})}},k=function(a,b,c){return{key:a,value:d.nu({run:function(a){b.apply(void 0,[a].concat(c))}})}},l=function(a){return function(b){return j(a,b)}},m=function(b){return function(c){return{key:b,value:d.nu({run:function(b,d){a.isSource(b,d)&&c(b,d)}})}}},n=function(a,c){return j(a,function(d,e){d.getSystem().getByUid(c).each(function(c){b.dispatchEvent(c,c.element(),a,e)})})},o=function(a,b,c){var d=b.partUids()[c];return n(a,d)},p=function(a,b){return j(a,function(a,c){a.getSystem().getByDom(c.event().target()).each(function(d){b(a,d,c)})})},q=function(a){return j(a,function(a,b){b.cut()})},r=function(a){return j(a,function(a,b){b.stop()})};return{derive:f,run:j,preventDefault:i,runActionExtra:k,runOnAttached:m(c.attachedToDom()),runOnDetached:m(c.detachedFromDom()),runOnInit:m(c.systemInit()),runOnExecute:l(c.execute()),redirectToUid:n,redirectToPart:o,runWithTarget:p,abort:g,can:h,cutter:q,stopper:r}}),g("49",["y"],function(a){var b=function(a,b,c){return a},c=function(a,b){return a},d=function(a,b){return a},e=a.none;return{markAsBehaviourApi:b,markAsExtraApi:c,markAsSketchApi:d,getAnnotation:e}}),g("4j",["x","6","t","u"],function(a,b,c,d){return function(){var e=arguments;return function(){for(var f=new c(arguments.length),g=0;g0&&e.unsuppMessage(m);var n={};return a.each(h,function(a){n[a]=b.constant(f[a])}),a.each(i,function(a){n[a]=b.constant(g.prototype.hasOwnProperty.call(f,a)?d.some(f[a]):d.none())}),n}}}),g("2n",["4j","4k"],function(a,b){return{immutable:a,immutableBag:b}}),g("6o",["6n","2n","2l"],function(a,b,c){var d=b.immutableBag(["tag"],["classes","attributes","styles","value","innerHtml","domChildren","defChildren"]),e=function(b){var c=f(b);return a.stringify(c,null,2)},f=function(a){return{tag:a.tag(),classes:a.classes().getOr([]),attributes:a.attributes().getOr({}),styles:a.styles().getOr({}),value:a.value().getOr(""),innerHtml:a.innerHtml().getOr(""),defChildren:a.defChildren().getOr(""),domChildren:a.domChildren().fold(function(){return""},function(a){return 0===a.length?"0 children, but still specified":c(a.length)})}};return{nu:d,defToStr:e,defToRaw:f}}),g("4a",["6o","13","x","w","v","6n","2n"],function(a,b,c,d,e,f,g){var h=["classes","attributes","styles","value","innerHtml","defChildren","domChildren"],i=g.immutableBag([],h),j=function(a){var b={},e=d.keys(a);return c.each(e,function(c){a[c].each(function(a){b[c]=a})}),i(b)},k=function(a){var b=l(a);return f.stringify(b,null,2)},l=function(a){return{classes:a.classes().getOr(""),attributes:a.attributes().getOr(""),styles:a.styles().getOr(""),value:a.value().getOr(""),innerHtml:a.innerHtml().getOr(""),defChildren:a.defChildren().getOr(""),domChildren:a.domChildren().fold(function(){return""},function(a){return 0===a.length?"0 children, but still specified":String(a.length)})}},m=function(a,c,d){return c.fold(function(){return d.fold(function(){return{}},function(c){return b.wrap(a,c)})},function(c){return d.fold(function(){return b.wrap(a,c)},function(c){return b.wrap(a,c)})})},n=function(c,d){var f=e.deepMerge({tag:c.tag(),classes:d.classes().getOr([]).concat(c.classes().getOr([])),attributes:e.merge(c.attributes().getOr({}),d.attributes().getOr({})),styles:e.merge(c.styles().getOr({}),d.styles().getOr({}))},d.innerHtml().or(c.innerHtml()).map(function(a){return b.wrap("innerHtml",a)}).getOr({}),m("domChildren",d.domChildren(),c.domChildren()),m("defChildren",d.defChildren(),c.defChildren()),d.value().or(c.value()).map(function(a){return b.wrap("value",a)}).getOr({}));return a.nu(f)};return{nu:i,derive:j,merge:n,modToStr:k,modToRaw:l}}),g("26",["3c","49","4a","28","13","2d","6","v","w","y","16","t","15","u"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){var o=function(b,c,d){return a.runOnExecute(function(a){d(a,b,c)})},p=function(b,c,d){return a.runOnInit(function(a,e){d(a,b,c)})},q=function(a,b,c,e,g,h){var i=f.objOfOnly(a),j=d.optionObjOf(b,[d.optionObjOfOnly("config",a)]);return u(i,j,b,c,e,g,h)},r=function(a,b,c,e,f,g){var h=a,i=d.optionObjOf(b,[d.optionOf("config",a)]);return u(h,i,b,c,e,f,g)},s=function(a,c,d){var e=function(b){var e=arguments;return b.config({name:g.constant(a)}).fold(function(){throw new n("We could not find any behaviour configuration for: "+a+". Using API: "+d)},function(a){var d=l.prototype.slice.call(e,1);return c.apply(void 0,[b,a.config,a.state].concat(d))})};return b.markAsBehaviourApi(e,d,c)},t=function(a){return{key:a,value:void 0}},u=function(a,d,l,m,n,o,p){var q=function(a){return e.hasKey(a,l)?a[l]():j.none()},r=i.map(n,function(a,b){return s(l,a,b)}),u=i.map(o,function(a,c){return b.markAsExtraApi(a,c)}),v=h.deepMerge(u,r,{revoke:g.curry(t,l),config:function(b){var c=f.asStructOrDie(l+"-config",a,b);return{key:l,value:{config:c,me:v,configAsRaw:k.cached(function(){return f.asRawOrDie(l+"-config",a,b)}),initialConfig:b,state:p}}},schema:function(){return d},exhibit:function(a,b){return q(a).bind(function(a){return e.readOptFrom(m,"exhibit").map(function(c){return c(b,a.config,a.state)})}).getOr(c.nu({}))},name:function(){return l},handlers:function(a){return q(a).bind(function(a){return e.readOptFrom(m,"events").map(function(b){return b(a.config,a.state)})}).getOr({})}});return v};return{executeEvent:o,loadEvent:p,create:q,createModes:r}}),g("6q",["x","6","w","1h","6p","u"],function(a,b,c,d,e,f){var g=function(a,b){return h(a,b,{validate:d.isFunction,label:"function"})},h=function(b,d,g){if(0===d.length)throw new f("You must specify at least one required field.");return e.validateStrArr("required",d),e.checkDupes(d),function(f){var h=c.keys(f),i=a.forall(d,function(b){return a.contains(h,b)});i||e.reqMessage(d,h),b(d,h);var j=a.filter(d,function(a){return!g.validate(f[a],a)});return j.length>0&&e.invalidTypeMessage(j,g.label),f}},i=function(b,c){var d=a.filter(c,function(c){return!a.contains(b,c)});d.length>0&&e.unsuppMessage(d)},j=b.noop;return{exactly:b.curry(g,i),ensure:b.curry(g,j),ensureWith:b.curry(h,j)}}),g("4f",["6q"],function(a){return a.ensure(["readState"])}),h("1v",Math),g("27",["4f","1v"],function(a,b){var c=function(){return a({readState:function(){return"No State required"}})};return{init:c}}),g("p",["26","27","28","13","2d","6"],function(a,b,c,d,e,f){var g=function(a){return d.wrapAll(a)},h=e.objOfOnly([c.strict("fields"),c.strict("name"),c.defaulted("active",{}),c.defaulted("apis",{}),c.defaulted("extra",{}),c.defaulted("state",b)]),i=function(b){var c=e.asRawOrDie("Creating behaviour: "+b.name,h,b);return a.create(c.fields,c.name,c.active,c.apis,c.extra,c.state)},j=e.objOfOnly([c.strict("branchKey"),c.strict("branches"),c.strict("name"),c.defaulted("active",{}),c.defaulted("apis",{}),c.defaulted("extra",{}),c.defaulted("state",b)]),k=function(b){var c=e.asRawOrDie("Creating behaviour: "+b.name,j,b);return a.createModes(e.choose(c.branchKey,c.branches),c.name,c.active,c.apis,c.extra,c.state)};return{derive:g,revoke:f.constant(void 0),noActive:f.constant({}),noApis:f.constant({}),noExtra:f.constant({}),noState:f.constant(b),create:i,createModes:k}}),g("4g",[],function(){return function(a,b,c){var d=c||!1,e=function(){b(),d=!0},f=function(){a(),d=!1},g=function(){var a=d?f:e;a()},h=function(){return d};return{on:e,off:f,toggle:g,isOn:h}}}),g("b",["1c"],function(a){var b=function(a){var b=a.dom().nodeName;return b.toLowerCase()},c=function(a){return a.dom().nodeType},d=function(a){return a.dom().nodeValue},e=function(a){return function(b){return c(b)===a}},f=function(d){return c(d)===a.COMMENT||"#comment"===b(d)},g=e(a.ELEMENT),h=e(a.TEXT),i=e(a.DOCUMENT);return{name:b,type:c,value:d,isElement:g,isText:h,isDocument:i,isComment:f}}),g("4h",["1h","x","w","b","u","15"],function(a,b,c,d,e,f){var g=function(b,c,d){if(!(a.isString(d)||a.isBoolean(d)||a.isNumber(d)))throw f.error("Invalid call to Attr.set. Key ",c,":: Value ",d,":: Element ",b),new e("Attribute value was not simple");b.setAttribute(c,d+"")},h=function(a,b,c){g(a.dom(),b,c)},i=function(a,b){var d=a.dom();c.each(b,function(a,b){g(d,b,a)})},j=function(a,b){var c=a.dom().getAttribute(b);return null===c?void 0:c},k=function(a,b){var c=a.dom();return!(!c||!c.hasAttribute)&&c.hasAttribute(b)},l=function(a,b){a.dom().removeAttribute(b)},m=function(a){var b=a.dom().attributes;return void 0===b||null===b||0===b.length},n=function(a){return b.foldl(a.dom().attributes,function(a,b){return a[b.name]=b.value,a},{})},o=function(a,b,c){k(a,c)&&!k(b,c)&&h(b,c,j(a,c))},p=function(a,c,e){d.isElement(a)&&d.isElement(c)&&b.each(e,function(b){o(a,c,b)})};return{clone:n,set:h,setAll:i,get:j,has:k,remove:l,hasNone:m,transfer:p}}),g("6r",["x","4h"],function(a,b){var c=function(a,c){var d=b.get(a,c);return void 0===d||""===d?[]:d.split(" ")},d=function(a,d,e){var f=c(a,d),g=f.concat([e]);b.set(a,d,g.join(" "))},e=function(d,e,f){var g=a.filter(c(d,e),function(a){return a!==f});g.length>0?b.set(d,e,g.join(" ")):b.remove(d,e)};return{read:c,add:d,remove:e}}),g("4i",["x","6r"],function(a,b){var c=function(a){return void 0!==a.dom().classList},d=function(a){return b.read(a,"class")},e=function(a,c){return b.add(a,"class",c)},f=function(a,c){return b.remove(a,"class",c)},g=function(b,c){a.contains(d(b),c)?f(b,c):e(b,c)};return{get:d,add:e,remove:f,toggle:g,supports:c}}),g("2e",["4g","4h","4i"],function(a,b,c){var d=function(a,b){c.supports(a)?a.dom().classList.add(b):c.add(a,b)},e=function(a){var d=c.supports(a)?a.dom().classList:c.get(a);0===d.length&&b.remove(a,"class")},f=function(a,b){if(c.supports(a)){var d=a.dom().classList;d.remove(b)}else c.remove(a,b);e(a)},g=function(a,b){return c.supports(a)?a.dom().classList.toggle(b):c.toggle(a,b)},h=function(b,d){var e=c.supports(b),f=b.dom().classList,g=function(){e?f.remove(d):c.remove(b,d)},h=function(){e?f.add(d):c.add(b,d)};return a(g,h,i(b,d))},i=function(a,b){return c.supports(a)&&a.dom().classList.contains(b)};return{add:d,remove:f,toggle:g,toggler:h,has:i}}),g("q",["2e"],function(a){var b=function(b,c,d){a.remove(b,d),a.add(b,c)},c=function(a,c,d){b(a.element(),c.alpha(),c.omega())},d=function(a,c,d){b(a.element(),c.omega(),c.alpha())},e=function(b,c,d){a.remove(b.element(),c.alpha()),a.remove(b.element(),c.omega())},f=function(b,c,d){return a.has(b.element(),c.alpha())},g=function(b,c,d){return a.has(b.element(),c.omega())};return{toAlpha:c,toOmega:d,isAlpha:f,isOmega:g,clear:e}}),g("r",["28"],function(a){return[a.strict("alpha"),a.strict("omega")]}),g("1",["p","q","r"],function(a,b,c){return a.create({fields:c,name:"swapping",apis:b})}),g("2o",[],function(){var a=function(a,b){var c=[],d=function(a){return c.push(a),b(a)},e=b(a);do e=e.bind(d);while(e.isSome());return c};return{toArray:a}}),g("z",["1h","x","6","y","2n","2o","19","a"],function(a,b,c,d,e,f,g,h){var i=function(a){return h.fromDom(a.dom().ownerDocument)},j=function(a){var b=i(a);return h.fromDom(b.dom().documentElement)},k=function(a){var b=a.dom(),c=b.ownerDocument.defaultView;return h.fromDom(c)},l=function(a){var b=a.dom();return d.from(b.parentNode).map(h.fromDom)},m=function(a){return l(a).bind(function(c){var d=u(c);return b.findIndex(d,function(b){return g.eq(a,b)})})},n=function(b,d){for(var e=a.isFunction(d)?d:c.constant(!1),f=b.dom(),g=[];null!==f.parentNode&&void 0!==f.parentNode;){var i=f.parentNode,j=h.fromDom(i);if(g.push(j),e(j)===!0)break;f=i}return g},o=function(a){var c=function(c){return b.filter(c,function(b){return!g.eq(a,b)})};return l(a).map(u).map(c).getOr([])},p=function(a){var b=a.dom();return d.from(b.offsetParent).map(h.fromDom)},q=function(a){var b=a.dom();return d.from(b.previousSibling).map(h.fromDom)},r=function(a){var b=a.dom();return d.from(b.nextSibling).map(h.fromDom)},s=function(a){return b.reverse(f.toArray(a,q))},t=function(a){return f.toArray(a,r)},u=function(a){var c=a.dom();return b.map(c.childNodes,h.fromDom)},v=function(a,b){var c=a.dom().childNodes;return d.from(c[b]).map(h.fromDom)},w=function(a){return v(a,0)},x=function(a){return v(a,a.dom().childNodes.length-1)},y=function(a,b){return a.dom().childNodes.length},z=e.immutable("element","offset"),A=function(a,b){var c=u(a);return c.length>0&&b0&&b.before(a,d),e(a)};return{empty:d,remove:e,unwrap:f}}),g("11",["16","a","b","1a"],function(a,b,c,d){var e=function(a){var b=c.isText(a)?a.dom().parentNode:a.dom();return void 0!==b&&null!==b&&b.ownerDocument.body.contains(b)},f=a.cached(function(){return g(b.fromDom(d))}),g=function(a){var c=a.dom().body;if(null===c||void 0===c)throw"Body is not available yet";return b.fromDom(c)};return{body:f,getBody:g,inBody:e}}),g("3",["2","s","x","y","9","10","11","z"],function(a,b,c,d,e,f,g,h){var i=function(d){a.emit(d,b.detachedFromDom());var e=d.components();c.each(e,i)},j=function(d){var e=d.components();c.each(e,j),a.emit(d,b.attachedToDom())},k=function(a,b){l(a,b,e.append)},l=function(a,b,c){a.getSystem().addToWorld(b),c(a.element(),b.element()),g.inBody(a.element())&&j(b),a.syncComponents()},m=function(a){i(a),f.remove(a.element()),a.getSystem().removeFromWorld(a)},n=function(a){var b=h.parent(a.element()).bind(function(b){return a.getSystem().getByDom(b).fold(d.none,d.some)});m(a),b.each(function(a){a.syncComponents()})},o=function(a){var b=a.components();c.each(b,m),f.empty(a.element()),a.syncComponents()},p=function(a,b){e.append(a,b.element());var d=h.children(b.element());c.each(d,function(a){b.getByDom(a).each(j)})},q=function(a){var b=h.children(a.element());c.each(b,function(b){a.getByDom(b).each(i)}),f.remove(a.element())};return{attach:k,attachWith:l,detach:n,detachChildren:o,attachSystem:p,detachSystem:q}}),g("6s",["x","a","z","1a"],function(a,b,c,d){var e=function(a,e){var f=e||d,g=f.createElement("div");return g.innerHTML=a,c.children(b.fromDom(g))},f=function(c,d){return a.map(c,function(a){return b.fromTag(a,d)})},g=function(c,d){return a.map(c,function(a){return b.fromText(a,d)})},h=function(c){return a.map(c,b.fromDom)};return{fromHtml:e,fromTags:f,fromText:g,fromDom:h}}),g("4m",["a","6s","9","2r","10","z"],function(a,b,c,d,e,f){var g=function(a){return a.dom().innerHTML},h=function(g,h){var i=f.owner(g),j=i.dom(),k=a.fromDom(j.createDocumentFragment()),l=b.fromHtml(h,j);d.append(k,l),e.empty(g),c.append(g,k)},i=function(b){var d=a.fromTag("div"),e=a.fromDom(b.dom().cloneNode(!0));return c.append(d,e),g(d)};return{get:g,set:h,getOuter:i}}),g("4n",["4h","a","9","2r","10","z"],function(a,b,c,d,e,f){var g=function(a,c){return b.fromDom(a.dom().cloneNode(c))},h=function(a){return g(a,!1)},i=function(a){return g(a,!0)},j=function(c,d){var e=b.fromTag(d),f=a.clone(c);return a.setAll(e,f),e},k=function(a,b){var c=j(a,b),e=f.children(i(a));return d.append(c,e),c},l=function(a,b){var g=j(a,b);c.before(a,g);var h=f.children(a);return d.append(g,h),e.remove(a),g};return{shallow:h,shallowAs:j,deep:i,copy:k,mutate:l}}),g("2s",["4m","4n"],function(a,b){var c=function(c){var d=b.shallow(c);return a.getOuter(d)};return{getHtml:c}}),g("12",["2s"],function(a){var b=function(b){return a.getHtml(b)};return{element:b}}),g("14",["y"],function(a){var b=function(a){for(var b=[],c=function(a){b.push(a)},d=0;d0&&!d.exists(n,function(b){return a.indexOf(b)>-1})}).getOr(j)}return j},p=function(a,b,c){},q={logEventCut:e.noop,logEventStopped:e.noop,logNoParent:e.noop,logEventNoHandlers:e.noop,logEventResponse:e.noop,write:e.noop},r=function(c,e,f){var g=k&&("*"===m||d.contains(m,c))?function(){var f=[];return{logEventCut:function(a,b,c){f.push({outcome:"cut",target:b,purpose:c})},logEventStopped:function(a,b,c){f.push({outcome:"stopped",target:b,purpose:c})},logNoParent:function(a,b,c){f.push({outcome:"no-parent",target:b,purpose:c})},logEventNoHandlers:function(a,b){f.push({outcome:"no-handlers-left",target:b})},logEventResponse:function(a,b,c){f.push({outcome:"response",purpose:c,target:b})},write:function(){d.contains(["mousemove","mouseover","mouseout",a.systemInit()],c)||h.log(c,{event:c,target:e.dom(),sequence:d.map(f,function(a){return d.contains(["cut","stopped","response"],a.outcome)?"{"+a.purpose+"} "+a.outcome+" at ("+b.element(a.target)+")":a.outcome})})}}}():q,i=f(g);return g.write(),i},s=function(a){var c=function(a){var e=a.spec();return{"(original.spec)":e,"(dom.ref)":a.element().dom(),"(element)":b.element(a.element()),"(initComponents)":d.map(void 0!==e.components?e.components:[],c),"(components)":d.map(a.components(),c),"(bound.events)":f.mapToArray(a.events(),function(a,b){return[b]}).join(", "),"(behaviours)":void 0!==e.behaviours?f.map(e.behaviours,function(b,c){return void 0===b?"--revoked--":{config:b.configAsRaw(),"original-config":b.initialConfig,state:a.readState(c)}}):"none"}};return c(a)},t=function(){return void 0!==window[l]?window[l]:(window[l]={systems:{},lookup:function(a){var d=window[l].systems,e=f.keys(d);return g.findMap(e,function(e){var f=d[e];return f.getByUid(a).toOption().map(function(a){return c.wrap(b.element(a.element()),s(a))})})}},window[l])},u=function(a,b){var c=t();c.systems[a]=b};return{logHandler:p,noLogger:e.constant(q),getTrace:o,monitorEvent:r,isDebugging:e.constant(k),registerInspector:u}}),g("5",[],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("4o",["1h","y"],function(a,b){return function(c,d,e,f,g){return c(e,f)?b.some(e):a.isFunction(g)&&g(e)?b.none():d(e,f,g)}}),g("2t",["1h","x","6","y","11","19","a","4o"],function(a,b,c,d,e,f,g,h){var i=function(a){return n(e.body(),a)},j=function(b,e,f){for(var h=b.dom(),i=a.isFunction(f)?f:c.constant(!1);h.parentNode;){h=h.parentNode;var j=g.fromDom(h);if(e(j))return d.some(j);if(i(j))break}return d.none()},k=function(a,b,c){var d=function(a){return b(a)};return h(d,j,a,b,c)},l=function(a,b){var c=a.dom();return c.parentNode?m(g.fromDom(c.parentNode),function(c){return!f.eq(a,c)&&b(c)}):d.none()},m=function(a,d){var e=b.find(a.dom().childNodes,c.compose(d,g.fromDom));return e.map(g.fromDom)},n=function(a,b){var c=function(a){for(var e=0;ed?c:e=c?c:a};return{cycleBy:a,cap:b}}),g("7l",["x","11","z"],function(a,b,c){var d=function(a){return h(b.body(),a)},e=function(b,d,e){return a.filter(c.parents(b,e),d)},f=function(b,d){return a.filter(c.siblings(b),d)},g=function(b,d){return a.filter(c.children(b),d)},h=function(b,d){var e=[];return a.each(c.children(b),function(a){d(a)&&(e=e.concat([a])),e=e.concat(h(a,d))}),e};return{all:d,ancestors:e,siblings:f,children:g,descendants:h}}),g("5l",["7l","2q"],function(a,b){var c=function(a){return b.all(a)},d=function(c,d,e){return a.ancestors(c,function(a){return b.is(a,d)},e)},e=function(c,d){return a.siblings(c,function(a){return b.is(a,d)})},f=function(c,d){return a.children(c,function(a){return b.is(a,d)})},g=function(a,c){return b.all(c,a)};return{all:c,ancestors:d,siblings:e,children:f,descendants:g}}),g("7j",["91","x","y","47","2e","5l","4t","u"],function(a,b,c,d,e,f,g,h){var i=function(a,c,d){var g=f.descendants(a.element(),"."+c.highlightClass());b.each(g,function(b){e.remove(b,c.highlightClass()),a.getSystem().getByDom(b).each(function(b){c.onDehighlight()(a,b)})})},j=function(a,b,c,d){var f=o(a,b,c,d);e.remove(d.element(),b.highlightClass()),f&&b.onDehighlight()(a,d)},k=function(a,b,c,d){var f=o(a,b,c,d);i(a,b,c),e.add(d.element(),b.highlightClass()),f||b.onHighlight()(a,d)},l=function(a,b,c){r(a,b,c).each(function(d){k(a,b,c,d)})},m=function(a,b,c){s(a,b,c).each(function(d){k(a,b,c,d)})},n=function(a,b,c,d){q(a,b,c,d).fold(function(a){throw new h(a)},function(d){k(a,b,c,d)})},o=function(a,b,c,d){return e.has(d.element(),b.highlightClass())},p=function(a,b,c){return g.descendant(a.element(),"."+b.highlightClass()).bind(a.getSystem().getByDom)},q=function(a,b,e,g){var h=f.descendants(a.element(),"."+b.itemClass());return c.from(h[g]).fold(function(){return d.error("No element found with index "+g)},a.getSystem().getByDom)},r=function(a,b,c){return g.descendant(a.element(),"."+b.itemClass()).bind(a.getSystem().getByDom)},s=function(a,b,d){var e=f.descendants(a.element(),"."+b.itemClass()),g=e.length>0?c.some(e[e.length-1]):c.none();return g.bind(a.getSystem().getByDom)},t=function(c,d,g,h){var i=f.descendants(c.element(),"."+d.itemClass()),j=b.findIndex(i,function(a){return e.has(a,d.highlightClass())});return j.bind(function(b){var d=a.cycleBy(b,h,0,i.length-1);return c.getSystem().getByDom(i[d])})},u=function(a,b,c){return t(a,b,c,-1)},v=function(a,b,c){return t(a,b,c,1)};return{dehighlightAll:i,dehighlight:j,highlight:k,highlightFirst:l,highlightLast:m,highlightAt:n,isHighlighted:o,getHighlighted:p,getFirst:r,getLast:s,getPrevious:u,getNext:v}}),g("7k",["4p","28"],function(a,b){return[b.strict("highlightClass"),b.strict("itemClass"),a.onHandler("onHighlight"),a.onHandler("onDehighlight")]}),g("5j",["p","7j","7k","t"],function(a,b,c,d){return a.create({fields:c,name:"highlighting",apis:b})}),g("94",["5j","6","8"],function(a,b,c){var d=function(){var a=function(a){return c.search(a.element())},b=function(a,b){a.getSystem().triggerFocus(b,a.element())};return{get:a,set:b}},e=function(){var c=function(b){return a.getHighlighted(b).map(function(a){return a.element()})},d=function(c,d){c.getSystem().getByDom(d).fold(b.noop,function(b){a.highlight(c,b)})};return{get:c,set:d}};return{dom:d,highlights:e}}),g("8q",["x","6"],function(a,b){var c=function(b){return function(c){return a.contains(b,c.raw().which)}},d=function(b){return function(c){return a.forall(b,function(a){return a(c)})}},e=function(a){return function(b){return b.raw().which===a}},f=function(a){return a.raw().shiftKey===!0};return{inSet:c,and:d,is:e,isShift:f,isNotShift:b.not(f)}}),g("8r",["8q","x"],function(a,b){var c=function(b,c){return{matches:a.is(b),classification:c}},d=function(a,b){return{matches:a,classification:b}},e=function(a,c){var d=b.find(a,function(a){return a.matches(c)});return d.map(function(a){return a.classification})};return{basic:c,rule:d,choose:e}}),g("8o",["3c","2f","s","94","4p","8r","28","v"],function(a,b,c,d,e,f,g,h){var i=function(i,j,k,l,m,n){var o=function(){return i.concat([g.defaulted("focusManager",d.dom()),e.output("handler",r),e.output("state",j)])},p=function(a,b,c,d){var e=k(a,b,c,d);return f.choose(e,b.event()).bind(function(e){return e(a,b,c,d)})},q=function(d,e){var f=l(d,e),g=a.derive(n.map(function(b){return a.run(c.focus(),function(a,c){b(a,d,e,c),c.stop()})}).toArray().concat([a.run(b.keydown(),function(a,b){p(a,b,d,e).each(function(a){b.stop()})})]));return h.deepMerge(f,g)},r={schema:o,processKey:p,toEvents:q,toApis:m};return r};return{typical:i}}),g("8p",["x","1v"],function(a,b){var c=function(b,c,d){var e=a.reverse(b.slice(0,c)),f=a.reverse(b.slice(c+1));return a.find(e.concat(f),d)},d=function(b,c,d){var e=a.reverse(b.slice(0,c));return a.find(e,d)},e=function(b,c,d){var e=b.slice(0,c),f=b.slice(c+1);return a.find(f.concat(e),d)},f=function(b,c,d){var e=b.slice(c+1);return a.find(e,d)};return{cyclePrev:c,cycleNext:e,tryPrev:d,tryNext:f}}),g("56",[],function(){var a=function(a){return void 0!==a.style};return{isSupported:a}}),g("38",["1h","x","w","y","4h","11","a","b","56","36","u","15","1j"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n=function(b,c,d){if(!a.isString(d))throw l.error("Invalid call to CSS.set. Property ",c,":: Value ",d,":: Element ",b),new k("CSS value must be a string: "+d);i.isSupported(b)&&b.style.setProperty(c,d)},o=function(a,b){i.isSupported(a)&&a.style.removeProperty(b)},p=function(a,b,c){var d=a.dom();n(d,b,c)},q=function(a,b){var d=a.dom();c.each(b,function(a,b){n(d,b,a)})},r=function(a,b){var d=a.dom();c.each(b,function(a,b){a.fold(function(){o(d,b)},function(a){n(d,b,a)})})},s=function(a,b){var c=a.dom(),d=m.getComputedStyle(c),e=d.getPropertyValue(b),g=""!==e||f.inBody(a)?e:t(c,b);return null===g?void 0:g},t=function(a,b){return i.isSupported(a)?a.style.getPropertyValue(b):""},u=function(a,b){var c=a.dom(),e=t(c,b);return d.from(e).filter(function(a){return a.length>0})},v=function(a,b,c){var d=g.fromTag(a);p(d,b,c);var e=u(d,b);return e.isSome()},w=function(a,b){var c=a.dom();o(c,b),e.has(a,"style")&&""===j.trim(e.get(a,"style"))&&e.remove(a,"style")},x=function(a,b){var c=e.get(a,"style"),d=b(a),f=void 0===c?e.remove:e.set;return f(a,"style",c),d},y=function(a,b){var c=a.dom(),d=b.dom();i.isSupported(c)&&i.isSupported(d)&&(d.style.cssText=c.style.cssText)},z=function(a){return a.dom().offsetWidth},A=function(a,b,c){u(a,c).each(function(a){u(b,c).isNone()&&p(b,c,a)})},B=function(a,c,d){h.isElement(a)&&h.isElement(c)&&b.each(d,function(b){A(a,c,b)})};return{copy:y,set:p,preserve:x,setAll:q,setOptions:r,remove:w,get:s,getRaw:u,isValidValue:v,reflow:z,transfer:B}}),g("78",["1h","x","38","56"],function(a,b,c,d){return function(e,f){var g=function(b,c){if(!a.isNumber(c)&&!c.match(/^[0-9]+$/))throw e+".set accepts only positive integer values. Value was "+c;var f=b.dom();d.isSupported(f)&&(f.style[e]=c+"px")},h=function(a){var b=f(a);if(b<=0||null===b){var d=c.get(a,e);return parseFloat(d)||0}return b},i=h,j=function(a,d){return b.foldl(d,function(b,d){var e=c.get(a,d),f=void 0===e?0:parseInt(e,10);return isNaN(f)?b:b+f},0)},k=function(a,b,c){var d=j(a,c),e=b>d?b-d:0;return e};return{set:g,get:h,getOuter:i,aggregate:j,max:k}}}),g("87",["38","78"],function(a,b){var c=b("height",function(a){return a.dom().offsetHeight}),d=function(a,b){c.set(a,b)},e=function(a){return c.get(a)},f=function(a){return c.getOuter(a)},g=function(b,d){var e=["margin-top","border-top-width","padding-top","padding-bottom","border-bottom-width","margin-bottom"],f=c.max(b,d,e);a.set(b,"max-height",f+"px")};return{set:d,get:e,getOuter:f,setMax:g}}),g("6u",["88","27","4p","8o","12","8p","8q","8r","28","x","6","y","19","8","5l","4t","87"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){var r=[i.defaulted("selector",'[data-alloy-tabstop="true"]'),i.option("onEscape"),i.option("onEnter"),i.defaulted("firstTabstop",0),i.defaulted("useTabstopAt",k.constant(!0)),i.option("visibilitySelector")],s=function(a,b,c){var d=o.descendants(a.element(),b.selector()),e=j.filter(d,function(a){return t(b,a)}),f=l.from(e[b.firstTabstop()]);f.each(function(b){var c=a.element();a.getSystem().triggerFocus(b,c)})},t=function(a,b){var c=a.visibilitySelector().bind(function(a){return p.closest(b,a)}).getOr(b);return q.get(c)>0},u=function(a,b){return n.search(a.element()).bind(function(a){return p.closest(a,b.selector())})},v=function(a,b,c,d,e){return e(b,c,function(a){return t(d,a)&&d.useTabstopAt(a)}).fold(function(){return l.some(!0)},function(b){var c=a.getSystem(),d=a.element();return c.triggerFocus(b,d),l.some(!0)})},w=function(a,b,c,d){var e=o.descendants(a.element(),c.selector());return u(a,c).bind(function(b){var f=j.findIndex(e,k.curry(m.eq,b));return f.bind(function(b){return v(a,e,b,c,d)})})},x=function(a,b,c,d){return w(a,b,c,f.cyclePrev)},y=function(a,b,c,d){return w(a,b,c,f.cycleNext)},z=function(a,b,c,d){return c.onEnter().bind(function(c){return c(a,b)})},A=function(a,b,c,d){return c.onEscape().bind(function(c){return c(a,b)})},B=k.constant([h.rule(g.and([g.isShift,g.inSet(a.TAB())]),x),h.rule(g.inSet(a.TAB()),y),h.rule(g.inSet(a.ESCAPE()),A),h.rule(g.and([g.isNotShift,g.inSet(a.ENTER())]),z)]),C=k.constant({}),D=k.constant({});return d.typical(r,b.init,B,C,D,l.some(s))}),g("8s",["4h","b"],function(a,b){var c=function(c){return"input"===b.name(c)&&"radio"!==a.get(c,"type")||"textarea"===b.name(c)};return{inside:c}}),g("8t",["8s","88","2","s","8q","y"],function(a,b,c,d,e,f){var g=function(a,b,e){return c.dispatch(a,e,d.execute()),f.some(!0)},h=function(c,d,h){return a.inside(h)&&e.inSet(b.SPACE())(d.event())?f.none():g(c,d,h)};return{defaultExecute:h}}),g("6v",["8s","88","27","8o","8t","12","8q","8r","28","6","y"],function(a,b,c,d,e,f,g,h,i,j,k){var l=[i.defaulted("execute",e.defaultExecute),i.defaulted("useSpace",!1),i.defaulted("useEnter",!0),i.defaulted("useDown",!1)],m=function(a,b,c,d){return c.execute()(a,b,a.element())},n=function(c,d,e,f){var i=e.useSpace()&&!a.inside(c.element())?b.SPACE():[],j=e.useEnter()?b.ENTER():[],k=e.useDown()?b.DOWN():[],l=i.concat(j).concat(k);return[h.rule(g.inSet(l),m)]},o=j.constant({}),p=j.constant({});return d.typical(l,c.init,n,o,p,k.none())}),g("4y",["4f","5","6","y"],function(a,b,c,d){var e=function(e){var f=b(d.none()),g=function(a,b){f.set(d.some({numRows:c.constant(a),numColumns:c.constant(b)}))},h=function(){return f.get().map(function(a){return a.numRows()})},i=function(){return f.get().map(function(a){return a.numColumns()})};return a({readState:c.constant({}),setGridSize:g,getNumRows:h,getNumColumns:i})},f=function(a){return a.state()(a)};return{flatgrid:e,init:f}}),g("9w",["38"],function(a){var b=function(a,b){return function(d){return"rtl"===c(d)?b:a}},c=function(b){return"rtl"===a.get(b,"direction")?"rtl":"ltr"};return{onDirection:b,getDirection:c}}),g("8u",["9w"],function(a){var b=function(a){return function(b,c,d,e){var g=a(b.element());return f(g,b,c,d,e)}},c=function(c,d){var e=a.onDirection(c,d);return b(e)},d=function(c,d){var e=a.onDirection(d,c);return b(e)},e=function(a){return function(b,c,d,e){return f(a,b,c,d,e)}},f=function(a,b,c,d,e){var f=d.focusManager().get(b).bind(function(c){return a(b.element(),c,d,e)});return f.map(function(a){return d.focusManager().set(b,a),!0})};return{east:d,west:c,north:e,south:e,move:e}}),g("9x",["x","2n"],function(a,b){var c=b.immutableBag(["index","candidates"],[]),d=function(b,d){return a.findIndex(b,d).map(function(a){return c({index:a,candidates:b})})};return{locate:d}}),g("9y",["6","4g","38"],function(a,b,c){var d=function(d,e,f,g){var h=c.get(d,e);void 0===h&&(h="");var i=h===f?g:f,j=a.curry(c.set,d,e,h),k=a.curry(c.set,d,e,i);return b(j,k,!1)},e=function(a){return d(a,"visibility","hidden","visible")},f=function(a,b){return d(a,"display","none",b)},g=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0},h=function(a){var b=a.dom();return!g(b)};return{toggler:e,displayToggler:f,isVisible:h}}),g("8v",["9x","x","6","19","5l","9y"],function(a,b,c,d,e,f){var g=function(a,b,c){var d=f.isVisible;return h(a,b,c,d)},h=function(g,h,i,j){var k=c.curry(d.eq,h),l=e.descendants(g,i),m=b.filter(l,f.isVisible);return a.locate(m,k)},i=function(a,c){return b.findIndex(a,function(a){return d.eq(c,a)})};return{locateVisible:g,locateIn:h,findIndex:i}}),g("8w",["91","6","y","1v"],function(a,b,c,d){var e=function(a,b,e,f){var g=d.floor(b/e),h=b%e;return f(g,h).bind(function(b){var d=b.row()*e+b.column();return d>=0&&d"}),c.anyValue()),m=b.defaulted("defaults",e.constant({})),n=b.defaulted("overrides",e.constant({})),o=c.objOf([i,j,k,l,m,n]),p=c.objOf([i,j,k,m,n]),q=c.objOf([i,j,k,l,m,n]),r=c.objOf([i,j,k,b.strict("unit"),l,m,n]),s=function(a){return a.fold(g.some,g.none,g.some,g.some)},t=function(a){var b=function(a){return a.name()};return a.fold(b,b,b,b)},u=function(a){return a.fold(e.identity,e.identity,e.identity,e.identity)},v=function(a,b){return function(d){var e=c.asStructOrDie("Converting part type",b,d);return a(e)}};return{required:v(h.required,o),external:v(h.external,p),optional:v(h.optional,q),group:v(h.group,r),asNamedPart:s,name:t,asCommon:u,original:e.constant("entirety")}}),g("72",["13","x","w","v","6n","6","6k","u"],function(a,b,c,d,e,f,g,h){var i="placeholder",j=g.generate([{single:["required","valueThunk"]},{multiple:["required","valueThunks"]}]),k=function(a){return b.contains([i],a)},l=function(b,d,g,i){return b.exists(function(a){return a!==g.owner})?j.single(!0,f.constant(g)):a.readOptFrom(i,g.name).fold(function(){throw new h("Unknown placeholder component: "+g.name+"\nKnown: ["+c.keys(i)+"]\nNamespace: "+b.getOr("none")+"\nSpec: "+e.stringify(g,null,2))},function(a){return a.replace()})},m=function(a,b,c,d){return c.uiType===i?l(a,b,c,d):j.single(!1,f.constant(c))},n=function(c,e,f,g){var h=m(c,e,f,g);return h.fold(function(h,i){var j=i(e,f.config,f.validated),k=a.readOptFrom(j,"components").getOr([]),l=b.bind(k,function(a){return n(c,e,a,g)});return[d.deepMerge(j,{components:l})]},function(a,b){var c=b(e,f.config,f.validated);return c})},o=function(a,c,d,e){return b.bind(d,function(b){return n(a,c,b,e)})},p=function(a,b){var c=!1,d=function(){return c},e=function(){if(c===!0)throw new h("Trying to use the same placeholder more than once: "+a);return c=!0,b},g=function(){return b.fold(function(a,b){return a},function(a,b){return a})};return{name:f.constant(a),required:g,used:d,replace:e}},q=function(a,b,d,f){var g=c.map(f,function(a,b){return p(b,a)}),i=o(a,b,d,g);return c.each(g,function(c){if(c.used()===!1&&c.required())throw new h("Placeholder: "+c.name()+" was not found in components list\nNamespace: "+a.getOr("none")+"\nComponents: "+e.stringify(b.components(),null,2))}),i},r=function(a,b){var c=b;return c.fold(function(b,c){return[c(a)]},function(b,c){return c(a)})};return{single:j.single,multiple:j.multiple,isSubstitute:k,placeholder:f.constant(i),substituteAll:o,substitutePlaces:q,singleReplace:r}}),g("71",["52","72","13","x","6","v"],function(a,b,c,d,e,f){var g=function(a,b,d,e){var g=d;return f.deepMerge(b.defaults()(a,d,e),d,{uid:a.partUids()[b.name()]},b.overrides()(a,d,e),{"debug.sketcher":c.wrap("part-"+b.name(),g)})},h=function(c,h,i){var j={},k={};return d.each(i,function(c){c.fold(function(a){j[a.pname()]=b.single(!0,function(b,c,d){return a.factory().sketch(g(b,a,c,d))})},function(b){var c=h.parts()[b.name()]();k[b.name()]=e.constant(g(h,b,c[a.original()]()))},function(a){j[a.pname()]=b.single(!1,function(b,c,d){return a.factory().sketch(g(b,a,c,d))})},function(a){j[a.pname()]=b.multiple(!0,function(b,c,e){var g=b[a.name()]();return d.map(g,function(c){return a.factory().sketch(f.deepMerge(a.defaults()(b,c),c,a.overrides()(b,c)))})})})}),{internals:e.constant(j),externals:e.constant(k)}};return{subs:h}}),g("51",["4p","71","52","72","4b","28","13","2d","x","6","v","w","y"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){var n=function(a,b){var d={};return i.each(b,function(b){c.asNamedPart(b).each(function(b){var c=o(a,b.pname());d[b.name()]=function(d){var e=h.asRawOrDie("Part: "+b.name()+" in "+a,h.objOf(b.schema()),d);return k.deepMerge(c,{config:d,validated:e})}})}),d},o=function(a,b){return{uiType:d.placeholder(),owner:a,name:b}},p=function(a,b,c){return{uiType:d.placeholder(),owner:a,name:b,config:c,validated:{}}},q=function(b){return i.bind(b,function(b){return b.fold(m.none,m.some,m.none,m.none).map(function(b){return f.strictObjOf(b.name(),b.schema().concat([a.snapshot(c.original())]))}).toArray()})},r=function(a){return i.map(a,c.name)},s=function(a,c,d){return b.subs(a,c,d)},t=function(a,b,c){return d.substitutePlaces(m.some(a),b,b.components(),c)},u=function(a,b,c){var d=b.partUids()[c];return a.getSystem().getByUid(d).toOption()},v=function(a,b,c){return u(a,b,c).getOrDie("Could not find part: "+c)},w=function(a,b,c){var d={},e=b.partUids(),f=a.getSystem();return i.each(c,function(a){d[a]=f.getByUid(e[a])}),l.map(d,j.constant)},x=function(a,b){var c=a.getSystem();return l.map(b.partUids(),function(a,b){return j.constant(c.getByUid(a))})},y=function(a,b,c){var d={},e=b.partUids(),f=a.getSystem();return i.each(c,function(a){d[a]=f.getByUid(e[a]).getOrDie()}),l.map(d,j.constant)},z=function(a,b){var c=r(b);return g.wrapAll(i.map(c,function(b){return{key:b,value:a+"-"+b}}))},A=function(a){return f.field("partUids","partUids",e.mergeWithThunk(function(b){return z(b.uid,a)}),h.anyValue())};return{generate:n,generateOne:p,schemas:q,names:r,substitutes:s,components:t,defaultUids:z,defaultUidsSchema:A,getAllParts:x,getPart:u,getPartOrDie:v,getParts:w,getPartsOrDie:y}}),g("73",["4p","72","28","13","2d","x","6","v","w","6n","u"],function(a,b,c,d,e,f,g,h,i,j,k){var l=function(e,g,h){var l=void 0!==h?h:"Unknown owner",m=function(){return[a.output("partUids",{})]},n=void 0!==g?g:m();if(0===e.length&&0===n.length)return m();var o=c.strictObjOf("parts",f.flatten([f.map(e,c.strict),f.map(n,function(a){return c.defaulted(a,b.single(!1,function(){throw new k("The optional part: "+a+" was not specified in the config, but it was used in components")}))})])),p=c.state("partUids",function(a){if(!d.hasKey(a,"parts"))throw new k("Part uid definition for owner: "+l+' requires "parts"\nExpected parts: '+e.join(", ")+"\nSpec: "+j.stringify(a,null,2));var b=i.map(a.parts,function(b,c){return d.readOptFrom(b,"uid").getOrThunk(function(){return a.uid+"-"+c})});return b});return[o,p]},m=function(b,d,e,f){var g=d.length>0?[c.strictObjOf("parts",d)]:[];return g.concat([c.strict("uid"),c.defaulted("dom",{}),c.defaulted("components",[]),a.snapshot("originalSpec"),c.defaulted("debug.sketcher",{})]).concat(e)},n=function(a,b,c,d){var f=m(a,d,c);return e.asRawOrDie(a+" [SpecSchema]",e.objOfOnly(f.concat(b)),c)},o=function(a,b,c,d,f){var g=m(a,d,f,c);return e.asStructOrDie(a+" [SpecSchema]",e.objOfOnly(g.concat(b)),c)},p=function(a,b,c){var d=h.deepMerge(b,c);return a(d)},q=function(a,b){return h.deepMerge(a,b)};return{asRawOrDie:n,asStructOrDie:o,addBehaviours:q,getPartsSchema:l,extend:p}}),g("50",["51","2z","73","13","v"],function(a,b,c,d,e){var f=function(a,b,f,g){var i=h(g),j=c.asStructOrDie(a,b,i,[],[]);return e.deepMerge(f(j,i),{"debug.sketcher":d.wrap(a,g)})},g=function(b,f,g,i,j){var k=h(j),l=a.schemas(g),m=a.defaultUidsSchema(g),n=c.asStructOrDie(b,f,k,l,[m]),o=a.substitutes(b,n,g),p=a.components(b,n,o.internals());return e.deepMerge(i(n,p,k,o.externals()),{"debug.sketcher":d.wrap(b,j)})},h=function(a){return e.deepMerge({uid:b.generate("uid")},a)};return{supplyUid:h,single:f,composite:g}}),g("33",["4z","50","49","51","52","28","2d","6","v","w"],function(a,b,c,d,e,f,g,h,i,j){var k=g.objOfOnly([f.strict("name"),f.strict("factory"),f.strict("configFields"),f.defaulted("apis",{}),f.defaulted("extraApis",{})]),l=g.objOfOnly([f.strict("name"),f.strict("factory"),f.strict("configFields"),f.strict("partFields"),f.defaulted("apis",{}),f.defaulted("extraApis",{})]),m=function(d){var e=g.asRawOrDie("Sketcher for "+d.name,k,d),f=function(a){return b.single(e.name,e.configFields,e.factory,a)},l=j.map(e.apis,a.makeApi),m=j.map(e.extraApis,function(a,b){return c.markAsExtraApi(a,b)});return i.deepMerge({name:h.constant(e.name),partFields:h.constant([]),configFields:h.constant(e.configFields),sketch:f},l,m)},n=function(e){var f=g.asRawOrDie("Sketcher for "+e.name,l,e),k=function(a){return b.composite(f.name,f.configFields,f.partFields,f.factory,a)},m=d.generate(f.name,f.partFields),n=j.map(f.apis,a.makeApi),o=j.map(f.extraApis,function(a,b){return c.markAsExtraApi(a,b)});return i.deepMerge({name:h.constant(f.name),partFields:h.constant(f.partFields),configFields:h.constant(f.configFields),sketch:k,parts:h.constant(m)},n,o)};return{single:m,composite:n}}),g("34",["3c","2","2f","s","x","7"],function(a,b,c,d,e,f){var g=function(g){var h=function(b){return a.run(d.execute(),function(a,c){b(a),c.stop()})},i=function(a,c){c.stop(),b.emitExecute(a)},j=function(a,b){b.cut()},k=f.detect().deviceType.isTouch()?[a.run(d.tap(),i)]:[a.run(c.click(),i),a.run(c.mousedown(),j)];return a.derive(e.flatten([g.map(h).toArray(),k]))};return{events:g}}),g("1m",["p","31","32","33","34","28","v"],function(a,b,c,d,e,f,g){var h=function(d,f){var h=e.events(d.action());return{uid:d.uid(),dom:d.dom(),components:d.components(),events:h,behaviours:g.deepMerge(a.derive([b.config({}),c.config({mode:"execution",useSpace:!0,useEnter:!0})]),d.buttonBehaviours()),domModification:{attributes:{type:"button",role:d.role().getOr("button")}},eventOrder:d.eventOrder()}};return d.single({name:"Button",factory:h,configFields:[f.defaulted("uid",void 0),f.strict("dom"),f.defaulted("components",[]),f.defaulted("buttonBehaviours",{}),f.option("action"),f.option("role"),f.defaulted("eventOrder",{})]})}),g("35",["13","x","v","a","b","4m","z","t"],function(a,b,c,d,e,f,g,h){var i=function(d){var e=void 0!==d.dom().attributes?d.dom().attributes:[];return b.foldl(e,function(b,d){return"class"===d.name?b:c.deepMerge(b,a.wrap(d.name,d.value))},{})},j=function(a){return h.prototype.slice.call(a.dom().classList,0)},k=function(a){var b=d.fromHtml(a),h=g.children(b),k=i(b),l=j(b),m=0===h.length?{}:{innerHtml:f.get(b)};return c.deepMerge({tag:e.name(b),classes:l,attributes:k},m)},l=function(a,b,d){return a.sketch(c.deepMerge({dom:k(b)},d))};return{fromHtml:k,sketch:l}}),g("1n",["35","36","h"],function(a,b,c){var d=function(d){var e=b.supplant(d,{prefix:c.prefix()});return a.fromHtml(e)},e=function(a){var b=d(a);return{dom:b}};return{dom:d,spec:e}}),g("k",["p","1f","1l","1m","v","1k","h","1n"],function(a,b,c,d,e,f,g,h){var i=function(a,b){return m(b,function(){a.execCommand(b)},{})},j=function(c){return a.derive([b.config({toggleClass:g.resolve("toolbar-button-selected"),toggleOnExecute:!1,aria:{mode:"pressed"}}),f.format(c,function(a,c){var d=c?b.on:b.off;d(a)})])},k=function(a,b){var c=j(b); +return m(b,function(){a.execCommand(b)},c)},l=function(a,b,c,d){var e=j(c);return m(b,d,e)},m=function(b,f,g){return d.sketch({dom:h.dom(''),action:f,buttonBehaviours:e.deepMerge(a.derive([c.config({})]),g)})};return{forToolbar:m,forToolbarCommand:i,forToolbarStateAction:l,forToolbarStateCommand:k}}),g("8z",["1v"],function(a){var b=function(b,c,d,e){return bd?d:b===c?c-1:a.max(c,b-e)},c=function(b,c,d,e){return b>d?b:bb.right)return f+1;var l=a.min(b.right,a.max(g,b.left))-b.left,m=d(l/b.width*k+c,c-1,f+1),n=a.round(m);return i&&m>=c&&m<=f?e(b,m,c,f,h,j):n};return{reduceBy:b,increaseBy:c,findValueOfX:f}}),g("74",["2","8z","6","y","7","1v"],function(a,b,c,d,e,f){var g="slider.change.value",h=e.detect().deviceType.isTouch(),i=function(a){var b=a.event().raw();return h&&void 0!==b.touches&&1===b.touches.length?d.some(b.touches[0]):h&&void 0!==b.touches?d.none():h||void 0===b.clientX?d.none():d.some(b)},j=function(a){var b=i(a);return b.map(function(a){return a.clientX})},k=function(b,c){a.emitWith(b,g,{value:c})},l=function(a,b){k(a,b.min(),d.none())},m=function(a,b){k(a,b.max(),d.none())},n=function(a,b){k(a,b.max()+1,d.none())},o=function(a,b){k(a,b.min()-1,d.none())},p=function(a,c,d,e){var f=b.findValueOfX(c,d.min(),d.max(),e,d.stepSize(),d.snapToGrid(),d.snapStart());k(a,f)},q=function(a,b,c,d){return j(d).map(function(d){return p(a,c,b,d),d})},r=function(a,c){var e=b.reduceBy(c.value().get(),c.min(),c.max(),c.stepSize());k(a,e,d.none())},s=function(a,c){var e=b.increaseBy(c.value().get(),c.min(),c.max(),c.stepSize());k(a,e,d.none())};return{setXFromEvent:q,setToLedge:o,setToRedge:n,moveLeftFromRedge:m,moveRightFromLedge:l,moveLeft:r,moveRight:s,changeEvent:c.constant(g)}}),g("53",["p","31","32","3c","2f","52","74","28","5","6","y","7"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=l.detect(),n=m.deviceType.isTouch(),o=function(a,b){return f.optional({name:""+a+"-edge",overrides:function(a){var c=d.derive([d.runActionExtra(e.touchstart(),b,[a])]),f=d.derive([d.runActionExtra(e.mousedown(),b,[a]),d.runActionExtra(e.mousemove(),function(a,c){c.mouseIsDown().get()&&b(a,c)},[a])]);return{events:n?c:f}}})},p=o("left",g.setToLedge),q=o("right",g.setToRedge),r=f.required({name:"thumb",defaults:j.constant({dom:{styles:{position:"absolute"}}}),overrides:function(a){return{events:d.derive([d.redirectToPart(e.touchstart(),a,"spectrum"),d.redirectToPart(e.touchmove(),a,"spectrum"),d.redirectToPart(e.touchend(),a,"spectrum")])}}}),s=f.required({schema:[h.state("mouseIsDown",function(){return i(!1)})],name:"spectrum",overrides:function(f){var h=function(a,b){var c=a.element().dom().getBoundingClientRect();g.setXFromEvent(a,f,c,b)},i=d.derive([d.run(e.touchstart(),h),d.run(e.touchmove(),h)]),j=d.derive([d.run(e.mousedown(),h),d.run(e.mousemove(),function(a,b){f.mouseIsDown().get()&&h(a,b)})]);return{behaviours:a.derive(n?[]:[c.config({mode:"special",onLeft:function(a){return g.moveLeft(a,f),k.some(!0)},onRight:function(a){return g.moveRight(a,f),k.some(!0)}}),b.config({})]),events:n?i:j}}});return[p,q,r,s]}),g("54",["28","5","6","7"],function(a,b,c,d){var e=d.detect().deviceType.isTouch();return[a.strict("min"),a.strict("max"),a.defaulted("stepSize",1),a.defaulted("onChange",c.noop),a.defaulted("onInit",c.noop),a.defaulted("onDragStart",c.noop),a.defaulted("onDragEnd",c.noop),a.defaulted("snapToGrid",!1),a.option("snapStart"),a.strict("getInitialValue"),a.defaulted("sliderBehaviours",{}),a.state("value",function(a){return b(a.min)})].concat(e?[]:[a.state("mouseIsDown",function(){return b(!1)})])}),g("5b",[],function(){var a=function(a,b,c){b.store().manager().onLoad(a,b,c)},b=function(a,b,c){b.store().manager().onUnload(a,b,c)},c=function(a,b,c,d){b.store().manager().setValue(a,b,c,d)},d=function(a,b,c){return b.store().manager().getValue(a,b,c)};return{onLoad:a,onUnload:b,setValue:c,getValue:d}}),g("5a",["3c","26","5b"],function(a,b,c){var d=function(d,e){var f=d.resetOnDom()?[a.runOnAttached(function(a,b){c.onLoad(a,d,e)}),a.runOnDetached(function(a,b){c.onUnload(a,d,e)})]:[b.loadEvent(d,e,c.onLoad)];return a.derive(f)};return{events:d}}),g("5d",["4f","5"],function(a,b){var c=function(){var c=b(null),d=function(){return{mode:"memory",value:c.get()}},e=function(){return null===c.get()},f=function(){c.set(null)};return a({set:c.set,get:c.get,isNotSet:e,clear:f,readState:d})},d=function(){var b=function(){};return a({readState:b})},e=function(){var c=b({}),d=function(){return{mode:"dataset",dataset:c.get()}};return a({readState:d,set:c.set,get:c.get})},f=function(a){return a.store().manager().state(a)};return{memory:c,dataset:e,manual:d,init:f}}),g("75",["5d","4p","28","13","6"],function(a,b,c,d,e){var f=function(a,b,c,d){b.store().getDataKey();c.set({}),b.store().setData()(a,d),b.onSetValue()(a,d)},g=function(a,b,c){var e=b.store().getDataKey()(a),f=c.get();return d.readOptFrom(f,e).fold(function(){return b.store().getFallbackEntry()(e)},function(a){return a})},h=function(a,b,c){b.store().initialValue().each(function(d){f(a,b,c,d)})},i=function(a,b,c){c.set({})};return[c.option("initialValue"),c.strict("getFallbackEntry"),c.strict("getDataKey"),c.strict("setData"),b.output("manager",{setValue:f,getValue:g,onLoad:h,onUnload:i,state:a.dataset})]}),g("76",["27","4p","28","6"],function(a,b,c,d){var e=function(a,b,c){return b.store().getValue()(a)},f=function(a,b,c,d){b.store().setValue()(a,d),b.onSetValue()(a,d)},g=function(a,b,c){b.store().initialValue().each(function(c){b.store().setValue()(a,c)})};return[c.strict("getValue"),c.defaulted("setValue",d.noop),c.option("initialValue"),b.output("manager",{setValue:f,getValue:e,onLoad:g,onUnload:d.noop,state:a.init})]}),g("77",["5d","4p","28"],function(a,b,c){var d=function(a,b,c,d){c.set(d),b.onSetValue()(a,d)},e=function(a,b,c){return c.get()},f=function(a,b,c){b.store().initialValue().each(function(a){c.isNotSet()&&c.set(a)})},g=function(a,b,c){c.clear()};return[c.option("initialValue"),b.output("manager",{setValue:d,getValue:e,onLoad:f,onUnload:g,state:a.memory})]}),g("5c",["75","76","77","4p","28","2d"],function(a,b,c,d,e,f){return[e.defaultedOf("store",{mode:"memory"},f.choose("mode",{memory:c,manual:b,dataset:a})),d.onHandler("onSetValue"),e.defaulted("resetOnDom",!1)]}),g("3f",["p","5a","5b","5c","5d"],function(a,b,c,d,e){var f=a.create({fields:d,name:"representing",active:b,apis:c,extra:{setValueFrom:function(a,b){var c=f.getValue(b);f.setValue(a,c)}},state:e});return f}),g("5m",["38","78"],function(a,b){var c=b("width",function(a){return a.dom().offsetWidth}),d=function(a,b){c.set(a,b)},e=function(a){return c.get(a)},f=function(a){return c.getOuter(a)},g=function(b,d){var e=["margin-left","border-left-width","padding-left","padding-right","border-right-width","margin-right"],f=c.max(b,d,e);a.set(b,"max-width",f+"px")};return{set:d,get:e,getOuter:f,setMax:g}}),g("55",["p","32","3f","3c","2f","51","74","x","6","v","y","7","38","5m"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){var o=l.detect().deviceType.isTouch(),p=function(l,p,q,r){var s=l.max()-l.min(),t=function(a){var b=a.element().dom().getBoundingClientRect();return(b.left+b.right)/2},u=function(a){return f.getPartOrDie(a,l,"thumb")},v=function(a,b,c){var d=c.value().get();return dc.max()?f.getPart(a,c,"right-edge").fold(function(){return b.width},function(a){return t(a)-b.left}):(c.value().get()-c.min())/s*b.width},w=function(a){var b=f.getPartOrDie(a,l,"spectrum"),c=b.element().dom().getBoundingClientRect(),d=a.element().dom().getBoundingClientRect(),e=v(a,c,l);return c.left-d.left+e},x=function(a){var b=w(a),c=u(a),d=n.get(c.element())/2;m.set(c.element(),"left",b-d+"px")},y=function(a,b){var c=l.value().get(),d=u(a);return c!==b||m.getRaw(d.element(),"left").isNone()?(l.value().set(b),x(a),l.onChange()(a,d,b),k.some(!0)):k.none()},z=function(a){y(a,l.min(),k.none())},A=function(a){y(a,l.max(),k.none())},B=o?[d.run(e.touchstart(),function(a,b){l.onDragStart()(a,u(a))}),d.run(e.touchend(),function(a,b){l.onDragEnd()(a,u(a))})]:[d.run(e.mousedown(),function(a,b){b.stop(),l.onDragStart()(a,u(a)),l.mouseIsDown().set(!0)}),d.run(e.mouseup(),function(a,b){l.onDragEnd()(a,u(a)),l.mouseIsDown().set(!1)})];return{uid:l.uid(),dom:l.dom(),components:p,behaviours:j.deepMerge(a.derive(h.flatten([o?[]:[b.config({mode:"special",focusIn:function(a){return f.getPart(a,l,"spectrum").map(b.focusIn).map(i.constant(!0))}})],[c.config({store:{mode:"manual",getValue:function(a){return l.value().get()}}})]])),l.sliderBehaviours()),events:d.derive([d.run(g.changeEvent(),function(a,b){y(a,b.event().value())}),d.runOnAttached(function(a,b){l.value().set(l.getInitialValue()());var c=u(a);x(a),l.onInit()(a,c,l.value().get())})].concat(B)),apis:{resetToMin:z,resetToMax:A,refresh:x},domModification:{styles:{position:"relative"}}}};return{sketch:p}}),g("37",["33","53","54","55","1v"],function(a,b,c,d,e){return a.composite({name:"Slider",configFields:c,partFields:b,factory:d.sketch,apis:{resetToMin:function(a,b){a.resetToMin(b)},resetToMax:function(a,b){a.resetToMax(b)},refresh:function(a,b){a.refresh(b)}}})}),g("39",["k"],function(a){var b=function(b,c,d){return a.forToolbar(c,function(){var a=d();b.setContextToolbar([{label:c+" group",items:a}])},{})};return{button:b}}),g("1o",["p","1e","1f","37","38","1k","h","39","1n"],function(a,b,c,d,e,f,g,h,i){var j=-1,k=function(b){var h=function(a){return a<0?"black":a>360?"white":"hsl("+a+", 100%, 50%)"},j=function(a,b,c){var d=h(c);e.set(b.element(),"background-color",d)},k=function(a,c,d){var f=h(d);e.set(c.element(),"background-color",f),b.onChange(a,c,f)};return d.sketch({dom:i.dom('
    '),components:[d.parts()["left-edge"](i.spec('
    ')),d.parts().spectrum({dom:i.dom('
    '),components:[i.spec('
    ')],behaviours:a.derive([c.config({toggleClass:g.resolve("thumb-active")})])}),d.parts()["right-edge"](i.spec('
    ')),d.parts().thumb({dom:i.dom('
    '),behaviours:a.derive([c.config({toggleClass:g.resolve("thumb-active")})])})],onChange:k,onDragStart:function(a,b){c.on(b)},onDragEnd:function(a,b){c.off(b)},onInit:j,stepSize:10,min:0,max:360,getInitialValue:b.getInitialValue,sliderBehaviours:a.derive([f.orientation(d.refresh)])})},l=function(a){return[k(a)]},m=function(a,b){var c={onChange:function(a,c,d){b.undoManager.transact(function(){b.formatter.apply("forecolor",{value:d}),b.nodeChanged()})},getInitialValue:function(){return j}};return h.button(a,"color",function(){return l(c)})};return{makeItems:l,sketch:m}}),g("3a",["p","1e","1f","37","28","2d","1k","h","1n"],function(a,b,c,d,e,f,g,h,i){var j=f.objOfOnly([e.strict("getInitialValue"),e.strict("onChange"),e.strict("category"),e.strict("sizes")]),k=function(b){var e=f.asRawOrDie("SizeSlider",j,b),k=function(a){return a>=0&&a
    '),components:[i.spec('
    ')]}),d.parts().thumb({dom:i.dom('
    '),behaviours:a.derive([c.config({toggleClass:h.resolve("thumb-active")})])})]})};return{sketch:k}}),g("57",["1h","6","y","a"],function(a,b,c,d){var e=function(e,f,g){for(var h=e.dom(),i=a.isFunction(g)?g:b.constant(!1);h.parentNode;){h=h.parentNode;var j=d.fromDom(h),k=f(j);if(k.isSome())return k;if(i(j))break}return c.none()},f=function(a,b,d){var f=b(a);return f.orThunk(function(){return d(a)?c.none():e(a,b,d)})};return{ancestor:e,closest:f}}),g("3b",["x","6","y","19","a","b","38","57","z"],function(a,b,c,d,e,f,g,h,i){var j=["9px","10px","11px","12px","14px","16px","18px","20px","24px","32px","36px"],k="medium",l=2,m=function(a){return c.from(j[a])},n=function(b){return a.findIndex(j,function(a){return a===b})},o=function(a,b){var d=f.isElement(b)?c.some(b):i.parent(b);return d.map(function(b){var c=h.closest(b,function(a){return g.getRaw(a,"font-size")},a);return c.getOrThunk(function(){return g.get(b,"font-size")})}).getOr("")},p=function(b){var c=b.selection.getStart(),f=e.fromDom(c),g=e.fromDom(b.getBody()),h=function(a){return d.eq(g,a)},i=o(h,f);return a.find(j,function(a){return i===a}).getOr(k)},q=function(a,b){var c=p(a);c!==b&&a.execCommand("fontSize",!1,b)},r=function(a){var b=p(a);return n(b).getOr(l)},s=function(a,b){m(b).each(function(b){q(a,b)})};return{candidates:b.constant(j),get:r,apply:s}}),g("1p",["3a","39","3b","1n"],function(a,b,c,d){var e=c.candidates(),f=function(b){return a.sketch({onChange:b.onChange,sizes:e,category:"font",getInitialValue:b.getInitialValue})},g=function(a){return[d.spec(''),f(a),d.spec('')]},h=function(a,d){var e={onChange:function(a){c.apply(d,a)},getInitialValue:function(){return c.get(d)}};return b.button(a,"font-size",function(){return g(e)})};return{makeItems:g,sketch:h}}),g("79",[],function(){function a(a,b){return function(){a.apply(b,arguments)}}function b(b){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof b)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],h(b,a(d,this),a(e,this))}function c(a){var b=this;return null===this._state?void this._deferreds.push(a):void i(function(){var c=b._state?a.onFulfilled:a.onRejected;if(null===c)return void(b._state?a.resolve:a.reject)(b._value);var d;try{d=c(b._value)}catch(b){return void a.reject(b)}a.resolve(d)})}function d(b){try{if(b===this)throw new TypeError("A promise cannot be resolved with itself.");if(b&&("object"==typeof b||"function"==typeof b)){var c=b.then;if("function"==typeof c)return void h(a(c,b),a(d,this),a(e,this))}this._state=!0,this._value=b,f.call(this)}catch(a){e.call(this,a)}}function e(a){this._state=!1,this._value=a,f.call(this)}function f(){for(var a=0,b=this._deferreds.length;a
    '),components:[g.asSpec()],action:function(a){var b=g.get(a);b.element().dom().click()}})};return{sketch:k}}),g("5e",[],function(){var a=function(a){return a.dom().textContent},b=function(a,b){a.dom().textContent=b};return{get:a,set:b}}),g("3g",["6","y","a","4h","5e","4t"],function(a,b,c,d,e,f){var g=function(a){return a.length>0},h=function(a){return void 0===a||null===a?"":a},i=function(a){var c=a.selection.getContent({format:"text"});return{url:"",text:c,title:"",target:"",link:b.none()}},j=function(a){var c=e.get(a),f=d.get(a,"href"),g=d.get(a,"title"),i=d.get(a,"target");return{url:h(f),text:c!==f?h(c):"",title:h(g),target:h(i),link:b.some(a)}},k=function(a){return q(a).fold(function(){return i(a)},function(a){return j(a)})},l=function(a){var b=d.get(a,"href"),c=e.get(a);return b===c},m=function(a,c,d){return d.text.filter(g).fold(function(){return l(a)?b.some(c):b.none()},b.some)},n=function(b,c){var d=c.link.bind(a.identity);d.each(function(a){b.execCommand("unlink")})},o=function(a,b){var c={};return c.href=a,b.title.filter(g).each(function(a){c.title=a}),b.target.filter(g).each(function(a){c.target=a}),c},p=function(b,c){c.url.filter(g).fold(function(){n(b,c)},function(f){var h=o(f,c),i=c.link.bind(a.identity);i.fold(function(){var a=c.text.filter(g).getOr(f);b.insertContent(b.dom.createHTML("a",h,b.dom.encode(a)))},function(a){var b=m(a,f,c);d.setAll(a,h),b.each(function(b){e.set(a,b)})})})},q=function(a){var b=c.fromDom(a.selection.getStart());return f.closest(b,"a")};return{getInfo:k,applyInfo:p,query:q}}),g("3t",["p","3c","28","6"],function(a,b,c,d){var e=function(e,f){var g=b.derive(f);return a.create({fields:[c.strict("enabled")],name:e,active:{events:d.constant(g)}})},f=function(b,c){var f=e(b,c);return{key:b,value:{config:{},me:f,configAsRaw:d.constant({}),initialConfig:{},state:a.noState()}}};return{events:e,config:f}}),g("7d",[],function(){var a=function(a,b,c){return b.find()(a)};return{getCurrent:a}}),g("7e",["28"],function(a){return[a.strict("find")]}),g("5f",["p","7d","7e"],function(a,b,c){return a.create({fields:c,name:"composing",apis:b})}),g("3u",["33","28","v"],function(a,b,c){var d=function(a,b){return{uid:a.uid(),dom:c.deepMerge({tag:"div",attributes:{role:"presentation"}},a.dom()),components:a.components(),behaviours:a.containerBehaviours(),events:a.events(),domModification:a.domModification(),eventOrder:a.eventOrder()}};return a.single({name:"Container",factory:d,configFields:[b.defaulted("components",[]),b.defaulted("containerBehaviours",{}),b.defaulted("events",{}),b.defaulted("domModification",{}),b.defaulted("eventOrder",{})]})}),g("5g",["p","5f","3f","3c","33","28","y"],function(a,b,c,d,e,f,g){var h=function(e,f){return{uid:e.uid(),dom:e.dom(),behaviours:a.derive([c.config({store:{mode:"memory",initialValue:e.getInitialValue()()}}),b.config({find:g.some})]),events:d.derive([d.runOnAttached(function(a,b){c.setValue(a,e.getInitialValue()())})])}};return e.single({name:"DataField",factory:h,configFields:[f.strict("uid"),f.strict("dom"),f.strict("getInitialValue")]})}),g("84",["4a","13"],function(a,b){var c=function(c,d){return a.nu({attributes:b.wrapAll([{key:d.tabAttr(),value:"true"}])})};return{exhibit:c}}),g("85",["28"],function(a){return[a.defaulted("tabAttr","data-alloy-tabstop")]}),g("64",["p","84","85"],function(a,b,c){return a.create({fields:c,name:"tabstopping",active:b})}),g("90",["u"],function(a){var b=function(a){return a.dom().value},c=function(b,c){if(void 0===c)throw new a("Value.set was undefined");b.dom().value=c};return{set:c,get:b}}),g("7f",["p","31","3f","64","4p","28","13","6","v","90"],function(a,b,c,d,e,f,g,h,i,j){var k=[f.option("data"),f.defaulted("inputAttributes",{}),f.defaulted("inputStyles",{}),f.defaulted("type","input"),f.defaulted("tag","input"),e.onHandler("onSetValue"),f.defaulted("styles",{}),f.option("placeholder"),f.defaulted("eventOrder",{}),f.defaulted("hasTabstop",!0),f.defaulted("inputBehaviours",{}),f.defaulted("selectOnFocus",!0)],l=function(e){return i.deepMerge(a.derive([c.config({store:{mode:"manual",initialValue:e.data().getOr(void 0),getValue:function(a){return j.get(a.element())},setValue:function(a,b){var c=j.get(a.element());c!==b&&j.set(a.element(),b)}},onSetValue:e.onSetValue()}),b.config({onFocus:e.selectOnFocus()===!1?h.noop:function(a){var b=a.element(),c=j.get(b);b.dom().setSelectionRange(0,c.length)}}),e.hasTabstop()?d.config({}):d.revoke()]),e.inputBehaviours())},m=function(a){return{tag:a.tag(),attributes:i.deepMerge(g.wrapAll([{key:"type",value:a.type()}].concat(a.placeholder().map(function(a){return{key:"placeholder",value:a}}).toArray())),a.inputAttributes()),styles:a.inputStyles()}};return{schema:h.constant(k),behaviours:l,dom:m}}),g("5h",["33","7f"],function(a,b){var c=function(a,c){return{uid:a.uid(),dom:b.dom(a),components:[],behaviours:b.behaviours(a),eventOrder:a.eventOrder()}};return a.single({name:"Input",configFields:b.schema(),factory:c})}),g("3h",["3t","p","5f","3f","1f","1g","3c","2","2f","1m","3u","5g","5h","y","h","1n"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p){var q="input-clearing",r=function(l,r){var s=f.record(m.sketch({placeholder:r,onSetValue:function(a,b){h.emit(a,i.input())},inputBehaviours:b.derive([c.config({find:n.some})]),selectOnFocus:!1})),t=f.record(j.sketch({dom:p.dom(''),action:function(a){var b=s.get(a);d.setValue(b,"")}}));return{name:l,spec:k.sketch({dom:p.dom('
    '),components:[s.asSpec(),t.asSpec()],containerBehaviours:b.derive([e.config({toggleClass:o.resolve("input-container-empty")}),c.config({find:function(a){return n.some(s.get(a))}}),a.config(q,[g.run(i.input(),function(a){var b=s.get(a),c=d.getValue(b),f=c.length>0?e.off:e.on;f(a)})])])})}},s=function(a){return{name:a,spec:l.sketch({dom:{tag:"span",styles:{display:"none"}},getInitialValue:function(){return n.none()}})}};return{field:r,hidden:s}}),g("7h",["x","4h","2e","b"],function(a,b,c,d){var e=["input","button","textarea"],f=function(a,b,c){b.disabled()&&n(a,b,c)},g=function(b){return a.contains(e,d.name(b.element()))},h=function(a){return b.has(a.element(),"disabled")},i=function(a){b.set(a.element(),"disabled","disabled")},j=function(a){b.remove(a.element(),"disabled")},k=function(a){return"true"===b.get(a.element(),"aria-disabled")},l=function(a){b.set(a.element(),"aria-disabled","true")},m=function(a){b.set(a.element(),"aria-disabled","false")},n=function(a,b,d){b.disableClass().each(function(b){c.add(a.element(),b)});var e=g(a)?i:l;e(a)},o=function(a,b,d){b.disableClass().each(function(b){c.remove(a.element(),b)});var e=g(a)?j:m;e(a)},p=function(a){return g(a)?h(a):k(a)};return{enable:o,disable:n,isDisabled:p,onLoad:f}}),g("7g",["3c","s","26","7h","4a","x"],function(a,b,c,d,e,f){var g=function(a,b,c){return e.nu({classes:b.disabled()?b.disableClass().map(f.pure).getOr([]):[]})},h=function(e,f){return a.derive([a.abort(b.execute(),function(a,b){return d.isDisabled(a,e,f)}),c.loadEvent(e,f,d.onLoad)])};return{exhibit:g,events:h}}),g("7i",["28"],function(a){return[a.defaulted("disabled",!1),a.option("disableClass")]}),g("5i",["p","7g","7h","7i"],function(a,b,c,d){return a.create({fields:d,name:"disabling",active:b,apis:c})}),g("5k",["p","5f","3f","50","51","52","28","x","v","w"],function(a,b,c,d,e,f,g,h,i,j){var k="form",l=[g.defaulted("formBehaviours",{})],m=function(a){return""},n=function(a){var b=function(){var a=[],b=function(b,c){return a.push(b),e.generateOne(k,m(b),c)};return{field:b,record:function(){return a}}}(),c=a(b),g=b.record(),i=h.map(g,function(a){return f.required({name:a,pname:m(a)})});return d.composite(k,l,i,o,c)},o=function(d,f,g){return i.deepMerge({"debug.sketcher":{Form:g},uid:d.uid(),dom:d.dom(),components:f,behaviours:i.deepMerge(a.derive([c.config({store:{mode:"manual",getValue:function(a){var f=e.getAllParts(a,d);return j.map(f,function(a,d){return a().bind(b.getCurrent).map(c.getValue)})},setValue:function(a,f){j.each(f,function(f,g){e.getPart(a,d,g).each(function(a){b.getCurrent(a).each(function(a){c.setValue(a,f)})})})}}})]),d.formBehaviours())})};return{sketch:n}}),g("1y",["y","5"],function(a,b){var c=function(c){var d=b(a.none()),e=function(){d.get().each(c)},f=function(){e(),d.set(a.none())},g=function(b){e(),d.set(a.some(b))},h=function(){return d.get().isSome()};return{clear:f,isSet:h,set:g}},d=function(){return c(function(a){a.destroy()})},e=function(){return c(function(a){a.unbind()})},f=function(){var c=b(a.none()),d=function(){c.get().each(function(a){a.destroy()})},e=function(){d(),c.set(a.none())},f=function(b){d(),c.set(a.some(b))},g=function(a){c.get().each(a)},h=function(){return c.get().isSome()};return{clear:e,isSet:h,set:f,run:g}},g=function(){var c=b(a.none()),d=function(){c.set(a.none())},e=function(b){c.set(a.some(b))},f=function(a){c.get().each(a)},g=function(){return c.get().isSome()};return{clear:d,set:e,isSet:g,on:f}};return{destroyable:d,unbindable:e,api:f,value:g}}),g("5n",[],function(){var a=1,b=-1,c=0,d=function(a){return{xValue:a,points:[]}},e=function(c,d){if(d===c.xValue)return c;var e=d-c.xValue>0?a:b,f={direction:e,xValue:d},g=function(){if(0===c.points.length)return[];var a=c.points[c.points.length-1];return a.direction===e?c.points.slice(0,c.points.length-1):c.points}();return{xValue:d,points:g.concat([f])}},f=function(d){if(0===d.points.length)return c;var e=d.points[0].direction,f=d.points[d.points.length-1].direction;return e===b&&f===b?b:e===a&&f==a?a:c};return{init:d,move:e,complete:f}}),g("3i",["3t","p","5i","5j","32","1e","3f","1g","3c","2","2f","1m","3u","5k","28","2d","x","5","y","1y","38","5l","4t","5m","1k","5n","h","1n"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B){var C=function(f){var C="navigateEvent",D="serializer-wrapper-events",E="form-events",F=p.objOf([o.strict("fields"),o.defaulted("maxFieldIndex",f.fields.length-1),o.strict("onExecute"),o.strict("getInitialValue"),o.state("state",function(){return{dialogSwipeState:t.value(),currentScreen:r(0)}})]),G=p.asRawOrDie("SerialisedDialog",F,f),H=function(a,d,e){return l.sketch({dom:B.dom(''),action:function(b){j.emitWith(b,C,{direction:a})},buttonBehaviours:b.derive([c.config({disableClass:A.resolve("toolbar-navigation-disabled"),disabled:!e})])})},I=function(a,b){w.descendant(a.element(),"."+A.resolve("serialised-dialog-chain")).each(function(a){u.set(a,"left",-G.state.currentScreen.get()*b.width+"px")})},J=function(a,b){var c=v.descendants(a.element(),"."+A.resolve("serialised-dialog-screen"));w.descendant(a.element(),"."+A.resolve("serialised-dialog-chain")).each(function(a){ +G.state.currentScreen.get()+b>=0&&G.state.currentScreen.get()+b'),components:[m.sketch({dom:B.dom('
    '),components:q.map(G.fields,function(a,b){return b<=G.maxFieldIndex?m.sketch({dom:B.dom('
    '),components:q.flatten([[H(-1,"previous",b>0)],[c.field(a.name,a.spec)],[H(1,"next",b'),behaviours:b.derive([d.config({highlightClass:A.resolve("dot-active"),itemClass:A.resolve("dot-item")})]),components:q.bind(G.fields,function(a,b){return b<=G.maxFieldIndex?[B.spec('
    ')]:[]})});return{dom:B.dom('
    '),components:[M.asSpec(),N.asSpec()],behaviours:b.derive([e.config({mode:"special",focusIn:function(a){var b=M.get(a);e.focusIn(b)}}),a.config(D,[i.run(k.touchstart(),function(a,b){G.state.dialogSwipeState.set(z.init(b.event().raw().touches[0].clientX))}),i.run(k.touchmove(),function(a,b){G.state.dialogSwipeState.on(function(a){b.event().prevent(),G.state.dialogSwipeState.set(z.move(a,b.event().raw().touches[0].clientX))})}),i.run(k.touchend(),function(a){G.state.dialogSwipeState.on(function(b){var c=M.get(a),d=-1*z.complete(b);J(c,d)})})])])}};return{sketch:C}}),g("3j",["6","7"],function(a,b){var c=b.detect(),d=function(a,b){var c=b.selection.getRng();a(),b.selection.setRng(c)},e=function(b,e){var f=c.os.isAndroid()?d:a.apply;f(e,b)};return{forAndroid:e}}),g("1r",["3f","y","16","3g","k","3h","3i","3j"],function(a,b,c,d,e,f,g,h){var i=c.cached(function(c,e){return[{label:"the link group",items:[g.sketch({fields:[f.field("url","Type or paste URL"),f.field("text","Link text"),f.field("title","Link title"),f.field("target","Link target"),f.hidden("link")],maxFieldIndex:["url","text","title","target"].length-1,getInitialValue:function(){return b.some(d.getInfo(e))},onExecute:function(b){var f=a.getValue(b);d.applyInfo(e,f),c.restoreToolbar(),e.focus()}})]}]}),j=function(a,b){return e.forToolbarStateAction(b,"link","link",function(){var c=i(a,b);a.setContextToolbar(c),h.forAndroid(b,function(){a.focusToolbar()}),d.query(b).each(function(a){b.selection.select(a.dom())})})};return{sketch:j}}),g("3k",[],function(){return[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}]}),g("7n",["13","6","y","4h","2e"],function(a,b,c,d,e){var f=function(c,d,e,f){return a.readOptFrom(d.routes(),f.start()).map(b.apply).bind(function(c){return a.readOptFrom(c,f.destination()).map(b.apply)})},g=function(a,b,c){var d=k(a,b,c);return d.bind(function(d){return h(a,b,c,d)})},h=function(a,c,d,e){return f(a,c,d,e).bind(function(a){return a.transition().map(function(c){return{transition:b.constant(c),route:b.constant(a)}})})},i=function(a,b,c){g(a,b,c).each(function(c){var f=c.transition();e.remove(a.element(),f.transitionClass()),d.remove(a.element(),b.destinationAttr())})},j=function(a,c,e,f){return{start:b.constant(d.get(a.element(),c.stateAttr())),destination:b.constant(f)}},k=function(a,e,f){var g=a.element();return d.has(g,e.destinationAttr())?c.some({start:b.constant(d.get(a.element(),e.stateAttr())),destination:b.constant(d.get(a.element(),e.destinationAttr()))}):c.none()},l=function(a,b,c,e){i(a,b,c),d.has(a.element(),b.stateAttr())&&d.get(a.element(),b.stateAttr())!==e&&b.onFinish()(a,e),d.set(a.element(),b.stateAttr(),e)},m=function(a,b,c,e){d.has(a.element(),b.destinationAttr())&&(d.set(a.element(),b.stateAttr(),d.get(a.element(),b.destinationAttr())),d.remove(a.element(),b.destinationAttr()))},n=function(a,b,c,f){m(a,b,c,f);var g=j(a,b,c,f);h(a,b,c,g).fold(function(){l(a,b,c,f)},function(g){i(a,b,c);var h=g.transition();e.add(a.element(),h.transitionClass()),d.set(a.element(),b.destinationAttr(),f)})},o=function(a,b,e){var f=a.element();return d.has(f,b.stateAttr())?c.some(d.get(f,b.stateAttr())):c.none()};return{findRoute:f,disableTransition:i,getCurrentRoute:k,jumpTo:l,progressTo:n,getState:o}}),g("7m",["3c","2f","7n"],function(a,b,c){var d=function(d,e){return a.derive([a.run(b.transitionend(),function(a,b){var f=b.event().raw();c.getCurrentRoute(a,d,e).each(function(b){c.findRoute(a,d,e,b).each(function(g){g.transition().each(function(g){f.propertyName===g.property()&&(c.jumpTo(a,d,e,b.destination()),d.onTransition()(a,b))})})})}),a.runOnAttached(function(a,b){c.jumpTo(a,d,e,d.initialState())})])};return{events:d}}),g("7o",["4p","28","2d","47"],function(a,b,c,d){return[b.defaulted("destinationAttr","data-transitioning-destination"),b.defaulted("stateAttr","data-transitioning-state"),b.strict("initialState"),a.onHandler("onTransition"),a.onHandler("onFinish"),b.strictOf("routes",c.setOf(d.value,c.setOf(d.value,c.objOfOnly([b.optionObjOfOnly("transition",[b.strict("property"),b.strict("transitionClass")])]))))]}),g("5o",["p","7m","7n","7o","13","w"],function(a,b,c,d,e,f){var g=function(a){var b={};return f.each(a,function(a,c){var d=c.split("<->");b[d[0]]=e.wrap(d[1],a),b[d[1]]=e.wrap(d[0],a)}),b},h=function(a,b,c){return e.wrapAll([{key:a,value:e.wrap(b,c)},{key:b,value:e.wrap(a,c)}])},i=function(a,b,c,d){return e.wrapAll([{key:a,value:e.wrapAll([{key:b,value:d},{key:c,value:d}])},{key:b,value:e.wrapAll([{key:a,value:d},{key:c,value:d}])},{key:c,value:e.wrapAll([{key:a,value:d},{key:b,value:d}])}])};return a.create({fields:d,name:"transitioning",active:b,apis:c,extra:{createRoutes:g,createBistate:h,createTristate:i}})}),g("7q",["27","4b","28","2d","x","6","w","6n","u"],function(a,b,c,d,e,f,g,h,i){var j=function(j,k){var l=e.map(k,function(e){return c.field(e.name(),e.name(),b.asOption(),d.objOf([c.strict("config"),c.defaulted("state",a)]))}),m=d.asStruct("component.behaviours",d.objOf(l),j.behaviours).fold(function(a){throw new i(d.formatError(a)+"\nComplete spec:\n"+h.stringify(j,null,2))},f.identity);return{list:k,data:g.map(m,function(a){var b=a();return f.constant(b.map(function(a){return{config:a.config(),state:a.state().init(a.config())}}))})}},k=function(a){return a.list},l=function(a){return a.data};return{generateFrom:j,getBehaviours:k,getData:l}}),g("7p",["7q","13","x","w","u"],function(a,b,c,d,e){var f=function(a){var e=b.readOptFrom(a,"behaviours").getOr({}),f=c.filter(d.keys(e),function(a){return void 0!==e[a]});return c.map(f,function(b){return a.behaviours[b].me})},g=function(b,c){return a.generateFrom(b,c)},h=function(a){var b=f(a);return g(a,b)};return{generate:h,generateFrom:g}}),g("5q",["6q"],function(a){return a.exactly(["getSystem","config","spec","connect","disconnect","element","syncComponents","readState","components","events"])}),g("6a",["6q"],function(a){return a.exactly(["debugInfo","triggerFocus","triggerEvent","triggerEscape","addToWorld","removeFromWorld","addToGui","removeFromGui","build","getByUid","getByDom","broadcast","broadcastOn"])}),g("5r",["6a","12","6","u"],function(a,b,c,d){return function(e){var f=function(a){return function(){throw new d("The component must be in a context to send: "+a+"\n"+b.element(e().element())+" is not in context.")}};return a({debugInfo:c.constant("fake"),triggerEvent:f("triggerEvent"),triggerFocus:f("triggerFocus"),triggerEscape:f("triggerEscape"),build:f("build"),addToWorld:f("addToWorld"),removeFromWorld:f("removeFromWorld"),addToGui:f("addToGui"),removeFromGui:f("removeFromGui"),getByUid:f("getByUid"),getByDom:f("getByDom"),broadcast:f("broadcast"),broadcastOn:f("broadcastOn")})}}),g("92",["13","w"],function(a,b){var c=function(c,d){var e={};return b.each(c,function(c,f){b.each(c,function(b,c){var g=a.readOr(c,[])(e);e[c]=g.concat([d(f,b)])})}),e};return{byInnerKey:c}}),g("7r",["92","4a","13","x","w","v","6n","6","47"],function(a,b,c,d,e,f,g,h,i){var j=function(a,b){return{name:h.constant(a),modification:b}},k=function(a,b){var e=d.bind(a,function(a){return a.modification().getOr([])});return i.value(c.wrap(b,e))},l=function(a,b,e){return a.length>1?i.error('Multiple behaviours have tried to change DOM "'+b+'". The guilty behaviours are: '+g.stringify(d.map(a,function(a){return a.name()}))+". At this stage, this is not supported. Future releases might provide strategies for resolving this."):0===a.length?i.value({}):i.value(a[0].modification().fold(function(){return{}},function(a){return c.wrap(b,a)}))},m=function(a,b,c,e){return i.error("Mulitple behaviours have tried to change the _"+b+'_ "'+a+'". The guilty behaviours are: '+g.stringify(d.bind(e,function(a){return void 0!==a.modification().getOr({})[b]?[a.name()]:[]}),null,2)+". This is not currently supported.")},n=function(a,b){var f=d.foldl(a,function(d,f){var g=f.modification().getOr({});return d.bind(function(d){var f=e.mapToArray(g,function(e,f){return void 0!==d[f]?m(b,f,g,a):i.value(c.wrap(f,e))});return c.consolidate(f,d)})},i.value({}));return f.map(function(a){return c.wrap(b,a)})},o={classes:k,attributes:n,styles:n,domChildren:l,defChildren:l,innerHtml:l,value:l},p=function(g,h,k,l){var m=f.deepMerge({},h);d.each(k,function(a){m[a.name()]=a.exhibit(g,l)});var n=a.byInnerKey(m,j),p=e.map(n,function(a,b){return d.bind(a,function(a){return a.modification().fold(function(){return[]},function(b){return[a]})})}),q=e.mapToArray(p,function(a,b){return c.readOptFrom(o,b).fold(function(){return i.error("Unknown field type: "+b)},function(c){return c(a,b)})}),r=c.consolidate(q,{});return r.map(b.nu)};return{combine:p}}),g("93",["6n","47","u"],function(a,b,c){var d=function(d,e,f,g){var h=f.slice(0);try{var i=h.sort(function(b,f){var h=b[e](),i=f[e](),j=g.indexOf(h),k=g.indexOf(i);if(j===-1)throw new c("The ordering for "+d+" does not have an entry for "+h+".\nOrder specified: "+a.stringify(g,null,2));if(k===-1)throw new c("The ordering for "+d+" does not have an entry for "+i+".\nOrder specified: "+a.stringify(g,null,2));return j1?f.filter(b,function(b){return f.contains(a,function(a){return a.name()===b})}).join(" > "):a[0].name();return e.wrap(c,d.nu(h,i))})});return e.consolidate(c,{})};return{combine:q}}),g("7t",["4p","6o","4a","4r","4b","28","13","2d","x","6","v","u"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=function(b){return h.asStruct("custom.definition",h.objOfOnly([f.field("dom","dom",e.strict(),h.objOfOnly([f.strict("tag"),f.defaulted("styles",{}),f.defaulted("classes",[]),f.defaulted("attributes",{}),f.option("value"),f.option("innerHtml")])),f.strict("components"),f.strict("uid"),f.defaulted("events",{}),f.defaulted("apis",j.constant({})),f.field("eventOrder","eventOrder",e.mergeWith({"alloy.execute":["disabling","alloy.base.behaviour","toggling"],"alloy.focus":["alloy.base.behaviour","keying","focusing"],"alloy.system.init":["alloy.base.behaviour","disabling","toggling","representing"],input:["alloy.base.behaviour","representing","streaming","invalidating"],"alloy.system.detached":["alloy.base.behaviour","representing"]}),h.anyValue()),f.option("domModification"),a.snapshot("originalSpec"),f.defaulted("debug.sketcher","unknown")]),b)},n=function(a){return g.wrap(d.idAttr(),a.uid())},o=function(a){var c={tag:a.dom().tag(),classes:a.dom().classes(),attributes:k.deepMerge(n(a),a.dom().attributes()),styles:a.dom().styles(),domChildren:i.map(a.components(),function(a){return a.element()})};return b.nu(k.deepMerge(c,a.dom().innerHtml().map(function(a){return g.wrap("innerHtml",a)}).getOr({}),a.dom().value().map(function(a){return g.wrap("value",a)}).getOr({})))},p=function(a){return a.domModification().fold(function(){return c.nu({})},c.nu)},q=function(a){return a.apis()},r=function(a){return a.events()};return{toInfo:m,toDefinition:o,toModification:p,toApis:q,toEvents:r}}),g("86",["x","2e","4i","t"],function(a,b,c,d){var e=function(c,d){a.each(d,function(a){b.add(c,a)})},f=function(c,d){a.each(d,function(a){b.remove(c,a)})},g=function(c,d){a.each(d,function(a){b.toggle(c,a)})},h=function(c,d){return a.forall(d,function(a){return b.has(c,a)})},i=function(c,d){return a.exists(d,function(a){return b.has(c,a)})},j=function(a){for(var b=a.dom().classList,c=new d(b.length),e=0;e1?f.some(a.slice(1)):f.none()})},r=function(a){return b.readOptFrom(j.get(),a)},s=function(a){return b.readOptFrom(i.get(),a)},t=function(a){var b=l.get()(i.get());return c.difference(d.keys(b),a)},u=function(){return k.get().bind(s)},v=function(){return i.get()};return{setContents:o,expand:p,refresh:r,collapse:q,lookupMenu:s,otherMenus:t,getPrimary:u,getMenus:v,clear:m,isClear:n}}}),g("7x",["8s","p","5f","5j","32","1x","3f","3p","3c","2","s","94","5u","9a","98","99","13","x","6","v","w","y","14","11","2e","86","4t"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A){var B=function(s,B){var C=function(a,b){return u.map(b,function(b,c){var d=m.sketch(t.deepMerge(b,{value:c,items:b.items,markers:q.narrow(B.markers,["item","selectedItem"]),fakeFocus:s.fakeFocus(),onHighlight:s.onHighlight(),focusManager:s.fakeFocus()?l.highlights():l.dom()}));return a.getSystem().build(d)})},D=n(),E=function(a){var b=C(a,s.data().menus());return D.setContents(s.data().primary(),b,s.data().expansions(),function(b){return G(a,b)}),D.getPrimary()},F=function(a){return g.getValue(a).value},G=function(a,b){return u.map(s.data().menus(),function(a,b){return r.bind(a.items,function(a){return"separator"===a.type?[]:[a.data.value]})})},H=function(a,b){d.highlight(a,b),d.getHighlighted(b).orThunk(function(){return d.getFirst(b)}).each(function(b){j.dispatch(a,b.element(),k.focusItem())})},I=function(a,b){return w.cat(r.map(b,a.lookupMenu))},J=function(a,b,c){return v.from(c[0]).bind(b.lookupMenu).map(function(d){var e=I(b,c.slice(1));r.each(e,function(a){y.add(a.element(),s.markers().backgroundMenu())}),x.inBody(d.element())||f.append(a,h.premade(d)),z.remove(d.element(),[s.markers().backgroundMenu()]),H(a,d);var g=I(b,b.otherMenus(c));return r.each(g,function(b){z.remove(b.element(),[s.markers().backgroundMenu()]),s.stayInDom()||f.remove(a,b)}),d})},K=function(a,b){var c=F(b);return D.expand(c).bind(function(c){return v.from(c[0]).bind(D.lookupMenu).each(function(c){x.inBody(c.element())||f.append(a,h.premade(c)),s.onOpenSubmenu()(a,b,c),d.highlightFirst(c)}),J(a,D,c)})},L=function(a,b){var c=F(b);return D.collapse(c).bind(function(c){return J(a,D,c).map(function(c){return s.onCollapseMenu()(a,b,c),c})})},M=function(a,b){var c=F(b);return D.refresh(c).bind(function(b){return J(a,D,b)})},N=function(b,c){return a.inside(c.element())?v.none():K(b,c)},O=function(b,c){return a.inside(c.element())?v.none():L(b,c)},P=function(a,b){return L(a,b).orThunk(function(){return s.onEscape()(a,b)})},Q=function(a){return function(b,c){return A.closest(c.getSource(),"."+s.markers().item()).bind(function(c){return b.getSystem().getByDom(c).bind(function(c){return a(b,c)})})}},R=i.derive([i.run(p.focus(),function(a,b){var c=b.event().menu();d.highlight(a,c)}),i.runOnExecute(function(a,b){var c=b.event().target();return a.getSystem().getByDom(c).bind(function(b){var c=F(b);return 0===c.indexOf("collapse-item")?L(a,b):K(a,b).orThunk(function(){return s.onExecute()(a,b)})})}),i.runOnAttached(function(a,b){E(a).each(function(b){f.append(a,h.premade(b)),s.openImmediately()&&(H(a,b),s.onOpenMenu()(a,b))})})].concat(s.navigateOnHover()?[i.run(o.hover(),function(a,b){var c=b.event().item();M(a,c),K(a,c),s.onHover()(a,c)})]:[])),S=function(a){d.getHighlighted(a).each(function(b){d.getHighlighted(b).each(function(b){L(a,b)})})};return{uid:s.uid(),dom:s.dom(),behaviours:t.deepMerge(b.derive([e.config({mode:"special",onRight:Q(N),onLeft:Q(O),onEscape:Q(P),focusIn:function(a,b){D.getPrimary().each(function(b){j.dispatch(a,b.element(),k.focusItem())})}}),d.config({highlightClass:s.markers().selectedMenu(),itemClass:s.markers().menu()}),c.config({find:function(a){return d.getHighlighted(a)}}),f.config({})]),s.tmenuBehaviours()),eventOrder:s.eventOrder(),apis:{collapseMenu:S},events:R}};return{make:B,collapseItem:s.constant("collapse-item")}}),g("5v",["33","4p","7x","28","13","3e"],function(a,b,c,d,e,f){var g=function(a,b,c){return{primary:a,menus:b,expansions:c}},h=function(a,b){return{primary:a,menus:e.wrap(a,b),expansions:{}}},i=function(a){return{value:f.generate(c.collapseItem()),text:a}};return a.single({name:"TieredMenu",configFields:[b.onStrictKeyboardHandler("onExecute"),b.onStrictKeyboardHandler("onEscape"),b.onStrictHandler("onOpenMenu"),b.onStrictHandler("onOpenSubmenu"),b.onHandler("onCollapseMenu"),d.defaulted("openImmediately",!0),d.strictObjOf("data",[d.strict("primary"),d.strict("menus"),d.strict("expansions")]),d.defaulted("fakeFocus",!1),b.onHandler("onHighlight"),b.onHandler("onHover"),b.tieredMenuMarkers(),d.strict("dom"),d.defaulted("navigateOnHover",!0),d.defaulted("stayInDom",!1),d.defaulted("tmenuBehaviours",{}),d.defaulted("eventOrder",{})],apis:{collapseMenu:function(a,b){a.collapseMenu(b)}},factory:c.make,extraApis:{tieredData:g,singleData:h,collapseItem:i}})}),g("3y",["6","2e","h"],function(a,b,c){var d=c.resolve("scrollable"),e=function(a){b.add(a,d)},f=function(a){b.remove(a,d)};return{register:e,deregister:f,scrollable:a.constant(d)}}),g("3l",["3t","p","3f","1f","5o","3p","1g","3c","1m","5u","5v","13","x","v","w","38","4t","5m","1k","h","3y"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u){var v=function(a){return l.readOptFrom(a,"format").getOr(a.title)},w=function(a,b){var c=y("Styles",[].concat(m.map(a.items,function(b){ +return x(v(b),b.title,b.isSelected(),b.getPreview(),l.hasKey(a.expansions,v(b)))})),b,!1),d=o.map(a.menus,function(c,d){var e=m.map(c,function(b){return x(v(b),b.title,void 0!==b.isSelected&&b.isSelected(),void 0!==b.getPreview?b.getPreview():"",l.hasKey(a.expansions,v(b)))});return y(d,e,b,!0)}),e=n.deepMerge(d,l.wrap("styles",c)),f=k.tieredData("styles",e,a.expansions);return{tmenu:f}},x=function(a,c,e,f,g){return{data:{value:a,text:c},type:"item",dom:{tag:"div",classes:g?[t.resolve("styles-item-is-menu")]:[]},toggling:{toggleOnExecute:!1,toggleClass:t.resolve("format-matches"),selected:e},itemBehaviours:b.derive(g?[]:[s.format(a,function(a,b){var c=b?d.on:d.off;c(a)})]),components:[{dom:{tag:"div",attributes:{style:f},innerHtml:c}}]}},y=function(c,d,g,l){return{value:c,dom:{tag:"div"},components:[i.sketch({dom:{tag:"div",classes:[t.resolve("styles-collapser")]},components:l?[{dom:{tag:"span",classes:[t.resolve("styles-collapse-icon")]}},f.text(c)]:[f.text(c)],action:function(a){if(l){var b=g().get(a);k.collapseMenu(b)}}}),{dom:{tag:"div",classes:[t.resolve("styles-menu-items-container")]},components:[j.parts().items({})],behaviours:b.derive([a.config("adhoc-scrollable-menu",[h.runOnAttached(function(a,b){p.set(a.element(),"overflow-y","auto"),p.set(a.element(),"-webkit-overflow-scrolling","touch"),u.register(a.element())}),h.runOnDetached(function(a){p.remove(a.element(),"overflow-y"),p.remove(a.element(),"-webkit-overflow-scrolling"),u.deregister(a.element())})])])}],items:d,menuBehaviours:b.derive([e.config({initialState:"after",routes:e.createTristate("before","current","after",{transition:{property:"transform",transitionClass:"transitioning"}})})])}},z=function(a){var b=w(a.formats,function(){return d}),d=g.record(k.sketch({dom:{tag:"div",classes:[t.resolve("styles-menu")]},components:[],fakeFocus:!0,stayInDom:!0,onExecute:function(b,d){var e=c.getValue(d);a.handle(d,e.value)},onEscape:function(){},onOpenMenu:function(a,b){var c=r.get(a.element());r.set(b.element(),c),e.jumpTo(b,"current")},onOpenSubmenu:function(a,b,c){var d=r.get(a.element()),f=q.ancestor(b.element(),'[role="menu"]').getOrDie("hacky"),g=a.getSystem().getByDom(f).getOrDie();r.set(c.element(),d),e.progressTo(g,"before"),e.jumpTo(c,"after"),e.progressTo(c,"current")},onCollapseMenu:function(a,b,c){var d=q.ancestor(b.element(),'[role="menu"]').getOrDie("hacky"),f=a.getSystem().getByDom(d).getOrDie();e.progressTo(f,"after"),e.progressTo(c,"current")},navigateOnHover:!1,openImmediately:!0,data:b.tmenu,markers:{backgroundMenu:t.resolve("styles-background-menu"),menu:t.resolve("styles-menu"),selectedMenu:t.resolve("styles-selected-menu"),item:t.resolve("styles-item"),selectedItem:t.resolve("styles-selected-item")}}));return d.asSpec()};return{sketch:z}}),g("3m",["13","x","v"],function(a,b,c){var d=function(b){var d=c.deepMerge(a.exclude(b,["items"]),{menu:!0}),e=f(b.items,b.title),g=c.deepMerge(e.menus,a.wrap(b.title,e.items)),h=c.deepMerge(e.expansions,a.wrap(b.title,b.title));return{item:d,menus:g,expansions:h}},e=function(b){return a.hasKey(b,"items")?d(b):{item:b,menus:{},expansions:{}}},f=function(a){return b.foldr(a,function(a,b){var d=e(b);return{menus:c.deepMerge(a.menus,d.menus),items:[d.item].concat(a.items),expansions:c.deepMerge(a.expansions,d.expansions)}},{menus:{},expansions:{},items:[]})};return{expand:f}}),g("1s",["1f","13","x","6","3e","v","3k","3l","3m"],function(a,b,c,d,e,f,g,h,i){var j=function(a,h){var i=function(b){return function(){return a.formatter.match(b)}},j=function(b){return function(){var c=a.formatter.getCssText(b);return c}},k=function(a){return f.deepMerge(a,{isSelected:i(a.format),getPreview:j(a.format)})},l=function(a){return f.deepMerge(a,{isSelected:d.constant(!1),getPreview:d.constant("")})},m=function(b){var c=e.generate(b.title),d=f.deepMerge(b,{format:c,isSelected:i(c),getPreview:j(c)});return a.formatter.register(c,d),d},n=b.readOptFrom(h,"style_formats").getOr(g),o=function(a){return c.map(a,function(a){if(b.hasKey(a,"items")){var c=o(a.items);return f.deepMerge(l(a),{items:c})}return b.hasKey(a,"format")?k(a):m(a)})};return o(n)},k=function(a,d){var e=function(d){return c.bind(d,function(c){if(void 0!==c.items){var d=e(c.items);return d.length>0?[c]:[]}var f=!b.hasKey(c,"format")||a.formatter.canApply(c.format);return f?[c]:[]})},f=e(d);return i.expand(f)},l=function(b,c,d){var e=k(b,c);return h.sketch({formats:e,handle:function(c,e){b.undoManager.transact(function(){a.isOn(c)?b.formatter.remove(e):b.formatter.apply(e)}),d()}})};return{register:j,ui:l}}),g("g",["p","1e","1f","1g","13","x","6","y","1h","1i","1j","1k","f","h","k","1o","1p","1q","1r","1s"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t){var u=["undo","bold","italic","link","image","bullist","styleselect"],v=function(a){var b=a.replace(/\|/g," ").trim();return b.length>0?b.split(/\s+/):[]},w=function(a){return f.bind(a,function(a){return i.isArray(a)?w(a):v(a)})},x=function(a){var b=void 0!==a.toolbar?a.toolbar:u;return i.isArray(b)?w(b):v(b)},y=function(d,f){var g=function(a){return function(){return o.forToolbarCommand(f,a)}},i=function(a){return function(){return o.forToolbarStateCommand(f,a)}},j=function(a,b,c){return function(){return o.forToolbarStateAction(f,a,b,c)}},k=g("undo"),u=g("redo"),v=i("bold"),w=i("italic"),x=i("underline"),y=g("removeformat"),z=function(){return s.sketch(d,f)},A=j("unlink","link",function(){f.execCommand("unlink",null,!1)}),B=function(){return r.sketch(f)},C=j("unordered-list","ul",function(){f.execCommand("InsertUnorderedList",null,!1)}),D=j("ordered-list","ol",function(){f.execCommand("InsertOrderedList",null,!1)}),E=function(){return q.sketch(d,f)},F=function(){return p.sketch(d,f)},G=t.register(f,f.settings),H=function(){return t.ui(f,G,function(){f.fire("scrollIntoView")})},I=function(){return o.forToolbar("style-formats",function(a){f.fire("toReading"),d.dropup().appear(H,c.on,a)},a.derive([c.config({toggleClass:n.resolve("toolbar-button-selected"),toggleOnExecute:!1,aria:{mode:"pressed"}}),b.config({channels:e.wrapAll([l.receive(m.orientationChanged(),c.off),l.receive(m.dropupDismissed(),c.off)])})]))},J=function(a,b){return{isSupported:function(){return a.forall(function(a){return e.hasKey(f.buttons,a)})},sketch:b}};return{undo:J(h.none(),k),redo:J(h.none(),u),bold:J(h.none(),v),italic:J(h.none(),w),underline:J(h.none(),x),removeformat:J(h.none(),y),link:J(h.none(),z),unlink:J(h.none(),A),image:J(h.none(),B),bullist:J(h.some("bullist"),C),numlist:J(h.some("numlist"),D),fontsizeselect:J(h.none(),E),forecolor:J(h.none(),F),styleselect:J(h.none(),I)}},z=function(a,b){var c=x(a),d={};return f.bind(c,function(a){var c=!e.hasKey(d,a)&&e.hasKey(b,a)&&b[a].isSupported()?[b[a].sketch()]:[];return d[a]=!0,c})};return{identify:x,setup:y,detect:z}}),g("3n",["6","a"],function(a,b){var c=function(b,c,d,e,f,g,h){return{target:a.constant(b),x:a.constant(c),y:a.constant(d),stop:e,prevent:f,kill:g,raw:a.constant(h)}},d=function(d,e){return function(f){if(d(f)){var g=b.fromDom(f.target),h=function(){f.stopPropagation()},i=function(){f.preventDefault()},j=a.compose(i,h),k=c(g,f.clientX,f.clientY,h,i,j,f);e(k)}}},e=function(b,c,e,f,g){var i=d(e,f);return b.dom().addEventListener(c,i,g),{unbind:a.curry(h,b,c,i,g)}},f=function(a,b,c,d){return e(a,b,c,d,!1)},g=function(a,b,c,d){return e(a,b,c,d,!0)},h=function(a,b,c,d){a.dom().removeEventListener(b,c,d)};return{bind:f,capture:g}}),g("1t",["6","3n"],function(a,b){var c=a.constant(!0),d=function(a,d,e){return b.bind(a,d,c,e)},e=function(a,d,e){return b.capture(a,d,c,e)};return{bind:d,capture:e}}),h("1u",clearInterval),h("1w",setInterval),g("i",["6","y","7","1t","a","1u","1v","1w"],function(a,b,c,d,e,f,g,h){var i=50,j=1e3/i,k=function(b){var c=b.matchMedia("(orientation: portrait)").matches;return{isPortrait:a.constant(c)}},l=function(a){var b=c.detect().os.isiOS(),d=k(a).isPortrait();return b&&!d?a.screen.height:a.screen.width},m=function(a,c){var g=e.fromDom(a),l=null,m=function(){f(l);var b=k(a);c.onChange(b),o(function(){c.onReady(b)})},n=d.bind(g,"orientationchange",m),o=function(c){f(l);var d=a.innerHeight,e=0;l=h(function(){d!==a.innerHeight?(f(l),c(b.some(a.innerHeight))):e>j&&(f(l),c(b.none())),e++},i)},p=function(){n.unbind()};return{onAdjustment:o,destroy:p}};return{get:k,onChange:m,getActualWidth:l}}),h("83",clearTimeout),g("9b",["83","1i"],function(a,b){return function(c,d){var e=null,f=function(){var a=arguments;e=b(function(){c.apply(null,a),e=null},d)},g=function(){null!==e&&(a(e),e=null)};return{cancel:g,schedule:f}}}),g("89",["9b","2f","s","13","5","6","y","19","1v"],function(a,b,c,d,e,f,g,h,i){var j=5,k=400,l=function(a){return void 0===a.raw().touches||1!==a.raw().touches.length?g.none():g.some(a.raw().touches[0])},m=function(a,b){var c=i.abs(a.clientX-b.x()),d=i.abs(a.clientY-b.y());return c>j||d>j},n=function(i){var j=e(g.none()),n=a(function(a){j.set(g.none()),i.triggerEvent(c.longpress(),a)},k),o=function(a){return l(a).each(function(b){n.cancel();var c={x:f.constant(b.clientX),y:f.constant(b.clientY),target:a.target};n.schedule(c),j.set(g.some(c))}),g.none()},p=function(a){return n.cancel(),l(a).each(function(a){j.get().each(function(b){m(a,b)&&j.set(g.none())})}),g.none()},q=function(a){n.cancel();var b=function(b){return h.eq(b.target(),a.target())};return j.get().filter(b).map(function(b){return i.triggerEvent(c.tap(),a)})},r=d.wrapAll([{key:b.touchstart(),value:o},{key:b.touchmove(),value:p},{key:b.touchend(),value:q}]),s=function(a,b){return d.readOptFrom(r,b).bind(function(b){return b(a)})};return{fireIfReady:s}};return{monitor:n}}),g("7y",["89","1t"],function(a,b){var c=function(c){var d=a.monitor({triggerEvent:function(a,b){c.onTapContent(b)}}),e=function(){return b.bind(c.body(),"touchend",function(a){d.fireIfReady(a,"touchend")})},f=function(){return b.bind(c.body(),"touchmove",function(a){d.fireIfReady(a,"touchmove")})},g=function(a){d.fireIfReady(a,"touchstart")};return{fireTouchstart:g,onTouchend:e,onTouchmove:f}};return{monitor:c}}),g("5x",["1f","x","6","7","19","8","1t","a","b","z","7y"],function(a,b,c,d,e,f,g,h,i,j,k){var l=d.detect().os.version.major>=6,m=function(d,m,n){var o=k.monitor(d),p=j.owner(m),q=function(a){return!e.eq(a.start(),a.finish())||a.soffset()!==a.foffset()},r=function(){return f.active(p).filter(function(a){return"input"===i.name(a)}).exists(function(a){return a.dom().selectionStart!==a.dom().selectionEnd})},s=function(){var b=d.doc().dom().hasFocus()&&d.getSelection().exists(q);n.getByDom(m).each((b||r())===!0?a.on:a.off)},t=[g.bind(d.body(),"touchstart",function(a){d.onTouchContent(),o.fireTouchstart(a)}),o.onTouchmove(),o.onTouchend(),g.bind(m,"touchstart",function(a){d.onTouchToolstrip()}),d.onToReading(function(){f.blur(d.body())}),d.onToEditing(c.noop),d.onScrollToCursor(function(a){a.preventDefault(),d.getCursorBox().each(function(a){var b=d.win(),c=a.top()>b.innerHeight||a.bottom()>b.innerHeight,e=c?a.bottom()-b.innerHeight+50:0;0!==e&&b.scrollTo(b.pageXOffset,b.pageYOffset+e)})})].concat(l===!0?[]:[g.bind(h.fromDom(d.win()),"blur",function(){n.getByDom(m).each(a.off)}),g.bind(p,"select",s),g.bind(d.doc(),"selectionchange",s)]),u=function(){b.each(t,function(a){a.unbind()})};return{destroy:u}};return{initEvents:m}}),g("7z",["x","6","8","a","b","1i"],function(a,b,c,d,e,f){var g=function(){return function(a){f(function(){a()},0)}},h=function(f){f.focus();var h=d.fromDom(f.document.body),i=c.active().exists(function(b){return a.contains(["input","textarea"],e.name(b))}),j=i?g(h):b.apply;j(function(){c.active().each(c.blur),c.focus(h)})};return{resume:h}}),h("9c",isNaN),h("8e",parseInt),g("80",["4h","9c","8e"],function(a,b,c){var d=function(d,e){var f=c(a.get(d,e),10);return b(f)?0:f};return{safeParse:d}}),g("a7",["7","y","u"],function(a,b,c){return function(d,e){var f=function(a){if(!d(a))throw new c("Can only get "+e+" value of a "+e+" node");return j(a).getOr("")},g=function(a){try{return h(a)}catch(a){return b.none()}},h=function(a){return d(a)?b.from(a.dom().nodeValue):b.none()},i=a.detect().browser,j=i.isIE()&&10===i.version.major?g:h,k=function(a,b){if(!d(a))throw new c("Can only set raw "+e+" value of a "+e+" node");a.dom().nodeValue=b};return{get:f,getOption:j,set:k}}}),g("a1",["b","a7"],function(a,b){var c=b(a.isText,"text"),d=function(a){return c.get(a)},e=function(a){return c.getOption(a)},f=function(a,b){c.set(a,b)};return{get:d,getOption:e,set:f}}),g("9d",["x","b","a1","z"],function(a,b,c,d){var e=function(a){return"img"===b.name(a)?1:c.getOption(a).fold(function(){return d.children(a).length},function(a){return a.length})},f=function(a,b){return e(a)===b},g=function(a,b){return 0===b},h="\xa0",i=function(a){return c.getOption(a).filter(function(a){return 0!==a.trim().length||a.indexOf(h)>-1}).isSome()},j=["img","br"],k=function(c){var d=i(c);return d||a.contains(j,b.name(c))};return{getEnd:e,isEnd:f,isStart:g,isCursorPosition:k}}),g("9e",["6k","2n"],function(a,b){var c=a.generate([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),d=b.immutable("start","soffset","finish","foffset"),e=function(a){return c.exact(a.start(),a.soffset(),a.finish(),a.foffset())};return{domRange:c.domRange,relative:c.relative,exact:c.exact,exactFromRange:e,range:d}}),g("9f",["19","a","z"],function(a,b,c){var d=function(a,b,d,e){var f=c.owner(a),g=f.dom().createRange();return g.setStart(a.dom(),b),g.setEnd(d.dom(),e),g},e=function(a,c,e,f){var g=d(a,c,e,f);return b.fromDom(g.commonAncestorContainer)},f=function(b,c,e,f){var g=d(b,c,e,f),h=a.eq(b,e)&&c===f;return g.collapsed&&!h};return{after:f,commonAncestorContainer:e}}),g("9g",["x","a","1a"],function(a,b,c){var d=function(d,e){var f=e||c,g=f.createDocumentFragment();return a.each(d,function(a){g.appendChild(a.dom())}),b.fromDom(g)};return{fromElements:d}}),g("9h",["6k"],function(a){var b=a.generate([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),c=function(a,b,c,d){return a.fold(b,c,d)};return{before:b.before,on:b.on,after:b.after,cata:c}}),g("9i",["6","y","19","a"],function(a,b,c,d){var e=function(a,b){var c=a.document.createRange();return f(c,b),c},f=function(a,b){a.selectNodeContents(b.dom())},g=function(a,b){return b.compareBoundaryPoints(a.END_TO_START,a)<1&&b.compareBoundaryPoints(a.START_TO_END,a)>-1},h=function(a){return a.document.createRange()},i=function(a,b){b.fold(function(b){a.setStartBefore(b.dom())},function(b,c){a.setStart(b.dom(),c)},function(b){a.setStartAfter(b.dom())})},j=function(a,b){b.fold(function(b){a.setEndBefore(b.dom())},function(b,c){a.setEnd(b.dom(),c)},function(b){a.setEndAfter(b.dom())})},k=function(a,b){o(a),a.insertNode(b.dom())},l=function(a,b,d,e){return c.eq(a,d)&&b===e},m=function(a,b,c){var d=a.document.createRange();return i(d,b),j(d,c),d},n=function(a,b,c,d,e){var f=a.document.createRange();return f.setStart(b.dom(),c),f.setEnd(d.dom(),e),f},o=function(a){a.deleteContents()},p=function(a){var b=a.cloneContents();return d.fromDom(b)},q=function(b){return{left:a.constant(b.left),top:a.constant(b.top),right:a.constant(b.right),bottom:a.constant(b.bottom),width:a.constant(b.width),height:a.constant(b.height)}},r=function(a){var c=a.getClientRects(),d=c.length>0?c[0]:a.getBoundingClientRect();return d.width>0||d.height>0?b.some(d).map(q):b.none()},s=function(a){var c=a.getBoundingClientRect();return c.width>0||c.height>0?b.some(c).map(q):b.none()},t=function(a){return a.toString()};return{create:h,replaceWith:k,selectNodeContents:e,selectNodeContentsUsing:f,isCollapsed:l,relativeToNative:m,exactToNative:n,deleteContents:o,cloneFragment:p,getFirstRect:r,getBounds:s,isWithin:g,toString:t}}),g("9j",["6k","6","y","16","a","9i"],function(a,b,c,d,e,f){var g=a.generate([{ltr:["start","soffset","finish","foffset"]},{rtl:["start","soffset","finish","foffset"]}]),h=function(a,b,c){return b(e.fromDom(c.startContainer),c.startOffset,e.fromDom(c.endContainer),c.endOffset)},i=function(a,e){return e.match({domRange:function(a){return{ltr:b.constant(a),rtl:c.none}},relative:function(b,e){return{ltr:d.cached(function(){return f.relativeToNative(a,b,e)}),rtl:d.cached(function(){return c.some(f.relativeToNative(a,e,b))})}},exact:function(b,e,g,h){return{ltr:d.cached(function(){return f.exactToNative(a,b,e,g,h)}),rtl:d.cached(function(){return c.some(f.exactToNative(a,g,h,b,e))})}}})},j=function(a,b){var c=b.ltr();if(c.collapsed){var d=b.rtl().filter(function(a){return a.collapsed===!1});return d.map(function(a){return g.rtl(e.fromDom(a.endContainer),a.endOffset,e.fromDom(a.startContainer),a.startOffset)}).getOrThunk(function(){return h(a,g.ltr,c)})}return h(a,g.ltr,c)},k=function(a,b){var c=i(a,b);return j(a,c)},l=function(a,b){var c=k(a,b);return c.match({ltr:function(b,c,d,e){var f=a.document.createRange();return f.setStart(b.dom(),c),f.setEnd(d.dom(),e),f},rtl:function(b,c,d,e){var f=a.document.createRange();return f.setStart(d.dom(),e),f.setEnd(b.dom(),c),f}})};return{ltr:g.ltr,rtl:g.rtl,diagnose:k,asLtrRange:l}}),g("a8",["1v"],function(a){var b=function(b,c,d,e,f){if(0===f)return 0;if(c===e)return f-1;for(var g=e,h=1;hi.bottom);else{if(dg)return h-1;g=j}}return 0},c=function(a,b,c){return b>=a.left&&b<=a.right&&c>=a.top&&c<=a.bottom};return{inRect:c,searchForPoint:b}}),g("a9",["y","14","a1","a8","1v"],function(a,b,c,d,e){var f=function(a,b,e,f,g){var h=function(c){var d=a.dom().createRange();return d.setStart(b.dom(),c),d.collapse(!0),d},i=function(a){var b=h(a);return b.getBoundingClientRect()},j=c.get(b).length,k=d.searchForPoint(i,e,f,g.right,j);return h(k)},g=function(c,e,g,h){var i=c.dom().createRange();i.selectNode(e.dom());var j=i.getClientRects(),k=b.findMap(j,function(b){return d.inRect(b,g,h)?a.some(b):a.none()});return k.map(function(a){return f(c,e,g,h,a)})};return{locate:g}}),g("a2",["y","14","b","z","a8","a9","1v"],function(a,b,c,d,e,f,g){var h=function(c,f,g,h){var j=c.dom().createRange(),k=d.children(f);return b.findMap(k,function(b){return j.selectNode(b.dom()),e.inRect(j.getBoundingClientRect(),g,h)?i(c,b,g,h):a.none()})},i=function(a,b,d,e){var g=c.isText(b)?f.locate:h;return g(a,b,d,e)},j=function(a,b,c,d){var e=a.dom().createRange();e.selectNode(b.dom());var f=e.getBoundingClientRect(),h=g.max(f.left,g.min(f.right,c)),j=g.max(f.top,g.min(f.bottom,d));return i(a,b,h,j)};return{locate:j}}),g("aa",["y","2t","z","9d"],function(a,b,c,d){var e=function(a){return b.descendant(a,d.isCursorPosition)},f=function(a){return g(a,d.isCursorPosition)},g=function(b,d){var e=function(b){for(var f=c.children(b),g=f.length-1;g>=0;g--){var h=f[g];if(d(h))return a.some(h);var i=e(h);if(i.isSome())return i}return a.none()};return e(b)};return{first:e,last:f}}),g("a3",["y","z","aa"],function(a,b,c){var d=!0,e=!1,f=function(a,b){return b-a.left0?r(c):a.none()},v=function(a){return u(a).map(function(a){return e.exact(a.start(),a.soffset(),a.finish(),a.foffset())})},w=function(a,b){var c=h.asLtrRange(a,b);return g.getFirstRect(c)},x=function(a,b){var c=h.asLtrRange(a,b);return g.getBounds(c)},y=function(a,b,c){return i.fromPoint(a,b,c)},z=function(a,b){var c=h.asLtrRange(a,b);return g.toString(c)},A=function(a){var b=a.getSelection();b.removeAllRanges()},B=function(a,b){var c=h.asLtrRange(a,b);return g.cloneFragment(c)},C=function(a,b,c){var e=h.asLtrRange(a,b),f=d.fromElements(c);g.replaceWith(e,f)},D=function(a,b){var c=h.asLtrRange(a,b);g.deleteContents(c)};return{setExact:o,getExact:u,get:v,setRelative:p,setToElement:s,clear:A,clone:B,replace:C,deleteAt:D,forElement:t,getFirstRect:w,getBounds:x,getAtPoint:y,findWithin:n,getAsString:z}}),g("81",["x","6","a","z","9d","9e","82"],function(a,b,c,d,e,f,g){var h=2,i=function(a){return{left:a.left,top:a.top,right:a.right,bottom:a.bottom,width:b.constant(h),height:a.height}},j=function(a){return{left:b.constant(a.left),top:b.constant(a.top),right:b.constant(a.right),bottom:b.constant(a.bottom),width:b.constant(a.width),height:b.constant(a.height)}},k=function(b){if(b.collapsed){var h=c.fromDom(b.startContainer);return d.parent(h).bind(function(c){var d=f.exact(h,b.startOffset,c,e.getEnd(c)),j=g.getFirstRect(b.startContainer.ownerDocument.defaultView,d);return j.map(i).map(a.pure)}).getOr([])}return a.map(b.getClientRects(),j)},l=function(a){var b=a.getSelection();return void 0!==b&&b.rangeCount>0?k(b.getRangeAt(0)):[]};return{getRectangles:l}}),g("5y",["6","y","1t","a","4h","1v","7z","h","80","81"],function(a,b,c,d,e,f,g,h,i,j){var k=50,l="data-"+h.resolve("last-outer-height"),m=function(a,b){e.set(a,l,b)},n=function(a){return i.safeParse(a,l)},o=function(b){return{top:a.constant(b.top()),bottom:a.constant(b.top()+b.height())}},p=function(a){var c=j.getRectangles(a);return c.length>0?b.some(c[0]).map(o):b.none()},q=function(a,c){var d=n(c),e=a.innerHeight;return d>e?b.some(d-e):b.none()},r=function(a,b,c){var d=b.top()>a.innerHeight||b.bottom()>a.innerHeight;return d?f.min(c,b.bottom()-a.innerHeight+k):0},s=function(a,b){var e=d.fromDom(b.document.body),f=function(){g.resume(b)},h=c.bind(d.fromDom(a),"resize",function(){q(a,e).each(function(a){p(b).each(function(c){var d=r(b,c,a);0!==d&&b.scrollTo(b.pageXOffset,b.pageYOffset+d)})}),m(e,a.innerHeight)});m(e,a.innerHeight);var i=function(){h.unbind()};return{toEditing:f,destroy:i}};return{setup:s}}),g("5z",["6","y","19","1t","a","82"],function(a,b,c,d,e,f){var g=function(a){return b.some(e.fromDom(a.dom().contentWindow.document.body))},h=function(a){return b.some(e.fromDom(a.dom().contentWindow.document))},i=function(a){return b.from(a.dom().contentWindow)},j=function(a){var b=i(a);return b.bind(f.getExact)},k=function(a){return a.getFrame()},l=function(a,b){return function(c){var d=c[a].getOrThunk(function(){var a=k(c);return function(){return b(a)}});return d()}},m=function(a,b,c,e){return a[c].getOrThunk(function(){return function(a){return d.bind(b,e,a)}})},n=function(b){return{left:a.constant(b.left),top:a.constant(b.top),right:a.constant(b.right),bottom:a.constant(b.bottom),width:a.constant(b.width),height:a.constant(b.height)}},o=function(d){var l=k(d),o=function(a){var d=function(a){return c.eq(a.start(),a.finish())&&a.soffset()===a.foffset()},e=function(a){var c=a.start().dom().getBoundingClientRect();return c.width>0||c.height>0?b.some(c).map(n):b.none()};return f.getExact(a).filter(d).bind(e)};return g(l).bind(function(b){return h(l).bind(function(c){return i(l).map(function(g){var h=e.fromDom(c.dom().documentElement),i=d.getCursorBox.getOrThunk(function(){return function(){return f.get(g).bind(function(a){return f.getFirstRect(g,a).orThunk(function(){return o(g)})})}}),k=d.setSelection.getOrThunk(function(){return function(a,b,c,d){f.setExact(g,a,b,c,d)}}),n=d.clearSelection.getOrThunk(function(){return function(){f.clear(g)}});return{body:a.constant(b),doc:a.constant(c),win:a.constant(g),html:a.constant(h),getSelection:a.curry(j,l),setSelection:k,clearSelection:n,frame:a.constant(l),onKeyup:m(d,c,"onKeyup","keyup"),onNodeChanged:m(d,c,"onNodeChanged","selectionchange"),onDomChanged:d.onDomChanged,onScrollToCursor:d.onScrollToCursor,onScrollToElement:d.onScrollToElement,onToReading:d.onToReading,onToEditing:d.onToEditing,onToolbarScrollStart:d.onToolbarScrollStart,onTouchContent:d.onTouchContent,onTapContent:d.onTapContent,onTouchToolstrip:d.onTouchToolstrip,getCursorBox:i}})})})};return{getBody:l("getBody",g),getDoc:l("getDoc",h),getWin:l("getWin",i),getSelection:l("getSelection",j),getFrame:k,getActiveApi:o}}),g("60",["x","7","4h","38","5l"],function(a,b,c,d,e){var f="data-ephox-mobile-fullscreen-style",g="display:none!important;",h="position:absolute!important;",i="top:0!important;left:0!important;margin:0!important;padding:0!important;width:100%!important;",j="background-color:rgb(255,255,255)!important;",k=b.detect().os.isAndroid(),l=function(a){var b=d.get(a,"background-color");return void 0!==b&&""!==b?"background-color:"+b+"!important":j},m=function(b,d){var j=function(a){var b=e.siblings(a,"*");return b},m=function(a){return function(b){var d=c.get(b,"style"),e=void 0===d?"no-styles":d.trim();e!==a&&(c.set(b,f,e),c.set(b,"style",a))}},n=e.ancestors(b,"*"),o=a.bind(n,j),p=l(d);a.each(o,m(g)),a.each(n,m(h+i+p));var q=k===!0?"":h;m(q+i+p)(b)},n=function(){var b=e.all("["+f+"]");a.each(b,function(a){var b=c.get(a,f);"no-styles"!==b?c.set(a,"style",b):c.remove(a,"style"),c.remove(a,f)})};return{clobberStyles:m,restoreStyles:n}}),g("61",["9","a","4h","4t"],function(a,b,c,d){var e=function(){var e=d.first("head").getOrDie(),f=function(){var d=b.fromTag("meta");return c.set(d,"name","viewport"),a.append(e,d),d},g=d.first('meta[name="viewport"]').getOrThunk(f),h=c.get(g,"content"),i=function(){c.set(g,"content","width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0")},j=function(){void 0!==h&&null!==h&&h.length>0?c.set(g,"content",h):c.remove(g,"content")};return{maximize:i,restore:j}};return{tag:e}}),g("3q",["1y","2e","5x","5y","5z","60","h","61"],function(a,b,c,d,e,f,g,h){var i=function(i,j){var k=h.tag(),l=a.api(),m=a.api(),n=function(){j.hide(),b.add(i.container,g.resolve("fullscreen-maximized")),b.add(i.container,g.resolve("android-maximized")),k.maximize(),b.add(i.body,g.resolve("android-scroll-reload")),l.set(d.setup(i.win,e.getWin(i.editor).getOrDie("no"))),e.getActiveApi(i.editor).each(function(a){f.clobberStyles(i.container,a.body()),m.set(c.initEvents(a,i.toolstrip,i.alloy))})},o=function(){k.restore(),j.show(),b.remove(i.container,g.resolve("fullscreen-maximized")),b.remove(i.container,g.resolve("android-maximized")),f.restoreStyles(),b.remove(i.body,g.resolve("android-scroll-reload")),m.clear(),l.clear()};return{enter:n,exit:o}};return{create:i}}),g("3r",["28","2d","6","a","z","1j"],function(a,b,c,d,e,f){return b.objOf([a.strictObjOf("editor",[a.strict("getFrame"),a.option("getBody"),a.option("getDoc"),a.option("getWin"),a.option("getSelection"),a.option("setSelection"),a.option("clearSelection"),a.option("cursorSaver"),a.option("onKeyup"),a.option("onNodeChanged"),a.option("getCursorBox"),a.strict("onDomChanged"),a.defaulted("onTouchContent",c.noop),a.defaulted("onTapContent",c.noop),a.defaulted("onTouchToolstrip",c.noop),a.defaulted("onScrollToCursor",c.constant({unbind:c.noop})),a.defaulted("onScrollToElement",c.constant({unbind:c.noop})),a.defaulted("onToEditing",c.constant({unbind:c.noop})),a.defaulted("onToReading",c.constant({unbind:c.noop})),a.defaulted("onToolbarScrollStart",c.identity)]),a.strict("socket"),a.strict("toolstrip"),a.strict("dropup"),a.strict("toolbar"),a.strict("container"),a.strict("alloy"),a.state("win",function(a){return e.owner(a.socket).dom().defaultView}),a.state("body",function(a){return d.fromDom(a.socket.dom().ownerDocument.body)}),a.defaulted("translate",c.identity),a.defaulted("setReadOnly",c.noop)])}),g("62",["83","1i"],function(a,b){var c=function(c,d){var e=null,f=null,g=function(){null!==e&&(a(e),e=null,f=null)},h=function(){f=arguments,null===e&&(e=b(function(){c.apply(null,f),e=null,f=null},d))};return{cancel:g,throttle:h}},d=function(c,d){var e=null,f=function(){null!==e&&(a(e),e=null)},g=function(){var a=arguments;null===e&&(e=b(function(){c.apply(null,a),e=null,a=null},d))};return{cancel:f,throttle:g}},e=function(c,d){var e=null,f=function(){null!==e&&(a(e),e=null)},g=function(){var f=arguments;null!==e&&a(e),e=b(function(){c.apply(null,f),e=null,f=null},d)};return{cancel:f,throttle:g}};return{adaptable:c,first:d,last:e}}),g("3s",["p","1f","1g","1m","3u","62","1i","h","1n"],function(a,b,c,d,e,f,g,h,i){var j=function(g,j){var k=c.record(e.sketch({dom:i.dom(''),containerBehaviours:a.derive([b.config({toggleClass:h.resolve("mask-tap-icon-selected"),toggleOnExecute:!1})])})),l=f.first(g,200);return e.sketch({dom:i.dom('
    '),components:[e.sketch({dom:i.dom('
    '),components:[d.sketch({dom:i.dom('
    '),components:[k.asSpec()],action:function(a){l.throttle()},buttonBehaviours:a.derive([b.config({toggleClass:h.resolve("mask-tap-icon-selected")})])})]})]})};return{sketch:j}}),g("1z",["3p","2d","6","9","38","3q","3r","3s"],function(a,b,c,d,e,f,g,h){var i=function(i){var j=b.asRawOrDie("Getting AndroidWebapp schema",g,i);e.set(j.toolstrip,"width","100%");var k=function(){j.setReadOnly(!0),n.enter()},l=a.build(h.sketch(k,j.translate));j.alloy.add(l);var m={show:function(){j.alloy.add(l)},hide:function(){j.alloy.remove(l)}};d.append(j.container,l.element());var n=f.create(j,m);return{setReadOnly:j.setReadOnly,refreshStructure:c.noop, +enter:n.enter,exit:n.exit,destroy:c.noop}};return{produce:i}}),g("63",["p","1x","4p","52","28","6"],function(a,b,c,d,e,f){var g=[e.defaulted("shell",!0),e.defaulted("toolbarBehaviours",{})],h=function(c){return{behaviours:a.derive([b.config({})])}},i=[d.optional({name:"groups",overrides:h})];return{name:f.constant("Toolbar"),schema:f.constant(g),parts:f.constant(i)}}),g("3v",["p","1x","33","51","63","v","y","15","u"],function(a,b,c,d,e,f,g,h,i){var j=function(c,e,j,k){var l=function(a,c){m(a).fold(function(){throw h.error("Toolbar was defined to not be a shell, but no groups container was specified in components"),new i("Toolbar was defined to not be a shell, but no groups container was specified in components")},function(a){b.set(a,c)})},m=function(a){return c.shell()?g.some(a):d.getPart(a,c,"groups")},n=c.shell()?{behaviours:[b.config({})],components:[]}:{behaviours:[],components:e};return{uid:c.uid(),dom:c.dom(),components:n.components,behaviours:f.deepMerge(a.derive(n.behaviours),c.toolbarBehaviours()),apis:{setGroups:l},domModification:{attributes:{role:"group"}}}};return c.composite({name:"Toolbar",configFields:e.schema(),partFields:e.parts(),factory:j,apis:{setGroups:function(a,b,c){a.setGroups(b,c)}}})}),g("65",["4p","52","28","6"],function(a,b,c,d){var e=[c.strict("items"),a.markers(["itemClass"]),c.defaulted("hasTabstop",!0),c.defaulted("tgroupBehaviours",{})],f=[b.group({name:"items",unit:"item",overrides:function(a){return{domModification:{classes:[a.markers().itemClass()]}}}})];return{name:d.constant("ToolbarGroup"),schema:d.constant(e),parts:d.constant(f)}}),g("3w",["p","32","64","33","65","v","u"],function(a,b,c,d,e,f,g){var h=function(d,e,g,h){return f.deepMerge({dom:{attributes:{role:"toolbar"}}},{uid:d.uid(),dom:d.dom(),components:e,behaviours:f.deepMerge(a.derive([b.config({mode:"flow",selector:"."+d.markers().itemClass()}),d.hasTabstop()?c.config({}):c.revoke()]),d.tgroupBehaviours()),"debug.sketcher":g["debug.sketcher"]})};return d.composite({name:"ToolbarGroup",configFields:e.schema(),partFields:e.parts(),factory:h})}),g("3x",["6","1t","4h","4t","h"],function(a,b,c,d,e){var f="data-"+e.resolve("horizontal-scroll"),g=function(a){a.dom().scrollTop=1;var b=0!==a.dom().scrollTop;return a.dom().scrollTop=0,b},h=function(a){a.dom().scrollLeft=1;var b=0!==a.dom().scrollLeft;return a.dom().scrollLeft=0,b},i=function(a){return a.dom().scrollTop>0||g(a)},j=function(a){return a.dom().scrollLeft>0||h(a)},k=function(a){c.set(a,f,"true")},l=function(a){return"true"===c.get(a,f)?j:i},m=function(c,e){return b.bind(c,"touchmove",function(b){d.closest(b.target(),e).filter(l).fold(function(){b.raw().preventDefault()},a.noop)})};return{exclusive:m,markAsHorizontal:k}}),g("20",["3t","p","32","1f","3p","3c","3u","3v","3w","x","5","6","38","3x","h","3y","1n"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){return function(){var r=function(c){var d=c.scrollable===!0?"${prefix}-toolbar-scrollable-group":"";return{dom:q.dom('
    '),tgroupBehaviours:b.derive([a.config("adhoc-scrollable-toolbar",c.scrollable===!0?[f.runOnInit(function(a,b){m.set(a.element(),"overflow-x","auto"),n.markAsHorizontal(a.element()),p.register(a.element())})]:[])]),components:[g.sketch({components:[i.parts().items({})]})],markers:{itemClass:o.resolve("toolbar-group-item")},items:c.items}},s=e.build(h.sketch({dom:q.dom('
    '),components:[h.parts().groups({})],toolbarBehaviours:b.derive([d.config({toggleClass:o.resolve("context-toolbar"),toggleOnExecute:!1,aria:{mode:"none"}}),c.config({mode:"cyclic"})]),shell:!0})),t=e.build(g.sketch({dom:{classes:[o.resolve("toolstrip")]},components:[e.premade(s)],containerBehaviours:b.derive([d.config({toggleClass:o.resolve("android-selection-context-toolbar"),toggleOnExecute:!1})])})),u=function(){h.setGroups(s,v.get()),d.off(s)},v=k([]),w=function(a){v.set(a),u()},x=function(a){return j.map(a,l.compose(i.sketch,r))},y=function(){h.refresh(s)},z=function(a){d.on(s),h.setGroups(s,a)},A=function(){d.isOn(s)&&u()},B=function(){c.focusIn(s)};return{wrapper:l.constant(t),toolbar:l.constant(s),createGroups:x,setGroups:w,setContextToolbar:z,restoreToolbar:A,refresh:y,focus:B}}}),g("21",["p","1x","1","3p","1m","3u","6","2e","h","1n"],function(a,b,c,d,e,f,g,h,i,j){var k=function(a){return d.build(e.sketch({dom:j.dom('
    '),action:function(){a.run(function(a){a.setReadOnly(!1)})}}))},l=function(){return d.build(f.sketch({dom:j.dom('
    '),components:[],containerBehaviours:a.derive([b.config({})])}))},m=function(a,c){b.append(a,d.premade(c))},n=function(a,c){b.remove(a,c)},o=function(a,b,d,e){var f=d===!0?c.toAlpha:c.toOmega;f(e);var g=d?m:n;g(a,b)};return{makeEditSwitch:k,makeSocket:l,updateMode:o}}),g("67",["2e","86","38"],function(a,b,c){var d=function(a,b){return b.getAnimationRoot().fold(function(){return a.element()},function(b){return b(a)})},e=function(a){return a.dimension().property()},f=function(a,b){return a.dimension().getDimension()(b)},g=function(a,c){var e=d(a,c);b.remove(e,[c.shrinkingClass(),c.growingClass()])},h=function(b,d){a.remove(b.element(),d.openClass()),a.add(b.element(),d.closedClass()),c.set(b.element(),e(d),"0px"),c.reflow(b.element())},i=function(a,b){j(a,b);var c=f(b,a.element());return h(a,b),c},j=function(b,d){a.remove(b.element(),d.closedClass()),a.add(b.element(),d.openClass()),c.remove(b.element(),e(d))},k=function(a,b,d){d.setCollapsed(),c.set(a.element(),e(b),f(b,a.element())),c.reflow(a.element()),g(a,b),h(a,b),b.onStartShrink()(a),b.onShrunk()(a)},l=function(b,g,i){i.setCollapsed(),c.set(b.element(),e(g),f(g,b.element())),c.reflow(b.element());var j=d(b,g);a.add(j,g.shrinkingClass()),h(b,g),g.onStartShrink()(b)},m=function(b,f,g){var h=i(b,f),k=d(b,f);a.add(k,f.growingClass()),j(b,f),c.set(b.element(),e(f),h),g.setExpanded(),f.onStartGrow()(b)},n=function(a,b,c){c.isExpanded()||m(a,b,c)},o=function(a,b,c){c.isExpanded()&&l(a,b,c)},p=function(a,b,c){c.isExpanded()&&k(a,b,c)},q=function(a,b,c){return c.isExpanded()},r=function(a,b,c){return c.isCollapsed()},s=function(b,c,e){var f=d(b,c);return a.has(f,c.growingClass())===!0},t=function(b,c,e){var f=d(b,c);return a.has(f,c.shrinkingClass())===!0},u=function(a,b,c){return s(a,b,c)===!0||t(a,b,c)===!0},v=function(a,b,c){var d=c.isExpanded()?l:m;d(a,b,c)};return{grow:n,shrink:o,immediateShrink:p,hasGrown:q,hasShrunk:r,isGrowing:s,isShrinking:t,isTransitioning:u,toggleGrow:v,disableTransitions:g}}),g("66",["3c","2f","67","4a","13","38"],function(a,b,c,d,e,f){var g=function(a,b){var c=b.expanded();return c?d.nu({classes:[b.openClass()],styles:{}}):d.nu({classes:[b.closedClass()],styles:e.wrap(b.dimension().property(),"0px")})},h=function(d,e){return a.derive([a.run(b.transitionend(),function(a,b){var g=b.event().raw();if(g.propertyName===d.dimension().property()){c.disableTransitions(a,d,e),e.isExpanded()&&f.remove(a.element(),d.dimension().property());var h=e.isExpanded()?d.onGrown():d.onShrunk();h(a,b)}})])};return{exhibit:g,events:h}}),g("68",["4p","28","2d","87","5m"],function(a,b,c,d,e){return[b.strict("closedClass"),b.strict("openClass"),b.strict("shrinkingClass"),b.strict("growingClass"),b.option("getAnimationRoot"),a.onHandler("onShrunk"),a.onHandler("onStartShrink"),a.onHandler("onGrown"),a.onHandler("onStartGrow"),b.defaulted("expanded",!1),b.strictOf("dimension",c.choose("property",{width:[a.output("property","width"),a.output("getDimension",function(a){return e.get(a)+"px"})],height:[a.output("property","height"),a.output("getDimension",function(a){return d.get(a)+"px"})]}))]}),g("69",["4f","5","6"],function(a,b,c){var d=function(d){var e=b(d.expanded()),f=function(){return"expanded: "+e.get()};return a({isExpanded:function(){return e.get()===!0},isCollapsed:function(){return e.get()===!1},setCollapsed:c.curry(e.set,!1),setExpanded:c.curry(e.set,!0),readState:f})};return{init:d}}),g("3z",["p","66","67","68","69"],function(a,b,c,d,e){return a.create({fields:d,name:"sliding",active:b,apis:c,state:e})}),g("22",["p","1x","3z","3p","3u","6","1j","1k","h"],function(a,b,c,d,e,f,g,h,i){var j=function(j,k){var l=d.build(e.sketch({dom:{tag:"div",classes:i.resolve("dropup")},components:[],containerBehaviours:a.derive([b.config({}),c.config({closedClass:i.resolve("dropup-closed"),openClass:i.resolve("dropup-open"),shrinkingClass:i.resolve("dropup-shrinking"),growingClass:i.resolve("dropup-growing"),dimension:{property:"height"},onShrunk:function(a){j(),k(),b.set(a,[])},onGrown:function(a){j(),k()}}),h.orientation(function(a,b){n(f.noop)})])})),m=function(a,d,e){c.hasShrunk(l)===!0&&c.isTransitioning(l)===!1&&g.requestAnimationFrame(function(){d(e),b.set(l,[a()]),c.grow(l)})},n=function(a){g.requestAnimationFrame(function(){a(),c.shrink(l)})};return{appear:m,disappear:n,component:f.constant(l),element:l.element}};return{build:j}}),g("6c",["88","s","89","28","2d","x","7","1t","b","z","1i"],function(a,b,c,d,e,f,g,h,i,j,k){var l=function(b){return b.raw().which===a.BACKSPACE()[0]&&!f.contains(["input","textarea"],i.name(b.target()))},m=g.detect().browser.isFirefox(),n=e.objOfOnly([d.strictFunction("triggerEvent"),d.strictFunction("broadcastEvent"),d.defaulted("stopBackspace",!0)]),o=function(a,b){return m?h.capture(a,"focus",b):h.bind(a,"focusin",b)},p=function(a,b){return m?h.capture(a,"blur",b):h.bind(a,"focusout",b)},q=function(a,d){var i=e.asRawOrDie("Getting GUI events settings",n,d),m=g.detect().deviceType.isTouch()?["touchstart","touchmove","touchend","gesturestart"]:["mousedown","mouseup","mouseover","mousemove","mouseout","click"],q=c.monitor(i),r=f.map(m.concat(["selectstart","input","contextmenu","change","transitionend","dragstart","dragover","drop"]),function(b){return h.bind(a,b,function(a){q.fireIfReady(a,b).each(function(b){b&&a.kill()});var c=i.triggerEvent(b,a);c&&a.kill()})}),s=h.bind(a,"keydown",function(a){var b=i.triggerEvent("keydown",a);b?a.kill():i.stopBackspace===!0&&l(a)&&a.prevent()}),t=o(a,function(a){var b=i.triggerEvent("focusin",a);b&&a.kill()}),u=p(a,function(a){var c=i.triggerEvent("focusout",a);c&&a.kill(),k(function(){i.triggerEvent(b.postBlur(),a)},0)}),v=j.defaultView(a),w=h.bind(v,"scroll",function(a){var c=i.broadcastEvent(b.windowScroll(),a);c&&a.kill()}),x=function(){f.each(r,function(a){a.unbind()}),s.unbind(),t.unbind(),u.unbind(),w.unbind()};return{unbind:x}};return{setup:q}}),g("8a",["13","5"],function(a,b){var c=function(c,d){var e=a.readOptFrom(c,"target").map(function(a){return a()}).getOr(d);return b(e)};return{derive:c}}),g("8b",["5","6","u"],function(a,b,c){var d=function(c,d){var e=a(!1),f=a(!1),g=function(){e.set(!0)},h=function(){f.set(!0)};return{stop:g,cut:h,isStopped:e.get,isCut:f.get,event:b.constant(c),setSource:d.set,getSource:d.get}},e=function(d){var e=a(!1),f=function(){e.set(!0)};return{stop:f,cut:b.noop,isStopped:e.get,isCut:b.constant(!1),event:b.constant(d),setTarget:b.die(new c("Cannot set target of a broadcasted event")),getTarget:b.die(new c("Cannot get target of a broadcasted event"))}},f=function(b,c){var e=a(c);return d(b,e)};return{fromSource:d,fromExternal:e,fromTarget:f}}),g("6d",["6b","8a","8b","6k","x","z","u"],function(a,b,c,d,e,f,g){var h=d.generate([{stopped:[]},{resume:["element"]},{complete:[]}]),i=function(b,d,e,g,i,j){var k=b(d,g),l=c.fromSource(e,i);return k.fold(function(){return j.logEventNoHandlers(d,g),h.complete()},function(b){var c=b.descHandler(),e=a.getHandler(c);return e(l),l.isStopped()?(j.logEventStopped(d,b.element(),c.purpose()),h.stopped()):l.isCut()?(j.logEventCut(d,b.element(),c.purpose()),h.complete()):f.parent(b.element()).fold(function(){return j.logNoParent(d,b.element(),c.purpose()),h.complete()},function(a){return j.logEventResponse(d,b.element(),c.purpose()),h.resume(a)})})},j=function(a,b,c,d,e,f){return i(a,b,c,d,e,f).fold(function(){return!0},function(d){return j(a,b,c,d,e,f)},function(){return!1})},k=function(a,c,d,e,f){var g=b.derive(d,e);return i(a,c,d,e,g,f)},l=function(b,d,f){var g=c.fromExternal(d);return e.each(b,function(b){var c=b.descHandler(),d=a.getHandler(c);d(g)}),g.isStopped()},m=function(a,b,c,d){var e=c.target();return n(a,b,c,e,d)},n=function(a,c,d,e,f){var g=b.derive(d,e);return j(a,c,d,e,g,f)};return{triggerHandler:k,triggerUntilStopped:m,triggerOnUntilStopped:n,broadcast:l}}),g("9n",["2t"],function(a){var b=function(b,c,d){var e=a.closest(b,function(a){return c(a).isSome()},d);return e.bind(c)};return{closest:b}}),g("8c",["9n","6b","2z","13","6","w","y","2n","15"],function(a,b,c,d,e,f,g,h,i){var j=h.immutable("element","descHandler"),k=function(a,b){return{id:e.constant(a),descHandler:e.constant(b)}};return function(){var e={},h=function(a,c,d){f.each(d,function(d,f){var g=void 0!==e[f]?e[f]:{};g[c]=b.curryArgs(d,a),e[f]=g})},i=function(a,b){return c.read(b).fold(function(a){return g.none()},function(c){var e=d.readOpt(c);return a.bind(e).map(function(a){return j(b,a)})})},l=function(a){return d.readOptFrom(e,a).map(function(a){return f.mapToArray(a,function(a,b){return k(b,a)})}).getOr([])},m=function(b,c,f){var g=d.readOpt(c),h=g(e);return a.closest(f,function(a){return i(h,a)},b)},n=function(a){f.each(e,function(b,c){b.hasOwnProperty(a)&&delete b[a]})};return{registerId:h,unregisterId:n,filterByType:l,find:m}}}),g("6e",["8c","12","2z","13","11","u"],function(a,b,c,d,e,f){return function(){var g=a(),h={},i=function(a){var b=a.element();return c.read(b).fold(function(){return c.write("uid-",a.element())},function(a){return a})},j=function(a,c){var d=h[c];if(d!==a)throw new f('The tagId "'+c+'" is already used by: '+b.element(d.element())+"\nCannot use it for: "+b.element(a.element())+"\nThe conflicting element is"+(e.inBody(d.element())?" ":" not ")+"already in the DOM");l(a)},k=function(a){var b=i(a);d.hasKey(h,b)&&j(a,b);var c=[a];g.registerId(c,b,a.events()),h[b]=a},l=function(a){c.read(a.element()).each(function(a){h[a]=void 0,g.unregisterId(a)})},m=function(a){return g.filterByType(a)},n=function(a,b,c){return g.find(a,b,c)},o=function(a){return d.readOpt(a)(h)};return{find:n,filter:m,register:k,unregister:l,getById:o}}}),g("40",["3p","s","3","6a","3u","4","6b","6c","6d","12","6e","2z","x","6","47","19","8","9","10","b","2e","z","u"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w){var x=function(){var b=a.build(e.sketch({dom:{tag:"div"}}));return y(b)},y=function(e){var j=function(a){return v.parent(e.element()).fold(function(){return!0},function(b){return p.eq(a,b)})},r=k(),u=function(a,b){return r.find(j,a,b)},x=h.setup(e.element(),{triggerEvent:function(a,b){return f.monitorEvent(a,b.target(),function(c){return i.triggerUntilStopped(u,a,b,c)})},broadcastEvent:function(a,b){var c=r.filter(a);return i.broadcast(c,b)}}),y=d({debugInfo:n.constant("real"),triggerEvent:function(a,b,c){f.monitorEvent(a,b,function(d){i.triggerOnUntilStopped(u,a,c,b,d)})},triggerFocus:function(a,c){l.read(a).fold(function(){q.focus(a)},function(d){f.monitorEvent(b.focus(),a,function(d){i.triggerHandler(u,b.focus(),{originator:n.constant(c),target:n.constant(a)},a,d)})})},triggerEscape:function(a,b){y.triggerEvent("keydown",a.element(),b.event())},getByUid:function(a){return H(a)},getByDom:function(a){return I(a)},build:a.build,addToGui:function(a){B(a)},removeFromGui:function(a){C(a)},addToWorld:function(a){z(a)},removeFromWorld:function(a){A(a)},broadcast:function(a){F(a)},broadcastOn:function(a,b){G(a,b)}}),z=function(a){a.connect(y),t.isText(a.element())||(r.register(a),m.each(a.components(),z),y.triggerEvent(b.systemInit(),a.element(),{target:n.constant(a.element())}))},A=function(a){t.isText(a.element())||(m.each(a.components(),A),r.unregister(a)),a.disconnect()},B=function(a){c.attach(e,a)},C=function(a){c.detach(a)},D=function(){x.unbind(),s.remove(e.element())},E=function(a){var c=r.filter(b.receive());m.each(c,function(b){var c=b.descHandler(),d=g.getHandler(c);d(a)})},F=function(a){E({universal:n.constant(!0),data:n.constant(a)})},G=function(a,b){E({universal:n.constant(!1),channels:n.constant(a),data:n.constant(b)})},H=function(a){return r.getById(a).fold(function(){return o.error(new w('Could not find component with uid: "'+a+'" in system.'))},o.value)},I=function(a){return l.read(a).bind(H)};return z(e),{root:n.constant(e),element:e.element,destroy:D,add:B,remove:C,getByUid:H,getByDom:I,addToWorld:z,removeFromWorld:A,broadcast:F,broadcastOn:G}};return{create:x,takeover:y}}),g("23",["p","1","3p","40","3u","6","h"],function(a,b,c,d,e,f,g){var h=f.constant(g.resolve("readonly-mode")),i=f.constant(g.resolve("edit-mode"));return function(f){var j=c.build(e.sketch({dom:{classes:[g.resolve("outer-container")].concat(f.classes)},containerBehaviours:a.derive([b.config({alpha:h(),omega:i()})])}));return d.takeover(j)}}),g("j",["1x","1","6","1y","1z","h","20","21","22","23"],function(a,b,c,d,e,f,g,h,i,j){return function(b){var k=j({classes:[f.resolve("android-container")]}),l=g(),m=d.api(),n=h.makeEditSwitch(m),o=h.makeSocket(),p=i.build(c.noop,b);k.add(l.wrapper()),k.add(o),k.add(p.component());var q=function(a){var b=l.createGroups(a);l.setGroups(b)},r=function(a){var b=l.createGroups(a);l.setContextToolbar(b)},s=function(){l.focus()},t=function(){l.restoreToolbar()},u=function(a){m.set(e.produce(a))},v=function(){m.run(function(b){b.exit(),a.remove(o,n)})},w=function(a){h.updateMode(o,n,a,k.root())};return{system:c.constant(k),element:k.element,init:u,exit:v,setToolbarGroups:q,setContextToolbar:r,focusToolbar:s,restoreToolbar:t,updateMode:w,socket:c.constant(o),dropup:c.constant(p)}}}),g("9o",["6"],function(a){var b=function(c,d){var e=function(a,e){return b(c+a,d+e)};return{left:a.constant(c),top:a.constant(d),translate:e}};return b}),g("9p",["6","19","a","b","2t","1a"],function(a,b,c,d,e,f){var g=function(d,g){var h=g||c.fromDom(f.documentElement);return e.ancestor(d,a.curry(b.eq,h)).isSome()},h=function(a){var b=a.dom();return b===b.window?a:d.isDocument(a)?b.defaultView||b.parentWindow:null};return{attached:g,windowOf:h}}),g("8d",["9o","9p","a"],function(a,b,c){var d=function(b){var c=b.getBoundingClientRect();return a(c.left,c.top)},e=function(a,b){return void 0!==a?a:void 0!==b?b:0},f=function(a){var d=a.dom().ownerDocument,f=d.body,g=b.windowOf(c.fromDom(d)),i=d.documentElement,j=e(g.pageYOffset,i.scrollTop),k=e(g.pageXOffset,i.scrollLeft),l=e(i.clientTop,f.clientTop),m=e(i.clientLeft,f.clientLeft);return h(a).translate(k-m,j-l)},g=function(b){var c=b.dom();return a(c.offsetLeft,c.offsetTop)},h=function(e){var f=e.dom(),g=f.ownerDocument,h=g.body,i=c.fromDom(g.documentElement);return h===f?a(h.offsetLeft,h.offsetTop):b.attached(e,i)?d(f):a(0,0)};return{absolute:f,relative:g,viewport:h}}),g("6f",["89","x","62","19","1t","87","8d","7y"],function(a,b,c,d,e,f,g,h){var i=function(a,i,j,k,l){var m=function(){i.run(function(a){a.highlightSelection()})},n=function(){i.run(function(a){a.refreshSelection()})},o=function(a,b){var c=a-k.dom().scrollTop;i.run(function(a){a.scrollIntoView(c,c+b)})},p=function(a){var b=g.absolute(a).top(),c=f.get(a);o(i,k,b,c)},q=function(){a.getCursorBox().each(function(a){o(a.top(),a.height())})},r=function(){i.run(function(a){a.clearSelection()})},s=function(){r(),z.throttle()},t=function(){q(a,i,k),i.run(function(a){a.syncHeight()})},u=function(){var b=f.get(j);i.run(function(a){a.setViewportOffset(b)}),n(i),t(a,i,k)},v=function(){i.run(function(a){a.toEditing()})},w=function(){i.run(function(a){a.toReading()})},x=function(a){i.run(function(b){b.onToolbarTouch(a)})},y=h.monitor(a),z=c.last(t,300),A=[a.onKeyup(s),a.onNodeChanged(n),a.onDomChanged(z.throttle),a.onDomChanged(n),a.onScrollToCursor(function(a){a.preventDefault(),z.throttle()}),a.onScrollToElement(function(a){p(a.element())}),a.onToEditing(v),a.onToReading(w),e.bind(a.doc(),"touchend",function(b){d.eq(a.html(),b.target())||d.eq(a.body(),b.target())}),e.bind(j,"transitionend",function(a){"height"===a.raw().propertyName&&u()}),e.capture(j,"touchstart",function(b){m(),x(b),a.onTouchToolstrip()}),e.bind(a.body(),"touchstart",function(b){r(),a.onTouchContent(),y.fireTouchstart(b)}),y.onTouchmove(),y.onTouchend(),e.bind(a.body(),"click",function(a){a.kill()}),e.bind(j,"touchmove",function(){a.onToolbarScrollStart()})],B=function(){b.each(A,function(a){a.unbind()})};return{destroy:B}};return{initEvents:i}}),g("9q",["8","1i"],function(a,b){var c=function(c){var d=c.dom().selectionStart,e=c.dom().selectionEnd,f=c.dom().selectionDirection;b(function(){c.dom().setSelectionRange(d,e,f),a.focus(c)},50)},d=function(a){var b=a.getSelection();if(b.rangeCount>0){var c=b.getRangeAt(0),d=a.document.createRange();d.setStart(c.startContainer,c.startOffset),d.setEnd(c.endContainer,c.endOffset),b.removeAllRanges(),b.addRange(d)}};return{refreshInput:c,refresh:d}}),g("8m",["19","8","a","9q"],function(a,b,c,d){var e=function(e,f){b.active().each(function(c){a.eq(c,f)||b.blur(c)}),e.focus(),b.focus(c.fromDom(e.document.body)),d.refresh(e)};return{resume:e}}),g("8f",["x","9","2r","10","1t","a","2e","86","38","z","8m","h","81"],function(a,b,c,d,e,f,g,h,i,j,k,l,m){return function(n,o){var p=n.document,q=f.fromTag("div");g.add(q,l.resolve("unfocused-selections")),b.append(f.fromDom(p.documentElement),q);var r=e.bind(q,"touchstart",function(a){a.prevent(),k.resume(n,o),u()}),s=function(a){var b=f.fromTag("span");return h.add(b,[l.resolve("layer-editor"),l.resolve("unfocused-selection")]),i.setAll(b,{left:a.left()+"px",top:a.top()+"px",width:a.width()+"px",height:a.height()+"px"}),b},t=function(){u();var b=m.getRectangles(n),d=a.map(b,s);c.append(q,d)},u=function(){d.empty(q)},v=function(){r.unbind(),d.remove(q)},w=function(){return j.children(q).length>0};return{update:t,isActive:w,destroy:v,clear:u}}}),g("9u",["x","y","1i"],function(a,b,c){var d=function(e){var f=b.none(),g=[],h=function(a){return d(function(b){i(function(c){b(a(c))})})},i=function(a){k()?m(a):g.push(a)},j=function(a){f=b.some(a),l(g),g=[]},k=function(){return f.isSome()},l=function(b){a.each(b,m)},m=function(a){f.each(function(b){c(function(){a(b)},0)})};return e(j),{get:i,map:h,isReady:k}},e=function(a){return d(function(b){b(a)})};return{nu:d,pure:e}}),g("a4",["t","1i"],function(a,b){var c=function(c){return function(){var d=a.prototype.slice.call(arguments),e=this;b(function(){c.apply(e,d)},0)}};return{bounce:c}}),g("9r",["9u","a4"],function(a,b){var c=function(d){var e=function(a){d(b.bounce(a))},f=function(a){return c(function(b){e(function(c){var d=a(c);b(d)})})},g=function(a){return c(function(b){e(function(c){a(c).get(b)})})},h=function(a){return c(function(b){e(function(c){a.get(b)})})},i=function(){return a.nu(e)};return{map:f,bind:g,anonBind:h,toLazy:i,get:e}},d=function(a){return c(function(b){b(a)})};return{nu:c,pure:d}}),g("9s",["y","1u","1v","1w"],function(a,b,c,d){var e=function(b,d,e){return c.abs(b-d)<=e?a.none():bc.abs(d-g))&&(b(a),m(g))}})},k)};return{animate:f}};return{create:f,adjust:e}}),g("a5",["y","14"],function(a,b){var c=function(c,d){var e=[{width:320,height:480,keyboard:{portrait:300,landscape:240}},{width:320,height:568,keyboard:{portrait:300,landscape:240}},{width:375,height:667,keyboard:{portrait:305,landscape:240}},{width:414,height:736,keyboard:{portrait:320,landscape:240}},{width:768,height:1024,keyboard:{portrait:320,landscape:400}},{width:1024,height:1366,keyboard:{portrait:380,landscape:460}}];return b.findMap(e,function(b){return c<=b.width&&d<=b.height?a.some(b.keyboard):a.none()}).getOr({portrait:d/5,landscape:c/4})};return{findDevice:c}}),g("9t",["38","z","87","a5","i"],function(a,b,c,d,e){var f=function(a){return d.findDevice(a.screen.width,a.screen.height)},g=function(a){var b=e.get(a).isPortrait(),c=f(a),d=b?c.portrait:c.landscape,g=b?a.screen.height:a.screen.width;return g-a.innerHeight>d?0:d},h=function(a,d){var e=b.owner(a).dom().defaultView,f=c.get(a)+c.get(d),h=g(e);return f-h},i=function(b,d,e){var f=h(d,e),g=c.get(d)+c.get(e)-f;a.set(b,"padding-bottom",g+"px")};return{getGreenzone:h,updatePadding:i}}),g("8k",["6k","x","6","4h","38","5l","z","87","9t","h","3y","80"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=a.generate([{fixed:["element","property","offsetY"]},{scroller:["element","offsetY"]}]),n="data-"+j.resolve("position-y-fixed"),o="data-"+j.resolve("y-property"),p="data-"+j.resolve("scrolling"),q="data-"+j.resolve("last-window-height"),r=function(a){return l.safeParse(a,n)},s=function(a){return d.get(a,o)},t=function(a){return l.safeParse(a,q)},u=function(a,b){var c=s(a);return m.fixed(a,c,b)},v=function(a,b){return m.scroller(a,b)},w=function(a){var b=r(a),c="true"===d.get(a,p)?v:u;return c(a,b)},x=function(a){var c=f.descendants(a,"["+n+"]");return b.map(c,w)},y=function(a){var b=d.get(a,"style");e.setAll(a,{position:"absolute",top:"0px"}),d.set(a,n,"0px"),d.set(a,o,"top");var c=function(){d.set(a,"style",b||""),d.remove(a,n),d.remove(a,o)};return{restore:c}},z=function(a,b,c){var f=d.get(c,"style");k.register(c),e.setAll(c,{position:"absolute",height:b+"px",width:"100%",top:a+"px"}),d.set(c,n,a+"px"),d.set(c,p,"true"),d.set(c,o,"top");var g=function(){k.deregister(c),d.set(c,"style",f||""),d.remove(c,n),d.remove(c,p),d.remove(c,o)};return{restore:g}},A=function(a,b,c){var f=d.get(a,"style");e.setAll(a,{position:"absolute",bottom:"0px"}),d.set(a,n,"0px"),d.set(a,o,"bottom");var g=function(){d.set(a,"style",f||""),d.remove(a,n),d.remove(a,o)};return{restore:g}},B=function(a,b,c){var e=g.owner(a).dom().defaultView,f=e.innerHeight;return d.set(a,q,f+"px"),f-b-c},C=function(a,b,f,j){var k=g.owner(a).dom().defaultView,l=y(f),m=h.get(f),o=h.get(j),p=B(a,m,o),q=z(m,p,a),r=A(j,m,p),s=!0,u=function(){s=!1,l.restore(),q.restore(),r.restore()},v=function(){var b=k.innerHeight,c=t(a);return b>c},w=function(){if(s){var c=h.get(f),g=h.get(j),k=B(a,c,g);d.set(a,n,c+"px"),e.set(a,"height",k+"px"),e.set(j,"bottom",-(c+k+g)+"px"),i.updatePadding(b,a,j)}},x=function(b){var c=b+"px";d.set(a,n,c),w()};return i.updatePadding(b,a,j),{setViewportOffset:x,isExpanding:v,isShrinking:c.not(v),refresh:w,restore:u}};return{findFixtures:x,takeover:C,getYFixedData:r}}),g("8g",["6","9r","4h","86","38","z","1v","9s","8k","h","80"],function(a,b,c,d,e,f,g,h,i,j,k){var l=h.create(),m=15,n=10,o=10,p="data-"+j.resolve("last-scroll-top"),q=function(a){var b=e.getRaw(a,"top").getOr(0);return parseInt(b,10)},r=function(a){return parseInt(a.dom().scrollTop,10)},s=function(c,d,f){return b.nu(function(b){var g=a.curry(r,c),h=function(a){c.dom().scrollTop=a,e.set(c,"top",q(c)+m+"px")},i=function(){c.dom().scrollTop=d,e.set(c,"top",f+"px"),b(d)};l.animate(g,d,m,h,i,o)})},t=function(d,e){return b.nu(function(b){var f=a.curry(r,d);c.set(d,p,f());var h=function(a,b){var e=k.safeParse(d,p);e!==d.dom().scrollTop?b(d.dom().scrollTop):(d.dom().scrollTop=a,c.set(d,p,a))},i=function(){d.dom().scrollTop=e,c.set(d,p,e),b(e)},j=g.abs(e-f()),m=g.ceil(j/n);l.animate(f,e,m,h,i,o)})},u=function(c,d){return b.nu(function(b){var f=a.curry(q,c),h=function(a){e.set(c,"top",a+"px")},i=function(){h(d),b(d)},j=g.abs(d-f()),k=g.ceil(j/n);l.animate(f,d,k,h,i,o)})},v=function(a,b){var c=b+i.getYFixedData(a)+"px";e.set(a,"top",c)},w=function(a,c,d){var e=f.owner(a).dom().defaultView;return b.nu(function(b){v(a,d),v(c,d),e.scrollTo(0,d),b(d)})};return{moveScrollAndTop:s,moveOnlyScroll:t,moveOnlyTop:u,moveWindowScroll:w}}),g("8h",["5","9u"],function(a,b){return function(c){var d=a(b.pure({})),e=function(a){var e=b.nu(function(b){return c(a).get(b)});d.set(e)},f=function(a){d.get().get(function(){a()})};return{start:e,idle:f}}}),g("8i",["6","8e","8g","9t","9q"],function(a,b,c,d,e){var f=function(b,f,g,h,i){var j=d.getGreenzone(f,g),k=a.curry(e.refresh,b);h>j||i>j?c.moveOnlyScroll(f,f.dom().scrollTop-j+i).get(k):h<0&&c.moveOnlyScroll(f,f.dom().scrollTop+h).get(k)};return{scrollIntoView:f}}),g("a6",["x"],function(a){var b=function(b,c){return c(function(c){var d=[],e=0,f=function(a){return function(f){d[a]=f,e++,e>=b.length&&c(d)}};0===b.length?c([]):a.each(b,function(a,b){a.get(f(b))})})};return{par:b}}),g("9v",["x","9r","a6"],function(a,b,c){var d=function(a){return c.par(a,b.nu)},e=function(b,c){var e=a.map(b,c);return d(e)},f=function(a,b){return function(c){return b(c).bind(a)}};return{par:d,mapM:e,compose:f}}),g("8j",["x","9r","9v","38","8g","8k"],function(a,b,c,d,e,f){var g=function(a,c,e,f){var g=e+f;return d.set(a,c,g+"px"),b.pure(f)},h=function(a,b,c){var f=b+c,g=d.getRaw(a,"top").getOr(c),h=f-parseInt(g,10),i=a.dom().scrollTop+h;return e.moveScrollAndTop(a,i,f)},i=function(a,b){return a.fold(function(a,c,d){return g(a,c,b,d)},function(a,c){return h(a,b,c)})},j=function(b,d){var e=f.findFixtures(b),g=a.map(e,function(a){return i(a,d)});return c.par(g)};return{updatePositions:j}}),g("8l",["8","9","10","a","38"],function(a,b,c,d,e){var f=function(f,g){var h=d.fromTag("input");e.setAll(h,{opacity:"0",position:"absolute",top:"-1000px",left:"-1000px"}),b.append(f,h),a.focus(h),g(h),c.remove(h)};return{input:f}}),g("6g",["6","y","62","8","1t","11","a","38","1u","83","15","1v","8e","1w","1i","8f","8g","8h","8i","8j","8k","i","8l","81"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x){var y=5,z=function(d,f,h,i,j,k){var l=r(function(a){return q.moveWindowScroll(d,f,a)}),m=function(){var c=x.getRectangles(k);return b.from(c[0]).bind(function(c){var d=c.top()-f.dom().scrollTop,e=d>i.innerHeight+y||d<-y;return e?b.some({top:a.constant(d),bottom:a.constant(d+c.height())}):b.none()})},n=c.last(function(){l.idle(function(){t.updatePositions(h,i.pageYOffset).get(function(){var a=m();a.each(function(a){f.dom().scrollTop=f.dom().scrollTop+a.top()}),l.start(0),j.refresh()})})},1e3),o=e.bind(g.fromDom(i),"scroll",function(){i.pageYOffset<0||n.throttle()});return t.updatePositions(h,i.pageYOffset).get(a.identity),{unbind:o.unbind}},A=function(b){var c=b.cWin(),i=b.ceBody(),j=b.socket(),k=b.toolstrip(),l=b.toolbar(),m=b.contentElement(),n=b.keyboardType(),o=b.outerWindow(),r=b.dropup(),t=u.takeover(j,i,k,r),x=n(b.outerBody(),c,f.body(),m,k,l),y=function(){x.toEditing(),I()},A=function(){x.toReading()},B=function(a){x.onToolbarTouch(a)},C=v.onChange(o,{onChange:a.noop,onReady:t.refresh});C.onAdjustment(function(){t.refresh()});var D=e.bind(g.fromDom(o),"resize",function(){t.isExpanding()&&t.refresh()}),E=z(k,j,b.outerBody(),o,t,c),F=p(c,m),G=function(){F.isActive()&&F.update()},H=function(){F.update()},I=function(){F.clear()},J=function(a,b){s.scrollIntoView(c,j,r,a,b)},K=function(){h.set(m,"height",m.dom().contentWindow.document.body.scrollHeight+"px")},L=function(b){t.setViewportOffset(b),q.moveOnlyTop(j,b).get(a.identity)},M=function(){t.restore(),C.destroy(),E.unbind(),D.unbind(),x.destroy(),F.destroy(),w.input(f.body(),d.blur)};return{toEditing:y,toReading:A,onToolbarTouch:B,refreshSelection:G,clearSelection:I,highlightSelection:H,scrollIntoView:J,updateToolbarPadding:a.noop,setViewportOffset:L,syncHeight:K,refreshStructure:t.refresh,destroy:M}};return{setup:A}}),g("6h",["x","6","8","1t","11","b","8m","8l"],function(a,b,c,d,e,f,g,h){var i=function(b,e,i,j){var k=function(){g.resume(e,j)},l=function(){h.input(b,c.blur)},m=d.bind(i,"keydown",function(b){a.contains(["input","textarea"],f.name(b.target()))||k()}),n=function(){},o=function(){m.unbind()};return{toReading:l,toEditing:k,onToolbarTouch:n,destroy:o}},j=function(a,d,e,f){var h=function(){c.blur(f)},i=function(){h()},j=function(){h()},k=function(){g.resume(d,f)};return{toReading:j,toEditing:k,onToolbarTouch:i,destroy:b.noop}};return{stubborn:i,timid:j}}),g("41",["6","1y","2n","8","a","2e","38","1a","6f","6g","5z","3x","6h","60","h","3y","61"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q){var r=function(r,s){var t=q.tag(),u=b.value(),v=b.value(),w=b.api(),x=b.api(),y=function(){ +s.hide();var b=e.fromDom(h);k.getActiveApi(r.editor).each(function(e){u.set({socketHeight:g.getRaw(r.socket,"height"),iframeHeight:g.getRaw(e.frame(),"height"),outerScroll:h.body.scrollTop}),v.set({exclusives:l.exclusive(b,"."+p.scrollable())}),f.add(r.container,o.resolve("fullscreen-maximized")),n.clobberStyles(r.container,e.body()),t.maximize(),g.set(r.socket,"overflow","scroll"),g.set(r.socket,"-webkit-overflow-scrolling","touch"),d.focus(e.body());var k=c.immutableBag(["cWin","ceBody","socket","toolstrip","toolbar","dropup","contentElement","cursor","keyboardType","isScrolling","outerWindow","outerBody"],[]);w.set(j.setup(k({cWin:e.win(),ceBody:e.body(),socket:r.socket,toolstrip:r.toolstrip,toolbar:r.toolbar,dropup:r.dropup.element(),contentElement:e.frame(),cursor:a.noop,outerBody:r.body,outerWindow:r.win,keyboardType:m.stubborn,isScrolling:function(){return v.get().exists(function(a){return a.socket.isScrolling()})}}))),w.run(function(a){a.syncHeight()}),x.set(i.initEvents(e,w,r.toolstrip,r.socket,r.dropup))})},z=function(){t.restore(),x.clear(),w.clear(),s.show(),u.on(function(a){a.socketHeight.each(function(a){g.set(r.socket,"height",a)}),a.iframeHeight.each(function(a){g.set(r.editor.getFrame(),"height",a)}),h.body.scrollTop=a.scrollTop}),u.clear(),v.on(function(a){a.exclusives.unbind()}),v.clear(),f.remove(r.container,o.resolve("fullscreen-maximized")),n.restoreStyles(),p.deregister(r.toolbar),g.remove(r.socket,"overflow"),g.remove(r.socket,"-webkit-overflow-scrolling"),d.blur(r.editor.getFrame()),k.getActiveApi(r.editor).each(function(a){a.clearSelection()})},A=function(){w.run(function(a){a.refreshStructure()})};return{enter:y,refreshStructure:A,exit:z}};return{create:r}}),g("24",["3p","2d","6","38","3r","41","3s"],function(a,b,c,d,e,f,g){var h=function(h){var i=b.asRawOrDie("Getting IosWebapp schema",e,h);d.set(i.toolstrip,"width","100%"),d.set(i.container,"position","relative");var j=function(){i.setReadOnly(!0),m.enter()},k=a.build(g.sketch(j,i.translate));i.alloy.add(k);var l={show:function(){i.alloy.add(k)},hide:function(){i.alloy.remove(k)}},m=f.create(i,l);return{setReadOnly:i.setReadOnly,refreshStructure:m.refreshStructure,enter:m.enter,exit:m.exit,destroy:c.noop}};return{produce:h}}),g("l",["1x","6","1y","24","h","20","21","22","23"],function(a,b,c,d,e,f,g,h,i){return function(j){var k=i({classes:[e.resolve("ios-container")]}),l=f(),m=c.api(),n=g.makeEditSwitch(m),o=g.makeSocket(),p=h.build(function(){m.run(function(a){a.refreshStructure()})},j);k.add(l.wrapper()),k.add(o),k.add(p.component());var q=function(a){var b=l.createGroups(a);l.setGroups(b)},r=function(a){var b=l.createGroups(a);l.setContextToolbar(b)},s=function(){l.focus()},t=function(){l.restoreToolbar()},u=function(a){m.set(d.produce(a))},v=function(){m.run(function(b){a.remove(o,n),b.exit()})},w=function(a){g.updateMode(o,n,a,k.root())};return{system:b.constant(k),element:k.element,init:u,exit:v,setToolbarGroups:q,setContextToolbar:r,focusToolbar:s,restoreToolbar:t,updateMode:w,socket:b.constant(o),dropup:b.constant(p)}}}),g("25",["1d"],function(a){return a("tinymce.EditorManager")}),g("m",["13","25"],function(a,b){var c=function(c){var d=a.readOptFrom(c.settings,"skin_url").fold(function(){return b.baseURL+"/skins/lightgray"},function(a){return a});return{content:d+"/content.mobile.min.css",ui:d+"/skin.mobile.min.css"}};return{derive:c}}),g("n",["x","6","w","f"],function(a,b,c,d){var e=["x-small","small","medium","large","x-large"],f=function(a,b,c){a.system().broadcastOn([d.formatChanged()],{command:b,state:c})},g=function(b,d){var e=c.keys(d.formatter.get());a.each(e,function(a){d.formatter.formatChanged(a,function(c){f(b,a,c)})}),a.each(["ul","ol"],function(a){d.selection.selectorChanged(a,function(c,d){f(b,a,c)})})};return{init:g,fontSizes:b.constant(e)}}),g("o",[],function(){var a=function(a){var b=function(){a._skinLoaded=!0,a.fire("SkinLoaded")};return function(){a.initialized?b():a.on("init",b)}};return{fireSkinLoaded:a}}),g("0",["1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x){var y=f.constant("toReading"),z=f.constant("toEditing");return m.add("mobile",function(m){var A=function(A){var B=v.derive(m);m.contentCSS.push(B.content),l.DOM.styleSheetLoader.load(B.ui,x.fireSkinLoaded(m));var C=function(){m.fire("scrollIntoView")},D=j.fromTag("div"),E=g.detect().os.isAndroid()?s(C):u(C),F=j.fromDom(A.targetNode);i.after(F,D),c.attachSystem(D,E.system());var G=function(a){return h.search(a).bind(function(a){return E.system().getByDom(a).toOption()})},H=A.targetNode.ownerDocument.defaultView,I=r.onChange(H,{onChange:function(){var a=E.system();a.broadcastOn([o.orientationChanged()],{width:r.getActualWidth(H)})},onReady:f.noop}),J=function(a,b,c){c===!1&&m.selection.collapse(),E.setToolbarGroups(c?a.get():b.get()),m.setMode(c===!0?"readonly":"design"),m.fire(c===!0?y():z()),E.updateMode(c)},K=function(a,b){return m.on(a,b),{unbind:function(){m.off(a)}}};return m.on("init",function(){E.init({editor:{getFrame:function(){return j.fromDom(m.contentAreaContainer.querySelector("iframe"))},onDomChanged:function(){return{unbind:f.noop}},onToReading:function(a){return K(y(),a)},onToEditing:function(a){return K(z(),a)},onScrollToCursor:function(a){m.on("scrollIntoView",function(b){a(b)});var b=function(){m.off("scrollIntoView"),I.destroy()};return{unbind:b}},onTouchToolstrip:function(){c()},onTouchContent:function(){var a=j.fromDom(m.editorContainer.querySelector("."+q.resolve("toolbar")));G(a).each(b.emitExecute),E.restoreToolbar(),c()},onTapContent:function(b){var c=b.target();if("img"===k.name(c))m.selection.select(c.dom()),b.kill();else if("a"===k.name(c)){var d=E.system().getByDom(j.fromDom(m.editorContainer));d.each(function(b){a.isAlpha(b)&&n.openLink(c.dom())})}}},container:j.fromDom(m.editorContainer),socket:j.fromDom(m.contentAreaContainer),toolstrip:j.fromDom(m.editorContainer.querySelector("."+q.resolve("toolstrip"))),toolbar:j.fromDom(m.editorContainer.querySelector("."+q.resolve("toolbar"))),dropup:E.dropup(),alloy:E.system(),translate:f.noop,setReadOnly:function(a){J(x,v,a)}});var c=function(){E.dropup().disappear(function(){E.system().broadcastOn([o.dropupDismissed()],{})})};d.registerInspector("remove this",E.system());var g={label:"The first group",scrollable:!1,items:[t.forToolbar("back",function(){m.selection.collapse(),E.exit()},{})]},h={label:"Back to read only",scrollable:!1,items:[t.forToolbar("readonly-back",function(){J(x,v,!0)},{})]},i={label:"The read only mode group",scrollable:!0,items:[]},l=p.setup(E,m),r=p.detect(m.settings,l),s={label:"the action group",scrollable:!0,items:r},u={label:"The extra group",scrollable:!1,items:[]},v=e([h,s,u]),x=e([g,i,u]);w.init(E,m)}),{iframeContainer:E.socket().element().dom(),editorContainer:E.element().dom()}};return{getNotificationManagerImpl:function(){return{open:f.identity,close:f.noop,reposition:f.noop,getArgs:f.identity}},renderUI:A}}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/themes/modern/theme.js b/media/vendor/tinymce/themes/modern/theme.js index dca03c85b6a73..d3615d48fc5af 100644 --- a/media/vendor/tinymce/themes/modern/theme.js +++ b/media/vendor/tinymce/themes/modern/theme.js @@ -57,8 +57,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -76,12 +76,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.themes.modern.Theme","global!window","tinymce.core.AddOnManager","tinymce.core.EditorManager","tinymce.core.Env","tinymce.core.ui.Api","tinymce.themes.modern.modes.Iframe","tinymce.themes.modern.modes.Inline","tinymce.themes.modern.ui.ProgressState","tinymce.themes.modern.ui.Resize","global!tinymce.util.Tools.resolve","tinymce.core.dom.DOMUtils","tinymce.core.ui.Factory","tinymce.core.util.Tools","tinymce.themes.modern.ui.A11y","tinymce.themes.modern.ui.Branding","tinymce.themes.modern.ui.ContextToolbars","tinymce.themes.modern.ui.Menubar","tinymce.themes.modern.ui.Sidebar","tinymce.themes.modern.ui.SkinLoaded","tinymce.themes.modern.ui.Toolbar","tinymce.core.ui.FloatPanel","tinymce.core.ui.Throbber","tinymce.core.util.Delay","tinymce.core.geom.Rect"] +["tinymce.themes.modern.Theme","global!window","tinymce.core.ThemeManager","tinymce.themes.modern.api.ThemeApi","tinymce.ui.Api","tinymce.ui.FormatControls","global!tinymce.util.Tools.resolve","tinymce.themes.modern.ui.Render","tinymce.themes.modern.ui.Resize","tinymce.ui.NotificationManagerImpl","tinymce.ui.WindowManagerImpl","tinymce.core.ui.Factory","tinymce.core.util.Tools","tinymce.ui.AbsoluteLayout","tinymce.ui.BrowseButton","tinymce.ui.Button","tinymce.ui.ButtonGroup","tinymce.ui.Checkbox","tinymce.ui.Collection","tinymce.ui.ColorBox","tinymce.ui.ColorButton","tinymce.ui.ColorPicker","tinymce.ui.ComboBox","tinymce.ui.Container","tinymce.ui.Control","tinymce.ui.DragHelper","tinymce.ui.DropZone","tinymce.ui.ElementPath","tinymce.ui.FieldSet","tinymce.ui.FilePicker","tinymce.ui.FitLayout","tinymce.ui.FlexLayout","tinymce.ui.FloatPanel","tinymce.ui.FlowLayout","tinymce.ui.Form","ephox.katamari.api.Arr","ephox.katamari.api.Fun","ephox.sugar.api.node.Element","ephox.sugar.api.search.SelectorFind","global!document","tinymce.core.dom.DOMUtils","tinymce.core.EditorManager","tinymce.core.Env","tinymce.ui.editorui.Align","tinymce.ui.editorui.FormatSelect","tinymce.ui.editorui.SimpleFormats","tinymce.ui.fmt.FontInfo","tinymce.ui.Widget","tinymce.ui.FormItem","tinymce.ui.GridLayout","tinymce.ui.Iframe","tinymce.ui.InfoBox","tinymce.ui.KeyboardNavigation","tinymce.ui.Label","tinymce.ui.Layout","tinymce.ui.ListBox","tinymce.ui.Menu","tinymce.ui.MenuBar","tinymce.ui.MenuButton","tinymce.ui.MenuItem","tinymce.ui.MessageBox","tinymce.ui.Movable","tinymce.ui.Notification","tinymce.ui.Panel","tinymce.ui.PanelButton","tinymce.ui.Path","tinymce.ui.Progress","tinymce.ui.Radio","tinymce.ui.ReflowQueue","tinymce.ui.Resizable","tinymce.ui.ResizeHandle","tinymce.ui.Scrollable","tinymce.ui.SelectBox","tinymce.ui.Selector","tinymce.ui.Slider","tinymce.ui.Spacer","tinymce.ui.SplitButton","tinymce.ui.StackLayout","tinymce.ui.TabPanel","tinymce.ui.TextBox","tinymce.ui.Throbber","tinymce.ui.Toolbar","tinymce.ui.Tooltip","tinymce.ui.Window","tinymce.themes.modern.api.Settings","tinymce.themes.modern.modes.Iframe","tinymce.themes.modern.modes.Inline","tinymce.themes.modern.ui.ProgressState","tinymce.themes.modern.api.Events","ephox.katamari.api.Option","global!Array","global!Error","global!String","global!setTimeout","tinymce.ui.DomUtils","tinymce.core.dom.DomQuery","tinymce.core.util.Class","tinymce.core.util.EventDispatcher","tinymce.ui.BoxUtils","tinymce.ui.ClassList","tinymce.ui.data.ObservableObject","tinymce.core.util.Delay","global!RegExp","tinymce.core.util.VK","tinymce.core.util.Color","tinymce.ui.content.LinkTargets","global!console","ephox.sugar.api.search.PredicateFind","ephox.sugar.api.search.Selectors","ephox.sugar.impl.ClosestOrAncestor","tinymce.ui.editorui.FormatUtils","ephox.sugar.api.node.Node","tinymce.themes.modern.ui.A11y","tinymce.themes.modern.ui.ContextToolbars","tinymce.themes.modern.ui.Menubar","tinymce.themes.modern.ui.Sidebar","tinymce.themes.modern.ui.SkinLoaded","tinymce.themes.modern.ui.Toolbar","tinymce.ui.data.Binding","tinymce.core.util.Observable","global!Object","ephox.katamari.api.Id","ephox.sugar.api.search.SelectorFilter","ephox.katamari.api.Type","ephox.sugar.api.node.Body","ephox.sugar.api.dom.Compare","ephox.sugar.api.node.NodeTypes","tinymce.core.geom.Rect","global!Date","global!Math","ephox.sugar.api.search.PredicateFilter","ephox.katamari.api.Thunk","ephox.sand.api.Node","ephox.sand.api.PlatformDetection","ephox.sugar.api.search.Traverse","ephox.sand.util.Global","ephox.sand.core.PlatformDetection","global!navigator","ephox.katamari.api.Struct","ephox.sugar.alien.Recurse","ephox.katamari.api.Resolve","ephox.sand.core.Browser","ephox.sand.core.OperatingSystem","ephox.sand.detect.DeviceType","ephox.sand.detect.UaString","ephox.sand.info.PlatformInfo","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.katamari.api.Global","ephox.sand.detect.Version","ephox.katamari.api.Strings","ephox.katamari.api.Obj","ephox.katamari.util.BagUtils","global!Number","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts"] jsc*/ defineGlobal("global!window", window); defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); @@ -96,12 +96,12 @@ defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve); */ define( - 'tinymce.core.AddOnManager', + 'tinymce.core.ThemeManager', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.AddOnManager'); + return resolve('tinymce.ThemeManager'); } ); @@ -136,32 +136,173 @@ define( */ define( - 'tinymce.core.Env', + 'tinymce.core.util.Tools', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.Env'); + return resolve('tinymce.util.Tools'); } ); /** - * ResolveGlobal.js + * Settings.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.ui.Api', + 'tinymce.themes.modern.api.Settings', [ - 'global!tinymce.util.Tools.resolve' + 'tinymce.core.EditorManager', + 'tinymce.core.util.Tools' ], - function (resolve) { - return resolve('tinymce.ui.Api'); + function (EditorManager, Tools) { + var isBrandingEnabled = function (editor) { + return editor.getParam('branding', true); + }; + + var hasMenubar = function (editor) { + return getMenubar(editor) !== false; + }; + + var getMenubar = function (editor) { + return editor.getParam('menubar'); + }; + + var hasStatusbar = function (editor) { + return editor.getParam('statusbar', true); + }; + + var getToolbarSize = function (editor) { + return editor.getParam('toolbar_items_size'); + }; + + var getResize = function (editor) { + var resize = editor.getParam('resize', 'vertical'); + if (resize === false) { + return 'none'; + } else if (resize === 'both') { + return 'both'; + } else { + return 'vertical'; + } + }; + + var isReadOnly = function (editor) { + return editor.getParam('readonly', false); + }; + + var getFixedToolbarContainer = function (editor) { + return editor.getParam('fixed_toolbar_container'); + }; + + var getInlineToolbarPositionHandler = function (editor) { + return editor.getParam('inline_toolbar_position_handler'); + }; + + var getMenu = function (editor) { + return editor.getParam('menu'); + }; + + var getRemovedMenuItems = function (editor) { + return editor.getParam('removed_menuitems', ''); + }; + + var getMinWidth = function (editor) { + return editor.getParam('min_width', 100); + }; + + var getMinHeight = function (editor) { + return editor.getParam('min_height', 100); + }; + + var getMaxWidth = function (editor) { + return editor.getParam('max_width', 0xFFFF); + }; + + var getMaxHeight = function (editor) { + return editor.getParam('max_height', 0xFFFF); + }; + + var getSkinUrl = function (editor) { + var settings = editor.settings; + var skin = settings.skin; + var skinUrl = settings.skin_url; + + if (skin !== false) { + var skinName = skin ? skin : 'lightgray'; + + if (skinUrl) { + skinUrl = editor.documentBaseURI.toAbsolute(skinUrl); + } else { + skinUrl = EditorManager.baseURL + '/skins/' + skinName; + } + } + + return skinUrl; + }; + + var isInline = function (editor) { + return editor.getParam('inline', false); + }; + + var getIndexedToolbars = function (settings, defaultToolbar) { + var toolbars = []; + + // Generate toolbar + for (var i = 1; i < 10; i++) { + var toolbar = settings['toolbar' + i]; + if (!toolbar) { + break; + } + + toolbars.push(toolbar); + } + + var mainToolbar = settings.toolbar ? [ settings.toolbar ] : [ defaultToolbar ]; + return toolbars.length > 0 ? toolbars : mainToolbar; + }; + + var getToolbars = function (editor) { + var toolbar = editor.getParam('toolbar'); + var defaultToolbar = 'undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image'; + + if (toolbar === false) { + return []; + } else if (Tools.isArray(toolbar)) { + return Tools.grep(toolbar, function (toolbar) { + return toolbar.length > 0; + }); + } else { + return getIndexedToolbars(editor.settings, defaultToolbar); + } + }; + + return { + isBrandingEnabled: isBrandingEnabled, + hasMenubar: hasMenubar, + getMenubar: getMenubar, + hasStatusbar: hasStatusbar, + getToolbarSize: getToolbarSize, + getResize: getResize, + isReadOnly: isReadOnly, + getFixedToolbarContainer: getFixedToolbarContainer, + getInlineToolbarPositionHandler: getInlineToolbarPositionHandler, + getMenu: getMenu, + getRemovedMenuItems: getRemovedMenuItems, + getMinWidth: getMinWidth, + getMinHeight: getMinHeight, + getMaxWidth: getMaxWidth, + getMaxHeight: getMaxHeight, + getSkinUrl: getSkinUrl, + isInline: isInline, + getToolbars: getToolbars + }; } ); @@ -206,22 +347,37 @@ define( ); /** - * ResolveGlobal.js + * Events.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.util.Tools', + 'tinymce.themes.modern.api.Events', [ - 'global!tinymce.util.Tools.resolve' ], - function (resolve) { - return resolve('tinymce.util.Tools'); + function () { + var fireSkinLoaded = function (editor) { + return editor.fire('SkinLoaded'); + }; + + var fireResizeEditor = function (editor) { + return editor.fire('ResizeEditor'); + }; + + var fireBeforeRenderUI = function (editor) { + return editor.fire('BeforeRenderUI'); + }; + + return { + fireSkinLoaded: fireSkinLoaded, + fireResizeEditor: fireResizeEditor, + fireBeforeRenderUI: fireBeforeRenderUI + }; } ); @@ -265,86 +421,7 @@ define( } ); -/** - * Branding.js - * - * Released under LGPL License. - * Copyright (c) 1999-2016 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -define( - 'tinymce.themes.modern.ui.Branding', - [ - 'tinymce.core.dom.DOMUtils' - ], - function (DOMUtils) { - var DOM = DOMUtils.DOM; - - var reposition = function (editor, poweredByElm, hasStatusbar) { - return function () { - var iframeWidth = editor.getContentAreaContainer().querySelector('iframe').offsetWidth; - var scrollbarWidth = Math.max(iframeWidth - editor.getDoc().documentElement.offsetWidth, 0); - - DOM.setStyle(poweredByElm, 'right', scrollbarWidth + 'px'); - if (hasStatusbar) { - DOM.setStyle(poweredByElm, 'top', '-16px'); - } else { - DOM.setStyle(poweredByElm, 'bottom', '1px'); - } - }; - }; - - var hide = function (poweredByElm) { - return function () { - DOM.hide(poweredByElm); - }; - }; - - var setupReposition = function (editor, poweredByElm, hasStatusbar) { - reposition(editor, poweredByElm, hasStatusbar)(); - editor.on('NodeChange ResizeEditor', reposition(editor, poweredByElm, hasStatusbar)); - }; - - var appendToStatusbar = function (editor, poweredByElm, statusbarElm) { - statusbarElm.appendChild(poweredByElm); - setupReposition(editor, poweredByElm, true); - }; - - var appendToContainer = function (editor, poweredByElm) { - editor.getContainer().appendChild(poweredByElm); - setupReposition(editor, poweredByElm, false); - }; - - var setupEventListeners = function (editor) { - editor.on('SkinLoaded', function () { - var poweredByElm = DOM.create('div', { 'class': 'mce-branding-powered-by' }); - var statusbarElm = editor.getContainer().querySelector('.mce-statusbar'); - - if (statusbarElm) { - appendToStatusbar(editor, poweredByElm, statusbarElm); - } else { - appendToContainer(editor, poweredByElm); - } - - DOM.bind(poweredByElm, 'click', hide(poweredByElm)); - }); - }; - - var setup = function (editor) { - if (editor.settings.branding !== false) { - setupEventListeners(editor); - } - }; - - return { - setup: setup - }; - } -); - +defineGlobal("global!document", document); /** * ResolveGlobal.js * @@ -356,12 +433,12 @@ define( */ define( - 'tinymce.core.util.Delay', + 'tinymce.core.geom.Rect', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.util.Delay'); + return resolve('tinymce.geom.Rect'); } ); @@ -376,12 +453,12 @@ define( */ define( - 'tinymce.core.geom.Rect', + 'tinymce.core.util.Delay', [ 'global!tinymce.util.Tools.resolve' ], function (resolve) { - return resolve('tinymce.geom.Rect'); + return resolve('tinymce.util.Delay'); } ); @@ -398,13 +475,11 @@ define( define( 'tinymce.themes.modern.ui.Toolbar', [ + 'tinymce.core.ui.Factory', 'tinymce.core.util.Tools', - 'tinymce.core.ui.Factory' + 'tinymce.themes.modern.api.Settings' ], - function (Tools, Factory) { - var defaultToolbar = "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | " + - "bullist numlist outdent indent | link image"; - + function (Factory, Tools, Settings) { var createToolbar = function (editor, items, size) { var toolbarItems = [], buttonGroup; @@ -431,7 +506,7 @@ define( } }; - if (item == "|") { + if (item === "|") { buttonGroup = null; } else { if (!buttonGroup) { @@ -444,7 +519,7 @@ define( itemName = item; item = editor.buttons[itemName]; - if (typeof item == "function") { + if (typeof item === "function") { item = item(); } @@ -477,40 +552,17 @@ define( * @return {Array} Array with toolbars. */ var createToolbars = function (editor, size) { - var toolbars = [], settings = editor.settings; + var toolbars = []; var addToolbar = function (items) { if (items) { toolbars.push(createToolbar(editor, items, size)); - return true; } }; - // Convert toolbar array to multiple options - if (Tools.isArray(settings.toolbar)) { - // Empty toolbar array is the same as a disabled toolbar - if (settings.toolbar.length === 0) { - return; - } - - Tools.each(settings.toolbar, function (toolbar, i) { - settings["toolbar" + (i + 1)] = toolbar; - }); - - delete settings.toolbar; - } - - // Generate toolbar - for (var i = 1; i < 10; i++) { - if (!addToolbar(settings["toolbar" + i])) { - break; - } - } - - // Generate toolbar or default toolbar unless it's disabled - if (!toolbars.length && settings.toolbar !== false) { - addToolbar(settings.toolbar || defaultToolbar); - } + Tools.each(Settings.getToolbars(editor), function (toolbar) { + addToolbar(toolbar); + }); if (toolbars.length) { return { @@ -544,14 +596,16 @@ define( define( 'tinymce.themes.modern.ui.ContextToolbars', [ + 'global!document', 'tinymce.core.dom.DOMUtils', - 'tinymce.core.util.Tools', - 'tinymce.core.util.Delay', - 'tinymce.core.ui.Factory', 'tinymce.core.geom.Rect', + 'tinymce.core.ui.Factory', + 'tinymce.core.util.Delay', + 'tinymce.core.util.Tools', + 'tinymce.themes.modern.api.Settings', 'tinymce.themes.modern.ui.Toolbar' ], - function (DOMUtils, Tools, Delay, Factory, Rect, Toolbar) { + function (document, DOMUtils, Rect, Factory, Delay, Tools, Settings, Toolbar) { var DOM = DOMUtils.DOM; var toClientRect = function (geomRect) { @@ -610,7 +664,7 @@ define( }; var addContextualToolbars = function (editor) { - var scrollContainer, settings = editor.settings; + var scrollContainer; var getContextToolbars = function () { return editor.contextToolbars || []; @@ -637,7 +691,7 @@ define( var reposition = function (match, shouldShow) { var relPos, panelRect, elementRect, contentAreaRect, panel, relRect, testPositions, smallElementWidthThreshold; - var handler = settings.inline_toolbar_position_handler; + var handler = Settings.getInlineToolbarPositionHandler(editor); if (editor.removed) { return; @@ -858,341 +912,591 @@ define( } ); -/** - * Menubar.js - * - * Released under LGPL License. - * Copyright (c) 1999-2016 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - +defineGlobal("global!Array", Array); +defineGlobal("global!Error", Error); define( - 'tinymce.themes.modern.ui.Menubar', + 'ephox.katamari.api.Fun', + [ - 'tinymce.core.util.Tools' + 'global!Array', + 'global!Error' ], - function (Tools) { - var defaultMenus = { - file: { title: 'File', items: 'newdocument' }, - edit: { title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall' }, - insert: { title: 'Insert', items: '|' }, - view: { title: 'View', items: 'visualaid |' }, - format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript | formats | removeformat' }, - table: { title: 'Table' }, - tools: { title: 'Tools' } - }; - var createMenuItem = function (menuItems, name) { - var menuItem; + function (Array, Error) { - if (name == '|') { - return { text: '|' }; - } + var noop = function () { }; - menuItem = menuItems[name]; + var compose = function (fa, fb) { + return function () { + return fa(fb.apply(null, arguments)); + }; + }; - return menuItem; + var constant = function (value) { + return function () { + return value; + }; }; - var createMenu = function (editorMenuItems, settings, context) { - var menuButton, menu, menuItems, isUserDefined, removedMenuItems; + var identity = function (x) { + return x; + }; - removedMenuItems = Tools.makeMap((settings.removed_menuitems || '').split(/[ ,]/)); + var tripleEquals = function(a, b) { + return a === b; + }; - // User defined menu - if (settings.menu) { - menu = settings.menu[context]; - isUserDefined = true; - } else { - menu = defaultMenus[context]; - } + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var curry = function (f) { + // equivalent to arguments.slice(1) + // starting at 1 because 0 is the f, makes things tricky. + // Pay attention to what variable is where, and the -1 magic. + // thankfully, we have tests for this. + var args = new Array(arguments.length - 1); + for (var i = 1; i < arguments.length; i++) args[i-1] = arguments[i]; - if (menu) { - menuButton = { text: menu.title }; - menuItems = []; + return function () { + var newArgs = new Array(arguments.length); + for (var j = 0; j < newArgs.length; j++) newArgs[j] = arguments[j]; - // Default/user defined items - Tools.each((menu.items || '').split(/[ ,]/), function (item) { - var menuItem = createMenuItem(editorMenuItems, item); + var all = args.concat(newArgs); + return f.apply(null, all); + }; + }; - if (menuItem && !removedMenuItems[item]) { - menuItems.push(createMenuItem(editorMenuItems, item)); - } - }); + var not = function (f) { + return function () { + return !f.apply(null, arguments); + }; + }; - // Added though context - if (!isUserDefined) { - Tools.each(editorMenuItems, function (menuItem) { - if (menuItem.context == context) { - if (menuItem.separator == 'before') { - menuItems.push({ text: '|' }); - } + var die = function (msg) { + return function () { + throw new Error(msg); + }; + }; - if (menuItem.prependToContext) { - menuItems.unshift(menuItem); - } else { - menuItems.push(menuItem); - } + var apply = function (f) { + return f(); + }; - if (menuItem.separator == 'after') { - menuItems.push({ text: '|' }); - } - } - }); - } + var call = function(f) { + f(); + }; - for (var i = 0; i < menuItems.length; i++) { - if (menuItems[i].text == '|') { - if (i === 0 || i == menuItems.length - 1) { - menuItems.splice(i, 1); - } - } - } + var never = constant(false); + var always = constant(true); + - menuButton.menu = menuItems; + return { + noop: noop, + compose: compose, + constant: constant, + identity: identity, + tripleEquals: tripleEquals, + curry: curry, + not: not, + die: die, + apply: apply, + call: call, + never: never, + always: always + }; + } +); - if (!menuButton.menu.length) { - return null; - } - } +defineGlobal("global!Object", Object); +define( + 'ephox.katamari.api.Option', - return menuButton; - }; + [ + 'ephox.katamari.api.Fun', + 'global!Object' + ], - var createMenuButtons = function (editor) { - var name, menuButtons = [], settings = editor.settings; + function (Fun, Object) { - var defaultMenuBar = []; - if (settings.menu) { - for (name in settings.menu) { - defaultMenuBar.push(name); - } - } else { - for (name in defaultMenus) { - defaultMenuBar.push(name); - } - } + var never = Fun.never; + var always = Fun.always; - var enabledMenuNames = typeof settings.menubar == "string" ? settings.menubar.split(/[ ,]/) : defaultMenuBar; - for (var i = 0; i < enabledMenuNames.length; i++) { - var menu = enabledMenuNames[i]; - menu = createMenu(editor.menuItems, editor.settings, menu); + /** + Option objects support the following methods: - if (menu) { - menuButtons.push(menu); + fold :: this Option a -> ((() -> b, a -> b)) -> Option b + + is :: this Option a -> a -> Boolean + + isSome :: this Option a -> () -> Boolean + + isNone :: this Option a -> () -> Boolean + + getOr :: this Option a -> a -> a + + getOrThunk :: this Option a -> (() -> a) -> a + + getOrDie :: this Option a -> String -> a + + or :: this Option a -> Option a -> Option a + - if some: return self + - if none: return opt + + orThunk :: this Option a -> (() -> Option a) -> Option a + - Same as "or", but uses a thunk instead of a value + + map :: this Option a -> (a -> b) -> Option b + - "fmap" operation on the Option Functor. + - same as 'each' + + ap :: this Option a -> Option (a -> b) -> Option b + - "apply" operation on the Option Apply/Applicative. + - Equivalent to <*> in Haskell/PureScript. + + each :: this Option a -> (a -> b) -> Option b + - same as 'map' + + bind :: this Option a -> (a -> Option b) -> Option b + - "bind"/"flatMap" operation on the Option Bind/Monad. + - Equivalent to >>= in Haskell/PureScript; flatMap in Scala. + + flatten :: {this Option (Option a))} -> () -> Option a + - "flatten"/"join" operation on the Option Monad. + + exists :: this Option a -> (a -> Boolean) -> Boolean + + forall :: this Option a -> (a -> Boolean) -> Boolean + + filter :: this Option a -> (a -> Boolean) -> Option a + + equals :: this Option a -> Option a -> Boolean + + equals_ :: this Option a -> (Option a, a -> Boolean) -> Boolean + + toArray :: this Option a -> () -> [a] + + */ + + var none = function () { return NONE; }; + + var NONE = (function () { + var eq = function (o) { + return o.isNone(); + }; + + // inlined from peanut, maybe a micro-optimisation? + var call = function (thunk) { return thunk(); }; + var id = function (n) { return n; }; + var noop = function () { }; + + var me = { + fold: function (n, s) { return n(); }, + is: never, + isSome: never, + isNone: always, + getOr: id, + getOrThunk: call, + getOrDie: function (msg) { + throw new Error(msg || 'error: getOrDie called on none.'); + }, + or: id, + orThunk: call, + map: none, + ap: none, + each: noop, + bind: none, + flatten: none, + exists: never, + forall: always, + filter: none, + equals: eq, + equals_: eq, + toArray: function () { return []; }, + toString: Fun.constant("none()") + }; + if (Object.freeze) Object.freeze(me); + return me; + })(); + + + /** some :: a -> Option a */ + var some = function (a) { + + // inlined from peanut, maybe a micro-optimisation? + var constant_a = function () { return a; }; + + var self = function () { + // can't Fun.constant this one + return me; + }; + + var map = function (f) { + return some(f(a)); + }; + + var bind = function (f) { + return f(a); + }; + + var me = { + fold: function (n, s) { return s(a); }, + is: function (v) { return a === v; }, + isSome: always, + isNone: never, + getOr: constant_a, + getOrThunk: constant_a, + getOrDie: constant_a, + or: self, + orThunk: self, + map: map, + ap: function (optfab) { + return optfab.fold(none, function(fab) { + return some(fab(a)); + }); + }, + each: function (f) { + f(a); + }, + bind: bind, + flatten: constant_a, + exists: bind, + forall: bind, + filter: function (f) { + return f(a) ? me : NONE; + }, + equals: function (o) { + return o.is(a); + }, + equals_: function (o, elementEq) { + return o.fold( + never, + function (b) { return elementEq(a, b); } + ); + }, + toArray: function () { + return [a]; + }, + toString: function () { + return 'some(' + a + ')'; } - } + }; + return me; + }; - return menuButtons; + /** from :: undefined|null|a -> Option a */ + var from = function (value) { + return value === null || value === undefined ? NONE : some(value); }; return { - createMenuButtons: createMenuButtons + some: some, + none: none, + from: from }; } ); -/** - * Resize.js - * - * Released under LGPL License. - * Copyright (c) 1999-2016 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - +defineGlobal("global!String", String); define( - 'tinymce.themes.modern.ui.Resize', + 'ephox.katamari.api.Arr', + [ - 'tinymce.core.dom.DOMUtils' + 'ephox.katamari.api.Option', + 'global!Array', + 'global!Error', + 'global!String' ], - function (DOMUtils) { - var DOM = DOMUtils.DOM; - var getSize = function (elm) { - return { - width: elm.clientWidth, - height: elm.clientHeight - }; - }; - var resizeTo = function (editor, width, height) { - var containerElm, iframeElm, containerSize, iframeSize, settings = editor.settings; + function (Option, Array, Error, String) { + // Use the native Array.indexOf if it is available (IE9+) otherwise fall back to manual iteration + // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf + var rawIndexOf = (function () { + var pIndexOf = Array.prototype.indexOf; - containerElm = editor.getContainer(); - iframeElm = editor.getContentAreaContainer().firstChild; - containerSize = getSize(containerElm); - iframeSize = getSize(iframeElm); + var fastIndex = function (xs, x) { return pIndexOf.call(xs, x); }; - if (width !== null) { - width = Math.max(settings.min_width || 100, width); - width = Math.min(settings.max_width || 0xFFFF, width); + var slowIndex = function(xs, x) { return slowIndexOf(xs, x); }; - DOM.setStyle(containerElm, 'width', width + (containerSize.width - iframeSize.width)); - DOM.setStyle(iframeElm, 'width', width); + return pIndexOf === undefined ? slowIndex : fastIndex; + })(); + + var indexOf = function (xs, x) { + // The rawIndexOf method does not wrap up in an option. This is for performance reasons. + var r = rawIndexOf(xs, x); + return r === -1 ? Option.none() : Option.some(r); + }; + + var contains = function (xs, x) { + return rawIndexOf(xs, x) > -1; + }; + + // Using findIndex is likely less optimal in Chrome (dynamic return type instead of bool) + // but if we need that micro-optimisation we can inline it later. + var exists = function (xs, pred) { + return findIndex(xs, pred).isSome(); + }; + + var range = function (num, f) { + var r = []; + for (var i = 0; i < num; i++) { + r.push(f(i)); } + return r; + }; - height = Math.max(settings.min_height || 100, height); - height = Math.min(settings.max_height || 0xFFFF, height); - DOM.setStyle(iframeElm, 'height', height); + // It's a total micro optimisation, but these do make some difference. + // Particularly for browsers other than Chrome. + // - length caching + // http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69 + // - not using push + // http://jsperf.com/array-direct-assignment-vs-push/2 - editor.fire('ResizeEditor'); + var chunk = function (array, size) { + var r = []; + for (var i = 0; i < array.length; i += size) { + var s = array.slice(i, i + size); + r.push(s); + } + return r; }; - var resizeBy = function (editor, dw, dh) { - var elm = editor.getContentAreaContainer(); - resizeTo(editor, elm.clientWidth + dw, elm.clientHeight + dh); + var map = function(xs, f) { + // pre-allocating array size when it's guaranteed to be known + // http://jsperf.com/push-allocated-vs-dynamic/22 + var len = xs.length; + var r = new Array(len); + for (var i = 0; i < len; i++) { + var x = xs[i]; + r[i] = f(x, i, xs); + } + return r; }; - return { - resizeTo: resizeTo, - resizeBy: resizeBy + // Unwound implementing other functions in terms of each. + // The code size is roughly the same, and it should allow for better optimisation. + var each = function(xs, f) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + f(x, i, xs); + } }; - } -); -/** - * Sidebar.js - * - * Released under LGPL License. - * Copyright (c) 1999-2016 Ephox Corp. All rights reserved - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ + var eachr = function (xs, f) { + for (var i = xs.length - 1; i >= 0; i--) { + var x = xs[i]; + f(x, i, xs); + } + }; -define( - 'tinymce.themes.modern.ui.Sidebar', - [ - 'tinymce.core.util.Tools', - 'tinymce.core.ui.Factory', - 'tinymce.core.Env' - ], - function (Tools, Factory, Env) { - var api = function (elm) { - return { - element: function () { - return elm; - } - }; + var partition = function(xs, pred) { + var pass = []; + var fail = []; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + var arr = pred(x, i, xs) ? pass : fail; + arr.push(x); + } + return { pass: pass, fail: fail }; }; - var trigger = function (sidebar, panel, callbackName) { - var callback = sidebar.settings[callbackName]; - if (callback) { - callback(api(panel.getEl('body'))); + var filter = function(xs, pred) { + var r = []; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + r.push(x); + } } + return r; }; - var hidePanels = function (name, container, sidebars) { - Tools.each(sidebars, function (sidebar) { - var panel = container.items().filter('#' + sidebar.name)[0]; + /* + * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f. + * + * f is a function that derives a value from an element - e.g. true or false, or a string. + * Elements are like if this function generates the same value for them (according to ===). + * + * + * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function. + * For a good explanation, see the group function (which is a special case of groupBy) + * http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group + */ + var groupBy = function (xs, f) { + if (xs.length === 0) { + return []; + } else { + var wasType = f(xs[0]); // initial case for matching + var r = []; + var group = []; - if (panel && panel.visible() && sidebar.name !== name) { - trigger(sidebar, panel, 'onhide'); - panel.visible(false); + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + var type = f(x); + if (type !== wasType) { + r.push(group); + group = []; + } + wasType = type; + group.push(x); } + if (group.length !== 0) { + r.push(group); + } + return r; + } + }; + + var foldr = function (xs, f, acc) { + eachr(xs, function (x) { + acc = f(acc, x); }); + return acc; }; - var deactivateButtons = function (toolbar) { - toolbar.items().each(function (ctrl) { - ctrl.active(false); + var foldl = function (xs, f, acc) { + each(xs, function (x) { + acc = f(acc, x); }); + return acc; }; - var findSidebar = function (sidebars, name) { - return Tools.grep(sidebars, function (sidebar) { - return sidebar.name === name; - })[0]; + var find = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + return Option.some(x); + } + } + return Option.none(); }; - var showPanel = function (editor, name, sidebars) { - return function (e) { - var btnCtrl = e.control; - var container = btnCtrl.parents().filter('panel')[0]; - var panel = container.find('#' + name)[0]; - var sidebar = findSidebar(sidebars, name); + var findIndex = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + return Option.some(i); + } + } - hidePanels(name, container, sidebars); - deactivateButtons(btnCtrl.parent()); + return Option.none(); + }; - if (panel && panel.visible()) { - trigger(sidebar, panel, 'onhide'); - panel.hide(); - btnCtrl.active(false); - } else { - if (panel) { - panel.show(); - trigger(sidebar, panel, 'onshow'); - } else { - panel = Factory.create({ - type: 'container', - name: name, - layout: 'stack', - classes: 'sidebar-panel', - html: '' - }); + var slowIndexOf = function (xs, x) { + for (var i = 0, len = xs.length; i < len; ++i) { + if (xs[i] === x) { + return i; + } + } - container.prepend(panel); - trigger(sidebar, panel, 'onrender'); - trigger(sidebar, panel, 'onshow'); - } + return -1; + }; - btnCtrl.active(true); - } + var push = Array.prototype.push; + var flatten = function (xs) { + // Note, this is possible because push supports multiple arguments: + // http://jsperf.com/concat-push/6 + // Note that in the past, concat() would silently work (very slowly) for array-like objects. + // With this change it will throw an error. + var r = []; + for (var i = 0, len = xs.length; i < len; ++i) { + // Ensure that each value is an array itself + if (! Array.prototype.isPrototypeOf(xs[i])) throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); + push.apply(r, xs[i]); + } + return r; + }; - editor.fire('ResizeEditor'); - }; + var bind = function (xs, f) { + var output = map(xs, f); + return flatten(output); }; - var isModernBrowser = function () { - return !Env.ie || Env.ie >= 11; + var forall = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; ++i) { + var x = xs[i]; + if (pred(x, i, xs) !== true) { + return false; + } + } + return true; }; - var hasSidebar = function (editor) { - return isModernBrowser() && editor.sidebars ? editor.sidebars.length > 0 : false; + var equal = function (a1, a2) { + return a1.length === a2.length && forall(a1, function (x, i) { + return x === a2[i]; + }); }; - var createSidebar = function (editor) { - var buttons = Tools.map(editor.sidebars, function (sidebar) { - var settings = sidebar.settings; + var slice = Array.prototype.slice; + var reverse = function (xs) { + var r = slice.call(xs, 0); + r.reverse(); + return r; + }; - return { - type: 'button', - icon: settings.icon, - image: settings.image, - tooltip: settings.tooltip, - onclick: showPanel(editor, sidebar.name, editor.sidebars) - }; + var difference = function (a1, a2) { + return filter(a1, function (x) { + return !contains(a2, x); }); + }; - return { - type: 'panel', - name: 'sidebar', - layout: 'stack', - classes: 'sidebar', - items: [ - { - type: 'toolbar', - layout: 'stack', - classes: 'sidebar-toolbar', - items: buttons - } - ] - }; + var mapToObject = function(xs, f) { + var r = {}; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + r[String(x)] = f(x, i); + } + return r; + }; + + var pure = function(x) { + return [x]; + }; + + var sort = function (xs, comparator) { + var copy = slice.call(xs, 0); + copy.sort(comparator); + return copy; + }; + + var head = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[0]); + }; + + var last = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[xs.length - 1]); }; return { - hasSidebar: hasSidebar, - createSidebar: createSidebar + map: map, + each: each, + eachr: eachr, + partition: partition, + filter: filter, + groupBy: groupBy, + indexOf: indexOf, + foldr: foldr, + foldl: foldl, + find: find, + findIndex: findIndex, + flatten: flatten, + bind: bind, + forall: forall, + exists: exists, + contains: contains, + equal: equal, + reverse: reverse, + chunk: chunk, + difference: difference, + mapToObject: mapToObject, + pure: pure, + sort: sort, + range: range, + head: head, + last: last }; } ); /** - * SkinLoaded.js + * Menubar.js * * Released under LGPL License. * Copyright (c) 1999-2016 Ephox Corp. All rights reserved @@ -1202,32 +1506,152 @@ define( */ define( - 'tinymce.themes.modern.ui.SkinLoaded', [ + 'tinymce.themes.modern.ui.Menubar', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'tinymce.core.util.Tools', + 'tinymce.themes.modern.api.Settings' ], - function () { - var fireSkinLoaded = function (editor) { - var done = function () { - editor._skinLoaded = true; - editor.fire('SkinLoaded'); - }; - - return function () { - if (editor.initialized) { - done(); - } else { - editor.on('init', done); - } - }; + function (Arr, Fun, Tools, Settings) { + var defaultMenus = { + file: { title: 'File', items: 'newdocument restoredraft | preview | print' }, + edit: { title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall' }, + view: { title: 'View', items: 'code | visualaid visualchars visualblocks | spellchecker | preview fullscreen' }, + insert: { title: 'Insert', items: 'image link media template codesample inserttable | charmap hr | pagebreak nonbreaking anchor toc | insertdatetime' }, + format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat | blockformats align | removeformat' }, + tools: { title: 'Tools', items: 'spellchecker spellcheckerlanguage | a11ycheck' }, + table: { title: 'Table' }, + help: { title: 'Help' } + }; + + var delimiterMenuNamePair = Fun.constant({ name: '|', item: { text: '|' } }); + + var createMenuNameItemPair = function (name, item) { + var menuItem = item ? { name: name, item: item } : null; + return name === '|' ? delimiterMenuNamePair() : menuItem; + }; + + var hasItemName = function (namedMenuItems, name) { + return Arr.findIndex(namedMenuItems, function (namedMenuItem) { + return namedMenuItem.name === name; + }).isSome(); + }; + + var isSeparator = function (namedMenuItem) { + return namedMenuItem && namedMenuItem.item.text === '|'; + }; + + var cleanupMenu = function (namedMenuItems) { + return Arr.filter(namedMenuItems, function (namedMenuItem, i, namedMenuItems) { + if (isSeparator(namedMenuItem)) { + return i > 0 && i < namedMenuItems.length - 1 && !isSeparator(namedMenuItems[i - 1]); + } else { + return true; + } + }); + }; + + var createMenu = function (editorMenuItems, menus, removedMenuItems, context) { + var menuButton, menu, namedMenuItems, isUserDefined; + + // User defined menu + if (menus) { + menu = menus[context]; + isUserDefined = true; + } else { + menu = defaultMenus[context]; + } + + if (menu) { + menuButton = { text: menu.title }; + namedMenuItems = []; + + // Default/user defined items + Tools.each((menu.items || '').split(/[ ,]/), function (name) { + var namedMenuItem = createMenuNameItemPair(name, editorMenuItems[name]); + + if (namedMenuItem && !removedMenuItems[name]) { + namedMenuItems.push(namedMenuItem); + } + }); + + // Added though context + if (!isUserDefined) { + Tools.each(editorMenuItems, function (item, name) { + if (item.context === context && !hasItemName(namedMenuItems, name)) { + if (item.separator === 'before') { + namedMenuItems.push(delimiterMenuNamePair()); + } + + if (item.prependToContext) { + namedMenuItems.unshift(createMenuNameItemPair(name, item)); + } else { + namedMenuItems.push(createMenuNameItemPair(name, item)); + } + + if (item.separator === 'after') { + namedMenuItems.push(delimiterMenuNamePair()); + } + } + }); + } + + menuButton.menu = Arr.map(cleanupMenu(namedMenuItems), function (menuItem) { + return menuItem.item; + }); + + if (!menuButton.menu.length) { + return null; + } + } + + return menuButton; + }; + + var getDefaultMenubar = function (editor) { + var name, defaultMenuBar = []; + var menu = Settings.getMenu(editor); + + if (menu) { + for (name in menu) { + defaultMenuBar.push(name); + } + } else { + for (name in defaultMenus) { + defaultMenuBar.push(name); + } + } + + return defaultMenuBar; + }; + + var createMenuButtons = function (editor) { + var menuButtons = []; + var defaultMenuBar = getDefaultMenubar(editor); + var removedMenuItems = Tools.makeMap(Settings.getRemovedMenuItems(editor).split(/[ ,]/)); + + var menubar = Settings.getMenubar(editor); + var enabledMenuNames = typeof menubar === "string" ? menubar.split(/[ ,]/) : defaultMenuBar; + for (var i = 0; i < enabledMenuNames.length; i++) { + var menuItems = enabledMenuNames[i]; + var menu = createMenu(editor.menuItems, Settings.getMenu(editor), removedMenuItems, menuItems); + if (menu) { + menuButtons.push(menu); + } + } + + return menuButtons; }; return { - fireSkinLoaded: fireSkinLoaded + createMenuButtons: createMenuButtons }; } ); /** - * Iframe.js + * Resize.js * * Released under LGPL License. * Copyright (c) 1999-2016 Ephox Corp. All rights reserved @@ -1237,145 +1661,15948 @@ define( */ define( - 'tinymce.themes.modern.modes.Iframe', + 'tinymce.themes.modern.ui.Resize', [ 'tinymce.core.dom.DOMUtils', - 'tinymce.core.ui.Factory', - 'tinymce.core.util.Tools', - 'tinymce.themes.modern.ui.A11y', - 'tinymce.themes.modern.ui.Branding', - 'tinymce.themes.modern.ui.ContextToolbars', - 'tinymce.themes.modern.ui.Menubar', - 'tinymce.themes.modern.ui.Resize', - 'tinymce.themes.modern.ui.Sidebar', - 'tinymce.themes.modern.ui.SkinLoaded', - 'tinymce.themes.modern.ui.Toolbar' + 'tinymce.themes.modern.api.Events', + 'tinymce.themes.modern.api.Settings' ], - function (DOMUtils, Factory, Tools, A11y, Branding, ContextToolbars, Menubar, Resize, Sidebar, SkinLoaded, Toolbar) { + function (DOMUtils, Events, Settings) { var DOM = DOMUtils.DOM; - - var switchMode = function (panel) { - return function (e) { - panel.find('*').disabled(e.mode === 'readonly'); + var getSize = function (elm) { + return { + width: elm.clientWidth, + height: elm.clientHeight }; }; - var editArea = function (border) { - return { - type: 'panel', - name: 'iframe', - layout: 'stack', - classes: 'edit-area', - border: border, - html: '' - }; + var resizeTo = function (editor, width, height) { + var containerElm, iframeElm, containerSize, iframeSize; + + containerElm = editor.getContainer(); + iframeElm = editor.getContentAreaContainer().firstChild; + containerSize = getSize(containerElm); + iframeSize = getSize(iframeElm); + + if (width !== null) { + width = Math.max(Settings.getMinWidth(editor), width); + width = Math.min(Settings.getMaxWidth(editor), width); + + DOM.setStyle(containerElm, 'width', width + (containerSize.width - iframeSize.width)); + DOM.setStyle(iframeElm, 'width', width); + } + + height = Math.max(Settings.getMinHeight(editor), height); + height = Math.min(Settings.getMaxHeight(editor), height); + DOM.setStyle(iframeElm, 'height', height); + + Events.fireResizeEditor(editor); }; - var editAreaContainer = function (editor) { + var resizeBy = function (editor, dw, dh) { + var elm = editor.getContentAreaContainer(); + resizeTo(editor, elm.clientWidth + dw, elm.clientHeight + dh); + }; + + return { + resizeTo: resizeTo, + resizeBy: resizeBy + }; + } +); + +/** + * 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.Env', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.Env'); + } +); + +/** + * Sidebar.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.ui.Sidebar', + [ + 'tinymce.core.Env', + 'tinymce.core.ui.Factory', + 'tinymce.core.util.Tools', + 'tinymce.themes.modern.api.Events' + ], + function (Env, Factory, Tools, Events) { + var api = function (elm) { return { - type: 'panel', - layout: 'stack', - classes: 'edit-aria-container', - border: '1 0 0 0', - items: [ - editArea('0'), - Sidebar.createSidebar(editor) - ] + element: function () { + return elm; + } }; }; - var render = function (editor, theme, args) { - var panel, resizeHandleCtrl, startSize, settings = editor.settings; - - if (args.skinUiCss) { - DOM.styleSheetLoader.load(args.skinUiCss, SkinLoaded.fireSkinLoaded(editor)); + var trigger = function (sidebar, panel, callbackName) { + var callback = sidebar.settings[callbackName]; + if (callback) { + callback(api(panel.getEl('body'))); } + }; - panel = theme.panel = Factory.create({ - type: 'panel', - role: 'application', - classes: 'tinymce', - style: 'visibility: hidden', - layout: 'stack', - border: 1, - items: [ - settings.menubar === false ? null : { type: 'menubar', border: '0 0 1 0', items: Menubar.createMenuButtons(editor) }, - Toolbar.createToolbars(editor, settings.toolbar_items_size), - Sidebar.hasSidebar(editor) ? editAreaContainer(editor) : editArea('1 0 0 0') - ] + var hidePanels = function (name, container, sidebars) { + Tools.each(sidebars, function (sidebar) { + var panel = container.items().filter('#' + sidebar.name)[0]; + + if (panel && panel.visible() && sidebar.name !== name) { + trigger(sidebar, panel, 'onhide'); + panel.visible(false); + } }); + }; - if (settings.resize !== false) { - resizeHandleCtrl = { - type: 'resizehandle', - direction: settings.resize, + var deactivateButtons = function (toolbar) { + toolbar.items().each(function (ctrl) { + ctrl.active(false); + }); + }; - onResizeStart: function () { - var elm = editor.getContentAreaContainer().firstChild; + var findSidebar = function (sidebars, name) { + return Tools.grep(sidebars, function (sidebar) { + return sidebar.name === name; + })[0]; + }; - startSize = { - width: elm.clientWidth, - height: elm.clientHeight - }; - }, + var showPanel = function (editor, name, sidebars) { + return function (e) { + var btnCtrl = e.control; + var container = btnCtrl.parents().filter('panel')[0]; + var panel = container.find('#' + name)[0]; + var sidebar = findSidebar(sidebars, name); + + hidePanels(name, container, sidebars); + deactivateButtons(btnCtrl.parent()); + + if (panel && panel.visible()) { + trigger(sidebar, panel, 'onhide'); + panel.hide(); + btnCtrl.active(false); + } else { + if (panel) { + panel.show(); + trigger(sidebar, panel, 'onshow'); + } else { + panel = Factory.create({ + type: 'container', + name: name, + layout: 'stack', + classes: 'sidebar-panel', + html: '' + }); + + container.prepend(panel); + trigger(sidebar, panel, 'onrender'); + trigger(sidebar, panel, 'onshow'); + } + + btnCtrl.active(true); + } + + Events.fireResizeEditor(editor); + }; + }; + + var isModernBrowser = function () { + return !Env.ie || Env.ie >= 11; + }; + + var hasSidebar = function (editor) { + return isModernBrowser() && editor.sidebars ? editor.sidebars.length > 0 : false; + }; + + var createSidebar = function (editor) { + var buttons = Tools.map(editor.sidebars, function (sidebar) { + var settings = sidebar.settings; + + return { + type: 'button', + icon: settings.icon, + image: settings.image, + tooltip: settings.tooltip, + onclick: showPanel(editor, sidebar.name, editor.sidebars) + }; + }); + + return { + type: 'panel', + name: 'sidebar', + layout: 'stack', + classes: 'sidebar', + items: [ + { + type: 'toolbar', + layout: 'stack', + classes: 'sidebar-toolbar', + items: buttons + } + ] + }; + }; + + return { + hasSidebar: hasSidebar, + createSidebar: createSidebar + }; + } +); +/** + * SkinLoaded.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.ui.SkinLoaded', [ + 'tinymce.themes.modern.api.Events' + ], + function (Events) { + var fireSkinLoaded = function (editor) { + var done = function () { + editor._skinLoaded = true; + Events.fireSkinLoaded(editor); + }; + + return function () { + if (editor.initialized) { + done(); + } else { + editor.on('init', done); + } + }; + }; + + return { + fireSkinLoaded: fireSkinLoaded + }; + } +); + +/** + * Iframe.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.modes.Iframe', + [ + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.ui.Factory', + 'tinymce.core.util.Tools', + 'tinymce.themes.modern.api.Events', + 'tinymce.themes.modern.api.Settings', + 'tinymce.themes.modern.ui.A11y', + 'tinymce.themes.modern.ui.ContextToolbars', + 'tinymce.themes.modern.ui.Menubar', + 'tinymce.themes.modern.ui.Resize', + 'tinymce.themes.modern.ui.Sidebar', + 'tinymce.themes.modern.ui.SkinLoaded', + 'tinymce.themes.modern.ui.Toolbar' + ], + function (DOMUtils, Factory, Tools, Events, Settings, A11y, ContextToolbars, Menubar, Resize, Sidebar, SkinLoaded, Toolbar) { + var DOM = DOMUtils.DOM; + + var switchMode = function (panel) { + return function (e) { + panel.find('*').disabled(e.mode === 'readonly'); + }; + }; + + var editArea = function (border) { + return { + type: 'panel', + name: 'iframe', + layout: 'stack', + classes: 'edit-area', + border: border, + html: '' + }; + }; + + var editAreaContainer = function (editor) { + return { + type: 'panel', + layout: 'stack', + classes: 'edit-aria-container', + border: '1 0 0 0', + items: [ + editArea('0'), + Sidebar.createSidebar(editor) + ] + }; + }; + + var render = function (editor, theme, args) { + var panel, resizeHandleCtrl, startSize; + + if (args.skinUiCss) { + DOM.styleSheetLoader.load(args.skinUiCss, SkinLoaded.fireSkinLoaded(editor)); + } + + panel = theme.panel = Factory.create({ + type: 'panel', + role: 'application', + classes: 'tinymce', + style: 'visibility: hidden', + layout: 'stack', + border: 1, + items: [ + { + type: 'container', + classes: 'top-part', + items: [ + Settings.hasMenubar(editor) === false ? null : { type: 'menubar', border: '0 0 1 0', items: Menubar.createMenuButtons(editor) }, + Toolbar.createToolbars(editor, Settings.getToolbarSize(editor)) + ] + }, + Sidebar.hasSidebar(editor) ? editAreaContainer(editor) : editArea('1 0 0 0') + ] + }); + + if (Settings.getResize(editor) !== "none") { + resizeHandleCtrl = { + type: 'resizehandle', + direction: Settings.getResize(editor), + + onResizeStart: function () { + var elm = editor.getContentAreaContainer().firstChild; + + startSize = { + width: elm.clientWidth, + height: elm.clientHeight + }; + }, + + onResize: function (e) { + if (Settings.getResize(editor) === 'both') { + Resize.resizeTo(editor, startSize.width + e.deltaX, startSize.height + e.deltaY); + } else { + Resize.resizeTo(editor, null, startSize.height + e.deltaY); + } + } + }; + } + + if (Settings.hasStatusbar(editor)) { + var brandingLabel = Settings.isBrandingEnabled(editor) ? { type: 'label', classes: 'branding', html: ' powered by tinymce' } : null; + + panel.add({ + type: 'panel', name: 'statusbar', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', ariaRoot: true, items: [ + { type: 'elementpath', editor: editor }, + resizeHandleCtrl, + brandingLabel + ] + }); + } + + Events.fireBeforeRenderUI(editor); + editor.on('SwitchMode', switchMode(panel)); + panel.renderBefore(args.targetNode).reflow(); + + if (Settings.isReadOnly(editor)) { + editor.setMode('readonly'); + } + + if (args.width) { + DOM.setStyle(panel.getEl(), 'width', args.width); + } + + // Remove the panel when the editor is removed + editor.on('remove', function () { + panel.remove(); + panel = null; + }); + + // Add accesibility shortcuts + A11y.addKeys(editor, panel); + ContextToolbars.addContextualToolbars(editor); + + return { + iframeContainer: panel.find('#iframe')[0].getEl(), + editorContainer: panel.getEl() + }; + }; + + return { + render: render + }; + } +); + +/** + * 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.dom.DomQuery', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.dom.DomQuery'); + } +); + +/** + * 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.ui.DomUtils', + [ + 'global!document', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.Env', + 'tinymce.core.util.Tools' + ], + function (document, DOMUtils, Env, Tools) { + "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 for
    in

    bug on IE 8 #6178 + DOMUtils.DOM.setHTML(elm, html); + } + }; + + return funcs; + } +); +/** + * 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.ui.Movable', + [ + 'global!document', + 'global!window', + 'tinymce.ui.DomUtils' + ], + function (document, window, 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; + } + }; + } +); +/** + * 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.util.Class', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.util.Class'); + } +); + +/** + * 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.util.EventDispatcher', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.util.EventDispatcher'); + } +); + +/** + * BoxUtils.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 class for box parsing and measuring. + * + * @private + * @class tinymce.ui.BoxUtils + */ +define( + 'tinymce.ui.BoxUtils', + [ + 'global!document' + ], + function (document) { + "use strict"; + + return { + /** + * Parses the specified box value. A box value contains 1-4 properties in clockwise order. + * + * @method parseBox + * @param {String/Number} value Box value "0 1 2 3" or "0" etc. + * @return {Object} Object with top/right/bottom/left properties. + * @private + */ + parseBox: function (value) { + var len, radix = 10; + + if (!value) { + return; + } + + if (typeof value === "number") { + value = value || 0; + + return { + top: value, + left: value, + bottom: value, + right: value + }; + } + + value = value.split(' '); + len = value.length; + + if (len === 1) { + value[1] = value[2] = value[3] = value[0]; + } else if (len === 2) { + value[2] = value[0]; + value[3] = value[1]; + } else if (len === 3) { + value[3] = value[1]; + } + + return { + top: parseInt(value[0], radix) || 0, + right: parseInt(value[1], radix) || 0, + bottom: parseInt(value[2], radix) || 0, + left: parseInt(value[3], radix) || 0 + }; + }, + + measureBox: function (elm, prefix) { + function getStyle(name) { + var defaultView = document.defaultView; + + if (defaultView) { + // Remove camelcase + name = name.replace(/[A-Z]/g, function (a) { + return '-' + a; + }); + + return defaultView.getComputedStyle(elm, null).getPropertyValue(name); + } + + return elm.currentStyle[name]; + } + + function getSide(name) { + var val = parseFloat(getStyle(name), 10); + + return isNaN(val) ? 0 : val; + } + + return { + top: getSide(prefix + "TopWidth"), + right: getSide(prefix + "RightWidth"), + bottom: getSide(prefix + "BottomWidth"), + left: getSide(prefix + "LeftWidth") + }; + } + }; + } +); + +/** + * ClassList.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 adding and removal of classes. + * + * @private + * @class tinymce.ui.ClassList + */ +define( + 'tinymce.ui.ClassList', + [ + "tinymce.core.util.Tools" + ], + function (Tools) { + "use strict"; + + function noop() { + } + + /** + * Constructs a new class list the specified onchange + * callback will be executed when the class list gets modifed. + * + * @constructor ClassList + * @param {function} onchange Onchange callback to be executed. + */ + function ClassList(onchange) { + this.cls = []; + this.cls._map = {}; + this.onchange = onchange || noop; + this.prefix = ''; + } + + Tools.extend(ClassList.prototype, { + /** + * Adds a new class to the class list. + * + * @method add + * @param {String} cls Class to be added. + * @return {tinymce.ui.ClassList} Current class list instance. + */ + add: function (cls) { + if (cls && !this.contains(cls)) { + this.cls._map[cls] = true; + this.cls.push(cls); + this._change(); + } + + return this; + }, + + /** + * Removes the specified class from the class list. + * + * @method remove + * @param {String} cls Class to be removed. + * @return {tinymce.ui.ClassList} Current class list instance. + */ + remove: function (cls) { + if (this.contains(cls)) { + for (var i = 0; i < this.cls.length; i++) { + if (this.cls[i] === cls) { + break; + } + } + + this.cls.splice(i, 1); + delete this.cls._map[cls]; + this._change(); + } + + return this; + }, + + /** + * Toggles a class in the class list. + * + * @method toggle + * @param {String} cls Class to be added/removed. + * @param {Boolean} state Optional state if it should be added/removed. + * @return {tinymce.ui.ClassList} Current class list instance. + */ + toggle: function (cls, state) { + var curState = this.contains(cls); + + if (curState !== state) { + if (curState) { + this.remove(cls); + } else { + this.add(cls); + } + + this._change(); + } + + return this; + }, + + /** + * Returns true if the class list has the specified class. + * + * @method contains + * @param {String} cls Class to look for. + * @return {Boolean} true/false if the class exists or not. + */ + contains: function (cls) { + return !!this.cls._map[cls]; + }, + + /** + * Returns a space separated list of classes. + * + * @method toString + * @return {String} Space separated list of classes. + */ + + _change: function () { + delete this.clsValue; + this.onchange.call(this); + } + }); + + // IE 8 compatibility + ClassList.prototype.toString = function () { + var value; + + if (this.clsValue) { + return this.clsValue; + } + + value = ''; + for (var i = 0; i < this.cls.length; i++) { + if (i > 0) { + value += ' '; + } + + value += this.prefix + this.cls[i]; + } + + return value; + }; + + return ClassList; + } +); +/** + * Selector.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 no-nested-ternary:0 */ + +/** + * Selector engine, enables you to select controls by using CSS like expressions. + * We currently only support basic CSS expressions to reduce the size of the core + * and the ones we support should be enough for most cases. + * + * @example + * Supported expressions: + * element + * element#name + * element.class + * element[attr] + * element[attr*=value] + * element[attr~=value] + * element[attr!=value] + * element[attr^=value] + * element[attr$=value] + * element: + * element:not() + * element:first + * element:last + * element:odd + * element:even + * element element + * element > element + * + * @class tinymce.ui.Selector + */ +define( + 'tinymce.ui.Selector', + [ + "tinymce.core.util.Class" + ], + function (Class) { + "use strict"; + + /** + * Produces an array with a unique set of objects. It will not compare the values + * but the references of the objects. + * + * @private + * @method unqiue + * @param {Array} array Array to make into an array with unique items. + * @return {Array} Array with unique items. + */ + function unique(array) { + var uniqueItems = [], i = array.length, item; + + while (i--) { + item = array[i]; + + if (!item.__checked) { + uniqueItems.push(item); + item.__checked = 1; + } + } + + i = uniqueItems.length; + while (i--) { + delete uniqueItems[i].__checked; + } + + return uniqueItems; + } + + var expression = /^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i; + + /*jshint maxlen:255 */ + /*eslint max-len:0 */ + var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + whiteSpace = /^\s*|\s*$/g, + Collection; + + var Selector = Class.extend({ + /** + * Constructs a new Selector instance. + * + * @constructor + * @method init + * @param {String} selector CSS like selector expression. + */ + init: function (selector) { + var match = this.match; + + function compileNameFilter(name) { + if (name) { + name = name.toLowerCase(); + + return function (item) { + return name === '*' || item.type === name; + }; + } + } + + function compileIdFilter(id) { + if (id) { + return function (item) { + return item._name === id; + }; + } + } + + function compileClassesFilter(classes) { + if (classes) { + classes = classes.split('.'); + + return function (item) { + var i = classes.length; + + while (i--) { + if (!item.classes.contains(classes[i])) { + return false; + } + } + + return true; + }; + } + } + + function compileAttrFilter(name, cmp, check) { + if (name) { + return function (item) { + var value = item[name] ? item[name]() : ''; + + return !cmp ? !!check : + cmp === "=" ? value === check : + cmp === "*=" ? value.indexOf(check) >= 0 : + cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 : + cmp === "!=" ? value != check : + cmp === "^=" ? value.indexOf(check) === 0 : + cmp === "$=" ? value.substr(value.length - check.length) === check : + false; + }; + } + } + + function compilePsuedoFilter(name) { + var notSelectors; + + if (name) { + name = /(?:not\((.+)\))|(.+)/i.exec(name); + + if (!name[1]) { + name = name[2]; + + return function (item, index, length) { + return name === 'first' ? index === 0 : + name === 'last' ? index === length - 1 : + name === 'even' ? index % 2 === 0 : + name === 'odd' ? index % 2 === 1 : + item[name] ? item[name]() : + false; + }; + } + + // Compile not expression + notSelectors = parseChunks(name[1], []); + + return function (item) { + return !match(item, notSelectors); + }; + } + } + + function compile(selector, filters, direct) { + var parts; + + function add(filter) { + if (filter) { + filters.push(filter); + } + } + + // Parse expression into parts + parts = expression.exec(selector.replace(whiteSpace, '')); + + add(compileNameFilter(parts[1])); + add(compileIdFilter(parts[2])); + add(compileClassesFilter(parts[3])); + add(compileAttrFilter(parts[4], parts[5], parts[6])); + add(compilePsuedoFilter(parts[7])); + + // Mark the filter with pseudo for performance + filters.pseudo = !!parts[7]; + filters.direct = direct; + + return filters; + } + + // Parser logic based on Sizzle by John Resig + function parseChunks(selector, selectors) { + var parts = [], extra, matches, i; + + do { + chunker.exec(""); + matches = chunker.exec(selector); + + if (matches) { + selector = matches[3]; + parts.push(matches[1]); + + if (matches[2]) { + extra = matches[3]; + break; + } + } + } while (matches); + + if (extra) { + parseChunks(extra, selectors); + } + + selector = []; + for (i = 0; i < parts.length; i++) { + if (parts[i] != '>') { + selector.push(compile(parts[i], [], parts[i - 1] === '>')); + } + } + + selectors.push(selector); + + return selectors; + } + + this._selectors = parseChunks(selector, []); + }, + + /** + * Returns true/false if the selector matches the specified control. + * + * @method match + * @param {tinymce.ui.Control} control Control to match against the selector. + * @param {Array} selectors Optional array of selectors, mostly used internally. + * @return {Boolean} true/false state if the control matches or not. + */ + match: function (control, selectors) { + var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item; + + selectors = selectors || this._selectors; + for (i = 0, l = selectors.length; i < l; i++) { + selector = selectors[i]; + sl = selector.length; + item = control; + count = 0; + + for (si = sl - 1; si >= 0; si--) { + filters = selector[si]; + + while (item) { + // Find the index and length since a pseudo filter like :first needs it + if (filters.pseudo) { + siblings = item.parent().items(); + index = length = siblings.length; + while (index--) { + if (siblings[index] === item) { + break; + } + } + } + + for (fi = 0, fl = filters.length; fi < fl; fi++) { + if (!filters[fi](item, index, length)) { + fi = fl + 1; + break; + } + } + + if (fi === fl) { + count++; + break; + } else { + // If it didn't match the right most expression then + // break since it's no point looking at the parents + if (si === sl - 1) { + break; + } + } + + item = item.parent(); + } + } + + // If we found all selectors then return true otherwise continue looking + if (count === sl) { + return true; + } + } + + return false; + }, + + /** + * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container. + * + * @method find + * @param {tinymce.ui.Control} container Container to look for items in. + * @return {tinymce.ui.Collection} Collection with matched elements. + */ + find: function (container) { + var matches = [], i, l, selectors = this._selectors; + + function collect(items, selector, index) { + var i, l, fi, fl, item, filters = selector[index]; + + for (i = 0, l = items.length; i < l; i++) { + item = items[i]; + + // Run each filter against the item + for (fi = 0, fl = filters.length; fi < fl; fi++) { + if (!filters[fi](item, i, l)) { + fi = fl + 1; + break; + } + } + + // All filters matched the item + if (fi === fl) { + // Matched item is on the last expression like: panel toolbar [button] + if (index == selector.length - 1) { + matches.push(item); + } else { + // Collect next expression type + if (item.items) { + collect(item.items(), selector, index + 1); + } + } + } else if (filters.direct) { + return; + } + + // Collect child items + if (item.items) { + collect(item.items(), selector, index); + } + } + } + + if (container.items) { + for (i = 0, l = selectors.length; i < l; i++) { + collect(container.items(), selectors[i], 0); + } + + // Unique the matches if needed + if (l > 1) { + matches = unique(matches); + } + } + + // Fix for circular reference + if (!Collection) { + // TODO: Fix me! + Collection = Selector.Collection; + } + + return new Collection(matches); + } + }); + + return Selector; + } +); + +/** + * Collection.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 + */ + +/** + * Control collection, this class contains control instances and it enables you to + * perform actions on all the contained items. This is very similar to how jQuery works. + * + * @example + * someCollection.show().disabled(true); + * + * @class tinymce.ui.Collection + */ +define( + 'tinymce.ui.Collection', + [ + "tinymce.core.util.Tools", + "tinymce.ui.Selector", + "tinymce.core.util.Class" + ], + function (Tools, Selector, Class) { + "use strict"; + + var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice; + + proto = { + /** + * Current number of contained control instances. + * + * @field length + * @type Number + */ + length: 0, + + /** + * Constructor for the collection. + * + * @constructor + * @method init + * @param {Array} items Optional array with items to add. + */ + init: function (items) { + if (items) { + this.add(items); + } + }, + + /** + * Adds new items to the control collection. + * + * @method add + * @param {Array} items Array if items to add to collection. + * @return {tinymce.ui.Collection} Current collection instance. + */ + add: function (items) { + var self = this; + + // Force single item into array + if (!Tools.isArray(items)) { + if (items instanceof Collection) { + self.add(items.toArray()); + } else { + push.call(self, items); + } + } else { + push.apply(self, items); + } + + return self; + }, + + /** + * Sets the contents of the collection. This will remove any existing items + * and replace them with the ones specified in the input array. + * + * @method set + * @param {Array} items Array with items to set into the Collection. + * @return {tinymce.ui.Collection} Collection instance. + */ + set: function (items) { + var self = this, len = self.length, i; + + self.length = 0; + self.add(items); + + // Remove old entries + for (i = self.length; i < len; i++) { + delete self[i]; + } + + return self; + }, + + /** + * Filters the collection item based on the specified selector expression or selector function. + * + * @method filter + * @param {String} selector Selector expression to filter items by. + * @return {tinymce.ui.Collection} Collection containing the filtered items. + */ + filter: function (selector) { + var self = this, i, l, matches = [], item, match; + + // Compile string into selector expression + if (typeof selector === "string") { + selector = new Selector(selector); + + match = function (item) { + return selector.match(item); + }; + } else { + // Use selector as matching function + match = selector; + } + + for (i = 0, l = self.length; i < l; i++) { + item = self[i]; + + if (match(item)) { + matches.push(item); + } + } + + return new Collection(matches); + }, + + /** + * Slices the items within the collection. + * + * @method slice + * @param {Number} index Index to slice at. + * @param {Number} len Optional length to slice. + * @return {tinymce.ui.Collection} Current collection. + */ + slice: function () { + return new Collection(slice.apply(this, arguments)); + }, + + /** + * Makes the current collection equal to the specified index. + * + * @method eq + * @param {Number} index Index of the item to set the collection to. + * @return {tinymce.ui.Collection} Current collection. + */ + eq: function (index) { + return index === -1 ? this.slice(index) : this.slice(index, +index + 1); + }, + + /** + * Executes the specified callback on each item in collection. + * + * @method each + * @param {function} callback Callback to execute for each item in collection. + * @return {tinymce.ui.Collection} Current collection instance. + */ + each: function (callback) { + Tools.each(this, callback); + + return this; + }, + + /** + * Returns an JavaScript array object of the contents inside the collection. + * + * @method toArray + * @return {Array} Array with all items from collection. + */ + toArray: function () { + return Tools.toArray(this); + }, + + /** + * Finds the index of the specified control or return -1 if it isn't in the collection. + * + * @method indexOf + * @param {Control} ctrl Control instance to look for. + * @return {Number} Index of the specified control or -1. + */ + indexOf: function (ctrl) { + var self = this, i = self.length; + + while (i--) { + if (self[i] === ctrl) { + break; + } + } + + return i; + }, + + /** + * Returns a new collection of the contents in reverse order. + * + * @method reverse + * @return {tinymce.ui.Collection} Collection instance with reversed items. + */ + reverse: function () { + return new Collection(Tools.toArray(this).reverse()); + }, + + /** + * Returns true/false if the class exists or not. + * + * @method hasClass + * @param {String} cls Class to check for. + * @return {Boolean} true/false state if the class exists or not. + */ + hasClass: function (cls) { + return this[0] ? this[0].classes.contains(cls) : false; + }, + + /** + * Sets/gets the specific property on the items in the collection. The same as executing control.(); + * + * @method prop + * @param {String} name Property name to get/set. + * @param {Object} value Optional object value to set. + * @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation. + */ + prop: function (name, value) { + var self = this, undef, item; + + if (value !== undef) { + self.each(function (item) { + if (item[name]) { + item[name](value); + } + }); + + return self; + } + + item = self[0]; + + if (item && item[name]) { + return item[name](); + } + }, + + /** + * Executes the specific function name with optional arguments an all items in collection if it exists. + * + * @example collection.exec("myMethod", arg1, arg2, arg3); + * @method exec + * @param {String} name Name of the function to execute. + * @param {Object} ... Multiple arguments to pass to each function. + * @return {tinymce.ui.Collection} Current collection. + */ + exec: function (name) { + var self = this, args = Tools.toArray(arguments).slice(1); + + self.each(function (item) { + if (item[name]) { + item[name].apply(item, args); + } + }); + + return self; + }, + + /** + * Remove all items from collection and DOM. + * + * @method remove + * @return {tinymce.ui.Collection} Current collection. + */ + remove: function () { + var i = this.length; + + while (i--) { + this[i].remove(); + } + + return this; + }, + + /** + * Adds a class to all items in the collection. + * + * @method addClass + * @param {String} cls Class to add to each item. + * @return {tinymce.ui.Collection} Current collection instance. + */ + addClass: function (cls) { + return this.each(function (item) { + item.classes.add(cls); + }); + }, + + /** + * Removes the specified class from all items in collection. + * + * @method removeClass + * @param {String} cls Class to remove from each item. + * @return {tinymce.ui.Collection} Current collection instance. + */ + removeClass: function (cls) { + return this.each(function (item) { + item.classes.remove(cls); + }); + } + + /** + * Fires the specified event by name and arguments on the control. This will execute all + * bound event handlers. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object} args Optional arguments to pass to the event. + * @return {tinymce.ui.Collection} Current collection instance. + */ + // fire: function(event, args) {}, -- Generated by code below + + /** + * Binds a callback to the specified event. This event can both be + * native browser events like "click" or custom ones like PostRender. + * + * The callback function will have two parameters the first one being the control that received the event + * the second one will be the event object either the browsers native event object or a custom JS object. + * + * @method on + * @param {String} name Name of the event to bind. For example "click". + * @param {String/function} callback Callback function to execute ones the event occurs. + * @return {tinymce.ui.Collection} Current collection instance. + */ + // on: function(name, callback) {}, -- Generated by code below + + /** + * Unbinds the specified event and optionally a specific callback. If you omit the name + * parameter all event handlers will be removed. If you omit the callback all event handles + * by the specified name will be removed. + * + * @method off + * @param {String} name Optional name for the event to unbind. + * @param {function} callback Optional callback function to unbind. + * @return {tinymce.ui.Collection} Current collection instance. + */ + // off: function(name, callback) {}, -- Generated by code below + + /** + * Shows the items in the current collection. + * + * @method show + * @return {tinymce.ui.Collection} Current collection instance. + */ + // show: function() {}, -- Generated by code below + + /** + * Hides the items in the current collection. + * + * @method hide + * @return {tinymce.ui.Collection} Current collection instance. + */ + // hide: function() {}, -- Generated by code below + + /** + * Sets/gets the text contents of the items in the current collection. + * + * @method text + * @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation. + */ + // text: function(value) {}, -- Generated by code below + + /** + * Sets/gets the name contents of the items in the current collection. + * + * @method name + * @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation. + */ + // name: function(value) {}, -- Generated by code below + + /** + * Sets/gets the disabled state on the items in the current collection. + * + * @method disabled + * @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation. + */ + // disabled: function(state) {}, -- Generated by code below + + /** + * Sets/gets the active state on the items in the current collection. + * + * @method active + * @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation. + */ + // active: function(state) {}, -- Generated by code below + + /** + * Sets/gets the selected state on the items in the current collection. + * + * @method selected + * @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation. + */ + // selected: function(state) {}, -- Generated by code below + + /** + * Sets/gets the selected state on the items in the current collection. + * + * @method visible + * @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation. + */ + // visible: function(state) {}, -- Generated by code below + }; + + // Extend tinymce.ui.Collection prototype with some generated control specific methods + Tools.each('fire on off show hide append prepend before after reflow'.split(' '), function (name) { + proto[name] = function () { + var args = Tools.toArray(arguments); + + this.each(function (ctrl) { + if (name in ctrl) { + ctrl[name].apply(ctrl, args); + } + }); + + return this; + }; + }); + + // Extend tinymce.ui.Collection prototype with some property methods + Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function (name) { + proto[name] = function (value) { + return this.prop(name, value); + }; + }); + + // Create class based on the new prototype + Collection = Class.extend(proto); + + // Stick Collection into Selector to prevent circual references + Selector.Collection = Collection; + + return Collection; + } +); +/** + * 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.ui.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; + } +); +/** + * 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.util.Observable', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.util.Observable'); + } +); + +/** + * ObservableObject.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 a object that is observable when properties changes a change event gets emitted. + * + * @private + * @class tinymce.data.ObservableObject + */ +define( + 'tinymce.ui.data.ObservableObject', + [ + 'tinymce.ui.data.Binding', + 'tinymce.core.util.Class', + 'tinymce.core.util.Observable', + 'tinymce.core.util.Tools' + ], function (Binding, Class, Observable, Tools) { + function isNode(node) { + return node.nodeType > 0; + } + + // Todo: Maybe this should be shallow compare since it might be huge object references + function isEqual(a, b) { + var k, checked; + + // Strict equals + if (a === b) { + return true; + } + + // Compare null + if (a === null || b === null) { + return a === b; + } + + // Compare number, boolean, string, undefined + if (typeof a !== "object" || typeof b !== "object") { + return a === b; + } + + // Compare arrays + if (Tools.isArray(b)) { + if (a.length !== b.length) { + return false; + } + + k = a.length; + while (k--) { + if (!isEqual(a[k], b[k])) { + return false; + } + } + } + + // Shallow compare nodes + if (isNode(a) || isNode(b)) { + return a === b; + } + + // Compare objects + checked = {}; + for (k in b) { + if (!isEqual(a[k], b[k])) { + return false; + } + + checked[k] = true; + } + + for (k in a) { + if (!checked[k] && !isEqual(a[k], b[k])) { + return false; + } + } + + return true; + } + + return Class.extend({ + Mixins: [Observable], + + /** + * Constructs a new observable object instance. + * + * @constructor + * @param {Object} data Initial data for the object. + */ + init: function (data) { + var name, value; + + data = data || {}; + + for (name in data) { + value = data[name]; + + if (value instanceof Binding) { + data[name] = value.create(this, name); + } + } + + this.data = data; + }, + + /** + * Sets a property on the value this will call + * observers if the value is a change from the current value. + * + * @method set + * @param {String/object} name Name of the property to set or a object of items to set. + * @param {Object} value Value to set for the property. + * @return {tinymce.data.ObservableObject} Observable object instance. + */ + set: function (name, value) { + var key, args, oldValue = this.data[name]; + + if (value instanceof Binding) { + value = value.create(this, name); + } + + if (typeof name === "object") { + for (key in name) { + this.set(key, name[key]); + } + + return this; + } + + if (!isEqual(oldValue, value)) { + this.data[name] = value; + + args = { + target: this, + name: name, + value: value, + oldValue: oldValue + }; + + this.fire('change:' + name, args); + this.fire('change', args); + } + + return this; + }, + + /** + * Gets a property by name. + * + * @method get + * @param {String} name Name of the property to get. + * @return {Object} Object value of propery. + */ + get: function (name) { + return this.data[name]; + }, + + /** + * Returns true/false if the specified property exists. + * + * @method has + * @param {String} name Name of the property to check for. + * @return {Boolean} true/false if the item exists. + */ + has: function (name) { + return name in this.data; + }, + + /** + * Returns a dynamic property binding for the specified property name. This makes + * it possible to sync the state of two properties in two ObservableObject instances. + * + * @method bind + * @param {String} name Name of the property to sync with the property it's inserted to. + * @return {tinymce.data.Binding} Data binding instance. + */ + bind: function (name) { + return Binding.create(this, name); + }, + + /** + * Destroys the observable object and fires the "destroy" + * event and clean up any internal resources. + * + * @method destroy + */ + destroy: function () { + this.fire('destroy'); + } + }); + } +); +/** + * ReflowQueue.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 automatically reflow controls on the next animation frame within a few milliseconds on older browsers. + * If the user manually reflows then the automatic reflow will be cancelled. This class is used internally when various control states + * changes that triggers a reflow. + * + * @class tinymce.ui.ReflowQueue + * @static + */ +define( + 'tinymce.ui.ReflowQueue', + [ + 'global!document', + 'tinymce.core.util.Delay' + ], + function (document, Delay) { + var dirtyCtrls = {}, animationFrameRequested; + + return { + /** + * Adds a control to the next automatic reflow call. This is the control that had a state + * change for example if the control was hidden/shown. + * + * @method add + * @param {tinymce.ui.Control} ctrl Control to add to queue. + */ + add: function (ctrl) { + var parent = ctrl.parent(); + + if (parent) { + if (!parent._layout || parent._layout.isNative()) { + return; + } + + if (!dirtyCtrls[parent._id]) { + dirtyCtrls[parent._id] = parent; + } + + if (!animationFrameRequested) { + animationFrameRequested = true; + + Delay.requestAnimationFrame(function () { + var id, ctrl; + + animationFrameRequested = false; + + for (id in dirtyCtrls) { + ctrl = dirtyCtrls[id]; + + if (ctrl.state.get('rendered')) { + ctrl.reflow(); + } + } + + dirtyCtrls = {}; + }, document.body); + } + } + }, + + /** + * Removes the specified control from the automatic reflow. This will happen when for example the user + * manually triggers a reflow. + * + * @method remove + * @param {tinymce.ui.Control} ctrl Control to remove from queue. + */ + remove: function (ctrl) { + if (dirtyCtrls[ctrl._id]) { + delete dirtyCtrls[ctrl._id]; + } + } + }; + } +); + +/** + * Control.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 consistent-this:0 */ + +/** + * This is the base class for all controls and containers. All UI control instances inherit + * from this one as it has the base logic needed by all of them. + * + * @class tinymce.ui.Control + */ +define( + 'tinymce.ui.Control', + [ + 'global!document', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.util.Class', + 'tinymce.core.util.EventDispatcher', + 'tinymce.core.util.Tools', + 'tinymce.ui.BoxUtils', + 'tinymce.ui.ClassList', + 'tinymce.ui.Collection', + 'tinymce.ui.data.ObservableObject', + 'tinymce.ui.DomUtils', + 'tinymce.ui.ReflowQueue' + ], + function (document, DomQuery, Class, EventDispatcher, Tools, BoxUtils, ClassList, Collection, ObservableObject, DomUtils, ReflowQueue) { + "use strict"; + + var hasMouseWheelEventSupport = "onmousewheel" in document; + var hasWheelEventSupport = false; + var classPrefix = "mce-"; + var Control, idCounter = 0; + + var proto = { + Statics: { + classPrefix: classPrefix + }, + + isRtl: function () { + return Control.rtl; + }, + + /** + * Class/id prefix to use for all controls. + * + * @final + * @field {String} classPrefix + */ + classPrefix: classPrefix, + + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} style Style CSS properties to add. + * @setting {String} border Border box values example: 1 1 1 1 + * @setting {String} padding Padding box values example: 1 1 1 1 + * @setting {String} margin Margin box values example: 1 1 1 1 + * @setting {Number} minWidth Minimal width for the control. + * @setting {Number} minHeight Minimal height for the control. + * @setting {String} classes Space separated list of classes to add. + * @setting {String} role WAI-ARIA role to use for control. + * @setting {Boolean} hidden Is the control hidden by default. + * @setting {Boolean} disabled Is the control disabled by default. + * @setting {String} name Name of the control instance. + */ + init: function (settings) { + var self = this, classes, defaultClasses; + + function applyClasses(classes) { + var i; + + classes = classes.split(' '); + for (i = 0; i < classes.length; i++) { + self.classes.add(classes[i]); + } + } + + self.settings = settings = Tools.extend({}, self.Defaults, settings); + + // Initial states + self._id = settings.id || ('mceu_' + (idCounter++)); + self._aria = { role: settings.role }; + self._elmCache = {}; + self.$ = DomQuery; + + self.state = new ObservableObject({ + visible: true, + active: false, + disabled: false, + value: '' + }); + + self.data = new ObservableObject(settings.data); + + self.classes = new ClassList(function () { + if (self.state.get('rendered')) { + self.getEl().className = this.toString(); + } + }); + self.classes.prefix = self.classPrefix; + + // Setup classes + classes = settings.classes; + if (classes) { + if (self.Defaults) { + defaultClasses = self.Defaults.classes; + + if (defaultClasses && classes != defaultClasses) { + applyClasses(defaultClasses); + } + } + + applyClasses(classes); + } + + Tools.each('title text name visible disabled active value'.split(' '), function (name) { + if (name in settings) { + self[name](settings[name]); + } + }); + + self.on('click', function () { + if (self.disabled()) { + return false; + } + }); + + /** + * Name/value object with settings for the current control. + * + * @field {Object} settings + */ + self.settings = settings; + + self.borderBox = BoxUtils.parseBox(settings.border); + self.paddingBox = BoxUtils.parseBox(settings.padding); + self.marginBox = BoxUtils.parseBox(settings.margin); + + if (settings.hidden) { + self.hide(); + } + }, + + // Will generate getter/setter methods for these properties + Properties: 'parent,name', + + /** + * Returns the root element to render controls into. + * + * @method getContainerElm + * @return {Element} HTML DOM element to render into. + */ + getContainerElm: function () { + return DomUtils.getContainer(); + }, + + /** + * Returns a control instance for the current DOM element. + * + * @method getParentCtrl + * @param {Element} elm HTML dom element to get parent control from. + * @return {tinymce.ui.Control} Control instance or undefined. + */ + getParentCtrl: function (elm) { + var ctrl, lookup = this.getRoot().controlIdLookup; + + while (elm && lookup) { + ctrl = lookup[elm.id]; + if (ctrl) { + break; + } + + elm = elm.parentNode; + } + + return ctrl; + }, + + /** + * Initializes the current controls layout rect. + * This will be executed by the layout managers to determine the + * default minWidth/minHeight etc. + * + * @method initLayoutRect + * @return {Object} Layout rect instance. + */ + initLayoutRect: function () { + var self = this, settings = self.settings, borderBox, layoutRect; + var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; + var startMinWidth, startMinHeight, initialSize; + + // Measure the current element + borderBox = self.borderBox = self.borderBox || BoxUtils.measureBox(elm, 'border'); + self.paddingBox = self.paddingBox || BoxUtils.measureBox(elm, 'padding'); + self.marginBox = self.marginBox || BoxUtils.measureBox(elm, 'margin'); + initialSize = DomUtils.getSize(elm); + + // Setup minWidth/minHeight and width/height + startMinWidth = settings.minWidth; + startMinHeight = settings.minHeight; + minWidth = startMinWidth || initialSize.width; + minHeight = startMinHeight || initialSize.height; + width = settings.width; + height = settings.height; + autoResize = settings.autoResize; + autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height; + + width = width || minWidth; + height = height || minHeight; + + var deltaW = borderBox.left + borderBox.right; + var deltaH = borderBox.top + borderBox.bottom; + + var maxW = settings.maxWidth || 0xFFFF; + var maxH = settings.maxHeight || 0xFFFF; + + // Setup initial layout rect + self._layoutRect = layoutRect = { + x: settings.x || 0, + y: settings.y || 0, + w: width, + h: height, + deltaW: deltaW, + deltaH: deltaH, + contentW: width - deltaW, + contentH: height - deltaH, + innerW: width - deltaW, + innerH: height - deltaH, + startMinWidth: startMinWidth || 0, + startMinHeight: startMinHeight || 0, + minW: Math.min(minWidth, maxW), + minH: Math.min(minHeight, maxH), + maxW: maxW, + maxH: maxH, + autoResize: autoResize, + scrollW: 0 + }; + + self._lastLayoutRect = {}; + + return layoutRect; + }, + + /** + * Getter/setter for the current layout rect. + * + * @method layoutRect + * @param {Object} [newRect] Optional new layout rect. + * @return {tinymce.ui.Control/Object} Current control or rect object. + */ + layoutRect: function (newRect) { + var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; + + // Initialize default layout rect + if (!curRect) { + curRect = self.initLayoutRect(); + } + + // Set new rect values + if (newRect) { + // Calc deltas between inner and outer sizes + deltaWidth = curRect.deltaW; + deltaHeight = curRect.deltaH; + + // Set x position + if (newRect.x !== undef) { + curRect.x = newRect.x; + } + + // Set y position + if (newRect.y !== undef) { + curRect.y = newRect.y; + } + + // Set minW + if (newRect.minW !== undef) { + curRect.minW = newRect.minW; + } + + // Set minH + if (newRect.minH !== undef) { + curRect.minH = newRect.minH; + } + + // Set new width and calculate inner width + size = newRect.w; + if (size !== undef) { + size = size < curRect.minW ? curRect.minW : size; + size = size > curRect.maxW ? curRect.maxW : size; + curRect.w = size; + curRect.innerW = size - deltaWidth; + } + + // Set new height and calculate inner height + size = newRect.h; + if (size !== undef) { + size = size < curRect.minH ? curRect.minH : size; + size = size > curRect.maxH ? curRect.maxH : size; + curRect.h = size; + curRect.innerH = size - deltaHeight; + } + + // Set new inner width and calculate width + size = newRect.innerW; + if (size !== undef) { + size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; + size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; + curRect.innerW = size; + curRect.w = size + deltaWidth; + } + + // Set new height and calculate inner height + size = newRect.innerH; + if (size !== undef) { + size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; + size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; + curRect.innerH = size; + curRect.h = size + deltaHeight; + } + + // Set new contentW + if (newRect.contentW !== undef) { + curRect.contentW = newRect.contentW; + } + + // Set new contentH + if (newRect.contentH !== undef) { + curRect.contentH = newRect.contentH; + } + + // Compare last layout rect with the current one to see if we need to repaint or not + lastLayoutRect = self._lastLayoutRect; + if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || + lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { + repaintControls = Control.repaintControls; + + if (repaintControls) { + if (repaintControls.map && !repaintControls.map[self._id]) { + repaintControls.push(self); + repaintControls.map[self._id] = true; + } + } + + lastLayoutRect.x = curRect.x; + lastLayoutRect.y = curRect.y; + lastLayoutRect.w = curRect.w; + lastLayoutRect.h = curRect.h; + } + + return self; + } + + return curRect; + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this, style, bodyStyle, bodyElm, rect, borderBox; + var borderW, borderH, lastRepaintRect, round, value; + + // Use Math.round on all values on IE < 9 + round = !document.createRange ? Math.round : function (value) { + return value; + }; + + style = self.getEl().style; + rect = self._layoutRect; + lastRepaintRect = self._lastRepaintRect || {}; + + borderBox = self.borderBox; + borderW = borderBox.left + borderBox.right; + borderH = borderBox.top + borderBox.bottom; + + if (rect.x !== lastRepaintRect.x) { + style.left = round(rect.x) + 'px'; + lastRepaintRect.x = rect.x; + } + + if (rect.y !== lastRepaintRect.y) { + style.top = round(rect.y) + 'px'; + lastRepaintRect.y = rect.y; + } + + if (rect.w !== lastRepaintRect.w) { + value = round(rect.w - borderW); + style.width = (value >= 0 ? value : 0) + 'px'; + lastRepaintRect.w = rect.w; + } + + if (rect.h !== lastRepaintRect.h) { + value = round(rect.h - borderH); + style.height = (value >= 0 ? value : 0) + 'px'; + lastRepaintRect.h = rect.h; + } + + // Update body if needed + if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { + value = round(rect.innerW); + + bodyElm = self.getEl('body'); + if (bodyElm) { + bodyStyle = bodyElm.style; + bodyStyle.width = (value >= 0 ? value : 0) + 'px'; + } + + lastRepaintRect.innerW = rect.innerW; + } + + if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { + value = round(rect.innerH); + + bodyElm = bodyElm || self.getEl('body'); + if (bodyElm) { + bodyStyle = bodyStyle || bodyElm.style; + bodyStyle.height = (value >= 0 ? value : 0) + 'px'; + } + + lastRepaintRect.innerH = rect.innerH; + } + + self._lastRepaintRect = lastRepaintRect; + self.fire('repaint', {}, false); + }, + + /** + * Updates the controls layout rect by re-measuing it. + */ + updateLayoutRect: function () { + var self = this; + + self.parent()._lastRect = null; + + DomUtils.css(self.getEl(), { width: '', height: '' }); + + self._layoutRect = self._lastRepaintRect = self._lastLayoutRect = null; + self.initLayoutRect(); + }, + + /** + * Binds a callback to the specified event. This event can both be + * native browser events like "click" or custom ones like PostRender. + * + * The callback function will be passed a DOM event like object that enables yout do stop propagation. + * + * @method on + * @param {String} name Name of the event to bind. For example "click". + * @param {String/function} callback Callback function to execute ones the event occurs. + * @return {tinymce.ui.Control} Current control object. + */ + on: function (name, callback) { + var self = this; + + function resolveCallbackName(name) { + var callback, scope; + + if (typeof name != 'string') { + return name; + } + + return function (e) { + if (!callback) { + self.parentsAndSelf().each(function (ctrl) { + var callbacks = ctrl.settings.callbacks; + + if (callbacks && (callback = callbacks[name])) { + scope = ctrl; + return false; + } + }); + } + + if (!callback) { + e.action = name; + this.fire('execute', e); + return; + } + + return callback.call(scope, e); + }; + } + + getEventDispatcher(self).on(name, resolveCallbackName(callback)); + + return self; + }, + + /** + * Unbinds the specified event and optionally a specific callback. If you omit the name + * parameter all event handlers will be removed. If you omit the callback all event handles + * by the specified name will be removed. + * + * @method off + * @param {String} [name] Name for the event to unbind. + * @param {function} [callback] Callback function to unbind. + * @return {tinymce.ui.Control} Current control object. + */ + off: function (name, callback) { + getEventDispatcher(this).off(name, callback); + return this; + }, + + /** + * Fires the specified event by name and arguments on the control. This will execute all + * bound event handlers. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object} [args] Arguments to pass to the event. + * @param {Boolean} [bubble] Value to control bubbling. Defaults to true. + * @return {Object} Current arguments object. + */ + fire: function (name, args, bubble) { + var self = this; + + args = args || {}; + + if (!args.control) { + args.control = self; + } + + args = getEventDispatcher(self).fire(name, args); + + // Bubble event up to parents + if (bubble !== false && self.parent) { + var parent = self.parent(); + while (parent && !args.isPropagationStopped()) { + parent.fire(name, args, false); + parent = parent.parent(); + } + } + + return args; + }, + + /** + * Returns true/false if the specified event has any listeners. + * + * @method hasEventListeners + * @param {String} name Name of the event to check for. + * @return {Boolean} True/false state if the event has listeners. + */ + hasEventListeners: function (name) { + return getEventDispatcher(this).has(name); + }, + + /** + * Returns a control collection with all parent controls. + * + * @method parents + * @param {String} selector Optional selector expression to find parents. + * @return {tinymce.ui.Collection} Collection with all parent controls. + */ + parents: function (selector) { + var self = this, ctrl, parents = new Collection(); + + // Add each parent to collection + for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { + parents.add(ctrl); + } + + // Filter away everything that doesn't match the selector + if (selector) { + parents = parents.filter(selector); + } + + return parents; + }, + + /** + * Returns the current control and it's parents. + * + * @method parentsAndSelf + * @param {String} selector Optional selector expression to find parents. + * @return {tinymce.ui.Collection} Collection with all parent controls. + */ + parentsAndSelf: function (selector) { + return new Collection(this).add(this.parents(selector)); + }, + + /** + * Returns the control next to the current control. + * + * @method next + * @return {tinymce.ui.Control} Next control instance. + */ + next: function () { + var parentControls = this.parent().items(); + + return parentControls[parentControls.indexOf(this) + 1]; + }, + + /** + * Returns the control previous to the current control. + * + * @method prev + * @return {tinymce.ui.Control} Previous control instance. + */ + prev: function () { + var parentControls = this.parent().items(); + + return parentControls[parentControls.indexOf(this) - 1]; + }, + + /** + * Sets the inner HTML of the control element. + * + * @method innerHtml + * @param {String} html Html string to set as inner html. + * @return {tinymce.ui.Control} Current control object. + */ + innerHtml: function (html) { + this.$el.html(html); + return this; + }, + + /** + * Returns the control DOM element or sub element. + * + * @method getEl + * @param {String} [suffix] Suffix to get element by. + * @return {Element} HTML DOM element for the current control or it's children. + */ + getEl: function (suffix) { + var id = suffix ? this._id + '-' + suffix : this._id; + + if (!this._elmCache[id]) { + this._elmCache[id] = DomQuery('#' + id)[0]; + } + + return this._elmCache[id]; + }, + + /** + * Sets the visible state to true. + * + * @method show + * @return {tinymce.ui.Control} Current control instance. + */ + show: function () { + return this.visible(true); + }, + + /** + * Sets the visible state to false. + * + * @method hide + * @return {tinymce.ui.Control} Current control instance. + */ + hide: function () { + return this.visible(false); + }, + + /** + * Focuses the current control. + * + * @method focus + * @return {tinymce.ui.Control} Current control instance. + */ + focus: function () { + try { + this.getEl().focus(); + } catch (ex) { + // Ignore IE error + } + + return this; + }, + + /** + * Blurs the current control. + * + * @method blur + * @return {tinymce.ui.Control} Current control instance. + */ + blur: function () { + this.getEl().blur(); + + return this; + }, + + /** + * Sets the specified aria property. + * + * @method aria + * @param {String} name Name of the aria property to set. + * @param {String} value Value of the aria property. + * @return {tinymce.ui.Control} Current control instance. + */ + aria: function (name, value) { + var self = this, elm = self.getEl(self.ariaTarget); + + if (typeof value === "undefined") { + return self._aria[name]; + } + + self._aria[name] = value; + + if (self.state.get('rendered')) { + elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); + } + + return self; + }, + + /** + * Encodes the specified string with HTML entities. It will also + * translate the string to different languages. + * + * @method encode + * @param {String/Object/Array} text Text to entity encode. + * @param {Boolean} [translate=true] False if the contents shouldn't be translated. + * @return {String} Encoded and possible traslated string. + */ + encode: function (text, translate) { + if (translate !== false) { + text = this.translate(text); + } + + return (text || '').replace(/[&<>"]/g, function (match) { + return '&#' + match.charCodeAt(0) + ';'; + }); + }, + + /** + * Returns the translated string. + * + * @method translate + * @param {String} text Text to translate. + * @return {String} Translated string or the same as the input. + */ + translate: function (text) { + return Control.translate ? Control.translate(text) : text; + }, + + /** + * Adds items before the current control. + * + * @method before + * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. + * @return {tinymce.ui.Control} Current control instance. + */ + before: function (items) { + var self = this, parent = self.parent(); + + if (parent) { + parent.insert(items, parent.items().indexOf(self), true); + } + + return self; + }, + + /** + * Adds items after the current control. + * + * @method after + * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. + * @return {tinymce.ui.Control} Current control instance. + */ + after: function (items) { + var self = this, parent = self.parent(); + + if (parent) { + parent.insert(items, parent.items().indexOf(self)); + } + + return self; + }, + + /** + * Removes the current control from DOM and from UI collections. + * + * @method remove + * @return {tinymce.ui.Control} Current control instance. + */ + remove: function () { + var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; + + if (self.items) { + var controls = self.items().toArray(); + i = controls.length; + while (i--) { + controls[i].remove(); + } + } + + if (parent && parent.items) { + newItems = []; + + parent.items().each(function (item) { + if (item !== self) { + newItems.push(item); + } + }); + + parent.items().set(newItems); + parent._lastRect = null; + } + + if (self._eventsRoot && self._eventsRoot == self) { + DomQuery(elm).off(); + } + + var lookup = self.getRoot().controlIdLookup; + if (lookup) { + delete lookup[self._id]; + } + + if (elm && elm.parentNode) { + elm.parentNode.removeChild(elm); + } + + self.state.set('rendered', false); + self.state.destroy(); + + self.fire('remove'); + + return self; + }, + + /** + * Renders the control before the specified element. + * + * @method renderBefore + * @param {Element} elm Element to render before. + * @return {tinymce.ui.Control} Current control instance. + */ + renderBefore: function (elm) { + DomQuery(elm).before(this.renderHtml()); + this.postRender(); + return this; + }, + + /** + * Renders the control to the specified element. + * + * @method renderBefore + * @param {Element} elm Element to render to. + * @return {tinymce.ui.Control} Current control instance. + */ + renderTo: function (elm) { + DomQuery(elm || this.getContainerElm()).append(this.renderHtml()); + this.postRender(); + return this; + }, + + preRender: function () { + }, + + render: function () { + }, + + renderHtml: function () { + return '

    '; + }, + + /** + * Post render method. Called after the control has been rendered to the target. + * + * @method postRender + * @return {tinymce.ui.Control} Current control instance. + */ + postRender: function () { + var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; + + self.$el = DomQuery(self.getEl()); + self.state.set('rendered', true); + + // Bind on settings + for (name in settings) { + if (name.indexOf("on") === 0) { + self.on(name.substr(2), settings[name]); + } + } + + if (self._eventsRoot) { + for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) { + parentEventsRoot = parent._eventsRoot; + } + + if (parentEventsRoot) { + for (name in parentEventsRoot._nativeEvents) { + self._nativeEvents[name] = true; + } + } + } + + bindPendingEvents(self); + + if (settings.style) { + elm = self.getEl(); + if (elm) { + elm.setAttribute('style', settings.style); + elm.style.cssText = settings.style; + } + } + + if (self.settings.border) { + box = self.borderBox; + self.$el.css({ + 'border-top-width': box.top, + 'border-right-width': box.right, + 'border-bottom-width': box.bottom, + 'border-left-width': box.left + }); + } + + // Add instance to lookup + var root = self.getRoot(); + if (!root.controlIdLookup) { + root.controlIdLookup = {}; + } + + root.controlIdLookup[self._id] = self; + + for (var key in self._aria) { + self.aria(key, self._aria[key]); + } + + if (self.state.get('visible') === false) { + self.getEl().style.display = 'none'; + } + + self.bindStates(); + + self.state.on('change:visible', function (e) { + var state = e.value, parentCtrl; + + if (self.state.get('rendered')) { + self.getEl().style.display = state === false ? 'none' : ''; + + // Need to force a reflow here on IE 8 + self.getEl().getBoundingClientRect(); + } + + // Parent container needs to reflow + parentCtrl = self.parent(); + if (parentCtrl) { + parentCtrl._lastRect = null; + } + + self.fire(state ? 'show' : 'hide'); + + ReflowQueue.add(self); + }); + + self.fire('postrender', {}, false); + }, + + bindStates: function () { + }, + + /** + * Scrolls the current control into view. + * + * @method scrollIntoView + * @param {String} align Alignment in view top|center|bottom. + * @return {tinymce.ui.Control} Current control instance. + */ + scrollIntoView: function (align) { + function getOffset(elm, rootElm) { + var x, y, parent = elm; + + x = y = 0; + while (parent && parent != rootElm && parent.nodeType) { + x += parent.offsetLeft || 0; + y += parent.offsetTop || 0; + parent = parent.offsetParent; + } + + return { x: x, y: y }; + } + + var elm = this.getEl(), parentElm = elm.parentNode; + var x, y, width, height, parentWidth, parentHeight; + var pos = getOffset(elm, parentElm); + + x = pos.x; + y = pos.y; + width = elm.offsetWidth; + height = elm.offsetHeight; + parentWidth = parentElm.clientWidth; + parentHeight = parentElm.clientHeight; + + if (align == "end") { + x -= parentWidth - width; + y -= parentHeight - height; + } else if (align == "center") { + x -= (parentWidth / 2) - (width / 2); + y -= (parentHeight / 2) - (height / 2); + } + + parentElm.scrollLeft = x; + parentElm.scrollTop = y; + + return this; + }, + + getRoot: function () { + var ctrl = this, rootControl, parents = []; + + while (ctrl) { + if (ctrl.rootControl) { + rootControl = ctrl.rootControl; + break; + } + + parents.push(ctrl); + rootControl = ctrl; + ctrl = ctrl.parent(); + } + + if (!rootControl) { + rootControl = this; + } + + var i = parents.length; + while (i--) { + parents[i].rootControl = rootControl; + } + + return rootControl; + }, + + /** + * Reflows the current control and it's parents. + * This should be used after you for example append children to the current control so + * that the layout managers know that they need to reposition everything. + * + * @example + * container.append({type: 'button', text: 'My button'}).reflow(); + * + * @method reflow + * @return {tinymce.ui.Control} Current control instance. + */ + reflow: function () { + ReflowQueue.remove(this); + + var parent = this.parent(); + if (parent && parent._layout && !parent._layout.isNative()) { + parent.reflow(); + } + + return this; + } + + /** + * Sets/gets the parent container for the control. + * + * @method parent + * @param {tinymce.ui.Container} parent Optional parent to set. + * @return {tinymce.ui.Control} Parent control or the current control on a set action. + */ + // parent: function(parent) {} -- Generated + + /** + * Sets/gets the text for the control. + * + * @method text + * @param {String} value Value to set to control. + * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. + */ + // text: function(value) {} -- Generated + + /** + * Sets/gets the disabled state on the control. + * + * @method disabled + * @param {Boolean} state Value to set to control. + * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. + */ + // disabled: function(state) {} -- Generated + + /** + * Sets/gets the active for the control. + * + * @method active + * @param {Boolean} state Value to set to control. + * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. + */ + // active: function(state) {} -- Generated + + /** + * Sets/gets the name for the control. + * + * @method name + * @param {String} value Value to set to control. + * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. + */ + // name: function(value) {} -- Generated + + /** + * Sets/gets the title for the control. + * + * @method title + * @param {String} value Value to set to control. + * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. + */ + // title: function(value) {} -- Generated + + /** + * Sets/gets the visible for the control. + * + * @method visible + * @param {Boolean} state Value to set to control. + * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. + */ + // visible: function(value) {} -- Generated + }; + + /** + * Setup state properties. + */ + Tools.each('text title visible disabled active value'.split(' '), function (name) { + proto[name] = function (value) { + if (arguments.length === 0) { + return this.state.get(name); + } + + if (typeof value != "undefined") { + this.state.set(name, value); + } + + return this; + }; + }); + + Control = Class.extend(proto); + + function getEventDispatcher(obj) { + if (!obj._eventDispatcher) { + obj._eventDispatcher = new EventDispatcher({ + scope: obj, + toggleEvent: function (name, state) { + if (state && EventDispatcher.isNative(name)) { + if (!obj._nativeEvents) { + obj._nativeEvents = {}; + } + + obj._nativeEvents[name] = true; + + if (obj.state.get('rendered')) { + bindPendingEvents(obj); + } + } + } + }); + } + + return obj._eventDispatcher; + } + + function bindPendingEvents(eventCtrl) { + var i, l, parents, eventRootCtrl, nativeEvents, name; + + function delegate(e) { + var control = eventCtrl.getParentCtrl(e.target); + + if (control) { + control.fire(e.type, e); + } + } + + function mouseLeaveHandler() { + var ctrl = eventRootCtrl._lastHoverCtrl; + + if (ctrl) { + ctrl.fire("mouseleave", { target: ctrl.getEl() }); + + ctrl.parents().each(function (ctrl) { + ctrl.fire("mouseleave", { target: ctrl.getEl() }); + }); + + eventRootCtrl._lastHoverCtrl = null; + } + } + + function mouseEnterHandler(e) { + var ctrl = eventCtrl.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents; + + // Over on a new control + if (ctrl !== lastCtrl) { + eventRootCtrl._lastHoverCtrl = ctrl; + + parents = ctrl.parents().toArray().reverse(); + parents.push(ctrl); + + if (lastCtrl) { + lastParents = lastCtrl.parents().toArray().reverse(); + lastParents.push(lastCtrl); + + for (idx = 0; idx < lastParents.length; idx++) { + if (parents[idx] !== lastParents[idx]) { + break; + } + } + + for (i = lastParents.length - 1; i >= idx; i--) { + lastCtrl = lastParents[i]; + lastCtrl.fire("mouseleave", { + target: lastCtrl.getEl() + }); + } + } + + for (i = idx; i < parents.length; i++) { + ctrl = parents[i]; + ctrl.fire("mouseenter", { + target: ctrl.getEl() + }); + } + } + } + + function fixWheelEvent(e) { + e.preventDefault(); + + if (e.type == "mousewheel") { + e.deltaY = -1 / 40 * e.wheelDelta; + + if (e.wheelDeltaX) { + e.deltaX = -1 / 40 * e.wheelDeltaX; + } + } else { + e.deltaX = 0; + e.deltaY = e.detail; + } + + e = eventCtrl.fire("wheel", e); + } + + nativeEvents = eventCtrl._nativeEvents; + if (nativeEvents) { + // Find event root element if it exists + parents = eventCtrl.parents().toArray(); + parents.unshift(eventCtrl); + for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) { + eventRootCtrl = parents[i]._eventsRoot; + } + + // Event root wasn't found the use the root control + if (!eventRootCtrl) { + eventRootCtrl = parents[parents.length - 1] || eventCtrl; + } + + // Set the eventsRoot property on children that didn't have it + eventCtrl._eventsRoot = eventRootCtrl; + for (l = i, i = 0; i < l; i++) { + parents[i]._eventsRoot = eventRootCtrl; + } + + var eventRootDelegates = eventRootCtrl._delegates; + if (!eventRootDelegates) { + eventRootDelegates = eventRootCtrl._delegates = {}; + } + + // Bind native event delegates + for (name in nativeEvents) { + if (!nativeEvents) { + return false; + } + + if (name === "wheel" && !hasWheelEventSupport) { + if (hasMouseWheelEventSupport) { + DomQuery(eventCtrl.getEl()).on("mousewheel", fixWheelEvent); + } else { + DomQuery(eventCtrl.getEl()).on("DOMMouseScroll", fixWheelEvent); + } + + continue; + } + + // Special treatment for mousenter/mouseleave since these doesn't bubble + if (name === "mouseenter" || name === "mouseleave") { + // Fake mousenter/mouseleave + if (!eventRootCtrl._hasMouseEnter) { + DomQuery(eventRootCtrl.getEl()).on("mouseleave", mouseLeaveHandler).on("mouseover", mouseEnterHandler); + eventRootCtrl._hasMouseEnter = 1; + } + } else if (!eventRootDelegates[name]) { + DomQuery(eventRootCtrl.getEl()).on(name, delegate); + eventRootDelegates[name] = true; + } + + // Remove the event once it's bound + nativeEvents[name] = false; + } + } + } + + return Control; + } +); + +/** + * KeyboardNavigation.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 keyboard navigation of controls and elements. + * + * @class tinymce.ui.KeyboardNavigation + */ +define( + 'tinymce.ui.KeyboardNavigation', + [ + 'global!document' + ], + function (document) { + "use strict"; + + var hasTabstopData = function (elm) { + return elm.getAttribute('data-mce-tabstop') ? true : false; + }; + + /** + * This class handles all keyboard navigation for WAI-ARIA support. Each root container + * gets an instance of this class. + * + * @constructor + */ + return function (settings) { + var root = settings.root, focusedElement, focusedControl; + + function isElement(node) { + return node && node.nodeType === 1; + } + + try { + focusedElement = document.activeElement; + } catch (ex) { + // IE sometimes fails to return a proper element + focusedElement = document.body; + } + + focusedControl = root.getParentCtrl(focusedElement); + + /** + * Returns the currently focused elements wai aria role of the currently + * focused element or specified element. + * + * @private + * @param {Element} elm Optional element to get role from. + * @return {String} Role of specified element. + */ + function getRole(elm) { + elm = elm || focusedElement; + + if (isElement(elm)) { + return elm.getAttribute('role'); + } + + return null; + } + + /** + * Returns the wai role of the parent element of the currently + * focused element or specified element. + * + * @private + * @param {Element} elm Optional element to get parent role from. + * @return {String} Role of the first parent that has a role. + */ + function getParentRole(elm) { + var role, parent = elm || focusedElement; + + while ((parent = parent.parentNode)) { + if ((role = getRole(parent))) { + return role; + } + } + } + + /** + * Returns a wai aria property by name for example aria-selected. + * + * @private + * @param {String} name Name of the aria property to get for example "disabled". + * @return {String} Aria property value. + */ + function getAriaProp(name) { + var elm = focusedElement; + + if (isElement(elm)) { + return elm.getAttribute('aria-' + name); + } + } + + /** + * Is the element a text input element or not. + * + * @private + * @param {Element} elm Element to check if it's an text input element or not. + * @return {Boolean} True/false if the element is a text element or not. + */ + function isTextInputElement(elm) { + var tagName = elm.tagName.toUpperCase(); + + // Notice: since type can be "email" etc we don't check the type + // So all input elements gets treated as text input elements + return tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT"; + } + + /** + * Returns true/false if the specified element can be focused or not. + * + * @private + * @param {Element} elm DOM element to check if it can be focused or not. + * @return {Boolean} True/false if the element can have focus. + */ + function canFocus(elm) { + if (isTextInputElement(elm) && !elm.hidden) { + return true; + } + + if (hasTabstopData(elm)) { + return true; + } + + if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(getRole(elm))) { + return true; + } + + return false; + } + + /** + * Returns an array of focusable visible elements within the specified container element. + * + * @private + * @param {Element} elm DOM element to find focusable elements within. + * @return {Array} Array of focusable elements. + */ + function getFocusElements(elm) { + var elements = []; + + function collect(elm) { + if (elm.nodeType != 1 || elm.style.display == 'none' || elm.disabled) { + return; + } + + if (canFocus(elm)) { + elements.push(elm); + } + + for (var i = 0; i < elm.childNodes.length; i++) { + collect(elm.childNodes[i]); + } + } + + collect(elm || root.getEl()); + + return elements; + } + + /** + * Returns the navigation root control for the specified control. The navigation root + * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group. + * It will look for parents of the specified target control or the currently focused control if this option is omitted. + * + * @private + * @param {tinymce.ui.Control} targetControl Optional target control to find root of. + * @return {tinymce.ui.Control} Navigation root control. + */ + function getNavigationRoot(targetControl) { + var navigationRoot, controls; + + targetControl = targetControl || focusedControl; + controls = targetControl.parents().toArray(); + controls.unshift(targetControl); + + for (var i = 0; i < controls.length; i++) { + navigationRoot = controls[i]; + + if (navigationRoot.settings.ariaRoot) { + break; + } + } + + return navigationRoot; + } + + /** + * Focuses the first item in the specified targetControl element or the last aria index if the + * navigation root has the ariaRemember option enabled. + * + * @private + * @param {tinymce.ui.Control} targetControl Target control to focus the first item in. + */ + function focusFirst(targetControl) { + var navigationRoot = getNavigationRoot(targetControl); + var focusElements = getFocusElements(navigationRoot.getEl()); + + if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) { + moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements); + } else { + moveFocusToIndex(0, focusElements); + } + } + + /** + * Moves the focus to the specified index within the elements list. + * This will scope the index to the size of the element list if it changed. + * + * @private + * @param {Number} idx Specified index to move to. + * @param {Array} elements Array with dom elements to move focus within. + * @return {Number} Input index or a changed index if it was out of range. + */ + function moveFocusToIndex(idx, elements) { + if (idx < 0) { + idx = elements.length - 1; + } else if (idx >= elements.length) { + idx = 0; + } + + if (elements[idx]) { + elements[idx].focus(); + } + + return idx; + } + + /** + * Moves the focus forwards or backwards. + * + * @private + * @param {Number} dir Direction to move in positive means forward, negative means backwards. + * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements. + */ + function moveFocus(dir, elements) { + var idx = -1, navigationRoot = getNavigationRoot(); + + elements = elements || getFocusElements(navigationRoot.getEl()); + + for (var i = 0; i < elements.length; i++) { + if (elements[i] === focusedElement) { + idx = i; + } + } + + idx += dir; + navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements); + } + + /** + * Moves the focus to the left this is called by the left key. + * + * @private + */ + function left() { + var parentRole = getParentRole(); + + if (parentRole == "tablist") { + moveFocus(-1, getFocusElements(focusedElement.parentNode)); + } else if (focusedControl.parent().submenu) { + cancel(); + } else { + moveFocus(-1); + } + } + + /** + * Moves the focus to the right this is called by the right key. + * + * @private + */ + function right() { + var role = getRole(), parentRole = getParentRole(); + + if (parentRole == "tablist") { + moveFocus(1, getFocusElements(focusedElement.parentNode)); + } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) { + enter(); + } else { + moveFocus(1); + } + } + + /** + * Moves the focus to the up this is called by the up key. + * + * @private + */ + function up() { + moveFocus(-1); + } + + /** + * Moves the focus to the up this is called by the down key. + * + * @private + */ + function down() { + var role = getRole(), parentRole = getParentRole(); + + if (role == "menuitem" && parentRole == "menubar") { + enter(); + } else if (role == "button" && getAriaProp('haspopup')) { + enter({ key: 'down' }); + } else { + moveFocus(1); + } + } + + /** + * Moves the focus to the next item or previous item depending on shift key. + * + * @private + * @param {DOMEvent} e DOM event object. + */ + function tab(e) { + var parentRole = getParentRole(); + + if (parentRole == "tablist") { + var elm = getFocusElements(focusedControl.getEl('body'))[0]; + + if (elm) { + elm.focus(); + } + } else { + moveFocus(e.shiftKey ? -1 : 1); + } + } + + /** + * Calls the cancel event on the currently focused control. This is normally done using the Esc key. + * + * @private + */ + function cancel() { + focusedControl.fire('cancel'); + } + + /** + * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys. + * + * @private + * @param {Object} aria Optional aria data to pass along with the enter event. + */ + function enter(aria) { + aria = aria || {}; + focusedControl.fire('click', { target: focusedElement, aria: aria }); + } + + root.on('keydown', function (e) { + function handleNonTabOrEscEvent(e, handler) { + // Ignore non tab keys for text elements + if (isTextInputElement(focusedElement) || hasTabstopData(focusedElement)) { + return; + } + + if (getRole(focusedElement) === 'slider') { + return; + } + + if (handler(e) !== false) { + e.preventDefault(); + } + } + + if (e.isDefaultPrevented()) { + return; + } + + switch (e.keyCode) { + case 37: // DOM_VK_LEFT + handleNonTabOrEscEvent(e, left); + break; + + case 39: // DOM_VK_RIGHT + handleNonTabOrEscEvent(e, right); + break; + + case 38: // DOM_VK_UP + handleNonTabOrEscEvent(e, up); + break; + + case 40: // DOM_VK_DOWN + handleNonTabOrEscEvent(e, down); + break; + + case 27: // DOM_VK_ESCAPE + cancel(); + break; + + case 14: // DOM_VK_ENTER + case 13: // DOM_VK_RETURN + case 32: // DOM_VK_SPACE + handleNonTabOrEscEvent(e, enter); + break; + + case 9: // DOM_VK_TAB + if (tab(e) !== false) { + e.preventDefault(); + } + break; + } + }); + + root.on('focusin', function (e) { + focusedElement = e.target; + focusedControl = e.control; + }); + + return { + focusFirst: focusFirst + }; + }; + } +); +/** + * Container.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 + */ + +/** + * Container control. This is extended by all controls that can have + * children such as panels etc. You can also use this class directly as an + * generic container instance. The container doesn't have any specific role or style. + * + * @-x-less Container.less + * @class tinymce.ui.Container + * @extends tinymce.ui.Control + */ +define( + 'tinymce.ui.Container', + [ + "tinymce.ui.Control", + "tinymce.ui.Collection", + "tinymce.ui.Selector", + "tinymce.core.ui.Factory", + "tinymce.ui.KeyboardNavigation", + "tinymce.core.util.Tools", + "tinymce.core.dom.DomQuery", + "tinymce.ui.ClassList", + "tinymce.ui.ReflowQueue" + ], + function (Control, Collection, Selector, Factory, KeyboardNavigation, Tools, $, ClassList, ReflowQueue) { + "use strict"; + + var selectorCache = {}; + + return Control.extend({ + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Array} items Items to add to container in JSON format or control instances. + * @setting {String} layout Layout manager by name to use. + * @setting {Object} defaults Default settings to apply to all items. + */ + init: function (settings) { + var self = this; + + self._super(settings); + settings = self.settings; + + if (settings.fixed) { + self.state.set('fixed', true); + } + + self._items = new Collection(); + + if (self.isRtl()) { + self.classes.add('rtl'); + } + + self.bodyClasses = new ClassList(function () { + if (self.state.get('rendered')) { + self.getEl('body').className = this.toString(); + } + }); + self.bodyClasses.prefix = self.classPrefix; + + self.classes.add('container'); + self.bodyClasses.add('container-body'); + + if (settings.containerCls) { + self.classes.add(settings.containerCls); + } + + self._layout = Factory.create((settings.layout || '') + 'layout'); + + if (self.settings.items) { + self.add(self.settings.items); + } else { + self.add(self.render()); + } + + // TODO: Fix this! + self._hasBody = true; + }, + + /** + * Returns a collection of child items that the container currently have. + * + * @method items + * @return {tinymce.ui.Collection} Control collection direct child controls. + */ + items: function () { + return this._items; + }, + + /** + * Find child controls by selector. + * + * @method find + * @param {String} selector Selector CSS pattern to find children by. + * @return {tinymce.ui.Collection} Control collection with child controls. + */ + find: function (selector) { + selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector); + + return selector.find(this); + }, + + /** + * Adds one or many items to the current container. This will create instances of + * the object representations if needed. + * + * @method add + * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container. + * @return {tinymce.ui.Collection} Current collection control. + */ + add: function (items) { + var self = this; + + self.items().add(self.create(items)).parent(self); + + return self; + }, + + /** + * Focuses the current container instance. This will look + * for the first control in the container and focus that. + * + * @method focus + * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not. + * @return {tinymce.ui.Collection} Current instance. + */ + focus: function (keyboard) { + var self = this, focusCtrl, keyboardNav, items; + + if (keyboard) { + keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav; + + if (keyboardNav) { + keyboardNav.focusFirst(self); + return; + } + } + + items = self.find('*'); + + // TODO: Figure out a better way to auto focus alert dialog buttons + if (self.statusbar) { + items.add(self.statusbar.items()); + } + + items.each(function (ctrl) { + if (ctrl.settings.autofocus) { + focusCtrl = null; + return false; + } + + if (ctrl.canFocus) { + focusCtrl = focusCtrl || ctrl; + } + }); + + if (focusCtrl) { + focusCtrl.focus(); + } + + return self; + }, + + /** + * Replaces the specified child control with a new control. + * + * @method replace + * @param {tinymce.ui.Control} oldItem Old item to be replaced. + * @param {tinymce.ui.Control} newItem New item to be inserted. + */ + replace: function (oldItem, newItem) { + var ctrlElm, items = this.items(), i = items.length; + + // Replace the item in collection + while (i--) { + if (items[i] === oldItem) { + items[i] = newItem; + break; + } + } + + if (i >= 0) { + // Remove new item from DOM + ctrlElm = newItem.getEl(); + if (ctrlElm) { + ctrlElm.parentNode.removeChild(ctrlElm); + } + + // Remove old item from DOM + ctrlElm = oldItem.getEl(); + if (ctrlElm) { + ctrlElm.parentNode.removeChild(ctrlElm); + } + } + + // Adopt the item + newItem.parent(this); + }, + + /** + * Creates the specified items. If any of the items is plain JSON style objects + * it will convert these into real tinymce.ui.Control instances. + * + * @method create + * @param {Array} items Array of items to convert into control instances. + * @return {Array} Array with control instances. + */ + create: function (items) { + var self = this, settings, ctrlItems = []; + + // Non array structure, then force it into an array + if (!Tools.isArray(items)) { + items = [items]; + } + + // Add default type to each child control + Tools.each(items, function (item) { + if (item) { + // Construct item if needed + if (!(item instanceof Control)) { + // Name only then convert it to an object + if (typeof item == "string") { + item = { type: item }; + } + + // Create control instance based on input settings and default settings + settings = Tools.extend({}, self.settings.defaults, item); + item.type = settings.type = settings.type || item.type || self.settings.defaultType || + (settings.defaults ? settings.defaults.type : null); + item = Factory.create(settings); + } + + ctrlItems.push(item); + } + }); + + return ctrlItems; + }, + + /** + * Renders new control instances. + * + * @private + */ + renderNew: function () { + var self = this; + + // Render any new items + self.items().each(function (ctrl, index) { + var containerElm; + + ctrl.parent(self); + + if (!ctrl.state.get('rendered')) { + containerElm = self.getEl('body'); + + // Insert or append the item + if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) { + $(containerElm.childNodes[index]).before(ctrl.renderHtml()); + } else { + $(containerElm).append(ctrl.renderHtml()); + } + + ctrl.postRender(); + ReflowQueue.add(ctrl); + } + }); + + self._layout.applyClasses(self.items().filter(':visible')); + self._lastRect = null; + + return self; + }, + + /** + * Appends new instances to the current container. + * + * @method append + * @param {Array/tinymce.ui.Collection} items Array if controls to append. + * @return {tinymce.ui.Container} Current container instance. + */ + append: function (items) { + return this.add(items).renderNew(); + }, + + /** + * Prepends new instances to the current container. + * + * @method prepend + * @param {Array/tinymce.ui.Collection} items Array if controls to prepend. + * @return {tinymce.ui.Container} Current container instance. + */ + prepend: function (items) { + var self = this; + + self.items().set(self.create(items).concat(self.items().toArray())); + + return self.renderNew(); + }, + + /** + * Inserts an control at a specific index. + * + * @method insert + * @param {Array/tinymce.ui.Collection} items Array if controls to insert. + * @param {Number} index Index to insert controls at. + * @param {Boolean} [before=false] Inserts controls before the index. + */ + insert: function (items, index, before) { + var self = this, curItems, beforeItems, afterItems; + + items = self.create(items); + curItems = self.items(); + + if (!before && index < curItems.length - 1) { + index += 1; + } + + if (index >= 0 && index < curItems.length) { + beforeItems = curItems.slice(0, index).toArray(); + afterItems = curItems.slice(index).toArray(); + curItems.set(beforeItems.concat(items, afterItems)); + } + + return self.renderNew(); + }, + + /** + * Populates the form fields from the specified JSON data object. + * + * Control items in the form that matches the data will have it's value set. + * + * @method fromJSON + * @param {Object} data JSON data object to set control values by. + * @return {tinymce.ui.Container} Current form instance. + */ + fromJSON: function (data) { + var self = this; + + for (var name in data) { + self.find('#' + name).value(data[name]); + } + + return self; + }, + + /** + * Serializes the form into a JSON object by getting all items + * that has a name and a value. + * + * @method toJSON + * @return {Object} JSON object with form data. + */ + toJSON: function () { + var self = this, data = {}; + + self.find('*').each(function (ctrl) { + var name = ctrl.name(), value = ctrl.value(); + + if (name && typeof value != "undefined") { + data[name] = value; + } + }); + + return data; + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout, role = this.settings.role; + + self.preRender(); + layout.preRender(self); + + return ( + '
    ' + + '
    ' + + (self.settings.html || '') + layout.renderHtml(self) + + '
    ' + + '
    ' + ); + }, + + /** + * Post render method. Called after the control has been rendered to the target. + * + * @method postRender + * @return {tinymce.ui.Container} Current combobox instance. + */ + postRender: function () { + var self = this, box; + + self.items().exec('postRender'); + self._super(); + + self._layout.postRender(self); + self.state.set('rendered', true); + + if (self.settings.style) { + self.$el.css(self.settings.style); + } + + if (self.settings.border) { + box = self.borderBox; + self.$el.css({ + 'border-top-width': box.top, + 'border-right-width': box.right, + 'border-bottom-width': box.bottom, + 'border-left-width': box.left + }); + } + + if (!self.parent()) { + self.keyboardNav = new KeyboardNavigation({ + root: self + }); + } + + return self; + }, + + /** + * Initializes the current controls layout rect. + * This will be executed by the layout managers to determine the + * default minWidth/minHeight etc. + * + * @method initLayoutRect + * @return {Object} Layout rect instance. + */ + initLayoutRect: function () { + var self = this, layoutRect = self._super(); + + // Recalc container size by asking layout manager + self._layout.recalc(self); + + return layoutRect; + }, + + /** + * Recalculates the positions of the controls in the current container. + * This is invoked by the reflow method and shouldn't be called directly. + * + * @method recalc + */ + recalc: function () { + var self = this, rect = self._layoutRect, lastRect = self._lastRect; + + if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) { + self._layout.recalc(self); + rect = self.layoutRect(); + self._lastRect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h }; + return true; + } + }, + + /** + * Reflows the current container and it's children and possible parents. + * This should be used after you for example append children to the current control so + * that the layout managers know that they need to reposition everything. + * + * @example + * container.append({type: 'button', text: 'My button'}).reflow(); + * + * @method reflow + * @return {tinymce.ui.Container} Current container instance. + */ + reflow: function () { + var i; + + ReflowQueue.remove(this); + + if (this.visible()) { + Control.repaintControls = []; + Control.repaintControls.map = {}; + + this.recalc(); + i = Control.repaintControls.length; + + while (i--) { + Control.repaintControls[i].repaint(); + } + + // TODO: Fix me! + if (this.settings.layout !== "flow" && this.settings.layout !== "stack") { + this.repaint(); + } + + Control.repaintControls = []; + } + + return this; + } + }); + } +); +/** + * DragHelper.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 + */ + +/** + * Drag/drop helper class. + * + * @example + * var dragHelper = new tinymce.ui.DragHelper('mydiv', { + * start: function(document, window, evt) { + * }, + * + * drag: function(evt) { + * }, + * + * end: function(evt) { + * } + * }); + * + * @class tinymce.ui.DragHelper + */ +define( + 'tinymce.ui.DragHelper', + [ + 'global!document', + 'global!window', + 'tinymce.core.dom.DomQuery' + ], + function (document, window, DomQuery) { + "use strict"; + + function getDocumentSize(doc) { + var documentElement, body, scrollWidth, clientWidth; + var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max; + + documentElement = doc.documentElement; + body = doc.body; + + scrollWidth = max(documentElement.scrollWidth, body.scrollWidth); + clientWidth = max(documentElement.clientWidth, body.clientWidth); + offsetWidth = max(documentElement.offsetWidth, body.offsetWidth); + + scrollHeight = max(documentElement.scrollHeight, body.scrollHeight); + clientHeight = max(documentElement.clientHeight, body.clientHeight); + offsetHeight = max(documentElement.offsetHeight, body.offsetHeight); + + return { + width: scrollWidth < offsetWidth ? clientWidth : scrollWidth, + height: scrollHeight < offsetHeight ? clientHeight : scrollHeight + }; + } + + function updateWithTouchData(e) { + var keys, i; + + if (e.changedTouches) { + keys = "screenX screenY pageX pageY clientX clientY".split(' '); + for (i = 0; i < keys.length; i++) { + e[keys[i]] = e.changedTouches[0][keys[i]]; + } + } + } + + return function (id, settings) { + var $eventOverlay, doc = settings.document || document, downButton, start, stop, drag, startX, startY; + + settings = settings || {}; + + function getHandleElm() { + return doc.getElementById(settings.handle || id); + } + + start = function (e) { + var docSize = getDocumentSize(doc), handleElm, cursor; + + updateWithTouchData(e); + + e.preventDefault(); + downButton = e.button; + handleElm = getHandleElm(); + startX = e.screenX; + startY = e.screenY; + + // Grab cursor from handle so we can place it on overlay + if (window.getComputedStyle) { + cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor"); + } else { + cursor = handleElm.runtimeStyle.cursor; + } + + $eventOverlay = DomQuery('
    ').css({ + position: "absolute", + top: 0, left: 0, + width: docSize.width, + height: docSize.height, + zIndex: 0x7FFFFFFF, + opacity: 0.0001, + cursor: cursor + }).appendTo(doc.body); + + DomQuery(doc).on('mousemove touchmove', drag).on('mouseup touchend', stop); + + settings.start(e); + }; + + drag = function (e) { + updateWithTouchData(e); + + if (e.button !== downButton) { + return stop(e); + } + + e.deltaX = e.screenX - startX; + e.deltaY = e.screenY - startY; + + e.preventDefault(); + settings.drag(e); + }; + + stop = function (e) { + updateWithTouchData(e); + + DomQuery(doc).off('mousemove touchmove', drag).off('mouseup touchend', stop); + + $eventOverlay.remove(); + + if (settings.stop) { + settings.stop(e); + } + }; + + /** + * Destroys the drag/drop helper instance. + * + * @method destroy + */ + this.destroy = function () { + DomQuery(getHandleElm()).off(); + }; + + DomQuery(getHandleElm()).on('mousedown touchstart', start); + }; + } +); +/** + * Scrollable.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 mixin makes controls scrollable using custom scrollbars. + * + * @-x-less Scrollable.less + * @mixin tinymce.ui.Scrollable + */ +define( + 'tinymce.ui.Scrollable', + [ + "tinymce.core.dom.DomQuery", + "tinymce.ui.DragHelper" + ], + function ($, DragHelper) { + "use strict"; + + return { + init: function () { + var self = this; + self.on('repaint', self.renderScroll); + }, + + renderScroll: function () { + var self = this, margin = 2; + + function repaintScroll() { + var hasScrollH, hasScrollV, bodyElm; + + function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) { + var containerElm, scrollBarElm, scrollThumbElm; + var containerSize, scrollSize, ratio, rect; + var posNameLower, sizeNameLower; + + scrollBarElm = self.getEl('scroll' + axisName); + if (scrollBarElm) { + posNameLower = posName.toLowerCase(); + sizeNameLower = sizeName.toLowerCase(); + + $(self.getEl('absend')).css(posNameLower, self.layoutRect()[contentSizeName] - 1); + + if (!hasScroll) { + $(scrollBarElm).css('display', 'none'); + return; + } + + $(scrollBarElm).css('display', 'block'); + containerElm = self.getEl('body'); + scrollThumbElm = self.getEl('scroll' + axisName + "t"); + containerSize = containerElm["client" + sizeName] - (margin * 2); + containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0; + scrollSize = containerElm["scroll" + sizeName]; + ratio = containerSize / scrollSize; + + rect = {}; + rect[posNameLower] = containerElm["offset" + posName] + margin; + rect[sizeNameLower] = containerSize; + $(scrollBarElm).css(rect); + + rect = {}; + rect[posNameLower] = containerElm["scroll" + posName] * ratio; + rect[sizeNameLower] = containerSize * ratio; + $(scrollThumbElm).css(rect); + } + } + + bodyElm = self.getEl('body'); + hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth; + hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight; + + repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height"); + repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width"); + } + + function addScroll() { + function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) { + var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix; + + $(self.getEl()).append( + '
    ' + + '
    ' + + '
    ' + ); + + self.draghelper = new DragHelper(axisId + 't', { + start: function () { + scrollStart = self.getEl('body')["scroll" + posName]; + $('#' + axisId).addClass(prefix + 'active'); + }, + + drag: function (e) { + var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect(); + + hasScrollH = layoutRect.contentW > layoutRect.innerW; + hasScrollV = layoutRect.contentH > layoutRect.innerH; + containerSize = self.getEl('body')["client" + sizeName] - (margin * 2); + containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0; + + ratio = containerSize / self.getEl('body')["scroll" + sizeName]; + self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio); + }, + + stop: function () { + $('#' + axisId).removeClass(prefix + 'active'); + } + }); + } + + self.classes.add('scroll'); + + addScrollAxis("v", "Top", "Height", "Y", "Width"); + addScrollAxis("h", "Left", "Width", "X", "Height"); + } + + if (self.settings.autoScroll) { + if (!self._hasScroll) { + self._hasScroll = true; + addScroll(); + + self.on('wheel', function (e) { + var bodyEl = self.getEl('body'); + + bodyEl.scrollLeft += (e.deltaX || 0) * 10; + bodyEl.scrollTop += e.deltaY * 10; + + repaintScroll(); + }); + + $(self.getEl('body')).on("scroll", repaintScroll); + } + + repaintScroll(); + } + } + }; + } +); +/** + * Panel.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 new panel. + * + * @-x-less Panel.less + * @class tinymce.ui.Panel + * @extends tinymce.ui.Container + * @mixes tinymce.ui.Scrollable + */ +define( + 'tinymce.ui.Panel', + [ + "tinymce.ui.Container", + "tinymce.ui.Scrollable" + ], + function (Container, Scrollable) { + "use strict"; + + return Container.extend({ + Defaults: { + layout: 'fit', + containerCls: 'panel' + }, + + Mixins: [Scrollable], + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout, innerHtml = self.settings.html; + + self.preRender(); + layout.preRender(self); + + if (typeof innerHtml == "undefined") { + innerHtml = ( + '
    ' + + layout.renderHtml(self) + + '
    ' + ); + } else { + if (typeof innerHtml == 'function') { + innerHtml = innerHtml.call(self); + } + + self._hasBody = false; + } + + return ( + '
    ' + + (self._preBodyHtml || '') + + innerHtml + + '
    ' + ); + } + }); + } +); + +/** + * Resizable.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 + */ + +/** + * Resizable mixin. Enables controls to be resized. + * + * @mixin tinymce.ui.Resizable + */ +define( + 'tinymce.ui.Resizable', + [ + "tinymce.ui.DomUtils" + ], + function (DomUtils) { + "use strict"; + + return { + /** + * Resizes the control to contents. + * + * @method resizeToContent + */ + resizeToContent: function () { + this._layoutRect.autoResize = true; + this._lastRect = null; + this.reflow(); + }, + + /** + * Resizes the control to a specific width/height. + * + * @method resizeTo + * @param {Number} w Control width. + * @param {Number} h Control height. + * @return {tinymce.ui.Control} Current control instance. + */ + resizeTo: function (w, h) { + // TODO: Fix hack + if (w <= 1 || h <= 1) { + var rect = DomUtils.getWindowSize(); + + w = w <= 1 ? w * rect.w : w; + h = h <= 1 ? h * rect.h : h; + } + + this._layoutRect.autoResize = false; + return this.layoutRect({ minW: w, minH: h, w: w, h: h }).reflow(); + }, + + /** + * Resizes the control to a specific relative width/height. + * + * @method resizeBy + * @param {Number} dw Relative control width. + * @param {Number} dh Relative control height. + * @return {tinymce.ui.Control} Current control instance. + */ + resizeBy: function (dw, dh) { + var self = this, rect = self.layoutRect(); + + return self.resizeTo(rect.w + dw, rect.h + dh); + } + }; + } +); +/** + * FloatPanel.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 creates a floating panel. + * + * @-x-less FloatPanel.less + * @class tinymce.ui.FloatPanel + * @extends tinymce.ui.Panel + * @mixes tinymce.ui.Movable + * @mixes tinymce.ui.Resizable + */ +define( + 'tinymce.ui.FloatPanel', + [ + 'global!document', + 'global!window', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.util.Delay', + 'tinymce.ui.DomUtils', + 'tinymce.ui.Movable', + 'tinymce.ui.Panel', + 'tinymce.ui.Resizable' + ], + function (document, window, DomQuery, Delay, DomUtils, Movable, Panel, Resizable) { + "use strict"; + + var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = []; + var zOrder = [], hasModal; + + function isChildOf(ctrl, parent) { + while (ctrl) { + if (ctrl == parent) { + return true; + } + + ctrl = ctrl.parent(); + } + } + + function skipOrHidePanels(e) { + // Hide any float panel when a click/focus out is out side that float panel and the + // float panels direct parent for example a click on a menu button + var i = visiblePanels.length; + + while (i--) { + var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); + + if (panel.settings.autohide) { + if (clickCtrl) { + if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { + continue; + } + } + + e = panel.fire('autohide', { target: e.target }); + if (!e.isDefaultPrevented()) { + panel.hide(); + } + } + } + } + + function bindDocumentClickHandler() { + + if (!documentClickHandler) { + documentClickHandler = function (e) { + // Gecko fires click event and in the wrong order on Mac so lets normalize + if (e.button == 2) { + return; + } + + skipOrHidePanels(e); + }; + + DomQuery(document).on('click touchstart', documentClickHandler); + } + } + + function bindDocumentScrollHandler() { + if (!documentScrollHandler) { + documentScrollHandler = function () { + var i; + + i = visiblePanels.length; + while (i--) { + repositionPanel(visiblePanels[i]); + } + }; + + DomQuery(window).on('scroll', documentScrollHandler); + } + } + + function bindWindowResizeHandler() { + if (!windowResizeHandler) { + var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight; + + windowResizeHandler = function () { + // Workaround for #7065 IE 7 fires resize events event though the window wasn't resized + if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) { + clientWidth = docElm.clientWidth; + clientHeight = docElm.clientHeight; + FloatPanel.hideAll(); + } + }; + + DomQuery(window).on('resize', windowResizeHandler); + } + } + + /** + * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will + * also reposition all child panels of the current panel. + */ + function repositionPanel(panel) { + var scrollY = DomUtils.getViewPort().y; + + function toggleFixedChildPanels(fixed, deltaY) { + var parent; + + for (var i = 0; i < visiblePanels.length; i++) { + if (visiblePanels[i] != panel) { + parent = visiblePanels[i].parent(); + + while (parent && (parent = parent.parent())) { + if (parent == panel) { + visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); + } + } + } + } + } + + if (panel.settings.autofix) { + if (!panel.state.get('fixed')) { + panel._autoFixY = panel.layoutRect().y; + + if (panel._autoFixY < scrollY) { + panel.fixed(true).layoutRect({ y: 0 }).repaint(); + toggleFixedChildPanels(true, scrollY - panel._autoFixY); + } + } else { + if (panel._autoFixY > scrollY) { + panel.fixed(false).layoutRect({ y: panel._autoFixY }).repaint(); + toggleFixedChildPanels(false, panel._autoFixY - scrollY); + } + } + } + } + + function addRemove(add, ctrl) { + var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal; + + if (add) { + zOrder.push(ctrl); + } else { + i = zOrder.length; + + while (i--) { + if (zOrder[i] === ctrl) { + zOrder.splice(i, 1); + } + } + } + + if (zOrder.length) { + for (i = 0; i < zOrder.length; i++) { + if (zOrder[i].modal) { + zIndex++; + topModal = zOrder[i]; + } + + zOrder[i].getEl().style.zIndex = zIndex; + zOrder[i].zIndex = zIndex; + zIndex++; + } + } + + var modalBlockEl = DomQuery('#' + ctrl.classPrefix + 'modal-block', ctrl.getContainerElm())[0]; + + if (topModal) { + DomQuery(modalBlockEl).css('z-index', topModal.zIndex - 1); + } else if (modalBlockEl) { + modalBlockEl.parentNode.removeChild(modalBlockEl); + hasModal = false; + } + + FloatPanel.currentZIndex = zIndex; + } + + var FloatPanel = Panel.extend({ + Mixins: [Movable, Resizable], + + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} autohide Automatically hide the panel. + */ + init: function (settings) { + var self = this; + + self._super(settings); + self._eventsRoot = self; + + self.classes.add('floatpanel'); + + // Hide floatpanes on click out side the root button + if (settings.autohide) { + bindDocumentClickHandler(); + bindWindowResizeHandler(); + visiblePanels.push(self); + } + + if (settings.autofix) { + bindDocumentScrollHandler(); + + self.on('move', function () { + repositionPanel(this); + }); + } + + self.on('postrender show', function (e) { + if (e.control == self) { + var $modalBlockEl, prefix = self.classPrefix; + + if (self.modal && !hasModal) { + $modalBlockEl = DomQuery('#' + prefix + 'modal-block', self.getContainerElm()); + if (!$modalBlockEl[0]) { + $modalBlockEl = DomQuery( + '
    ' + ).appendTo(self.getContainerElm()); + } + + Delay.setTimeout(function () { + $modalBlockEl.addClass(prefix + 'in'); + DomQuery(self.getEl()).addClass(prefix + 'in'); + }); + + hasModal = true; + } + + addRemove(true, self); + } + }); + + self.on('show', function () { + self.parents().each(function (ctrl) { + if (ctrl.state.get('fixed')) { + self.fixed(true); + return false; + } + }); + }); + + if (settings.popover) { + self._preBodyHtml = '
    '; + self.classes.add('popover').add('bottom').add(self.isRtl() ? 'end' : 'start'); + } + + self.aria('label', settings.ariaLabel); + self.aria('labelledby', self._id); + self.aria('describedby', self.describedBy || self._id + '-none'); + }, + + fixed: function (state) { + var self = this; + + if (self.state.get('fixed') != state) { + if (self.state.get('rendered')) { + var viewport = DomUtils.getViewPort(); + + if (state) { + self.layoutRect().y -= viewport.y; + } else { + self.layoutRect().y += viewport.y; + } + } + + self.classes.toggle('fixed', state); + self.state.set('fixed', state); + } + + return self; + }, + + /** + * Shows the current float panel. + * + * @method show + * @return {tinymce.ui.FloatPanel} Current floatpanel instance. + */ + show: function () { + var self = this, i, state = self._super(); + + i = visiblePanels.length; + while (i--) { + if (visiblePanels[i] === self) { + break; + } + } + + if (i === -1) { + visiblePanels.push(self); + } + + return state; + }, + + /** + * Hides the current float panel. + * + * @method hide + * @return {tinymce.ui.FloatPanel} Current floatpanel instance. + */ + hide: function () { + removeVisiblePanel(this); + addRemove(false, this); + + return this._super(); + }, + + /** + * Hide all visible float panels with he autohide setting enabled. This is for + * manually hiding floating menus or panels. + * + * @method hideAll + */ + hideAll: function () { + FloatPanel.hideAll(); + }, + + /** + * Closes the float panel. This will remove the float panel from page and fire the close event. + * + * @method close + */ + close: function () { + var self = this; + + if (!self.fire('close').isDefaultPrevented()) { + self.remove(); + addRemove(false, self); + } + + return self; + }, + + /** + * Removes the float panel from page. + * + * @method remove + */ + remove: function () { + removeVisiblePanel(this); + this._super(); + }, + + postRender: function () { + var self = this; + + if (self.settings.bodyRole) { + this.getEl('body').setAttribute('role', self.settings.bodyRole); + } + + return self._super(); + } + }); + + /** + * Hide all visible float panels with he autohide setting enabled. This is for + * manually hiding floating menus or panels. + * + * @static + * @method hideAll + */ + FloatPanel.hideAll = function () { + var i = visiblePanels.length; + + while (i--) { + var panel = visiblePanels[i]; + + if (panel && panel.settings.autohide) { + panel.hide(); + visiblePanels.splice(i, 1); + } + } + }; + + function removeVisiblePanel(panel) { + var i; + + i = visiblePanels.length; + while (i--) { + if (visiblePanels[i] === panel) { + visiblePanels.splice(i, 1); + } + } + + i = zOrder.length; + while (i--) { + if (zOrder[i] === panel) { + zOrder.splice(i, 1); + } + } + } + + return FloatPanel; + } +); + +/** + * Inline.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.modes.Inline', + [ + 'global!document', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.ui.Factory', + 'tinymce.themes.modern.api.Events', + 'tinymce.themes.modern.api.Settings', + 'tinymce.themes.modern.ui.A11y', + 'tinymce.themes.modern.ui.ContextToolbars', + 'tinymce.themes.modern.ui.Menubar', + 'tinymce.themes.modern.ui.SkinLoaded', + 'tinymce.themes.modern.ui.Toolbar', + 'tinymce.ui.FloatPanel' + ], + function (document, DOMUtils, Factory, Events, Settings, A11y, ContextToolbars, Menubar, SkinLoaded, Toolbar, FloatPanel) { + var render = function (editor, theme, args) { + var panel, inlineToolbarContainer; + var DOM = DOMUtils.DOM; + + var fixedToolbarContainer = Settings.getFixedToolbarContainer(editor); + if (fixedToolbarContainer) { + inlineToolbarContainer = DOM.select(fixedToolbarContainer)[0]; + } + + var reposition = function () { + if (panel && panel.moveRel && panel.visible() && !panel._fixed) { + // TODO: This is kind of ugly and doesn't handle multiple scrollable elements + var scrollContainer = editor.selection.getScrollContainer(), body = editor.getBody(); + var deltaX = 0, deltaY = 0; + + if (scrollContainer) { + var bodyPos = DOM.getPos(body), scrollContainerPos = DOM.getPos(scrollContainer); + + deltaX = Math.max(0, scrollContainerPos.x - bodyPos.x); + deltaY = Math.max(0, scrollContainerPos.y - bodyPos.y); + } + + panel.fixed(false).moveRel(body, editor.rtl ? ['tr-br', 'br-tr'] : ['tl-bl', 'bl-tl', 'tr-br']).moveBy(deltaX, deltaY); + } + }; + + var show = function () { + if (panel) { + panel.show(); + reposition(); + DOM.addClass(editor.getBody(), 'mce-edit-focus'); + } + }; + + var hide = function () { + if (panel) { + // We require two events as the inline float panel based toolbar does not have autohide=true + panel.hide(); + + // All other autohidden float panels will be closed below. + FloatPanel.hideAll(); + + DOM.removeClass(editor.getBody(), 'mce-edit-focus'); + } + }; + + var render = function () { + if (panel) { + if (!panel.visible()) { + show(); + } + + return; + } + + // Render a plain panel inside the inlineToolbarContainer if it's defined + panel = theme.panel = Factory.create({ + type: inlineToolbarContainer ? 'panel' : 'floatpanel', + role: 'application', + classes: 'tinymce tinymce-inline', + layout: 'flex', + direction: 'column', + align: 'stretch', + autohide: false, + autofix: true, + fixed: !!inlineToolbarContainer, + border: 1, + items: [ + Settings.hasMenubar(editor) === false ? null : { type: 'menubar', border: '0 0 1 0', items: Menubar.createMenuButtons(editor) }, + Toolbar.createToolbars(editor, Settings.getToolbarSize(editor)) + ] + }); + + // Add statusbar + /*if (settings.statusbar !== false) { + panel.add({type: 'panel', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', items: [ + {type: 'elementpath'} + ]}); + }*/ + + Events.fireBeforeRenderUI(editor); + panel.renderTo(inlineToolbarContainer || document.body).reflow(); + + A11y.addKeys(editor, panel); + show(); + ContextToolbars.addContextualToolbars(editor); + + editor.on('nodeChange', reposition); + editor.on('activate', show); + editor.on('deactivate', hide); + + editor.nodeChanged(); + }; + + editor.settings.content_editable = true; + + editor.on('focus', function () { + // Render only when the CSS file has been loaded + if (args.skinUiCss) { + DOM.styleSheetLoader.load(args.skinUiCss, render, render); + } else { + render(); + } + }); + + editor.on('blur hide', hide); + + // Remove the panel when the editor is removed + editor.on('remove', function () { + if (panel) { + panel.remove(); + panel = null; + } + }); + + // Preload skin css + if (args.skinUiCss) { + DOM.styleSheetLoader.load(args.skinUiCss, SkinLoaded.fireSkinLoaded(editor)); + } + + return {}; + }; + + return { + render: render + }; + } +); + +/** + * Throbber.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 display a Throbber for any element. + * + * @-x-less Throbber.less + * @class tinymce.ui.Throbber + */ +define( + 'tinymce.ui.Throbber', + [ + "tinymce.core.dom.DomQuery", + "tinymce.ui.Control", + "tinymce.core.util.Delay" + ], + function ($, Control, Delay) { + "use strict"; + + /** + * Constructs a new throbber. + * + * @constructor + * @param {Element} elm DOM Html element to display throbber in. + * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll. + */ + return function (elm, inline) { + var self = this, state, classPrefix = Control.classPrefix, timer; + + /** + * Shows the throbber. + * + * @method show + * @param {Number} [time] Time to wait before showing. + * @param {function} [callback] Optional callback to execute when the throbber is shown. + * @return {tinymce.ui.Throbber} Current throbber instance. + */ + self.show = function (time, callback) { + function render() { + if (state) { + $(elm).append( + '
    ' + ); + + if (callback) { + callback(); + } + } + } + + self.hide(); + + state = true; + + if (time) { + timer = Delay.setTimeout(render, time); + } else { + render(); + } + + return self; + }; + + /** + * Hides the throbber. + * + * @method hide + * @return {tinymce.ui.Throbber} Current throbber instance. + */ + self.hide = function () { + var child = elm.lastChild; + + Delay.clearTimeout(timer); + + if (child && child.className.indexOf('throbber') != -1) { + child.parentNode.removeChild(child); + } + + state = false; + + return self; + }; + }; + } +); + +/** + * ProgressState.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.ui.ProgressState', + [ + 'tinymce.ui.Throbber' + ], + function (Throbber) { + var setup = function (editor, theme) { + var throbber; + + editor.on('ProgressState', function (e) { + throbber = throbber || new Throbber(theme.panel.getEl('body')); + + if (e.state) { + throbber.show(e.time); + } else { + throbber.hide(); + } + }); + }; + + return { + setup: setup + }; + } +); + +/** + * Render.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.ui.Render', + [ + 'tinymce.core.EditorManager', + 'tinymce.themes.modern.api.Settings', + 'tinymce.themes.modern.modes.Iframe', + 'tinymce.themes.modern.modes.Inline', + 'tinymce.themes.modern.ui.ProgressState' + ], + function (EditorManager, Settings, Iframe, Inline, ProgressState) { + var renderUI = function (editor, theme, args) { + var skinUrl = Settings.getSkinUrl(editor); + + if (skinUrl) { + args.skinUiCss = skinUrl + '/skin.min.css'; + editor.contentCSS.push(skinUrl + '/content' + (editor.inline ? '.inline' : '') + '.min.css'); + } + + ProgressState.setup(editor, theme); + + return Settings.isInline(editor) ? Inline.render(editor, theme, args) : Iframe.render(editor, theme, args); + }; + + return { + renderUI: renderUI + }; + } +); + +defineGlobal("global!setTimeout", setTimeout); +/** + * 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.ui.Tooltip', + [ + "tinymce.ui.Control", + "tinymce.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 ( + '' + ); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:text', function (e) { + self.getEl().lastChild.innerHTML = self.encode(e.value); + }); + + return self._super(); + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this, style, rect; + + style = self.getEl().style; + rect = self._layoutRect; + + style.left = rect.x + 'px'; + style.top = rect.y + 'px'; + style.zIndex = 0xFFFF + 0xFFFF; + } + }); + } +); +/** + * Widget.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 + */ + +/** + * Widget base class a widget is a control that has a tooltip and some basic states. + * + * @class tinymce.ui.Widget + * @extends tinymce.ui.Control + */ +define( + 'tinymce.ui.Widget', + [ + "tinymce.ui.Control", + "tinymce.ui.Tooltip" + ], + function (Control, Tooltip) { + "use strict"; + + var tooltip; + + var Widget = Control.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} tooltip Tooltip text to display when hovering. + * @setting {Boolean} autofocus True if the control should be focused when rendered. + * @setting {String} text Text to display inside widget. + */ + init: function (settings) { + var self = this; + + self._super(settings); + settings = self.settings; + self.canFocus = true; + + if (settings.tooltip && Widget.tooltips !== false) { + self.on('mouseenter', function (e) { + var tooltip = self.tooltip().moveTo(-0xFFFF); + + if (e.control == self) { + var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']); + + tooltip.classes.toggle('tooltip-n', rel == 'bc-tc'); + tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl'); + tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr'); + + tooltip.moveRel(self.getEl(), rel); + } else { + tooltip.hide(); + } + }); + + self.on('mouseleave mousedown click', function () { + self.tooltip().hide(); + }); + } + + self.aria('label', settings.ariaLabel || settings.tooltip); + }, + + /** + * Returns the current tooltip instance. + * + * @method tooltip + * @return {tinymce.ui.Tooltip} Tooltip instance. + */ + tooltip: function () { + if (!tooltip) { + tooltip = new Tooltip({ type: 'tooltip' }); + tooltip.renderTo(); + } + + return tooltip; + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this, settings = self.settings; + + self._super(); + + if (!self.parent() && (settings.width || settings.height)) { + self.initLayoutRect(); + self.repaint(); + } + + if (settings.autofocus) { + self.focus(); + } + }, + + bindStates: function () { + var self = this; + + function disable(state) { + self.aria('disabled', state); + self.classes.toggle('disabled', state); + } + + function active(state) { + self.aria('pressed', state); + self.classes.toggle('active', state); + } + + self.state.on('change:disabled', function (e) { + disable(e.value); + }); + + self.state.on('change:active', function (e) { + active(e.value); + }); + + if (self.state.get('disabled')) { + disable(true); + } + + if (self.state.get('active')) { + active(true); + } + + return self._super(); + }, + + /** + * Removes the current control from DOM and from UI collections. + * + * @method remove + * @return {tinymce.ui.Control} Current control instance. + */ + remove: function () { + this._super(); + + if (tooltip) { + tooltip.remove(); + tooltip = null; + } + } + }); + + return Widget; + } +); + +/** + * Progress.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 + */ + +/** + * Progress control. + * + * @-x-less Progress.less + * @class tinymce.ui.Progress + * @extends tinymce.ui.Control + */ +define( + 'tinymce.ui.Progress', + [ + "tinymce.ui.Widget" + ], + function (Widget) { + "use strict"; + + return Widget.extend({ + Defaults: { + value: 0 + }, + + init: function (settings) { + var self = this; + + self._super(settings); + self.classes.add('progress'); + + if (!self.settings.filter) { + self.settings.filter = function (value) { + return Math.round(value); + }; + } + }, + + renderHtml: function () { + var self = this, id = self._id, prefix = this.classPrefix; + + return ( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    0%
    ' + + '
    ' + ); + }, + + postRender: function () { + var self = this; + + self._super(); + self.value(self.settings.value); + + return self; + }, + + bindStates: function () { + var self = this; + + function setValue(value) { + value = self.settings.filter(value); + self.getEl().lastChild.innerHTML = value + '%'; + self.getEl().firstChild.firstChild.style.width = value + '%'; + } + + self.state.on('change:value', function (e) { + setValue(e.value); + }); + + setValue(self.state.get('value')); + + return self._super(); + } + }); + } +); +/** + * Notification.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 notification instance. + * + * @-x-less Notification.less + * @class tinymce.ui.Notification + * @extends tinymce.ui.Container + * @mixes tinymce.ui.Movable + */ +define( + 'tinymce.ui.Notification', + [ + "tinymce.ui.Control", + "tinymce.ui.Movable", + "tinymce.ui.Progress", + "tinymce.core.util.Delay" + ], + function (Control, Movable, Progress, Delay) { + var updateLiveRegion = function (ctx, text) { + ctx.getEl().lastChild.textContent = text + (ctx.progressBar ? ' ' + ctx.progressBar.value() + '%' : ''); + }; + + return Control.extend({ + Mixins: [Movable], + + Defaults: { + classes: 'widget notification' + }, + + init: function (settings) { + var self = this; + + self._super(settings); + + self.maxWidth = settings.maxWidth; + + if (settings.text) { + self.text(settings.text); + } + + if (settings.icon) { + self.icon = settings.icon; + } + + if (settings.color) { + self.color = settings.color; + } + + if (settings.type) { + self.classes.add('notification-' + settings.type); + } + + if (settings.timeout && (settings.timeout < 0 || settings.timeout > 0) && !settings.closeButton) { + self.closeButton = false; + } else { + self.classes.add('has-close'); + self.closeButton = true; + } + + if (settings.progressBar) { + self.progressBar = new Progress(); + } + + self.on('click', function (e) { + if (e.target.className.indexOf(self.classPrefix + 'close') != -1) { + self.close(); + } + }); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, prefix = self.classPrefix, icon = '', closeButton = '', progressBar = '', notificationStyle = ''; + + if (self.icon) { + icon = ''; + } + + notificationStyle = ' style="max-width: ' + self.maxWidth + 'px;' + (self.color ? 'background-color: ' + self.color + ';"' : '"'); + + if (self.closeButton) { + closeButton = ''; + } + + if (self.progressBar) { + progressBar = self.progressBar.renderHtml(); + } + + return ( + '' + ); + }, + + postRender: function () { + var self = this; + + Delay.setTimeout(function () { + self.$el.addClass(self.classPrefix + 'in'); + updateLiveRegion(self, self.state.get('text')); + }, 100); + + return self._super(); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:text', function (e) { + self.getEl().firstChild.innerHTML = e.value; + updateLiveRegion(self, e.value); + }); + if (self.progressBar) { + self.progressBar.bindStates(); + self.progressBar.state.on('change:value', function (e) { + updateLiveRegion(self, self.state.get('text')); + }); + } + return self._super(); + }, + + close: function () { + var self = this; + + if (!self.fire('close').isDefaultPrevented()) { + self.remove(); + } + + return self; + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this, style, rect; + + style = self.getEl().style; + rect = self._layoutRect; + + style.left = rect.x + 'px'; + style.top = rect.y + 'px'; + + // Hardcoded arbitrary z-value because we want the + // notifications under the other windows + style.zIndex = 0xFFFF - 1; + } + }); + } +); +/** + * NotificationManagerImpl.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.ui.NotificationManagerImpl', + [ + 'ephox.katamari.api.Arr', + 'global!setTimeout', + 'tinymce.core.util.Tools', + 'tinymce.ui.DomUtils', + 'tinymce.ui.Notification' + ], + function (Arr, setTimeout, Tools, DomUtils, Notification) { + return function (editor) { + var getEditorContainer = function (editor) { + return editor.inline ? editor.getElement() : editor.getContentAreaContainer(); + }; + + var getContainerWidth = function () { + var container = getEditorContainer(editor); + return DomUtils.getSize(container).width; + }; + + // Since the viewport will change based on the present notifications, we need to move them all to the + // top left of the viewport to give an accurate size measurement so we can position them later. + var prePositionNotifications = function (notifications) { + Arr.each(notifications, function (notification) { + notification.moveTo(0, 0); + }); + }; + + var positionNotifications = function (notifications) { + if (notifications.length > 0) { + var firstItem = notifications.slice(0, 1)[0]; + var container = getEditorContainer(editor); + firstItem.moveRel(container, 'tc-tc'); + Arr.each(notifications, function (notification, index) { + if (index > 0) { + notification.moveRel(notifications[index - 1].getEl(), 'bc-tc'); + } + }); + } + }; + + var reposition = function (notifications) { + prePositionNotifications(notifications); + positionNotifications(notifications); + }; + + var open = function (args, closeCallback) { + var extendedArgs = Tools.extend(args, { maxWidth: getContainerWidth() }); + var notif = new Notification(extendedArgs); + notif.args = extendedArgs; + + //If we have a timeout value + if (extendedArgs.timeout > 0) { + notif.timer = setTimeout(function () { + notif.close(); + closeCallback(); + }, extendedArgs.timeout); + } + + notif.on('close', function () { + closeCallback(); + }); + + notif.renderTo(); + + return notif; + }; + + var close = function (notification) { + notification.close(); + }; + + var getArgs = function (notification) { + return notification.args; + }; + + return { + open: open, + close: close, + reposition: reposition, + getArgs: getArgs + }; + }; + } +); + +/** + * Window.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 new window. + * + * @-x-less Window.less + * @class tinymce.ui.Window + * @extends tinymce.ui.FloatPanel + */ +define( + 'tinymce.ui.Window', + [ + 'global!document', + 'global!setTimeout', + 'global!window', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.Env', + 'tinymce.core.util.Delay', + 'tinymce.ui.BoxUtils', + 'tinymce.ui.DomUtils', + 'tinymce.ui.DragHelper', + 'tinymce.ui.FloatPanel', + 'tinymce.ui.Panel' + ], + function (document, setTimeout, window, DomQuery, Env, Delay, BoxUtils, DomUtils, DragHelper, FloatPanel, Panel) { + "use strict"; + + var windows = [], oldMetaValue = ''; + + function toggleFullScreenState(state) { + var noScaleMetaValue = 'width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0', + viewport = DomQuery("meta[name=viewport]")[0], + contentValue; + + if (Env.overrideViewPort === false) { + return; + } + + if (!viewport) { + viewport = document.createElement('meta'); + viewport.setAttribute('name', 'viewport'); + document.getElementsByTagName('head')[0].appendChild(viewport); + } + + contentValue = viewport.getAttribute('content'); + if (contentValue && typeof oldMetaValue != 'undefined') { + oldMetaValue = contentValue; + } + + viewport.setAttribute('content', state ? noScaleMetaValue : oldMetaValue); + } + + function toggleBodyFullScreenClasses(classPrefix, state) { + if (checkFullscreenWindows() && state === false) { + DomQuery([document.documentElement, document.body]).removeClass(classPrefix + 'fullscreen'); + } + } + + function checkFullscreenWindows() { + for (var i = 0; i < windows.length; i++) { + if (windows[i]._fullscreen) { + return true; + } + } + return false; + } + + function handleWindowResize() { + if (!Env.desktop) { + var lastSize = { + w: window.innerWidth, + h: window.innerHeight + }; + + Delay.setInterval(function () { + var w = window.innerWidth, + h = window.innerHeight; + + if (lastSize.w != w || lastSize.h != h) { + lastSize = { + w: w, + h: h + }; + + DomQuery(window).trigger('resize'); + } + }, 100); + } + + function reposition() { + var i, rect = DomUtils.getWindowSize(), layoutRect; + + for (i = 0; i < windows.length; i++) { + layoutRect = windows[i].layoutRect(); + + windows[i].moveTo( + windows[i].settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2), + windows[i].settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2) + ); + } + } + + DomQuery(window).on('resize', reposition); + } + + var Window = FloatPanel.extend({ + modal: true, + + Defaults: { + border: 1, + layout: 'flex', + containerCls: 'panel', + role: 'dialog', + callbacks: { + submit: function () { + this.fire('submit', { data: this.toJSON() }); + }, + + close: function () { + this.close(); + } + } + }, + + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + var self = this; + + self._super(settings); + + if (self.isRtl()) { + self.classes.add('rtl'); + } + + self.classes.add('window'); + self.bodyClasses.add('window-body'); + self.state.set('fixed', true); + + // Create statusbar + if (settings.buttons) { + self.statusbar = new Panel({ + layout: 'flex', + border: '1 0 0 0', + spacing: 3, + padding: 10, + align: 'center', + pack: self.isRtl() ? 'start' : 'end', + defaults: { + type: 'button' + }, + items: settings.buttons + }); + + self.statusbar.classes.add('foot'); + self.statusbar.parent(self); + } + + self.on('click', function (e) { + var closeClass = self.classPrefix + 'close'; + + if (DomUtils.hasClass(e.target, closeClass) || DomUtils.hasClass(e.target.parentNode, closeClass)) { + self.close(); + } + }); + + self.on('cancel', function () { + self.close(); + }); + + self.aria('describedby', self.describedBy || self._id + '-none'); + self.aria('label', settings.title); + self._fullscreen = false; + }, + + /** + * Recalculates the positions of the controls in the current container. + * This is invoked by the reflow method and shouldn't be called directly. + * + * @method recalc + */ + recalc: function () { + var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc; + + if (self._fullscreen) { + self.layoutRect(DomUtils.getWindowSize()); + self.layoutRect().contentH = self.layoutRect().innerH; + } + + self._super(); + + layoutRect = self.layoutRect(); + + // Resize window based on title width + if (self.settings.title && !self._fullscreen) { + width = layoutRect.headerW; + if (width > layoutRect.w) { + x = layoutRect.x - Math.max(0, width / 2); + self.layoutRect({ w: width, x: x }); + needsRecalc = true; + } + } + + // Resize window based on statusbar width + if (statusbar) { + statusbar.layoutRect({ w: self.layoutRect().innerW }).recalc(); + + width = statusbar.layoutRect().minW + layoutRect.deltaW; + if (width > layoutRect.w) { + x = layoutRect.x - Math.max(0, width - layoutRect.w); + self.layoutRect({ w: width, x: x }); + needsRecalc = true; + } + } + + // Recalc body and disable auto resize + if (needsRecalc) { + self.recalc(); + } + }, + + /** + * Initializes the current controls layout rect. + * This will be executed by the layout managers to determine the + * default minWidth/minHeight etc. + * + * @method initLayoutRect + * @return {Object} Layout rect instance. + */ + initLayoutRect: function () { + var self = this, layoutRect = self._super(), deltaH = 0, headEl; + + // Reserve vertical space for title + if (self.settings.title && !self._fullscreen) { + headEl = self.getEl('head'); + + var size = DomUtils.getSize(headEl); + + layoutRect.headerW = size.width; + layoutRect.headerH = size.height; + + deltaH += layoutRect.headerH; + } + + // Reserve vertical space for statusbar + if (self.statusbar) { + deltaH += self.statusbar.layoutRect().h; + } + + layoutRect.deltaH += deltaH; + layoutRect.minH += deltaH; + //layoutRect.innerH -= deltaH; + layoutRect.h += deltaH; + + var rect = DomUtils.getWindowSize(); + + layoutRect.x = self.settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2); + layoutRect.y = self.settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2); + + return layoutRect; + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix; + var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html; + + self.preRender(); + layout.preRender(self); + + if (settings.title) { + headerHtml = ( + '
    ' + + '
    ' + self.encode(settings.title) + '
    ' + + '
    ' + + '' + + '
    ' + ); + } + + if (settings.url) { + html = ''; + } + + if (typeof html == "undefined") { + html = layout.renderHtml(self); + } + + if (self.statusbar) { + footerHtml = self.statusbar.renderHtml(); + } + + return ( + '
    ' + + '
    ' + + headerHtml + + '
    ' + + html + + '
    ' + + footerHtml + + '
    ' + + '
    ' + ); + }, + + /** + * Switches the window fullscreen mode. + * + * @method fullscreen + * @param {Boolean} state True/false state. + * @return {tinymce.ui.Window} Current window instance. + */ + fullscreen: function (state) { + var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect; + + if (state != self._fullscreen) { + DomQuery(window).on('resize', function () { + var time; + + if (self._fullscreen) { + // Time the layout time if it's to slow use a timeout to not hog the CPU + if (!slowRendering) { + time = new Date().getTime(); + + var rect = DomUtils.getWindowSize(); + self.moveTo(0, 0).resizeTo(rect.w, rect.h); + + if ((new Date().getTime()) - time > 50) { + slowRendering = true; + } + } else { + if (!self._timer) { + self._timer = Delay.setTimeout(function () { + var rect = DomUtils.getWindowSize(); + self.moveTo(0, 0).resizeTo(rect.w, rect.h); + + self._timer = 0; + }, 50); + } + } + } + }); + + layoutRect = self.layoutRect(); + self._fullscreen = state; + + if (!state) { + self.borderBox = BoxUtils.parseBox(self.settings.border); + self.getEl('head').style.display = ''; + layoutRect.deltaH += layoutRect.headerH; + DomQuery([documentElement, document.body]).removeClass(prefix + 'fullscreen'); + self.classes.remove('fullscreen'); + self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h); + } else { + self._initial = { x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h }; + + self.borderBox = BoxUtils.parseBox('0'); + self.getEl('head').style.display = 'none'; + layoutRect.deltaH -= layoutRect.headerH + 2; + DomQuery([documentElement, document.body]).addClass(prefix + 'fullscreen'); + self.classes.add('fullscreen'); + + var rect = DomUtils.getWindowSize(); + self.moveTo(0, 0).resizeTo(rect.w, rect.h); + } + } + + return self.reflow(); + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this, startPos; + + setTimeout(function () { + self.classes.add('in'); + self.fire('open'); + }, 0); + + self._super(); + + if (self.statusbar) { + self.statusbar.postRender(); + } + + self.focus(); + + this.dragHelper = new DragHelper(self._id + '-dragh', { + start: function () { + startPos = { + x: self.layoutRect().x, + y: self.layoutRect().y + }; + }, + + drag: function (e) { + self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY); + } + }); + + self.on('submit', function (e) { + if (!e.isDefaultPrevented()) { + self.close(); + } + }); + + windows.push(self); + toggleFullScreenState(true); + }, + + /** + * Fires a submit event with the serialized form. + * + * @method submit + * @return {Object} Event arguments object. + */ + submit: function () { + return this.fire('submit', { data: this.toJSON() }); + }, + + /** + * Removes the current control from DOM and from UI collections. + * + * @method remove + * @return {tinymce.ui.Control} Current control instance. + */ + remove: function () { + var self = this, i; + + self.dragHelper.destroy(); + self._super(); + + if (self.statusbar) { + this.statusbar.remove(); + } + + toggleBodyFullScreenClasses(self.classPrefix, false); + + i = windows.length; + while (i--) { + if (windows[i] === self) { + windows.splice(i, 1); + } + } + + toggleFullScreenState(windows.length > 0); + }, + + /** + * Returns the contentWindow object of the iframe if it exists. + * + * @method getContentWindow + * @return {Window} window object or null. + */ + getContentWindow: function () { + var ifr = this.getEl().getElementsByTagName('iframe')[0]; + return ifr ? ifr.contentWindow : null; + } + }); + + handleWindowResize(); + + return Window; + } +); +/** + * MessageBox.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 create MessageBoxes like alerts/confirms etc. + * + * @class tinymce.ui.MessageBox + * @extends tinymce.ui.FloatPanel + */ +define( + 'tinymce.ui.MessageBox', + [ + 'global!document', + 'tinymce.ui.Window' + ], + function (document, Window) { + "use strict"; + + var MessageBox = Window.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + settings = { + border: 1, + padding: 20, + layout: 'flex', + pack: "center", + align: "center", + containerCls: 'panel', + autoScroll: true, + buttons: { type: "button", text: "Ok", action: "ok" }, + items: { + type: "label", + multiline: true, + maxWidth: 500, + maxHeight: 200 + } + }; + + this._super(settings); + }, + + Statics: { + /** + * Ok buttons constant. + * + * @static + * @final + * @field {Number} OK + */ + OK: 1, + + /** + * Ok/cancel buttons constant. + * + * @static + * @final + * @field {Number} OK_CANCEL + */ + OK_CANCEL: 2, + + /** + * yes/no buttons constant. + * + * @static + * @final + * @field {Number} YES_NO + */ + YES_NO: 3, + + /** + * yes/no/cancel buttons constant. + * + * @static + * @final + * @field {Number} YES_NO_CANCEL + */ + YES_NO_CANCEL: 4, + + /** + * Constructs a new message box and renders it to the body element. + * + * @static + * @method msgBox + * @param {Object} settings Name/value object with settings. + */ + msgBox: function (settings) { + var buttons, callback = settings.callback || function () { }; + + function createButton(text, status, primary) { + return { + type: "button", + text: text, + subtype: primary ? 'primary' : '', + onClick: function (e) { + e.control.parents()[1].close(); + callback(status); + } + }; + } + + switch (settings.buttons) { + case MessageBox.OK_CANCEL: + buttons = [ + createButton('Ok', true, true), + createButton('Cancel', false) + ]; + break; + + case MessageBox.YES_NO: + case MessageBox.YES_NO_CANCEL: + buttons = [ + createButton('Yes', 1, true), + createButton('No', 0) + ]; + + if (settings.buttons == MessageBox.YES_NO_CANCEL) { + buttons.push(createButton('Cancel', -1)); + } + break; + + default: + buttons = [ + createButton('Ok', true, true) + ]; + break; + } + + return new Window({ + padding: 20, + x: settings.x, + y: settings.y, + minWidth: 300, + minHeight: 100, + layout: "flex", + pack: "center", + align: "center", + buttons: buttons, + title: settings.title, + role: 'alertdialog', + items: { + type: "label", + multiline: true, + maxWidth: 500, + maxHeight: 200, + text: settings.text + }, + onPostRender: function () { + this.aria('describedby', this.items()[0]._id); + }, + onClose: settings.onClose, + onCancel: function () { + callback(false); + } + }).renderTo(document.body).reflow(); + }, + + /** + * Creates a new alert dialog. + * + * @method alert + * @param {Object} settings Settings for the alert dialog. + * @param {function} [callback] Callback to execute when the user makes a choice. + */ + alert: function (settings, callback) { + if (typeof settings == "string") { + settings = { text: settings }; + } + + settings.callback = callback; + return MessageBox.msgBox(settings); + }, + + /** + * Creates a new confirm dialog. + * + * @method confirm + * @param {Object} settings Settings for the confirm dialog. + * @param {function} [callback] Callback to execute when the user makes a choice. + */ + confirm: function (settings, callback) { + if (typeof settings == "string") { + settings = { text: settings }; + } + + settings.callback = callback; + settings.buttons = MessageBox.OK_CANCEL; + + return MessageBox.msgBox(settings); + } + } + }); + + return MessageBox; + } +); + +/** + * WindowManagerImpl.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.ui.WindowManagerImpl', + [ + "tinymce.ui.Window", + "tinymce.ui.MessageBox" + ], + function (Window, MessageBox) { + return function (editor) { + var open = function (args, params, closeCallback) { + var win; + + args.title = args.title || ' '; + + // Handle URL + args.url = args.url || args.file; // Legacy + if (args.url) { + args.width = parseInt(args.width || 320, 10); + args.height = parseInt(args.height || 240, 10); + } + + // Handle body + if (args.body) { + args.items = { + defaults: args.defaults, + type: args.bodyType || 'form', + items: args.body, + data: args.data, + callbacks: args.commands + }; + } + + if (!args.url && !args.buttons) { + args.buttons = [ + { + text: 'Ok', subtype: 'primary', onclick: function () { + win.find('form')[0].submit(); + } + }, + + { + text: 'Cancel', onclick: function () { + win.close(); + } + } + ]; + } + + win = new Window(args); + + win.on('close', function () { + closeCallback(win); + }); + + // Handle data + if (args.data) { + win.on('postRender', function () { + this.find('*').each(function (ctrl) { + var name = ctrl.name(); + + if (name in args.data) { + ctrl.value(args.data[name]); + } + }); + }); + } + + // store args and parameters + win.features = args || {}; + win.params = params || {}; + + win = win.renderTo().reflow(); + + return win; + }; + + var alert = function (message, choiceCallback, closeCallback) { + var win; + + win = MessageBox.alert(message, function () { + choiceCallback(); + }); + + win.on('close', function () { + closeCallback(win); + }); + + return win; + }; + + var confirm = function (message, choiceCallback, closeCallback) { + var win; + + win = MessageBox.confirm(message, function (state) { + choiceCallback(state); + }); + + win.on('close', function () { + closeCallback(win); + }); + + return win; + }; + + var close = function (window) { + window.close(); + }; + + var getParams = function (window) { + return window.params; + }; + + var setParams = function (window, params) { + window.params = params; + }; + + return { + open: open, + alert: alert, + confirm: confirm, + close: close, + getParams: getParams, + setParams: setParams + }; + }; + } +); + +/** + * ThemeApi.js + * + * Released under LGPL License. + * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +define( + 'tinymce.themes.modern.api.ThemeApi', + [ + 'tinymce.themes.modern.ui.Render', + 'tinymce.themes.modern.ui.Resize', + 'tinymce.ui.NotificationManagerImpl', + 'tinymce.ui.WindowManagerImpl' + ], + function (Render, Resize, NotificationManagerImpl, WindowManagerImpl) { + var get = function (editor) { + var renderUI = function (args) { + return Render.renderUI(editor, this, args); + }; + + var resizeTo = function (w, h) { + return Resize.resizeTo(editor, w, h); + }; + + var resizeBy = function (dw, dh) { + return Resize.resizeBy(editor, dw, dh); + }; + + var getNotificationManagerImpl = function () { + return NotificationManagerImpl(editor); + }; + + var getWindowManagerImpl = function () { + return WindowManagerImpl(editor); + }; + + return { + renderUI: renderUI, + resizeTo: resizeTo, + resizeBy: resizeBy, + getNotificationManagerImpl: getNotificationManagerImpl, + getWindowManagerImpl: getWindowManagerImpl + }; + }; + + return { + get: get + }; + } +); + +/** + * Layout.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 + */ + +/** + * Base layout manager class. + * + * @class tinymce.ui.Layout + */ +define( + 'tinymce.ui.Layout', + [ + "tinymce.core.util.Class", + "tinymce.core.util.Tools" + ], + function (Class, Tools) { + "use strict"; + + return Class.extend({ + Defaults: { + firstControlClass: 'first', + lastControlClass: 'last' + }, + + /** + * Constructs a layout instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + this.settings = Tools.extend({}, this.Defaults, settings); + }, + + /** + * This method gets invoked before the layout renders the controls. + * + * @method preRender + * @param {tinymce.ui.Container} container Container instance to preRender. + */ + preRender: function (container) { + container.bodyClasses.add(this.settings.containerClass); + }, + + /** + * Applies layout classes to the container. + * + * @private + */ + applyClasses: function (items) { + var self = this, settings = self.settings, firstClass, lastClass, firstItem, lastItem; + + firstClass = settings.firstControlClass; + lastClass = settings.lastControlClass; + + items.each(function (item) { + item.classes.remove(firstClass).remove(lastClass).add(settings.controlClass); + + if (item.visible()) { + if (!firstItem) { + firstItem = item; + } + + lastItem = item; + } + }); + + if (firstItem) { + firstItem.classes.add(firstClass); + } + + if (lastItem) { + lastItem.classes.add(lastClass); + } + }, + + /** + * Renders the specified container and any layout specific HTML. + * + * @method renderHtml + * @param {tinymce.ui.Container} container Container to render HTML for. + */ + renderHtml: function (container) { + var self = this, html = ''; + + self.applyClasses(container.items()); + + container.items().each(function (item) { + html += item.renderHtml(); + }); + + return html; + }, + + /** + * Recalculates the positions of the controls in the specified container. + * + * @method recalc + * @param {tinymce.ui.Container} container Container instance to recalc. + */ + recalc: function () { + }, + + /** + * This method gets invoked after the layout renders the controls. + * + * @method postRender + * @param {tinymce.ui.Container} container Container instance to postRender. + */ + postRender: function () { + }, + + isNative: function () { + return false; + } + }); + } +); +/** + * AbsoluteLayout.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 + */ + +/** + * LayoutManager for absolute positioning. This layout manager is more of + * a base class for other layouts but can be created and used directly. + * + * @-x-less AbsoluteLayout.less + * @class tinymce.ui.AbsoluteLayout + * @extends tinymce.ui.Layout + */ +define( + 'tinymce.ui.AbsoluteLayout', + [ + "tinymce.ui.Layout" + ], + function (Layout) { + "use strict"; + + return Layout.extend({ + Defaults: { + containerClass: 'abs-layout', + controlClass: 'abs-layout-item' + }, + + /** + * Recalculates the positions of the controls in the specified container. + * + * @method recalc + * @param {tinymce.ui.Container} container Container instance to recalc. + */ + recalc: function (container) { + container.items().filter(':visible').each(function (ctrl) { + var settings = ctrl.settings; + + ctrl.layoutRect({ + x: settings.x, + y: settings.y, + w: settings.w, + h: settings.h + }); + + if (ctrl.recalc) { + ctrl.recalc(); + } + }); + }, + + /** + * Renders the specified container and any layout specific HTML. + * + * @method renderHtml + * @param {tinymce.ui.Container} container Container to render HTML for. + */ + renderHtml: function (container) { + return '
    ' + this._super(container); + } + }); + } +); +/** + * Button.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 create buttons. You can create them directly or through the Factory. + * + * @example + * // Create and render a button to the body element + * tinymce.ui.Factory.create({ + * type: 'button', + * text: 'My button' + * }).renderTo(document.body); + * + * @-x-less Button.less + * @class tinymce.ui.Button + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Button', + [ + 'global!document', + 'global!window', + 'tinymce.ui.Widget' + ], + function (document, window, Widget) { + "use strict"; + + return Widget.extend({ + Defaults: { + classes: "widget btn", + role: "button" + }, + + /** + * Constructs a new button instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} size Size of the button small|medium|large. + * @setting {String} image Image to use for icon. + * @setting {String} icon Icon to use for button. + */ + init: function (settings) { + var self = this, size; + + self._super(settings); + settings = self.settings; + + size = self.settings.size; + + self.on('click mousedown', function (e) { + e.preventDefault(); + }); + + self.on('touchstart', function (e) { + self.fire('click', e); + e.preventDefault(); + }); + + if (settings.subtype) { + self.classes.add(settings.subtype); + } + + if (size) { + self.classes.add('btn-' + size); + } + + if (settings.icon) { + self.icon(settings.icon); + } + }, + + /** + * Sets/gets the current button icon. + * + * @method icon + * @param {String} [icon] New icon identifier. + * @return {String|tinymce.ui.MenuButton} Current icon or current MenuButton instance. + */ + icon: function (icon) { + if (!arguments.length) { + return this.state.get('icon'); + } + + this.state.set('icon', icon); + + return this; + }, + + /** + * Repaints the button for example after it's been resizes by a layout engine. + * + * @method repaint + */ + repaint: function () { + var btnElm = this.getEl().firstChild, + btnStyle; + + if (btnElm) { + btnStyle = btnElm.style; + btnStyle.width = btnStyle.height = "100%"; + } + + this._super(); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix; + var icon = self.state.get('icon'), image, text = self.state.get('text'), textHtml = ''; + + image = self.settings.image; + if (image) { + icon = 'none'; + + // Support for [high dpi, low dpi] image sources + if (typeof image != "string") { + image = window.getSelection ? image[0] : image[1]; + } + + image = ' style="background-image: url(\'' + image + '\')"'; + } else { + image = ''; + } + + if (text) { + self.classes.add('btn-has-text'); + textHtml = '' + self.encode(text) + ''; + } + + icon = icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; + + return ( + '
    ' + + '' + + '
    ' + ); + }, + + bindStates: function () { + var self = this, $ = self.$, textCls = self.classPrefix + 'txt'; + + function setButtonText(text) { + var $span = $('span.' + textCls, self.getEl()); + + if (text) { + if (!$span[0]) { + $('button:first', self.getEl()).append(''); + $span = $('span.' + textCls, self.getEl()); + } + + $span.html(self.encode(text)); + } else { + $span.remove(); + } + + self.classes.toggle('btn-has-text', !!text); + } + + self.state.on('change:text', function (e) { + setButtonText(e.value); + }); + + self.state.on('change:icon', function (e) { + var icon = e.value, prefix = self.classPrefix; + + self.settings.icon = icon; + icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; + + var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0]; + + if (icon) { + if (!iconElm || iconElm != btnElm.firstChild) { + iconElm = document.createElement('i'); + btnElm.insertBefore(iconElm, btnElm.firstChild); + } + + iconElm.className = icon; + } else if (iconElm) { + btnElm.removeChild(iconElm); + } + + setButtonText(self.state.get('text')); + }); + + return self._super(); + } + }); + } +); + +defineGlobal("global!RegExp", RegExp); +/** + * BrowseButton.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 new browse button. + * + * @-x-less BrowseButton.less + * @class tinymce.ui.BrowseButton + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.BrowseButton', + [ + 'tinymce.ui.Button', + 'tinymce.core.util.Tools', + 'tinymce.ui.DomUtils', + 'tinymce.core.dom.DomQuery', + 'global!RegExp' + ], + function (Button, Tools, DomUtils, $, RegExp) { + return Button.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} multiple True if the dropzone is a multiple control. + * @setting {Number} maxLength Max length for the dropzone. + * @setting {Number} size Size of the dropzone in characters. + */ + init: function (settings) { + var self = this; + + settings = Tools.extend({ + text: "Browse...", + multiple: false, + accept: null // by default accept any files + }, settings); + + self._super(settings); + + self.classes.add('browsebutton'); + + if (settings.multiple) { + self.classes.add('multiple'); + } + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + var input = DomUtils.create('input', { + type: 'file', + id: self._id + '-browse', + accept: self.settings.accept + }); + + self._super(); + + $(input).on('change', function (e) { + var files = e.target.files; + + self.value = function () { + if (!files.length) { + return null; + } else if (self.settings.multiple) { + return files; + } else { + return files[0]; + } + }; + + e.preventDefault(); + + if (files.length) { + self.fire('change', e); + } + }); + + // ui.Button prevents default on click, so we shouldn't let the click to propagate up to it + $(input).on('click', function (e) { + e.stopPropagation(); + }); + + $(self.getEl('button')).on('click', function (e) { + e.stopPropagation(); + input.click(); + }); + + // in newer browsers input doesn't have to be attached to dom to trigger browser dialog + // however older IE11 (< 11.1358.14393.0) still requires this + self.getEl().appendChild(input); + }, + + + remove: function () { + $(this.getEl('button')).off(); + $(this.getEl('input')).off(); + + this._super(); + } + }); + } +); + +/** + * ButtonGroup.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 control enables you to put multiple buttons into a group. This is + * useful when you want to combine similar toolbar buttons into a group. + * + * @example + * // Create and render a buttongroup with two buttons to the body element + * tinymce.ui.Factory.create({ + * type: 'buttongroup', + * items: [ + * {text: 'Button A'}, + * {text: 'Button B'} + * ] + * }).renderTo(document.body); + * + * @-x-less ButtonGroup.less + * @class tinymce.ui.ButtonGroup + * @extends tinymce.ui.Container + */ +define( + 'tinymce.ui.ButtonGroup', + [ + "tinymce.ui.Container" + ], + function (Container) { + "use strict"; + + return Container.extend({ + Defaults: { + defaultType: 'button', + role: 'group' + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout; + + self.classes.add('btn-group'); + self.preRender(); + layout.preRender(self); + + return ( + '
    ' + + '
    ' + + (self.settings.html || '') + layout.renderHtml(self) + + '
    ' + + '
    ' + ); + } + }); + } +); +/** + * Checkbox.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 control creates a custom checkbox. + * + * @example + * // Create and render a checkbox to the body element + * tinymce.core.ui.Factory.create({ + * type: 'checkbox', + * checked: true, + * text: 'My checkbox' + * }).renderTo(document.body); + * + * @-x-less Checkbox.less + * @class tinymce.ui.Checkbox + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Checkbox', + [ + 'global!document', + 'tinymce.ui.Widget' + ], + function (document, Widget) { + "use strict"; + + return Widget.extend({ + Defaults: { + classes: "checkbox", + role: "checkbox", + checked: false + }, + + /** + * Constructs a new Checkbox instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} checked True if the checkbox should be checked by default. + */ + init: function (settings) { + var self = this; + + self._super(settings); + + self.on('click mousedown', function (e) { + e.preventDefault(); + }); + + self.on('click', function (e) { + e.preventDefault(); + + if (!self.disabled()) { + self.checked(!self.checked()); + } + }); + + self.checked(self.settings.checked); + }, + + /** + * Getter/setter function for the checked state. + * + * @method checked + * @param {Boolean} [state] State to be set. + * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. + */ + checked: function (state) { + if (!arguments.length) { + return this.state.get('checked'); + } + + this.state.set('checked', state); + + return this; + }, + + /** + * Getter/setter function for the value state. + * + * @method value + * @param {Boolean} [state] State to be set. + * @return {Boolean|tinymce.ui.Checkbox} True/false or checkbox if it's a set operation. + */ + value: function (state) { + if (!arguments.length) { + return this.checked(); + } + + return this.checked(state); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix; + + return ( + '
    ' + + '' + + '' + self.encode(self.state.get('text')) + '' + + '
    ' + ); + }, + + bindStates: function () { + var self = this; + + function checked(state) { + self.classes.toggle("checked", state); + self.aria('checked', state); + } + + self.state.on('change:text', function (e) { + self.getEl('al').firstChild.data = self.translate(e.value); + }); + + self.state.on('change:checked change:value', function (e) { + self.fire('change'); + checked(e.value); + }); + + self.state.on('change:icon', function (e) { + var icon = e.value, prefix = self.classPrefix; + + if (typeof icon == 'undefined') { + return self.settings.icon; + } + + self.settings.icon = icon; + icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; + + var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0]; + + if (icon) { + if (!iconElm || iconElm != btnElm.firstChild) { + iconElm = document.createElement('i'); + btnElm.insertBefore(iconElm, btnElm.firstChild); + } + + iconElm.className = icon; + } else if (iconElm) { + btnElm.removeChild(iconElm); + } + }); + + if (self.state.get('checked')) { + checked(true); + } + + return self._super(); + } + }); + } +); +/** + * 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.util.VK', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.util.VK'); + } +); + +/** + * ComboBox.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 creates a combobox control. Select box that you select a value from or + * type a value into. + * + * @-x-less ComboBox.less + * @class tinymce.ui.ComboBox + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.ComboBox', + [ + 'global!document', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.ui.Factory', + 'tinymce.core.util.Tools', + 'tinymce.core.util.VK', + 'tinymce.ui.DomUtils', + 'tinymce.ui.Widget' + ], + function (document, DomQuery, Factory, Tools, VK, DomUtils, Widget) { + "use strict"; + + return Widget.extend({ + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} placeholder Placeholder text to display. + */ + init: function (settings) { + var self = this; + + self._super(settings); + settings = self.settings; + + self.classes.add('combobox'); + self.subinput = true; + self.ariaTarget = 'inp'; // TODO: Figure out a better way + + settings.menu = settings.menu || settings.values; + + if (settings.menu) { + settings.icon = 'caret'; + } + + self.on('click', function (e) { + var elm = e.target, root = self.getEl(); + + if (!DomQuery.contains(root, elm) && elm != root) { + return; + } + + while (elm && elm != root) { + if (elm.id && elm.id.indexOf('-open') != -1) { + self.fire('action'); + + if (settings.menu) { + self.showMenu(); + + if (e.aria) { + self.menu.items()[0].focus(); + } + } + } + + elm = elm.parentNode; + } + }); + + // TODO: Rework this + self.on('keydown', function (e) { + var rootControl; + + if (e.keyCode == 13 && e.target.nodeName === 'INPUT') { + e.preventDefault(); + + // Find root control that we can do toJSON on + self.parents().reverse().each(function (ctrl) { + if (ctrl.toJSON) { + rootControl = ctrl; + return false; + } + }); + + // Fire event on current text box with the serialized data of the whole form + self.fire('submit', { data: rootControl.toJSON() }); + } + }); + + self.on('keyup', function (e) { + if (e.target.nodeName == "INPUT") { + var oldValue = self.state.get('value'); + var newValue = e.target.value; + + if (newValue !== oldValue) { + self.state.set('value', newValue); + self.fire('autocomplete', e); + } + } + }); + + self.on('mouseover', function (e) { + var tooltip = self.tooltip().moveTo(-0xFFFF); + + if (self.statusLevel() && e.target.className.indexOf(self.classPrefix + 'status') !== -1) { + var statusMessage = self.statusMessage() || 'Ok'; + var rel = tooltip.text(statusMessage).show().testMoveRel(e.target, ['bc-tc', 'bc-tl', 'bc-tr']); + + tooltip.classes.toggle('tooltip-n', rel == 'bc-tc'); + tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl'); + tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr'); + + tooltip.moveRel(e.target, rel); + } + }); + }, + + statusLevel: function (value) { + if (arguments.length > 0) { + this.state.set('statusLevel', value); + } + + return this.state.get('statusLevel'); + }, + + statusMessage: function (value) { + if (arguments.length > 0) { + this.state.set('statusMessage', value); + } + + return this.state.get('statusMessage'); + }, + + showMenu: function () { + var self = this, settings = self.settings, menu; + + if (!self.menu) { + menu = settings.menu || []; + + // Is menu array then auto constuct menu control + if (menu.length) { + menu = { + type: 'menu', + items: menu + }; + } else { + menu.type = menu.type || 'menu'; + } + + self.menu = Factory.create(menu).parent(self).renderTo(self.getContainerElm()); + self.fire('createmenu'); + self.menu.reflow(); + self.menu.on('cancel', function (e) { + if (e.control === self.menu) { + self.focus(); + } + }); + + self.menu.on('show hide', function (e) { + e.control.items().each(function (ctrl) { + ctrl.active(ctrl.value() == self.value()); + }); + }).fire('show'); + + self.menu.on('select', function (e) { + self.value(e.control.value()); + }); + + self.on('focusin', function (e) { + if (e.target.tagName.toUpperCase() == 'INPUT') { + self.menu.hide(); + } + }); + + self.aria('expanded', true); + } + + self.menu.show(); + self.menu.layoutRect({ w: self.layoutRect().w }); + self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); + }, + + /** + * Focuses the input area of the control. + * + * @method focus + */ + focus: function () { + this.getEl('inp').focus(); + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this, elm = self.getEl(), openElm = self.getEl('open'), rect = self.layoutRect(); + var width, lineHeight, innerPadding = 0, inputElm = elm.firstChild; + + if (self.statusLevel() && self.statusLevel() !== 'none') { + innerPadding = ( + parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-right'), 10) - + parseInt(DomUtils.getRuntimeStyle(inputElm, 'padding-left'), 10) + ); + } + + if (openElm) { + width = rect.w - DomUtils.getSize(openElm).width - 10; + } else { + width = rect.w - 10; + } + + // Detect old IE 7+8 add lineHeight to align caret vertically in the middle + var doc = document; + if (doc.all && (!doc.documentMode || doc.documentMode <= 8)) { + lineHeight = (self.layoutRect().h - 2) + 'px'; + } + + DomQuery(inputElm).css({ + width: width - innerPadding, + lineHeight: lineHeight + }); + + self._super(); + + return self; + }, + + /** + * Post render method. Called after the control has been rendered to the target. + * + * @method postRender + * @return {tinymce.ui.ComboBox} Current combobox instance. + */ + postRender: function () { + var self = this; + + DomQuery(this.getEl('inp')).on('change', function (e) { + self.state.set('value', e.target.value); + self.fire('change', e); + }); + + return self._super(); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix; + var value = self.state.get('value') || ''; + var icon, text, openBtnHtml = '', extraAttrs = '', statusHtml = ''; + + if ("spellcheck" in settings) { + extraAttrs += ' spellcheck="' + settings.spellcheck + '"'; + } + + if (settings.maxLength) { + extraAttrs += ' maxlength="' + settings.maxLength + '"'; + } + + if (settings.size) { + extraAttrs += ' size="' + settings.size + '"'; + } + + if (settings.subtype) { + extraAttrs += ' type="' + settings.subtype + '"'; + } + + statusHtml = ''; + + if (self.disabled()) { + extraAttrs += ' disabled="disabled"'; + } + + icon = settings.icon; + if (icon && icon != 'caret') { + icon = prefix + 'ico ' + prefix + 'i-' + settings.icon; + } + + text = self.state.get('text'); + + if (icon || text) { + openBtnHtml = ( + '
    ' + + '' + + '
    ' + ); + + self.classes.add('has-open'); + } + + return ( + '
    ' + + '' + + statusHtml + + openBtnHtml + + '
    ' + ); + }, + + value: function (value) { + if (arguments.length) { + this.state.set('value', value); + return this; + } + + // Make sure the real state is in sync + if (this.state.get('rendered')) { + this.state.set('value', this.getEl('inp').value); + } + + return this.state.get('value'); + }, + + showAutoComplete: function (items, term) { + var self = this; + + if (items.length === 0) { + self.hideMenu(); + return; + } + + var insert = function (value, title) { + return function () { + self.fire('selectitem', { + title: title, + value: value + }); + }; + }; + + if (self.menu) { + self.menu.items().remove(); + } else { + self.menu = Factory.create({ + type: 'menu', + classes: 'combobox-menu', + layout: 'flow' + }).parent(self).renderTo(); + } + + Tools.each(items, function (item) { + self.menu.add({ + text: item.title, + url: item.previewUrl, + match: term, + classes: 'menu-item-ellipsis', + onclick: insert(item.value, item.title) + }); + }); + + self.menu.renderNew(); + self.hideMenu(); + + self.menu.on('cancel', function (e) { + if (e.control.parent() === self.menu) { + e.stopPropagation(); + self.focus(); + self.hideMenu(); + } + }); + + self.menu.on('select', function () { + self.focus(); + }); + + var maxW = self.layoutRect().w; + self.menu.layoutRect({ w: maxW, minW: 0, maxW: maxW }); + self.menu.reflow(); + self.menu.show(); + self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); + }, + + hideMenu: function () { + if (this.menu) { + this.menu.hide(); + } + }, + + bindStates: function () { + var self = this; + + self.state.on('change:value', function (e) { + if (self.getEl('inp').value != e.value) { + self.getEl('inp').value = e.value; + } + }); + + self.state.on('change:disabled', function (e) { + self.getEl('inp').disabled = e.value; + }); + + self.state.on('change:statusLevel', function (e) { + var statusIconElm = self.getEl('status'); + var prefix = self.classPrefix, value = e.value; + + DomUtils.css(statusIconElm, 'display', value === 'none' ? 'none' : ''); + DomUtils.toggleClass(statusIconElm, prefix + 'i-checkmark', value === 'ok'); + DomUtils.toggleClass(statusIconElm, prefix + 'i-warning', value === 'warn'); + DomUtils.toggleClass(statusIconElm, prefix + 'i-error', value === 'error'); + self.classes.toggle('has-status', value !== 'none'); + self.repaint(); + }); + + DomUtils.on(self.getEl('status'), 'mouseleave', function () { + self.tooltip().hide(); + }); + + self.on('cancel', function (e) { + if (self.menu && self.menu.visible()) { + e.stopPropagation(); + self.hideMenu(); + } + }); + + var focusIdx = function (idx, menu) { + if (menu && menu.items().length > 0) { + menu.items().eq(idx)[0].focus(); + } + }; + + self.on('keydown', function (e) { + var keyCode = e.keyCode; + + if (e.target.nodeName === 'INPUT') { + if (keyCode === VK.DOWN) { + e.preventDefault(); + self.fire('autocomplete'); + focusIdx(0, self.menu); + } else if (keyCode === VK.UP) { + e.preventDefault(); + focusIdx(-1, self.menu); + } + } + }); + + return self._super(); + }, + + remove: function () { + DomQuery(this.getEl('inp')).off(); + + if (this.menu) { + this.menu.remove(); + } + + this._super(); + } + }); + } +); +/** + * ColorBox.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 widget lets you enter colors and browse for colors by pressing the color button. It also displays + * a preview of the current color. + * + * @-x-less ColorBox.less + * @class tinymce.ui.ColorBox + * @extends tinymce.ui.ComboBox + */ +define( + 'tinymce.ui.ColorBox', + [ + "tinymce.ui.ComboBox" + ], + function (ComboBox) { + "use strict"; + + return ComboBox.extend({ + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + var self = this; + + settings.spellcheck = false; + + if (settings.onaction) { + settings.icon = 'none'; + } + + self._super(settings); + + self.classes.add('colorbox'); + self.on('change keyup postrender', function () { + self.repaintColor(self.value()); + }); + }, + + repaintColor: function (value) { + var openElm = this.getEl('open'); + var elm = openElm ? openElm.getElementsByTagName('i')[0] : null; + + if (elm) { + try { + elm.style.background = value; + } catch (ex) { + // Ignore + } + } + }, + + bindStates: function () { + var self = this; + + self.state.on('change:value', function (e) { + if (self.state.get('rendered')) { + self.repaintColor(e.value); + } + }); + + return self._super(); + } + }); + } +); +/** + * PanelButton.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 new panel button. + * + * @class tinymce.ui.PanelButton + * @extends tinymce.ui.Button + */ +define( + 'tinymce.ui.PanelButton', + [ + "tinymce.ui.Button", + "tinymce.ui.FloatPanel" + ], + function (Button, FloatPanel) { + "use strict"; + + return Button.extend({ + /** + * Shows the panel for the button. + * + * @method showPanel + */ + showPanel: function () { + var self = this, settings = self.settings; + + self.classes.add('opened'); + + if (!self.panel) { + var panelSettings = settings.panel; + + // Wrap panel in grid layout if type if specified + // This makes it possible to add forms or other containers directly in the panel option + if (panelSettings.type) { + panelSettings = { + layout: 'grid', + items: panelSettings + }; + } + + panelSettings.role = panelSettings.role || 'dialog'; + panelSettings.popover = true; + panelSettings.autohide = true; + panelSettings.ariaRoot = true; + + self.panel = new FloatPanel(panelSettings).on('hide', function () { + self.classes.remove('opened'); + }).on('cancel', function (e) { + e.stopPropagation(); + self.focus(); + self.hidePanel(); + }).parent(self).renderTo(self.getContainerElm()); + + self.panel.fire('show'); + self.panel.reflow(); + } else { + self.panel.show(); + } + + var rel = self.panel.testMoveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tc', 'bc-tl', 'bc-tr'] : ['bc-tc', 'bc-tr', 'bc-tl'])); + + self.panel.classes.toggle('start', rel === 'bc-tl'); + self.panel.classes.toggle('end', rel === 'bc-tr'); + + self.panel.moveRel(self.getEl(), rel); + }, + + /** + * Hides the panel for the button. + * + * @method hidePanel + */ + hidePanel: function () { + var self = this; + + if (self.panel) { + self.panel.hide(); + } + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + self.aria('haspopup', true); + + self.on('click', function (e) { + if (e.control === self) { + if (self.panel && self.panel.visible()) { + self.hidePanel(); + } else { + self.showPanel(); + self.panel.focus(!!e.aria); + } + } + }); + + return self._super(); + }, + + remove: function () { + if (this.panel) { + this.panel.remove(); + this.panel = null; + } + + return this._super(); + } + }); + } +); +/** + * ColorButton.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 creates a color button control. This is a split button in which the main + * button has a visual representation of the currently selected color. When clicked + * the caret button displays a color picker, allowing the user to select a new color. + * + * @-x-less ColorButton.less + * @class tinymce.ui.ColorButton + * @extends tinymce.ui.PanelButton + */ +define( + 'tinymce.ui.ColorButton', + [ + "tinymce.ui.PanelButton", + "tinymce.core.dom.DOMUtils" + ], + function (PanelButton, DomUtils) { + "use strict"; + + var DOM = DomUtils.DOM; + + return PanelButton.extend({ + /** + * Constructs a new ColorButton instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + this._super(settings); + this.classes.add('splitbtn'); + this.classes.add('colorbutton'); + }, + + /** + * Getter/setter for the current color. + * + * @method color + * @param {String} [color] Color to set. + * @return {String|tinymce.ui.ColorButton} Current color or current instance. + */ + color: function (color) { + if (color) { + this._color = color; + this.getEl('preview').style.backgroundColor = color; + return this; + } + + return this._color; + }, + + /** + * Resets the current color. + * + * @method resetColor + * @return {tinymce.ui.ColorButton} Current instance. + */ + resetColor: function () { + this._color = null; + this.getEl('preview').style.backgroundColor = null; + return this; + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix, text = self.state.get('text'); + var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; + var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '', + textHtml = ''; + + if (text) { + self.classes.add('btn-has-text'); + textHtml = '' + self.encode(text) + ''; + } + + return ( + '
    ' + + '' + + '' + + '
    ' + ); + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this, onClickHandler = self.settings.onclick; + + self.on('click', function (e) { + if (e.aria && e.aria.key === 'down') { + return; + } + + if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) { + e.stopImmediatePropagation(); + onClickHandler.call(self, e); + } + }); + + delete self.settings.onclick; + + return self._super(); + } + }); + } +); + +/** + * 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.util.Color', + [ + 'global!tinymce.util.Tools.resolve' + ], + function (resolve) { + return resolve('tinymce.util.Color'); + } +); + +/** + * ColorPicker.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 + */ + +/** + * Color picker widget lets you select colors. + * + * @-x-less ColorPicker.less + * @class tinymce.ui.ColorPicker + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.ColorPicker', + [ + "tinymce.ui.Widget", + "tinymce.ui.DragHelper", + "tinymce.ui.DomUtils", + "tinymce.core.util.Color" + ], + function (Widget, DragHelper, DomUtils, Color) { + "use strict"; + + return Widget.extend({ + Defaults: { + classes: "widget colorpicker" + }, + + /** + * Constructs a new colorpicker instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} color Initial color value. + */ + init: function (settings) { + this._super(settings); + }, + + postRender: function () { + var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm; + + hueRootElm = self.getEl('h'); + huePointElm = self.getEl('hp'); + svRootElm = self.getEl('sv'); + svPointElm = self.getEl('svp'); + + function getPos(elm, event) { + var pos = DomUtils.getPos(elm), x, y; + + x = event.pageX - pos.x; + y = event.pageY - pos.y; + + x = Math.max(0, Math.min(x / elm.clientWidth, 1)); + y = Math.max(0, Math.min(y / elm.clientHeight, 1)); + + return { + x: x, + y: y + }; + } + + function updateColor(hsv, hueUpdate) { + var hue = (360 - hsv.h) / 360; + + DomUtils.css(huePointElm, { + top: (hue * 100) + '%' + }); + + if (!hueUpdate) { + DomUtils.css(svPointElm, { + left: hsv.s + '%', + top: (100 - hsv.v) + '%' + }); + } + + svRootElm.style.background = new Color({ s: 100, v: 100, h: hsv.h }).toHex(); + self.color().parse({ s: hsv.s, v: hsv.v, h: hsv.h }); + } + + function updateSaturationAndValue(e) { + var pos; + + pos = getPos(svRootElm, e); + hsv.s = pos.x * 100; + hsv.v = (1 - pos.y) * 100; + + updateColor(hsv); + self.fire('change'); + } + + function updateHue(e) { + var pos; + + pos = getPos(hueRootElm, e); + hsv = color.toHsv(); + hsv.h = (1 - pos.y) * 360; + updateColor(hsv, true); + self.fire('change'); + } + + self._repaint = function () { + hsv = color.toHsv(); + updateColor(hsv); + }; + + self._super(); + + self._svdraghelper = new DragHelper(self._id + '-sv', { + start: updateSaturationAndValue, + drag: updateSaturationAndValue + }); + + self._hdraghelper = new DragHelper(self._id + '-h', { + start: updateHue, + drag: updateHue + }); + + self._repaint(); + }, + + rgb: function () { + return this.color().toRgb(); + }, + + value: function (value) { + var self = this; + + if (arguments.length) { + self.color().parse(value); + + if (self._rendered) { + self._repaint(); + } + } else { + return self.color().toHex(); + } + }, + + color: function () { + if (!this._color) { + this._color = new Color(); + } + + return this._color; + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix, hueHtml; + var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000'; + + function getOldIeFallbackHtml() { + var i, l, html = '', gradientPrefix, stopsList; + + gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='; + stopsList = stops.split(','); + for (i = 0, l = stopsList.length - 1; i < l; i++) { + html += ( + '
    ' + ); + } + + return html; + } + + var gradientCssText = ( + 'background: -ms-linear-gradient(top,' + stops + ');' + + 'background: linear-gradient(to bottom,' + stops + ');' + ); + + hueHtml = ( + '
    ' + + getOldIeFallbackHtml() + + '
    ' + + '
    ' + ); + + return ( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + hueHtml + + '
    ' + ); + } + }); + } +); +/** + * DropZone.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 new dropzone. + * + * @-x-less DropZone.less + * @class tinymce.ui.DropZone + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.DropZone', + [ + 'tinymce.ui.Widget', + 'tinymce.core.util.Tools', + 'tinymce.ui.DomUtils', + 'global!RegExp' + ], + function (Widget, Tools, DomUtils, RegExp) { + return Widget.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} multiple True if the dropzone is a multiple control. + * @setting {Number} maxLength Max length for the dropzone. + * @setting {Number} size Size of the dropzone in characters. + */ + init: function (settings) { + var self = this; + + settings = Tools.extend({ + height: 100, + text: "Drop an image here", + multiple: false, + accept: null // by default accept any files + }, settings); + + self._super(settings); + + self.classes.add('dropzone'); + + if (settings.multiple) { + self.classes.add('multiple'); + } + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, attrs, elm; + var cfg = self.settings; + + attrs = { + id: self._id, + hidefocus: '1' + }; + + elm = DomUtils.create('div', attrs, '' + this.translate(cfg.text) + ''); + + if (cfg.height) { + DomUtils.css(elm, 'height', cfg.height + 'px'); + } + + if (cfg.width) { + DomUtils.css(elm, 'width', cfg.width + 'px'); + } + + elm.className = self.classes; + + return elm.outerHTML; + }, + + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + var toggleDragClass = function (e) { + e.preventDefault(); + self.classes.toggle('dragenter'); + self.getEl().className = self.classes; + }; + + var filter = function (files) { + var accept = self.settings.accept; + if (typeof accept !== 'string') { + return files; + } + + var re = new RegExp('(' + accept.split(/\s*,\s*/).join('|') + ')$', 'i'); + return Tools.grep(files, function (file) { + return re.test(file.name); + }); + }; + + self._super(); + + self.$el.on('dragover', function (e) { + e.preventDefault(); + }); + + self.$el.on('dragenter', toggleDragClass); + self.$el.on('dragleave', toggleDragClass); + + self.$el.on('drop', function (e) { + e.preventDefault(); + + if (self.state.get('disabled')) { + return; + } + + var files = filter(e.dataTransfer.files); + + self.value = function () { + if (!files.length) { + return null; + } else if (self.settings.multiple) { + return files; + } else { + return files[0]; + } + }; + + if (files.length) { + self.fire('change', e); + } + }); + }, + + remove: function () { + this.$el.off(); + this._super(); + } + }); + } +); + +/** + * Path.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 new path control. + * + * @-x-less Path.less + * @class tinymce.ui.Path + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Path', + [ + "tinymce.ui.Widget" + ], + function (Widget) { + "use strict"; + + return Widget.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {String} delimiter Delimiter to display between row in path. + */ + init: function (settings) { + var self = this; + + if (!settings.delimiter) { + settings.delimiter = '\u00BB'; + } + + self._super(settings); + self.classes.add('path'); + self.canFocus = true; + + self.on('click', function (e) { + var index, target = e.target; + + if ((index = target.getAttribute('data-index'))) { + self.fire('select', { value: self.row()[index], index: index }); + } + }); + + self.row(self.settings.row); + }, + + /** + * Focuses the current control. + * + * @method focus + * @return {tinymce.ui.Control} Current control instance. + */ + focus: function () { + var self = this; + + self.getEl().firstChild.focus(); + + return self; + }, + + /** + * Sets/gets the data to be used for the path. + * + * @method row + * @param {Array} row Array with row name is rendered to path. + */ + row: function (row) { + if (!arguments.length) { + return this.state.get('row'); + } + + this.state.set('row', row); + + return this; + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this; + + return ( + '
    ' + + self._getDataPathHtml(self.state.get('row')) + + '
    ' + ); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:row', function (e) { + self.innerHtml(self._getDataPathHtml(e.value)); + }); + + return self._super(); + }, + + _getDataPathHtml: function (data) { + var self = this, parts = data || [], i, l, html = '', prefix = self.classPrefix; + + for (i = 0, l = parts.length; i < l; i++) { + html += ( + (i > 0 ? '' : '') + + '
    ' + parts[i].name + '
    ' + ); + } + + if (!html) { + html = '
    \u00a0
    '; + } + + return html; + } + }); + } +); + +/** + * ElementPath.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 control creates an path for the current selections parent elements in TinyMCE. + * + * @class tinymce.ui.ElementPath + * @extends tinymce.ui.Path + */ +define( + 'tinymce.ui.ElementPath', + [ + "tinymce.ui.Path" + ], + function (Path) { + return Path.extend({ + /** + * Post render method. Called after the control has been rendered to the target. + * + * @method postRender + * @return {tinymce.ui.ElementPath} Current combobox instance. + */ + postRender: function () { + var self = this, editor = self.settings.editor; + + function isHidden(elm) { + if (elm.nodeType === 1) { + if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) { + return true; + } + + if (elm.getAttribute('data-mce-type') === 'bookmark') { + return true; + } + } + + return false; + } + + if (editor.settings.elementpath !== false) { + self.on('select', function (e) { + editor.focus(); + editor.selection.select(this.row()[e.index].element); + editor.nodeChanged(); + }); + + editor.on('nodeChange', function (e) { + var outParents = [], parents = e.parents, i = parents.length; + + while (i--) { + if (parents[i].nodeType == 1 && !isHidden(parents[i])) { + var args = editor.fire('ResolveName', { + name: parents[i].nodeName.toLowerCase(), + target: parents[i] + }); + + if (!args.isDefaultPrevented()) { + outParents.push({ name: args.name, element: parents[i] }); + } + + if (args.isPropagationStopped()) { + break; + } + } + } + + self.row(outParents); + }); + } + + return self._super(); + } + }); + } +); +/** + * FormItem.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 a container created by the form element with + * a label and control item. + * + * @class tinymce.ui.FormItem + * @extends tinymce.ui.Container + * @setting {String} label Label to display for the form item. + */ +define( + 'tinymce.ui.FormItem', + [ + "tinymce.ui.Container" + ], + function (Container) { + "use strict"; + + return Container.extend({ + Defaults: { + layout: 'flex', + align: 'center', + defaults: { + flex: 1 + } + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout, prefix = self.classPrefix; + + self.classes.add('formitem'); + layout.preRender(self); + + return ( + '
    ' + + (self.settings.title ? ('
    ' + + self.settings.title + '
    ') : '') + + '
    ' + + (self.settings.html || '') + layout.renderHtml(self) + + '
    ' + + '
    ' + ); + } + }); + } +); +/** + * Form.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 creates a form container. A form container has the ability + * to automatically wrap items in tinymce.ui.FormItem instances. + * + * Each FormItem instance is a container for the label and the item. + * + * @example + * tinymce.core.ui.Factory.create({ + * type: 'form', + * items: [ + * {type: 'textbox', label: 'My text box'} + * ] + * }).renderTo(document.body); + * + * @class tinymce.ui.Form + * @extends tinymce.ui.Container + */ +define( + 'tinymce.ui.Form', + [ + "tinymce.ui.Container", + "tinymce.ui.FormItem", + "tinymce.core.util.Tools" + ], + function (Container, FormItem, Tools) { + "use strict"; + + return Container.extend({ + Defaults: { + containerCls: 'form', + layout: 'flex', + direction: 'column', + align: 'stretch', + flex: 1, + padding: 15, + labelGap: 30, + spacing: 10, + callbacks: { + submit: function () { + this.submit(); + } + } + }, + + /** + * This method gets invoked before the control is rendered. + * + * @method preRender + */ + preRender: function () { + var self = this, items = self.items(); + + if (!self.settings.formItemDefaults) { + self.settings.formItemDefaults = { + layout: 'flex', + autoResize: "overflow", + defaults: { flex: 1 } + }; + } + + // Wrap any labeled items in FormItems + items.each(function (ctrl) { + var formItem, label = ctrl.settings.label; + + if (label) { + formItem = new FormItem(Tools.extend({ + items: { + type: 'label', + id: ctrl._id + '-l', + text: label, + flex: 0, + forId: ctrl._id, + disabled: ctrl.disabled() + } + }, self.settings.formItemDefaults)); + + formItem.type = 'formitem'; + ctrl.aria('labelledby', ctrl._id + '-l'); + + if (typeof ctrl.settings.flex == "undefined") { + ctrl.settings.flex = 1; + } + + self.replace(ctrl, formItem); + formItem.add(ctrl); + } + }); + }, + + /** + * Fires a submit event with the serialized form. + * + * @method submit + * @return {Object} Event arguments object. + */ + submit: function () { + return this.fire('submit', { data: this.toJSON() }); + }, + + /** + * Post render method. Called after the control has been rendered to the target. + * + * @method postRender + * @return {tinymce.ui.ComboBox} Current combobox instance. + */ + postRender: function () { + var self = this; + + self._super(); + self.fromJSON(self.settings.data); + }, + + bindStates: function () { + var self = this; + + self._super(); + + function recalcLabels() { + var maxLabelWidth = 0, labels = [], i, labelGap, items; + + if (self.settings.labelGapCalc === false) { + return; + } + + if (self.settings.labelGapCalc == "children") { + items = self.find('formitem'); + } else { + items = self.items(); + } + + items.filter('formitem').each(function (item) { + var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth; + + maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth; + labels.push(labelCtrl); + }); + + labelGap = self.settings.labelGap || 0; + + i = labels.length; + while (i--) { + labels[i].settings.minWidth = maxLabelWidth + labelGap; + } + } + + self.on('show', recalcLabels); + recalcLabels(); + } + }); + } +); +/** + * FieldSet.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 creates fieldset containers. + * + * @-x-less FieldSet.less + * @class tinymce.ui.FieldSet + * @extends tinymce.ui.Form + */ +define( + 'tinymce.ui.FieldSet', + [ + "tinymce.ui.Form" + ], + function (Form) { + "use strict"; + + return Form.extend({ + Defaults: { + containerCls: 'fieldset', + layout: 'flex', + direction: 'column', + align: 'stretch', + flex: 1, + padding: "25 15 5 15", + labelGap: 30, + spacing: 10, + border: 1 + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout, prefix = self.classPrefix; + + self.preRender(); + layout.preRender(self); + + return ( + '
    ' + + (self.settings.title ? ('' + + self.settings.title + '') : '') + + '
    ' + + (self.settings.html || '') + layout.renderHtml(self) + + '
    ' + + '
    ' + ); + } + }); + } +); +defineGlobal("global!Date", Date); +defineGlobal("global!Math", Math); +define( + 'ephox.katamari.api.Id', + [ + 'global!Date', + 'global!Math', + 'global!String' + ], + + function (Date, Math, String) { + + /** + * Generate a unique identifier. + * + * The unique portion of the identifier only contains an underscore + * and digits, so that it may safely be used within HTML attributes. + * + * The chance of generating a non-unique identifier has been minimized + * by combining the current time, a random number and a one-up counter. + * + * generate :: String -> String + */ + var unique = 0; + + var generate = function (prefix) { + var date = new Date(); + var time = date.getTime(); + var random = Math.floor(Math.random() * 1000000000); + + unique++; + + return prefix + '_' + random + unique + String(time); + }; + + return { + generate: generate + }; + + } +); + +define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; }); +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.katamari.api.Thunk', + + [ + ], + + function () { + + var cached = function (f) { + var called = false; + var r; + return function() { + if (!called) { + called = true; + r = f.apply(null, arguments); + } + return r; + }; + }; + + return { + cached: cached + }; + } +); + +define( + 'ephox.sugar.api.node.NodeTypes', + + [ + + ], + + function () { + return { + ATTRIBUTE: 2, + CDATA_SECTION: 4, + COMMENT: 8, + DOCUMENT: 9, + DOCUMENT_TYPE: 10, + DOCUMENT_FRAGMENT: 11, + ELEMENT: 1, + TEXT: 3, + PROCESSING_INSTRUCTION: 7, + ENTITY_REFERENCE: 5, + ENTITY: 6, + NOTATION: 12 + }; + } +); +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.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.katamari.api.Type', + + [ + 'global!Array', + 'global!String' + ], + + function (Array, String) { + var typeOf = function(x) { + if (x === null) return 'null'; + var t = typeof x; + if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array'; + if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string'; + return t; + }; + + var isType = function (type) { + return function (value) { + return typeOf(value) === type; + }; + }; + + return { + isString: isType('string'), + isObject: isType('object'), + isArray: isType('array'), + isNull: isType('null'), + isBoolean: isType('boolean'), + isUndefined: isType('undefined'), + isFunction: isType('function'), + isNumber: isType('number') + }; + } +); + + +define( + 'ephox.katamari.data.Immutable', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'global!Array', + 'global!Error' + ], + + function (Arr, Fun, Array, Error) { + return function () { + var fields = arguments; + return function(/* values */) { + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var values = new Array(arguments.length); + for (var i = 0; i < values.length; i++) values[i] = arguments[i]; + + if (fields.length !== values.length) + throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments'); + + var struct = {}; + Arr.each(fields, function (name, i) { + struct[name] = Fun.constant(values[i]); + }); + return struct; + }; + }; + } +); + +define( + 'ephox.katamari.api.Obj', + + [ + 'ephox.katamari.api.Option', + 'global!Object' + ], + + function (Option, Object) { + // There are many variations of Object iteration that are faster than the 'for-in' style: + // http://jsperf.com/object-keys-iteration/107 + // + // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering + var keys = (function () { + var fastKeys = Object.keys; + + // This technically means that 'each' and 'find' on IE8 iterate through the object twice. + // This code doesn't run on IE8 much, so it's an acceptable tradeoff. + // If it becomes a problem we can always duplicate the feature detection inside each and find as well. + var slowKeys = function (o) { + var r = []; + for (var i in o) { + if (o.hasOwnProperty(i)) { + r.push(i); + } + } + return r; + }; + + return fastKeys === undefined ? slowKeys : fastKeys; + })(); + + + var each = function (obj, f) { + var props = keys(obj); + for (var k = 0, len = props.length; k < len; k++) { + var i = props[k]; + var x = obj[i]; + f(x, i, obj); + } + }; + + /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */ + var objectMap = function (obj, f) { + return tupleMap(obj, function (x, i, obj) { + return { + k: i, + v: f(x, i, obj) + }; + }); + }; + + /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */ + var tupleMap = function (obj, f) { + var r = {}; + each(obj, function (x, i) { + var tuple = f(x, i, obj); + r[tuple.k] = tuple.v; + }); + return r; + }; + + /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */ + var bifilter = function (obj, pred) { + var t = {}; + var f = {}; + each(obj, function(x, i) { + var branch = pred(x, i) ? t : f; + branch[i] = x; + }); + return { + t: t, + f: f + }; + }; + + /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */ + var mapToArray = function (obj, f) { + var r = []; + each(obj, function(value, name) { + r.push(f(value, name)); + }); + return r; + }; + + /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */ + var find = function (obj, pred) { + var props = keys(obj); + for (var k = 0, len = props.length; k < len; k++) { + var i = props[k]; + var x = obj[i]; + if (pred(x, i, obj)) { + return Option.some(x); + } + } + return Option.none(); + }; + + /** values :: JsObj(k, v) -> [v] */ + var values = function (obj) { + return mapToArray(obj, function (v) { + return v; + }); + }; + + var size = function (obj) { + return values(obj).length; + }; + + return { + bifilter: bifilter, + each: each, + map: objectMap, + mapToArray: mapToArray, + tupleMap: tupleMap, + find: find, + keys: keys, + values: values, + size: size + }; + } +); +define( + 'ephox.katamari.util.BagUtils', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Type', + 'global!Error' + ], + + function (Arr, Type, Error) { + var sort = function (arr) { + return arr.slice(0).sort(); + }; + + var reqMessage = function (required, keys) { + throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.'); + }; + + var unsuppMessage = function (unsupported) { + throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', ')); + }; + + var validateStrArr = function (label, array) { + if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.'); + Arr.each(array, function (a) { + if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.'); + }); + }; + + var invalidTypeMessage = function (incorrect, type) { + throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.'); + }; + + var checkDupes = function (everything) { + var sorted = sort(everything); + var dupe = Arr.find(sorted, function (s, i) { + return i < sorted.length -1 && s === sorted[i + 1]; + }); + + dupe.each(function (d) { + throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].'); + }); + }; + + return { + sort: sort, + reqMessage: reqMessage, + unsuppMessage: unsuppMessage, + validateStrArr: validateStrArr, + invalidTypeMessage: invalidTypeMessage, + checkDupes: checkDupes + }; + } +); +define( + 'ephox.katamari.data.MixedBag', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.util.BagUtils', + 'global!Error', + 'global!Object' + ], + + function (Arr, Fun, Obj, Option, BagUtils, Error, Object) { + + return function (required, optional) { + var everything = required.concat(optional); + if (everything.length === 0) throw new Error('You must specify at least one required or optional field.'); + + BagUtils.validateStrArr('required', required); + BagUtils.validateStrArr('optional', optional); + + BagUtils.checkDupes(everything); + + return function (obj) { + var keys = Obj.keys(obj); + + // Ensure all required keys are present. + var allReqd = Arr.forall(required, function (req) { + return Arr.contains(keys, req); + }); + + if (! allReqd) BagUtils.reqMessage(required, keys); + + var unsupported = Arr.filter(keys, function (key) { + return !Arr.contains(everything, key); + }); + + if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); + + var r = {}; + Arr.each(required, function (req) { + r[req] = Fun.constant(obj[req]); + }); + + Arr.each(optional, function (opt) { + r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none()); + }); + + return r; + }; + }; + } +); +define( + 'ephox.katamari.api.Struct', + + [ + 'ephox.katamari.data.Immutable', + 'ephox.katamari.data.MixedBag' + ], + + function (Immutable, MixedBag) { + return { + immutable: Immutable, + immutableBag: MixedBag + }; + } +); + +define( + 'ephox.sugar.alien.Recurse', + + [ + + ], + + function () { + /** + * Applies f repeatedly until it completes (by returning Option.none()). + * + * Normally would just use recursion, but JavaScript lacks tail call optimisation. + * + * This is what recursion looks like when manually unravelled :) + */ + var toArray = function (target, f) { + var r = []; + + var recurse = function (e) { + r.push(e); + return f(e); + }; + + var cur = f(target); + do { + cur = cur.bind(recurse); + } while (cur.isSome()); + + return r; + }; + + return { + toArray: toArray + }; + } +); +define( + 'ephox.katamari.api.Global', + + [ + ], + + function () { + // Use window object as the global if it's available since CSP will block script evals + if (typeof window !== 'undefined') { + return window; + } else { + return Function('return this;')(); + } + } +); + + +define( + 'ephox.katamari.api.Resolve', + + [ + 'ephox.katamari.api.Global' + ], + + function (Global) { + /** path :: ([String], JsObj?) -> JsObj */ + var path = function (parts, scope) { + var o = scope !== undefined ? scope : Global; + for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) + o = o[parts[i]]; + return o; + }; + + /** resolve :: (String, JsObj?) -> JsObj */ + var resolve = function (p, scope) { + var parts = p.split('.'); + return path(parts, scope); + }; + + /** step :: (JsObj, String) -> JsObj */ + var step = function (o, part) { + if (o[part] === undefined || o[part] === null) + o[part] = {}; + return o[part]; + }; + + /** forge :: ([String], JsObj?) -> JsObj */ + var forge = function (parts, target) { + var o = target !== undefined ? target : Global; + for (var i = 0; i < parts.length; ++i) + o = step(o, parts[i]); + return o; + }; + + /** namespace :: (String, JsObj?) -> JsObj */ + var namespace = function (name, target) { + var parts = name.split('.'); + return forge(parts, target); + }; + + return { + path: path, + resolve: resolve, + forge: forge, + namespace: namespace + }; + } +); + + +define( + 'ephox.sand.util.Global', + + [ + 'ephox.katamari.api.Resolve' + ], + + function (Resolve) { + var unsafe = function (name, scope) { + return Resolve.resolve(name, scope); + }; + + var getOrDie = function (name, scope) { + var actual = unsafe(name, scope); + + if (actual === undefined) throw name + ' not available on this browser'; + return actual; + }; + + return { + getOrDie: getOrDie + }; + } +); +define( + 'ephox.sand.api.Node', + + [ + 'ephox.sand.util.Global' + ], + + function (Global) { + /* + * MDN says (yes) for IE, but it's undefined on IE8 + */ + var node = function () { + var f = Global.getOrDie('Node'); + return f; + }; + + /* + * Most of numerosity doesn't alter the methods on the object. + * We're making an exception for Node, because bitwise and is so easy to get wrong. + * + * Might be nice to ADT this at some point instead of having individual methods. + */ + + var compareDocumentPosition = function (a, b, match) { + // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions + // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation + // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition + return (a.compareDocumentPosition(b) & match) !== 0; + }; + + var documentPositionPreceding = function (a, b) { + return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING); + }; + + var documentPositionContainedBy = function (a, b) { + return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY); + }; + + return { + documentPositionPreceding: documentPositionPreceding, + documentPositionContainedBy: documentPositionContainedBy + }; + } +); +defineGlobal("global!Number", Number); +define( + 'ephox.sand.detect.Version', + + [ + 'ephox.katamari.api.Arr', + 'global!Number', + 'global!String' + ], + + function (Arr, Number, String) { + var firstMatch = function (regexes, s) { + for (var i = 0; i < regexes.length; i++) { + var x = regexes[i]; + if (x.test(s)) return x; + } + return undefined; + }; + + var find = function (regexes, agent) { + var r = firstMatch(regexes, agent); + if (!r) return { major : 0, minor : 0 }; + var group = function(i) { + return Number(agent.replace(r, '$' + i)); + }; + return nu(group(1), group(2)); + }; + + var detect = function (versionRegexes, agent) { + var cleanedAgent = String(agent).toLowerCase(); + + if (versionRegexes.length === 0) return unknown(); + return find(versionRegexes, cleanedAgent); + }; + + var unknown = function () { + return nu(0, 0); + }; + + var nu = function (major, minor) { + return { major: major, minor: minor }; + }; + + return { + nu: nu, + detect: detect, + unknown: unknown + }; + } +); +define( + 'ephox.sand.core.Browser', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sand.detect.Version' + ], + + function (Fun, Version) { + var edge = 'Edge'; + var chrome = 'Chrome'; + var ie = 'IE'; + var opera = 'Opera'; + var firefox = 'Firefox'; + var safari = 'Safari'; + + var isBrowser = function (name, current) { + return function () { + return current === name; + }; + }; + + var unknown = function () { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; + + var nu = function (info) { + var current = info.current; + var version = info.version; + + return { + current: current, + version: version, + + // INVESTIGATE: Rename to Edge ? + isEdge: isBrowser(edge, current), + isChrome: isBrowser(chrome, current), + // NOTE: isIe just looks too weird + isIE: isBrowser(ie, current), + isOpera: isBrowser(opera, current), + isFirefox: isBrowser(firefox, current), + isSafari: isBrowser(safari, current) + }; + }; + + return { + unknown: unknown, + nu: nu, + edge: Fun.constant(edge), + chrome: Fun.constant(chrome), + ie: Fun.constant(ie), + opera: Fun.constant(opera), + firefox: Fun.constant(firefox), + safari: Fun.constant(safari) + }; + } +); +define( + 'ephox.sand.core.OperatingSystem', + + [ + 'ephox.katamari.api.Fun', + 'ephox.sand.detect.Version' + ], + + function (Fun, Version) { + var windows = 'Windows'; + var ios = 'iOS'; + var android = 'Android'; + var linux = 'Linux'; + var osx = 'OSX'; + var solaris = 'Solaris'; + var freebsd = 'FreeBSD'; + + // Though there is a bit of dupe with this and Browser, trying to + // reuse code makes it much harder to follow and change. + var isOS = function (name, current) { + return function () { + return current === name; + }; + }; + + var unknown = function () { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; + + var nu = function (info) { + var current = info.current; + var version = info.version; + + return { + current: current, + version: version, + + isWindows: isOS(windows, current), + // TODO: Fix capitalisation + isiOS: isOS(ios, current), + isAndroid: isOS(android, current), + isOSX: isOS(osx, current), + isLinux: isOS(linux, current), + isSolaris: isOS(solaris, current), + isFreeBSD: isOS(freebsd, current) + }; + }; + + return { + unknown: unknown, + nu: nu, + + windows: Fun.constant(windows), + ios: Fun.constant(ios), + android: Fun.constant(android), + linux: Fun.constant(linux), + osx: Fun.constant(osx), + solaris: Fun.constant(solaris), + freebsd: Fun.constant(freebsd) + }; + } +); +define( + 'ephox.sand.detect.DeviceType', + + [ + 'ephox.katamari.api.Fun' + ], + + function (Fun) { + return function (os, browser, userAgent) { + var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; + var isiPhone = os.isiOS() && !isiPad; + var isAndroid3 = os.isAndroid() && os.version.major === 3; + var isAndroid4 = os.isAndroid() && os.version.major === 4; + var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true ); + var isTouch = os.isiOS() || os.isAndroid(); + var isPhone = isTouch && !isTablet; + + var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; + + return { + isiPad : Fun.constant(isiPad), + isiPhone: Fun.constant(isiPhone), + isTablet: Fun.constant(isTablet), + isPhone: Fun.constant(isPhone), + isTouch: Fun.constant(isTouch), + isAndroid: os.isAndroid, + isiOS: os.isiOS, + isWebView: Fun.constant(iOSwebview) + }; + }; + } +); +define( + 'ephox.sand.detect.UaString', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sand.detect.Version', + 'global!String' + ], + + function (Arr, Version, String) { + var detect = function (candidates, userAgent) { + var agent = String(userAgent).toLowerCase(); + return Arr.find(candidates, function (candidate) { + return candidate.search(agent); + }); + }; + + // They (browser and os) are the same at the moment, but they might + // not stay that way. + var detectBrowser = function (browsers, userAgent) { + return detect(browsers, userAgent).map(function (browser) { + var version = Version.detect(browser.versionRegexes, userAgent); + return { + current: browser.name, + version: version + }; + }); + }; + + var detectOs = function (oses, userAgent) { + return detect(oses, userAgent).map(function (os) { + var version = Version.detect(os.versionRegexes, userAgent); + return { + current: os.name, + version: version + }; + }); + }; + + return { + detectBrowser: detectBrowser, + detectOs: detectOs + }; + } +); +define( + 'ephox.katamari.str.StrAppend', + + [ + + ], + + function () { + var addToStart = function (str, prefix) { + return prefix + str; + }; + + var addToEnd = function (str, suffix) { + return str + suffix; + }; + + var removeFromStart = function (str, numChars) { + return str.substring(numChars); + }; + + var removeFromEnd = function (str, numChars) { + return str.substring(0, str.length - numChars); + }; + + return { + addToStart: addToStart, + addToEnd: addToEnd, + removeFromStart: removeFromStart, + removeFromEnd: removeFromEnd + }; + } +); +define( + 'ephox.katamari.str.StringParts', + + [ + 'ephox.katamari.api.Option', + 'global!Error' + ], + + function (Option, Error) { + /** Return the first 'count' letters from 'str'. +- * e.g. first("abcde", 2) === "ab" +- */ + var first = function(str, count) { + return str.substr(0, count); + }; + + /** Return the last 'count' letters from 'str'. + * e.g. last("abcde", 2) === "de" + */ + var last = function(str, count) { + return str.substr(str.length - count, str.length); + }; + + var head = function(str) { + return str === '' ? Option.none() : Option.some(str.substr(0, 1)); + }; + + var tail = function(str) { + return str === '' ? Option.none() : Option.some(str.substring(1)); + }; + + return { + first: first, + last: last, + head: head, + tail: tail + }; + } +); +define( + 'ephox.katamari.api.Strings', + + [ + 'ephox.katamari.str.StrAppend', + 'ephox.katamari.str.StringParts', + 'global!Error' + ], + + function (StrAppend, StringParts, Error) { + var checkRange = function(str, substr, start) { + if (substr === '') return true; + if (str.length < substr.length) return false; + var x = str.substr(start, start + substr.length); + return x === substr; + }; + + /** Given a string and object, perform template-replacements on the string, as specified by the object. + * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"] + * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format. + */ + var supplant = function(str, obj) { + var isStringOrNumber = function(a) { + var t = typeof a; + return t === 'string' || t === 'number'; + }; + + return str.replace(/\${([^{}]*)}/g, + function (a, b) { + var value = obj[b]; + return isStringOrNumber(value) ? value : a; + } + ); + }; + + var removeLeading = function (str, prefix) { + return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str; + }; + + var removeTrailing = function (str, prefix) { + return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str; + }; + + var ensureLeading = function (str, prefix) { + return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix); + }; + + var ensureTrailing = function (str, prefix) { + return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix); + }; + + var contains = function(str, substr) { + return str.indexOf(substr) !== -1; + }; + + var capitalize = function(str) { + return StringParts.head(str).bind(function (head) { + return StringParts.tail(str).map(function (tail) { + return head.toUpperCase() + tail; + }); + }).getOr(str); + }; + + /** Does 'str' start with 'prefix'? + * Note: all strings start with the empty string. + * More formally, for all strings x, startsWith(x, ""). + * This is so that for all strings x and y, startsWith(y + x, y) + */ + var startsWith = function(str, prefix) { + return checkRange(str, prefix, 0); + }; + + /** Does 'str' end with 'suffix'? + * Note: all strings end with the empty string. + * More formally, for all strings x, endsWith(x, ""). + * This is so that for all strings x and y, endsWith(x + y, y) + */ + var endsWith = function(str, suffix) { + return checkRange(str, suffix, str.length - suffix.length); + }; + + + /** removes all leading and trailing spaces */ + var trim = function(str) { + return str.replace(/^\s+|\s+$/g, ''); + }; + + var lTrim = function(str) { + return str.replace(/^\s+/g, ''); + }; + + var rTrim = function(str) { + return str.replace(/\s+$/g, ''); + }; + + return { + supplant: supplant, + startsWith: startsWith, + removeLeading: removeLeading, + removeTrailing: removeTrailing, + ensureLeading: ensureLeading, + ensureTrailing: ensureTrailing, + endsWith: endsWith, + contains: contains, + trim: trim, + lTrim: lTrim, + rTrim: rTrim, + capitalize: capitalize + }; + } +); + +define( + 'ephox.sand.info.PlatformInfo', + + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Strings' + ], + + function (Fun, Strings) { + var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; + + var checkContains = function (target) { + return function (uastring) { + return Strings.contains(uastring, target); + }; + }; + + var browsers = [ + { + name : 'Edge', + versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], + search: function (uastring) { + var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit'); + return monstrosity; + } + }, + { + name : 'Chrome', + versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], + search : function (uastring) { + return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe'); + } + }, + { + name : 'IE', + versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], + search: function (uastring) { + return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident'); + } + }, + // INVESTIGATE: Is this still the Opera user agent? + { + name : 'Opera', + versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], + search : checkContains('opera') + }, + { + name : 'Firefox', + versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], + search : checkContains('firefox') + }, + { + name : 'Safari', + versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], + search : function (uastring) { + return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit'); + } + } + ]; + + var oses = [ + { + name : 'Windows', + search : checkContains('win'), + versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name : 'iOS', + search : function (uastring) { + return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad'); + }, + versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] + }, + { + name : 'Android', + search : checkContains('android'), + versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name : 'OSX', + search : checkContains('os x'), + versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/] + }, + { + name : 'Linux', + search : checkContains('linux'), + versionRegexes: [ ] + }, + { name : 'Solaris', + search : checkContains('sunos'), + versionRegexes: [ ] + }, + { + name : 'FreeBSD', + search : checkContains('freebsd'), + versionRegexes: [ ] + } + ]; + + return { + browsers: Fun.constant(browsers), + oses: Fun.constant(oses) + }; + } +); +define( + 'ephox.sand.core.PlatformDetection', + + [ + 'ephox.sand.core.Browser', + 'ephox.sand.core.OperatingSystem', + 'ephox.sand.detect.DeviceType', + 'ephox.sand.detect.UaString', + 'ephox.sand.info.PlatformInfo' + ], + + function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) { + var detect = function (userAgent) { + var browsers = PlatformInfo.browsers(); + var oses = PlatformInfo.oses(); + + var browser = UaString.detectBrowser(browsers, userAgent).fold( + Browser.unknown, + Browser.nu + ); + var os = UaString.detectOs(oses, userAgent).fold( + OperatingSystem.unknown, + OperatingSystem.nu + ); + var deviceType = DeviceType(os, browser, userAgent); + + return { + browser: browser, + os: os, + deviceType: deviceType + }; + }; + + return { + detect: detect + }; + } +); +defineGlobal("global!navigator", navigator); +define( + 'ephox.sand.api.PlatformDetection', + + [ + 'ephox.katamari.api.Thunk', + 'ephox.sand.core.PlatformDetection', + 'global!navigator' + ], + + function (Thunk, PlatformDetection, navigator) { + var detect = Thunk.cached(function () { + var userAgent = navigator.userAgent; + return PlatformDetection.detect(userAgent); + }); + + return { + detect: detect + }; + } +); +define( + 'ephox.sugar.api.search.Selectors', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.NodeTypes', + 'global!Error', + 'global!document' + ], + + function (Arr, Option, Element, NodeTypes, Error, document) { + /* + * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons, + * instead of doing object lookup feature detection on every call + */ + var STANDARD = 0; + var MSSTANDARD = 1; + var WEBKITSTANDARD = 2; + var FIREFOXSTANDARD = 3; + + var selectorType = (function () { + var test = document.createElement('span'); + // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. + // Still check for the others, but do it last. + return test.matches !== undefined ? STANDARD : + test.msMatchesSelector !== undefined ? MSSTANDARD : + test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD : + test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD : + -1; + })(); + + + var ELEMENT = NodeTypes.ELEMENT; + var DOCUMENT = NodeTypes.DOCUMENT; + + var is = function (element, selector) { + var elem = element.dom(); + if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches + + // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. + // Still check for the others, but do it last. + else if (selectorType === STANDARD) return elem.matches(selector); + else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector); + else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector); + else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector); + else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :( + }; + + var bypassSelector = function (dom) { + // Only elements and documents support querySelector + return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT || + // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ + dom.childElementCount === 0; + }; + + var all = function (selector, scope) { + var base = scope === undefined ? document : scope.dom(); + return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom); + }; + + var one = function (selector, scope) { + var base = scope === undefined ? document : scope.dom(); + return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom); + }; + + return { + all: all, + is: is, + one: one + }; + } +); + +define( + 'ephox.sugar.api.dom.Compare', + + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.Node', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.search.Selectors' + ], + + function (Arr, Fun, Node, PlatformDetection, Selectors) { + + var eq = function (e1, e2) { + return e1.dom() === e2.dom(); + }; + + var isEqualNode = function (e1, e2) { + return e1.dom().isEqualNode(e2.dom()); + }; + + var member = function (element, elements) { + return Arr.exists(elements, Fun.curry(eq, element)); + }; + + // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself). + var regularContains = function (e1, e2) { + var d1 = e1.dom(), d2 = e2.dom(); + return d1 === d2 ? false : d1.contains(d2); + }; + + var ieContains = function (e1, e2) { + // IE only implements the contains() method for Element nodes. + // It fails for Text nodes, so implement it using compareDocumentPosition() + // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect + // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1': + // Also, compareDocumentPosition defines a node containing itself as false. + return Node.documentPositionContainedBy(e1.dom(), e2.dom()); + }; + + var browser = PlatformDetection.detect().browser; + + // Returns: true if node e1 contains e2, otherwise false. + // (returns false if e1===e2: A node does not contain itself). + var contains = browser.isIE() ? ieContains : regularContains; + + return { + eq: eq, + isEqualNode: isEqualNode, + member: member, + contains: contains, + + // Only used by DomUniverse. Remove (or should Selectors.is move here?) + is: Selectors.is + }; + } +); + +define( + 'ephox.sugar.api.search.Traverse', + + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Struct', + 'ephox.sugar.alien.Recurse', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element' + ], + + function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) { + // The document associated with the current element + var owner = function (element) { + return Element.fromDom(element.dom().ownerDocument); + }; + + var documentElement = function (element) { + // TODO: Avoid unnecessary wrap/unwrap here + var doc = owner(element); + return Element.fromDom(doc.dom().documentElement); + }; + + // The window element associated with the element + var defaultView = function (element) { + var el = element.dom(); + var defaultView = el.ownerDocument.defaultView; + return Element.fromDom(defaultView); + }; + + var parent = function (element) { + var dom = element.dom(); + return Option.from(dom.parentNode).map(Element.fromDom); + }; + + var findIndex = function (element) { + return parent(element).bind(function (p) { + // TODO: Refactor out children so we can avoid the constant unwrapping + var kin = children(p); + return Arr.findIndex(kin, function (elem) { + return Compare.eq(element, elem); + }); + }); + }; + + var parents = function (element, isRoot) { + var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); + + // This is used a *lot* so it needs to be performant, not recursive + var dom = element.dom(); + var ret = []; + + while (dom.parentNode !== null && dom.parentNode !== undefined) { + var rawParent = dom.parentNode; + var parent = Element.fromDom(rawParent); + ret.push(parent); + + if (stop(parent) === true) break; + else dom = rawParent; + } + return ret; + }; + + var siblings = function (element) { + // TODO: Refactor out children so we can just not add self instead of filtering afterwards + var filterSelf = function (elements) { + return Arr.filter(elements, function (x) { + return !Compare.eq(element, x); + }); + }; + + return parent(element).map(children).map(filterSelf).getOr([]); + }; + + var offsetParent = function (element) { + var dom = element.dom(); + return Option.from(dom.offsetParent).map(Element.fromDom); + }; + + var prevSibling = function (element) { + var dom = element.dom(); + return Option.from(dom.previousSibling).map(Element.fromDom); + }; + + var nextSibling = function (element) { + var dom = element.dom(); + return Option.from(dom.nextSibling).map(Element.fromDom); + }; + + var prevSiblings = function (element) { + // This one needs to be reversed, so they're still in DOM order + return Arr.reverse(Recurse.toArray(element, prevSibling)); + }; + + var nextSiblings = function (element) { + return Recurse.toArray(element, nextSibling); + }; + + var children = function (element) { + var dom = element.dom(); + return Arr.map(dom.childNodes, Element.fromDom); + }; + + var child = function (element, index) { + var children = element.dom().childNodes; + return Option.from(children[index]).map(Element.fromDom); + }; + + var firstChild = function (element) { + return child(element, 0); + }; + + var lastChild = function (element) { + return child(element, element.dom().childNodes.length - 1); + }; + + var childNodesCount = function (element, index) { + return element.dom().childNodes.length; + }; + + var spot = Struct.immutable('element', 'offset'); + var leaf = function (element, offset) { + var cs = children(element); + return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); + }; + + return { + owner: owner, + defaultView: defaultView, + documentElement: documentElement, + parent: parent, + findIndex: findIndex, + parents: parents, + siblings: siblings, + prevSibling: prevSibling, + offsetParent: offsetParent, + prevSiblings: prevSiblings, + nextSibling: nextSibling, + nextSiblings: nextSiblings, + children: children, + child: child, + firstChild: firstChild, + lastChild: lastChild, + childNodesCount: childNodesCount, + leaf: leaf + }; + } +); + +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 + }; + } +); + +/** + * LinkTargets.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 is enables you to get anything that you can link to in a element. + * + * @private + * @class tinymce.ui.LinkTargets + */ +define( + 'tinymce.ui.content.LinkTargets', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Id', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.SelectorFilter', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.util.Tools' + ], + function (Arr, Fun, Id, Element, SelectorFilter, DOMUtils, Tools) { + var trim = Tools.trim; + var hasContentEditableState = function (value) { + return function (node) { + if (node && node.nodeType === 1) { + if (node.contentEditable === value) { + return true; + } + + if (node.getAttribute('data-mce-contenteditable') === value) { + return true; + } + } + + return false; + }; + }; + + var isContentEditableTrue = hasContentEditableState('true'); + var isContentEditableFalse = hasContentEditableState('false'); + + var create = function (type, title, url, level, attach) { + return { + type: type, + title: title, + url: url, + level: level, + attach: attach + }; + }; + + var isChildOfContentEditableTrue = function (node) { + while ((node = node.parentNode)) { + var value = node.contentEditable; + if (value && value !== 'inherit') { + return isContentEditableTrue(node); + } + } + + return false; + }; + + var select = function (selector, root) { + return Arr.map(SelectorFilter.descendants(Element.fromDom(root), selector), function (element) { + return element.dom(); + }); + }; + + var getElementText = function (elm) { + return elm.innerText || elm.textContent; + }; + + var getOrGenerateId = function (elm) { + return elm.id ? elm.id : Id.generate('h'); + }; + + var isAnchor = function (elm) { + return elm && elm.nodeName === 'A' && (elm.id || elm.name); + }; + + var isValidAnchor = function (elm) { + return isAnchor(elm) && isEditable(elm); + }; + + var isHeader = function (elm) { + return elm && /^(H[1-6])$/.test(elm.nodeName); + }; + + var isEditable = function (elm) { + return isChildOfContentEditableTrue(elm) && !isContentEditableFalse(elm); + }; + + var isValidHeader = function (elm) { + return isHeader(elm) && isEditable(elm); + }; + + var getLevel = function (elm) { + return isHeader(elm) ? parseInt(elm.nodeName.substr(1), 10) : 0; + }; + + var headerTarget = function (elm) { + var headerId = getOrGenerateId(elm); + + var attach = function () { + elm.id = headerId; + }; + + return create('header', getElementText(elm), '#' + headerId, getLevel(elm), attach); + }; + + var anchorTarget = function (elm) { + var anchorId = elm.id || elm.name; + var anchorText = getElementText(elm); + + return create('anchor', anchorText ? anchorText : '#' + anchorId, '#' + anchorId, 0, Fun.noop); + }; + + var getHeaderTargets = function (elms) { + return Arr.map(Arr.filter(elms, isValidHeader), headerTarget); + }; + + var getAnchorTargets = function (elms) { + return Arr.map(Arr.filter(elms, isValidAnchor), anchorTarget); + }; + + var getTargetElements = function (elm) { + var elms = select('h1,h2,h3,h4,h5,h6,a:not([href])', elm); + return elms; + }; + + var hasTitle = function (target) { + return trim(target.title).length > 0; + }; + + var find = function (elm) { + var elms = getTargetElements(elm); + return Arr.filter(getHeaderTargets(elms).concat(getAnchorTargets(elms)), hasTitle); + }; + + return { + find: find + }; + } +); + +/** + * FilePicker.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 creates a file picker control. + * + * @class tinymce.ui.FilePicker + * @extends tinymce.ui.ComboBox + */ +define( + 'tinymce.ui.FilePicker', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'global!window', + 'tinymce.ui.content.LinkTargets', + 'tinymce.core.EditorManager', + 'tinymce.ui.ComboBox', + 'tinymce.core.util.Tools' + ], + function (Arr, Fun, window, LinkTargets, EditorManager, ComboBox, Tools) { + "use strict"; + + var getActiveEditor = function () { + return window.tinymce ? window.tinymce.activeEditor : EditorManager.activeEditor; + }; + + var history = {}; + var HISTORY_LENGTH = 5; + + var clearHistory = function () { + history = {}; + }; + + var toMenuItem = function (target) { + return { + title: target.title, + value: { + title: { raw: target.title }, + url: target.url, + attach: target.attach + } + }; + }; + + var toMenuItems = function (targets) { + return Tools.map(targets, toMenuItem); + }; + + var staticMenuItem = function (title, url) { + return { + title: title, + value: { + title: title, + url: url, + attach: Fun.noop + } + }; + }; + + var isUniqueUrl = function (url, targets) { + var foundTarget = Arr.exists(targets, function (target) { + return target.url === url; + }); + + return !foundTarget; + }; + + var getSetting = function (editorSettings, name, defaultValue) { + var value = name in editorSettings ? editorSettings[name] : defaultValue; + return value === false ? null : value; + }; + + var createMenuItems = function (term, targets, fileType, editorSettings) { + var separator = { title: '-' }; + + var fromHistoryMenuItems = function (history) { + var historyItems = history.hasOwnProperty(fileType) ? history[fileType] : [ ]; + var uniqueHistory = Arr.filter(historyItems, function (url) { + return isUniqueUrl(url, targets); + }); + + return Tools.map(uniqueHistory, function (url) { + return { + title: url, + value: { + title: url, + url: url, + attach: Fun.noop + } + }; + }); + }; + + var fromMenuItems = function (type) { + var filteredTargets = Arr.filter(targets, function (target) { + return target.type === type; + }); + + return toMenuItems(filteredTargets); + }; + + var anchorMenuItems = function () { + var anchorMenuItems = fromMenuItems('anchor'); + var topAnchor = getSetting(editorSettings, 'anchor_top', '#top'); + var bottomAchor = getSetting(editorSettings, 'anchor_bottom', '#bottom'); + + if (topAnchor !== null) { + anchorMenuItems.unshift(staticMenuItem('', topAnchor)); + } + + if (bottomAchor !== null) { + anchorMenuItems.push(staticMenuItem('', bottomAchor)); + } + + return anchorMenuItems; + }; + + var join = function (items) { + return Arr.foldl(items, function (a, b) { + var bothEmpty = a.length === 0 || b.length === 0; + return bothEmpty ? a.concat(b) : a.concat(separator, b); + }, []); + }; + + if (editorSettings.typeahead_urls === false) { + return []; + } + + return fileType === 'file' ? join([ + filterByQuery(term, fromHistoryMenuItems(history)), + filterByQuery(term, fromMenuItems('header')), + filterByQuery(term, anchorMenuItems()) + ]) : filterByQuery(term, fromHistoryMenuItems(history)); + }; + + var addToHistory = function (url, fileType) { + var items = history[fileType]; + + if (!/^https?/.test(url)) { + return; + } + + if (items) { + if (Arr.indexOf(items, url) === -1) { + history[fileType] = items.slice(0, HISTORY_LENGTH).concat(url); + } + } else { + history[fileType] = [url]; + } + }; + + var filterByQuery = function (term, menuItems) { + var lowerCaseTerm = term.toLowerCase(); + var result = Tools.grep(menuItems, function (item) { + return item.title.toLowerCase().indexOf(lowerCaseTerm) !== -1; + }); + + return result.length === 1 && result[0].title === term ? [] : result; + }; + + var getTitle = function (linkDetails) { + var title = linkDetails.title; + return title.raw ? title.raw : title; + }; + + var setupAutoCompleteHandler = function (ctrl, editorSettings, bodyElm, fileType) { + var autocomplete = function (term) { + var linkTargets = LinkTargets.find(bodyElm); + var menuItems = createMenuItems(term, linkTargets, fileType, editorSettings); + ctrl.showAutoComplete(menuItems, term); + }; + + ctrl.on('autocomplete', function () { + autocomplete(ctrl.value()); + }); + + ctrl.on('selectitem', function (e) { + var linkDetails = e.value; + + ctrl.value(linkDetails.url); + var title = getTitle(linkDetails); + + if (fileType === 'image') { + ctrl.fire('change', { meta: { alt: title, attach: linkDetails.attach } }); + } else { + ctrl.fire('change', { meta: { text: title, attach: linkDetails.attach } }); + } + + ctrl.focus(); + }); + + ctrl.on('click', function (e) { + if (ctrl.value().length === 0 && e.target.nodeName === 'INPUT') { + autocomplete(''); + } + }); + + ctrl.on('PostRender', function () { + ctrl.getRoot().on('submit', function (e) { + if (!e.isDefaultPrevented()) { + addToHistory(ctrl.value(), fileType); + } + }); + }); + }; + + var statusToUiState = function (result) { + var status = result.status, message = result.message; + + if (status === 'valid') { + return { status: 'ok', message: message }; + } else if (status === 'unknown') { + return { status: 'warn', message: message }; + } else if (status === 'invalid') { + return { status: 'warn', message: message }; + } else { + return { status: 'none', message: '' }; + } + }; + + var setupLinkValidatorHandler = function (ctrl, editorSettings, fileType) { + var validatorHandler = editorSettings.filepicker_validator_handler; + if (validatorHandler) { + var validateUrl = function (url) { + if (url.length === 0) { + ctrl.statusLevel('none'); + return; + } + + validatorHandler({ + url: url, + type: fileType + }, function (result) { + var uiState = statusToUiState(result); + + ctrl.statusMessage(uiState.message); + ctrl.statusLevel(uiState.status); + }); + }; + + ctrl.state.on('change:value', function (e) { + validateUrl(e.value); + }); + } + }; + + return ComboBox.extend({ + Statics: { + clearHistory: clearHistory + }, + + /** + * Constructs a new control instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + var self = this, editor = getActiveEditor(), editorSettings = editor.settings; + var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes; + var fileType = settings.filetype; + + settings.spellcheck = false; + + fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types; + if (fileBrowserCallbackTypes) { + fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/); + } + + if (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType]) { + fileBrowserCallback = editorSettings.file_picker_callback; + if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) { + actionCallback = function () { + var meta = self.fire('beforecall').meta; + + meta = Tools.extend({ filetype: fileType }, meta); + + // file_picker_callback(callback, currentValue, metaData) + fileBrowserCallback.call( + editor, + function (value, meta) { + self.value(value).fire('change', { meta: meta }); + }, + self.value(), + meta + ); + }; + } else { + // Legacy callback: file_picker_callback(id, currentValue, filetype, window) + fileBrowserCallback = editorSettings.file_browser_callback; + if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) { + actionCallback = function () { + fileBrowserCallback( + self.getEl('inp').id, + self.value(), + fileType, + window + ); + }; + } + } + } + + if (actionCallback) { + settings.icon = 'browse'; + settings.onaction = actionCallback; + } + + self._super(settings); + + setupAutoCompleteHandler(self, editorSettings, editor.getBody(), fileType); + setupLinkValidatorHandler(self, editorSettings, fileType); + } + }); + } +); +/** + * FitLayout.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 layout manager will resize the control to be the size of it's parent container. + * In other words width: 100% and height: 100%. + * + * @-x-less FitLayout.less + * @class tinymce.ui.FitLayout + * @extends tinymce.ui.AbsoluteLayout + */ +define( + 'tinymce.ui.FitLayout', + [ + "tinymce.ui.AbsoluteLayout" + ], + function (AbsoluteLayout) { + "use strict"; + + return AbsoluteLayout.extend({ + /** + * Recalculates the positions of the controls in the specified container. + * + * @method recalc + * @param {tinymce.ui.Container} container Container instance to recalc. + */ + recalc: function (container) { + var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox; + + container.items().filter(':visible').each(function (ctrl) { + ctrl.layoutRect({ + x: paddingBox.left, + y: paddingBox.top, + w: contLayoutRect.innerW - paddingBox.right - paddingBox.left, + h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom + }); + + if (ctrl.recalc) { + ctrl.recalc(); + } + }); + } + }); + } +); +/** + * FlexLayout.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 layout manager works similar to the CSS flex box. + * + * @setting {String} direction row|row-reverse|column|column-reverse + * @setting {Number} flex A positive-number to flex by. + * @setting {String} align start|end|center|stretch + * @setting {String} pack start|end|justify + * + * @class tinymce.ui.FlexLayout + * @extends tinymce.ui.AbsoluteLayout + */ +define( + 'tinymce.ui.FlexLayout', + [ + "tinymce.ui.AbsoluteLayout" + ], + function (AbsoluteLayout) { + "use strict"; + + return AbsoluteLayout.extend({ + /** + * Recalculates the positions of the controls in the specified container. + * + * @method recalc + * @param {tinymce.ui.Container} container Container instance to recalc. + */ + recalc: function (container) { + // A ton of variables, needs to be in the same scope for performance + var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction; + var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos; + var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName; + var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName; + var alignDeltaSizeName, alignContentSizeName; + var max = Math.max, min = Math.min; + + // Get container items, properties and settings + items = container.items().filter(':visible'); + contLayoutRect = container.layoutRect(); + contPaddingBox = container.paddingBox; + contSettings = container.settings; + direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction; + align = contSettings.align; + pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack; + spacing = contSettings.spacing || 0; + + if (direction == "row-reversed" || direction == "column-reverse") { + items = items.set(items.toArray().reverse()); + direction = direction.split('-')[0]; + } + + // Setup axis variable name for row/column direction since the calculations is the same + if (direction == "column") { + posName = "y"; + sizeName = "h"; + minSizeName = "minH"; + maxSizeName = "maxH"; + innerSizeName = "innerH"; + beforeName = 'top'; + deltaSizeName = "deltaH"; + contentSizeName = "contentH"; + + alignBeforeName = "left"; + alignSizeName = "w"; + alignAxisName = "x"; + alignInnerSizeName = "innerW"; + alignMinSizeName = "minW"; + alignAfterName = "right"; + alignDeltaSizeName = "deltaW"; + alignContentSizeName = "contentW"; + } else { + posName = "x"; + sizeName = "w"; + minSizeName = "minW"; + maxSizeName = "maxW"; + innerSizeName = "innerW"; + beforeName = 'left'; + deltaSizeName = "deltaW"; + contentSizeName = "contentW"; + + alignBeforeName = "top"; + alignSizeName = "h"; + alignAxisName = "y"; + alignInnerSizeName = "innerH"; + alignMinSizeName = "minH"; + alignAfterName = "bottom"; + alignDeltaSizeName = "deltaH"; + alignContentSizeName = "contentH"; + } + + // Figure out total flex, availableSpace and collect any max size elements + availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName]; + maxAlignEndPos = totalFlex = 0; + for (i = 0, l = items.length; i < l; i++) { + ctrl = items[i]; + ctrlLayoutRect = ctrl.layoutRect(); + ctrlSettings = ctrl.settings; + flex = ctrlSettings.flex; + availableSpace -= (i < l - 1 ? spacing : 0); + + if (flex > 0) { + totalFlex += flex; + + // Flexed item has a max size then we need to check if we will hit that size + if (ctrlLayoutRect[maxSizeName]) { + maxSizeItems.push(ctrl); + } + + ctrlLayoutRect.flex = flex; + } + + availableSpace -= ctrlLayoutRect[minSizeName]; + + // Calculate the align end position to be used to check for overflow/underflow + size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName]; + if (size > maxAlignEndPos) { + maxAlignEndPos = size; + } + } + + // Calculate minW/minH + rect = {}; + if (availableSpace < 0) { + rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName]; + } else { + rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName]; + } + + rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName]; + + rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace; + rect[alignContentSizeName] = maxAlignEndPos; + rect.minW = min(rect.minW, contLayoutRect.maxW); + rect.minH = min(rect.minH, contLayoutRect.maxH); + rect.minW = max(rect.minW, contLayoutRect.startMinWidth); + rect.minH = max(rect.minH, contLayoutRect.startMinHeight); + + // Resize container container if minSize was changed + if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { + rect.w = rect.minW; + rect.h = rect.minH; + + container.layoutRect(rect); + this.recalc(container); + + // Forced recalc for example if items are hidden/shown + if (container._lastRect === null) { + var parentCtrl = container.parent(); + if (parentCtrl) { + parentCtrl._lastRect = null; + parentCtrl.recalc(); + } + } + + return; + } + + // Handle max size elements, check if they will become to wide with current options + ratio = availableSpace / totalFlex; + for (i = 0, l = maxSizeItems.length; i < l; i++) { + ctrl = maxSizeItems[i]; + ctrlLayoutRect = ctrl.layoutRect(); + maxSize = ctrlLayoutRect[maxSizeName]; + size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio; + + if (size > maxSize) { + availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]); + totalFlex -= ctrlLayoutRect.flex; + ctrlLayoutRect.flex = 0; + ctrlLayoutRect.maxFlexSize = maxSize; + } else { + ctrlLayoutRect.maxFlexSize = 0; + } + } + + // Setup new ratio, target layout rect, start position + ratio = availableSpace / totalFlex; + pos = contPaddingBox[beforeName]; + rect = {}; + + // Handle pack setting moves the start position to end, center + if (totalFlex === 0) { + if (pack == "end") { + pos = availableSpace + contPaddingBox[beforeName]; + } else if (pack == "center") { + pos = Math.round( + (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2) + ) + contPaddingBox[beforeName]; + + if (pos < 0) { + pos = contPaddingBox[beforeName]; + } + } else if (pack == "justify") { + pos = contPaddingBox[beforeName]; + spacing = Math.floor(availableSpace / (items.length - 1)); + } + } + + // Default aligning (start) the other ones needs to be calculated while doing the layout + rect[alignAxisName] = contPaddingBox[alignBeforeName]; + + // Start laying out controls + for (i = 0, l = items.length; i < l; i++) { + ctrl = items[i]; + ctrlLayoutRect = ctrl.layoutRect(); + size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName]; + + // Align the control on the other axis + if (align === "center") { + rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2)); + } else if (align === "stretch") { + rect[alignSizeName] = max( + ctrlLayoutRect[alignMinSizeName] || 0, + contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName] + ); + rect[alignAxisName] = contPaddingBox[alignBeforeName]; + } else if (align === "end") { + rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top; + } + + // Calculate new size based on flex + if (ctrlLayoutRect.flex > 0) { + size += ctrlLayoutRect.flex * ratio; + } + + rect[sizeName] = size; + rect[posName] = pos; + ctrl.layoutRect(rect); + + // Recalculate containers + if (ctrl.recalc) { + ctrl.recalc(); + } + + // Move x/y position + pos += size + spacing; + } + } + }); + } +); +/** + * FlowLayout.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 layout manager will place the controls by using the browsers native layout. + * + * @-x-less FlowLayout.less + * @class tinymce.ui.FlowLayout + * @extends tinymce.ui.Layout + */ +define( + 'tinymce.ui.FlowLayout', + [ + "tinymce.ui.Layout" + ], + function (Layout) { + return Layout.extend({ + Defaults: { + containerClass: 'flow-layout', + controlClass: 'flow-layout-item', + endClass: 'break' + }, + + /** + * Recalculates the positions of the controls in the specified container. + * + * @method recalc + * @param {tinymce.ui.Container} container Container instance to recalc. + */ + recalc: function (container) { + container.items().filter(':visible').each(function (ctrl) { + if (ctrl.recalc) { + ctrl.recalc(); + } + }); + }, + + isNative: function () { + return true; + } + }); + } +); +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); + }; + + // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise + 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 + }; + } +); + +/** + * FormatUtils.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.ui.editorui.FormatUtils', + [ + ], + function () { + var toggleFormat = function (editor, fmt) { + return function () { + editor.execCommand('mceToggleFormat', false, fmt); + }; + }; + + var postRenderFormat = function (editor, name) { + return function () { + var self = this; + + // TODO: Fix this + if (editor.formatter) { + editor.formatter.formatChanged(name, function (state) { + self.active(state); + }); + } else { + editor.on('init', function () { + editor.formatter.formatChanged(name, function (state) { + self.active(state); + }); + }); + } + }; + }; + + return { + toggleFormat: toggleFormat, + postRenderFormat: postRenderFormat + }; + } +); + +/** + * Align.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.ui.editorui.Align', + [ + 'tinymce.core.util.Tools', + 'tinymce.ui.editorui.FormatUtils' + ], + function (Tools, FormatUtils) { + var register = function (editor) { + editor.addMenuItem('align', { + text: 'Align', + menu: [ + { text: 'Left', icon: 'alignleft', onclick: FormatUtils.toggleFormat(editor, 'alignleft') }, + { text: 'Center', icon: 'aligncenter', onclick: FormatUtils.toggleFormat(editor, 'aligncenter') }, + { text: 'Right', icon: 'alignright', onclick: FormatUtils.toggleFormat(editor, 'alignright') }, + { text: 'Justify', icon: 'alignjustify', onclick: FormatUtils.toggleFormat(editor, 'alignjustify') } + ] + }); + + Tools.each({ + alignleft: ['Align left', 'JustifyLeft'], + aligncenter: ['Align center', 'JustifyCenter'], + alignright: ['Align right', 'JustifyRight'], + alignjustify: ['Justify', 'JustifyFull'], + alignnone: ['No alignment', 'JustifyNone'] + }, function (item, name) { + editor.addButton(name, { + tooltip: item[0], + cmd: item[1], + onPostRender: FormatUtils.postRenderFormat(editor, name) + }); + }); + }; + + return { + register: register + }; + } +); + +/** + * FormatSelect.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.ui.editorui.FormatSelect', + [ + 'tinymce.core.util.Tools', + 'tinymce.ui.editorui.FormatUtils' + ], + function (Tools, FormatUtils) { + var defaultBlocks = ( + 'Paragraph=p;' + + 'Heading 1=h1;' + + 'Heading 2=h2;' + + 'Heading 3=h3;' + + 'Heading 4=h4;' + + 'Heading 5=h5;' + + 'Heading 6=h6;' + + 'Preformatted=pre' + ); + + var createFormats = function (formats) { + formats = formats.replace(/;$/, '').split(';'); + + var i = formats.length; + while (i--) { + formats[i] = formats[i].split('='); + } + + return formats; + }; + + var createListBoxChangeHandler = function (editor, items, formatName) { + return function () { + var self = this; + + editor.on('nodeChange', function (e) { + var formatter = editor.formatter; + var value = null; + + Tools.each(e.parents, function (node) { + Tools.each(items, function (item) { + if (formatName) { + if (formatter.matchNode(node, formatName, { value: item.value })) { + value = item.value; + } + } else { + if (formatter.matchNode(node, item.value)) { + value = item.value; + } + } + + if (value) { + return false; + } + }); + + if (value) { + return false; + } + }); + + self.value(value); + }); + }; + }; + + var lazyFormatSelectBoxItems = function (editor, blocks) { + return function () { + var items = []; + + Tools.each(blocks, function (block) { + items.push({ + text: block[0], + value: block[1], + textStyle: function () { + return editor.formatter.getCssText(block[1]); + } + }); + }); + + return { + type: 'listbox', + text: blocks[0][0], + values: items, + fixedWidth: true, + onselect: function (e) { + if (e.control) { + var fmt = e.control.value(); + FormatUtils.toggleFormat(editor, fmt)(); + } + }, + onPostRender: createListBoxChangeHandler(editor, items) + }; + }; + }; + + var buildMenuItems = function (editor, blocks) { + return Tools.map(blocks, function (block) { + return { + text: block[0], + onclick: FormatUtils.toggleFormat(editor, block[1]), + textStyle: function () { + return editor.formatter.getCssText(block[1]); + } + }; + }); + }; + + var register = function (editor) { + var blocks = createFormats(editor.settings.block_formats || defaultBlocks); + + editor.addMenuItem('blockformats', { + text: 'Blocks', + menu: buildMenuItems(editor, blocks) + }); + + editor.addButton('formatselect', lazyFormatSelectBoxItems(editor, blocks)); + }; + + return { + register: register + }; + } +); + + +/** + * SimpleFormats.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.ui.editorui.SimpleFormats', + [ + 'tinymce.core.util.Tools', + 'tinymce.ui.editorui.FormatUtils' + ], + function (Tools, FormatUtils) { + var registerButtons = function (editor) { + Tools.each({ + bold: 'Bold', + italic: 'Italic', + underline: 'Underline', + strikethrough: 'Strikethrough', + subscript: 'Subscript', + superscript: 'Superscript' + }, function (text, name) { + editor.addButton(name, { + tooltip: text, + onPostRender: FormatUtils.postRenderFormat(editor, name), + onclick: FormatUtils.toggleFormat(editor, name) + }); + }); + + editor.addButton('removeformat', { + title: 'Clear formatting', + cmd: 'RemoveFormat' + }); + }; + + var registerMenuItems = function (editor) { + Tools.each({ + bold: ['Bold', 'Bold', 'Meta+B'], + italic: ['Italic', 'Italic', 'Meta+I'], + underline: ['Underline', 'Underline', 'Meta+U'], + strikethrough: ['Strikethrough', 'Strikethrough'], + subscript: ['Subscript', 'Subscript'], + superscript: ['Superscript', 'Superscript'], + removeformat: ['Clear formatting', 'RemoveFormat'] + }, function (item, name) { + editor.addMenuItem(name, { + text: item[0], + icon: name, + shortcut: item[2], + cmd: item[1] + }); + }); + + editor.addMenuItem('codeformat', { + text: 'Code', + icon: 'code', + onclick: FormatUtils.toggleFormat(editor, 'code') + }); + }; + + var register = function (editor) { + registerButtons(editor); + registerMenuItems(editor); + }; + + return { + register: register + }; + } +); + +/** + * FontInfo.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 computing font size for elements. + * + * @private + * @class tinymce.fmt.FontInfo + */ +define( + 'tinymce.ui.fmt.FontInfo', + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'tinymce.core.dom.DOMUtils' + ], + function (Fun, Option, Element, Node, DOMUtils) { + var getSpecifiedFontProp = function (propName, rootElm, elm) { + while (elm !== rootElm) { + if (elm.style[propName]) { + var foundStyle = elm.style[propName]; + return foundStyle !== '' ? Option.some(foundStyle) : Option.none(); + } + elm = elm.parentNode; + } + return Option.none(); + }; + + var toPt = function (fontSize) { + if (/[0-9.]+px$/.test(fontSize)) { + return Math.round(parseInt(fontSize, 10) * 72 / 96) + 'pt'; + } + + return fontSize; + }; + + var normalizeFontFamily = function (fontFamily) { + // 'Font name', Font -> Font name,Font + return fontFamily.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); + }; + + var getComputedFontProp = function (propName, elm) { + return Option.from(DOMUtils.DOM.getStyle(elm, propName, true)); + }; + + var getFontProp = function (propName) { + return function (rootElm, elm) { + return Option.from(elm) + .map(Element.fromDom) + .filter(Node.isElement) + .bind(function (element) { + return getSpecifiedFontProp(propName, rootElm, element.dom()) + .or(getComputedFontProp(propName, element.dom())); + }) + .getOr(''); + }; + }; + + return { + getFontSize: getFontProp('fontSize'), + getFontFamily: Fun.compose(normalizeFontFamily, getFontProp('fontFamily')), + toPt: toPt + }; + } +); + +/** + * FormatControls.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 containing all TinyMCE specific control types such as + * format listboxes, fontlist boxes, toolbar buttons etc. + * + * @class tinymce.ui.FormatControls + */ +define( + 'tinymce.ui.FormatControls', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.SelectorFind', + 'global!document', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.EditorManager', + 'tinymce.core.Env', + 'tinymce.core.util.Tools', + 'tinymce.ui.Control', + 'tinymce.ui.editorui.Align', + 'tinymce.ui.editorui.FormatSelect', + 'tinymce.ui.editorui.SimpleFormats', + 'tinymce.ui.FloatPanel', + 'tinymce.ui.fmt.FontInfo', + 'tinymce.ui.Widget' + ], + function (Arr, Fun, Element, SelectorFind, document, DOMUtils, EditorManager, Env, Tools, Control, Align, FormatSelect, SimpleFormats, FloatPanel, FontInfo, Widget) { + var each = Tools.each; + + var flatten = function (ar) { + return Arr.foldl(ar, function (result, item) { + return result.concat(item); + }, []); + }; + + Control.translate = function (text) { + return EditorManager.translate(text); + }; + + Widget.tooltips = !Env.iOS; + + function setupContainer(editor) { + if (editor.settings.ui_container) { + Env.container = SelectorFind.descendant(Element.fromDom(document.body), editor.settings.ui_container).fold(Fun.constant(null), function (elm) { + return elm.dom(); + }); + } + } + + function setupRtlMode(editor) { + if (editor.rtl) { + Control.rtl = true; + } + } + + function registerControls(editor) { + var formatMenu; + + function createFontNameListBoxChangeHandler(items) { + return function () { + var self = this; + + var getFirstFont = function (fontFamily) { + return fontFamily ? fontFamily.split(',')[0] : ''; + }; + + editor.on('init nodeChange', function (e) { + var fontFamily, value = null; + + fontFamily = FontInfo.getFontFamily(editor.getBody(), e.element); + + each(items, function (item) { + if (item.value.toLowerCase() === fontFamily.toLowerCase()) { + value = item.value; + } + }); + + each(items, function (item) { + if (!value && getFirstFont(item.value).toLowerCase() === getFirstFont(fontFamily).toLowerCase()) { + value = item.value; + } + }); + + self.value(value); + + if (!value && fontFamily) { + self.text(getFirstFont(fontFamily)); + } + }); + }; + } + + function createFontSizeListBoxChangeHandler(items) { + return function () { + var self = this; + + editor.on('init nodeChange', function (e) { + var px, pt, value = null; + + px = FontInfo.getFontSize(editor.getBody(), e.element); + pt = FontInfo.toPt(px); + + each(items, function (item) { + if (item.value === px) { + value = px; + } else if (item.value === pt) { + value = pt; + } + }); + + self.value(value); + + if (!value) { + self.text(pt); + } + }); + }; + } + + function createFormats(formats) { + formats = formats.replace(/;$/, '').split(';'); + + var i = formats.length; + while (i--) { + formats[i] = formats[i].split('='); + } + + return formats; + } + + function createFormatMenu() { + var count = 0, newFormats = []; + + var defaultStyleFormats = [ + { + title: 'Headings', items: [ + { title: 'Heading 1', format: 'h1' }, + { title: 'Heading 2', format: 'h2' }, + { title: 'Heading 3', format: 'h3' }, + { title: 'Heading 4', format: 'h4' }, + { title: 'Heading 5', format: 'h5' }, + { title: 'Heading 6', format: 'h6' } + ] + }, + + { + title: 'Inline', items: [ + { title: 'Bold', icon: 'bold', format: 'bold' }, + { title: 'Italic', icon: 'italic', format: 'italic' }, + { title: 'Underline', icon: 'underline', format: 'underline' }, + { title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough' }, + { title: 'Superscript', icon: 'superscript', format: 'superscript' }, + { title: 'Subscript', icon: 'subscript', format: 'subscript' }, + { title: 'Code', icon: 'code', format: 'code' } + ] + }, + + { + title: 'Blocks', items: [ + { title: 'Paragraph', format: 'p' }, + { title: 'Blockquote', format: 'blockquote' }, + { title: 'Div', format: 'div' }, + { title: 'Pre', format: 'pre' } + ] + }, + + { + title: 'Alignment', items: [ + { title: 'Left', icon: 'alignleft', format: 'alignleft' }, + { title: 'Center', icon: 'aligncenter', format: 'aligncenter' }, + { title: 'Right', icon: 'alignright', format: 'alignright' }, + { title: 'Justify', icon: 'alignjustify', format: 'alignjustify' } + ] + } + ]; + + function createMenu(formats) { + var menu = []; + + if (!formats) { + return; + } + + each(formats, function (format) { + var menuItem = { + text: format.title, + icon: format.icon + }; + + if (format.items) { + menuItem.menu = createMenu(format.items); + } else { + var formatName = format.format || "custom" + count++; + + if (!format.format) { + format.name = formatName; + newFormats.push(format); + } + + menuItem.format = formatName; + menuItem.cmd = format.cmd; + } + + menu.push(menuItem); + }); + + return menu; + } + + function createStylesMenu() { + var menu; + + if (editor.settings.style_formats_merge) { + if (editor.settings.style_formats) { + menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats)); + } else { + menu = createMenu(defaultStyleFormats); + } + } else { + menu = createMenu(editor.settings.style_formats || defaultStyleFormats); + } + + return menu; + } + + editor.on('init', function () { + each(newFormats, function (format) { + editor.formatter.register(format.name, format); + }); + }); + + return { + type: 'menu', + items: createStylesMenu(), + onPostRender: function (e) { + editor.fire('renderFormatsMenu', { control: e.control }); + }, + itemDefaults: { + preview: true, + + textStyle: function () { + if (this.settings.format) { + return editor.formatter.getCssText(this.settings.format); + } + }, + + onPostRender: function () { + var self = this; + + self.parent().on('show', function () { + var formatName, command; + + formatName = self.settings.format; + if (formatName) { + self.disabled(!editor.formatter.canApply(formatName)); + self.active(editor.formatter.match(formatName)); + } + + command = self.settings.cmd; + if (command) { + self.active(editor.queryCommandState(command)); + } + }); + }, + + onclick: function () { + if (this.settings.format) { + toggleFormat(this.settings.format); + } + + if (this.settings.cmd) { + editor.execCommand(this.settings.cmd); + } + } + } + }; + } + + formatMenu = createFormatMenu(); + + function initOnPostRender(name) { + return function () { + var self = this; + + // TODO: Fix this + if (editor.formatter) { + editor.formatter.formatChanged(name, function (state) { + self.active(state); + }); + } else { + editor.on('init', function () { + editor.formatter.formatChanged(name, function (state) { + self.active(state); + }); + }); + } + }; + } + + // Simple command controls :[,] + each({ + outdent: ['Decrease indent', 'Outdent'], + indent: ['Increase indent', 'Indent'], + cut: ['Cut', 'Cut'], + copy: ['Copy', 'Copy'], + paste: ['Paste', 'Paste'], + help: ['Help', 'mceHelp'], + selectall: ['Select all', 'SelectAll'], + visualaid: ['Visual aids', 'mceToggleVisualAid'], + newdocument: ['New document', 'mceNewDocument'] + }, function (item, name) { + editor.addButton(name, { + tooltip: item[0], + cmd: item[1] + }); + }); + + // Simple command controls with format state + each({ + blockquote: ['Blockquote', 'mceBlockQuote'], + subscript: ['Subscript', 'Subscript'], + superscript: ['Superscript', 'Superscript'] + }, function (item, name) { + editor.addButton(name, { + tooltip: item[0], + cmd: item[1], + onPostRender: initOnPostRender(name) + }); + }); + + function toggleUndoRedoState(type) { + return function () { + var self = this; + + function checkState() { + var typeFn = type == 'redo' ? 'hasRedo' : 'hasUndo'; + return editor.undoManager ? editor.undoManager[typeFn]() : false; + } + + self.disabled(!checkState()); + editor.on('Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', function () { + self.disabled(editor.readonly || !checkState()); + }); + }; + } + + function toggleVisualAidState() { + var self = this; + + editor.on('VisualAid', function (e) { + self.active(e.hasVisual); + }); + + self.active(editor.hasVisual); + } + + var trimMenuItems = function (menuItems) { + var outputMenuItems = menuItems; + + if (outputMenuItems.length > 0 && outputMenuItems[0].text === '-') { + outputMenuItems = outputMenuItems.slice(1); + } + + if (outputMenuItems.length > 0 && outputMenuItems[outputMenuItems.length - 1].text === '-') { + outputMenuItems = outputMenuItems.slice(0, outputMenuItems.length - 1); + } + + return outputMenuItems; + }; + + var createCustomMenuItems = function (names) { + var items, nameList; + + if (typeof names === 'string') { + nameList = names.split(' '); + } else if (Tools.isArray(names)) { + return flatten(Tools.map(names, createCustomMenuItems)); + } + + items = Tools.grep(nameList, function (name) { + return name === '|' || name in editor.menuItems; + }); + + return Tools.map(items, function (name) { + return name === '|' ? { text: '-' } : editor.menuItems[name]; + }); + }; + + var createContextMenuItems = function (context) { + var outputMenuItems = [{ text: '-' }]; + var menuItems = Tools.grep(editor.menuItems, function (menuItem) { + return menuItem.context === context; + }); + + Tools.each(menuItems, function (menuItem) { + if (menuItem.separator == 'before') { + outputMenuItems.push({ text: '|' }); + } + + if (menuItem.prependToContext) { + outputMenuItems.unshift(menuItem); + } else { + outputMenuItems.push(menuItem); + } + + if (menuItem.separator == 'after') { + outputMenuItems.push({ text: '|' }); + } + }); + + return outputMenuItems; + }; + + var createInsertMenu = function (editorSettings) { + if (editorSettings.insert_button_items) { + return trimMenuItems(createCustomMenuItems(editorSettings.insert_button_items)); + } else { + return trimMenuItems(createContextMenuItems('insert')); + } + }; + + editor.addButton('undo', { + tooltip: 'Undo', + onPostRender: toggleUndoRedoState('undo'), + cmd: 'undo' + }); + + editor.addButton('redo', { + tooltip: 'Redo', + onPostRender: toggleUndoRedoState('redo'), + cmd: 'redo' + }); + + editor.addMenuItem('newdocument', { + text: 'New document', + icon: 'newdocument', + cmd: 'mceNewDocument' + }); + + editor.addMenuItem('undo', { + text: 'Undo', + icon: 'undo', + shortcut: 'Meta+Z', + onPostRender: toggleUndoRedoState('undo'), + cmd: 'undo' + }); + + editor.addMenuItem('redo', { + text: 'Redo', + icon: 'redo', + shortcut: 'Meta+Y', + onPostRender: toggleUndoRedoState('redo'), + cmd: 'redo' + }); + + editor.addMenuItem('visualaid', { + text: 'Visual aids', + selectable: true, + onPostRender: toggleVisualAidState, + cmd: 'mceToggleVisualAid' + }); + + editor.addButton('remove', { + tooltip: 'Remove', + icon: 'remove', + cmd: 'Delete' + }); + + editor.addButton('insert', { + type: 'menubutton', + icon: 'insert', + menu: [], + oncreatemenu: function () { + this.menu.add(createInsertMenu(editor.settings)); + this.menu.renderNew(); + } + }); + + each({ + cut: ['Cut', 'Cut', 'Meta+X'], + copy: ['Copy', 'Copy', 'Meta+C'], + paste: ['Paste', 'Paste', 'Meta+V'], + selectall: ['Select all', 'SelectAll', 'Meta+A'] + }, function (item, name) { + editor.addMenuItem(name, { + text: item[0], + icon: name, + shortcut: item[2], + cmd: item[1] + }); + }); + + editor.on('mousedown', function () { + FloatPanel.hideAll(); + }); + + function toggleFormat(fmt) { + if (fmt.control) { + fmt = fmt.control.value(); + } + + if (fmt) { + editor.execCommand('mceToggleFormat', false, fmt); + } + } + + function hideMenuObjects(menu) { + var count = menu.length; + + Tools.each(menu, function (item) { + if (item.menu) { + item.hidden = hideMenuObjects(item.menu) === 0; + } + + var formatName = item.format; + if (formatName) { + item.hidden = !editor.formatter.canApply(formatName); + } + + if (item.hidden) { + count--; + } + }); + + return count; + } + + function hideFormatMenuItems(menu) { + var count = menu.items().length; + + menu.items().each(function (item) { + if (item.menu) { + item.visible(hideFormatMenuItems(item.menu) > 0); + } + + if (!item.menu && item.settings.menu) { + item.visible(hideMenuObjects(item.settings.menu) > 0); + } + + var formatName = item.settings.format; + if (formatName) { + item.visible(editor.formatter.canApply(formatName)); + } + + if (!item.visible()) { + count--; + } + }); + + return count; + } + + editor.addButton('styleselect', { + type: 'menubutton', + text: 'Formats', + menu: formatMenu, + onShowMenu: function () { + if (editor.settings.style_formats_autohide) { + hideFormatMenuItems(this.menu); + } + } + }); + + editor.addButton('fontselect', function () { + var defaultFontsFormats = + 'Andale Mono=andale mono,monospace;' + + 'Arial=arial,helvetica,sans-serif;' + + 'Arial Black=arial black,sans-serif;' + + 'Book Antiqua=book antiqua,palatino,serif;' + + 'Comic Sans MS=comic sans ms,sans-serif;' + + 'Courier New=courier new,courier,monospace;' + + 'Georgia=georgia,palatino,serif;' + + 'Helvetica=helvetica,arial,sans-serif;' + + 'Impact=impact,sans-serif;' + + 'Symbol=symbol;' + + 'Tahoma=tahoma,arial,helvetica,sans-serif;' + + 'Terminal=terminal,monaco,monospace;' + + 'Times New Roman=times new roman,times,serif;' + + 'Trebuchet MS=trebuchet ms,geneva,sans-serif;' + + 'Verdana=verdana,geneva,sans-serif;' + + 'Webdings=webdings;' + + 'Wingdings=wingdings,zapf dingbats'; + + var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats); + + each(fonts, function (font) { + items.push({ + text: { raw: font[0] }, + value: font[1], + textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : '' + }); + }); + + return { + type: 'listbox', + text: 'Font Family', + tooltip: 'Font Family', + values: items, + fixedWidth: true, + onPostRender: createFontNameListBoxChangeHandler(items), + onselect: function (e) { + if (e.control.settings.value) { + editor.execCommand('FontName', false, e.control.settings.value); + } + } + }; + }); + + editor.addButton('fontsizeselect', function () { + var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt'; + var fontsizeFormats = editor.settings.fontsize_formats || defaultFontsizeFormats; + + each(fontsizeFormats.split(' '), function (item) { + var text = item, value = item; + // Allow text=value font sizes. + var values = item.split('='); + if (values.length > 1) { + text = values[0]; + value = values[1]; + } + items.push({ text: text, value: value }); + }); + + return { + type: 'listbox', + text: 'Font Sizes', + tooltip: 'Font Sizes', + values: items, + fixedWidth: true, + onPostRender: createFontSizeListBoxChangeHandler(items), + onclick: function (e) { + if (e.control.settings.value) { + editor.execCommand('FontSize', false, e.control.settings.value); + } + } + }; + }); + + editor.addMenuItem('formats', { + text: 'Formats', + menu: formatMenu + }); + } + + var setup = function (editor) { + setupRtlMode(editor); + registerControls(editor); + setupContainer(editor); + FormatSelect.register(editor); + Align.register(editor); + SimpleFormats.register(editor); + }; + + return { + setup: setup + }; + } +); + +/** + * GridLayout.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 layout manager places controls in a grid. + * + * @setting {Number} spacing Spacing between controls. + * @setting {Number} spacingH Horizontal spacing between controls. + * @setting {Number} spacingV Vertical spacing between controls. + * @setting {Number} columns Number of columns to use. + * @setting {String/Array} alignH start|end|center|stretch or array of values for each column. + * @setting {String/Array} alignV start|end|center|stretch or array of values for each column. + * @setting {String} pack start|end + * + * @class tinymce.ui.GridLayout + * @extends tinymce.ui.AbsoluteLayout + */ +define( + 'tinymce.ui.GridLayout', + [ + "tinymce.ui.AbsoluteLayout" + ], + function (AbsoluteLayout) { + "use strict"; + + return AbsoluteLayout.extend({ + /** + * Recalculates the positions of the controls in the specified container. + * + * @method recalc + * @param {tinymce.ui.Container} container Container instance to recalc. + */ + recalc: function (container) { + var settings, rows, cols, items, contLayoutRect, width, height, rect, + ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY, + colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx; + + // Get layout settings + settings = container.settings; + items = container.items().filter(':visible'); + contLayoutRect = container.layoutRect(); + cols = settings.columns || Math.ceil(Math.sqrt(items.length)); + rows = Math.ceil(items.length / cols); + spacingH = settings.spacingH || settings.spacing || 0; + spacingV = settings.spacingV || settings.spacing || 0; + alignH = settings.alignH || settings.align; + alignV = settings.alignV || settings.align; + contPaddingBox = container.paddingBox; + reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl(); + + if (alignH && typeof alignH == "string") { + alignH = [alignH]; + } + + if (alignV && typeof alignV == "string") { + alignV = [alignV]; + } + + // Zero padd columnWidths + for (x = 0; x < cols; x++) { + colWidths.push(0); + } + + // Zero padd rowHeights + for (y = 0; y < rows; y++) { + rowHeights.push(0); + } + + // Calculate columnWidths and rowHeights + for (y = 0; y < rows; y++) { + for (x = 0; x < cols; x++) { + ctrl = items[y * cols + x]; + + // Out of bounds + if (!ctrl) { + break; + } + + ctrlLayoutRect = ctrl.layoutRect(); + ctrlMinWidth = ctrlLayoutRect.minW; + ctrlMinHeight = ctrlLayoutRect.minH; + + colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x]; + rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y]; + } + } + + // Calculate maxX + availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right; + for (maxX = 0, x = 0; x < cols; x++) { + maxX += colWidths[x] + (x > 0 ? spacingH : 0); + availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x]; + } + + // Calculate maxY + availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom; + for (maxY = 0, y = 0; y < rows; y++) { + maxY += rowHeights[y] + (y > 0 ? spacingV : 0); + availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y]; + } + + maxX += contPaddingBox.left + contPaddingBox.right; + maxY += contPaddingBox.top + contPaddingBox.bottom; + + // Calculate minW/minH + rect = {}; + rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW); + rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH); + + rect.contentW = rect.minW - contLayoutRect.deltaW; + rect.contentH = rect.minH - contLayoutRect.deltaH; + rect.minW = Math.min(rect.minW, contLayoutRect.maxW); + rect.minH = Math.min(rect.minH, contLayoutRect.maxH); + rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth); + rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight); + + // Resize container container if minSize was changed + if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { + rect.w = rect.minW; + rect.h = rect.minH; + + container.layoutRect(rect); + this.recalc(container); + + // Forced recalc for example if items are hidden/shown + if (container._lastRect === null) { + var parentCtrl = container.parent(); + if (parentCtrl) { + parentCtrl._lastRect = null; + parentCtrl.recalc(); + } + } + + return; + } + + // Update contentW/contentH so absEnd moves correctly + if (contLayoutRect.autoResize) { + rect = container.layoutRect(rect); + rect.contentW = rect.minW - contLayoutRect.deltaW; + rect.contentH = rect.minH - contLayoutRect.deltaH; + } + + var flexV; + + if (settings.packV == 'start') { + flexV = 0; + } else { + flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0; + } + + // Calculate totalFlex + var totalFlex = 0; + var flexWidths = settings.flexWidths; + if (flexWidths) { + for (x = 0; x < flexWidths.length; x++) { + totalFlex += flexWidths[x]; + } + } else { + totalFlex = cols; + } + + // Calculate new column widths based on flex values + var ratio = availableWidth / totalFlex; + for (x = 0; x < cols; x++) { + colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio; + } + + // Move/resize controls + posY = contPaddingBox.top; + for (y = 0; y < rows; y++) { + posX = contPaddingBox.left; + height = rowHeights[y] + flexV; + + for (x = 0; x < cols; x++) { + if (reverseRows) { + idx = y * cols + cols - 1 - x; + } else { + idx = y * cols + x; + } + + ctrl = items[idx]; + + // No more controls to render then break + if (!ctrl) { + break; + } + + // Get control settings and calculate x, y + ctrlSettings = ctrl.settings; + ctrlLayoutRect = ctrl.layoutRect(); + width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth); + ctrlLayoutRect.x = posX; + ctrlLayoutRect.y = posY; + + // Align control horizontal + align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null); + if (align == "center") { + ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2); + } else if (align == "right") { + ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w; + } else if (align == "stretch") { + ctrlLayoutRect.w = width; + } + + // Align control vertical + align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null); + if (align == "center") { + ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2); + } else if (align == "bottom") { + ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h; + } else if (align == "stretch") { + ctrlLayoutRect.h = height; + } + + ctrl.layoutRect(ctrlLayoutRect); + + posX += width + spacingH; + + if (ctrl.recalc) { + ctrl.recalc(); + } + } + + posY += height + spacingV; + } + } + }); + } +); + +/** + * Iframe.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 + */ + +/*jshint scripturl:true */ + +/** + * This class creates an iframe. + * + * @setting {String} url Url to open in the iframe. + * + * @-x-less Iframe.less + * @class tinymce.ui.Iframe + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Iframe', + [ + "tinymce.ui.Widget", + "tinymce.core.util.Delay" + ], + function (Widget, Delay) { + "use strict"; + + return Widget.extend({ + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this; + + self.classes.add('iframe'); + self.canFocus = false; + + /*eslint no-script-url:0 */ + return ( + '' + ); + }, + + /** + * Setter for the iframe source. + * + * @method src + * @param {String} src Source URL for iframe. + */ + src: function (src) { + this.getEl().src = src; + }, + + /** + * Inner HTML for the iframe. + * + * @method html + * @param {String} html HTML string to set as HTML inside the iframe. + * @param {function} callback Optional callback to execute when the iframe body is filled with contents. + * @return {tinymce.ui.Iframe} Current iframe control. + */ + html: function (html, callback) { + var self = this, body = this.getEl().contentWindow.document.body; + + // Wait for iframe to initialize IE 10 takes time + if (!body) { + Delay.setTimeout(function () { + self.html(html); + }); + } else { + body.innerHTML = html; + + if (callback) { + callback(); + } + } + + return this; + } + }); + } +); + +/** + * InfoBox.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 + */ + +/** + * .... + * + * @-x-less InfoBox.less + * @class tinymce.ui.InfoBox + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.InfoBox', + [ + "tinymce.ui.Widget" + ], + function (Widget) { + "use strict"; + + return Widget.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} multiline Multiline label. + */ + init: function (settings) { + var self = this; + + self._super(settings); + self.classes.add('widget').add('infobox'); + self.canFocus = false; + }, + + severity: function (level) { + this.classes.remove('error'); + this.classes.remove('warning'); + this.classes.remove('success'); + this.classes.add(level); + }, + + help: function (state) { + this.state.set('help', state); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, prefix = self.classPrefix; + + return ( + '
    ' + + '
    ' + + self.encode(self.state.get('text')) + + '' + + '
    ' + + '
    ' + ); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:text', function (e) { + self.getEl('body').firstChild.data = self.encode(e.value); + + if (self.state.get('rendered')) { + self.updateLayoutRect(); + } + }); + + self.state.on('change:help', function (e) { + self.classes.toggle('has-help', e.value); + + if (self.state.get('rendered')) { + self.updateLayoutRect(); + } + }); + + return self._super(); + } + }); + } +); + +/** + * Label.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 creates a label element. A label is a simple text control + * that can be bound to other controls. + * + * @-x-less Label.less + * @class tinymce.ui.Label + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Label', + [ + "tinymce.ui.Widget", + "tinymce.ui.DomUtils" + ], + function (Widget, DomUtils) { + "use strict"; + + return Widget.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} multiline Multiline label. + */ + init: function (settings) { + var self = this; + + self._super(settings); + self.classes.add('widget').add('label'); + self.canFocus = false; + + if (settings.multiline) { + self.classes.add('autoscroll'); + } + + if (settings.strong) { + self.classes.add('strong'); + } + }, + + /** + * Initializes the current controls layout rect. + * This will be executed by the layout managers to determine the + * default minWidth/minHeight etc. + * + * @method initLayoutRect + * @return {Object} Layout rect instance. + */ + initLayoutRect: function () { + var self = this, layoutRect = self._super(); + + if (self.settings.multiline) { + var size = DomUtils.getSize(self.getEl()); + + // Check if the text fits within maxW if not then try word wrapping it + if (size.width > layoutRect.maxW) { + layoutRect.minW = layoutRect.maxW; + self.classes.add('multiline'); + } + + self.getEl().style.width = layoutRect.minW + 'px'; + layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height); + } + + return layoutRect; + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this; + + if (!self.settings.multiline) { + self.getEl().style.lineHeight = self.layoutRect().h + 'px'; + } + + return self._super(); + }, + + severity: function (level) { + this.classes.remove('error'); + this.classes.remove('warning'); + this.classes.remove('success'); + this.classes.add(level); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, targetCtrl, forName, forId = self.settings.forId; + var text = self.settings.html ? self.settings.html : self.encode(self.state.get('text')); + + if (!forId && (forName = self.settings.forName)) { + targetCtrl = self.getRoot().find('#' + forName)[0]; + + if (targetCtrl) { + forId = targetCtrl._id; + } + } + + if (forId) { + return ( + '' + ); + } + + return ( + '' + + text + + '' + ); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:text', function (e) { + self.innerHtml(self.encode(e.value)); + + if (self.state.get('rendered')) { + self.updateLayoutRect(); + } + }); + + return self._super(); + } + }); + } +); + +/** + * Toolbar.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 new toolbar. + * + * @class tinymce.ui.Toolbar + * @extends tinymce.ui.Container + */ +define( + 'tinymce.ui.Toolbar', + [ + "tinymce.ui.Container" + ], + function (Container) { + "use strict"; + + return Container.extend({ + Defaults: { + role: 'toolbar', + layout: 'flow' + }, + + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + var self = this; + + self._super(settings); + self.classes.add('toolbar'); + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + self.items().each(function (ctrl) { + ctrl.classes.add('toolbar-item'); + }); + + return self._super(); + } + }); + } +); +/** + * MenuBar.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 new menubar. + * + * @-x-less MenuBar.less + * @class tinymce.ui.MenuBar + * @extends tinymce.ui.Container + */ +define( + 'tinymce.ui.MenuBar', + [ + "tinymce.ui.Toolbar" + ], + function (Toolbar) { + "use strict"; + + return Toolbar.extend({ + Defaults: { + role: 'menubar', + containerCls: 'menubar', + ariaRoot: true, + defaults: { + type: 'menubutton' + } + } + }); + } +); +/** + * MenuButton.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 new menu button. + * + * @-x-less MenuButton.less + * @class tinymce.ui.MenuButton + * @extends tinymce.ui.Button + */ +define( + 'tinymce.ui.MenuButton', + [ + 'global!window', + 'tinymce.core.ui.Factory', + 'tinymce.ui.Button', + 'tinymce.ui.MenuBar' + ], + function (window, Factory, Button, MenuBar) { + "use strict"; + + // TODO: Maybe add as some global function + function isChildOf(node, parent) { + while (node) { + if (parent === node) { + return true; + } + + node = node.parentNode; + } + + return false; + } + + var MenuButton = Button.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + var self = this; + + self._renderOpen = true; + + self._super(settings); + settings = self.settings; + + self.classes.add('menubtn'); + + if (settings.fixedWidth) { + self.classes.add('fixed-width'); + } + + self.aria('haspopup', true); + + self.state.set('menu', settings.menu || self.render()); + }, + + /** + * Shows the menu for the button. + * + * @method showMenu + */ + showMenu: function (toggle) { + var self = this, menu; + + if (self.menu && self.menu.visible() && toggle !== false) { + return self.hideMenu(); + } + + if (!self.menu) { + menu = self.state.get('menu') || []; + self.classes.add('opened'); + + // Is menu array then auto constuct menu control + if (menu.length) { + menu = { + type: 'menu', + animate: true, + items: menu + }; + } else { + menu.type = menu.type || 'menu'; + menu.animate = true; + } + + if (!menu.renderTo) { + self.menu = Factory.create(menu).parent(self).renderTo(); + } else { + self.menu = menu.parent(self).show().renderTo(); + } + + self.fire('createmenu'); + self.menu.reflow(); + self.menu.on('cancel', function (e) { + if (e.control.parent() === self.menu) { + e.stopPropagation(); + self.focus(); + self.hideMenu(); + } + }); + + // Move focus to button when a menu item is selected/clicked + self.menu.on('select', function () { + self.focus(); + }); + + self.menu.on('show hide', function (e) { + if (e.control === self.menu) { + self.activeMenu(e.type == 'show'); + self.classes.toggle('opened', e.type == 'show'); + } + + self.aria('expanded', e.type == 'show'); + }).fire('show'); + } + + self.menu.show(); + self.menu.layoutRect({ w: self.layoutRect().w }); + self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); + self.fire('showmenu'); + }, + + /** + * Hides the menu for the button. + * + * @method hideMenu + */ + hideMenu: function () { + var self = this; + + if (self.menu) { + self.menu.items().each(function (item) { + if (item.hideMenu) { + item.hideMenu(); + } + }); + + self.menu.hide(); + } + }, + + /** + * Sets the active menu state. + * + * @private + */ + activeMenu: function (state) { + this.classes.toggle('active', state); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix; + var icon = self.settings.icon, image, text = self.state.get('text'), + textHtml = ''; + + image = self.settings.image; + if (image) { + icon = 'none'; + + // Support for [high dpi, low dpi] image sources + if (typeof image != "string") { + image = window.getSelection ? image[0] : image[1]; + } + + image = ' style="background-image: url(\'' + image + '\')"'; + } else { + image = ''; + } + + if (text) { + self.classes.add('btn-has-text'); + textHtml = '' + self.encode(text) + ''; + } + + icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; + + self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button'); + + return ( + '
    ' + + '' + + '
    ' + ); + }, + + /** + * Gets invoked after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + self.on('click', function (e) { + if (e.control === self && isChildOf(e.target, self.getEl())) { + self.focus(); + self.showMenu(!e.aria); + + if (e.aria) { + self.menu.items().filter(':visible')[0].focus(); + } + } + }); + + self.on('mouseenter', function (e) { + var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu; + + if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) { + parent.items().filter('MenuButton').each(function (ctrl) { + if (ctrl.hideMenu && ctrl != overCtrl) { + if (ctrl.menu && ctrl.menu.visible()) { + hasVisibleSiblingMenu = true; + } + + ctrl.hideMenu(); + } + }); + + if (hasVisibleSiblingMenu) { + overCtrl.focus(); // Fix for: #5887 + overCtrl.showMenu(); + } + } + }); + + return self._super(); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:menu', function () { + if (self.menu) { + self.menu.remove(); + } + + self.menu = null; + }); + + return self._super(); + }, + + /** + * Removes the control and it's menus. + * + * @method remove + */ + remove: function () { + this._super(); + + if (this.menu) { + this.menu.remove(); + } + } + }); + + return MenuButton; + } +); + +/** + * MenuItem.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 new menu item. + * + * @-x-less MenuItem.less + * @class tinymce.ui.MenuItem + * @extends tinymce.ui.Control + */ +define( + 'tinymce.ui.MenuItem', + [ + "tinymce.ui.Widget", + "tinymce.core.ui.Factory", + "tinymce.core.Env", + "tinymce.core.util.Delay" + ], + function (Widget, Factory, Env, Delay) { + "use strict"; + + return Widget.extend({ + Defaults: { + border: 0, + role: 'menuitem' + }, + + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} selectable Selectable menu. + * @setting {Array} menu Submenu array with items. + * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X + */ + init: function (settings) { + var self = this, text; + + self._super(settings); + + settings = self.settings; + + self.classes.add('menu-item'); + + if (settings.menu) { + self.classes.add('menu-item-expand'); + } + + if (settings.preview) { + self.classes.add('menu-item-preview'); + } + + text = self.state.get('text'); + if (text === '-' || text === '|') { + self.classes.add('menu-item-sep'); + self.aria('role', 'separator'); + self.state.set('text', '-'); + } + + if (settings.selectable) { + self.aria('role', 'menuitemcheckbox'); + self.classes.add('menu-item-checkbox'); + settings.icon = 'selected'; + } + + if (!settings.preview && !settings.selectable) { + self.classes.add('menu-item-normal'); + } + + self.on('mousedown', function (e) { + e.preventDefault(); + }); + + if (settings.menu && !settings.ariaHideMenu) { + self.aria('haspopup', true); + } + }, + + /** + * Returns true/false if the menuitem has sub menu. + * + * @method hasMenus + * @return {Boolean} True/false state if it has submenu. + */ + hasMenus: function () { + return !!this.settings.menu; + }, + + /** + * Shows the menu for the menu item. + * + * @method showMenu + */ + showMenu: function () { + var self = this, settings = self.settings, menu, parent = self.parent(); + + parent.items().each(function (ctrl) { + if (ctrl !== self) { + ctrl.hideMenu(); + } + }); + + if (settings.menu) { + menu = self.menu; + + if (!menu) { + menu = settings.menu; + + // Is menu array then auto constuct menu control + if (menu.length) { + menu = { + type: 'menu', + animate: true, + items: menu + }; + } else { + menu.type = menu.type || 'menu'; + menu.animate = true; + } + + if (parent.settings.itemDefaults) { + menu.itemDefaults = parent.settings.itemDefaults; + } + + menu = self.menu = Factory.create(menu).parent(self).renderTo(); + menu.reflow(); + menu.on('cancel', function (e) { + e.stopPropagation(); + self.focus(); + menu.hide(); + }); + menu.on('show hide', function (e) { + if (e.control.items) { + e.control.items().each(function (ctrl) { + ctrl.active(ctrl.settings.selected); + }); + } + }).fire('show'); + + menu.on('hide', function (e) { + if (e.control === menu) { + self.classes.remove('selected'); + } + }); + + menu.submenu = true; + } else { + menu.show(); + } + + menu._parentMenu = parent; + + menu.classes.add('menu-sub'); + + var rel = menu.testMoveRel( + self.getEl(), + self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br'] + ); + + menu.moveRel(self.getEl(), rel); + menu.rel = rel; + + rel = 'menu-sub-' + rel; + menu.classes.remove(menu._lastRel).add(rel); + menu._lastRel = rel; + + self.classes.add('selected'); + self.aria('expanded', true); + } + }, + + /** + * Hides the menu for the menu item. + * + * @method hideMenu + */ + hideMenu: function () { + var self = this; + + if (self.menu) { + self.menu.items().each(function (item) { + if (item.hideMenu) { + item.hideMenu(); + } + }); + + self.menu.hide(); + self.aria('expanded', false); + } + + return self; + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.state.get('text'); + var icon = self.settings.icon, image = '', shortcut = settings.shortcut; + var url = self.encode(settings.url), iconHtml = ''; + + // Converts shortcut format to Mac/PC variants + function convertShortcut(shortcut) { + var i, value, replace = {}; + + if (Env.mac) { + replace = { + alt: '⌥', + ctrl: '⌘', + shift: '⇧', + meta: '⌘' + }; + } else { + replace = { + meta: 'Ctrl' + }; + } + + shortcut = shortcut.split('+'); + + for (i = 0; i < shortcut.length; i++) { + value = replace[shortcut[i].toLowerCase()]; + + if (value) { + shortcut[i] = value; + } + } + + return shortcut.join('+'); + } + + function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + function markMatches(text) { + var match = settings.match || ''; + + return match ? text.replace(new RegExp(escapeRegExp(match), 'gi'), function (match) { + return '!mce~match[' + match + ']mce~match!'; + }) : text; + } + + function boldMatches(text) { + return text. + replace(new RegExp(escapeRegExp('!mce~match['), 'g'), ''). + replace(new RegExp(escapeRegExp(']mce~match!'), 'g'), ''); + } + + if (icon) { + self.parent().classes.add('menu-has-icons'); + } + + if (settings.image) { + image = ' style="background-image: url(\'' + settings.image + '\')"'; + } + + if (shortcut) { + shortcut = convertShortcut(shortcut); + } + + icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none'); + iconHtml = (text !== '-' ? '\u00a0' : ''); + + text = boldMatches(self.encode(markMatches(text))); + url = boldMatches(self.encode(markMatches(url))); + + return ( + '
    ' + + iconHtml + + (text !== '-' ? '' + text + '' : '') + + (shortcut ? '
    ' + shortcut + '
    ' : '') + + (settings.menu ? '
    ' : '') + + (url ? '' : '') + + '
    ' + ); + }, + + /** + * Gets invoked after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this, settings = self.settings; + + var textStyle = settings.textStyle; + if (typeof textStyle == "function") { + textStyle = textStyle.call(this); + } + + if (textStyle) { + var textElm = self.getEl('text'); + if (textElm) { + textElm.setAttribute('style', textStyle); + } + } + + self.on('mouseenter click', function (e) { + if (e.control === self) { + if (!settings.menu && e.type === 'click') { + self.fire('select'); + + // Edge will crash if you stress it see #2660 + Delay.requestAnimationFrame(function () { + self.parent().hideAll(); + }); + } else { + self.showMenu(); + + if (e.aria) { + self.menu.focus(true); + } + } + } + }); + + self._super(); + + return self; + }, + + hover: function () { + var self = this; + + self.parent().items().each(function (ctrl) { + ctrl.classes.remove('selected'); + }); + + self.classes.toggle('selected', true); + + return self; + }, + + active: function (state) { + if (typeof state != "undefined") { + this.aria('checked', state); + } + + return this._super(state); + }, + + /** + * Removes the control and it's menus. + * + * @method remove + */ + remove: function () { + this._super(); + + if (this.menu) { + this.menu.remove(); + } + } + }); + } +); + +/** + * Menu.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 new menu. + * + * @-x-less Menu.less + * @class tinymce.ui.Menu + * @extends tinymce.ui.FloatPanel + */ +define( + 'tinymce.ui.Menu', + [ + 'tinymce.core.Env', + 'tinymce.core.util.Delay', + 'tinymce.core.util.Tools', + 'tinymce.ui.FloatPanel', + 'tinymce.ui.MenuItem', + 'tinymce.ui.Throbber' + ], + function (Env, Delay, Tools, FloatPanel, MenuItem, Throbber) { + "use strict"; + + return FloatPanel.extend({ + Defaults: { + defaultType: 'menuitem', + border: 1, + layout: 'stack', + role: 'application', + bodyRole: 'menu', + ariaRoot: true + }, + + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + */ + init: function (settings) { + var self = this; + + settings.autohide = true; + settings.constrainToViewport = true; + + if (typeof settings.items === 'function') { + settings.itemsFactory = settings.items; + settings.items = []; + } + + if (settings.itemDefaults) { + var items = settings.items, i = items.length; + + while (i--) { + items[i] = Tools.extend({}, settings.itemDefaults, items[i]); + } + } + + self._super(settings); + self.classes.add('menu'); + + if (settings.animate && Env.ie !== 11) { + // IE 11 can't handle transforms it looks horrible and blurry so lets disable that + self.classes.add('animate'); + } + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + this.classes.toggle('menu-align', true); + + this._super(); + + this.getEl().style.height = ''; + this.getEl('body').style.height = ''; + + return this; + }, + + /** + * Hides/closes the menu. + * + * @method cancel + */ + cancel: function () { + var self = this; + + self.hideAll(); + self.fire('select'); + }, + + /** + * Loads new items from the factory items function. + * + * @method load + */ + load: function () { + var self = this, time, factory; + + function hideThrobber() { + if (self.throbber) { + self.throbber.hide(); + self.throbber = null; + } + } + + factory = self.settings.itemsFactory; + if (!factory) { + return; + } + + if (!self.throbber) { + self.throbber = new Throbber(self.getEl('body'), true); + + if (self.items().length === 0) { + self.throbber.show(); + self.fire('loading'); + } else { + self.throbber.show(100, function () { + self.items().remove(); + self.fire('loading'); + }); + } + + self.on('hide close', hideThrobber); + } + + self.requestTime = time = new Date().getTime(); + + self.settings.itemsFactory(function (items) { + if (items.length === 0) { + self.hide(); + return; + } + + if (self.requestTime !== time) { + return; + } + + self.getEl().style.width = ''; + self.getEl('body').style.width = ''; + + hideThrobber(); + self.items().remove(); + self.getEl('body').innerHTML = ''; + + self.add(items); + self.renderNew(); + self.fire('loaded'); + }); + }, + + /** + * Hide menu and all sub menus. + * + * @method hideAll + */ + hideAll: function () { + var self = this; + + this.find('menuitem').exec('hideMenu'); + + return self._super(); + }, + + /** + * Invoked before the menu is rendered. + * + * @method preRender + */ + preRender: function () { + var self = this; + + self.items().each(function (ctrl) { + var settings = ctrl.settings; + + if (settings.icon || settings.image || settings.selectable) { + self._hasIcons = true; + return false; + } + }); + + if (self.settings.itemsFactory) { + self.on('postrender', function () { + if (self.settings.itemsFactory) { + self.load(); + } + }); + } + + self.on('show hide', function (e) { + if (e.control === self) { + if (e.type === 'show') { + Delay.setTimeout(function () { + self.classes.add('in'); + }, 0); + } else { + self.classes.remove('in'); + } + } + }); + + return self._super(); + } + }); + } +); + +/** + * ListBox.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 new list box control. + * + * @-x-less ListBox.less + * @class tinymce.ui.ListBox + * @extends tinymce.ui.MenuButton + */ +define( + 'tinymce.ui.ListBox', + [ + "tinymce.ui.MenuButton", + "tinymce.ui.Menu" + ], + function (MenuButton, Menu) { + "use strict"; + + return MenuButton.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Array} values Array with values to add to list box. + */ + init: function (settings) { + var self = this, values, selected, selectedText, lastItemCtrl; + + function setSelected(menuValues) { + // Try to find a selected value + for (var i = 0; i < menuValues.length; i++) { + selected = menuValues[i].selected || settings.value === menuValues[i].value; + + if (selected) { + selectedText = selectedText || menuValues[i].text; + self.state.set('value', menuValues[i].value); + return true; + } + + // If the value has a submenu, try to find the selected values in that menu + if (menuValues[i].menu) { + if (setSelected(menuValues[i].menu)) { + return true; + } + } + } + } + + self._super(settings); + settings = self.settings; + + self._values = values = settings.values; + if (values) { + if (typeof settings.value != "undefined") { + setSelected(values); + } + + // Default with first item + if (!selected && values.length > 0) { + selectedText = values[0].text; + self.state.set('value', values[0].value); + } + + self.state.set('menu', values); + } + + self.state.set('text', settings.text || selectedText); + + self.classes.add('listbox'); + + self.on('select', function (e) { + var ctrl = e.control; + + if (lastItemCtrl) { + e.lastControl = lastItemCtrl; + } + + if (settings.multiple) { + ctrl.active(!ctrl.active()); + } else { + self.value(e.control.value()); + } + + lastItemCtrl = ctrl; + }); + }, + + /** + * Getter/setter function for the control value. + * + * @method value + * @param {String} [value] Value to be set. + * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation. + */ + bindStates: function () { + var self = this; + + function activateMenuItemsByValue(menu, value) { + if (menu instanceof Menu) { + menu.items().each(function (ctrl) { + if (!ctrl.hasMenus()) { + ctrl.active(ctrl.value() === value); + } + }); + } + } + + function getSelectedItem(menuValues, value) { + var selectedItem; + + if (!menuValues) { + return; + } + + for (var i = 0; i < menuValues.length; i++) { + if (menuValues[i].value === value) { + return menuValues[i]; + } + + if (menuValues[i].menu) { + selectedItem = getSelectedItem(menuValues[i].menu, value); + if (selectedItem) { + return selectedItem; + } + } + } + } + + self.on('show', function (e) { + activateMenuItemsByValue(e.control, self.value()); + }); + + self.state.on('change:value', function (e) { + var selectedItem = getSelectedItem(self.state.get('menu'), e.value); + + if (selectedItem) { + self.text(selectedItem.text); + } else { + self.text(self.settings.text); + } + }); + + return self._super(); + } + }); + } +); + +/** + * Radio.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 new radio button. + * + * @-x-less Radio.less + * @class tinymce.ui.Radio + * @extends tinymce.ui.Checkbox + */ +define( + 'tinymce.ui.Radio', + [ + "tinymce.ui.Checkbox" + ], + function (Checkbox) { + "use strict"; + + return Checkbox.extend({ + Defaults: { + classes: "radio", + role: "radio" + } + }); + } +); +/** + * ResizeHandle.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 + */ + +/** + * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events. + * + * @-x-less ResizeHandle.less + * @class tinymce.ui.ResizeHandle + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.ResizeHandle', + [ + "tinymce.ui.Widget", + "tinymce.ui.DragHelper" + ], + function (Widget, DragHelper) { + "use strict"; + + return Widget.extend({ + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, prefix = self.classPrefix; + + self.classes.add('resizehandle'); + + if (self.settings.direction == "both") { + self.classes.add('resizehandle-both'); + } + + self.canFocus = false; + + return ( + '
    ' + + '' + + '
    ' + ); + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + self._super(); + + self.resizeDragHelper = new DragHelper(this._id, { + start: function () { + self.fire('ResizeStart'); + }, + + drag: function (e) { + if (self.settings.direction != "both") { + e.deltaX = 0; + } + + self.fire('Resize', e); + }, + + stop: function () { + self.fire('ResizeEnd'); + } + }); + }, + + remove: function () { + if (this.resizeDragHelper) { + this.resizeDragHelper.destroy(); + } + + return this._super(); + } + }); + } +); + +/** + * SelectBox.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 new select box control. + * + * @-x-less SelectBox.less + * @class tinymce.ui.SelectBox + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.SelectBox', + [ + "tinymce.ui.Widget" + ], + function (Widget) { + "use strict"; + + function createOptions(options) { + var strOptions = ''; + if (options) { + for (var i = 0; i < options.length; i++) { + strOptions += ''; + } + } + return strOptions; + } + + return Widget.extend({ + Defaults: { + classes: "selectbox", + role: "selectbox", + options: [] + }, + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Array} options Array with options to add to the select box. + */ + init: function (settings) { + var self = this; + + self._super(settings); + + if (self.settings.size) { + self.size = self.settings.size; + } + + if (self.settings.options) { + self._options = self.settings.options; + } + + self.on('keydown', function (e) { + var rootControl; + + if (e.keyCode == 13) { + e.preventDefault(); + + // Find root control that we can do toJSON on + self.parents().reverse().each(function (ctrl) { + if (ctrl.toJSON) { + rootControl = ctrl; + return false; + } + }); + + // Fire event on current text box with the serialized data of the whole form + self.fire('submit', { data: rootControl.toJSON() }); + } + }); + }, + + /** + * Getter/setter function for the options state. + * + * @method options + * @param {Array} [state] State to be set. + * @return {Array|tinymce.ui.SelectBox} Array of string options. + */ + options: function (state) { + if (!arguments.length) { + return this.state.get('options'); + } + + this.state.set('options', state); + + return this; + }, + + renderHtml: function () { + var self = this, options, size = ''; + + options = createOptions(self._options); + + if (self.size) { + size = ' size = "' + self.size + '"'; + } + + return ( + '' + ); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:options', function (e) { + self.getEl().innerHTML = createOptions(e.value); + }); + + return self._super(); + } + }); + } +); + +/** + * Slider.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 + */ + +/** + * Slider control. + * + * @-x-less Slider.less + * @class tinymce.ui.Slider + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Slider', + [ + "tinymce.ui.Widget", + "tinymce.ui.DragHelper", + "tinymce.ui.DomUtils" + ], + function (Widget, DragHelper, DomUtils) { + "use strict"; + + function constrain(value, minVal, maxVal) { + if (value < minVal) { + value = minVal; + } + + if (value > maxVal) { + value = maxVal; + } + + return value; + } + + function setAriaProp(el, name, value) { + el.setAttribute('aria-' + name, value); + } + + function updateSliderHandle(ctrl, value) { + var maxHandlePos, shortSizeName, sizeName, stylePosName, styleValue, handleEl; + + if (ctrl.settings.orientation == "v") { + stylePosName = "top"; + sizeName = "height"; + shortSizeName = "h"; + } else { + stylePosName = "left"; + sizeName = "width"; + shortSizeName = "w"; + } + + handleEl = ctrl.getEl('handle'); + maxHandlePos = (ctrl.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName]; + + styleValue = (maxHandlePos * ((value - ctrl._minValue) / (ctrl._maxValue - ctrl._minValue))) + 'px'; + handleEl.style[stylePosName] = styleValue; + handleEl.style.height = ctrl.layoutRect().h + 'px'; + + setAriaProp(handleEl, 'valuenow', value); + setAriaProp(handleEl, 'valuetext', '' + ctrl.settings.previewFilter(value)); + setAriaProp(handleEl, 'valuemin', ctrl._minValue); + setAriaProp(handleEl, 'valuemax', ctrl._maxValue); + } + + return Widget.extend({ + init: function (settings) { + var self = this; + + if (!settings.previewFilter) { + settings.previewFilter = function (value) { + return Math.round(value * 100) / 100.0; + }; + } + + self._super(settings); + self.classes.add('slider'); + + if (settings.orientation == "v") { + self.classes.add('vertical'); + } + + self._minValue = settings.minValue || 0; + self._maxValue = settings.maxValue || 100; + self._initValue = self.state.get('value'); + }, + + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix; + + return ( + '
    ' + + '
    ' + + '
    ' + ); + }, + + reset: function () { + this.value(this._initValue).repaint(); + }, + + postRender: function () { + var self = this, minValue, maxValue, screenCordName, + stylePosName, sizeName, shortSizeName; + + function toFraction(min, max, val) { + return (val + min) / (max - min); + } + + function fromFraction(min, max, val) { + return (val * (max - min)) - min; + } + + function handleKeyboard(minValue, maxValue) { + function alter(delta) { + var value; + + value = self.value(); + value = fromFraction(minValue, maxValue, toFraction(minValue, maxValue, value) + (delta * 0.05)); + value = constrain(value, minValue, maxValue); + + self.value(value); + + self.fire('dragstart', { value: value }); + self.fire('drag', { value: value }); + self.fire('dragend', { value: value }); + } + + self.on('keydown', function (e) { + switch (e.keyCode) { + case 37: + case 38: + alter(-1); + break; + + case 39: + case 40: + alter(1); + break; + } + }); + } + + function handleDrag(minValue, maxValue, handleEl) { + var startPos, startHandlePos, maxHandlePos, handlePos, value; + + self._dragHelper = new DragHelper(self._id, { + handle: self._id + "-handle", + + start: function (e) { + startPos = e[screenCordName]; + startHandlePos = parseInt(self.getEl('handle').style[stylePosName], 10); + maxHandlePos = (self.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName]; + self.fire('dragstart', { value: value }); + }, + + drag: function (e) { + var delta = e[screenCordName] - startPos; + + handlePos = constrain(startHandlePos + delta, 0, maxHandlePos); + handleEl.style[stylePosName] = handlePos + 'px'; + + value = minValue + (handlePos / maxHandlePos) * (maxValue - minValue); + self.value(value); + + self.tooltip().text('' + self.settings.previewFilter(value)).show().moveRel(handleEl, 'bc tc'); + + self.fire('drag', { value: value }); + }, + + stop: function () { + self.tooltip().hide(); + self.fire('dragend', { value: value }); + } + }); + } + + minValue = self._minValue; + maxValue = self._maxValue; + + if (self.settings.orientation == "v") { + screenCordName = "screenY"; + stylePosName = "top"; + sizeName = "height"; + shortSizeName = "h"; + } else { + screenCordName = "screenX"; + stylePosName = "left"; + sizeName = "width"; + shortSizeName = "w"; + } + + self._super(); + + handleKeyboard(minValue, maxValue, self.getEl('handle')); + handleDrag(minValue, maxValue, self.getEl('handle')); + }, + + repaint: function () { + this._super(); + updateSliderHandle(this, this.value()); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:value', function (e) { + updateSliderHandle(self, e.value); + }); + + return self._super(); + } + }); + } +); +/** + * Spacer.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 spacer. This control is used in flex layouts for example. + * + * @-x-less Spacer.less + * @class tinymce.ui.Spacer + * @extends tinymce.ui.Widget + */ +define( + 'tinymce.ui.Spacer', + [ + "tinymce.ui.Widget" + ], + function (Widget) { + "use strict"; + + return Widget.extend({ + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this; + + self.classes.add('spacer'); + self.canFocus = false; + + return '
    '; + } + }); + } +); +/** + * SplitButton.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 split button. + * + * @-x-less SplitButton.less + * @class tinymce.ui.SplitButton + * @extends tinymce.ui.Button + */ +define( + 'tinymce.ui.SplitButton', + [ + 'global!window', + 'tinymce.core.dom.DomQuery', + 'tinymce.ui.DomUtils', + 'tinymce.ui.MenuButton' + ], + function (window, DomQuery, DomUtils, MenuButton) { + return MenuButton.extend({ + Defaults: { + classes: "widget btn splitbtn", + role: "button" + }, + + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm; + + self._super(); + + mainButtonElm = elm.firstChild; + menuButtonElm = elm.lastChild; + + DomQuery(mainButtonElm).css({ + width: rect.w - DomUtils.getSize(menuButtonElm).width, + height: rect.h - 2 + }); + + DomQuery(menuButtonElm).css({ + height: rect.h - 2 + }); + + return self; + }, + + /** + * Sets the active menu state. + * + * @private + */ + activeMenu: function (state) { + var self = this; + + DomQuery(self.getEl().lastChild).toggleClass(self.classPrefix + 'active', state); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, id = self._id, prefix = self.classPrefix, image; + var icon = self.state.get('icon'), text = self.state.get('text'), + textHtml = ''; + + image = self.settings.image; + if (image) { + icon = 'none'; + + // Support for [high dpi, low dpi] image sources + if (typeof image != "string") { + image = window.getSelection ? image[0] : image[1]; + } + + image = ' style="background-image: url(\'' + image + '\')"'; + } else { + image = ''; + } + + icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; + + if (text) { + self.classes.add('btn-has-text'); + textHtml = '' + self.encode(text) + ''; + } + + return ( + '
    ' + + '' + + '' + + '
    ' + ); + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this, onClickHandler = self.settings.onclick; + + self.on('click', function (e) { + var node = e.target; + + if (e.control == this) { + // Find clicks that is on the main button + while (node) { + if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) { + e.stopImmediatePropagation(); + + if (onClickHandler) { + onClickHandler.call(this, e); + } + + return; + } + + node = node.parentNode; + } + } + }); + + delete self.settings.onclick; + + return self._super(); + } + }); + } +); +/** + * StackLayout.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 layout uses the browsers layout when the items are blocks. + * + * @-x-less StackLayout.less + * @class tinymce.ui.StackLayout + * @extends tinymce.ui.FlowLayout + */ +define( + 'tinymce.ui.StackLayout', + [ + "tinymce.ui.FlowLayout" + ], + function (FlowLayout) { + "use strict"; + + return FlowLayout.extend({ + Defaults: { + containerClass: 'stack-layout', + controlClass: 'stack-layout-item', + endClass: 'break' + }, + + isNative: function () { + return true; + } + }); + } +); +/** + * TabPanel.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 tab panel control. + * + * @-x-less TabPanel.less + * @class tinymce.ui.TabPanel + * @extends tinymce.ui.Panel + * + * @setting {Number} activeTab Active tab index. + */ +define( + 'tinymce.ui.TabPanel', + [ + "tinymce.ui.Panel", + "tinymce.core.dom.DomQuery", + "tinymce.ui.DomUtils" + ], + function (Panel, $, DomUtils) { + "use strict"; + + return Panel.extend({ + Defaults: { + layout: 'absolute', + defaults: { + type: 'panel' + } + }, + + /** + * Activates the specified tab by index. + * + * @method activateTab + * @param {Number} idx Index of the tab to activate. + */ + activateTab: function (idx) { + var activeTabElm; + + if (this.activeTabId) { + activeTabElm = this.getEl(this.activeTabId); + $(activeTabElm).removeClass(this.classPrefix + 'active'); + activeTabElm.setAttribute('aria-selected', "false"); + } + + this.activeTabId = 't' + idx; + + activeTabElm = this.getEl('t' + idx); + activeTabElm.setAttribute('aria-selected', "true"); + $(activeTabElm).addClass(this.classPrefix + 'active'); + + this.items()[idx].show().fire('showtab'); + this.reflow(); + + this.items().each(function (item, i) { + if (idx != i) { + item.hide(); + } + }); + }, + + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix; + + self.preRender(); + layout.preRender(self); + + self.items().each(function (ctrl, i) { + var id = self._id + '-t' + i; + + ctrl.aria('role', 'tabpanel'); + ctrl.aria('labelledby', id); + + tabsHtml += ( + '' + ); + }); + + return ( + '
    ' + + '
    ' + + tabsHtml + + '
    ' + + '
    ' + + layout.renderHtml(self) + + '
    ' + + '
    ' + ); + }, + + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; + + self._super(); + + self.settings.activeTab = self.settings.activeTab || 0; + self.activateTab(self.settings.activeTab); + + this.on('click', function (e) { + var targetParent = e.target.parentNode; + + if (targetParent && targetParent.id == self._id + '-head') { + var i = targetParent.childNodes.length; - onResize: function (e) { - if (settings.resize === 'both') { - Resize.resizeTo(editor, startSize.width + e.deltaX, startSize.height + e.deltaY); - } else { - Resize.resizeTo(editor, null, startSize.height + e.deltaY); + while (i--) { + if (targetParent.childNodes[i] == e.target) { + self.activateTab(i); + } } } - }; - } - - // Add statusbar if needed - if (settings.statusbar !== false) { - panel.add({ - type: 'panel', name: 'statusbar', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', ariaRoot: true, items: [ - { type: 'elementpath', editor: editor }, - resizeHandleCtrl - ] }); - } + }, - editor.fire('BeforeRenderUI'); - editor.on('SwitchMode', switchMode(panel)); - panel.renderBefore(args.targetNode).reflow(); + /** + * Initializes the current controls layout rect. + * This will be executed by the layout managers to determine the + * default minWidth/minHeight etc. + * + * @method initLayoutRect + * @return {Object} Layout rect instance. + */ + initLayoutRect: function () { + var self = this, rect, minW, minH; - if (settings.readonly) { - editor.setMode('readonly'); - } + minW = DomUtils.getSize(self.getEl('head')).width; + minW = minW < 0 ? 0 : minW; + minH = 0; - if (args.width) { - DOM.setStyle(panel.getEl(), 'width', args.width); - } + self.items().each(function (item) { + minW = Math.max(minW, item.layoutRect().minW); + minH = Math.max(minH, item.layoutRect().minH); + }); - // Remove the panel when the editor is removed - editor.on('remove', function () { - panel.remove(); - panel = null; - }); + self.items().each(function (ctrl) { + ctrl.settings.x = 0; + ctrl.settings.y = 0; + ctrl.settings.w = minW; + ctrl.settings.h = minH; - // Add accesibility shortcuts - A11y.addKeys(editor, panel); - ContextToolbars.addContextualToolbars(editor); - Branding.setup(editor); + ctrl.layoutRect({ + x: 0, + y: 0, + w: minW, + h: minH + }); + }); - return { - iframeContainer: panel.find('#iframe')[0].getEl(), - editorContainer: panel.getEl() - }; - }; + var headH = DomUtils.getSize(self.getEl('head')).height; - return { - render: render - }; + self.settings.minWidth = minW; + self.settings.minHeight = minH + headH; + + rect = self._super(); + rect.deltaH += headH; + rect.innerH = rect.h - rect.deltaH; + + return rect; + } + }); } ); /** - * ResolveGlobal.js + * TextBox.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -1384,224 +17611,379 @@ define( * Contributing: http://www.tinymce.com/contributing */ -define( - 'tinymce.core.ui.FloatPanel', - [ - 'global!tinymce.util.Tools.resolve' - ], - function (resolve) { - return resolve('tinymce.ui.FloatPanel'); - } -); - /** - * Inline.js - * - * Released under LGPL License. - * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * Creates a new textbox. * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing + * @-x-less TextBox.less + * @class tinymce.ui.TextBox + * @extends tinymce.ui.Widget */ - define( - 'tinymce.themes.modern.modes.Inline', + 'tinymce.ui.TextBox', [ + 'global!document', 'tinymce.core.util.Tools', - 'tinymce.core.ui.Factory', - 'tinymce.core.dom.DOMUtils', - 'tinymce.core.ui.FloatPanel', - 'tinymce.themes.modern.ui.Toolbar', - 'tinymce.themes.modern.ui.Menubar', - 'tinymce.themes.modern.ui.ContextToolbars', - 'tinymce.themes.modern.ui.A11y', - 'tinymce.themes.modern.ui.SkinLoaded' + 'tinymce.ui.DomUtils', + 'tinymce.ui.Widget' ], - function (Tools, Factory, DOMUtils, FloatPanel, Toolbar, Menubar, ContextToolbars, A11y, SkinLoaded) { - var render = function (editor, theme, args) { - var panel, inlineToolbarContainer, settings = editor.settings; - var DOM = DOMUtils.DOM; + function (document, Tools, DomUtils, Widget) { + return Widget.extend({ + /** + * Constructs a instance with the specified settings. + * + * @constructor + * @param {Object} settings Name/value object with settings. + * @setting {Boolean} multiline True if the textbox is a multiline control. + * @setting {Number} maxLength Max length for the textbox. + * @setting {Number} size Size of the textbox in characters. + */ + init: function (settings) { + var self = this; - if (settings.fixed_toolbar_container) { - inlineToolbarContainer = DOM.select(settings.fixed_toolbar_container)[0]; - } + self._super(settings); - var reposition = function () { - if (panel && panel.moveRel && panel.visible() && !panel._fixed) { - // TODO: This is kind of ugly and doesn't handle multiple scrollable elements - var scrollContainer = editor.selection.getScrollContainer(), body = editor.getBody(); - var deltaX = 0, deltaY = 0; + self.classes.add('textbox'); - if (scrollContainer) { - var bodyPos = DOM.getPos(body), scrollContainerPos = DOM.getPos(scrollContainer); + if (settings.multiline) { + self.classes.add('multiline'); + } else { + self.on('keydown', function (e) { + var rootControl; - deltaX = Math.max(0, scrollContainerPos.x - bodyPos.x); - deltaY = Math.max(0, scrollContainerPos.y - bodyPos.y); - } + if (e.keyCode == 13) { + e.preventDefault(); - panel.fixed(false).moveRel(body, editor.rtl ? ['tr-br', 'br-tr'] : ['tl-bl', 'bl-tl', 'tr-br']).moveBy(deltaX, deltaY); - } - }; + // Find root control that we can do toJSON on + self.parents().reverse().each(function (ctrl) { + if (ctrl.toJSON) { + rootControl = ctrl; + return false; + } + }); - var show = function () { - if (panel) { - panel.show(); - reposition(); - DOM.addClass(editor.getBody(), 'mce-edit-focus'); + // Fire event on current text box with the serialized data of the whole form + self.fire('submit', { data: rootControl.toJSON() }); + } + }); + + self.on('keyup', function (e) { + self.state.set('value', e.target.value); + }); } - }; + }, - var hide = function () { - if (panel) { - // We require two events as the inline float panel based toolbar does not have autohide=true - panel.hide(); + /** + * Repaints the control after a layout operation. + * + * @method repaint + */ + repaint: function () { + var self = this, style, rect, borderBox, borderW, borderH = 0, lastRepaintRect; - // All other autohidden float panels will be closed below. - FloatPanel.hideAll(); + style = self.getEl().style; + rect = self._layoutRect; + lastRepaintRect = self._lastRepaintRect || {}; - DOM.removeClass(editor.getBody(), 'mce-edit-focus'); + // Detect old IE 7+8 add lineHeight to align caret vertically in the middle + var doc = document; + if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) { + style.lineHeight = (rect.h - borderH) + 'px'; } - }; - var render = function () { - if (panel) { - if (!panel.visible()) { - show(); - } + borderBox = self.borderBox; + borderW = borderBox.left + borderBox.right + 8; + borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0); - return; + if (rect.x !== lastRepaintRect.x) { + style.left = rect.x + 'px'; + lastRepaintRect.x = rect.x; } - // Render a plain panel inside the inlineToolbarContainer if it's defined - panel = theme.panel = Factory.create({ - type: inlineToolbarContainer ? 'panel' : 'floatpanel', - role: 'application', - classes: 'tinymce tinymce-inline', - layout: 'flex', - direction: 'column', - align: 'stretch', - autohide: false, - autofix: true, - fixed: !!inlineToolbarContainer, - border: 1, - items: [ - settings.menubar === false ? null : { type: 'menubar', border: '0 0 1 0', items: Menubar.createMenuButtons(editor) }, - Toolbar.createToolbars(editor, settings.toolbar_items_size) - ] - }); + if (rect.y !== lastRepaintRect.y) { + style.top = rect.y + 'px'; + lastRepaintRect.y = rect.y; + } - // Add statusbar - /*if (settings.statusbar !== false) { - panel.add({type: 'panel', classes: 'statusbar', layout: 'flow', border: '1 0 0 0', items: [ - {type: 'elementpath'} - ]}); - }*/ + if (rect.w !== lastRepaintRect.w) { + style.width = (rect.w - borderW) + 'px'; + lastRepaintRect.w = rect.w; + } - editor.fire('BeforeRenderUI'); - panel.renderTo(inlineToolbarContainer || document.body).reflow(); + if (rect.h !== lastRepaintRect.h) { + style.height = (rect.h - borderH) + 'px'; + lastRepaintRect.h = rect.h; + } - A11y.addKeys(editor, panel); - show(); - ContextToolbars.addContextualToolbars(editor); + self._lastRepaintRect = lastRepaintRect; + self.fire('repaint', {}, false); - editor.on('nodeChange', reposition); - editor.on('activate', show); - editor.on('deactivate', hide); + return self; + }, - editor.nodeChanged(); - }; + /** + * Renders the control as a HTML string. + * + * @method renderHtml + * @return {String} HTML representing the control. + */ + renderHtml: function () { + var self = this, settings = self.settings, attrs, elm; - settings.content_editable = true; + attrs = { + id: self._id, + hidefocus: '1' + }; - editor.on('focus', function () { - // Render only when the CSS file has been loaded - if (args.skinUiCss) { - DOM.styleSheetLoader.load(args.skinUiCss, render, render); - } else { - render(); + Tools.each([ + 'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min', + 'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple' + ], function (name) { + attrs[name] = settings[name]; + }); + + if (self.disabled()) { + attrs.disabled = 'disabled'; } - }); - editor.on('blur hide', hide); + if (settings.subtype) { + attrs.type = settings.subtype; + } - // Remove the panel when the editor is removed - editor.on('remove', function () { - if (panel) { - panel.remove(); - panel = null; + elm = DomUtils.create(settings.multiline ? 'textarea' : 'input', attrs); + elm.value = self.state.get('value'); + elm.className = self.classes; + + return elm.outerHTML; + }, + + value: function (value) { + if (arguments.length) { + this.state.set('value', value); + return this; } - }); - // Preload skin css - if (args.skinUiCss) { - DOM.styleSheetLoader.load(args.skinUiCss, SkinLoaded.fireSkinLoaded(editor)); - } + // Make sure the real state is in sync + if (this.state.get('rendered')) { + this.state.set('value', this.getEl().value); + } - return {}; - }; + return this.state.get('value'); + }, - return { - render: render - }; - } -); + /** + * Called after the control has been rendered. + * + * @method postRender + */ + postRender: function () { + var self = this; -/** - * 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 - */ + self.getEl().value = self.state.get('value'); + self._super(); -define( - 'tinymce.core.ui.Throbber', - [ - 'global!tinymce.util.Tools.resolve' - ], - function (resolve) { - return resolve('tinymce.ui.Throbber'); + self.$el.on('change', function (e) { + self.state.set('value', e.target.value); + self.fire('change', e); + }); + }, + + bindStates: function () { + var self = this; + + self.state.on('change:value', function (e) { + if (self.getEl().value != e.value) { + self.getEl().value = e.value; + } + }); + + self.state.on('change:disabled', function (e) { + self.getEl().disabled = e.value; + }); + + return self._super(); + }, + + remove: function () { + this.$el.off(); + this._super(); + } + }); } ); /** - * ProgressState.js + * Api.js * * Released under LGPL License. - * Copyright (c) 1999-2016 Ephox Corp. All rights reserved + * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ define( - 'tinymce.themes.modern.ui.ProgressState', + 'tinymce.ui.Api', [ - 'tinymce.core.ui.Throbber' + 'tinymce.core.ui.Factory', + 'tinymce.core.util.Tools', + 'tinymce.ui.AbsoluteLayout', + 'tinymce.ui.BrowseButton', + 'tinymce.ui.Button', + 'tinymce.ui.ButtonGroup', + 'tinymce.ui.Checkbox', + 'tinymce.ui.Collection', + 'tinymce.ui.ColorBox', + 'tinymce.ui.ColorButton', + 'tinymce.ui.ColorPicker', + 'tinymce.ui.ComboBox', + 'tinymce.ui.Container', + 'tinymce.ui.Control', + 'tinymce.ui.DragHelper', + 'tinymce.ui.DropZone', + 'tinymce.ui.ElementPath', + 'tinymce.ui.FieldSet', + 'tinymce.ui.FilePicker', + 'tinymce.ui.FitLayout', + 'tinymce.ui.FlexLayout', + 'tinymce.ui.FloatPanel', + 'tinymce.ui.FlowLayout', + 'tinymce.ui.Form', + 'tinymce.ui.FormatControls', + 'tinymce.ui.FormItem', + 'tinymce.ui.GridLayout', + 'tinymce.ui.Iframe', + 'tinymce.ui.InfoBox', + 'tinymce.ui.KeyboardNavigation', + 'tinymce.ui.Label', + 'tinymce.ui.Layout', + 'tinymce.ui.ListBox', + 'tinymce.ui.Menu', + 'tinymce.ui.MenuBar', + 'tinymce.ui.MenuButton', + 'tinymce.ui.MenuItem', + 'tinymce.ui.MessageBox', + 'tinymce.ui.Movable', + 'tinymce.ui.Notification', + 'tinymce.ui.Panel', + 'tinymce.ui.PanelButton', + 'tinymce.ui.Path', + 'tinymce.ui.Progress', + 'tinymce.ui.Radio', + 'tinymce.ui.ReflowQueue', + 'tinymce.ui.Resizable', + 'tinymce.ui.ResizeHandle', + 'tinymce.ui.Scrollable', + 'tinymce.ui.SelectBox', + 'tinymce.ui.Selector', + 'tinymce.ui.Slider', + 'tinymce.ui.Spacer', + 'tinymce.ui.SplitButton', + 'tinymce.ui.StackLayout', + 'tinymce.ui.TabPanel', + 'tinymce.ui.TextBox', + 'tinymce.ui.Throbber', + 'tinymce.ui.Toolbar', + 'tinymce.ui.Tooltip', + 'tinymce.ui.Widget', + 'tinymce.ui.Window' ], - function (Throbber) { - var setup = function (editor, theme) { - var throbber; + function ( + Factory, Tools, AbsoluteLayout, BrowseButton, Button, ButtonGroup, Checkbox, Collection, ColorBox, ColorButton, ColorPicker, ComboBox, Container, Control, + DragHelper, DropZone, ElementPath, FieldSet, FilePicker, FitLayout, FlexLayout, FloatPanel, FlowLayout, Form, FormatControls, FormItem, GridLayout, Iframe, + InfoBox, KeyboardNavigation, Label, Layout, ListBox, Menu, MenuBar, MenuButton, MenuItem, MessageBox, Movable, Notification, Panel, PanelButton, Path, Progress, + Radio, ReflowQueue, Resizable, ResizeHandle, Scrollable, SelectBox, Selector, Slider, Spacer, SplitButton, StackLayout, TabPanel, TextBox, Throbber, Toolbar, + Tooltip, Widget, Window + ) { + var getApi = function () { + return { + Selector: Selector, + Collection: Collection, + ReflowQueue: ReflowQueue, + Control: Control, + Factory: Factory, + KeyboardNavigation: KeyboardNavigation, + Container: Container, + DragHelper: DragHelper, + Scrollable: Scrollable, + Panel: Panel, + Movable: Movable, + Resizable: Resizable, + FloatPanel: FloatPanel, + Window: Window, + MessageBox: MessageBox, + Tooltip: Tooltip, + Widget: Widget, + Progress: Progress, + Notification: Notification, + Layout: Layout, + AbsoluteLayout: AbsoluteLayout, + Button: Button, + ButtonGroup: ButtonGroup, + Checkbox: Checkbox, + ComboBox: ComboBox, + ColorBox: ColorBox, + PanelButton: PanelButton, + ColorButton: ColorButton, + ColorPicker: ColorPicker, + Path: Path, + ElementPath: ElementPath, + FormItem: FormItem, + Form: Form, + FieldSet: FieldSet, + FilePicker: FilePicker, + FitLayout: FitLayout, + FlexLayout: FlexLayout, + FlowLayout: FlowLayout, + FormatControls: FormatControls, + GridLayout: GridLayout, + Iframe: Iframe, + InfoBox: InfoBox, + Label: Label, + Toolbar: Toolbar, + MenuBar: MenuBar, + MenuButton: MenuButton, + MenuItem: MenuItem, + Throbber: Throbber, + Menu: Menu, + ListBox: ListBox, + Radio: Radio, + ResizeHandle: ResizeHandle, + SelectBox: SelectBox, + Slider: Slider, + Spacer: Spacer, + SplitButton: SplitButton, + StackLayout: StackLayout, + TabPanel: TabPanel, + TextBox: TextBox, + DropZone: DropZone, + BrowseButton: BrowseButton + }; + }; - editor.on('ProgressState', function (e) { - throbber = throbber || new Throbber(theme.panel.getEl('body')); + var appendTo = function (target) { + if (target.ui) { + Tools.each(getApi(), function (ref, key) { + target.ui[key] = ref; + }); + } else { + target.ui = getApi(); + } + }; - if (e.state) { - throbber.show(e.time); - } else { - throbber.hide(); - } + var registerToFactory = function () { + Tools.each(getApi(), function (ref, key) { + Factory.add(key, ref); }); }; - return { - setup: setup + var Api = { + appendTo: appendTo, + registerToFactory: registerToFactory }; + + return Api; } ); - /** * Theme.js * @@ -1616,64 +17998,21 @@ define( 'tinymce.themes.modern.Theme', [ 'global!window', - 'tinymce.core.AddOnManager', - 'tinymce.core.EditorManager', - 'tinymce.core.Env', - 'tinymce.core.ui.Api', - 'tinymce.themes.modern.modes.Iframe', - 'tinymce.themes.modern.modes.Inline', - 'tinymce.themes.modern.ui.ProgressState', - 'tinymce.themes.modern.ui.Resize' + 'tinymce.core.ThemeManager', + 'tinymce.themes.modern.api.ThemeApi', + 'tinymce.ui.Api', + 'tinymce.ui.FormatControls' ], - function (window, AddOnManager, EditorManager, Env, Api, Iframe, Inline, ProgressState, Resize) { - var ThemeManager = AddOnManager.ThemeManager; - + function (window, ThemeManager, ThemeApi, Api, FormatControls) { + Api.registerToFactory(); Api.appendTo(window.tinymce ? window.tinymce : {}); - var renderUI = function (editor, theme, args) { - var settings = editor.settings; - var skin = settings.skin !== false ? settings.skin || 'lightgray' : false; - - if (skin) { - var skinUrl = settings.skin_url; - - if (skinUrl) { - skinUrl = editor.documentBaseURI.toAbsolute(skinUrl); - } else { - skinUrl = EditorManager.baseURL + '/skins/' + skin; - } - - args.skinUiCss = skinUrl + '/skin.min.css'; - - // Load content.min.css or content.inline.min.css - editor.contentCSS.push(skinUrl + '/content' + (editor.inline ? '.inline' : '') + '.min.css'); - } - - ProgressState.setup(editor, theme); - - if (settings.inline) { - return Inline.render(editor, theme, args); - } - - return Iframe.render(editor, theme, args); - }; - ThemeManager.add('modern', function (editor) { - return { - renderUI: function (args) { - return renderUI(editor, this, args); - }, - resizeTo: function (w, h) { - return Resize.resizeTo(editor, w, h); - }, - resizeBy: function (dw, dh) { - return Resize.resizeBy(editor, dw, dh); - } - }; + FormatControls.setup(editor); + return ThemeApi.get(editor); }); - return function () { - }; + return function () { }; } ); diff --git a/media/vendor/tinymce/themes/modern/theme.min.js b/media/vendor/tinymce/themes/modern/theme.min.js index 524b421e8647b..3a6d2eb542fc0 100644 --- a/media/vendor/tinymce/themes/modern/theme.min.js +++ b/media/vendor/tinymce/themes/modern/theme.min.js @@ -1 +1,5 @@ -!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=0;c--)for(d=f.length-1;d>=0;d--)if(f[d].predicate(e[c]))return{toolbar:f[d],element:e[c]};return null};a.on("click keyup setContent ObjectResized",function(b){("setcontent"!==b.type||b.selection)&&c.setEditorTimeout(a,function(){var b;b=u(a.selection.getNode()),b?(t(),s(b)):t()})}),a.on("blur hide contextmenu",t),a.on("ObjectResizeStart",function(){var b=u(a.selection.getNode());b&&b.toolbar.panel&&b.toolbar.panel.hide()}),a.on("ResizeEditor ResizeWindow",q(!0)),a.on("nodeChange",q(!1)),a.on("remove",function(){b.each(n(),function(a){a.panel&&a.panel.remove()}),a.contextToolbars={}}),a.shortcuts.add("ctrl+shift+e > ctrl+shift+p","",function(){var b=u(a.selection.getNode());b&&b.toolbar.panel&&b.toolbar.panel.items()[0].focus()})};return{addContextualToolbars:m}}),g("h",["d"],function(a){var b={file:{title:"File",items:"newdocument"},edit:{title:"Edit",items:"undo redo | cut copy paste pastetext | selectall"},insert:{title:"Insert",items:"|"},view:{title:"View",items:"visualaid |"},format:{title:"Format",items:"bold italic underline strikethrough superscript subscript | formats | removeformat"},table:{title:"Table"},tools:{title:"Tools"}},c=function(a,b){var c;return"|"==b?{text:"|"}:c=a[b]},d=function(d,e,f){var g,h,i,j,k;if(k=a.makeMap((e.removed_menuitems||"").split(/[ ,]/)),e.menu?(h=e.menu[f],j=!0):h=b[f],h){g={text:h.title},i=[],a.each((h.items||"").split(/[ ,]/),function(a){var b=c(d,a);b&&!k[a]&&i.push(c(d,a))}),j||a.each(d,function(a){a.context==f&&("before"==a.separator&&i.push({text:"|"}),a.prependToContext?i.unshift(a):i.push(a),"after"==a.separator&&i.push({text:"|"}))});for(var l=0;l=11},k=function(a){return!(!j()||!a.sidebars)&&a.sidebars.length>0},l=function(b){var c=a.map(b.sidebars,function(a){var c=a.settings;return{type:"button",icon:c.icon,image:c.image,tooltip:c.tooltip,onclick:i(b,a.name,b.sidebars)}});return{type:"panel",name:"sidebar",layout:"stack",classes:"sidebar",items:[{type:"toolbar",layout:"stack",classes:"sidebar-toolbar",items:c}]}};return{hasSidebar:k,createSidebar:l}}),g("j",[],function(){var a=function(a){var b=function(){a._skinLoaded=!0,a.fire("SkinLoaded")};return function(){a.initialized?b():a.on("init",b)}};return{fireSkinLoaded:a}}),g("6",["b","c","d","e","f","g","h","9","i","j","k"],function(a,b,c,d,e,f,g,h,i,j,k){var l=a.DOM,m=function(a){return function(b){a.find("*").disabled("readonly"===b.mode)}},n=function(a){return{type:"panel",name:"iframe",layout:"stack",classes:"edit-area",border:a,html:""}},o=function(a){return{type:"panel",layout:"stack",classes:"edit-aria-container",border:"1 0 0 0",items:[n("0"),i.createSidebar(a)]}},p=function(a,c,p){var q,r,s,t=a.settings;return p.skinUiCss&&l.styleSheetLoader.load(p.skinUiCss,j.fireSkinLoaded(a)),q=c.panel=b.create({type:"panel",role:"application",classes:"tinymce",style:"visibility: hidden",layout:"stack",border:1,items:[t.menubar===!1?null:{type:"menubar",border:"0 0 1 0",items:g.createMenuButtons(a)},k.createToolbars(a,t.toolbar_items_size),i.hasSidebar(a)?o(a):n("1 0 0 0")]}),t.resize!==!1&&(r={type:"resizehandle",direction:t.resize,onResizeStart:function(){var b=a.getContentAreaContainer().firstChild;s={width:b.clientWidth,height:b.clientHeight}},onResize:function(b){"both"===t.resize?h.resizeTo(a,s.width+b.deltaX,s.height+b.deltaY):h.resizeTo(a,null,s.height+b.deltaY)}}),t.statusbar!==!1&&q.add({type:"panel",name:"statusbar",classes:"statusbar",layout:"flow",border:"1 0 0 0",ariaRoot:!0,items:[{type:"elementpath",editor:a},r]}),a.fire("BeforeRenderUI"),a.on("SwitchMode",m(q)),q.renderBefore(p.targetNode).reflow(),t.readonly&&a.setMode("readonly"),p.width&&l.setStyle(q.getEl(),"width",p.width),a.on("remove",function(){q.remove(),q=null}),d.addKeys(a,q),f.addContextualToolbars(a),e.setup(a),{iframeContainer:q.find("#iframe")[0].getEl(),editorContainer:q.getEl()}};return{render:p}}),g("l",["a"],function(a){return a("tinymce.ui.FloatPanel")}),g("7",["d","c","b","l","k","h","g","e","j"],function(a,b,c,d,e,f,g,h,i){var j=function(a,j,k){var l,m,n=a.settings,o=c.DOM;n.fixed_toolbar_container&&(m=o.select(n.fixed_toolbar_container)[0]);var p=function(){if(l&&l.moveRel&&l.visible()&&!l._fixed){var b=a.selection.getScrollContainer(),c=a.getBody(),d=0,e=0;if(b){var f=o.getPos(c),g=o.getPos(b);d=Math.max(0,g.x-f.x),e=Math.max(0,g.y-f.y)}l.fixed(!1).moveRel(c,a.rtl?["tr-br","br-tr"]:["tl-bl","bl-tl","tr-br"]).moveBy(d,e)}},q=function(){l&&(l.show(),p(),o.addClass(a.getBody(),"mce-edit-focus"))},r=function(){l&&(l.hide(),d.hideAll(),o.removeClass(a.getBody(),"mce-edit-focus"))},s=function(){return l?void(l.visible()||q()):(l=j.panel=b.create({type:m?"panel":"floatpanel",role:"application",classes:"tinymce tinymce-inline",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!!m,border:1,items:[n.menubar===!1?null:{type:"menubar",border:"0 0 1 0",items:f.createMenuButtons(a)},e.createToolbars(a,n.toolbar_items_size)]}),a.fire("BeforeRenderUI"),l.renderTo(m||document.body).reflow(),h.addKeys(a,l),q(),g.addContextualToolbars(a),a.on("nodeChange",p),a.on("activate",q),a.on("deactivate",r),void a.nodeChanged())};return n.content_editable=!0,a.on("focus",function(){k.skinUiCss?o.styleSheetLoader.load(k.skinUiCss,s,s):s()}),a.on("blur hide",r),a.on("remove",function(){l&&(l.remove(),l=null)}),k.skinUiCss&&o.styleSheetLoader.load(k.skinUiCss,i.fireSkinLoaded(a)),{}};return{render:j}}),g("m",["a"],function(a){return a("tinymce.ui.Throbber")}),g("8",["m"],function(a){var b=function(b,c){var d;b.on("ProgressState",function(b){d=d||new a(c.panel.getEl("body")),b.state?d.show(b.time):d.hide()})};return{setup:b}}),g("0",["1","2","3","4","5","6","7","8","9"],function(a,b,c,d,e,f,g,h,i){var j=b.ThemeManager;e.appendTo(a.tinymce?a.tinymce:{});var k=function(a,b,d){var e=a.settings,i=e.skin!==!1&&(e.skin||"lightgray");if(i){var j=e.skin_url;j=j?a.documentBaseURI.toAbsolute(j):c.baseURL+"/skins/"+i,d.skinUiCss=j+"/skin.min.css",a.contentCSS.push(j+"/content"+(a.inline?".inline":"")+".min.css")}return h.setup(a,b),e.inline?g.render(a,b,d):f.render(a,b,d)};return j.add("modern",function(a){return{renderUI:function(b){return k(a,this,b)},resizeTo:function(b,c){return i.resizeTo(a,b,c)},resizeBy:function(b,c){return i.resizeBy(a,b,c)}}}),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;i0?c:f},u=function(a){var c=a.getParam("toolbar"),d="undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image";return c===!1?[]:b.isArray(c)?b.grep(c,function(a){return a.length>0}):t(a.settings,d)};return{isBrandingEnabled:c,hasMenubar:d,getMenubar:e,hasStatusbar:f,getToolbarSize:g,getResize:h,isReadOnly:i,getFixedToolbarContainer:j,getInlineToolbarPositionHandler:k,getMenu:l,getRemovedMenuItems:m,getMinWidth:n,getMinHeight:o,getMaxWidth:p,getMaxHeight:q,getSkinUrl:r,isInline:s,getToolbars:u}}),g("14",["6"],function(a){return a("tinymce.dom.DOMUtils")}),g("b",["6"],function(a){return a("tinymce.ui.Factory")}),g("2g",[],function(){var a=function(a){return a.fire("SkinLoaded")},b=function(a){return a.fire("ResizeEditor")},c=function(a){return a.fire("BeforeRenderUI")};return{fireSkinLoaded:a,fireResizeEditor:b,fireBeforeRenderUI:c}}),g("34",[],function(){var a=function(a,b){return function(){var c=a.find(b)[0];c&&c.focus(!0)}},b=function(b,c){b.shortcuts.add("Alt+F9","",a(c,"menubar")),b.shortcuts.add("Alt+F10,F10","",a(c,"toolbar")),b.shortcuts.add("Alt+F11","",a(c,"elementpath")),c.on("cancel",function(){b.focus()})};return{addKeys:b}}),h("13",document),g("3j",["6"],function(a){return a("tinymce.geom.Rect")}),g("2t",["6"],function(a){return a("tinymce.util.Delay")}),g("39",["b","c","2c"],function(a,b,c){var d=function(c,d,e){var f,g=[];if(d)return b.each(d.split(/[ ,]/),function(b){var d,h=function(){var a=c.selection;b.settings.stateSelector&&a.selectorChanged(b.settings.stateSelector,function(a){b.active(a)},!0),b.settings.disabledStateSelector&&a.selectorChanged(b.settings.disabledStateSelector,function(a){b.disabled(a)})};"|"===b?f=null:(f||(f={type:"buttongroup",items:[]},g.push(f)),c.buttons[b]&&(d=b,b=c.buttons[d],"function"==typeof b&&(b=b()),b.type=b.type||"button",b.size=e,b=a.create(b),f.items.push(b),c.initialized?h():c.on("init",h)))}),{type:"toolbar",layout:"flow",items:g}},e=function(a,e){var f=[],g=function(b){b&&f.push(d(a,b,e))};if(b.each(c.getToolbars(a),function(a){g(a)}),f.length)return{type:"panel",layout:"stack",classes:"toolbar-grp",ariaRoot:!0,ariaRemember:!0,items:f}};return{createToolbar:d,createToolbars:e}}),g("35",["13","14","3j","b","2t","c","2c","39"],function(a,b,c,d,e,f,g,h){var i=b.DOM,j=function(a){return{left:a.x,top:a.y,width:a.w,height:a.h,right:a.x+a.w,bottom:a.y+a.h}},k=function(a){f.each(a.contextToolbars,function(a){a.panel&&a.panel.hide()})},l=function(a,b){a.moveTo(b.left,b.top)},m=function(a,b,c){b=b?b.substr(0,2):"",f.each({t:"down",b:"up"},function(d,e){a.classes.toggle("arrow-"+d,c(e,b.substr(0,1)))}),f.each({l:"left",r:"right"},function(d,e){a.classes.toggle("arrow-"+d,c(e,b.substr(1,1)))})},n=function(a,b,c,d,e,f){return f=j({x:b,y:c,w:f.w,h:f.h}),a&&(f=a({elementRect:j(d),contentAreaRect:j(e),panelRect:f})),f},o=function(b){var j,o=function(){return b.contextToolbars||[]},p=function(a){var c,d,e;return c=i.getPos(b.getContentAreaContainer()),d=b.dom.getRect(a),e=b.dom.getRoot(),"BODY"===e.nodeName&&(d.x-=e.ownerDocument.documentElement.scrollLeft||e.scrollLeft,d.y-=e.ownerDocument.documentElement.scrollTop||e.scrollTop),d.x+=c.x,d.y+=c.y,d},q=function(a,d){var e,f,h,j,o,q,r,s,t=g.getInlineToolbarPositionHandler(b);if(!b.removed){if(!a||!a.toolbar.panel)return void k(b);r=["bc-tc","tc-bc","tl-bl","bl-tl","tr-br","br-tr"],o=a.toolbar.panel,d&&o.show(),h=p(a.element),f=i.getRect(o.getEl()),j=i.getRect(b.getContentAreaContainer()||b.getBody()),s=25,"inline"!==i.getStyle(a.element,"display",!0)&&(h.w=a.element.clientWidth,h.h=a.element.clientHeight),b.inline||(j.w=b.getDoc().documentElement.offsetWidth),b.selection.controlSelection.isResizable(a.element)&&h.w=0;c--)for(d=f.length-1;d>=0;d--)if(f[d].predicate(e[c]))return{toolbar:f[d],element:e[c]};return null};b.on("click keyup setContent ObjectResized",function(a){("setcontent"!==a.type||a.selection)&&e.setEditorTimeout(b,function(){var a;a=v(b.selection.getNode()),a?(u(),t(a)):u()})}),b.on("blur hide contextmenu",u),b.on("ObjectResizeStart",function(){var a=v(b.selection.getNode());a&&a.toolbar.panel&&a.toolbar.panel.hide()}),b.on("ResizeEditor ResizeWindow",r(!0)),b.on("nodeChange",r(!1)),b.on("remove",function(){f.each(o(),function(a){a.panel&&a.panel.remove()}),b.contextToolbars={}}),b.shortcuts.add("ctrl+shift+e > ctrl+shift+p","",function(){var a=v(b.selection.getNode());a&&a.toolbar.panel&&a.toolbar.panel.items()[0].focus()})};return{addContextualToolbars:o}}),h("2i",Array),h("2j",Error),g("10",["2i","2j"],function(a,b){var c=function(){},d=function(a,b){return function(){return a(b.apply(null,arguments))}},e=function(a){return function(){return a}},f=function(a){return a},g=function(a,b){return a===b},h=function(b){for(var c=new a(arguments.length-1),d=1;d-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e0&&b=11},l=function(a){return!(!k()||!a.sidebars)&&a.sidebars.length>0},m=function(a){var b=c.map(a.sidebars,function(b){var c=b.settings;return{type:"button",icon:c.icon,image:c.image,tooltip:c.tooltip,onclick:j(a,b.name,a.sidebars)}});return{type:"panel",name:"sidebar",layout:"stack",classes:"sidebar",items:[{type:"toolbar",layout:"stack",classes:"sidebar-toolbar",items:b}]}};return{hasSidebar:l,createSidebar:m}}),g("38",["2g"],function(a){var b=function(b){var c=function(){b._skinLoaded=!0,a.fireSkinLoaded(b)};return function(){b.initialized?c():b.on("init",c)}};return{fireSkinLoaded:b}}),g("2d",["14","b","c","2g","2c","34","35","36","8","37","38","39"],function(a,b,c,d,e,f,g,h,i,j,k,l){var m=a.DOM,n=function(a){return function(b){a.find("*").disabled("readonly"===b.mode)}},o=function(a){return{type:"panel",name:"iframe",layout:"stack",classes:"edit-area",border:a,html:""}},p=function(a){return{type:"panel",layout:"stack",classes:"edit-aria-container",border:"1 0 0 0",items:[o("0"),j.createSidebar(a)]}},q=function(a,c,q){var r,s,t;if(q.skinUiCss&&m.styleSheetLoader.load(q.skinUiCss,k.fireSkinLoaded(a)),r=c.panel=b.create({type:"panel",role:"application",classes:"tinymce",style:"visibility: hidden",layout:"stack",border:1,items:[{type:"container",classes:"top-part",items:[e.hasMenubar(a)===!1?null:{type:"menubar",border:"0 0 1 0",items:h.createMenuButtons(a)},l.createToolbars(a,e.getToolbarSize(a))]},j.hasSidebar(a)?p(a):o("1 0 0 0")]}),"none"!==e.getResize(a)&&(s={type:"resizehandle",direction:e.getResize(a),onResizeStart:function(){var b=a.getContentAreaContainer().firstChild;t={width:b.clientWidth,height:b.clientHeight}},onResize:function(b){"both"===e.getResize(a)?i.resizeTo(a,t.width+b.deltaX,t.height+b.deltaY):i.resizeTo(a,null,t.height+b.deltaY)}}),e.hasStatusbar(a)){var u=e.isBrandingEnabled(a)?{type:"label",classes:"branding",html:' powered by tinymce'}:null;r.add({type:"panel",name:"statusbar",classes:"statusbar",layout:"flow",border:"1 0 0 0",ariaRoot:!0,items:[{type:"elementpath",editor:a},s,u]})}return d.fireBeforeRenderUI(a),a.on("SwitchMode",n(r)),r.renderBefore(q.targetNode).reflow(),e.isReadOnly(a)&&a.setMode("readonly"),q.width&&m.setStyle(r.getEl(),"width",q.width),a.on("remove",function(){r.remove(),r=null}),f.addKeys(a,r),g.addContextualToolbars(a),{iframeContainer:r.find("#iframe")[0].getEl(),editorContainer:r.getEl()}};return{render:q}}),g("2n",["6"],function(a){return a("tinymce.dom.DomQuery")}),g("2m",["13","14","16","c"],function(a,b,c,d){"use strict";var e=0,f={id:function(){return"mceu_"+e++},create:function(c,e,f){var g=a.createElement(c);return b.DOM.setAttribs(g,e),"string"==typeof f?g.innerHTML=f:d.each(f,function(a){a.nodeType&&g.appendChild(a)}),g},createFragment:function(a){return b.DOM.createFragment(a)},getWindowSize:function(){return b.DOM.getViewPort()},getSize:function(a){var b,c;if(a.getBoundingClientRect){var d=a.getBoundingClientRect();b=Math.max(d.width||d.right-d.left,a.offsetWidth),c=Math.max(d.height||d.bottom-d.bottom,a.offsetHeight)}else b=a.offsetWidth,c=a.offsetHeight;return{width:b,height:c}},getPos:function(a,c){return b.DOM.getPos(a,c||f.getContainer())},getContainer:function(){return c.container?c.container:a.body},getViewPort:function(a){return b.DOM.getViewPort(a)},get:function(b){return a.getElementById(b)},addClass:function(a,c){return b.DOM.addClass(a,c)},removeClass:function(a,c){return b.DOM.removeClass(a,c)},hasClass:function(a,c){return b.DOM.hasClass(a,c)},toggleClass:function(a,c,d){return b.DOM.toggleClass(a,c,d)},css:function(a,c,d){return b.DOM.setStyle(a,c,d)},getRuntimeStyle:function(a,c){return b.DOM.getStyle(a,c,!0)},on:function(a,c,d,e){return b.DOM.bind(a,c,d,e)},off:function(a,c,d){return b.DOM.unbind(a,c,d)},fire:function(a,c,d){return b.DOM.fire(a,c,d)},innerHtml:function(a,c){b.DOM.setHTML(a,c)}};return f}),g("1p",["13","1","2m"],function(a,b,c){"use strict";function d(b,d,e){var f,g,h,i,j,k,l,m,n,o;return n=c.getViewPort(),g=c.getPos(d),h=g.x,i=g.y,b.state.get("fixed")&&"static"==c.getRuntimeStyle(a.body,"position")&&(h-=n.x,i-=n.y),f=b.getEl(),o=c.getSize(f),j=o.width,k=o.height,o=c.getSize(d),l=o.width,m=o.height,e=(e||"").split(""),"b"===e[0]&&(i+=m),"r"===e[1]&&(h+=l),"c"===e[0]&&(i+=Math.round(m/2)),"c"===e[1]&&(h+=Math.round(l/2)),"b"===e[3]&&(i-=k),"r"===e[4]&&(h-=j),"c"===e[3]&&(i-=Math.round(k/2)),"c"===e[4]&&(h-=Math.round(j/2)),{x:h,y:i,w:j,h:k}}return{testMoveRel:function(a,b){for(var e=c.getViewPort(),f=0;f0&&g.x+g.w0&&g.y+g.he.x&&g.x+g.we.y&&g.y+g.hb?(a=b-c,a<0?0:a):a}var f=this;if(f.settings.constrainToViewport){var g=c.getViewPort(b),h=f.layoutRect();a=e(a,g.w+g.x,h.w),d=e(d,g.h+g.y,h.h)}return f.state.get("rendered")?f.layoutRect({x:a,y:d}).repaint():(f.settings.x=a,f.settings.y=d),f.fire("move",{x:a,y:d}),f}}}),g("2o",["6"],function(a){return a("tinymce.util.Class")}),g("2p",["6"],function(a){return a("tinymce.util.EventDispatcher")}),g("2q",["13"],function(a){"use strict";return{parseBox:function(a){var b,c=10;if(a)return"number"==typeof a?(a=a||0,{top:a,left:a,bottom:a,right:a}):(a=a.split(" "),b=a.length,1===b?a[1]=a[2]=a[3]=a[0]:2===b?(a[2]=a[0],a[3]=a[1]):3===b&&(a[3]=a[1]),{top:parseInt(a[0],c)||0,right:parseInt(a[1],c)||0,bottom:parseInt(a[2],c)||0,left:parseInt(a[3],c)||0})},measureBox:function(b,c){function d(c){var d=a.defaultView;return d?(c=c.replace(/[A-Z]/g,function(a){return"-"+a}),d.getComputedStyle(b,null).getPropertyValue(c)):b.currentStyle[c]}function e(a){var b=parseFloat(d(a),10);return isNaN(b)?0:b}return{top:e(c+"TopWidth"),right:e(c+"RightWidth"),bottom:e(c+"BottomWidth"),left:e(c+"LeftWidth")}}}}),g("2r",["c"],function(a){"use strict";function b(){}function c(a){this.cls=[],this.cls._map={},this.onchange=a||b,this.prefix=""}return a.extend(c.prototype,{add:function(a){return a&&!this.contains(a)&&(this.cls._map[a]=!0,this.cls.push(a),this._change()),this},remove:function(a){if(this.contains(a)){for(var b=0;b0&&(a+=" "),a+=this.prefix+this.cls[b];return a},c}),g("21",["2o"],function(a){"use strict";function b(a){for(var b,c=[],d=a.length;d--;)b=a[d],b.__checked||(c.push(b),b.__checked=1);for(d=c.length;d--;)delete c[d].__checked;return c}var c,d=/^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i,e=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,f=/^\s*|\s*$/g,g=a.extend({init:function(a){function b(a){if(a)return a=a.toLowerCase(),function(b){return"*"===a||b.type===a}}function c(a){if(a)return function(b){return b._name===a}}function g(a){if(a)return a=a.split("."),function(b){for(var c=a.length;c--;)if(!b.classes.contains(a[c]))return!1;return!0}}function h(a,b,c){if(a)return function(d){var e=d[a]?d[a]():"";return b?"="===b?e===c:"*="===b?e.indexOf(c)>=0:"~="===b?(" "+e+" ").indexOf(" "+c+" ")>=0:"!="===b?e!=c:"^="===b?0===e.indexOf(c):"$="===b&&e.substr(e.length-c.length)===c:!!c}}function i(a){var b;if(a)return a=/(?:not\((.+)\))|(.+)/i.exec(a),a[1]?(b=k(a[1],[]),function(a){return!l(a,b)}):(a=a[2],function(b,c,d){return"first"===a?0===c:"last"===a?c===d-1:"even"===a?c%2===0:"odd"===a?c%2===1:!!b[a]&&b[a]()})}function j(a,e,j){function k(a){a&&e.push(a)}var l;return l=d.exec(a.replace(f,"")),k(b(l[1])),k(c(l[2])),k(g(l[3])),k(h(l[4],l[5],l[6])),k(i(l[7])),e.pseudo=!!l[7],e.direct=j,e}function k(a,b){var c,d,f,g=[];do if(e.exec(""),d=e.exec(a),d&&(a=d[3],g.push(d[1]),d[2])){c=d[3];break}while(d);for(c&&k(c,b),a=[],f=0;f"!=g[f]&&a.push(j(g[f],[],">"===g[f-1]));return b.push(a),b}var l=this.match;this._selectors=k(a,[])},match:function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o;for(b=b||this._selectors,c=0,d=b.length;c=0;e--)for(j=g[e];o;){if(j.pseudo)for(m=o.parent().items(),k=l=m.length;k--&&m[k]!==o;);for(h=0,i=j.length;h1&&(h=b(h))}return c||(c=g.Collection),new c(h)}});return g}),g("i",["c","21","2o"],function(a,b,c){"use strict";var d,e,f=Array.prototype.push,g=Array.prototype.slice;return e={length:0,init:function(a){a&&this.add(a)},add:function(b){var c=this;return a.isArray(b)?f.apply(c,b):b instanceof d?c.add(b.toArray()):f.call(c,b),c},set:function(a){var b,c=this,d=c.length;for(c.length=0,c.add(a),b=c.length;b0}function f(a,b){var c,g;if(a===b)return!0;if(null===a||null===b)return a===b;if("object"!=typeof a||"object"!=typeof b)return a===b;if(d.isArray(b)){if(a.length!==b.length)return!1;for(c=a.length;c--;)if(!f(a[c],b[c]))return!1}if(e(a)||e(b))return a===b;g={};for(c in b){if(!f(a[c],b[c]))return!1;g[c]=!0}for(c in a)if(!g[c]&&!f(a[c],b[c]))return!1;return!0}return b.extend({Mixins:[c],init:function(b){var c,d;b=b||{};for(c in b)d=b[c],d instanceof a&&(b[c]=d.create(this,c));this.data=b},set:function(b,c){var d,e,g=this.data[b];if(c instanceof a&&(c=c.create(this,b)),"object"==typeof b){for(d in b)this.set(d,b[d]);return this}return f(g,c)||(this.data[b]=c,e={target:this,name:b,value:c,oldValue:g},this.fire("change:"+b,e),this.fire("change",e)),this},get:function(a){return this.data[a]},has:function(a){return a in this.data},bind:function(b){return a.create(this,b)},destroy:function(){this.fire("destroy")}})}),g("1w",["13","2t"],function(a,b){var c,d={};return{add:function(e){var f=e.parent();if(f){if(!f._layout||f._layout.isNative())return;d[f._id]||(d[f._id]=f),c||(c=!0,b.requestAnimationFrame(function(){var a,b;c=!1;for(a in d)b=d[a],b.state.get("rendered")&&b.reflow();d={}},a.body))}},remove:function(a){d[a._id]&&delete d[a._id]}}}),g("o",["13","2n","2o","2p","c","2q","2r","i","2s","2m","1w"],function(a,b,c,d,e,f,g,h,i,j,k){"use strict";function l(a){return a._eventDispatcher||(a._eventDispatcher=new d({scope:a,toggleEvent:function(b,c){c&&d.isNative(b)&&(a._nativeEvents||(a._nativeEvents={}),a._nativeEvents[b]=!0,a.state.get("rendered")&&m(a))}})),a._eventDispatcher}function m(a){function c(b){var c=a.getParentCtrl(b.target);c&&c.fire(b.type,b)}function d(){var a=j._lastHoverCtrl;a&&(a.fire("mouseleave",{target:a.getEl()}),a.parents().each(function(a){a.fire("mouseleave",{target:a.getEl()})}),j._lastHoverCtrl=null)}function e(b){var c,d,e,f=a.getParentCtrl(b.target),g=j._lastHoverCtrl,h=0;if(f!==g){if(j._lastHoverCtrl=f,d=f.parents().toArray().reverse(),d.push(f),g){for(e=g.parents().toArray().reverse(),e.push(g),h=0;h=h;c--)g=e[c],g.fire("mouseleave",{target:g.getEl()})}for(c=h;ci.maxW?i.maxW:c,i.w=c,i.innerW=c-d),c=a.h,c!==f&&(c=ci.maxH?i.maxH:c,i.h=c,i.innerH=c-e),c=a.innerW,c!==f&&(c=ci.maxW-d?i.maxW-d:c,i.innerW=c,i.w=c+d),c=a.innerH,c!==f&&(c=ci.maxH-e?i.maxH-e:c,i.innerH=c,i.h=c+e),a.contentW!==f&&(i.contentW=a.contentW),a.contentH!==f&&(i.contentH=a.contentH),b=h._lastLayoutRect,b.x===i.x&&b.y===i.y&&b.w===i.w&&b.h===i.h||(g=n.repaintControls,g&&g.map&&!g.map[h._id]&&(g.push(h),g.map[h._id]=!0),b.x=i.x,b.y=i.y,b.w=i.w,b.h=i.h),h):i},repaint:function(){var b,c,d,e,f,g,h,i,j,k,l=this;j=a.createRange?function(a){return a}:Math.round,b=l.getEl().style,e=l._layoutRect,i=l._lastRepaintRect||{},f=l.borderBox,g=f.left+f.right,h=f.top+f.bottom,e.x!==i.x&&(b.left=j(e.x)+"px",i.x=e.x),e.y!==i.y&&(b.top=j(e.y)+"px",i.y=e.y),e.w!==i.w&&(k=j(e.w-g),b.width=(k>=0?k:0)+"px",i.w=e.w),e.h!==i.h&&(k=j(e.h-h),b.height=(k>=0?k:0)+"px",i.h=e.h),l._hasBody&&e.innerW!==i.innerW&&(k=j(e.innerW),d=l.getEl("body"),d&&(c=d.style,c.width=(k>=0?k:0)+"px"),i.innerW=e.innerW),l._hasBody&&e.innerH!==i.innerH&&(k=j(e.innerH),d=d||l.getEl("body"),d&&(c=c||d.style,c.height=(k>=0?k:0)+"px"),i.innerH=e.innerH),l._lastRepaintRect=i,l.fire("repaint",{},!1)},updateLayoutRect:function(){var a=this;a.parent()._lastRect=null,j.css(a.getEl(),{width:"",height:""}),a._layoutRect=a._lastRepaintRect=a._lastLayoutRect=null,a.initLayoutRect()},on:function(a,b){function c(a){var b,c;return"string"!=typeof a?a:function(e){return b||d.parentsAndSelf().each(function(d){var e=d.settings.callbacks;if(e&&(b=e[a]))return c=d,!1}),b?b.call(c,e):(e.action=a,void this.fire("execute",e))}}var d=this;return l(d).on(a,c(b)),d},off:function(a,b){return l(this).off(a,b),this},fire:function(a,b,c){var d=this;if(b=b||{},b.control||(b.control=d),b=l(d).fire(a,b),c!==!1&&d.parent)for(var e=d.parent();e&&!b.isPropagationStopped();)e.fire(a,b,!1),e=e.parent();return b},hasEventListeners:function(a){return l(this).has(a)},parents:function(a){var b,c=this,d=new h;for(b=c.parent();b;b=b.parent())d.add(b);return a&&(d=d.filter(a)),d},parentsAndSelf:function(a){return new h(this).add(this.parents(a))},next:function(){var a=this.parent().items();return a[a.indexOf(this)+1]},prev:function(){var a=this.parent().items();return a[a.indexOf(this)-1]},innerHtml:function(a){return this.$el.html(a),this},getEl:function(a){var c=a?this._id+"-"+a:this._id;return this._elmCache[c]||(this._elmCache[c]=b("#"+c)[0]),this._elmCache[c]},show:function(){return this.visible(!0)},hide:function(){return this.visible(!1)},focus:function(){try{this.getEl().focus()}catch(a){}return this},blur:function(){return this.getEl().blur(),this},aria:function(a,b){var c=this,d=c.getEl(c.ariaTarget);return"undefined"==typeof b?c._aria[a]:(c._aria[a]=b,c.state.get("rendered")&&d.setAttribute("role"==a?a:"aria-"+a,b),c)},encode:function(a,b){return b!==!1&&(a=this.translate(a)),(a||"").replace(/[&<>"]/g,function(a){return"&#"+a.charCodeAt(0)+";"})},translate:function(a){return n.translate?n.translate(a):a},before:function(a){var b=this,c=b.parent();return c&&c.insert(a,c.items().indexOf(b),!0),b},after:function(a){var b=this,c=b.parent();return c&&c.insert(a,c.items().indexOf(b)),b},remove:function(){var a,c,d=this,e=d.getEl(),f=d.parent();if(d.items){var g=d.items().toArray();for(c=g.length;c--;)g[c].remove()}f&&f.items&&(a=[],f.items().each(function(b){b!==d&&a.push(b)}),f.items().set(a),f._lastRect=null),d._eventsRoot&&d._eventsRoot==d&&b(e).off();var h=d.getRoot().controlIdLookup;return h&&delete h[d._id],e&&e.parentNode&&e.parentNode.removeChild(e),d.state.set("rendered",!1),d.state.destroy(),d.fire("remove"),d},renderBefore:function(a){return b(a).before(this.renderHtml()),this.postRender(),this},renderTo:function(a){return b(a||this.getContainerElm()).append(this.renderHtml()),this.postRender(),this},preRender:function(){},render:function(){},renderHtml:function(){return'
    '},postRender:function(){var a,c,d,e,f,g=this,h=g.settings;g.$el=b(g.getEl()),g.state.set("rendered",!0);for(e in h)0===e.indexOf("on")&&g.on(e.substr(2),h[e]);if(g._eventsRoot){for(d=g.parent();!f&&d;d=d.parent())f=d._eventsRoot;if(f)for(e in f._nativeEvents)g._nativeEvents[e]=!0}m(g),h.style&&(a=g.getEl(),a&&(a.setAttribute("style",h.style),a.style.cssText=h.style)),g.settings.border&&(c=g.borderBox,g.$el.css({"border-top-width":c.top,"border-right-width":c.right,"border-bottom-width":c.bottom,"border-left-width":c.left}));var i=g.getRoot();i.controlIdLookup||(i.controlIdLookup={}),i.controlIdLookup[g._id]=g;for(var j in g._aria)g.aria(j,g._aria[j]);g.state.get("visible")===!1&&(g.getEl().style.display="none"),g.bindStates(),g.state.on("change:visible",function(a){var b,c=a.value;g.state.get("rendered")&&(g.getEl().style.display=c===!1?"none":"",g.getEl().getBoundingClientRect()),b=g.parent(),b&&(b._lastRect=null),g.fire(c?"show":"hide"),k.add(g)}),g.fire("postrender",{},!1)},bindStates:function(){},scrollIntoView:function(a){function b(a,b){var c,d,e=a;for(c=d=0;e&&e!=b&&e.nodeType;)c+=e.offsetLeft||0,d+=e.offsetTop||0,e=e.offsetParent;return{x:c,y:d}}var c,d,e,f,g,h,i=this.getEl(),j=i.parentNode,k=b(i,j);return c=k.x,d=k.y,e=i.offsetWidth,f=i.offsetHeight,g=j.clientWidth,h=j.clientHeight,"end"==a?(c-=g-e,d-=h-f):"center"==a&&(c-=g/2-e/2,d-=h/2-f/2),j.scrollLeft=c,j.scrollTop=d,this},getRoot:function(){for(var a,b=this,c=[];b;){if(b.rootControl){a=b.rootControl;break}c.push(b),a=b,b=b.parent()}a||(a=this);for(var d=c.length;d--;)c[d].rootControl=a;return a},reflow:function(){k.remove(this);var a=this.parent();return a&&a._layout&&!a._layout.isNative()&&a.reflow(),this}};return e.each("text title visible disabled active value".split(" "),function(a){s[a]=function(b){return 0===arguments.length?this.state.get(a):("undefined"!=typeof b&&this.state.set(a,b),this)}}),n=c.extend(s)}),g("1g",["13"],function(a){"use strict";var b=function(a){return!!a.getAttribute("data-mce-tabstop")};return function(c){function d(a){return a&&1===a.nodeType}function e(a){return a=a||v,d(a)?a.getAttribute("role"):null}function f(a){for(var b,c=a||v;c=c.parentNode;)if(b=e(c))return b}function g(a){var b=v;if(d(b))return b.getAttribute("aria-"+a)}function h(a){var b=a.tagName.toUpperCase();return"INPUT"==b||"TEXTAREA"==b||"SELECT"==b}function i(a){return!(!h(a)||a.hidden)||(!!b(a)||!!/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(e(a)))}function j(a){function b(a){if(1==a.nodeType&&"none"!=a.style.display&&!a.disabled){i(a)&&c.push(a);for(var d=0;d=b.length&&(a=0),b[a]&&b[a].focus(),a}function n(a,b){var c=-1,d=k();b=b||j(d.getEl());for(var e=0;e=0&&(c=b.getEl(),c&&c.parentNode.removeChild(c),c=a.getEl(),c&&c.parentNode.removeChild(c)),b.parent(this)},create:function(b){var c,e=this,g=[];return f.isArray(b)||(b=[b]),f.each(b,function(b){b&&(b instanceof a||("string"==typeof b&&(b={type:b}),c=f.extend({},e.settings.defaults,b),b.type=c.type=c.type||b.type||e.settings.defaultType||(c.defaults?c.defaults.type:null),b=d.create(c)),g.push(b))}),g},renderNew:function(){var a=this;return a.items().each(function(b,c){var d;b.parent(a),b.state.get("rendered")||(d=a.getEl("body"),d.hasChildNodes()&&c<=d.childNodes.length-1?g(d.childNodes[c]).before(b.renderHtml()):g(d).append(b.renderHtml()),b.postRender(),i.add(b))}),a._layout.applyClasses(a.items().filter(":visible")),a._lastRect=null,a},append:function(a){return this.add(a).renderNew()},prepend:function(a){var b=this;return b.items().set(b.create(a).concat(b.items().toArray())),b.renderNew()},insert:function(a,b,c){var d,e,f,g=this;return a=g.create(a),d=g.items(),!c&&b=0&&b
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "},postRender:function(){var a,b=this;return b.items().exec("postRender"),b._super(),b._layout.postRender(b),b.state.set("rendered",!0),b.settings.style&&b.$el.css(b.settings.style),b.settings.border&&(a=b.borderBox,b.$el.css({"border-top-width":a.top,"border-right-width":a.right,"border-bottom-width":a.bottom,"border-left-width":a.left})),b.parent()||(b.keyboardNav=new e({root:b})),b},initLayoutRect:function(){var a=this,b=a._super();return a._layout.recalc(a),b},recalc:function(){var a=this,b=a._layoutRect,c=a._lastRect;if(!c||c.w!=b.w||c.h!=b.h)return a._layout.recalc(a),b=a.layoutRect(),a._lastRect={x:b.x,y:b.y,w:b.w,h:b.h},!0},reflow:function(){var b;if(i.remove(this),this.visible()){for(a.repaintControls=[],a.repaintControls.map={},this.recalc(),b=a.repaintControls.length;b--;)a.repaintControls[b].repaint();"flow"!==this.settings.layout&&"stack"!==this.settings.layout&&this.repaint(),a.repaintControls=[]}return this}})}),g("p",["13","1","2n"],function(a,b,c){"use strict";function d(a){var b,c,d,e,f,g,h,i,j=Math.max;return b=a.documentElement,c=a.body,d=j(b.scrollWidth,c.scrollWidth),e=j(b.clientWidth,c.clientWidth),f=j(b.offsetWidth,c.offsetWidth),g=j(b.scrollHeight,c.scrollHeight),h=j(b.clientHeight,c.clientHeight),i=j(b.offsetHeight,c.offsetHeight),{width:d").css({position:"absolute",top:0,left:0,width:q.width,height:q.height,zIndex:2147483647,opacity:1e-4,cursor:k}).appendTo(p.body),c(p).on("mousemove touchmove",m).on("mouseup touchend",l),g.start(a)},m=function(a){return e(a),a.button!==j?l(a):(a.deltaX=a.screenX-n,a.deltaY=a.screenY-o,a.preventDefault(),void g.drag(a))},l=function(a){e(a),c(p).off("mousemove touchmove",m).off("mouseup touchend",l),i.remove(),g.stop&&g.stop(a)},this.destroy=function(){c(h()).off()},c(h()).on("mousedown touchstart",k)}}),g("1z",["2n","p"],function(a,b){"use strict";return{init:function(){var a=this;a.on("repaint",a.renderScroll)},renderScroll:function(){function c(){function b(b,g,h,i,j,k){var l,m,n,o,p,q,r,s,t;if(m=e.getEl("scroll"+b)){if(s=g.toLowerCase(),t=h.toLowerCase(),a(e.getEl("absend")).css(s,e.layoutRect()[i]-1),!j)return void a(m).css("display","none");a(m).css("display","block"),l=e.getEl("body"),n=e.getEl("scroll"+b+"t"),o=l["client"+h]-2*f,o-=c&&d?m["client"+k]:0,p=l["scroll"+h],q=o/p,r={},r[s]=l["offset"+g]+f,r[t]=o,a(m).css(r),r={},r[s]=l["scroll"+g]*q,r[t]=o*q,a(n).css(r)}}var c,d,g;g=e.getEl("body"),c=g.scrollWidth>g.clientWidth,d=g.scrollHeight>g.clientHeight,b("h","Left","Width","contentW",c,"Height"),b("v","Top","Height","contentH",d,"Width")}function d(){function c(c,d,g,h,i){var j,k=e._id+"-scroll"+c,l=e.classPrefix;a(e.getEl()).append('
    '),e.draghelper=new b(k+"t",{start:function(){j=e.getEl("body")["scroll"+d],a("#"+k).addClass(l+"active")},drag:function(a){var b,k,l,m,n=e.layoutRect();k=n.contentW>n.innerW,l=n.contentH>n.innerH,m=e.getEl("body")["client"+g]-2*f,m-=k&&l?e.getEl("scroll"+c)["client"+i]:0,b=m/e.getEl("body")["scroll"+g],e.getEl("body")["scroll"+d]=j+a["delta"+h]/b},stop:function(){a("#"+k).removeClass(l+"active")}})}e.classes.add("scroll"),c("v","Top","Height","Y","Width"),c("h","Left","Width","X","Height")}var e=this,f=2;e.settings.autoScroll&&(e._hasScroll||(e._hasScroll=!0,d(),e.on("wheel",function(a){var b=e.getEl("body");b.scrollLeft+=10*(a.deltaX||0),b.scrollTop+=10*a.deltaY,c()}),a(e.getEl("body")).on("scroll",c)),c())}}}),g("1r",["n","1z"],function(a,b){"use strict";return a.extend({Defaults:{layout:"fit",containerCls:"panel"},Mixins:[b],renderHtml:function(){var a=this,b=a._layout,c=a.settings.html;return a.preRender(),b.preRender(a),"undefined"==typeof c?c='
    '+b.renderHtml(a)+"
    ":("function"==typeof c&&(c=c.call(a)),a._hasBody=!1),'
    '+(a._preBodyHtml||"")+c+"
    "}})}),g("1x",["2m"],function(a){"use strict";return{resizeToContent:function(){this._layoutRect.autoResize=!0,this._lastRect=null,this.reflow()},resizeTo:function(b,c){if(b<=1||c<=1){var d=a.getWindowSize();b=b<=1?b*d.w:b,c=c<=1?c*d.h:c}return this._layoutRect.autoResize=!1,this.layoutRect({minW:b,minH:c,w:b,h:c}).reflow()},resizeBy:function(a,b){var c=this,d=c.layoutRect();return c.resizeTo(d.w+a,d.h+b)}}}),g("w",["13","1","2n","2t","2m","1p","1r","1x"],function(a,b,c,d,e,f,g,h){"use strict";function i(a,b){for(;a;){if(a==b)return!0;a=a.parent()}}function j(a){for(var b=u.length;b--;){var c=u[b],d=c.getParentCtrl(a.target);if(c.settings.autohide){if(d&&(i(d,c)||c.parent()===d))continue;a=c.fire("autohide",{target:a.target}),a.isDefaultPrevented()||c.hide()}}}function k(){q||(q=function(a){2!=a.button&&j(a)},c(a).on("click touchstart",q))}function l(){r||(r=function(){var a;for(a=u.length;a--;)n(u[a])},c(b).on("scroll",r))}function m(){if(!s){var d=a.documentElement,e=d.clientWidth,f=d.clientHeight;s=function(){a.all&&e==d.clientWidth&&f==d.clientHeight||(e=d.clientWidth,f=d.clientHeight,w.hideAll())},c(b).on("resize",s)}}function n(a){function b(b,c){for(var d,e=0;ec&&(a.fixed(!1).layoutRect({y:a._autoFixY}).repaint(),b(!1,a._autoFixY-c)):(a._autoFixY=a.layoutRect().y,a._autoFixY').appendTo(b.getContainerElm())),d.setTimeout(function(){e.addClass(f+"in"),c(b.getEl()).addClass(f+"in")}),t=!0),o(!0,b)}}),b.on("show",function(){b.parents().each(function(a){if(a.state.get("fixed"))return b.fixed(!0),!1})}),a.popover&&(b._preBodyHtml='
    ',b.classes.add("popover").add("bottom").add(b.isRtl()?"end":"start")),b.aria("label",a.ariaLabel),b.aria("labelledby",b._id),b.aria("describedby",b.describedBy||b._id+"-none")},fixed:function(a){var b=this;if(b.state.get("fixed")!=a){if(b.state.get("rendered")){var c=e.getViewPort();a?b.layoutRect().y-=c.y:b.layoutRect().y+=c.y}b.classes.toggle("fixed",a),b.state.set("fixed",a)}return b},show:function(){var a,b=this,c=b._super();for(a=u.length;a--&&u[a]!==b;);return a===-1&&u.push(b),c},hide:function(){return p(this),o(!1,this),this._super()},hideAll:function(){w.hideAll()},close:function(){var a=this;return a.fire("close").isDefaultPrevented()||(a.remove(),o(!1,a)),a},remove:function(){p(this),this._super()},postRender:function(){var a=this;return a.settings.bodyRole&&this.getEl("body").setAttribute("role",a.settings.bodyRole),a._super()}});return w.hideAll=function(){for(var a=u.length;a--;){var b=u[a];b&&b.settings.autohide&&(b.hide(),u.splice(a,1))}},w}),g("2e",["13","14","b","2g","2c","34","35","36","38","39","w"],function(a,b,c,d,e,f,g,h,i,j,k){var l=function(l,m,n){var o,p,q=b.DOM,r=e.getFixedToolbarContainer(l);r&&(p=q.select(r)[0]);var s=function(){if(o&&o.moveRel&&o.visible()&&!o._fixed){var a=l.selection.getScrollContainer(),b=l.getBody(),c=0,d=0;if(a){var e=q.getPos(b),f=q.getPos(a);c=Math.max(0,f.x-e.x),d=Math.max(0,f.y-e.y)}o.fixed(!1).moveRel(b,l.rtl?["tr-br","br-tr"]:["tl-bl","bl-tl","tr-br"]).moveBy(c,d)}},t=function(){o&&(o.show(),s(),q.addClass(l.getBody(),"mce-edit-focus"))},u=function(){o&&(o.hide(),k.hideAll(),q.removeClass(l.getBody(),"mce-edit-focus"))},v=function(){return o?void(o.visible()||t()):(o=m.panel=c.create({type:p?"panel":"floatpanel",role:"application",classes:"tinymce tinymce-inline",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!!p,border:1,items:[e.hasMenubar(l)===!1?null:{type:"menubar",border:"0 0 1 0",items:h.createMenuButtons(l)},j.createToolbars(l,e.getToolbarSize(l))]}),d.fireBeforeRenderUI(l),o.renderTo(p||a.body).reflow(),f.addKeys(l,o),t(),g.addContextualToolbars(l),l.on("nodeChange",s),l.on("activate",t),l.on("deactivate",u),void l.nodeChanged())};return l.settings.content_editable=!0,l.on("focus",function(){n.skinUiCss?q.styleSheetLoader.load(n.skinUiCss,v,v):v()}),l.on("blur hide",u),l.on("remove",function(){o&&(o.remove(),o=null)}),n.skinUiCss&&q.styleSheetLoader.load(n.skinUiCss,i.fireSkinLoaded(l)),{}};return{render:l}}),g("28",["2n","o","2t"],function(a,b,c){"use strict";return function(d,e){var f,g,h=this,i=b.classPrefix;h.show=function(b,j){function k(){f&&(a(d).append('
    '),j&&j())}return h.hide(),f=!0,b?g=c.setTimeout(k,b):k(),h},h.hide=function(){var a=d.lastChild;return c.clearTimeout(g),a&&a.className.indexOf("throbber")!=-1&&a.parentNode.removeChild(a),f=!1,h}}}),g("2f",["28"],function(a){var b=function(b,c){var d;b.on("ProgressState",function(b){d=d||new a(c.panel.getEl("body")),b.state?d.show(b.time):d.hide()})};return{setup:b}}),g("7",["15","2c","2d","2e","2f"],function(a,b,c,d,e){var f=function(a,f,g){var h=b.getSkinUrl(a);return h&&(g.skinUiCss=h+"/skin.min.css",a.contentCSS.push(h+"/content"+(a.inline?".inline":"")+".min.css")),e.setup(a,f),b.isInline(a)?d.render(a,f,g):c.render(a,f,g)};return{renderUI:f}}),h("2l",setTimeout),g("2a",["o","1p"],function(a,b){return a.extend({Mixins:[b],Defaults:{classes:"widget tooltip tooltip-n"},renderHtml:function(){var a=this,b=a.classPrefix;return'"},bindStates:function(){var a=this;return a.state.on("change:text",function(b){a.getEl().lastChild.innerHTML=a.encode(b.value)}),a._super()},repaint:function(){var a,b,c=this;a=c.getEl().style,b=c._layoutRect,a.left=b.x+"px",a.top=b.y+"px",a.zIndex=131070}})}),g("1b",["o","2a"],function(a,b){"use strict";var c,d=a.extend({init:function(a){var b=this;b._super(a),a=b.settings,b.canFocus=!0,a.tooltip&&d.tooltips!==!1&&(b.on("mouseenter",function(c){var d=b.tooltip().moveTo(-65535);if(c.control==b){var e=d.text(a.tooltip).show().testMoveRel(b.getEl(),["bc-tc","bc-tl","bc-tr"]);d.classes.toggle("tooltip-n","bc-tc"==e),d.classes.toggle("tooltip-nw","bc-tl"==e),d.classes.toggle("tooltip-ne","bc-tr"==e),d.moveRel(b.getEl(),e)}else d.hide()}),b.on("mouseleave mousedown click",function(){b.tooltip().hide()})),b.aria("label",a.ariaLabel||a.tooltip)},tooltip:function(){return c||(c=new b({type:"tooltip"}),c.renderTo()),c},postRender:function(){var a=this,b=a.settings;a._super(),a.parent()||!b.width&&!b.height||(a.initLayoutRect(),a.repaint()),b.autofocus&&a.focus()},bindStates:function(){function a(a){c.aria("disabled",a),c.classes.toggle("disabled",a)}function b(a){c.aria("pressed",a),c.classes.toggle("active",a)}var c=this;return c.state.on("change:disabled",function(b){a(b.value)}),c.state.on("change:active",function(a){b(a.value)}),c.state.get("disabled")&&a(!0),c.state.get("active")&&b(!0),c._super()},remove:function(){this._super(),c&&(c.remove(),c=null)}});return d}),g("1u",["1b"],function(a){"use strict";return a.extend({Defaults:{value:0},init:function(a){var b=this;b._super(a),b.classes.add("progress"),b.settings.filter||(b.settings.filter=function(a){return Math.round(a)})},renderHtml:function(){var a=this,b=a._id,c=this.classPrefix;return'
    0%
    '},postRender:function(){var a=this;return a._super(),a.value(a.settings.value),a},bindStates:function(){function a(a){a=b.settings.filter(a),b.getEl().lastChild.innerHTML=a+"%",b.getEl().firstChild.firstChild.style.width=a+"%"}var b=this;return b.state.on("change:value",function(b){a(b.value)}),a(b.state.get("value")),b._super()}})}),g("1q",["o","1p","1u","2t"],function(a,b,c,d){var e=function(a,b){a.getEl().lastChild.textContent=b+(a.progressBar?" "+a.progressBar.value()+"%":"")};return a.extend({Mixins:[b],Defaults:{classes:"widget notification"},init:function(a){var b=this;b._super(a),b.maxWidth=a.maxWidth,a.text&&b.text(a.text),a.icon&&(b.icon=a.icon),a.color&&(b.color=a.color),a.type&&b.classes.add("notification-"+a.type),a.timeout&&(a.timeout<0||a.timeout>0)&&!a.closeButton?b.closeButton=!1:(b.classes.add("has-close"),b.closeButton=!0),a.progressBar&&(b.progressBar=new c),b.on("click",function(a){a.target.className.indexOf(b.classPrefix+"close")!=-1&&b.close()})},renderHtml:function(){var a=this,b=a.classPrefix,c="",d="",e="",f="";return a.icon&&(c=''),f=' style="max-width: '+a.maxWidth+"px;"+(a.color?"background-color: "+a.color+';"':'"'),a.closeButton&&(d=''),a.progressBar&&(e=a.progressBar.renderHtml()),''},postRender:function(){var a=this;return d.setTimeout(function(){a.$el.addClass(a.classPrefix+"in"),e(a,a.state.get("text"))},100),a._super()},bindStates:function(){var a=this;return a.state.on("change:text",function(b){a.getEl().firstChild.innerHTML=b.value,e(a,b.value)}),a.progressBar&&(a.progressBar.bindStates(),a.progressBar.state.on("change:value",function(b){e(a,a.state.get("text"))})),a._super()},close:function(){var a=this;return a.fire("close").isDefaultPrevented()||a.remove(),a},repaint:function(){var a,b,c=this;a=c.getEl().style,b=c._layoutRect,a.left=b.x+"px",a.top=b.y+"px",a.zIndex=65534}})}),g("9",["z","2l","c","2m","1q"],function(a,b,c,d,e){return function(f){var g=function(a){return a.inline?a.getElement():a.getContentAreaContainer()},h=function(){var a=g(f);return d.getSize(a).width},i=function(b){a.each(b,function(a){a.moveTo(0,0)})},j=function(b){if(b.length>0){var c=b.slice(0,1)[0],d=g(f);c.moveRel(d,"tc-tc"),a.each(b,function(a,c){c>0&&a.moveRel(b[c-1].getEl(),"bc-tc")})}},k=function(a){i(a),j(a)},l=function(a,d){var f=c.extend(a,{maxWidth:h()}),g=new e(f);return g.args=f,f.timeout>0&&(g.timer=b(function(){g.close(),d()},f.timeout)),g.on("close",function(){d()}),g.renderTo(),g},m=function(a){a.close()},n=function(a){return a.args};return{open:l,close:m,reposition:k,getArgs:n}}}),g("2b",["13","2l","1","2n","16","2t","2q","2m","p","w","1r"],function(a,b,c,d,e,f,g,h,i,j,k){"use strict";function l(b){var c,f="width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0",g=d("meta[name=viewport]")[0];e.overrideViewPort!==!1&&(g||(g=a.createElement("meta"),g.setAttribute("name","viewport"),a.getElementsByTagName("head")[0].appendChild(g)),c=g.getAttribute("content"),c&&"undefined"!=typeof q&&(q=c),g.setAttribute("content",b?f:q))}function m(b,c){n()&&c===!1&&d([a.documentElement,a.body]).removeClass(b+"fullscreen")}function n(){for(var a=0;aa.w&&(c=a.x-Math.max(0,b/2),e.layoutRect({w:b,x:c}),d=!0)),f&&(f.layoutRect({w:e.layoutRect().innerW}).recalc(),b=f.layoutRect().minW+a.deltaW,b>a.w&&(c=a.x-Math.max(0,b-a.w),e.layoutRect({w:b,x:c}),d=!0)),d&&e.recalc()},initLayoutRect:function(){var a,b=this,c=b._super(),d=0;if(b.settings.title&&!b._fullscreen){a=b.getEl("head");var e=h.getSize(a);c.headerW=e.width,c.headerH=e.height,d+=c.headerH}b.statusbar&&(d+=b.statusbar.layoutRect().h),c.deltaH+=d,c.minH+=d,c.h+=d;var f=h.getWindowSize();return c.x=b.settings.x||Math.max(0,f.w/2-c.w/2),c.y=b.settings.y||Math.max(0,f.h/2-c.h/2),c},renderHtml:function(){var a=this,b=a._layout,c=a._id,d=a.classPrefix,e=a.settings,f="",g="",h=e.html;return a.preRender(),b.preRender(a),e.title&&(f='
    '+a.encode(e.title)+'
    '),e.url&&(h=''),"undefined"==typeof h&&(h=b.renderHtml(a)),a.statusbar&&(g=a.statusbar.renderHtml()),'
    '+f+'
    '+h+"
    "+g+"
    "},fullscreen:function(b){var e,i,j=this,k=a.documentElement,l=j.classPrefix;if(b!=j._fullscreen)if(d(c).on("resize",function(){var a;if(j._fullscreen)if(e)j._timer||(j._timer=f.setTimeout(function(){var a=h.getWindowSize();j.moveTo(0,0).resizeTo(a.w,a.h),j._timer=0},50));else{a=(new Date).getTime();var b=h.getWindowSize();j.moveTo(0,0).resizeTo(b.w,b.h),(new Date).getTime()-a>50&&(e=!0)}}),i=j.layoutRect(),j._fullscreen=b,b){j._initial={x:i.x,y:i.y,w:i.w,h:i.h},j.borderBox=g.parseBox("0"),j.getEl("head").style.display="none",i.deltaH-=i.headerH+2,d([k,a.body]).addClass(l+"fullscreen"),j.classes.add("fullscreen");var m=h.getWindowSize();j.moveTo(0,0).resizeTo(m.w,m.h)}else j.borderBox=g.parseBox(j.settings.border),j.getEl("head").style.display="",i.deltaH+=i.headerH,d([k,a.body]).removeClass(l+"fullscreen"),j.classes.remove("fullscreen"),j.moveTo(j._initial.x,j._initial.y).resizeTo(j._initial.w,j._initial.h);return j.reflow()},postRender:function(){var a,c=this;b(function(){c.classes.add("in"),c.fire("open")},0),c._super(),c.statusbar&&c.statusbar.postRender(),c.focus(),this.dragHelper=new i(c._id+"-dragh",{start:function(){a={x:c.layoutRect().x,y:c.layoutRect().y}},drag:function(b){c.moveTo(a.x+b.deltaX,a.y+b.deltaY)}}),c.on("submit",function(a){a.isDefaultPrevented()||c.close()}),p.push(c),l(!0)},submit:function(){return this.fire("submit",{ +data:this.toJSON()})},remove:function(){var a,b=this;for(b.dragHelper.destroy(),b._super(),b.statusbar&&this.statusbar.remove(),m(b.classPrefix,!1),a=p.length;a--;)p[a]===b&&p.splice(a,1);l(p.length>0)},getContentWindow:function(){var a=this.getEl().getElementsByTagName("iframe")[0];return a?a.contentWindow:null}});return o(),r}),g("1o",["13","2b"],function(a,b){"use strict";var c=b.extend({init:function(a){a={border:1,padding:20,layout:"flex",pack:"center",align:"center",containerCls:"panel",autoScroll:!0,buttons:{type:"button",text:"Ok",action:"ok"},items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200}},this._super(a)},Statics:{OK:1,OK_CANCEL:2,YES_NO:3,YES_NO_CANCEL:4,msgBox:function(d){function e(a,b,c){return{type:"button",text:a,subtype:c?"primary":"",onClick:function(a){a.control.parents()[1].close(),g(b)}}}var f,g=d.callback||function(){};switch(d.buttons){case c.OK_CANCEL:f=[e("Ok",!0,!0),e("Cancel",!1)];break;case c.YES_NO:case c.YES_NO_CANCEL:f=[e("Yes",1,!0),e("No",0)],d.buttons==c.YES_NO_CANCEL&&f.push(e("Cancel",-1));break;default:f=[e("Ok",!0,!0)]}return new b({padding:20,x:d.x,y:d.y,minWidth:300,minHeight:100,layout:"flex",pack:"center",align:"center",buttons:f,title:d.title,role:"alertdialog",items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200,text:d.text},onPostRender:function(){this.aria("describedby",this.items()[0]._id)},onClose:d.onClose,onCancel:function(){g(!1)}}).renderTo(a.body).reflow()},alert:function(a,b){return"string"==typeof a&&(a={text:a}),a.callback=b,c.msgBox(a)},confirm:function(a,b){return"string"==typeof a&&(a={text:a}),a.callback=b,a.buttons=c.OK_CANCEL,c.msgBox(a)}}});return c}),g("a",["2b","1o"],function(a,b){return function(c){var d=function(b,c,d){var e;return b.title=b.title||" ",b.url=b.url||b.file,b.url&&(b.width=parseInt(b.width||320,10),b.height=parseInt(b.height||240,10)),b.body&&(b.items={defaults:b.defaults,type:b.bodyType||"form",items:b.body,data:b.data,callbacks:b.commands}),b.url||b.buttons||(b.buttons=[{text:"Ok",subtype:"primary",onclick:function(){e.find("form")[0].submit()}},{text:"Cancel",onclick:function(){e.close()}}]),e=new a(b),e.on("close",function(){d(e)}),b.data&&e.on("postRender",function(){this.find("*").each(function(a){var c=a.name();c in b.data&&a.value(b.data[c])})}),e.features=b||{},e.params=c||{},e=e.renderTo().reflow()},e=function(a,c,d){var e;return e=b.alert(a,function(){c()}),e.on("close",function(){d(e)}),e},f=function(a,c,d){var e;return e=b.confirm(a,function(a){c(a)}),e.on("close",function(){d(e)}),e},g=function(a){a.close()},h=function(a){return a.params},i=function(a,b){a.params=b};return{open:d,alert:e,confirm:f,close:g,getParams:h,setParams:i}}}),g("3",["7","8","9","a"],function(a,b,c,d){var e=function(e){var f=function(b){return a.renderUI(e,this,b)},g=function(a,c){return b.resizeTo(e,a,c)},h=function(a,c){return b.resizeBy(e,a,c)},i=function(){return c(e)},j=function(){return d(e)};return{renderUI:f,resizeTo:g,resizeBy:h,getNotificationManagerImpl:i,getWindowManagerImpl:j}};return{get:e}}),g("1i",["2o","c"],function(a,b){"use strict";return a.extend({Defaults:{firstControlClass:"first",lastControlClass:"last"},init:function(a){this.settings=b.extend({},this.Defaults,a)},preRender:function(a){a.bodyClasses.add(this.settings.containerClass)},applyClasses:function(a){var b,c,d,e,f=this,g=f.settings;b=g.firstControlClass,c=g.lastControlClass,a.each(function(a){a.classes.remove(b).remove(c).add(g.controlClass),a.visible()&&(d||(d=a),e=a)}),d&&d.classes.add(b),e&&e.classes.add(c)},renderHtml:function(a){var b=this,c="";return b.applyClasses(a.items()),a.items().each(function(a){c+=a.renderHtml()}),c},recalc:function(){},postRender:function(){},isNative:function(){return!1}})}),g("d",["1i"],function(a){"use strict";return a.extend({Defaults:{containerClass:"abs-layout",controlClass:"abs-layout-item"},recalc:function(a){a.items().filter(":visible").each(function(a){var b=a.settings;a.layoutRect({x:b.x,y:b.y,w:b.w,h:b.h}),a.recalc&&a.recalc()})},renderHtml:function(a){return'
    '+this._super(a)}})}),g("f",["13","1","1b"],function(a,b,c){"use strict";return c.extend({Defaults:{classes:"widget btn",role:"button"},init:function(a){var b,c=this;c._super(a),a=c.settings,b=c.settings.size,c.on("click mousedown",function(a){a.preventDefault()}),c.on("touchstart",function(a){c.fire("click",a),a.preventDefault()}),a.subtype&&c.classes.add(a.subtype),b&&c.classes.add("btn-"+b),a.icon&&c.icon(a.icon)},icon:function(a){return arguments.length?(this.state.set("icon",a),this):this.state.get("icon")},repaint:function(){var a,b=this.getEl().firstChild;b&&(a=b.style,a.width=a.height="100%"),this._super()},renderHtml:function(){var a,c=this,d=c._id,e=c.classPrefix,f=c.state.get("icon"),g=c.state.get("text"),h="";return a=c.settings.image,a?(f="none","string"!=typeof a&&(a=b.getSelection?a[0]:a[1]),a=" style=\"background-image: url('"+a+"')\""):a="",g&&(c.classes.add("btn-has-text"),h=''+c.encode(g)+""),f=f?e+"ico "+e+"i-"+f:"",'
    "},bindStates:function(){function b(a){var b=d("span."+e,c.getEl());a?(b[0]||(d("button:first",c.getEl()).append(''),b=d("span."+e,c.getEl())),b.html(c.encode(a))):b.remove(),c.classes.toggle("btn-has-text",!!a)}var c=this,d=c.$,e=c.classPrefix+"txt";return c.state.on("change:text",function(a){b(a.value)}),c.state.on("change:icon",function(d){var e=d.value,f=c.classPrefix;c.settings.icon=e,e=e?f+"ico "+f+"i-"+c.settings.icon:"";var g=c.getEl().firstChild,h=g.getElementsByTagName("i")[0];e?(h&&h==g.firstChild||(h=a.createElement("i"),g.insertBefore(h,g.firstChild)),h.className=e):h&&g.removeChild(h),b(c.state.get("text"))}),c._super()}})}),h("2u",RegExp),g("e",["f","c","2m","2n","2u"],function(a,b,c,d,e){return a.extend({init:function(a){var c=this;a=b.extend({text:"Browse...",multiple:!1,accept:null},a),c._super(a),c.classes.add("browsebutton"),a.multiple&&c.classes.add("multiple")},postRender:function(){var a=this,b=c.create("input",{type:"file",id:a._id+"-browse",accept:a.settings.accept});a._super(),d(b).on("change",function(b){var c=b.target.files;a.value=function(){return c.length?a.settings.multiple?c:c[0]:null},b.preventDefault(),c.length&&a.fire("change",b)}),d(b).on("click",function(a){a.stopPropagation()}),d(a.getEl("button")).on("click",function(a){a.stopPropagation(),b.click()}),a.getEl().appendChild(b)},remove:function(){d(this.getEl("button")).off(),d(this.getEl("input")).off(),this._super()}})}),g("g",["n"],function(a){"use strict";return a.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var a=this,b=a._layout;return a.classes.add("btn-group"),a.preRender(),b.preRender(a),'
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "}})}),g("h",["13","1b"],function(a,b){"use strict";return b.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(a){var b=this;b._super(a),b.on("click mousedown",function(a){a.preventDefault()}),b.on("click",function(a){a.preventDefault(),b.disabled()||b.checked(!b.checked())}),b.checked(b.settings.checked)},checked:function(a){return arguments.length?(this.state.set("checked",a),this):this.state.get("checked")},value:function(a){return arguments.length?this.checked(a):this.checked()},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix;return'
    '+a.encode(a.state.get("text"))+"
    "},bindStates:function(){function b(a){c.classes.toggle("checked",a),c.aria("checked",a)}var c=this;return c.state.on("change:text",function(a){c.getEl("al").firstChild.data=c.translate(a.value)}),c.state.on("change:checked change:value",function(a){c.fire("change"),b(a.value)}),c.state.on("change:icon",function(b){var d=b.value,e=c.classPrefix;if("undefined"==typeof d)return c.settings.icon;c.settings.icon=d,d=d?e+"ico "+e+"i-"+c.settings.icon:"";var f=c.getEl().firstChild,g=f.getElementsByTagName("i")[0];d?(g&&g==f.firstChild||(g=a.createElement("i"),f.insertBefore(g,f.firstChild)),g.className=d):g&&f.removeChild(g)}),c.state.get("checked")&&b(!0),c._super()}})}),g("2v",["6"],function(a){return a("tinymce.util.VK")}),g("m",["13","2n","b","c","2v","2m","1b"],function(a,b,c,d,e,f,g){"use strict";return g.extend({init:function(a){var c=this;c._super(a),a=c.settings,c.classes.add("combobox"),c.subinput=!0,c.ariaTarget="inp",a.menu=a.menu||a.values,a.menu&&(a.icon="caret"),c.on("click",function(d){var e=d.target,f=c.getEl();if(b.contains(f,e)||e==f)for(;e&&e!=f;)e.id&&e.id.indexOf("-open")!=-1&&(c.fire("action"),a.menu&&(c.showMenu(),d.aria&&c.menu.items()[0].focus())),e=e.parentNode}),c.on("keydown",function(a){var b;13==a.keyCode&&"INPUT"===a.target.nodeName&&(a.preventDefault(),c.parents().reverse().each(function(a){if(a.toJSON)return b=a,!1}),c.fire("submit",{data:b.toJSON()}))}),c.on("keyup",function(a){if("INPUT"==a.target.nodeName){var b=c.state.get("value"),d=a.target.value;d!==b&&(c.state.set("value",d),c.fire("autocomplete",a))}}),c.on("mouseover",function(a){var b=c.tooltip().moveTo(-65535);if(c.statusLevel()&&a.target.className.indexOf(c.classPrefix+"status")!==-1){var d=c.statusMessage()||"Ok",e=b.text(d).show().testMoveRel(a.target,["bc-tc","bc-tl","bc-tr"]);b.classes.toggle("tooltip-n","bc-tc"==e),b.classes.toggle("tooltip-nw","bc-tl"==e),b.classes.toggle("tooltip-ne","bc-tr"==e),b.moveRel(a.target,e)}})},statusLevel:function(a){return arguments.length>0&&this.state.set("statusLevel",a),this.state.get("statusLevel")},statusMessage:function(a){return arguments.length>0&&this.state.set("statusMessage",a),this.state.get("statusMessage")},showMenu:function(){var a,b=this,d=b.settings;b.menu||(a=d.menu||[],a.length?a={type:"menu",items:a}:a.type=a.type||"menu",b.menu=c.create(a).parent(b).renderTo(b.getContainerElm()),b.fire("createmenu"),b.menu.reflow(),b.menu.on("cancel",function(a){a.control===b.menu&&b.focus()}),b.menu.on("show hide",function(a){a.control.items().each(function(a){a.active(a.value()==b.value())})}).fire("show"),b.menu.on("select",function(a){b.value(a.control.value())}),b.on("focusin",function(a){"INPUT"==a.target.tagName.toUpperCase()&&b.menu.hide()}),b.aria("expanded",!0)),b.menu.show(),b.menu.layoutRect({w:b.layoutRect().w}),b.menu.moveRel(b.getEl(),b.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var c,d,e=this,g=e.getEl(),h=e.getEl("open"),i=e.layoutRect(),j=0,k=g.firstChild;e.statusLevel()&&"none"!==e.statusLevel()&&(j=parseInt(f.getRuntimeStyle(k,"padding-right"),10)-parseInt(f.getRuntimeStyle(k,"padding-left"),10)),c=h?i.w-f.getSize(h).width-10:i.w-10;var l=a;return l.all&&(!l.documentMode||l.documentMode<=8)&&(d=e.layoutRect().h-2+"px"),b(k).css({width:c-j,lineHeight:d}),e._super(),e},postRender:function(){var a=this;return b(this.getEl("inp")).on("change",function(b){a.state.set("value",b.target.value),a.fire("change",b)}),a._super()},renderHtml:function(){var a,b,c=this,d=c._id,e=c.settings,f=c.classPrefix,g=c.state.get("value")||"",h="",i="",j="";return"spellcheck"in e&&(i+=' spellcheck="'+e.spellcheck+'"'),e.maxLength&&(i+=' maxlength="'+e.maxLength+'"'),e.size&&(i+=' size="'+e.size+'"'),e.subtype&&(i+=' type="'+e.subtype+'"'),j='',c.disabled()&&(i+=' disabled="disabled"'),a=e.icon,a&&"caret"!=a&&(a=f+"ico "+f+"i-"+e.icon),b=c.state.get("text"),(a||b)&&(h='
    ",c.classes.add("has-open")),'
    '+j+h+"
    "},value:function(a){return arguments.length?(this.state.set("value",a),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(a,b){var e=this;if(0===a.length)return void e.hideMenu();var f=function(a,b){return function(){e.fire("selectitem",{title:b,value:a})}};e.menu?e.menu.items().remove():e.menu=c.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(e).renderTo(),d.each(a,function(a){e.menu.add({text:a.title,url:a.previewUrl,match:b,classes:"menu-item-ellipsis",onclick:f(a.value,a.title)})}),e.menu.renderNew(),e.hideMenu(),e.menu.on("cancel",function(a){a.control.parent()===e.menu&&(a.stopPropagation(),e.focus(),e.hideMenu())}),e.menu.on("select",function(){e.focus()});var g=e.layoutRect().w;e.menu.layoutRect({w:g,minW:0,maxW:g}),e.menu.reflow(),e.menu.show(),e.menu.moveRel(e.getEl(),e.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var a=this;a.state.on("change:value",function(b){a.getEl("inp").value!=b.value&&(a.getEl("inp").value=b.value)}),a.state.on("change:disabled",function(b){a.getEl("inp").disabled=b.value}),a.state.on("change:statusLevel",function(b){var c=a.getEl("status"),d=a.classPrefix,e=b.value;f.css(c,"display","none"===e?"none":""),f.toggleClass(c,d+"i-checkmark","ok"===e),f.toggleClass(c,d+"i-warning","warn"===e),f.toggleClass(c,d+"i-error","error"===e),a.classes.toggle("has-status","none"!==e),a.repaint()}),f.on(a.getEl("status"),"mouseleave",function(){a.tooltip().hide()}),a.on("cancel",function(b){a.menu&&a.menu.visible()&&(b.stopPropagation(),a.hideMenu())});var b=function(a,b){b&&b.items().length>0&&b.items().eq(a)[0].focus()};return a.on("keydown",function(c){var d=c.keyCode;"INPUT"===c.target.nodeName&&(d===e.DOWN?(c.preventDefault(),a.fire("autocomplete"),b(0,a.menu)):d===e.UP&&(c.preventDefault(),b(-1,a.menu)))}),a._super()},remove:function(){b(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}})}),g("j",["m"],function(a){"use strict";return a.extend({init:function(a){var b=this;a.spellcheck=!1,a.onaction&&(a.icon="none"),b._super(a),b.classes.add("colorbox"),b.on("change keyup postrender",function(){b.repaintColor(b.value())})},repaintColor:function(a){var b=this.getEl("open"),c=b?b.getElementsByTagName("i")[0]:null;if(c)try{c.style.background=a}catch(a){}},bindStates:function(){var a=this;return a.state.on("change:value",function(b){a.state.get("rendered")&&a.repaintColor(b.value)}),a._super()}})}),g("1s",["f","w"],function(a,b){"use strict";return a.extend({showPanel:function(){var a=this,c=a.settings;if(a.classes.add("opened"),a.panel)a.panel.show();else{var d=c.panel;d.type&&(d={layout:"grid",items:d}),d.role=d.role||"dialog",d.popover=!0,d.autohide=!0,d.ariaRoot=!0,a.panel=new b(d).on("hide",function(){a.classes.remove("opened")}).on("cancel",function(b){b.stopPropagation(),a.focus(),a.hidePanel()}).parent(a).renderTo(a.getContainerElm()),a.panel.fire("show"),a.panel.reflow()}var e=a.panel.testMoveRel(a.getEl(),c.popoverAlign||(a.isRtl()?["bc-tc","bc-tl","bc-tr"]:["bc-tc","bc-tr","bc-tl"]));a.panel.classes.toggle("start","bc-tl"===e),a.panel.classes.toggle("end","bc-tr"===e),a.panel.moveRel(a.getEl(),e)},hidePanel:function(){var a=this;a.panel&&a.panel.hide()},postRender:function(){var a=this;return a.aria("haspopup",!0),a.on("click",function(b){b.control===a&&(a.panel&&a.panel.visible()?a.hidePanel():(a.showPanel(),a.panel.focus(!!b.aria)))}),a._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}})}),g("k",["1s","14"],function(a,b){"use strict";var c=b.DOM;return a.extend({init:function(a){this._super(a),this.classes.add("splitbtn"),this.classes.add("colorbutton")},color:function(a){return a?(this._color=a,this.getEl("preview").style.backgroundColor=a,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix,d=a.state.get("text"),e=a.settings.icon?c+"ico "+c+"i-"+a.settings.icon:"",f=a.settings.image?" style=\"background-image: url('"+a.settings.image+"')\"":"",g="";return d&&(a.classes.add("btn-has-text"),g=''+a.encode(d)+""),'
    '},postRender:function(){var a=this,b=a.settings.onclick;return a.on("click",function(d){d.aria&&"down"===d.aria.key||d.control!=a||c.getParent(d.target,"."+a.classPrefix+"open")||(d.stopImmediatePropagation(),b.call(a,d))}),delete a.settings.onclick,a._super()}})}),g("2w",["6"],function(a){return a("tinymce.util.Color")}),g("l",["1b","p","2m","2w"],function(a,b,c,d){"use strict";return a.extend({Defaults:{classes:"widget colorpicker"},init:function(a){this._super(a)},postRender:function(){function a(a,b){var d,e,f=c.getPos(a);return d=b.pageX-f.x,e=b.pageY-f.y,d=Math.max(0,Math.min(d/a.clientWidth,1)),e=Math.max(0,Math.min(e/a.clientHeight,1)),{x:d,y:e}}function e(a,b){var e=(360-a.h)/360;c.css(j,{top:100*e+"%"}),b||c.css(l,{left:a.s+"%",top:100-a.v+"%"}),k.style.background=new d({s:100,v:100,h:a.h}).toHex(),m.color().parse({s:a.s,v:a.v,h:a.h})}function f(b){var c;c=a(k,b),h.s=100*c.x,h.v=100*(1-c.y),e(h),m.fire("change")}function g(b){var c;c=a(i,b),h=n.toHsv(),h.h=360*(1-c.y),e(h,!0),m.fire("change")}var h,i,j,k,l,m=this,n=m.color();i=m.getEl("h"),j=m.getEl("hp"),k=m.getEl("sv"),l=m.getEl("svp"),m._repaint=function(){h=n.toHsv(),e(h)},m._super(),m._svdraghelper=new b(m._id+"-sv",{start:f,drag:f}),m._hdraghelper=new b(m._id+"-h",{start:g,drag:g}),m._repaint()},rgb:function(){return this.color().toRgb()},value:function(a){var b=this;return arguments.length?(b.color().parse(a),void(b._rendered&&b._repaint())):b.color().toHex()},color:function(){return this._color||(this._color=new d),this._color},renderHtml:function(){function a(){var a,b,c,d,g="";for(c="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",d=f.split(","),a=0,b=d.length-1;a';return g}var b,c=this,d=c._id,e=c.classPrefix,f="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000",g="background: -ms-linear-gradient(top,"+f+");background: linear-gradient(to bottom,"+f+");";return b='
    '+a()+'
    ','
    '+b+"
    "}})}),g("q",["1b","c","2m","2u"],function(a,b,c,d){return a.extend({init:function(a){var c=this;a=b.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},a),c._super(a),c.classes.add("dropzone"),a.multiple&&c.classes.add("multiple")},renderHtml:function(){var a,b,d=this,e=d.settings;return a={id:d._id,hidefocus:"1"},b=c.create("div",a,""+this.translate(e.text)+""),e.height&&c.css(b,"height",e.height+"px"),e.width&&c.css(b,"width",e.width+"px"),b.className=d.classes,b.outerHTML},postRender:function(){var a=this,c=function(b){b.preventDefault(),a.classes.toggle("dragenter"),a.getEl().className=a.classes},e=function(c){var e=a.settings.accept;if("string"!=typeof e)return c;var f=new d("("+e.split(/\s*,\s*/).join("|")+")$","i");return b.grep(c,function(a){return f.test(a.name)})};a._super(),a.$el.on("dragover",function(a){a.preventDefault()}),a.$el.on("dragenter",c),a.$el.on("dragleave",c),a.$el.on("drop",function(b){if(b.preventDefault(),!a.state.get("disabled")){var c=e(b.dataTransfer.files);a.value=function(){return c.length?a.settings.multiple?c:c[0]:null},c.length&&a.fire("change",b)}})},remove:function(){this.$el.off(),this._super()}})}),g("1t",["1b"],function(a){"use strict";return a.extend({init:function(a){var b=this;a.delimiter||(a.delimiter="\xbb"),b._super(a),b.classes.add("path"),b.canFocus=!0,b.on("click",function(a){var c,d=a.target;(c=d.getAttribute("data-index"))&&b.fire("select",{value:b.row()[c],index:c})}),b.row(b.settings.row)},focus:function(){var a=this;return a.getEl().firstChild.focus(),a},row:function(a){return arguments.length?(this.state.set("row",a),this):this.state.get("row")},renderHtml:function(){var a=this;return'
    '+a._getDataPathHtml(a.state.get("row"))+"
    "},bindStates:function(){var a=this;return a.state.on("change:row",function(b){a.innerHtml(a._getDataPathHtml(b.value))}),a._super()},_getDataPathHtml:function(a){var b,c,d=this,e=a||[],f="",g=d.classPrefix;for(b=0,c=e.length;b0?'":"")+'
    '+e[b].name+"
    ";return f||(f='
    \xa0
    '),f}})}),g("r",["1t"],function(a){return a.extend({postRender:function(){function a(a){if(1===a.nodeType){if("BR"==a.nodeName||a.getAttribute("data-mce-bogus"))return!0;if("bookmark"===a.getAttribute("data-mce-type"))return!0}return!1}var b=this,c=b.settings.editor;return c.settings.elementpath!==!1&&(b.on("select",function(a){c.focus(),c.selection.select(this.row()[a.index].element),c.nodeChanged()}),c.on("nodeChange",function(d){for(var e=[],f=d.parents,g=f.length;g--;)if(1==f[g].nodeType&&!a(f[g])){var h=c.fire("ResolveName",{name:f[g].nodeName.toLowerCase(),target:f[g]});if(h.isDefaultPrevented()||e.push({name:h.name,element:f[g]}),h.isPropagationStopped())break}b.row(e)})),b._super()}})}),g("1c",["n"],function(a){"use strict";return a.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var a=this,b=a._layout,c=a.classPrefix;return a.classes.add("formitem"),b.preRender(a),'
    '+(a.settings.title?'
    '+a.settings.title+"
    ":"")+'
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "}})}),g("y",["n","1c","c"],function(a,b,c){"use strict";return a.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:15,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var a=this,d=a.items();a.settings.formItemDefaults||(a.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),d.each(function(d){var e,f=d.settings.label;f&&(e=new b(c.extend({items:{type:"label",id:d._id+"-l",text:f,flex:0,forId:d._id,disabled:d.disabled()}},a.settings.formItemDefaults)),e.type="formitem",d.aria("labelledby",d._id+"-l"),"undefined"==typeof d.settings.flex&&(d.settings.flex=1),a.replace(d,e),e.add(d))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){var a=this;a._super(),a.fromJSON(a.settings.data)},bindStates:function(){function a(){var a,c,d,e=0,f=[];if(b.settings.labelGapCalc!==!1)for(d="children"==b.settings.labelGapCalc?b.find("formitem"):b.items(),d.filter("formitem").each(function(a){var b=a.items()[0],c=b.getEl().clientWidth;e=c>e?c:e,f.push(b)}),c=b.settings.labelGap||0,a=f.length;a--;)f[a].settings.minWidth=e+c}var b=this;b._super(),b.on("show",a),a()}})}),g("s",["y"],function(a){"use strict";return a.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var a=this,b=a._layout,c=a.classPrefix;return a.preRender(),b.preRender(a),'
    '+(a.settings.title?''+a.settings.title+"":"")+'
    '+(a.settings.html||"")+b.renderHtml(a)+"
    "}})}),h("3k",Date),h("3l",Math),g("3d",["3k","3l","2k"],function(a,b,c){var d=0,e=function(e){var f=new a,g=f.getTime(),h=b.floor(1e9*b.random());return d++,e+"_"+h+d+c(g)};return{generate:e}}),g("2y",[],function(){return"undefined"==typeof console&&(console={log:function(){}}),console}),g("11",["10","2j","2y","13"],function(a,b,c,d){var e=function(a,b){var e=b||d,f=e.createElement("div");if(f.innerHTML=a,!f.hasChildNodes()||f.childNodes.length>1)throw c.error("HTML does not have a single root node",a),"HTML must have a single root node";return h(f.childNodes[0])},f=function(a,b){var c=b||d,e=c.createElement(a);return h(e)},g=function(a,b){var c=b||d,e=c.createTextNode(a);return h(e)},h=function(c){if(null===c||void 0===c)throw new b("Node cannot be null or undefined");return{dom:a.constant(c)}};return{fromHtml:e,fromTag:f,fromText:g,fromDom:h}}),g("3n",[],function(){var a=function(a){var b,c=!1;return function(){return c||(c=!0,b=a.apply(null,arguments)),b}};return{cached:a}}),g("3i",[],function(){return{ATTRIBUTE:2,CDATA_SECTION:4,COMMENT:8,DOCUMENT:9,DOCUMENT_TYPE:10,DOCUMENT_FRAGMENT:11,ELEMENT:1,TEXT:3,PROCESSING_INSTRUCTION:7,ENTITY_REFERENCE:5,ENTITY:6,NOTATION:12}}),g("33",["3i"],function(a){var b=function(a){var b=a.dom().nodeName;return b.toLowerCase()},c=function(a){return a.dom().nodeType},d=function(a){return a.dom().nodeValue},e=function(a){return function(b){return c(b)===a}},f=function(d){return c(d)===a.COMMENT||"#comment"===b(d)},g=e(a.ELEMENT),h=e(a.TEXT),i=e(a.DOCUMENT);return{name:b,type:c,value:d,isElement:g,isText:h,isDocument:i,isComment:f}}),g("3g",["3n","11","33","13"],function(a,b,c,d){var e=function(a){var b=c.isText(a)?a.dom().parentNode:a.dom();return void 0!==b&&null!==b&&b.ownerDocument.body.contains(b)},f=a.cached(function(){return g(b.fromDom(d))}),g=function(a){var c=a.dom().body;if(null===c||void 0===c)throw"Body is not available yet";return b.fromDom(c)};return{body:f,getBody:g,inBody:e}}),g("3f",["2i","2k"],function(a,b){var c=function(c){if(null===c)return"null";var d=typeof c;return"object"===d&&a.prototype.isPrototypeOf(c)?"array":"object"===d&&b.prototype.isPrototypeOf(c)?"string":d},d=function(a){return function(b){return c(b)===a}};return{isString:d("string"),isObject:d("object"),isArray:d("array"),isNull:d("null"),isBoolean:d("boolean"),isUndefined:d("undefined"),isFunction:d("function"),isNumber:d("number")}}),g("42",["z","10","2i","2j"],function(a,b,c,d){return function(){var e=arguments;return function(){for(var f=new c(arguments.length),g=0;g0&&e.unsuppMessage(m);var n={};return a.each(h,function(a){n[a]=b.constant(f[a])}),a.each(i,function(a){n[a]=b.constant(g.prototype.hasOwnProperty.call(f,a)?d.some(f[a]):d.none())}),n}}}),g("3u",["42","43"],function(a,b){return{immutable:a,immutableBag:b}}),g("3v",[],function(){var a=function(a,b){var c=[],d=function(a){return c.push(a),b(a)},e=b(a);do e=e.bind(d);while(e.isSome());return c};return{toArray:a}}),g("44",[],function(){return"undefined"!=typeof window?window:Function("return this;")()}),g("3w",["44"],function(a){var b=function(b,c){for(var d=void 0!==c?c:a,e=0;e0&&b0},C=function(b){var c=A(b);return a.filter(y(c).concat(z(c)),B)};return{find:C}}),g("t",["z","10","1","2x","15","m","c"],function(a,b,c,d,e,f,g){"use strict";var h=function(){return c.tinymce?c.tinymce.activeEditor:e.activeEditor},i={},j=5,k=function(){i={}},l=function(a){return{title:a.title,value:{title:{raw:a.title},url:a.url,attach:a.attach}}},m=function(a){return g.map(a,l)},n=function(a,c){return{title:a,value:{title:a,url:c,attach:b.noop}}},o=function(b,c){var d=a.exists(c,function(a){return a.url===b});return!d},p=function(a,b,c){var d=b in a?a[b]:c;return d===!1?null:d},q=function(c,d,e,f){var h={title:"-"},j=function(c){var f=c.hasOwnProperty(e)?c[e]:[],h=a.filter(f,function(a){return o(a,d)});return g.map(h,function(a){return{title:a,value:{title:a,url:a,attach:b.noop}}})},k=function(b){var c=a.filter(d,function(a){return a.type===b});return m(c)},l=function(){var a=k("anchor"),b=p(f,"anchor_top","#top"),c=p(f,"anchor_bottom","#bottom");return null!==b&&a.unshift(n("",b)),null!==c&&a.push(n("",c)),a},q=function(b){return a.foldl(b,function(a,b){var c=0===a.length||0===b.length;return c?a.concat(b):a.concat(h,b)},[])};return f.typeahead_urls===!1?[]:"file"===e?q([s(c,j(i)),s(c,k("header")),s(c,l())]):s(c,j(i))},r=function(b,c){var d=i[c];/^https?/.test(b)&&(d?a.indexOf(d,b)===-1&&(i[c]=d.slice(0,j).concat(b)):i[c]=[b])},s=function(a,b){var c=a.toLowerCase(),d=g.grep(b,function(a){return a.title.toLowerCase().indexOf(c)!==-1});return 1===d.length&&d[0].title===a?[]:d},t=function(a){var b=a.title;return b.raw?b.raw:b},u=function(a,b,c,e){var f=function(f){var g=d.find(c),h=q(f,g,e,b);a.showAutoComplete(h,f)};a.on("autocomplete",function(){f(a.value())}),a.on("selectitem",function(b){var c=b.value;a.value(c.url);var d=t(c);"image"===e?a.fire("change",{meta:{alt:d,attach:c.attach}}):a.fire("change",{meta:{text:d,attach:c.attach}}),a.focus()}),a.on("click",function(b){0===a.value().length&&"INPUT"===b.target.nodeName&&f("")}),a.on("PostRender",function(){a.getRoot().on("submit",function(b){b.isDefaultPrevented()||r(a.value(),e)})})},v=function(a){var b=a.status,c=a.message;return"valid"===b?{status:"ok",message:c}:"unknown"===b?{status:"warn",message:c}:"invalid"===b?{status:"warn",message:c}:{status:"none",message:""}},w=function(a,b,c){var d=b.filepicker_validator_handler;if(d){var e=function(b){return 0===b.length?void a.statusLevel("none"):void d({url:b,type:c},function(b){var c=v(b);a.statusMessage(c.message),a.statusLevel(c.status)})};a.state.on("change:value",function(a){e(a.value)})}};return f.extend({Statics:{clearHistory:k},init:function(a){var b,d,e,f=this,i=h(),j=i.settings,k=a.filetype;a.spellcheck=!1,e=j.file_picker_types||j.file_browser_callback_types,e&&(e=g.makeMap(e,/[, ]/)),e&&!e[k]||(d=j.file_picker_callback,!d||e&&!e[k]?(d=j.file_browser_callback,!d||e&&!e[k]||(b=function(){d(f.getEl("inp").id,f.value(),k,c)})):b=function(){var a=f.fire("beforecall").meta;a=g.extend({filetype:k},a),d.call(i,function(a,b){f.value(a).fire("change",{meta:b})},f.value(),a)}),b&&(a.icon="browse",a.onaction=b),f._super(a),u(f,j,i.getBody(),k),w(f,j,k)}})}),g("u",["d"],function(a){"use strict";return a.extend({recalc:function(a){var b=a.layoutRect(),c=a.paddingBox;a.items().filter(":visible").each(function(a){a.layoutRect({x:c.left,y:c.top,w:b.innerW-c.right-c.left,h:b.innerH-c.top-c.bottom}),a.recalc&&a.recalc()})}})}),g("v",["d"],function(a){"use strict";return a.extend({recalc:function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N=[],O=Math.max,P=Math.min;for(d=a.items().filter(":visible"),e=a.layoutRect(),f=a.paddingBox,g=a.settings,m=a.isRtl()?g.direction||"row-reversed":g.direction,h=g.align,i=a.isRtl()?g.pack||"end":g.pack,j=g.spacing||0,"row-reversed"!=m&&"column-reverse"!=m||(d=d.set(d.toArray().reverse()),m=m.split("-")[0]),"column"==m?(z="y",x="h",y="minH",A="maxH",C="innerH",B="top",D="deltaH",E="contentH",J="left",H="w",F="x",G="innerW",I="minW",K="right",L="deltaW",M="contentW"):(z="x",x="w",y="minW",A="maxW",C="innerW",B="left",D="deltaW",E="contentW",J="top",H="h",F="y",G="innerH",I="minH",K="bottom",L="deltaH",M="contentH"),l=e[C]-f[B]-f[B],w=k=0,b=0,c=d.length;b0&&(k+=q,o[A]&&N.push(n),o.flex=q),l-=o[y],r=f[J]+o[I]+f[K],r>w&&(w=r);if(u={},l<0?u[y]=e[y]-l+e[D]:u[y]=e[C]-l+e[D],u[I]=w+e[L],u[E]=e[C]-l,u[M]=w,u.minW=P(u.minW,e.maxW),u.minH=P(u.minH,e.maxH),u.minW=O(u.minW,e.startMinWidth),u.minH=O(u.minH,e.startMinHeight),!e.autoResize||u.minW==e.minW&&u.minH==e.minH){for(t=l/k,b=0,c=N.length;bs?(l-=o[A]-o[y],k-=o.flex,o.flex=0,o.maxFlexSize=s):o.maxFlexSize=0;for(t=l/k,v=f[B],u={},0===k&&("end"==i?v=l+f[B]:"center"==i?(v=Math.round(e[C]/2-(e[C]-l)/2)+f[B],v<0&&(v=f[B])):"justify"==i&&(v=f[B],j=Math.floor(l/(d.length-1)))),u[F]=f[J],b=0,c=d.length;b0&&(r+=o.flex*t),u[x]=r,u[z]=v,n.layoutRect(u),n.recalc&&n.recalc(),v+=r+j}else if(u.w=u.minW,u.h=u.minH,a.layoutRect(u),this.recalc(a),null===a._lastRect){var Q=a.parent();Q&&(Q._lastRect=null,Q.recalc())}}})}),g("x",["1i"],function(a){return a.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(a){a.items().filter(":visible").each(function(a){a.recalc&&a.recalc()})},isNative:function(){return!0}})}),g("31",["3f","2h"],function(a,b){return function(c,d,e,f,g){return c(e,f)?b.some(e):a.isFunction(g)&&g(e)?b.none():d(e,f,g)}}),g("2z",["3f","z","10","2h","3g","3h","11","31"],function(a,b,c,d,e,f,g,h){var i=function(a){return n(e.body(),a)},j=function(b,e,f){for(var h=b.dom(),i=a.isFunction(f)?f:c.constant(!1);h.parentNode;){h=h.parentNode;var j=g.fromDom(h);if(e(j))return d.some(j);if(i(j))break}return d.none()},k=function(a,b,c){var d=function(a){return b(a)};return h(d,j,a,b,c)},l=function(a,b){var c=a.dom();return c.parentNode?m(g.fromDom(c.parentNode),function(c){return!f.eq(a,c)&&b(c)}):d.none()},m=function(a,d){var e=b.find(a.dom().childNodes,c.compose(d,g.fromDom));return e.map(g.fromDom)},n=function(a,b){var c=function(a){for(var e=0;e0),!b.menu&&b.settings.menu&&b.visible(k(b.settings.menu)>0);var d=b.settings.format;d&&b.visible(a.formatter.canApply(d)),b.visible()||c--}),c}var m;m=e(),t({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"]},function(b,c){a.addButton(c,{tooltip:b[0],cmd:b[1]})}),t({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"]},function(b,c){a.addButton(c,{tooltip:b[0],cmd:b[1],onPostRender:f(c)})});var p=function(a){var b=a;return b.length>0&&"-"===b[0].text&&(b=b.slice(1)),b.length>0&&"-"===b[b.length-1].text&&(b=b.slice(0,b.length-1)),b},q=function(b){var c,d;if("string"==typeof b)d=b.split(" ");else if(i.isArray(b))return u(i.map(b,q));return c=i.grep(d,function(b){return"|"===b||b in a.menuItems}),i.map(c,function(b){return"|"===b?{text:"-"}:a.menuItems[b]})},r=function(b){var c=[{text:"-"}],d=i.grep(a.menuItems,function(a){return a.context===b});return i.each(d,function(a){"before"==a.separator&&c.push({text:"|"}),a.prependToContext?c.unshift(a):c.push(a),"after"==a.separator&&c.push({text:"|"})}),c},s=function(a){return p(a.insert_button_items?q(a.insert_button_items):r("insert"))};a.addButton("undo",{tooltip:"Undo",onPostRender:g("undo"),cmd:"undo"}),a.addButton("redo",{tooltip:"Redo",onPostRender:g("redo"),cmd:"redo"}),a.addMenuItem("newdocument",{text:"New document",icon:"newdocument",cmd:"mceNewDocument"}),a.addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:g("undo"),cmd:"undo"}),a.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:g("redo"),cmd:"redo"}),a.addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:h,cmd:"mceToggleVisualAid"}),a.addButton("remove",{tooltip:"Remove",icon:"remove",cmd:"Delete"}),a.addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(s(a.settings)),this.menu.renderNew()}}),t({cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"]},function(b,c){a.addMenuItem(c,{text:b[0],icon:c,shortcut:b[2],cmd:b[1]})}),a.on("mousedown",function(){n.hideAll()}),a.addButton("styleselect",{type:"menubutton",text:"Formats",menu:m,onShowMenu:function(){a.settings.style_formats_autohide&&l(this.menu)}}),a.addButton("fontselect",function(){var c="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",e=[],f=d(a.settings.font_formats||c);return t(f,function(a){e.push({text:{raw:a[0]},value:a[1],textStyle:a[1].indexOf("dings")==-1?"font-family:"+a[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:b(e),onselect:function(b){b.control.settings.value&&a.execCommand("FontName",!1,b.control.settings.value)}}}),a.addButton("fontsizeselect",function(){var b=[],d="8pt 10pt 12pt 14pt 18pt 24pt 36pt",e=a.settings.fontsize_formats||d;return t(e.split(" "),function(a){var c=a,d=a,e=a.split("=");e.length>1&&(c=e[0],d=e[1]),b.push({text:c,value:d})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:b,fixedWidth:!0,onPostRender:c(b),onclick:function(b){b.control.settings.value&&a.execCommand("FontSize",!1,b.control.settings.value)}}}),a.addMenuItem("formats",{text:"Formats",menu:m})}var t=i.each,u=function(b){return a.foldl(b,function(a,b){return a.concat(b)},[])};j.translate=function(a){return g.translate(a)},p.tooltips=!h.iOS;var v=function(a){r(a),s(a),q(a),l.register(a),k.register(a),m.register(a)};return{setup:v}}),g("1d",["d"],function(a){"use strict";return a.extend({recalc:function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E=[],F=[];b=a.settings,e=a.items().filter(":visible"),f=a.layoutRect(),d=b.columns||Math.ceil(Math.sqrt(e.length)),c=Math.ceil(e.length/d),s=b.spacingH||b.spacing||0,t=b.spacingV||b.spacing||0,u=b.alignH||b.align,v=b.alignV||b.align,q=a.paddingBox,C="reverseRows"in b?b.reverseRows:a.isRtl(),u&&"string"==typeof u&&(u=[u]),v&&"string"==typeof v&&(v=[v]);for(l=0;lE[l]?y:E[l],F[m]=z>F[m]?z:F[m];for(A=f.innerW-q.left-q.right,w=0,l=0;l0?s:0),A-=(l>0?s:0)+E[l];for(B=f.innerH-q.top-q.bottom,x=0,m=0;m0?t:0),B-=(m>0?t:0)+F[m];if(w+=q.left+q.right,x+=q.top+q.bottom,i={},i.minW=w+(f.w-f.innerW),i.minH=x+(f.h-f.innerH),i.contentW=i.minW-f.deltaW,i.contentH=i.minH-f.deltaH,i.minW=Math.min(i.minW,f.maxW),i.minH=Math.min(i.minH,f.maxH),i.minW=Math.max(i.minW,f.startMinWidth),i.minH=Math.max(i.minH,f.startMinHeight),!f.autoResize||i.minW==f.minW&&i.minH==f.minH){f.autoResize&&(i=a.layoutRect(i),i.contentW=i.minW-f.deltaW,i.contentH=i.minH-f.deltaH);var G;G="start"==b.packV?0:B>0?Math.floor(B/c):0;var H=0,I=b.flexWidths;if(I)for(l=0;l'},src:function(a){this.getEl().src=a},html:function(a,c){var d=this,e=this.getEl().contentWindow.document.body;return e?(e.innerHTML=a,c&&c()):b.setTimeout(function(){d.html(a)}),this}})}),g("1f",["1b"],function(a){"use strict";return a.extend({init:function(a){var b=this;b._super(a),b.classes.add("widget").add("infobox"), +b.canFocus=!1},severity:function(a){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(a)},help:function(a){this.state.set("help",a)},renderHtml:function(){var a=this,b=a.classPrefix;return'
    '+a.encode(a.state.get("text"))+'
    '},bindStates:function(){var a=this;return a.state.on("change:text",function(b){a.getEl("body").firstChild.data=a.encode(b.value),a.state.get("rendered")&&a.updateLayoutRect()}),a.state.on("change:help",function(b){a.classes.toggle("has-help",b.value),a.state.get("rendered")&&a.updateLayoutRect()}),a._super()}})}),g("1h",["1b","2m"],function(a,b){"use strict";return a.extend({init:function(a){var b=this;b._super(a),b.classes.add("widget").add("label"),b.canFocus=!1,a.multiline&&b.classes.add("autoscroll"),a.strong&&b.classes.add("strong")},initLayoutRect:function(){var a=this,c=a._super();if(a.settings.multiline){var d=b.getSize(a.getEl());d.width>c.maxW&&(c.minW=c.maxW,a.classes.add("multiline")),a.getEl().style.width=c.minW+"px",c.startMinH=c.h=c.minH=Math.min(c.maxH,b.getSize(a.getEl()).height)}return c},repaint:function(){var a=this;return a.settings.multiline||(a.getEl().style.lineHeight=a.layoutRect().h+"px"),a._super()},severity:function(a){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(a)},renderHtml:function(){var a,b,c=this,d=c.settings.forId,e=c.settings.html?c.settings.html:c.encode(c.state.get("text"));return!d&&(b=c.settings.forName)&&(a=c.getRoot().find("#"+b)[0],a&&(d=a._id)),d?'":''+e+""},bindStates:function(){var a=this;return a.state.on("change:text",function(b){a.innerHtml(a.encode(b.value)),a.state.get("rendered")&&a.updateLayoutRect()}),a._super()}})}),g("29",["n"],function(a){"use strict";return a.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(a){var b=this;b._super(a),b.classes.add("toolbar")},postRender:function(){var a=this;return a.items().each(function(a){a.classes.add("toolbar-item")}),a._super()}})}),g("1l",["29"],function(a){"use strict";return a.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}})}),g("1m",["1","b","f","1l"],function(a,b,c,d){"use strict";function e(a,b){for(;a;){if(b===a)return!0;a=a.parentNode}return!1}var f=c.extend({init:function(a){var b=this;b._renderOpen=!0,b._super(a),a=b.settings,b.classes.add("menubtn"),a.fixedWidth&&b.classes.add("fixed-width"),b.aria("haspopup",!0),b.state.set("menu",a.menu||b.render())},showMenu:function(a){var c,d=this;return d.menu&&d.menu.visible()&&a!==!1?d.hideMenu():(d.menu||(c=d.state.get("menu")||[],d.classes.add("opened"),c.length?c={type:"menu",animate:!0,items:c}:(c.type=c.type||"menu",c.animate=!0),c.renderTo?d.menu=c.parent(d).show().renderTo():d.menu=b.create(c).parent(d).renderTo(),d.fire("createmenu"),d.menu.reflow(),d.menu.on("cancel",function(a){a.control.parent()===d.menu&&(a.stopPropagation(),d.focus(),d.hideMenu())}),d.menu.on("select",function(){d.focus()}),d.menu.on("show hide",function(a){a.control===d.menu&&(d.activeMenu("show"==a.type),d.classes.toggle("opened","show"==a.type)),d.aria("expanded","show"==a.type)}).fire("show")),d.menu.show(),d.menu.layoutRect({w:d.layoutRect().w}),d.menu.moveRel(d.getEl(),d.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]),void d.fire("showmenu"))},hideMenu:function(){var a=this;a.menu&&(a.menu.items().each(function(a){a.hideMenu&&a.hideMenu()}),a.menu.hide())},activeMenu:function(a){this.classes.toggle("active",a)},renderHtml:function(){var b,c=this,e=c._id,f=c.classPrefix,g=c.settings.icon,h=c.state.get("text"),i="";return b=c.settings.image,b?(g="none","string"!=typeof b&&(b=a.getSelection?b[0]:b[1]),b=" style=\"background-image: url('"+b+"')\""):b="",h&&(c.classes.add("btn-has-text"),i=''+c.encode(h)+""),g=c.settings.icon?f+"ico "+f+"i-"+g:"",c.aria("role",c.parent()instanceof d?"menuitem":"button"),'
    '},postRender:function(){var a=this;return a.on("click",function(b){b.control===a&&e(b.target,a.getEl())&&(a.focus(),a.showMenu(!b.aria),b.aria&&a.menu.items().filter(":visible")[0].focus())}),a.on("mouseenter",function(b){var c,d=b.control,e=a.parent();d&&e&&d instanceof f&&d.parent()==e&&(e.items().filter("MenuButton").each(function(a){a.hideMenu&&a!=d&&(a.menu&&a.menu.visible()&&(c=!0),a.hideMenu())}),c&&(d.focus(),d.showMenu()))}),a._super()},bindStates:function(){var a=this;return a.state.on("change:menu",function(){a.menu&&a.menu.remove(),a.menu=null}),a._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}});return f}),g("1n",["1b","b","16","2t"],function(a,b,c,d){"use strict";return a.extend({Defaults:{border:0,role:"menuitem"},init:function(a){var b,c=this;c._super(a),a=c.settings,c.classes.add("menu-item"),a.menu&&c.classes.add("menu-item-expand"),a.preview&&c.classes.add("menu-item-preview"),b=c.state.get("text"),"-"!==b&&"|"!==b||(c.classes.add("menu-item-sep"),c.aria("role","separator"),c.state.set("text","-")),a.selectable&&(c.aria("role","menuitemcheckbox"),c.classes.add("menu-item-checkbox"),a.icon="selected"),a.preview||a.selectable||c.classes.add("menu-item-normal"),c.on("mousedown",function(a){a.preventDefault()}),a.menu&&!a.ariaHideMenu&&c.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var a,c=this,d=c.settings,e=c.parent();if(e.items().each(function(a){a!==c&&a.hideMenu()}),d.menu){a=c.menu,a?a.show():(a=d.menu,a.length?a={type:"menu",animate:!0,items:a}:(a.type=a.type||"menu",a.animate=!0),e.settings.itemDefaults&&(a.itemDefaults=e.settings.itemDefaults),a=c.menu=b.create(a).parent(c).renderTo(),a.reflow(),a.on("cancel",function(b){b.stopPropagation(),c.focus(),a.hide()}),a.on("show hide",function(a){a.control.items&&a.control.items().each(function(a){a.active(a.settings.selected)})}).fire("show"),a.on("hide",function(b){b.control===a&&c.classes.remove("selected")}),a.submenu=!0),a._parentMenu=e,a.classes.add("menu-sub");var f=a.testMoveRel(c.getEl(),c.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);a.moveRel(c.getEl(),f),a.rel=f,f="menu-sub-"+f,a.classes.remove(a._lastRel).add(f),a._lastRel=f,c.classes.add("selected"),c.aria("expanded",!0)}},hideMenu:function(){var a=this;return a.menu&&(a.menu.items().each(function(a){a.hideMenu&&a.hideMenu()}),a.menu.hide(),a.aria("expanded",!1)),a},renderHtml:function(){function a(a){var b,d,e={};for(e=c.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},a=a.split("+"),b=0;b").replace(new RegExp(b("]mce~match!"),"g"),"")}var f=this,g=f._id,h=f.settings,i=f.classPrefix,j=f.state.get("text"),k=f.settings.icon,l="",m=h.shortcut,n=f.encode(h.url),o="";return k&&f.parent().classes.add("menu-has-icons"),h.image&&(l=" style=\"background-image: url('"+h.image+"')\""),m&&(m=a(m)),k=i+"ico "+i+"i-"+(f.settings.icon||"none"),o="-"!==j?'\xa0":"",j=e(f.encode(d(j))),n=e(f.encode(d(n))),'
    '+o+("-"!==j?''+j+"":"")+(m?'
    '+m+"
    ":"")+(h.menu?'
    ':"")+(n?'":"")+"
    "},postRender:function(){var a=this,b=a.settings,c=b.textStyle;if("function"==typeof c&&(c=c.call(this)),c){var e=a.getEl("text");e&&e.setAttribute("style",c)}return a.on("mouseenter click",function(c){c.control===a&&(b.menu||"click"!==c.type?(a.showMenu(),c.aria&&a.menu.focus(!0)):(a.fire("select"),d.requestAnimationFrame(function(){a.parent().hideAll()})))}),a._super(),a},hover:function(){var a=this;return a.parent().items().each(function(a){a.classes.remove("selected")}),a.classes.toggle("selected",!0),a},active:function(a){return"undefined"!=typeof a&&this.aria("checked",a),this._super(a)},remove:function(){this._super(),this.menu&&this.menu.remove()}})}),g("1k",["16","2t","c","w","1n","28"],function(a,b,c,d,e,f){"use strict";return d.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(b){var d=this;if(b.autohide=!0,b.constrainToViewport=!0,"function"==typeof b.items&&(b.itemsFactory=b.items,b.items=[]),b.itemDefaults)for(var e=b.items,f=e.length;f--;)e[f]=c.extend({},b.itemDefaults,e[f]);d._super(b),d.classes.add("menu"),b.animate&&11!==a.ie&&d.classes.add("animate")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){var a=this;a.hideAll(),a.fire("select")},load:function(){function a(){d.throbber&&(d.throbber.hide(),d.throbber=null)}var b,c,d=this;c=d.settings.itemsFactory,c&&(d.throbber||(d.throbber=new f(d.getEl("body"),!0),0===d.items().length?(d.throbber.show(),d.fire("loading")):d.throbber.show(100,function(){d.items().remove(),d.fire("loading")}),d.on("hide close",a)),d.requestTime=b=(new Date).getTime(),d.settings.itemsFactory(function(c){return 0===c.length?void d.hide():void(d.requestTime===b&&(d.getEl().style.width="",d.getEl("body").style.width="",a(),d.items().remove(),d.getEl("body").innerHTML="",d.add(c),d.renderNew(),d.fire("loaded")))}))},hideAll:function(){var a=this;return this.find("menuitem").exec("hideMenu"),a._super()},preRender:function(){var a=this;return a.items().each(function(b){var c=b.settings;if(c.icon||c.image||c.selectable)return a._hasIcons=!0,!1}),a.settings.itemsFactory&&a.on("postrender",function(){a.settings.itemsFactory&&a.load()}),a.on("show hide",function(c){c.control===a&&("show"===c.type?b.setTimeout(function(){a.classes.add("in")},0):a.classes.remove("in"))}),a._super()}})}),g("1j",["1m","1k"],function(a,b){"use strict";return a.extend({init:function(a){function b(c){for(var f=0;f0&&(e=c[0].text,g.state.set("value",c[0].value)),g.state.set("menu",c)),g.state.set("text",a.text||e),g.classes.add("listbox"),g.on("select",function(b){var c=b.control;f&&(b.lastControl=f),a.multiple?c.active(!c.active()):g.value(b.control.value()),f=c})},bindStates:function(){function a(a,c){a instanceof b&&a.items().each(function(a){a.hasMenus()||a.active(a.value()===c)})}function c(a,b){var d;if(a)for(var e=0;e'},postRender:function(){var a=this;a._super(),a.resizeDragHelper=new b(this._id,{start:function(){a.fire("ResizeStart")},drag:function(b){"both"!=a.settings.direction&&(b.deltaX=0),a.fire("Resize",b)},stop:function(){a.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}})}),g("20",["1b"],function(a){"use strict";function b(a){var b="";if(a)for(var c=0;c'+a[c]+"";return b}return a.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(a){var b=this;b._super(a),b.settings.size&&(b.size=b.settings.size),b.settings.options&&(b._options=b.settings.options),b.on("keydown",function(a){var c;13==a.keyCode&&(a.preventDefault(),b.parents().reverse().each(function(a){if(a.toJSON)return c=a,!1}),b.fire("submit",{data:c.toJSON()}))})},options:function(a){return arguments.length?(this.state.set("options",a),this):this.state.get("options")},renderHtml:function(){var a,c=this,d="";return a=b(c._options),c.size&&(d=' size = "'+c.size+'"'),'"},bindStates:function(){var a=this;return a.state.on("change:options",function(c){a.getEl().innerHTML=b(c.value)}),a._super()}})}),g("22",["1b","p","2m"],function(a,b,c){"use strict";function d(a,b,c){return ac&&(a=c),a}function e(a,b,c){a.setAttribute("aria-"+b,c)}function f(a,b){var d,f,g,h,i,j;"v"==a.settings.orientation?(h="top",g="height",f="h"):(h="left",g="width",f="w"),j=a.getEl("handle"),d=(a.layoutRect()[f]||100)-c.getSize(j)[g],i=d*((b-a._minValue)/(a._maxValue-a._minValue))+"px",j.style[h]=i,j.style.height=a.layoutRect().h+"px",e(j,"valuenow",b),e(j,"valuetext",""+a.settings.previewFilter(b)),e(j,"valuemin",a._minValue),e(j,"valuemax",a._maxValue)}return a.extend({init:function(a){var b=this;a.previewFilter||(a.previewFilter=function(a){return Math.round(100*a)/100}),b._super(a),b.classes.add("slider"),"v"==a.orientation&&b.classes.add("vertical"),b._minValue=a.minValue||0,b._maxValue=a.maxValue||100,b._initValue=b.state.get("value")},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix;return'
    '},reset:function(){this.value(this._initValue).repaint()},postRender:function(){function a(a,b,c){return(c+a)/(b-a)}function e(a,b,c){return c*(b-a)-a}function f(b,c){function f(f){var g;g=n.value(),g=e(b,c,a(b,c,g)+.05*f),g=d(g,b,c),n.value(g),n.fire("dragstart",{value:g}),n.fire("drag",{value:g}),n.fire("dragend",{value:g})}n.on("keydown",function(a){switch(a.keyCode){case 37:case 38:f(-1);break;case 39:case 40:f(1)}})}function g(a,e,f){var g,h,i,o,p;n._dragHelper=new b(n._id,{handle:n._id+"-handle",start:function(a){g=a[j],h=parseInt(n.getEl("handle").style[k],10),i=(n.layoutRect()[m]||100)-c.getSize(f)[l],n.fire("dragstart",{value:p})},drag:function(b){var c=b[j]-g;o=d(h+c,0,i),f.style[k]=o+"px",p=a+o/i*(e-a),n.value(p),n.tooltip().text(""+n.settings.previewFilter(p)).show().moveRel(f,"bc tc"),n.fire("drag",{value:p})},stop:function(){n.tooltip().hide(),n.fire("dragend",{value:p})}})}var h,i,j,k,l,m,n=this;h=n._minValue,i=n._maxValue,"v"==n.settings.orientation?(j="screenY",k="top",l="height",m="h"):(j="screenX",k="left",l="width",m="w"),n._super(),f(h,i,n.getEl("handle")),g(h,i,n.getEl("handle"))},repaint:function(){this._super(),f(this,this.value())},bindStates:function(){var a=this;return a.state.on("change:value",function(b){f(a,b.value)}),a._super()}})}),g("23",["1b"],function(a){"use strict";return a.extend({renderHtml:function(){var a=this;return a.classes.add("spacer"),a.canFocus=!1,'
    '}})}),g("24",["1","2n","2m","1m"],function(a,b,c,d){return d.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var a,d,e=this,f=e.getEl(),g=e.layoutRect();return e._super(),a=f.firstChild,d=f.lastChild,b(a).css({width:g.w-c.getSize(d).width,height:g.h-2}),b(d).css({height:g.h-2}),e},activeMenu:function(a){var c=this;b(c.getEl().lastChild).toggleClass(c.classPrefix+"active",a)},renderHtml:function(){var b,c=this,d=c._id,e=c.classPrefix,f=c.state.get("icon"),g=c.state.get("text"),h="";return b=c.settings.image,b?(f="none","string"!=typeof b&&(b=a.getSelection?b[0]:b[1]),b=" style=\"background-image: url('"+b+"')\""):b="",f=c.settings.icon?e+"ico "+e+"i-"+f:"",g&&(c.classes.add("btn-has-text"),h=''+c.encode(g)+""),'
    '},postRender:function(){var a=this,b=a.settings.onclick;return a.on("click",function(a){var c=a.target;if(a.control==this)for(;c;){if(a.aria&&"down"!=a.aria.key||"BUTTON"==c.nodeName&&c.className.indexOf("open")==-1)return a.stopImmediatePropagation(),void(b&&b.call(this,a));c=c.parentNode}}),delete a.settings.onclick,a._super()}})}),g("25",["x"],function(a){"use strict";return a.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}})}),g("26",["1r","2n","2m"],function(a,b,c){"use strict";return a.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(a){var c;this.activeTabId&&(c=this.getEl(this.activeTabId),b(c).removeClass(this.classPrefix+"active"),c.setAttribute("aria-selected","false")),this.activeTabId="t"+a,c=this.getEl("t"+a),c.setAttribute("aria-selected","true"),b(c).addClass(this.classPrefix+"active"),this.items()[a].show().fire("showtab"),this.reflow(),this.items().each(function(b,c){a!=c&&b.hide()})},renderHtml:function(){var a=this,b=a._layout,c="",d=a.classPrefix;return a.preRender(),b.preRender(a),a.items().each(function(b,e){var f=a._id+"-t"+e;b.aria("role","tabpanel"),b.aria("labelledby",f),c+='"}),'
    '+c+'
    '+b.renderHtml(a)+"
    "},postRender:function(){var a=this;a._super(),a.settings.activeTab=a.settings.activeTab||0,a.activateTab(a.settings.activeTab),this.on("click",function(b){var c=b.target.parentNode;if(c&&c.id==a._id+"-head")for(var d=c.childNodes.length;d--;)c.childNodes[d]==b.target&&a.activateTab(d)})},initLayoutRect:function(){var a,b,d,e=this;b=c.getSize(e.getEl("head")).width,b=b<0?0:b,d=0,e.items().each(function(a){b=Math.max(b,a.layoutRect().minW),d=Math.max(d,a.layoutRect().minH)}),e.items().each(function(a){a.settings.x=0,a.settings.y=0,a.settings.w=b,a.settings.h=d,a.layoutRect({x:0,y:0,w:b,h:d})});var f=c.getSize(e.getEl("head")).height;return e.settings.minWidth=b,e.settings.minHeight=d+f,a=e._super(),a.deltaH+=f,a.innerH=a.h-a.deltaH,a}})}),g("27",["13","c","2m","1b"],function(a,b,c,d){return d.extend({init:function(a){var b=this;b._super(a),b.classes.add("textbox"),a.multiline?b.classes.add("multiline"):(b.on("keydown",function(a){var c;13==a.keyCode&&(a.preventDefault(),b.parents().reverse().each(function(a){if(a.toJSON)return c=a,!1}),b.fire("submit",{data:c.toJSON()}))}),b.on("keyup",function(a){b.state.set("value",a.target.value)}))},repaint:function(){var b,c,d,e,f,g=this,h=0;b=g.getEl().style,c=g._layoutRect,f=g._lastRepaintRect||{};var i=a;return!g.settings.multiline&&i.all&&(!i.documentMode||i.documentMode<=8)&&(b.lineHeight=c.h-h+"px"),d=g.borderBox,e=d.left+d.right+8,h=d.top+d.bottom+(g.settings.multiline?8:0),c.x!==f.x&&(b.left=c.x+"px",f.x=c.x),c.y!==f.y&&(b.top=c.y+"px",f.y=c.y),c.w!==f.w&&(b.width=c.w-e+"px",f.w=c.w),c.h!==f.h&&(b.height=c.h-h+"px",f.h=c.h),g._lastRepaintRect=f,g.fire("repaint",{},!1),g},renderHtml:function(){var a,d,e=this,f=e.settings;return a={id:e._id,hidefocus:"1"},b.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(b){a[b]=f[b]}),e.disabled()&&(a.disabled="disabled"),f.subtype&&(a.type=f.subtype),d=c.create(f.multiline?"textarea":"input",a),d.value=e.state.get("value"),d.className=e.classes,d.outerHTML},value:function(a){return arguments.length?(this.state.set("value",a),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var a=this;a.getEl().value=a.state.get("value"),a._super(),a.$el.on("change",function(b){a.state.set("value",b.target.value),a.fire("change",b)})},bindStates:function(){var a=this;return a.state.on("change:value",function(b){a.getEl().value!=b.value&&(a.getEl().value=b.value)}),a.state.on("change:disabled",function(b){a.getEl().disabled=b.value}),a._super()},remove:function(){this.$el.off(),this._super()}})}),g("4",["b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","5","1c","1d","1e","1f","1g","1h","1i","1j","1k","1l","1m","1n","1o","1p","1q","1r","1s","1t","1u","1v","1w","1x","1y","1z","20","21","22","23","24","25","26","27","28","29","2a","1b","2b"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca,da,ea,fa,ga,ha){var ia=function(){return{Selector:Y,Collection:h,ReflowQueue:T,Control:n,Factory:a,KeyboardNavigation:D,Container:m,DragHelper:o,Scrollable:W,Panel:O,Movable:M,Resizable:U,FloatPanel:v,Window:ha,MessageBox:L,Tooltip:fa,Widget:ga,Progress:R,Notification:N,Layout:F,AbsoluteLayout:c,Button:e,ButtonGroup:f,Checkbox:g,ComboBox:l,ColorBox:i,PanelButton:P,ColorButton:j,ColorPicker:k,Path:Q,ElementPath:q,FormItem:z,Form:x,FieldSet:r,FilePicker:s,FitLayout:t,FlexLayout:u,FlowLayout:w,FormatControls:y,GridLayout:A,Iframe:B,InfoBox:C,Label:E,Toolbar:ea,MenuBar:I,MenuButton:J,MenuItem:K,Throbber:da,Menu:H,ListBox:G,Radio:S,ResizeHandle:V,SelectBox:X,Slider:Z,Spacer:$,SplitButton:_,StackLayout:aa,TabPanel:ba,TextBox:ca,DropZone:p,BrowseButton:d}},ja=function(a){a.ui?b.each(ia(),function(b,c){a.ui[c]=b}):a.ui=ia()},ka=function(){b.each(ia(),function(b,c){a.add(c,b)})},la={appendTo:ja,registerToFactory:ka};return la}),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return d.registerToFactory(),d.appendTo(a.tinymce?a.tinymce:{}),b.add("modern",function(a){return e.setup(a),c.get(a)}),function(){}}),d("0")()}(); \ No newline at end of file diff --git a/media/vendor/tinymce/tinymce.js b/media/vendor/tinymce/tinymce.js index c0e57c33ddaa3..5bb01f1435923 100644 --- a/media/vendor/tinymce/tinymce.js +++ b/media/vendor/tinymce/tinymce.js @@ -1,4 +1,4 @@ -// 4.6.7 (2017-09-18) +// 4.7.0 (2017-10-03) (function () { var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)} @@ -58,8 +58,8 @@ var req = function (ids, callback) { var len = ids.length; var instances = new Array(len); for (var i = 0; i < len; ++i) - instances.push(dem(ids[i])); - callback.apply(null, callback); + instances[i] = dem(ids[i]); + callback.apply(null, instances); }; var ephox = {}; @@ -77,12 +77,12 @@ ephox.bolt = { var define = def; var require = req; var demand = dem; -// this helps with minificiation when using a lot of global references +// this helps with minification when using a lot of global references var defineGlobal = function (id, ref) { define(id, [], function () { return ref; }); }; /*jsc -["tinymce.core.api.Main","ephox.katamari.api.Fun","tinymce.core.api.Tinymce","global!Array","global!Error","tinymce.core.AddOnManager","tinymce.core.api.Formatter","tinymce.core.api.NotificationManager","tinymce.core.api.WindowManager","tinymce.core.dom.BookmarkManager","tinymce.core.dom.ControlSelection","tinymce.core.dom.DomQuery","tinymce.core.dom.DOMUtils","tinymce.core.dom.EventUtils","tinymce.core.dom.RangeUtils","tinymce.core.dom.ScriptLoader","tinymce.core.dom.Selection","tinymce.core.dom.Serializer","tinymce.core.dom.Sizzle","tinymce.core.dom.TreeWalker","tinymce.core.Editor","tinymce.core.EditorCommands","tinymce.core.EditorManager","tinymce.core.EditorObservable","tinymce.core.Env","tinymce.core.FocusManager","tinymce.core.geom.Rect","tinymce.core.html.DomParser","tinymce.core.html.Entities","tinymce.core.html.Node","tinymce.core.html.SaxParser","tinymce.core.html.Schema","tinymce.core.html.Serializer","tinymce.core.html.Styles","tinymce.core.html.Writer","tinymce.core.Shortcuts","tinymce.core.ui.Api","tinymce.core.UndoManager","tinymce.core.util.Class","tinymce.core.util.Color","tinymce.core.util.Delay","tinymce.core.util.EventDispatcher","tinymce.core.util.I18n","tinymce.core.util.JSON","tinymce.core.util.JSONP","tinymce.core.util.JSONRequest","tinymce.core.util.LocalStorage","tinymce.core.util.Observable","tinymce.core.util.Promise","tinymce.core.util.Tools","tinymce.core.util.URI","tinymce.core.util.VK","tinymce.core.util.XHR","tinymce.core.util.Arr","tinymce.core.dom.Range","tinymce.core.dom.StyleSheetLoader","ephox.katamari.api.Cell","tinymce.core.fmt.ApplyFormat","tinymce.core.fmt.FormatChanged","tinymce.core.fmt.FormatRegistry","tinymce.core.fmt.MatchFormat","tinymce.core.fmt.Preview","tinymce.core.fmt.RemoveFormat","tinymce.core.fmt.ToggleFormat","tinymce.core.keyboard.FormatShortcuts","ephox.katamari.api.Arr","ephox.katamari.api.Option","tinymce.core.EditorView","tinymce.core.ui.NotificationManagerImpl","tinymce.core.ui.WindowManagerImpl","tinymce.core.caret.CaretBookmark","tinymce.core.caret.CaretContainer","tinymce.core.caret.CaretPosition","tinymce.core.dom.NodeType","tinymce.core.text.Zwsp","ephox.sugar.api.node.Element","ephox.sugar.api.search.Selectors","tinymce.core.dom.RangePoint","ephox.sugar.api.dom.Compare","tinymce.core.dom.ScrollIntoView","tinymce.core.dom.TridentSelection","tinymce.core.selection.FragmentReader","tinymce.core.selection.MultiRange","tinymce.core.delete.DeleteCommands","tinymce.core.InsertContent","tinymce.core.EditorFocus","tinymce.core.EditorSettings","tinymce.core.init.Render","tinymce.core.Mode","tinymce.core.ui.Sidebar","global!document","tinymce.core.util.Uuid","ephox.katamari.api.Type","tinymce.core.ErrorReporter","tinymce.core.LegacyInput","tinymce.core.ui.Selector","tinymce.core.ui.Collection","tinymce.core.ui.ReflowQueue","tinymce.core.ui.Control","tinymce.core.ui.Factory","tinymce.core.ui.KeyboardNavigation","tinymce.core.ui.Container","tinymce.core.ui.DragHelper","tinymce.core.ui.Scrollable","tinymce.core.ui.Panel","tinymce.core.ui.Movable","tinymce.core.ui.Resizable","tinymce.core.ui.FloatPanel","tinymce.core.ui.Window","tinymce.core.ui.MessageBox","tinymce.core.ui.Tooltip","tinymce.core.ui.Widget","tinymce.core.ui.Progress","tinymce.core.ui.Notification","tinymce.core.ui.Layout","tinymce.core.ui.AbsoluteLayout","tinymce.core.ui.Button","tinymce.core.ui.ButtonGroup","tinymce.core.ui.Checkbox","tinymce.core.ui.ComboBox","tinymce.core.ui.ColorBox","tinymce.core.ui.PanelButton","tinymce.core.ui.ColorButton","tinymce.core.ui.ColorPicker","tinymce.core.ui.Path","tinymce.core.ui.ElementPath","tinymce.core.ui.FormItem","tinymce.core.ui.Form","tinymce.core.ui.FieldSet","tinymce.core.ui.FilePicker","tinymce.core.ui.FitLayout","tinymce.core.ui.FlexLayout","tinymce.core.ui.FlowLayout","tinymce.core.ui.FormatControls","tinymce.core.ui.GridLayout","tinymce.core.ui.Iframe","tinymce.core.ui.InfoBox","tinymce.core.ui.Label","tinymce.core.ui.Toolbar","tinymce.core.ui.MenuBar","tinymce.core.ui.MenuButton","tinymce.core.ui.MenuItem","tinymce.core.ui.Throbber","tinymce.core.ui.Menu","tinymce.core.ui.ListBox","tinymce.core.ui.Radio","tinymce.core.ui.ResizeHandle","tinymce.core.ui.SelectBox","tinymce.core.ui.Slider","tinymce.core.ui.Spacer","tinymce.core.ui.SplitButton","tinymce.core.ui.StackLayout","tinymce.core.ui.TabPanel","tinymce.core.ui.TextBox","tinymce.core.ui.DropZone","tinymce.core.ui.BrowseButton","tinymce.core.undo.Levels","global!Object","global!String","ephox.katamari.api.Future","ephox.katamari.api.Futures","ephox.katamari.api.Result","tinymce.core.util.Fun","tinymce.core.caret.CaretCandidate","tinymce.core.geom.ClientRect","tinymce.core.text.ExtendingChar","tinymce.core.dom.RangeNormalizer","tinymce.core.fmt.CaretFormat","tinymce.core.fmt.ExpandRange","tinymce.core.fmt.FormatUtils","tinymce.core.fmt.Hooks","tinymce.core.fmt.MergeFormats","tinymce.core.fmt.DefaultFormats","ephox.sand.api.Node","ephox.sand.api.PlatformDetection","global!console","ephox.sugar.api.node.NodeTypes","ephox.sugar.api.properties.Css","ephox.sugar.api.search.Traverse","tinymce.core.ui.DomUtils","tinymce.core.data.ObservableObject","tinymce.core.ui.BoxUtils","tinymce.core.ui.ClassList","ephox.sugar.api.dom.Insert","ephox.sugar.api.dom.Replication","ephox.sugar.api.node.Fragment","ephox.sugar.api.node.Node","ephox.sugar.api.search.SelectorFind","tinymce.core.dom.ElementType","tinymce.core.dom.Parents","tinymce.core.selection.SelectionUtils","tinymce.core.selection.SimpleTableModel","tinymce.core.selection.TableCellSelection","tinymce.core.delete.BlockBoundaryDelete","tinymce.core.delete.BlockRangeDelete","tinymce.core.delete.CefDelete","tinymce.core.delete.DeleteUtils","tinymce.core.delete.InlineBoundaryDelete","tinymce.core.delete.TableDelete","tinymce.core.caret.CaretWalker","tinymce.core.dom.ElementUtils","tinymce.core.dom.PaddingBr","tinymce.core.InsertList","tinymce.core.caret.CaretFinder","ephox.katamari.api.Obj","ephox.katamari.api.Strings","ephox.katamari.api.Struct","global!window","tinymce.core.init.Init","tinymce.core.PluginManager","tinymce.core.ThemeManager","tinymce.core.content.LinkTargets","tinymce.core.fmt.FontInfo","global!RegExp","tinymce.core.undo.Fragments","ephox.katamari.api.LazyValue","ephox.katamari.async.Bounce","ephox.katamari.async.AsyncValues","tinymce.core.caret.CaretUtils","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.sugar.alien.Recurse","ephox.sand.util.Global","ephox.katamari.api.Thunk","ephox.sand.core.PlatformDetection","global!navigator","ephox.sugar.api.dom.Remove","ephox.sugar.api.node.Text","ephox.sugar.api.search.SelectorFilter","ephox.sugar.api.properties.Attr","ephox.sugar.api.node.Body","ephox.sugar.impl.Style","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts","tinymce.core.data.Binding","ephox.sugar.api.dom.InsertAll","ephox.sugar.api.search.PredicateFind","ephox.sugar.impl.ClosestOrAncestor","ephox.katamari.api.Options","tinymce.core.delete.BlockBoundary","tinymce.core.delete.MergeBlocks","tinymce.core.delete.CefDeleteAction","tinymce.core.delete.DeleteElement","tinymce.core.keyboard.BoundaryCaret","tinymce.core.keyboard.BoundaryLocation","tinymce.core.keyboard.BoundarySelection","tinymce.core.keyboard.InlineUtils","tinymce.core.delete.TableDeleteAction","tinymce.core.init.InitContentBody","tinymce.core.undo.Diff","global!setTimeout","ephox.katamari.util.BagUtils","ephox.katamari.api.Resolve","ephox.sand.core.Browser","ephox.sand.core.OperatingSystem","ephox.sand.detect.DeviceType","ephox.sand.detect.UaString","ephox.sand.info.PlatformInfo","ephox.sugar.impl.NodeValue","ephox.sugar.api.search.PredicateFilter","tinymce.core.dom.Empty","ephox.katamari.api.Adt","tinymce.core.caret.CaretContainerInline","tinymce.core.caret.CaretContainerRemove","tinymce.core.text.Bidi","tinymce.core.util.LazyEvaluator","tinymce.core.caret.CaretContainerInput","tinymce.core.EditorUpload","tinymce.core.ForceBlocks","tinymce.core.keyboard.KeyboardOverrides","tinymce.core.NodeChange","tinymce.core.SelectionOverrides","tinymce.core.util.Quirks","ephox.katamari.api.Global","ephox.sand.detect.Version","ephox.sugar.api.search.SelectorExists","tinymce.core.file.Uploader","tinymce.core.file.ImageScanner","tinymce.core.file.BlobCache","tinymce.core.file.UploadStatus","tinymce.core.keyboard.ArrowKeys","tinymce.core.keyboard.DeleteBackspaceKeys","tinymce.core.keyboard.EnterKey","tinymce.core.keyboard.SpaceKey","tinymce.core.DragDropOverrides","tinymce.core.caret.FakeCaret","tinymce.core.caret.LineUtils","tinymce.core.keyboard.CefUtils","tinymce.core.dom.NodePath","global!Number","tinymce.core.file.Conversions","ephox.sand.api.URL","tinymce.core.keyboard.CefNavigation","tinymce.core.keyboard.MatchKeys","tinymce.core.keyboard.InsertNewLine","tinymce.core.keyboard.InsertSpace","tinymce.core.dom.MousePosition","tinymce.core.dom.Dimensions","ephox.sand.api.Window","tinymce.core.caret.LineWalker","ephox.katamari.api.Merger"] +["tinymce.core.api.Main","ephox.katamari.api.Fun","global!window","tinymce.core.api.Tinymce","global!Array","global!Error","tinymce.core.AddOnManager","tinymce.core.api.Formatter","tinymce.core.api.NotificationManager","tinymce.core.api.WindowManager","tinymce.core.dom.BookmarkManager","tinymce.core.dom.ControlSelection","tinymce.core.dom.DomQuery","tinymce.core.dom.DOMUtils","tinymce.core.dom.EventUtils","tinymce.core.dom.RangeUtils","tinymce.core.dom.ScriptLoader","tinymce.core.dom.Selection","tinymce.core.dom.Serializer","tinymce.core.dom.Sizzle","tinymce.core.dom.TreeWalker","tinymce.core.Editor","tinymce.core.EditorCommands","tinymce.core.EditorManager","tinymce.core.EditorObservable","tinymce.core.Env","tinymce.core.FocusManager","tinymce.core.geom.Rect","tinymce.core.html.DomParser","tinymce.core.html.Entities","tinymce.core.html.Node","tinymce.core.html.SaxParser","tinymce.core.html.Schema","tinymce.core.html.Serializer","tinymce.core.html.Styles","tinymce.core.html.Writer","tinymce.core.Shortcuts","tinymce.core.ui.Factory","tinymce.core.UndoManager","tinymce.core.util.Class","tinymce.core.util.Color","tinymce.core.util.Delay","tinymce.core.util.EventDispatcher","tinymce.core.util.I18n","tinymce.core.util.JSON","tinymce.core.util.JSONP","tinymce.core.util.JSONRequest","tinymce.core.util.LocalStorage","tinymce.core.util.Observable","tinymce.core.util.Promise","tinymce.core.util.Tools","tinymce.core.util.URI","tinymce.core.util.VK","tinymce.core.util.XHR","ephox.katamari.api.Arr","global!document","ephox.sand.api.URL","global!matchMedia","global!navigator","global!clearInterval","global!clearTimeout","global!setInterval","global!setTimeout","tinymce.core.util.Arr","tinymce.core.dom.StyleSheetLoader","ephox.sugar.api.node.Element","ephox.katamari.api.Cell","tinymce.core.fmt.ApplyFormat","tinymce.core.fmt.FormatChanged","tinymce.core.fmt.FormatRegistry","tinymce.core.fmt.MatchFormat","tinymce.core.fmt.Preview","tinymce.core.fmt.RemoveFormat","tinymce.core.fmt.ToggleFormat","tinymce.core.keyboard.FormatShortcuts","ephox.katamari.api.Option","tinymce.core.EditorView","tinymce.core.ui.NotificationManagerImpl","tinymce.core.selection.SelectionBookmark","tinymce.core.ui.WindowManagerImpl","tinymce.core.caret.CaretBookmark","tinymce.core.caret.CaretContainer","tinymce.core.caret.CaretPosition","tinymce.core.dom.NodeType","tinymce.core.text.Zwsp","ephox.sugar.api.search.Selectors","tinymce.core.dom.RangePoint","ephox.sugar.api.dom.Compare","tinymce.core.EditorFocus","tinymce.core.dom.ScrollIntoView","tinymce.core.selection.EventProcessRanges","tinymce.core.selection.GetSelectionContent","tinymce.core.selection.MultiRange","tinymce.core.selection.SetSelectionContent","tinymce.core.InsertContent","tinymce.core.delete.DeleteCommands","tinymce.core.EditorSettings","tinymce.core.init.Render","tinymce.core.Mode","tinymce.core.ui.Sidebar","tinymce.core.util.Uuid","ephox.katamari.api.Type","tinymce.core.ErrorReporter","tinymce.core.LegacyInput","tinymce.core.undo.Levels","ephox.sand.api.XMLHttpRequest","global!Object","global!String","ephox.sand.util.Global","ephox.katamari.api.Future","ephox.katamari.api.Futures","ephox.katamari.api.Result","global!console","tinymce.core.util.Fun","tinymce.core.caret.CaretCandidate","tinymce.core.geom.ClientRect","tinymce.core.text.ExtendingChar","tinymce.core.dom.RangeNormalizer","tinymce.core.fmt.CaretFormat","tinymce.core.fmt.ExpandRange","tinymce.core.fmt.FormatUtils","tinymce.core.fmt.Hooks","tinymce.core.fmt.MergeFormats","tinymce.core.fmt.DefaultFormats","ephox.sand.api.Node","ephox.sand.api.PlatformDetection","ephox.sugar.api.node.NodeTypes","ephox.sugar.api.properties.Css","ephox.sugar.api.search.Traverse","ephox.sugar.api.node.Node","ephox.sugar.api.node.Text","ephox.sugar.api.selection.Selection","ephox.sugar.api.selection.WindowSelection","tinymce.core.caret.CaretFinder","ephox.sugar.api.dom.Focus","tinymce.core.dom.ElementType","tinymce.core.selection.FragmentReader","tinymce.core.caret.CaretWalker","tinymce.core.dom.ElementUtils","tinymce.core.dom.PaddingBr","tinymce.core.InsertList","tinymce.core.delete.BlockBoundaryDelete","tinymce.core.delete.BlockRangeDelete","tinymce.core.delete.CefDelete","tinymce.core.delete.DeleteUtils","tinymce.core.delete.InlineBoundaryDelete","tinymce.core.delete.TableDelete","ephox.katamari.api.Obj","ephox.katamari.api.Strings","ephox.katamari.api.Struct","tinymce.core.init.Init","tinymce.core.PluginManager","tinymce.core.ThemeManager","tinymce.core.undo.Fragments","ephox.katamari.api.Resolve","ephox.katamari.api.LazyValue","ephox.katamari.async.Bounce","ephox.katamari.async.AsyncValues","tinymce.core.caret.CaretUtils","ephox.sugar.api.dom.Insert","ephox.sugar.api.dom.Remove","ephox.sugar.impl.NodeValue","ephox.sugar.api.search.SelectorFilter","ephox.katamari.data.Immutable","ephox.katamari.data.MixedBag","ephox.sugar.alien.Recurse","ephox.katamari.api.Thunk","ephox.sand.core.PlatformDetection","ephox.sugar.api.properties.Attr","ephox.sugar.api.node.Body","ephox.sugar.impl.Style","ephox.katamari.str.StrAppend","ephox.katamari.str.StringParts","ephox.katamari.api.Adt","ephox.sugar.api.dom.DocumentPosition","ephox.sugar.api.node.Fragment","ephox.sugar.api.selection.Situ","ephox.sugar.selection.core.NativeRange","ephox.sugar.selection.core.SelectionDirection","ephox.sugar.selection.query.CaretRange","ephox.sugar.selection.query.Within","ephox.sugar.selection.quirks.Prefilter","ephox.sugar.api.search.PredicateExists","ephox.sugar.api.dom.Replication","ephox.sugar.api.search.SelectorFind","tinymce.core.dom.Parents","tinymce.core.selection.SelectionUtils","tinymce.core.selection.SimpleTableModel","tinymce.core.selection.TableCellSelection","tinymce.core.delete.BlockBoundary","tinymce.core.delete.MergeBlocks","ephox.katamari.api.Options","ephox.sugar.api.search.PredicateFind","tinymce.core.delete.CefDeleteAction","tinymce.core.delete.DeleteElement","tinymce.core.keyboard.BoundaryCaret","tinymce.core.keyboard.BoundaryLocation","tinymce.core.keyboard.BoundarySelection","tinymce.core.keyboard.InlineUtils","tinymce.core.delete.TableDeleteAction","tinymce.core.init.InitContentBody","tinymce.core.undo.Diff","ephox.katamari.api.Global","ephox.katamari.util.BagUtils","ephox.sand.core.Browser","ephox.sand.core.OperatingSystem","ephox.sand.detect.DeviceType","ephox.sand.detect.UaString","ephox.sand.info.PlatformInfo","ephox.sugar.api.dom.InsertAll","ephox.sugar.api.search.PredicateFilter","ephox.sugar.selection.query.ContainerPoint","ephox.sugar.selection.query.EdgePoint","global!Math","ephox.sugar.impl.ClosestOrAncestor","tinymce.core.dom.Empty","tinymce.core.caret.CaretContainerInline","tinymce.core.caret.CaretContainerRemove","tinymce.core.text.Bidi","tinymce.core.util.LazyEvaluator","tinymce.core.caret.CaretContainerInput","tinymce.core.EditorUpload","tinymce.core.ForceBlocks","tinymce.core.keyboard.KeyboardOverrides","tinymce.core.NodeChange","tinymce.core.SelectionOverrides","tinymce.core.util.Quirks","ephox.sand.detect.Version","ephox.sugar.selection.alien.Geometry","ephox.sugar.selection.query.TextPoint","ephox.sugar.api.selection.CursorPosition","ephox.sugar.api.search.SelectorExists","tinymce.core.file.Uploader","tinymce.core.file.ImageScanner","tinymce.core.file.BlobCache","tinymce.core.file.UploadStatus","tinymce.core.keyboard.ArrowKeys","tinymce.core.keyboard.DeleteBackspaceKeys","tinymce.core.keyboard.EnterKey","tinymce.core.keyboard.SpaceKey","tinymce.core.DragDropOverrides","tinymce.core.caret.FakeCaret","tinymce.core.caret.LineUtils","tinymce.core.keyboard.CefUtils","tinymce.core.dom.NodePath","global!Number","ephox.sugar.api.selection.Awareness","tinymce.core.file.Conversions","tinymce.core.keyboard.CefNavigation","tinymce.core.keyboard.MatchKeys","tinymce.core.keyboard.InsertNewLine","tinymce.core.keyboard.InsertSpace","tinymce.core.dom.MousePosition","tinymce.core.dom.Dimensions","ephox.sand.api.Blob","ephox.sand.api.FileReader","ephox.sand.api.Uint8Array","ephox.sand.api.Window","tinymce.core.caret.LineWalker","ephox.katamari.api.Merger"] jsc*/ defineGlobal("global!Array", Array); defineGlobal("global!Error", Error); @@ -177,411 +177,628 @@ define( } ); -/** - * Promise.js - * - * Released under LGPL License. - * Copyright (c) 1999-2017 Ephox Corp. All rights reserved - * - * Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill - * - * License: http://www.tinymce.com/license - * Contributing: http://www.tinymce.com/contributing - */ - -/* eslint-disable */ -/* jshint ignore:start */ - -/** - * Modifed to be a feature fill and wrapped as tinymce module. - */ +defineGlobal("global!window", window); +defineGlobal("global!Object", Object); define( - 'tinymce.core.util.Promise', - [], - function () { - if (window.Promise) { - return window.Promise; - } - - // Use polyfill for setImmediate for performance gains - var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) || - function (fn) { setTimeout(fn, 1); }; - - // Polyfill for Function.prototype.bind - function bind(fn, thisArg) { - return function () { - fn.apply(thisArg, arguments); - }; - } + 'ephox.katamari.api.Option', - var isArray = Array.isArray || function (value) { return Object.prototype.toString.call(value) === "[object Array]"; }; + [ + 'ephox.katamari.api.Fun', + 'global!Object' + ], - function Promise(fn) { - if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new'); - if (typeof fn !== 'function') throw new TypeError('not a function'); - this._state = null; - this._value = null; - this._deferreds = []; + function (Fun, Object) { - doResolve(fn, bind(resolve, this), bind(reject, this)); - } + var never = Fun.never; + var always = Fun.always; - function handle(deferred) { - var me = this; - if (this._state === null) { - this._deferreds.push(deferred); - return; - } - asap(function () { - var cb = me._state ? deferred.onFulfilled : deferred.onRejected; - if (cb === null) { - (me._state ? deferred.resolve : deferred.reject)(me._value); - return; - } - var ret; - try { - ret = cb(me._value); - } - catch (e) { - deferred.reject(e); - return; - } - deferred.resolve(ret); - }); - } + /** + Option objects support the following methods: - function resolve(newValue) { - try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure - if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.'); - if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { - var then = newValue.then; - if (typeof then === 'function') { - doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this)); - return; - } - } - this._state = true; - this._value = newValue; - finale.call(this); - } catch (e) { reject.call(this, e); } - } + fold :: this Option a -> ((() -> b, a -> b)) -> Option b - function reject(newValue) { - this._state = false; - this._value = newValue; - finale.call(this); - } + is :: this Option a -> a -> Boolean - function finale() { - for (var i = 0, len = this._deferreds.length; i < len; i++) { - handle.call(this, this._deferreds[i]); - } - this._deferreds = null; - } + isSome :: this Option a -> () -> Boolean - function Handler(onFulfilled, onRejected, resolve, reject) { - this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; - this.onRejected = typeof onRejected === 'function' ? onRejected : null; - this.resolve = resolve; - this.reject = reject; - } + isNone :: this Option a -> () -> Boolean - /** - * Take a potentially misbehaving resolver function and make sure - * onFulfilled and onRejected are only called once. - * - * Makes no guarantees about asynchrony. - */ - function doResolve(fn, onFulfilled, onRejected) { - var done = false; - try { - fn(function (value) { - if (done) return; - done = true; - onFulfilled(value); - }, function (reason) { - if (done) return; - done = true; - onRejected(reason); - }); - } catch (ex) { - if (done) return; - done = true; - onRejected(ex); - } - } + getOr :: this Option a -> a -> a - Promise.prototype['catch'] = function (onRejected) { - return this.then(null, onRejected); - }; + getOrThunk :: this Option a -> (() -> a) -> a - Promise.prototype.then = function (onFulfilled, onRejected) { - var me = this; - return new Promise(function (resolve, reject) { - handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject)); - }); - }; + getOrDie :: this Option a -> String -> a - Promise.all = function () { - var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments); + or :: this Option a -> Option a -> Option a + - if some: return self + - if none: return opt - return new Promise(function (resolve, reject) { - if (args.length === 0) return resolve([]); - var remaining = args.length; - function res(i, val) { - try { - if (val && (typeof val === 'object' || typeof val === 'function')) { - var then = val.then; - if (typeof then === 'function') { - then.call(val, function (val) { res(i, val); }, reject); - return; - } - } - args[i] = val; - if (--remaining === 0) { - resolve(args); - } - } catch (ex) { - reject(ex); - } - } - for (var i = 0; i < args.length; i++) { - res(i, args[i]); - } - }); - }; + orThunk :: this Option a -> (() -> Option a) -> Option a + - Same as "or", but uses a thunk instead of a value - Promise.resolve = function (value) { - if (value && typeof value === 'object' && value.constructor === Promise) { - return value; - } + map :: this Option a -> (a -> b) -> Option b + - "fmap" operation on the Option Functor. + - same as 'each' - return new Promise(function (resolve) { - resolve(value); - }); - }; + ap :: this Option a -> Option (a -> b) -> Option b + - "apply" operation on the Option Apply/Applicative. + - Equivalent to <*> in Haskell/PureScript. - Promise.reject = function (value) { - return new Promise(function (resolve, reject) { - reject(value); - }); - }; + each :: this Option a -> (a -> b) -> Option b + - same as 'map' - Promise.race = function (values) { - return new Promise(function (resolve, reject) { - for (var i = 0, len = values.length; i < len; i++) { - values[i].then(resolve, reject); - } - }); - }; + bind :: this Option a -> (a -> Option b) -> Option b + - "bind"/"flatMap" operation on the Option Bind/Monad. + - Equivalent to >>= in Haskell/PureScript; flatMap in Scala. - return Promise; - } -); + flatten :: {this Option (Option a))} -> () -> Option a + - "flatten"/"join" operation on the Option Monad. -/* jshint ignore:end */ -/* eslint-enable */ -/** - * Delay.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 - */ + exists :: this Option a -> (a -> Boolean) -> Boolean -/** - * Utility class for working with delayed actions like setTimeout. - * - * @class tinymce.util.Delay - */ -define( - 'tinymce.core.util.Delay', - [ - "tinymce.core.util.Promise" - ], - function (Promise) { - var requestAnimationFramePromise; + forall :: this Option a -> (a -> Boolean) -> Boolean - function requestAnimationFrame(callback, element) { - var i, requestAnimationFrameFunc = window.requestAnimationFrame, vendors = ['ms', 'moz', 'webkit']; + filter :: this Option a -> (a -> Boolean) -> Option a - function featurefill(callback) { - window.setTimeout(callback, 0); - } + equals :: this Option a -> Option a -> Boolean - for (i = 0; i < vendors.length && !requestAnimationFrameFunc; i++) { - requestAnimationFrameFunc = window[vendors[i] + 'RequestAnimationFrame']; - } + equals_ :: this Option a -> (Option a, a -> Boolean) -> Boolean - if (!requestAnimationFrameFunc) { - requestAnimationFrameFunc = featurefill; - } + toArray :: this Option a -> () -> [a] - requestAnimationFrameFunc(callback, element); - } + */ - function wrappedSetTimeout(callback, time) { - if (typeof time != 'number') { - time = 0; - } + var none = function () { return NONE; }; - return setTimeout(callback, time); - } + var NONE = (function () { + var eq = function (o) { + return o.isNone(); + }; - function wrappedSetInterval(callback, time) { - if (typeof time != 'number') { - time = 1; // IE 8 needs it to be > 0 - } + // inlined from peanut, maybe a micro-optimisation? + var call = function (thunk) { return thunk(); }; + var id = function (n) { return n; }; + var noop = function () { }; - return setInterval(callback, time); - } + var me = { + fold: function (n, s) { return n(); }, + is: never, + isSome: never, + isNone: always, + getOr: id, + getOrThunk: call, + getOrDie: function (msg) { + throw new Error(msg || 'error: getOrDie called on none.'); + }, + or: id, + orThunk: call, + map: none, + ap: none, + each: noop, + bind: none, + flatten: none, + exists: never, + forall: always, + filter: none, + equals: eq, + equals_: eq, + toArray: function () { return []; }, + toString: Fun.constant("none()") + }; + if (Object.freeze) Object.freeze(me); + return me; + })(); - function wrappedClearTimeout(id) { - return clearTimeout(id); - } - function wrappedClearInterval(id) { - return clearInterval(id); - } + /** some :: a -> Option a */ + var some = function (a) { - function debounce(callback, time) { - var timer, func; + // inlined from peanut, maybe a micro-optimisation? + var constant_a = function () { return a; }; - func = function () { - var args = arguments; + var self = function () { + // can't Fun.constant this one + return me; + }; - clearTimeout(timer); + var map = function (f) { + return some(f(a)); + }; - timer = wrappedSetTimeout(function () { - callback.apply(this, args); - }, time); + var bind = function (f) { + return f(a); }; - func.stop = function () { - clearTimeout(timer); + var me = { + fold: function (n, s) { return s(a); }, + is: function (v) { return a === v; }, + isSome: always, + isNone: never, + getOr: constant_a, + getOrThunk: constant_a, + getOrDie: constant_a, + or: self, + orThunk: self, + map: map, + ap: function (optfab) { + return optfab.fold(none, function(fab) { + return some(fab(a)); + }); + }, + each: function (f) { + f(a); + }, + bind: bind, + flatten: constant_a, + exists: bind, + forall: bind, + filter: function (f) { + return f(a) ? me : NONE; + }, + equals: function (o) { + return o.is(a); + }, + equals_: function (o, elementEq) { + return o.fold( + never, + function (b) { return elementEq(a, b); } + ); + }, + toArray: function () { + return [a]; + }, + toString: function () { + return 'some(' + a + ')'; + } }; + return me; + }; - return func; - } + /** from :: undefined|null|a -> Option a */ + var from = function (value) { + return value === null || value === undefined ? NONE : some(value); + }; return { - /** - * Requests an animation frame and fallbacks to a timeout on older browsers. - * - * @method requestAnimationFrame - * @param {function} callback Callback to execute when a new frame is available. - * @param {DOMElement} element Optional element to scope it to. - */ - requestAnimationFrame: function (callback, element) { - if (requestAnimationFramePromise) { - requestAnimationFramePromise.then(callback); - return; - } - - requestAnimationFramePromise = new Promise(function (resolve) { - if (!element) { - element = document.body; - } + some: some, + none: none, + from: from + }; + } +); - requestAnimationFrame(resolve, element); - }).then(callback); - }, +defineGlobal("global!String", String); +define( + 'ephox.katamari.api.Arr', - /** - * Sets a timer in ms and executes the specified callback when the timer runs out. - * - * @method setTimeout - * @param {function} callback Callback to execute when timer runs out. - * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. - * @return {Number} Timeout id number. - */ - setTimeout: wrappedSetTimeout, + [ + 'ephox.katamari.api.Option', + 'global!Array', + 'global!Error', + 'global!String' + ], - /** - * Sets an interval timer in ms and executes the specified callback at every interval of that time. - * - * @method setInterval - * @param {function} callback Callback to execute when interval time runs out. - * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. - * @return {Number} Timeout id number. - */ - setInterval: wrappedSetInterval, + function (Option, Array, Error, String) { + // Use the native Array.indexOf if it is available (IE9+) otherwise fall back to manual iteration + // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf + var rawIndexOf = (function () { + var pIndexOf = Array.prototype.indexOf; - /** - * Sets an editor timeout it's similar to setTimeout except that it checks if the editor instance is - * still alive when the callback gets executed. - * - * @method setEditorTimeout - * @param {tinymce.Editor} editor Editor instance to check the removed state on. - * @param {function} callback Callback to execute when timer runs out. - * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. - * @return {Number} Timeout id number. - */ - setEditorTimeout: function (editor, callback, time) { - return wrappedSetTimeout(function () { - if (!editor.removed) { - callback(); - } - }, time); - }, + var fastIndex = function (xs, x) { return pIndexOf.call(xs, x); }; - /** - * Sets an interval timer it's similar to setInterval except that it checks if the editor instance is - * still alive when the callback gets executed. - * - * @method setEditorInterval - * @param {function} callback Callback to execute when interval time runs out. - * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. - * @return {Number} Timeout id number. - */ - setEditorInterval: function (editor, callback, time) { - var timer; + var slowIndex = function(xs, x) { return slowIndexOf(xs, x); }; - timer = wrappedSetInterval(function () { - if (!editor.removed) { - callback(); - } else { - clearInterval(timer); - } - }, time); + return pIndexOf === undefined ? slowIndex : fastIndex; + })(); - return timer; - }, + var indexOf = function (xs, x) { + // The rawIndexOf method does not wrap up in an option. This is for performance reasons. + var r = rawIndexOf(xs, x); + return r === -1 ? Option.none() : Option.some(r); + }; - /** - * Creates debounced callback function that only gets executed once within the specified time. - * - * @method debounce - * @param {function} callback Callback to execute when timer finishes. - * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. - * @return {Function} debounced function callback. - */ - debounce: debounce, + var contains = function (xs, x) { + return rawIndexOf(xs, x) > -1; + }; - // Throttle needs to be debounce due to backwards compatibility. - throttle: debounce, + // Using findIndex is likely less optimal in Chrome (dynamic return type instead of bool) + // but if we need that micro-optimisation we can inline it later. + var exists = function (xs, pred) { + return findIndex(xs, pred).isSome(); + }; - /** - * Clears an interval timer so it won't execute. - * - * @method clearInterval - * @param {Number} Interval timer id number. - */ - clearInterval: wrappedClearInterval, + var range = function (num, f) { + var r = []; + for (var i = 0; i < num; i++) { + r.push(f(i)); + } + return r; + }; - /** - * Clears an timeout timer so it won't execute. - * - * @method clearTimeout - * @param {Number} Timeout timer id number. - */ - clearTimeout: wrappedClearTimeout + // It's a total micro optimisation, but these do make some difference. + // Particularly for browsers other than Chrome. + // - length caching + // http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69 + // - not using push + // http://jsperf.com/array-direct-assignment-vs-push/2 + + var chunk = function (array, size) { + var r = []; + for (var i = 0; i < array.length; i += size) { + var s = array.slice(i, i + size); + r.push(s); + } + return r; + }; + + var map = function(xs, f) { + // pre-allocating array size when it's guaranteed to be known + // http://jsperf.com/push-allocated-vs-dynamic/22 + var len = xs.length; + var r = new Array(len); + for (var i = 0; i < len; i++) { + var x = xs[i]; + r[i] = f(x, i, xs); + } + return r; + }; + + // Unwound implementing other functions in terms of each. + // The code size is roughly the same, and it should allow for better optimisation. + var each = function(xs, f) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + f(x, i, xs); + } + }; + + var eachr = function (xs, f) { + for (var i = xs.length - 1; i >= 0; i--) { + var x = xs[i]; + f(x, i, xs); + } + }; + + var partition = function(xs, pred) { + var pass = []; + var fail = []; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + var arr = pred(x, i, xs) ? pass : fail; + arr.push(x); + } + return { pass: pass, fail: fail }; + }; + + var filter = function(xs, pred) { + var r = []; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + r.push(x); + } + } + return r; + }; + + /* + * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f. + * + * f is a function that derives a value from an element - e.g. true or false, or a string. + * Elements are like if this function generates the same value for them (according to ===). + * + * + * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function. + * For a good explanation, see the group function (which is a special case of groupBy) + * http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group + */ + var groupBy = function (xs, f) { + if (xs.length === 0) { + return []; + } else { + var wasType = f(xs[0]); // initial case for matching + var r = []; + var group = []; + + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + var type = f(x); + if (type !== wasType) { + r.push(group); + group = []; + } + wasType = type; + group.push(x); + } + if (group.length !== 0) { + r.push(group); + } + return r; + } + }; + + var foldr = function (xs, f, acc) { + eachr(xs, function (x) { + acc = f(acc, x); + }); + return acc; + }; + + var foldl = function (xs, f, acc) { + each(xs, function (x) { + acc = f(acc, x); + }); + return acc; + }; + + var find = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + return Option.some(x); + } + } + return Option.none(); + }; + + var findIndex = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + if (pred(x, i, xs)) { + return Option.some(i); + } + } + + return Option.none(); + }; + + var slowIndexOf = function (xs, x) { + for (var i = 0, len = xs.length; i < len; ++i) { + if (xs[i] === x) { + return i; + } + } + + return -1; + }; + + var push = Array.prototype.push; + var flatten = function (xs) { + // Note, this is possible because push supports multiple arguments: + // http://jsperf.com/concat-push/6 + // Note that in the past, concat() would silently work (very slowly) for array-like objects. + // With this change it will throw an error. + var r = []; + for (var i = 0, len = xs.length; i < len; ++i) { + // Ensure that each value is an array itself + if (! Array.prototype.isPrototypeOf(xs[i])) throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); + push.apply(r, xs[i]); + } + return r; + }; + + var bind = function (xs, f) { + var output = map(xs, f); + return flatten(output); + }; + + var forall = function (xs, pred) { + for (var i = 0, len = xs.length; i < len; ++i) { + var x = xs[i]; + if (pred(x, i, xs) !== true) { + return false; + } + } + return true; + }; + + var equal = function (a1, a2) { + return a1.length === a2.length && forall(a1, function (x, i) { + return x === a2[i]; + }); + }; + + var slice = Array.prototype.slice; + var reverse = function (xs) { + var r = slice.call(xs, 0); + r.reverse(); + return r; + }; + + var difference = function (a1, a2) { + return filter(a1, function (x) { + return !contains(a2, x); + }); + }; + + var mapToObject = function(xs, f) { + var r = {}; + for (var i = 0, len = xs.length; i < len; i++) { + var x = xs[i]; + r[String(x)] = f(x, i); + } + return r; + }; + + var pure = function(x) { + return [x]; + }; + + var sort = function (xs, comparator) { + var copy = slice.call(xs, 0); + copy.sort(comparator); + return copy; + }; + + var head = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[0]); + }; + + var last = function (xs) { + return xs.length === 0 ? Option.none() : Option.some(xs[xs.length - 1]); + }; + + return { + map: map, + each: each, + eachr: eachr, + partition: partition, + filter: filter, + groupBy: groupBy, + indexOf: indexOf, + foldr: foldr, + foldl: foldl, + find: find, + findIndex: findIndex, + flatten: flatten, + bind: bind, + forall: forall, + exists: exists, + contains: contains, + equal: equal, + reverse: reverse, + chunk: chunk, + difference: difference, + mapToObject: mapToObject, + pure: pure, + sort: sort, + range: range, + head: head, + last: last + }; + } +); +defineGlobal("global!document", document); +define( + 'ephox.katamari.api.Global', + + [ + ], + + function () { + // Use window object as the global if it's available since CSP will block script evals + if (typeof window !== 'undefined') { + return window; + } else { + return Function('return this;')(); + } + } +); + + +define( + 'ephox.katamari.api.Resolve', + + [ + 'ephox.katamari.api.Global' + ], + + function (Global) { + /** path :: ([String], JsObj?) -> JsObj */ + var path = function (parts, scope) { + var o = scope !== undefined ? scope : Global; + for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) + o = o[parts[i]]; + return o; + }; + + /** resolve :: (String, JsObj?) -> JsObj */ + var resolve = function (p, scope) { + var parts = p.split('.'); + return path(parts, scope); + }; + + /** step :: (JsObj, String) -> JsObj */ + var step = function (o, part) { + if (o[part] === undefined || o[part] === null) + o[part] = {}; + return o[part]; + }; + + /** forge :: ([String], JsObj?) -> JsObj */ + var forge = function (parts, target) { + var o = target !== undefined ? target : Global; + for (var i = 0; i < parts.length; ++i) + o = step(o, parts[i]); + return o; + }; + + /** namespace :: (String, JsObj?) -> JsObj */ + var namespace = function (name, target) { + var parts = name.split('.'); + return forge(parts, target); + }; + + return { + path: path, + resolve: resolve, + forge: forge, + namespace: namespace + }; + } +); + + +define( + 'ephox.sand.util.Global', + + [ + 'ephox.katamari.api.Resolve' + ], + + function (Resolve) { + var unsafe = function (name, scope) { + return Resolve.resolve(name, scope); + }; + + var getOrDie = function (name, scope) { + var actual = unsafe(name, scope); + + if (actual === undefined) throw name + ' not available on this browser'; + return actual; + }; + + return { + getOrDie: getOrDie }; } ); +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 + }; + } +); +defineGlobal("global!matchMedia", matchMedia); +defineGlobal("global!navigator", navigator); /** * Env.js * @@ -603,8 +820,13 @@ define( define( 'tinymce.core.Env', [ + 'ephox.sand.api.URL', + 'global!document', + 'global!matchMedia', + 'global!navigator', + 'global!window' ], - function () { + function (URL, document, matchMedia, navigator, window) { var nav = navigator, userAgent = nav.userAgent; var opera, webkit, ie, ie11, ie12, gecko, mac, iDevice, android, fileApi, phone, tablet, windowsPhone; @@ -770,95 +992,512 @@ define( } ); +defineGlobal("global!clearInterval", clearInterval); +defineGlobal("global!clearTimeout", clearTimeout); +defineGlobal("global!setInterval", setInterval); +defineGlobal("global!setTimeout", setTimeout); /** - * EventUtils.js + * Promise.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * + * Promise polyfill under MIT license: https://github.com/taylorhakes/promise-polyfill + * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ -/*jshint loopfunc:true*/ -/*eslint no-loop-func:0 */ +/* eslint-disable */ +/* jshint ignore:start */ /** - * This class wraps the browsers native event logic with more convenient methods. - * - * @class tinymce.dom.EventUtils + * Modifed to be a feature fill and wrapped as tinymce module. */ define( - 'tinymce.core.dom.EventUtils', - [ - "tinymce.core.util.Delay", - "tinymce.core.Env" - ], - function (Delay, Env) { - "use strict"; + 'tinymce.core.util.Promise', + [], + function () { + if (window.Promise) { + return window.Promise; + } - var eventExpandoPrefix = "mce-data-"; - var mouseEventRe = /^(?:mouse|contextmenu)|click/; - var deprecated = { - keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1, - webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1 - }; + // Use polyfill for setImmediate for performance gains + var asap = Promise.immediateFn || (typeof setImmediate === 'function' && setImmediate) || + function (fn) { setTimeout(fn, 1); }; - // Checks if it is our own isDefaultPrevented function - var hasIsDefaultPrevented = function (event) { - return event.isDefaultPrevented === returnTrue || event.isDefaultPrevented === returnFalse; - }; + // Polyfill for Function.prototype.bind + function bind(fn, thisArg) { + return function () { + fn.apply(thisArg, arguments); + }; + } - // Dummy function that gets replaced on the delegation state functions - var returnFalse = function () { - return false; - }; + var isArray = Array.isArray || function (value) { return Object.prototype.toString.call(value) === "[object Array]"; }; - // Dummy function that gets replaced on the delegation state functions - var returnTrue = function () { - return true; - }; + function Promise(fn) { + if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new'); + if (typeof fn !== 'function') throw new TypeError('not a function'); + this._state = null; + this._value = null; + this._deferreds = []; - /** - * Binds a native event to a callback on the speified target. - */ - function addEvent(target, name, callback, capture) { - if (target.addEventListener) { - target.addEventListener(name, callback, capture || false); - } else if (target.attachEvent) { - target.attachEvent('on' + name, callback); - } + doResolve(fn, bind(resolve, this), bind(reject, this)); } - /** - * Unbinds a native event callback on the specified target. - */ - function removeEvent(target, name, callback, capture) { - if (target.removeEventListener) { - target.removeEventListener(name, callback, capture || false); - } else if (target.detachEvent) { - target.detachEvent('on' + name, callback); + function handle(deferred) { + var me = this; + if (this._state === null) { + this._deferreds.push(deferred); + return; } + asap(function () { + var cb = me._state ? deferred.onFulfilled : deferred.onRejected; + if (cb === null) { + (me._state ? deferred.resolve : deferred.reject)(me._value); + return; + } + var ret; + try { + ret = cb(me._value); + } + catch (e) { + deferred.reject(e); + return; + } + deferred.resolve(ret); + }); } - /** - * Gets the event target based on shadow dom properties like path and deepPath. - */ - function getTargetFromShadowDom(event, defaultTarget) { - var path, target = defaultTarget; - - // When target element is inside Shadow DOM we need to take first element from path - // otherwise we'll get Shadow Root parent, not actual target element - - // Normalize target for WebComponents v0 implementation (in Chrome) - path = event.path; - if (path && path.length > 0) { - target = path[0]; - } - - // Normalize target for WebComponents v1 implementation (standard) - if (event.deepPath) { - path = event.deepPath(); + function resolve(newValue) { + try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure + if (newValue === this) throw new TypeError('A promise cannot be resolved with itself.'); + if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { + var then = newValue.then; + if (typeof then === 'function') { + doResolve(bind(then, newValue), bind(resolve, this), bind(reject, this)); + return; + } + } + this._state = true; + this._value = newValue; + finale.call(this); + } catch (e) { reject.call(this, e); } + } + + function reject(newValue) { + this._state = false; + this._value = newValue; + finale.call(this); + } + + function finale() { + for (var i = 0, len = this._deferreds.length; i < len; i++) { + handle.call(this, this._deferreds[i]); + } + this._deferreds = null; + } + + function Handler(onFulfilled, onRejected, resolve, reject) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.resolve = resolve; + this.reject = reject; + } + + /** + * Take a potentially misbehaving resolver function and make sure + * onFulfilled and onRejected are only called once. + * + * Makes no guarantees about asynchrony. + */ + function doResolve(fn, onFulfilled, onRejected) { + var done = false; + try { + fn(function (value) { + if (done) return; + done = true; + onFulfilled(value); + }, function (reason) { + if (done) return; + done = true; + onRejected(reason); + }); + } catch (ex) { + if (done) return; + done = true; + onRejected(ex); + } + } + + Promise.prototype['catch'] = function (onRejected) { + return this.then(null, onRejected); + }; + + Promise.prototype.then = function (onFulfilled, onRejected) { + var me = this; + return new Promise(function (resolve, reject) { + handle.call(me, new Handler(onFulfilled, onRejected, resolve, reject)); + }); + }; + + Promise.all = function () { + var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments); + + return new Promise(function (resolve, reject) { + if (args.length === 0) return resolve([]); + var remaining = args.length; + function res(i, val) { + try { + if (val && (typeof val === 'object' || typeof val === 'function')) { + var then = val.then; + if (typeof then === 'function') { + then.call(val, function (val) { res(i, val); }, reject); + return; + } + } + args[i] = val; + if (--remaining === 0) { + resolve(args); + } + } catch (ex) { + reject(ex); + } + } + for (var i = 0; i < args.length; i++) { + res(i, args[i]); + } + }); + }; + + Promise.resolve = function (value) { + if (value && typeof value === 'object' && value.constructor === Promise) { + return value; + } + + return new Promise(function (resolve) { + resolve(value); + }); + }; + + Promise.reject = function (value) { + return new Promise(function (resolve, reject) { + reject(value); + }); + }; + + Promise.race = function (values) { + return new Promise(function (resolve, reject) { + for (var i = 0, len = values.length; i < len; i++) { + values[i].then(resolve, reject); + } + }); + }; + + return Promise; + } +); + +/* jshint ignore:end */ +/* eslint-enable */ +/** + * Delay.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 class for working with delayed actions like setTimeout. + * + * @class tinymce.util.Delay + */ +define( + 'tinymce.core.util.Delay', + [ + 'global!clearInterval', + 'global!clearTimeout', + 'global!document', + 'global!setInterval', + 'global!setTimeout', + 'global!window', + 'tinymce.core.util.Promise' + ], + function (clearInterval, clearTimeout, document, setInterval, setTimeout, window, Promise) { + var requestAnimationFramePromise; + + function requestAnimationFrame(callback, element) { + var i, requestAnimationFrameFunc = window.requestAnimationFrame, vendors = ['ms', 'moz', 'webkit']; + + function featurefill(callback) { + window.setTimeout(callback, 0); + } + + for (i = 0; i < vendors.length && !requestAnimationFrameFunc; i++) { + requestAnimationFrameFunc = window[vendors[i] + 'RequestAnimationFrame']; + } + + if (!requestAnimationFrameFunc) { + requestAnimationFrameFunc = featurefill; + } + + requestAnimationFrameFunc(callback, element); + } + + function wrappedSetTimeout(callback, time) { + if (typeof time != 'number') { + time = 0; + } + + return setTimeout(callback, time); + } + + function wrappedSetInterval(callback, time) { + if (typeof time != 'number') { + time = 1; // IE 8 needs it to be > 0 + } + + return setInterval(callback, time); + } + + function wrappedClearTimeout(id) { + return clearTimeout(id); + } + + function wrappedClearInterval(id) { + return clearInterval(id); + } + + function debounce(callback, time) { + var timer, func; + + func = function () { + var args = arguments; + + clearTimeout(timer); + + timer = wrappedSetTimeout(function () { + callback.apply(this, args); + }, time); + }; + + func.stop = function () { + clearTimeout(timer); + }; + + return func; + } + + return { + /** + * Requests an animation frame and fallbacks to a timeout on older browsers. + * + * @method requestAnimationFrame + * @param {function} callback Callback to execute when a new frame is available. + * @param {DOMElement} element Optional element to scope it to. + */ + requestAnimationFrame: function (callback, element) { + if (requestAnimationFramePromise) { + requestAnimationFramePromise.then(callback); + return; + } + + requestAnimationFramePromise = new Promise(function (resolve) { + if (!element) { + element = document.body; + } + + requestAnimationFrame(resolve, element); + }).then(callback); + }, + + /** + * Sets a timer in ms and executes the specified callback when the timer runs out. + * + * @method setTimeout + * @param {function} callback Callback to execute when timer runs out. + * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. + * @return {Number} Timeout id number. + */ + setTimeout: wrappedSetTimeout, + + /** + * Sets an interval timer in ms and executes the specified callback at every interval of that time. + * + * @method setInterval + * @param {function} callback Callback to execute when interval time runs out. + * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. + * @return {Number} Timeout id number. + */ + setInterval: wrappedSetInterval, + + /** + * Sets an editor timeout it's similar to setTimeout except that it checks if the editor instance is + * still alive when the callback gets executed. + * + * @method setEditorTimeout + * @param {tinymce.Editor} editor Editor instance to check the removed state on. + * @param {function} callback Callback to execute when timer runs out. + * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. + * @return {Number} Timeout id number. + */ + setEditorTimeout: function (editor, callback, time) { + return wrappedSetTimeout(function () { + if (!editor.removed) { + callback(); + } + }, time); + }, + + /** + * Sets an interval timer it's similar to setInterval except that it checks if the editor instance is + * still alive when the callback gets executed. + * + * @method setEditorInterval + * @param {function} callback Callback to execute when interval time runs out. + * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. + * @return {Number} Timeout id number. + */ + setEditorInterval: function (editor, callback, time) { + var timer; + + timer = wrappedSetInterval(function () { + if (!editor.removed) { + callback(); + } else { + clearInterval(timer); + } + }, time); + + return timer; + }, + + /** + * Creates debounced callback function that only gets executed once within the specified time. + * + * @method debounce + * @param {function} callback Callback to execute when timer finishes. + * @param {Number} time Optional time to wait before the callback is executed, defaults to 0. + * @return {Function} debounced function callback. + */ + debounce: debounce, + + // Throttle needs to be debounce due to backwards compatibility. + throttle: debounce, + + /** + * Clears an interval timer so it won't execute. + * + * @method clearInterval + * @param {Number} Interval timer id number. + */ + clearInterval: wrappedClearInterval, + + /** + * Clears an timeout timer so it won't execute. + * + * @method clearTimeout + * @param {Number} Timeout timer id number. + */ + clearTimeout: wrappedClearTimeout + }; + } +); + +/** + * EventUtils.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 + */ + +/*jshint loopfunc:true*/ +/*eslint no-loop-func:0 */ + +/** + * This class wraps the browsers native event logic with more convenient methods. + * + * @class tinymce.dom.EventUtils + */ +define( + 'tinymce.core.dom.EventUtils', + [ + 'global!document', + 'global!window', + 'tinymce.core.Env', + 'tinymce.core.util.Delay' + ], + function (document, window, Env, Delay) { + "use strict"; + + var eventExpandoPrefix = "mce-data-"; + var mouseEventRe = /^(?:mouse|contextmenu)|click/; + var deprecated = { + keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1, + webkitMovementX: 1, webkitMovementY: 1, keyIdentifier: 1 + }; + + // Checks if it is our own isDefaultPrevented function + var hasIsDefaultPrevented = function (event) { + return event.isDefaultPrevented === returnTrue || event.isDefaultPrevented === returnFalse; + }; + + // Dummy function that gets replaced on the delegation state functions + var returnFalse = function () { + return false; + }; + + // Dummy function that gets replaced on the delegation state functions + var returnTrue = function () { + return true; + }; + + /** + * Binds a native event to a callback on the speified target. + */ + function addEvent(target, name, callback, capture) { + if (target.addEventListener) { + target.addEventListener(name, callback, capture || false); + } else if (target.attachEvent) { + target.attachEvent('on' + name, callback); + } + } + + /** + * Unbinds a native event callback on the specified target. + */ + function removeEvent(target, name, callback, capture) { + if (target.removeEventListener) { + target.removeEventListener(name, callback, capture || false); + } else if (target.detachEvent) { + target.detachEvent('on' + name, callback); + } + } + + /** + * Gets the event target based on shadow dom properties like path and deepPath. + */ + function getTargetFromShadowDom(event, defaultTarget) { + var path, target = defaultTarget; + + // When target element is inside Shadow DOM we need to take first element from path + // otherwise we'll get Shadow Root parent, not actual target element + + // Normalize target for WebComponents v0 implementation (in Chrome) + path = event.path; + if (path && path.length > 0) { + target = path[0]; + } + + // Normalize target for WebComponents v1 implementation (standard) + if (event.deepPath) { + path = event.deepPath(); if (path && path.length > 0) { target = path[0]; } @@ -3619,10 +4258,11 @@ define( define( 'tinymce.core.util.Tools', [ - "tinymce.core.Env", - "tinymce.core.util.Arr" + 'global!window', + 'tinymce.core.Env', + 'tinymce.core.util.Arr' ], - function (Env, Arr) { + function (window, Env, Arr) { /** * Removes whitespace from the beginning and end of a string. * @@ -4095,12 +4735,13 @@ define( define( 'tinymce.core.dom.DomQuery', [ - "tinymce.core.dom.EventUtils", - "tinymce.core.dom.Sizzle", - "tinymce.core.util.Tools", - "tinymce.core.Env" + 'global!document', + 'tinymce.core.dom.EventUtils', + 'tinymce.core.dom.Sizzle', + 'tinymce.core.Env', + 'tinymce.core.util.Tools' ], - function (EventUtils, Sizzle, Tools, Env) { + function (document, EventUtils, Sizzle, Env, Tools) { var doc = document, push = Array.prototype.push, slice = Array.prototype.slice; var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/; var Event = EventUtils.Event, undef; @@ -5644,2329 +6285,2141 @@ define( } ); -/** - * Range.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 - */ - -/** - * Old IE Range. - * - * @private - * @class tinymce.dom.Range - */ define( - 'tinymce.core.dom.Range', + 'ephox.katamari.api.LazyValue', + [ - "tinymce.core.util.Tools" + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'global!setTimeout' ], - function (Tools) { - // Range constructor - function Range(dom) { - var self = this, - doc = dom.doc, - EXTRACT = 0, - CLONE = 1, - DELETE = 2, - TRUE = true, - FALSE = false, - START_OFFSET = 'startOffset', - START_CONTAINER = 'startContainer', - END_CONTAINER = 'endContainer', - END_OFFSET = 'endOffset', - extend = Tools.extend, - nodeIndex = dom.nodeIndex; - function createDocumentFragment() { - return doc.createDocumentFragment(); - } + function (Arr, Option, setTimeout) { + var nu = function (baseFn) { + var data = Option.none(); + var callbacks = []; - function setStart(n, o) { - _setEndPoint(TRUE, n, o); - } + /** map :: this LazyValue a -> (a -> b) -> LazyValue b */ + var map = function (f) { + return nu(function (nCallback) { + get(function (data) { + nCallback(f(data)); + }); + }); + }; - function setEnd(n, o) { - _setEndPoint(FALSE, n, o); - } + var get = function (nCallback) { + if (isReady()) call(nCallback); + else callbacks.push(nCallback); + }; - function setStartBefore(n) { - setStart(n.parentNode, nodeIndex(n)); - } + var set = function (x) { + data = Option.some(x); + run(callbacks); + callbacks = []; + }; - function setStartAfter(n) { - setStart(n.parentNode, nodeIndex(n) + 1); - } + var isReady = function () { + return data.isSome(); + }; - function setEndBefore(n) { - setEnd(n.parentNode, nodeIndex(n)); - } + var run = function (cbs) { + Arr.each(cbs, call); + }; - function setEndAfter(n) { - setEnd(n.parentNode, nodeIndex(n) + 1); - } + var call = function(cb) { + data.each(function(x) { + setTimeout(function() { + cb(x); + }, 0); + }); + }; - function collapse(ts) { - if (ts) { - self[END_CONTAINER] = self[START_CONTAINER]; - self[END_OFFSET] = self[START_OFFSET]; - } else { - self[START_CONTAINER] = self[END_CONTAINER]; - self[START_OFFSET] = self[END_OFFSET]; - } + // Lazy values cache the value and kick off immediately + baseFn(set); - self.collapsed = TRUE; - } + return { + get: get, + map: map, + isReady: isReady + }; + }; - function selectNode(n) { - setStartBefore(n); - setEndAfter(n); - } + var pure = function (a) { + return nu(function (callback) { + callback(a); + }); + }; - function selectNodeContents(n) { - setStart(n, 0); - setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length); - } + return { + nu: nu, + pure: pure + }; + } +); +define( + 'ephox.katamari.async.Bounce', - function compareBoundaryPoints(h, r) { - var sc = self[START_CONTAINER], so = self[START_OFFSET], ec = self[END_CONTAINER], eo = self[END_OFFSET], - rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset; + [ + 'global!Array', + 'global!setTimeout' + ], - // Check START_TO_START - if (h === 0) { - return _compareBoundaryPoints(sc, so, rsc, rso); - } + function (Array, setTimeout) { - // Check START_TO_END - if (h === 1) { - return _compareBoundaryPoints(ec, eo, rsc, rso); - } + var bounce = function(f) { + return function() { + var args = Array.prototype.slice.call(arguments); + var me = this; + setTimeout(function() { + f.apply(me, args); + }, 0); + }; + }; - // Check END_TO_END - if (h === 2) { - return _compareBoundaryPoints(ec, eo, rec, reo); - } + return { + bounce: bounce + }; + } +); - // Check END_TO_START - if (h === 3) { - return _compareBoundaryPoints(sc, so, rec, reo); - } - } +define( + 'ephox.katamari.api.Future', - function deleteContents() { - _traverse(DELETE); - } + [ + 'ephox.katamari.api.LazyValue', + 'ephox.katamari.async.Bounce' + ], - function extractContents() { - return _traverse(EXTRACT); - } + /** A future value that is evaluated on demand. The base function is re-evaluated each time 'get' is called. */ + function (LazyValue, Bounce) { + var nu = function (baseFn) { + var get = function(callback) { + baseFn(Bounce.bounce(callback)); + }; - function cloneContents() { - return _traverse(CLONE); - } + /** map :: this Future a -> (a -> b) -> Future b */ + var map = function (fab) { + return nu(function (callback) { + get(function (a) { + var value = fab(a); + callback(value); + }); + }); + }; - function insertNode(n) { - var startContainer = this[START_CONTAINER], - startOffset = this[START_OFFSET], nn, o; + /** bind :: this Future a -> (a -> Future b) -> Future b */ + var bind = function (aFutureB) { + return nu(function (callback) { + get(function (a) { + aFutureB(a).get(callback); + }); + }); + }; - // Node is TEXT_NODE or CDATA - if ((startContainer.nodeType === 3 || startContainer.nodeType === 4) && startContainer.nodeValue) { - if (!startOffset) { - // At the start of text - startContainer.parentNode.insertBefore(n, startContainer); - } else if (startOffset >= startContainer.nodeValue.length) { - // At the end of text - dom.insertAfter(n, startContainer); - } else { - // Middle, need to split - nn = startContainer.splitText(startOffset); - startContainer.parentNode.insertBefore(n, nn); - } - } else { - // Insert element node - if (startContainer.childNodes.length > 0) { - o = startContainer.childNodes[startOffset]; - } + /** anonBind :: this Future a -> Future b -> Future b + * Returns a future, which evaluates the first future, ignores the result, then evaluates the second. + */ + var anonBind = function (futureB) { + return nu(function (callback) { + get(function (a) { + futureB.get(callback); + }); + }); + }; - if (o) { - startContainer.insertBefore(n, o); - } else { - if (startContainer.nodeType == 3) { - dom.insertAfter(n, startContainer); - } else { - startContainer.appendChild(n); - } - } - } - } + var toLazy = function () { + return LazyValue.nu(get); + }; - function surroundContents(n) { - var f = self.extractContents(); + return { + map: map, + bind: bind, + anonBind: anonBind, + toLazy: toLazy, + get: get + }; - self.insertNode(n); - n.appendChild(f); - self.selectNode(n); - } + }; - function cloneRange() { - return extend(new Range(dom), { - startContainer: self[START_CONTAINER], - startOffset: self[START_OFFSET], - endContainer: self[END_CONTAINER], - endOffset: self[END_OFFSET], - collapsed: self.collapsed, - commonAncestorContainer: self.commonAncestorContainer - }); - } + /** a -> Future a */ + var pure = function (a) { + return nu(function (callback) { + callback(a); + }); + }; - // Private methods + return { + nu: nu, + pure: pure + }; + } +); - function _getSelectedNode(container, offset) { - var child; +define( + 'ephox.katamari.async.AsyncValues', - // TEXT_NODE - if (container.nodeType == 3) { - return container; - } + [ + 'ephox.katamari.api.Arr' + ], - if (offset < 0) { - return container; - } + function (Arr) { + /* + * NOTE: an `asyncValue` must have a `get` function which gets given a callback and calls + * that callback with a value once it is ready + * + * e.g + * { + * get: function (callback) { callback(10); } + * } + */ + var par = function (asyncValues, nu) { + return nu(function(callback) { + var r = []; + var count = 0; - child = container.firstChild; - while (child && offset > 0) { - --offset; - child = child.nextSibling; - } + var cb = function(i) { + return function(value) { + r[i] = value; + count++; + if (count >= asyncValues.length) { + callback(r); + } + }; + }; - if (child) { - return child; + if (asyncValues.length === 0) { + callback([]); + } else { + Arr.each(asyncValues, function(asyncValue, i) { + asyncValue.get(cb(i)); + }); } + }); + }; - return container; - } + return { + par: par + }; + } +); +define( + 'ephox.katamari.api.Futures', - function _isCollapsed() { - return (self[START_CONTAINER] == self[END_CONTAINER] && self[START_OFFSET] == self[END_OFFSET]); - } + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Future', + 'ephox.katamari.async.AsyncValues' + ], + + function (Arr, Future, AsyncValues) { + /** par :: [Future a] -> Future [a] */ + var par = function(futures) { + return AsyncValues.par(futures, Future.nu); + }; - function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) { - var c, offsetC, n, cmnRoot, childA, childB; + /** mapM :: [a] -> (a -> Future b) -> Future [b] */ + var mapM = function(array, fn) { + var futures = Arr.map(array, fn); + return par(futures); + }; - // In the first case the boundary-points have the same container. A is before B - // if its offset is less than the offset of B, A is equal to B if its offset is - // equal to the offset of B, and A is after B if its offset is greater than the - // offset of B. - if (containerA == containerB) { - if (offsetA == offsetB) { - return 0; // equal - } + /** Kleisli composition of two functions: a -> Future b. + * Note the order of arguments: g is invoked first, then the result passed to f. + * This is in line with f . g = \x -> f (g a) + * + * compose :: ((b -> Future c), (a -> Future b)) -> a -> Future c + */ + var compose = function (f, g) { + return function (a) { + return g(a).bind(f); + }; + }; - if (offsetA < offsetB) { - return -1; // before - } + return { + par: par, + mapM: mapM, + compose: compose + }; + } +); +define( + 'ephox.katamari.api.Result', - return 1; // after - } + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option' + ], - // In the second case a child node C of the container of A is an ancestor - // container of B. In this case, A is before B if the offset of A is less than or - // equal to the index of the child node C and A is after B otherwise. - c = containerB; - while (c && c.parentNode != containerA) { - c = c.parentNode; - } + function (Fun, Option) { + /* The type signatures for Result + * is :: this Result a -> a -> Bool + * or :: this Result a -> Result a -> Result a + * orThunk :: this Result a -> (_ -> Result a) -> Result a + * map :: this Result a -> (a -> b) -> Result b + * each :: this Result a -> (a -> _) -> _ + * bind :: this Result a -> (a -> Result b) -> Result b + * fold :: this Result a -> (_ -> b, a -> b) -> b + * exists :: this Result a -> (a -> Bool) -> Bool + * forall :: this Result a -> (a -> Bool) -> Bool + * toOption :: this Result a -> Option a + * isValue :: this Result a -> Bool + * isError :: this Result a -> Bool + * getOr :: this Result a -> a -> a + * getOrThunk :: this Result a -> (_ -> a) -> a + * getOrDie :: this Result a -> a (or throws error) + */ - if (c) { - offsetC = 0; - n = containerA.firstChild; + var value = function (o) { + var is = function (v) { + return o === v; + }; - while (n != c && offsetC < offsetA) { - offsetC++; - n = n.nextSibling; - } + var or = function (opt) { + return value(o); + }; - if (offsetA <= offsetC) { - return -1; // before - } + var orThunk = function (f) { + return value(o); + }; - return 1; // after - } + var map = function (f) { + return value(f(o)); + }; - // In the third case a child node C of the container of B is an ancestor container - // of A. In this case, A is before B if the index of the child node C is less than - // the offset of B and A is after B otherwise. - c = containerA; - while (c && c.parentNode != containerB) { - c = c.parentNode; - } + var each = function (f) { + f(o); + }; - if (c) { - offsetC = 0; - n = containerB.firstChild; + var bind = function (f) { + return f(o); + }; - while (n != c && offsetC < offsetB) { - offsetC++; - n = n.nextSibling; - } + var fold = function (_, onValue) { + return onValue(o); + }; - if (offsetC < offsetB) { - return -1; // before - } + var exists = function (f) { + return f(o); + }; - return 1; // after - } + var forall = function (f) { + return f(o); + }; - // In the fourth case, none of three other cases hold: the containers of A and B - // are siblings or descendants of sibling nodes. In this case, A is before B if - // the container of A is before the container of B in a pre-order traversal of the - // Ranges' context tree and A is after B otherwise. - cmnRoot = dom.findCommonAncestor(containerA, containerB); - childA = containerA; + var toOption = function () { + return Option.some(o); + }; + + return { + is: is, + isValue: Fun.constant(true), + isError: Fun.constant(false), + getOr: Fun.constant(o), + getOrThunk: Fun.constant(o), + getOrDie: Fun.constant(o), + or: or, + orThunk: orThunk, + fold: fold, + map: map, + each: each, + bind: bind, + exists: exists, + forall: forall, + toOption: toOption + }; + }; - while (childA && childA.parentNode != cmnRoot) { - childA = childA.parentNode; - } + var error = function (message) { + var getOrThunk = function (f) { + return f(); + }; - if (!childA) { - childA = cmnRoot; - } + var getOrDie = function () { + return Fun.die(message)(); + }; - childB = containerB; - while (childB && childB.parentNode != cmnRoot) { - childB = childB.parentNode; - } + var or = function (opt) { + return opt; + }; - if (!childB) { - childB = cmnRoot; - } + var orThunk = function (f) { + return f(); + }; - if (childA == childB) { - return 0; // equal - } + var map = function (f) { + return error(message); + }; - n = cmnRoot.firstChild; - while (n) { - if (n == childA) { - return -1; // before - } + var bind = function (f) { + return error(message); + }; - if (n == childB) { - return 1; // after - } + var fold = function (onError, _) { + return onError(message); + }; - n = n.nextSibling; - } - } + return { + is: Fun.constant(false), + isValue: Fun.constant(false), + isError: Fun.constant(true), + getOr: Fun.identity, + getOrThunk: getOrThunk, + getOrDie: getOrDie, + or: or, + orThunk: orThunk, + fold: fold, + map: map, + each: Fun.noop, + bind: bind, + exists: Fun.constant(false), + forall: Fun.constant(true), + toOption: Option.none + }; + }; - function _setEndPoint(st, n, o) { - var ec, sc; + return { + value: value, + error: error + }; + } +); - if (st) { - self[START_CONTAINER] = n; - self[START_OFFSET] = o; - } else { - self[END_CONTAINER] = n; - self[END_OFFSET] = o; - } +/** + * StyleSheetLoader.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 + */ - // If one boundary-point of a Range is set to have a root container - // other than the current one for the Range, the Range is collapsed to - // the new position. This enforces the restriction that both boundary- - // points of a Range must have the same root container. - ec = self[END_CONTAINER]; - while (ec.parentNode) { - ec = ec.parentNode; - } +/** + * This class handles loading of external stylesheets and fires events when these are loaded. + * + * @class tinymce.dom.StyleSheetLoader + * @private + */ +define( + 'tinymce.core.dom.StyleSheetLoader', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Future', + 'ephox.katamari.api.Futures', + 'ephox.katamari.api.Result', + 'global!navigator', + 'tinymce.core.util.Delay', + 'tinymce.core.util.Tools' + ], + function (Arr, Fun, Future, Futures, Result, navigator, Delay, Tools) { + "use strict"; - sc = self[START_CONTAINER]; - while (sc.parentNode) { - sc = sc.parentNode; - } + return function (document, settings) { + var idCount = 0, loadedStates = {}, maxLoadTime; - if (sc == ec) { - // The start position of a Range is guaranteed to never be after the - // end position. To enforce this restriction, if the start is set to - // be at a position after the end, the Range is collapsed to that - // position. - if (_compareBoundaryPoints(self[START_CONTAINER], self[START_OFFSET], self[END_CONTAINER], self[END_OFFSET]) > 0) { - self.collapse(st); - } - } else { - self.collapse(st); - } + settings = settings || {}; + maxLoadTime = settings.maxLoadTime || 5000; - self.collapsed = _isCollapsed(); - self.commonAncestorContainer = dom.findCommonAncestor(self[START_CONTAINER], self[END_CONTAINER]); + function appendToHead(node) { + document.getElementsByTagName('head')[0].appendChild(node); } - function _traverse(how) { - var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep; + /** + * Loads the specified css style sheet file and call the loadedCallback once it's finished loading. + * + * @method load + * @param {String} url Url to be loaded. + * @param {Function} loadedCallback Callback to be executed when loaded. + * @param {Function} errorCallback Callback to be executed when failed loading. + */ + function load(url, loadedCallback, errorCallback) { + var link, style, startTime, state; - if (self[START_CONTAINER] == self[END_CONTAINER]) { - return _traverseSameContainer(how); - } + function passed() { + var callbacks = state.passed, i = callbacks.length; - for (c = self[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == self[START_CONTAINER]) { - return _traverseCommonStartContainer(c, how); + while (i--) { + callbacks[i](); } - ++endContainerDepth; + state.status = 2; + state.passed = []; + state.failed = []; } - for (c = self[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) { - if (p == self[END_CONTAINER]) { - return _traverseCommonEndContainer(c, how); + function failed() { + var callbacks = state.failed, i = callbacks.length; + + while (i--) { + callbacks[i](); } - ++startContainerDepth; + state.status = 3; + state.passed = []; + state.failed = []; } - depthDiff = startContainerDepth - endContainerDepth; - - startNode = self[START_CONTAINER]; - while (depthDiff > 0) { - startNode = startNode.parentNode; - depthDiff--; + // Sniffs for older WebKit versions that have the link.onload but a broken one + function isOldWebKit() { + var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/); + return !!(webKitChunks && webKitChunks[1] < 536); } - endNode = self[END_CONTAINER]; - while (depthDiff < 0) { - endNode = endNode.parentNode; - depthDiff++; + // Calls the waitCallback until the test returns true or the timeout occurs + function wait(testCallback, waitCallback) { + if (!testCallback()) { + // Wait for timeout + if ((new Date().getTime()) - startTime < maxLoadTime) { + Delay.setTimeout(waitCallback); + } else { + failed(); + } + } } - // ascend the ancestor hierarchy until we have a common parent. - for (sp = startNode.parentNode, ep = endNode.parentNode; sp != ep; sp = sp.parentNode, ep = ep.parentNode) { - startNode = sp; - endNode = ep; - } + // Workaround for WebKit that doesn't properly support the onload event for link elements + // Or WebKit that fires the onload event before the StyleSheet is added to the document + function waitForWebKitLinkLoaded() { + wait(function () { + var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner; - return _traverseCommonAncestors(startNode, endNode, how); - } - - function _traverseSameContainer(how) { - var frag, s, sub, n, cnt, sibling, xferNode, start, len; - - if (how != DELETE) { - frag = createDocumentFragment(); - } - - // If selection is empty, just return the fragment - if (self[START_OFFSET] == self[END_OFFSET]) { - return frag; - } - - // Text node needs special case handling - if (self[START_CONTAINER].nodeType == 3) { // TEXT_NODE - // get the substring - s = self[START_CONTAINER].nodeValue; - sub = s.substring(self[START_OFFSET], self[END_OFFSET]); - - // set the original text node to its new value - if (how != CLONE) { - n = self[START_CONTAINER]; - start = self[START_OFFSET]; - len = self[END_OFFSET] - self[START_OFFSET]; - - if (start === 0 && len >= n.nodeValue.length - 1) { - n.parentNode.removeChild(n); - } else { - n.deleteData(start, len); + while (i--) { + styleSheet = styleSheets[i]; + owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement; + if (owner && owner.id === link.id) { + passed(); + return true; + } } - - // Nothing is partially selected, so collapse to start point - self.collapse(TRUE); - } - - if (how == DELETE) { - return; - } - - if (sub.length > 0) { - frag.appendChild(doc.createTextNode(sub)); - } - - return frag; - } - - // Copy nodes between the start/end offsets. - n = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]); - cnt = self[END_OFFSET] - self[START_OFFSET]; - - while (n && cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); - - if (frag) { - frag.appendChild(xferNode); - } - - --cnt; - n = sibling; + }, waitForWebKitLinkLoaded); } - // Nothing is partially selected, so collapse to start point - if (how != CLONE) { - self.collapse(TRUE); + // Workaround for older Geckos that doesn't have any onload event for StyleSheets + function waitForGeckoLinkLoaded() { + wait(function () { + try { + // Accessing the cssRules will throw an exception until the CSS file is loaded + var cssRules = style.sheet.cssRules; + passed(); + return !!cssRules; + } catch (ex) { + // Ignore + } + }, waitForGeckoLinkLoaded); } - return frag; - } - - function _traverseCommonStartContainer(endAncestor, how) { - var frag, n, endIdx, cnt, sibling, xferNode; - - if (how != DELETE) { - frag = createDocumentFragment(); - } + url = Tools._addCacheSuffix(url); - n = _traverseRightBoundary(endAncestor, how); + if (!loadedStates[url]) { + state = { + passed: [], + failed: [] + }; - if (frag) { - frag.appendChild(n); + loadedStates[url] = state; + } else { + state = loadedStates[url]; } - endIdx = nodeIndex(endAncestor); - cnt = endIdx - self[START_OFFSET]; - - if (cnt <= 0) { - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - self.setEndBefore(endAncestor); - self.collapse(FALSE); - } - - return frag; + if (loadedCallback) { + state.passed.push(loadedCallback); } - n = endAncestor.previousSibling; - while (cnt > 0) { - sibling = n.previousSibling; - xferNode = _traverseFullySelected(n, how); - - if (frag) { - frag.insertBefore(xferNode, frag.firstChild); - } - - --cnt; - n = sibling; + if (errorCallback) { + state.failed.push(errorCallback); } - // Collapse to just before the endAncestor, which - // is partially selected. - if (how != CLONE) { - self.setEndBefore(endAncestor); - self.collapse(FALSE); + // Is loading wait for it to pass + if (state.status == 1) { + return; } - return frag; - } - - function _traverseCommonEndContainer(startAncestor, how) { - var frag, startIdx, n, cnt, sibling, xferNode; - - if (how != DELETE) { - frag = createDocumentFragment(); + // Has finished loading and was success + if (state.status == 2) { + passed(); + return; } - n = _traverseLeftBoundary(startAncestor, how); - if (frag) { - frag.appendChild(n); + // Has finished loading and was a failure + if (state.status == 3) { + failed(); + return; } - startIdx = nodeIndex(startAncestor); - ++startIdx; // Because we already traversed it - - cnt = self[END_OFFSET] - startIdx; - n = startAncestor.nextSibling; - while (n && cnt > 0) { - sibling = n.nextSibling; - xferNode = _traverseFullySelected(n, how); + // Start loading + state.status = 1; + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.id = 'u' + (idCount++); + link.async = false; + link.defer = false; + startTime = new Date().getTime(); - if (frag) { - frag.appendChild(xferNode); + // Feature detect onload on link element and sniff older webkits since it has an broken onload event + if ("onload" in link && !isOldWebKit()) { + link.onload = waitForWebKitLinkLoaded; + link.onerror = failed; + } else { + // Sniff for old Firefox that doesn't support the onload event on link elements + // TODO: Remove this in the future when everyone uses modern browsers + if (navigator.userAgent.indexOf("Firefox") > 0) { + style = document.createElement('style'); + style.textContent = '@import "' + url + '"'; + waitForGeckoLinkLoaded(); + appendToHead(style); + return; } - --cnt; - n = sibling; - } - - if (how != CLONE) { - self.setStartAfter(startAncestor); - self.collapse(TRUE); + // Use the id owner on older webkits + waitForWebKitLinkLoaded(); } - return frag; + appendToHead(link); + link.href = url; } - function _traverseCommonAncestors(startAncestor, endAncestor, how) { - var n, frag, startOffset, endOffset, cnt, sibling, nextSibling; - - if (how != DELETE) { - frag = createDocumentFragment(); - } - - n = _traverseLeftBoundary(startAncestor, how); - if (frag) { - frag.appendChild(n); - } - - startOffset = nodeIndex(startAncestor); - endOffset = nodeIndex(endAncestor); - ++startOffset; + var loadF = function (url) { + return Future.nu(function (resolve) { + load( + url, + Fun.compose(resolve, Fun.constant(Result.value(url))), + Fun.compose(resolve, Fun.constant(Result.error(url))) + ); + }); + }; - cnt = endOffset - startOffset; - sibling = startAncestor.nextSibling; + var unbox = function (result) { + return result.fold(Fun.identity, Fun.identity); + }; - while (cnt > 0) { - nextSibling = sibling.nextSibling; - n = _traverseFullySelected(sibling, how); + var loadAll = function (urls, success, failure) { + Futures.par(Arr.map(urls, loadF)).get(function (result) { + var parts = Arr.partition(result, function (r) { + return r.isValue(); + }); - if (frag) { - frag.appendChild(n); + if (parts.fail.length > 0) { + failure(parts.fail.map(unbox)); + } else { + success(parts.pass.map(unbox)); } + }); + }; - sibling = nextSibling; - --cnt; - } - - n = _traverseRightBoundary(endAncestor, how); - - if (frag) { - frag.appendChild(n); - } - - if (how != CLONE) { - self.setStartAfter(startAncestor); - self.collapse(TRUE); - } - - return frag; - } - - function _traverseRightBoundary(root, how) { - var next = _getSelectedNode(self[END_CONTAINER], self[END_OFFSET] - 1), parent, clonedParent; - var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != self[END_CONTAINER]; - - if (next == root) { - return _traverseNode(next, isFullySelected, FALSE, how); - } - - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, FALSE, how); - - while (parent) { - while (next) { - prevSibling = next.previousSibling; - clonedChild = _traverseNode(next, isFullySelected, FALSE, how); - - if (how != DELETE) { - clonedParent.insertBefore(clonedChild, clonedParent.firstChild); - } - - isFullySelected = TRUE; - next = prevSibling; - } + return { + load: load, + loadAll: loadAll + }; + }; + } +); - if (parent == root) { - return clonedParent; - } +/** + * TreeWalker.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 + */ - next = parent.previousSibling; - parent = parent.parentNode; +/** + * TreeWalker class enables you to walk the DOM in a linear manner. + * + * @class tinymce.dom.TreeWalker + * @example + * var walker = new tinymce.dom.TreeWalker(startNode); + * + * do { + * console.log(walker.current()); + * } while (walker.next()); + */ +define( + 'tinymce.core.dom.TreeWalker', + [ + ], + function () { + /** + * Constructs a new TreeWalker instance. + * + * @constructor + * @method TreeWalker + * @param {Node} startNode Node to start walking from. + * @param {node} rootNode Optional root node to never walk out of. + */ + return function (startNode, rootNode) { + var node = startNode; - clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how); + function findSibling(node, startName, siblingName, shallow) { + var sibling, parent; - if (how != DELETE) { - clonedGrandParent.appendChild(clonedParent); + if (node) { + // Walk into nodes if it has a start + if (!shallow && node[startName]) { + return node[startName]; } - clonedParent = clonedGrandParent; - } - } - - function _traverseLeftBoundary(root, how) { - var next = _getSelectedNode(self[START_CONTAINER], self[START_OFFSET]), isFullySelected = next != self[START_CONTAINER]; - var parent, clonedParent, nextSibling, clonedChild, clonedGrandParent; - - if (next == root) { - return _traverseNode(next, isFullySelected, TRUE, how); - } - - parent = next.parentNode; - clonedParent = _traverseNode(parent, FALSE, TRUE, how); - - while (parent) { - while (next) { - nextSibling = next.nextSibling; - clonedChild = _traverseNode(next, isFullySelected, TRUE, how); - - if (how != DELETE) { - clonedParent.appendChild(clonedChild); + // Return the sibling if it has one + if (node != rootNode) { + sibling = node[siblingName]; + if (sibling) { + return sibling; } - isFullySelected = TRUE; - next = nextSibling; - } - - if (parent == root) { - return clonedParent; - } - - next = parent.nextSibling; - parent = parent.parentNode; - - clonedGrandParent = _traverseNode(parent, FALSE, TRUE, how); - - if (how != DELETE) { - clonedGrandParent.appendChild(clonedParent); + // Walk up the parents to look for siblings + for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) { + sibling = parent[siblingName]; + if (sibling) { + return sibling; + } + } } - - clonedParent = clonedGrandParent; } } - function _traverseNode(n, isFullySelected, isLeft, how) { - var txtValue, newNodeValue, oldNodeValue, offset, newNode; - - if (isFullySelected) { - return _traverseFullySelected(n, how); - } - - // TEXT_NODE - if (n.nodeType == 3) { - txtValue = n.nodeValue; - - if (isLeft) { - offset = self[START_OFFSET]; - newNodeValue = txtValue.substring(offset); - oldNodeValue = txtValue.substring(0, offset); - } else { - offset = self[END_OFFSET]; - newNodeValue = txtValue.substring(0, offset); - oldNodeValue = txtValue.substring(offset); - } - - if (how != CLONE) { - n.nodeValue = oldNodeValue; - } + function findPreviousNode(node, startName, siblingName, shallow) { + var sibling, parent, child; - if (how == DELETE) { + if (node) { + sibling = node[siblingName]; + if (rootNode && sibling === rootNode) { return; } - newNode = dom.clone(n, FALSE); - newNode.nodeValue = newNodeValue; - - return newNode; - } - - if (how == DELETE) { - return; - } + if (sibling) { + if (!shallow) { + // Walk up the parents to look for siblings + for (child = sibling[startName]; child; child = child[startName]) { + if (!child[startName]) { + return child; + } + } + } - return dom.clone(n, FALSE); - } + return sibling; + } - function _traverseFullySelected(n, how) { - if (how != DELETE) { - return how == CLONE ? dom.clone(n, TRUE) : n; + parent = node.parentNode; + if (parent && parent !== rootNode) { + return parent; + } } - - n.parentNode.removeChild(n); - } - - function toStringIE() { - return dom.create('body', null, cloneContents()).outerText; } - extend(self, { - // Initial states - startContainer: doc, - startOffset: 0, - endContainer: doc, - endOffset: 0, - collapsed: TRUE, - commonAncestorContainer: doc, - - // Range constants - START_TO_START: 0, - START_TO_END: 1, - END_TO_END: 2, - END_TO_START: 3, + /** + * Returns the current node. + * + * @method current + * @return {Node} Current node where the walker is. + */ + this.current = function () { + return node; + }; - // Public methods - setStart: setStart, - setEnd: setEnd, - setStartBefore: setStartBefore, - setStartAfter: setStartAfter, - setEndBefore: setEndBefore, - setEndAfter: setEndAfter, - collapse: collapse, - selectNode: selectNode, - selectNodeContents: selectNodeContents, - compareBoundaryPoints: compareBoundaryPoints, - deleteContents: deleteContents, - extractContents: extractContents, - cloneContents: cloneContents, - insertNode: insertNode, - surroundContents: surroundContents, - cloneRange: cloneRange, - toStringIE: toStringIE - }); + /** + * Walks to the next node in tree. + * + * @method next + * @return {Node} Current node where the walker is after moving to the next node. + */ + this.next = function (shallow) { + node = findSibling(node, 'firstChild', 'nextSibling', shallow); + return node; + }; - return self; - } + /** + * Walks to the previous node in tree. + * + * @method prev + * @return {Node} Current node where the walker is after moving to the previous node. + */ + this.prev = function (shallow) { + node = findSibling(node, 'lastChild', 'previousSibling', shallow); + return node; + }; - // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype - Range.prototype.toString = function () { - return this.toStringIE(); + this.prev2 = function (shallow) { + node = findPreviousNode(node, 'lastChild', 'previousSibling', shallow); + return node; + }; }; - - return Range; } ); -defineGlobal("global!Object", Object); +define("global!console", [], function () { if (typeof console === "undefined") console = { log: function () {} }; return console; }); define( - 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', [ 'ephox.katamari.api.Fun', - 'global!Object' + 'global!Error', + 'global!console', + 'global!document' ], - function (Fun, Object) { - - var never = Fun.never; - var always = Fun.always; + 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]); + }; - /** - Option objects support the following methods: + var fromTag = function (tag, scope) { + var doc = scope || document; + var node = doc.createElement(tag); + return fromDom(node); + }; - fold :: this Option a -> ((() -> b, a -> b)) -> Option b + var fromText = function (text, scope) { + var doc = scope || document; + var node = doc.createTextNode(text); + return fromDom(node); + }; - is :: this Option a -> a -> Boolean + var fromDom = function (node) { + if (node === null || node === undefined) throw new Error('Node cannot be null or undefined'); + return { + dom: Fun.constant(node) + }; + }; - isSome :: this Option a -> () -> Boolean + return { + fromHtml: fromHtml, + fromTag: fromTag, + fromText: fromText, + fromDom: fromDom + }; + } +); - isNone :: this Option a -> () -> Boolean +/** + * Entities.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 + */ - getOr :: this Option a -> a -> a +/*jshint bitwise:false */ +/*eslint no-bitwise:0 */ - getOrThunk :: this Option a -> (() -> a) -> a +/** + * Entity encoder class. + * + * @class tinymce.html.Entities + * @static + * @version 3.4 + */ +define( + 'tinymce.core.html.Entities', + [ + 'ephox.sugar.api.node.Element', + 'tinymce.core.util.Tools' + ], + function (Element, Tools) { + var makeMap = Tools.makeMap; - getOrDie :: this Option a -> String -> a + var namedEntities, baseEntities, reverseEntities, + attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + rawCharsRegExp = /[<>&\"\']/g, + entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi, + asciiMap = { + 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020", + 135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152", + 142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022", + 150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A", + 156: "\u0153", 158: "\u017E", 159: "\u0178" + }; - or :: this Option a -> Option a -> Option a - - if some: return self - - if none: return opt + // Raw entities + baseEntities = { + '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code + "'": ''', + '<': '<', + '>': '>', + '&': '&', + '\u0060': '`' + }; - orThunk :: this Option a -> (() -> Option a) -> Option a - - Same as "or", but uses a thunk instead of a value + // Reverse lookup table for raw entities + reverseEntities = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + ''': "'" + }; - map :: this Option a -> (a -> b) -> Option b - - "fmap" operation on the Option Functor. - - same as 'each' + // Decodes text by using the browser + function nativeDecode(text) { + var elm; - ap :: this Option a -> Option (a -> b) -> Option b - - "apply" operation on the Option Apply/Applicative. - - Equivalent to <*> in Haskell/PureScript. + elm = Element.fromTag("div").dom(); + elm.innerHTML = text; - each :: this Option a -> (a -> b) -> Option b - - same as 'map' + return elm.textContent || elm.innerText || text; + } - bind :: this Option a -> (a -> Option b) -> Option b - - "bind"/"flatMap" operation on the Option Bind/Monad. - - Equivalent to >>= in Haskell/PureScript; flatMap in Scala. + // Build a two way lookup table for the entities + function buildEntitiesLookup(items, radix) { + var i, chr, entity, lookup = {}; - flatten :: {this Option (Option a))} -> () -> Option a - - "flatten"/"join" operation on the Option Monad. + if (items) { + items = items.split(','); + radix = radix || 10; - exists :: this Option a -> (a -> Boolean) -> Boolean + // Build entities lookup table + for (i = 0; i < items.length; i += 2) { + chr = String.fromCharCode(parseInt(items[i], radix)); - forall :: this Option a -> (a -> Boolean) -> Boolean + // Only add non base entities + if (!baseEntities[chr]) { + entity = '&' + items[i + 1] + ';'; + lookup[chr] = entity; + lookup[entity] = chr; + } + } - filter :: this Option a -> (a -> Boolean) -> Option a + return lookup; + } + } - equals :: this Option a -> Option a -> Boolean + // Unpack entities lookup where the numbers are in radix 32 to reduce the size + namedEntities = buildEntitiesLookup( + '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + + '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + + '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + + '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + + '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + + '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + + '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + + '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + + '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + + '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + + 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + + 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + + 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + + 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + + 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + + '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + + '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + + '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + + '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + + '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + + 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + + 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + + 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + + '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + + '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); - equals_ :: this Option a -> (Option a, a -> Boolean) -> Boolean + var Entities = { + /** + * Encodes the specified string using raw entities. This means only the required XML base entities will be encoded. + * + * @method encodeRaw + * @param {String} text Text to encode. + * @param {Boolean} attr Optional flag to specify if the text is attribute contents. + * @return {String} Entity encoded text. + */ + encodeRaw: function (text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { + return baseEntities[chr] || chr; + }); + }, - toArray :: this Option a -> () -> [a] - - */ - - var none = function () { return NONE; }; - - var NONE = (function () { - var eq = function (o) { - return o.isNone(); - }; + /** + * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents + * since it doesn't know if the context is within a attribute or text node. This was added for compatibility + * and is exposed as the DOMUtils.encode function. + * + * @method encodeAllRaw + * @param {String} text Text to encode. + * @return {String} Entity encoded text. + */ + encodeAllRaw: function (text) { + return ('' + text).replace(rawCharsRegExp, function (chr) { + return baseEntities[chr] || chr; + }); + }, - // inlined from peanut, maybe a micro-optimisation? - var call = function (thunk) { return thunk(); }; - var id = function (n) { return n; }; - var noop = function () { }; + /** + * Encodes the specified string using numeric entities. The core entities will be + * encoded as named ones but all non lower ascii characters will be encoded into numeric entities. + * + * @method encodeNumeric + * @param {String} text Text to encode. + * @param {Boolean} attr Optional flag to specify if the text is attribute contents. + * @return {String} Entity encoded text. + */ + encodeNumeric: function (text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { + // Multi byte sequence convert it to a single entity + if (chr.length > 1) { + return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; + } - var me = { - fold: function (n, s) { return n(); }, - is: never, - isSome: never, - isNone: always, - getOr: id, - getOrThunk: call, - getOrDie: function (msg) { - throw new Error(msg || 'error: getOrDie called on none.'); - }, - or: id, - orThunk: call, - map: none, - ap: none, - each: noop, - bind: none, - flatten: none, - exists: never, - forall: always, - filter: none, - equals: eq, - equals_: eq, - toArray: function () { return []; }, - toString: Fun.constant("none()") - }; - if (Object.freeze) Object.freeze(me); - return me; - })(); + return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';'; + }); + }, + /** + * Encodes the specified string using named entities. The core entities will be encoded + * as named ones but all non lower ascii characters will be encoded into named entities. + * + * @method encodeNamed + * @param {String} text Text to encode. + * @param {Boolean} attr Optional flag to specify if the text is attribute contents. + * @param {Object} entities Optional parameter with entities to use. + * @return {String} Entity encoded text. + */ + encodeNamed: function (text, attr, entities) { + entities = entities || namedEntities; - /** some :: a -> Option a */ - var some = function (a) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { + return baseEntities[chr] || entities[chr] || chr; + }); + }, - // inlined from peanut, maybe a micro-optimisation? - var constant_a = function () { return a; }; + /** + * Returns an encode function based on the name(s) and it's optional entities. + * + * @method getEncodeFunc + * @param {String} name Comma separated list of encoders for example named,numeric. + * @param {String} entities Optional parameter with entities to use instead of the built in set. + * @return {function} Encode function to be used. + */ + getEncodeFunc: function (name, entities) { + entities = buildEntitiesLookup(entities) || namedEntities; - var self = function () { - // can't Fun.constant this one - return me; - }; + function encodeNamedAndNumeric(text, attr) { + return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { + if (baseEntities[chr] !== undefined) { + return baseEntities[chr]; + } - var map = function (f) { - return some(f(a)); - }; + if (entities[chr] !== undefined) { + return entities[chr]; + } - var bind = function (f) { - return f(a); - }; + // Convert multi-byte sequences to a single entity. + if (chr.length > 1) { + return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; + } - var me = { - fold: function (n, s) { return s(a); }, - is: function (v) { return a === v; }, - isSome: always, - isNone: never, - getOr: constant_a, - getOrThunk: constant_a, - getOrDie: constant_a, - or: self, - orThunk: self, - map: map, - ap: function (optfab) { - return optfab.fold(none, function(fab) { - return some(fab(a)); + return '&#' + chr.charCodeAt(0) + ';'; }); - }, - each: function (f) { - f(a); - }, - bind: bind, - flatten: constant_a, - exists: bind, - forall: bind, - filter: function (f) { - return f(a) ? me : NONE; - }, - equals: function (o) { - return o.is(a); - }, - equals_: function (o, elementEq) { - return o.fold( - never, - function (b) { return elementEq(a, b); } - ); - }, - toArray: function () { - return [a]; - }, - toString: function () { - return 'some(' + a + ')'; } - }; - return me; - }; - - /** from :: undefined|null|a -> Option a */ - var from = function (value) { - return value === null || value === undefined ? NONE : some(value); - }; - - return { - some: some, - none: none, - from: from - }; - } -); - -defineGlobal("global!String", String); -define( - 'ephox.katamari.api.Arr', - [ - 'ephox.katamari.api.Option', - 'global!Array', - 'global!Error', - 'global!String' - ], + function encodeCustomNamed(text, attr) { + return Entities.encodeNamed(text, attr, entities); + } - function (Option, Array, Error, String) { - // Use the native Array.indexOf if it is available (IE9+) otherwise fall back to manual iteration - // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf - var rawIndexOf = (function () { - var pIndexOf = Array.prototype.indexOf; + // Replace + with , to be compatible with previous TinyMCE versions + name = makeMap(name.replace(/\+/g, ',')); - var fastIndex = function (xs, x) { return pIndexOf.call(xs, x); }; + // Named and numeric encoder + if (name.named && name.numeric) { + return encodeNamedAndNumeric; + } - var slowIndex = function(xs, x) { return slowIndexOf(xs, x); }; + // Named encoder + if (name.named) { + // Custom names + if (entities) { + return encodeCustomNamed; + } - return pIndexOf === undefined ? slowIndex : fastIndex; - })(); + return Entities.encodeNamed; + } - var indexOf = function (xs, x) { - // The rawIndexOf method does not wrap up in an option. This is for performance reasons. - var r = rawIndexOf(xs, x); - return r === -1 ? Option.none() : Option.some(r); - }; + // Numeric + if (name.numeric) { + return Entities.encodeNumeric; + } - var contains = function (xs, x) { - return rawIndexOf(xs, x) > -1; - }; + // Raw encoder + return Entities.encodeRaw; + }, - // Using findIndex is likely less optimal in Chrome (dynamic return type instead of bool) - // but if we need that micro-optimisation we can inline it later. - var exists = function (xs, pred) { - return findIndex(xs, pred).isSome(); - }; + /** + * Decodes the specified string, this will replace entities with raw UTF characters. + * + * @method decode + * @param {String} text Text to entity decode. + * @return {String} Entity decoded string. + */ + decode: function (text) { + return text.replace(entityRegExp, function (all, numeric) { + if (numeric) { + if (numeric.charAt(0).toLowerCase() === 'x') { + numeric = parseInt(numeric.substr(1), 16); + } else { + numeric = parseInt(numeric, 10); + } - var range = function (num, f) { - var r = []; - for (var i = 0; i < num; i++) { - r.push(f(i)); - } - return r; - }; + // Support upper UTF + if (numeric > 0xFFFF) { + numeric -= 0x10000; - // It's a total micro optimisation, but these do make some difference. - // Particularly for browsers other than Chrome. - // - length caching - // http://jsperf.com/browser-diet-jquery-each-vs-for-loop/69 - // - not using push - // http://jsperf.com/array-direct-assignment-vs-push/2 + return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF)); + } - var chunk = function (array, size) { - var r = []; - for (var i = 0; i < array.length; i += size) { - var s = array.slice(i, i + size); - r.push(s); - } - return r; - }; + return asciiMap[numeric] || String.fromCharCode(numeric); + } - var map = function(xs, f) { - // pre-allocating array size when it's guaranteed to be known - // http://jsperf.com/push-allocated-vs-dynamic/22 - var len = xs.length; - var r = new Array(len); - for (var i = 0; i < len; i++) { - var x = xs[i]; - r[i] = f(x, i, xs); + return reverseEntities[all] || namedEntities[all] || nativeDecode(all); + }); } - return r; }; - // Unwound implementing other functions in terms of each. - // The code size is roughly the same, and it should allow for better optimisation. - var each = function(xs, f) { - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - f(x, i, xs); - } - }; + return Entities; + } +); - var eachr = function (xs, f) { - for (var i = xs.length - 1; i >= 0; i--) { - var x = xs[i]; - f(x, i, xs); - } - }; +/** + * Schema.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 + */ - var partition = function(xs, pred) { - var pass = []; - var fail = []; - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - var arr = pred(x, i, xs) ? pass : fail; - arr.push(x); - } - return { pass: pass, fail: fail }; - }; +/** + * Schema validator class. + * + * @class tinymce.html.Schema + * @example + * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) + * alert('span is valid child of p.'); + * + * if (tinymce.activeEditor.schema.getElementRule('p')) + * alert('P is a valid element.'); + * + * @class tinymce.html.Schema + * @version 3.4 + */ +define( + 'tinymce.core.html.Schema', + [ + "tinymce.core.util.Tools" + ], + function (Tools) { + var mapCache = {}, dummyObj = {}; + var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; - var filter = function(xs, pred) { - var r = []; - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - if (pred(x, i, xs)) { - r.push(x); - } - } - return r; - }; + function split(items, delim) { + items = Tools.trim(items); + return items ? items.split(delim || ' ') : []; + } - /* - * Groups an array into contiguous arrays of like elements. Whether an element is like or not depends on f. - * - * f is a function that derives a value from an element - e.g. true or false, or a string. - * Elements are like if this function generates the same value for them (according to ===). - * + /** + * Builds a schema lookup table * - * Order of the elements is preserved. Arr.flatten() on the result will return the original list, as with Haskell groupBy function. - * For a good explanation, see the group function (which is a special case of groupBy) - * http://hackage.haskell.org/package/base-4.7.0.0/docs/Data-List.html#v:group + * @private + * @param {String} type html4, html5 or html5-strict schema type. + * @return {Object} Schema lookup table. */ - var groupBy = function (xs, f) { - if (xs.length === 0) { - return []; - } else { - var wasType = f(xs[0]); // initial case for matching - var r = []; - var group = []; + function compileSchema(type) { + var schema = {}, globalAttributes, blockContent; + var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - var type = f(x); - if (type !== wasType) { - r.push(group); - group = []; - } - wasType = type; - group.push(x); - } - if (group.length !== 0) { - r.push(group); - } - return r; - } - }; + function add(name, attributes, children) { + var ni, attributesOrder, element; - var foldr = function (xs, f, acc) { - eachr(xs, function (x) { - acc = f(acc, x); - }); - return acc; - }; + function arrayToMap(array, obj) { + var map = {}, i, l; - var foldl = function (xs, f, acc) { - each(xs, function (x) { - acc = f(acc, x); - }); - return acc; - }; + for (i = 0, l = array.length; i < l; i++) { + map[array[i]] = obj || {}; + } - var find = function (xs, pred) { - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - if (pred(x, i, xs)) { - return Option.some(x); + return map; } - } - return Option.none(); - }; - var findIndex = function (xs, pred) { - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - if (pred(x, i, xs)) { - return Option.some(i); + children = children || []; + attributes = attributes || ""; + + if (typeof children === "string") { + children = split(children); } - } - return Option.none(); - }; + name = split(name); + ni = name.length; + while (ni--) { + attributesOrder = split([globalAttributes, attributes].join(' ')); - var slowIndexOf = function (xs, x) { - for (var i = 0, len = xs.length; i < len; ++i) { - if (xs[i] === x) { - return i; + element = { + attributes: arrayToMap(attributesOrder), + attributesOrder: attributesOrder, + children: arrayToMap(children, dummyObj) + }; + + schema[name[ni]] = element; } } - return -1; - }; + function addAttrs(name, attributes) { + var ni, schemaItem, i, l; - var push = Array.prototype.push; - var flatten = function (xs) { - // Note, this is possible because push supports multiple arguments: - // http://jsperf.com/concat-push/6 - // Note that in the past, concat() would silently work (very slowly) for array-like objects. - // With this change it will throw an error. - var r = []; - for (var i = 0, len = xs.length; i < len; ++i) { - // Ensure that each value is an array itself - if (! Array.prototype.isPrototypeOf(xs[i])) throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs); - push.apply(r, xs[i]); + name = split(name); + ni = name.length; + attributes = split(attributes); + while (ni--) { + schemaItem = schema[name[ni]]; + for (i = 0, l = attributes.length; i < l; i++) { + schemaItem.attributes[attributes[i]] = {}; + schemaItem.attributesOrder.push(attributes[i]); + } + } } - return r; - }; - - var bind = function (xs, f) { - var output = map(xs, f); - return flatten(output); - }; - var forall = function (xs, pred) { - for (var i = 0, len = xs.length; i < len; ++i) { - var x = xs[i]; - if (pred(x, i, xs) !== true) { - return false; - } + // Use cached schema + if (mapCache[type]) { + return mapCache[type]; } - return true; - }; - var equal = function (a1, a2) { - return a1.length === a2.length && forall(a1, function (x, i) { - return x === a2[i]; - }); - }; + // Attributes present on all elements + globalAttributes = "id accesskey class dir lang style tabindex title role"; - var slice = Array.prototype.slice; - var reverse = function (xs) { - var r = slice.call(xs, 0); - r.reverse(); - return r; - }; + // Event attributes can be opt-in/opt-out + /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + + "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + + "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + + "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + + "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + + "onwaiting" + );*/ - var difference = function (a1, a2) { - return filter(a1, function (x) { - return !contains(a2, x); - }); - }; + // Block content elements + blockContent = + "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul"; - var mapToObject = function(xs, f) { - var r = {}; - for (var i = 0, len = xs.length; i < len; i++) { - var x = xs[i]; - r[String(x)] = f(x, i); + // Phrasing content elements from the HTML5 spec (inline) + phrasingContent = + "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + + "label map noscript object q s samp script select small span strong sub sup " + + "textarea u var #text #comment" + ; + + // Add HTML5 items to globalAttributes, blockContent, phrasingContent + if (type != "html4") { + globalAttributes += " contenteditable contextmenu draggable dropzone " + + "hidden spellcheck translate"; + blockContent += " article aside details dialog figure header footer hgroup section nav"; + phrasingContent += " audio canvas command datalist mark meter output picture " + + "progress time wbr video ruby bdi keygen"; } - return r; - }; - var pure = function(x) { - return [x]; - }; + // Add HTML4 elements unless it's html5-strict + if (type != "html5-strict") { + globalAttributes += " xml:lang"; - var sort = function (xs, comparator) { - var copy = slice.call(xs, 0); - copy.sort(comparator); - return copy; - }; + html4PhrasingContent = "acronym applet basefont big font strike tt"; + phrasingContent = [phrasingContent, html4PhrasingContent].join(' '); - return { - map: map, - each: each, - eachr: eachr, - partition: partition, - filter: filter, - groupBy: groupBy, - indexOf: indexOf, - foldr: foldr, - foldl: foldl, - find: find, - findIndex: findIndex, - flatten: flatten, - bind: bind, - forall: forall, - exists: exists, - contains: contains, - equal: equal, - reverse: reverse, - chunk: chunk, - difference: difference, - mapToObject: mapToObject, - pure: pure, - sort: sort, - range: range - }; - } -); -defineGlobal("global!setTimeout", setTimeout); -define( - 'ephox.katamari.api.LazyValue', + each(split(html4PhrasingContent), function (name) { + add(name, "", phrasingContent); + }); - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Option', - 'global!setTimeout' - ], + html4BlockContent = "center dir isindex noframes"; + blockContent = [blockContent, html4BlockContent].join(' '); - function (Arr, Option, setTimeout) { - var nu = function (baseFn) { - var data = Option.none(); - var callbacks = []; + // Flow content elements from the HTML5 spec (block+inline) + flowContent = [blockContent, phrasingContent].join(' '); - /** map :: this LazyValue a -> (a -> b) -> LazyValue b */ - var map = function (f) { - return nu(function (nCallback) { - get(function (data) { - nCallback(f(data)); - }); + each(split(html4BlockContent), function (name) { + add(name, "", flowContent); }); - }; - - var get = function (nCallback) { - if (isReady()) call(nCallback); - else callbacks.push(nCallback); - }; + } - var set = function (x) { - data = Option.some(x); - run(callbacks); - callbacks = []; - }; + // Flow content elements from the HTML5 spec (block+inline) + flowContent = flowContent || [blockContent, phrasingContent].join(" "); - var isReady = function () { - return data.isSome(); - }; + // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement + // Schema items , , + add("html", "manifest", "head body"); + add("head", "", "base command link meta noscript script style title"); + add("title hr noscript br"); + add("base", "href target"); + add("link", "href rel media hreflang type sizes hreflang"); + add("meta", "name http-equiv content charset"); + add("style", "media type scoped"); + add("script", "src async defer type charset"); + add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " + + "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " + + "onpopstate onresize onscroll onstorage onunload", flowContent); + add("address dt dd div caption", "", flowContent); + add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent); + add("blockquote", "cite", flowContent); + add("ol", "reversed start type", "li"); + add("ul", "", "li"); + add("li", "value", flowContent); + add("dl", "", "dt dd"); + add("a", "href target rel media hreflang type", phrasingContent); + add("q", "cite", phrasingContent); + add("ins del", "cite datetime", flowContent); + add("img", "src sizes srcset alt usemap ismap width height"); + add("iframe", "src name width height", flowContent); + add("embed", "src type width height"); + add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' ')); + add("param", "name value"); + add("map", "name", [flowContent, "area"].join(' ')); + add("area", "alt coords shape href target rel media hreflang type"); + add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : "")); + add("colgroup", "span", "col"); + add("col", "span"); + add("tbody thead tfoot", "", "tr"); + add("tr", "", "td th"); + add("td", "colspan rowspan headers", flowContent); + add("th", "colspan rowspan headers scope abbr", flowContent); + add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent); + add("fieldset", "disabled form name", [flowContent, "legend"].join(' ')); + add("label", "form for", phrasingContent); + add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " + + "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width" + ); + add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value", + type == "html4" ? flowContent : phrasingContent); + add("select", "disabled form multiple name required size", "option optgroup"); + add("optgroup", "disabled label", "option"); + add("option", "disabled label selected value"); + add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap"); + add("menu", "type label", [flowContent, "li"].join(' ')); + add("noscript", "", flowContent); - var run = function (cbs) { - Arr.each(cbs, call); - }; + // Extend with HTML5 elements + if (type != "html4") { + add("wbr"); + add("ruby", "", [phrasingContent, "rt rp"].join(' ')); + add("figcaption", "", flowContent); + add("mark rt rp summary bdi", "", phrasingContent); + add("canvas", "width height", flowContent); + add("video", "src crossorigin poster preload autoplay mediagroup loop " + + "muted controls width height buffered", [flowContent, "track source"].join(' ')); + add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " + + "buffered volume", [flowContent, "track source"].join(' ')); + add("picture", "", "img source"); + add("source", "src srcset type media sizes"); + add("track", "kind src srclang label default"); + add("datalist", "", [phrasingContent, "option"].join(' ')); + add("article section nav aside header footer", "", flowContent); + add("hgroup", "", "h1 h2 h3 h4 h5 h6"); + add("figure", "", [flowContent, "figcaption"].join(' ')); + add("time", "datetime", phrasingContent); + add("dialog", "open", flowContent); + add("command", "type label icon disabled checked radiogroup command"); + add("output", "for form name", phrasingContent); + add("progress", "value max", phrasingContent); + add("meter", "value min max low high optimum", phrasingContent); + add("details", "open", [flowContent, "summary"].join(' ')); + add("keygen", "autofocus challenge disabled form keytype name"); + } - var call = function(cb) { - data.each(function(x) { - setTimeout(function() { - cb(x); - }, 0); - }); - }; + // Extend with HTML4 attributes unless it's html5-strict + if (type != "html5-strict") { + addAttrs("script", "language xml:space"); + addAttrs("style", "xml:space"); + addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace"); + addAttrs("embed", "align name hspace vspace"); + addAttrs("param", "valuetype type"); + addAttrs("a", "charset name rev shape coords"); + addAttrs("br", "clear"); + addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); + addAttrs("img", "name longdesc align border hspace vspace"); + addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align"); + addAttrs("font basefont", "size color face"); + addAttrs("input", "usemap align"); + addAttrs("select", "onchange"); + addAttrs("textarea"); + addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align"); + addAttrs("ul", "type compact"); + addAttrs("li", "type"); + addAttrs("ol dl menu dir", "compact"); + addAttrs("pre", "width xml:space"); + addAttrs("hr", "align noshade size width"); + addAttrs("isindex", "prompt"); + addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor"); + addAttrs("col", "width align char charoff valign"); + addAttrs("colgroup", "width align char charoff valign"); + addAttrs("thead", "align char charoff valign"); + addAttrs("tr", "align char charoff valign bgcolor"); + addAttrs("th", "axis align char charoff valign nowrap bgcolor width height"); + addAttrs("form", "accept"); + addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height"); + addAttrs("tfoot", "align char charoff valign"); + addAttrs("tbody", "align char charoff valign"); + addAttrs("area", "nohref"); + addAttrs("body", "background bgcolor text link vlink alink"); + } - // Lazy values cache the value and kick off immediately - baseFn(set); + // Extend with HTML5 attributes unless it's html4 + if (type != "html4") { + addAttrs("input button select textarea", "autofocus"); + addAttrs("input textarea", "placeholder"); + addAttrs("a", "download"); + addAttrs("link script img", "crossorigin"); + addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc + } - return { - get: get, - map: map, - isReady: isReady - }; - }; + // Special: iframe, ruby, video, audio, label - var pure = function (a) { - return nu(function (callback) { - callback(a); + // Delete children of the same name from it's parent + // For example: form can't have a child of the name form + each(split('a form meter progress dfn'), function (name) { + if (schema[name]) { + delete schema[name].children[name]; + } }); - }; - return { - nu: nu, - pure: pure - }; - } -); -define( - 'ephox.katamari.async.Bounce', + // Delete header, footer, sectioning and heading content descendants + /*each('dt th address', function(name) { + delete schema[name].children[name]; + });*/ - [ - 'global!Array', - 'global!setTimeout' - ], + // Caption can't have tables + delete schema.caption.children.table; - function (Array, setTimeout) { + // Delete scripts by default due to possible XSS + delete schema.script; - var bounce = function(f) { - return function() { - var args = Array.prototype.slice.call(arguments); - var me = this; - setTimeout(function() { - f.apply(me, args); - }, 0); - }; - }; + // TODO: LI:s can only have value if parent is OL - return { - bounce: bounce - }; - } -); + // TODO: Handle transparent elements + // a ins del canvas map -define( - 'ephox.katamari.api.Future', + mapCache[type] = schema; - [ - 'ephox.katamari.api.LazyValue', - 'ephox.katamari.async.Bounce' - ], + return schema; + } - /** A future value that is evaluated on demand. The base function is re-evaluated each time 'get' is called. */ - function (LazyValue, Bounce) { - var nu = function (baseFn) { - var get = function(callback) { - baseFn(Bounce.bounce(callback)); - }; + function compileElementMap(value, mode) { + var styles; - /** map :: this Future a -> (a -> b) -> Future b */ - var map = function (fab) { - return nu(function (callback) { - get(function (a) { - var value = fab(a); - callback(value); - }); - }); - }; + if (value) { + styles = {}; - /** bind :: this Future a -> (a -> Future b) -> Future b */ - var bind = function (aFutureB) { - return nu(function (callback) { - get(function (a) { - aFutureB(a).get(callback); - }); - }); - }; + if (typeof value == 'string') { + value = { + '*': value + }; + } - /** anonBind :: this Future a -> Future b -> Future b - * Returns a future, which evaluates the first future, ignores the result, then evaluates the second. - */ - var anonBind = function (futureB) { - return nu(function (callback) { - get(function (a) { - futureB.get(callback); - }); + // Convert styles into a rule list + each(value, function (value, key) { + styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/); }); - }; - - var toLazy = function () { - return LazyValue.nu(get); - }; - - return { - map: map, - bind: bind, - anonBind: anonBind, - toLazy: toLazy, - get: get - }; - - }; - - /** a -> Future a */ - var pure = function (a) { - return nu(function (callback) { - callback(a); - }); - }; - - return { - nu: nu, - pure: pure - }; - } -); - -define( - 'ephox.katamari.async.AsyncValues', + } - [ - 'ephox.katamari.api.Arr' - ], + return styles; + } - function (Arr) { - /* - * NOTE: an `asyncValue` must have a `get` function which gets given a callback and calls - * that callback with a value once it is ready + /** + * Constructs a new Schema instance. * - * e.g - * { - * get: function (callback) { callback(10); } - * } + * @constructor + * @method Schema + * @param {Object} settings Name/value settings object. */ - var par = function (asyncValues, nu) { - return nu(function(callback) { - var r = []; - var count = 0; + return function (settings) { + var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems; + var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses; + var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap; + var customElementsMap = {}, specialElements = {}; - var cb = function(i) { - return function(value) { - r[i] = value; - count++; - if (count >= asyncValues.length) { - callback(r); - } - }; - }; + // Creates an lookup table map object for the specified option or the default value + function createLookupTable(option, defaultValue, extendWith) { + var value = settings[option]; - if (asyncValues.length === 0) { - callback([]); + if (!value) { + // Get cached default map or make it if needed + value = mapCache[option]; + + if (!value) { + value = makeMap(defaultValue, ' ', makeMap(defaultValue.toUpperCase(), ' ')); + value = extend(value, extendWith); + + mapCache[option] = value; + } } else { - Arr.each(asyncValues, function(asyncValue, i) { - asyncValue.get(cb(i)); - }); + // Create custom map + value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); } - }); - }; - - return { - par: par - }; - } -); -define( - 'ephox.katamari.api.Futures', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Future', - 'ephox.katamari.async.AsyncValues' - ], + return value; + } - function (Arr, Future, AsyncValues) { - /** par :: [Future a] -> Future [a] */ - var par = function(futures) { - return AsyncValues.par(futures, Future.nu); - }; + settings = settings || {}; + schemaItems = compileSchema(settings.schema); - /** mapM :: [a] -> (a -> Future b) -> Future [b] */ - var mapM = function(array, fn) { - var futures = Arr.map(array, fn); - return par(futures); - }; + // Allow all elements and attributes if verify_html is set to false + if (settings.verify_html === false) { + settings.valid_elements = '*[*]'; + } - /** Kleisli composition of two functions: a -> Future b. - * Note the order of arguments: g is invoked first, then the result passed to f. - * This is in line with f . g = \x -> f (g a) - * - * compose :: ((b -> Future c), (a -> Future b)) -> a -> Future c - */ - var compose = function (f, g) { - return function (a) { - return g(a).bind(f); - }; - }; + validStyles = compileElementMap(settings.valid_styles); + invalidStyles = compileElementMap(settings.invalid_styles, 'map'); + validClasses = compileElementMap(settings.valid_classes, 'map'); - return { - par: par, - mapM: mapM, - compose: compose - }; - } -); -define( - 'ephox.katamari.api.Result', + // Setup map objects + whiteSpaceElementsMap = createLookupTable( + 'whitespace_elements', + 'pre script noscript style textarea video audio iframe object code' + ); + selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); + shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + + 'meta param embed source wbr track'); + boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + + 'noshade nowrap readonly selected autoplay loop controls'); + nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object ' + + 'script pre code', shortEndedElementsMap); + moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap); + textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + + 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); + blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + + 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + + 'datalist select optgroup figcaption', textBlockElementsMap); + textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' + + 'dfn code mark q sup sub samp'); - [ - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option' - ], + each((settings.special || 'script noscript noframes noembed title style textarea xmp').split(' '), function (name) { + specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi'); + }); - function (Fun, Option) { - /* The type signatures for Result - * is :: this Result a -> a -> Bool - * or :: this Result a -> Result a -> Result a - * orThunk :: this Result a -> (_ -> Result a) -> Result a - * map :: this Result a -> (a -> b) -> Result b - * each :: this Result a -> (a -> _) -> _ - * bind :: this Result a -> (a -> Result b) -> Result b - * fold :: this Result a -> (_ -> b, a -> b) -> b - * exists :: this Result a -> (a -> Bool) -> Bool - * forall :: this Result a -> (a -> Bool) -> Bool - * toOption :: this Result a -> Option a - * isValue :: this Result a -> Bool - * isError :: this Result a -> Bool - * getOr :: this Result a -> a -> a - * getOrThunk :: this Result a -> (_ -> a) -> a - * getOrDie :: this Result a -> a (or throws error) - */ + // Converts a wildcard expression string to a regexp for example *a will become /.*a/. + function patternToRegExp(str) { + return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); + } - var value = function (o) { - var is = function (v) { - return o === v; - }; + // Parses the specified valid_elements string and adds to the current rules + // This function is a bit hard to read since it's heavily optimized for speed + function addValidElements(validElements) { + var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, + prefix, outputName, globalAttributes, globalAttributesOrder, key, value, + elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/, + attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, + hasPatternsRegExp = /[*?+]/; - var or = function (opt) { - return value(o); - }; + if (validElements) { + // Split valid elements into an array with rules + validElements = split(validElements, ','); - var orThunk = function (f) { - return value(o); - }; + if (elements['@']) { + globalAttributes = elements['@'].attributes; + globalAttributesOrder = elements['@'].attributesOrder; + } - var map = function (f) { - return value(f(o)); - }; + // Loop all rules + for (ei = 0, el = validElements.length; ei < el; ei++) { + // Parse element rule + matches = elementRuleRegExp.exec(validElements[ei]); + if (matches) { + // Setup local names for matches + prefix = matches[1]; + elementName = matches[2]; + outputName = matches[3]; + attrData = matches[5]; - var each = function (f) { - f(o); - }; + // Create new attributes and attributesOrder + attributes = {}; + attributesOrder = []; - var bind = function (f) { - return f(o); - }; + // Create the new element + element = { + attributes: attributes, + attributesOrder: attributesOrder + }; - var fold = function (_, onValue) { - return onValue(o); - }; + // Padd empty elements prefix + if (prefix === '#') { + element.paddEmpty = true; + } - var exists = function (f) { - return f(o); - }; + // Remove empty elements prefix + if (prefix === '-') { + element.removeEmpty = true; + } - var forall = function (f) { - return f(o); - }; + if (matches[4] === '!') { + element.removeEmptyAttrs = true; + } - var toOption = function () { - return Option.some(o); - }; - - return { - is: is, - isValue: Fun.constant(true), - isError: Fun.constant(false), - getOr: Fun.constant(o), - getOrThunk: Fun.constant(o), - getOrDie: Fun.constant(o), - or: or, - orThunk: orThunk, - fold: fold, - map: map, - each: each, - bind: bind, - exists: exists, - forall: forall, - toOption: toOption - }; - }; + // Copy attributes from global rule into current rule + if (globalAttributes) { + for (key in globalAttributes) { + attributes[key] = globalAttributes[key]; + } - var error = function (message) { - var getOrThunk = function (f) { - return f(); - }; + attributesOrder.push.apply(attributesOrder, globalAttributesOrder); + } - var getOrDie = function () { - return Fun.die(message)(); - }; + // Attributes defined + if (attrData) { + attrData = split(attrData, '|'); + for (ai = 0, al = attrData.length; ai < al; ai++) { + matches = attrRuleRegExp.exec(attrData[ai]); + if (matches) { + attr = {}; + attrType = matches[1]; + attrName = matches[2].replace(/::/g, ':'); + prefix = matches[3]; + value = matches[4]; - var or = function (opt) { - return opt; - }; + // Required + if (attrType === '!') { + element.attributesRequired = element.attributesRequired || []; + element.attributesRequired.push(attrName); + attr.required = true; + } - var orThunk = function (f) { - return f(); - }; + // Denied from global + if (attrType === '-') { + delete attributes[attrName]; + attributesOrder.splice(inArray(attributesOrder, attrName), 1); + continue; + } - var map = function (f) { - return error(message); - }; + // Default value + if (prefix) { + // Default value + if (prefix === '=') { + element.attributesDefault = element.attributesDefault || []; + element.attributesDefault.push({ name: attrName, value: value }); + attr.defaultValue = value; + } - var bind = function (f) { - return error(message); - }; + // Forced value + if (prefix === ':') { + element.attributesForced = element.attributesForced || []; + element.attributesForced.push({ name: attrName, value: value }); + attr.forcedValue = value; + } - var fold = function (onError, _) { - return onError(message); - }; + // Required values + if (prefix === '<') { + attr.validValues = makeMap(value, '?'); + } + } - return { - is: Fun.constant(false), - isValue: Fun.constant(false), - isError: Fun.constant(true), - getOr: Fun.identity, - getOrThunk: getOrThunk, - getOrDie: getOrDie, - or: or, - orThunk: orThunk, - fold: fold, - map: map, - each: Fun.noop, - bind: bind, - exists: Fun.constant(false), - forall: Fun.constant(true), - toOption: Option.none - }; - }; + // Check for attribute patterns + if (hasPatternsRegExp.test(attrName)) { + element.attributePatterns = element.attributePatterns || []; + attr.pattern = patternToRegExp(attrName); + element.attributePatterns.push(attr); + } else { + // Add attribute to order list if it doesn't already exist + if (!attributes[attrName]) { + attributesOrder.push(attrName); + } - return { - value: value, - error: error - }; - } -); + attributes[attrName] = attr; + } + } + } + } -/** - * StyleSheetLoader.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 - */ + // Global rule, store away these for later usage + if (!globalAttributes && elementName == '@') { + globalAttributes = attributes; + globalAttributesOrder = attributesOrder; + } -/** - * This class handles loading of external stylesheets and fires events when these are loaded. - * - * @class tinymce.dom.StyleSheetLoader - * @private - */ -define( - 'tinymce.core.dom.StyleSheetLoader', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Future', - 'ephox.katamari.api.Futures', - 'ephox.katamari.api.Result', - 'tinymce.core.util.Delay', - 'tinymce.core.util.Tools' - ], - function (Arr, Fun, Future, Futures, Result, Delay, Tools) { - "use strict"; + // Handle substitute elements such as b/strong + if (outputName) { + element.outputName = elementName; + elements[outputName] = element; + } - return function (document, settings) { - var idCount = 0, loadedStates = {}, maxLoadTime; + // Add pattern or exact element + if (hasPatternsRegExp.test(elementName)) { + element.pattern = patternToRegExp(elementName); + patternElements.push(element); + } else { + elements[elementName] = element; + } + } + } + } + } - settings = settings || {}; - maxLoadTime = settings.maxLoadTime || 5000; + function setValidElements(validElements) { + elements = {}; + patternElements = []; - function appendToHead(node) { - document.getElementsByTagName('head')[0].appendChild(node); - } + addValidElements(validElements); - /** - * Loads the specified css style sheet file and call the loadedCallback once it's finished loading. - * - * @method load - * @param {String} url Url to be loaded. - * @param {Function} loadedCallback Callback to be executed when loaded. - * @param {Function} errorCallback Callback to be executed when failed loading. - */ - function load(url, loadedCallback, errorCallback) { - var link, style, startTime, state; + each(schemaItems, function (element, name) { + children[name] = element.children; + }); + } - function passed() { - var callbacks = state.passed, i = callbacks.length; + // Adds custom non HTML elements to the schema + function addCustomElements(customElements) { + var customElementRegExp = /^(~)?(.+)$/; - while (i--) { - callbacks[i](); - } + if (customElements) { + // Flush cached items since we are altering the default maps + mapCache.text_block_elements = mapCache.block_elements = null; - state.status = 2; - state.passed = []; - state.failed = []; - } + each(split(customElements, ','), function (rule) { + var matches = customElementRegExp.exec(rule), + inline = matches[1] === '~', + cloneName = inline ? 'span' : 'div', + name = matches[2]; - function failed() { - var callbacks = state.failed, i = callbacks.length; + children[name] = children[cloneName]; + customElementsMap[name] = cloneName; - while (i--) { - callbacks[i](); - } + // If it's not marked as inline then add it to valid block elements + if (!inline) { + blockElementsMap[name.toUpperCase()] = {}; + blockElementsMap[name] = {}; + } - state.status = 3; - state.passed = []; - state.failed = []; - } + // Add elements clone if needed + if (!elements[name]) { + var customRule = elements[cloneName]; - // Sniffs for older WebKit versions that have the link.onload but a broken one - function isOldWebKit() { - var webKitChunks = navigator.userAgent.match(/WebKit\/(\d*)/); - return !!(webKitChunks && webKitChunks[1] < 536); - } + customRule = extend({}, customRule); + delete customRule.removeEmptyAttrs; + delete customRule.removeEmpty; - // Calls the waitCallback until the test returns true or the timeout occurs - function wait(testCallback, waitCallback) { - if (!testCallback()) { - // Wait for timeout - if ((new Date().getTime()) - startTime < maxLoadTime) { - Delay.setTimeout(waitCallback); - } else { - failed(); + elements[name] = customRule; } - } - } - // Workaround for WebKit that doesn't properly support the onload event for link elements - // Or WebKit that fires the onload event before the StyleSheet is added to the document - function waitForWebKitLinkLoaded() { - wait(function () { - var styleSheets = document.styleSheets, styleSheet, i = styleSheets.length, owner; - - while (i--) { - styleSheet = styleSheets[i]; - owner = styleSheet.ownerNode ? styleSheet.ownerNode : styleSheet.owningElement; - if (owner && owner.id === link.id) { - passed(); - return true; + // Add custom elements at span/div positions + each(children, function (element, elmName) { + if (element[cloneName]) { + children[elmName] = element = extend({}, children[elmName]); + element[name] = element[cloneName]; } - } - }, waitForWebKitLinkLoaded); + }); + }); } + } - // Workaround for older Geckos that doesn't have any onload event for StyleSheets - function waitForGeckoLinkLoaded() { - wait(function () { - try { - // Accessing the cssRules will throw an exception until the CSS file is loaded - var cssRules = style.sheet.cssRules; - passed(); - return !!cssRules; - } catch (ex) { - // Ignore - } - }, waitForGeckoLinkLoaded); - } + // Adds valid children to the schema object + function addValidChildren(validChildren) { + var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; - url = Tools._addCacheSuffix(url); + // Invalidate the schema cache if the schema is mutated + mapCache[settings.schema] = null; - if (!loadedStates[url]) { - state = { - passed: [], - failed: [] - }; + if (validChildren) { + each(split(validChildren, ','), function (rule) { + var matches = childRuleRegExp.exec(rule), parent, prefix; - loadedStates[url] = state; - } else { - state = loadedStates[url]; - } + if (matches) { + prefix = matches[1]; - if (loadedCallback) { - state.passed.push(loadedCallback); - } + // Add/remove items from default + if (prefix) { + parent = children[matches[2]]; + } else { + parent = children[matches[2]] = { '#comment': {} }; + } - if (errorCallback) { - state.failed.push(errorCallback); - } + parent = children[matches[2]]; - // Is loading wait for it to pass - if (state.status == 1) { - return; + each(split(matches[3], '|'), function (child) { + if (prefix === '-') { + delete parent[child]; + } else { + parent[child] = {}; + } + }); + } + }); } + } - // Has finished loading and was success - if (state.status == 2) { - passed(); - return; - } + function getElementRule(name) { + var element = elements[name], i; - // Has finished loading and was a failure - if (state.status == 3) { - failed(); - return; + // Exact match found + if (element) { + return element; } - // Start loading - state.status = 1; - link = document.createElement('link'); - link.rel = 'stylesheet'; - link.type = 'text/css'; - link.id = 'u' + (idCount++); - link.async = false; - link.defer = false; - startTime = new Date().getTime(); + // No exact match then try the patterns + i = patternElements.length; + while (i--) { + element = patternElements[i]; - // Feature detect onload on link element and sniff older webkits since it has an broken onload event - if ("onload" in link && !isOldWebKit()) { - link.onload = waitForWebKitLinkLoaded; - link.onerror = failed; - } else { - // Sniff for old Firefox that doesn't support the onload event on link elements - // TODO: Remove this in the future when everyone uses modern browsers - if (navigator.userAgent.indexOf("Firefox") > 0) { - style = document.createElement('style'); - style.textContent = '@import "' + url + '"'; - waitForGeckoLinkLoaded(); - appendToHead(style); - return; + if (element.pattern.test(name)) { + return element; } - - // Use the id owner on older webkits - waitForWebKitLinkLoaded(); } + } - appendToHead(link); - link.href = url; - } + if (!settings.valid_elements) { + // No valid elements defined then clone the elements from the schema spec + each(schemaItems, function (element, name) { + elements[name] = { + attributes: element.attributes, + attributesOrder: element.attributesOrder + }; - var loadF = function (url) { - return Future.nu(function (resolve) { - load( - url, - Fun.compose(resolve, Fun.constant(Result.value(url))), - Fun.compose(resolve, Fun.constant(Result.error(url))) - ); + children[name] = element.children; }); - }; - - var unbox = function (result) { - return result.fold(Fun.identity, Fun.identity); - }; - var loadAll = function (urls, success, failure) { - Futures.par(Arr.map(urls, loadF)).get(function (result) { - var parts = Arr.partition(result, function (r) { - return r.isValue(); + // Switch these on HTML4 + if (settings.schema != "html5") { + each(split('strong/b em/i'), function (item) { + item = split(item, '/'); + elements[item[1]].outputName = item[0]; }); + } - if (parts.fail.length > 0) { - failure(parts.fail.map(unbox)); - } else { - success(parts.pass.map(unbox)); + // Add default alt attribute for images, removed since alt="" is treated as presentational. + // elements.img.attributesDefault = [{name: 'alt', value: ''}]; + + // Remove these if they are empty by default + each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function (name) { + if (elements[name]) { + elements[name].removeEmpty = true; } }); - }; - return { - load: load, - loadAll: loadAll - }; - }; - } -); + // Padd these by default + each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function (name) { + elements[name].paddEmpty = true; + }); -/** - * TreeWalker.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 - */ + // Remove these if they have no attributes + each(split('span'), function (name) { + elements[name].removeEmptyAttrs = true; + }); -/** - * TreeWalker class enables you to walk the DOM in a linear manner. - * - * @class tinymce.dom.TreeWalker - * @example - * var walker = new tinymce.dom.TreeWalker(startNode); - * - * do { - * console.log(walker.current()); - * } while (walker.next()); - */ -define( - 'tinymce.core.dom.TreeWalker', - [ - ], - function () { - /** - * Constructs a new TreeWalker instance. - * - * @constructor - * @method TreeWalker - * @param {Node} startNode Node to start walking from. - * @param {node} rootNode Optional root node to never walk out of. - */ - return function (startNode, rootNode) { - var node = startNode; + // Remove these by default + // TODO: Reenable in 4.1 + /*each(split('script style'), function(name) { + delete elements[name]; + });*/ + } else { + setValidElements(settings.valid_elements); + } - function findSibling(node, startName, siblingName, shallow) { - var sibling, parent; + addCustomElements(settings.custom_elements); + addValidChildren(settings.valid_children); + addValidElements(settings.extended_valid_elements); - if (node) { - // Walk into nodes if it has a start - if (!shallow && node[startName]) { - return node[startName]; - } + // Todo: Remove this when we fix list handling to be valid + addValidChildren('+ol[ul|ol],+ul[ul|ol]'); - // Return the sibling if it has one - if (node != rootNode) { - sibling = node[siblingName]; - if (sibling) { - return sibling; - } - // Walk up the parents to look for siblings - for (parent = node.parentNode; parent && parent != rootNode; parent = parent.parentNode) { - sibling = parent[siblingName]; - if (sibling) { - return sibling; - } - } - } + // Some elements are not valid by themselves - require parents + each({ + dd: 'dl', + dt: 'dl', + li: 'ul ol', + td: 'tr', + th: 'tr', + tr: 'tbody thead tfoot', + tbody: 'table', + thead: 'table', + tfoot: 'table', + legend: 'fieldset', + area: 'map', + param: 'video audio object' + }, function (parents, item) { + if (elements[item]) { + elements[item].parentsRequired = split(parents); } - } - - function findPreviousNode(node, startName, siblingName, shallow) { - var sibling, parent, child; - - if (node) { - sibling = node[siblingName]; - if (rootNode && sibling === rootNode) { - return; - } + }); - if (sibling) { - if (!shallow) { - // Walk up the parents to look for siblings - for (child = sibling[startName]; child; child = child[startName]) { - if (!child[startName]) { - return child; - } - } - } - return sibling; + // Delete invalid elements + if (settings.invalid_elements) { + each(explode(settings.invalid_elements), function (item) { + if (elements[item]) { + delete elements[item]; } + }); + } - parent = node.parentNode; - if (parent && parent !== rootNode) { - return parent; - } - } + // If the user didn't allow span only allow internal spans + if (!getElementRule('span')) { + addValidElements('span[!data-mce-type|*]'); } /** - * Returns the current node. + * Name/value map object with valid parents and children to those parents. * - * @method current - * @return {Node} Current node where the walker is. + * @example + * children = { + * div:{p:{}, h1:{}} + * }; + * @field children + * @type Object */ - this.current = function () { - return node; - }; + self.children = children; /** - * Walks to the next node in tree. + * Name/value map object with valid styles for each element. * - * @method next - * @return {Node} Current node where the walker is after moving to the next node. + * @method getValidStyles + * @type Object */ - this.next = function (shallow) { - node = findSibling(node, 'firstChild', 'nextSibling', shallow); - return node; + self.getValidStyles = function () { + return validStyles; }; /** - * Walks to the previous node in tree. + * Name/value map object with valid styles for each element. * - * @method prev - * @return {Node} Current node where the walker is after moving to the previous node. + * @method getInvalidStyles + * @type Object */ - this.prev = function (shallow) { - node = findSibling(node, 'lastChild', 'previousSibling', shallow); - return node; + self.getInvalidStyles = function () { + return invalidStyles; }; - this.prev2 = function (shallow) { - node = findPreviousNode(node, 'lastChild', 'previousSibling', shallow); - return node; + /** + * Name/value map object with valid classes for each element. + * + * @method getValidClasses + * @type Object + */ + self.getValidClasses = function () { + return validClasses; }; - }; - } -); - -/** - * Entities.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 - */ - -/*jshint bitwise:false */ -/*eslint no-bitwise:0 */ - -/** - * Entity encoder class. - * - * @class tinymce.html.Entities - * @static - * @version 3.4 - */ -define( - 'tinymce.core.html.Entities', - [ - "tinymce.core.util.Tools" - ], - function (Tools) { - var makeMap = Tools.makeMap; - var namedEntities, baseEntities, reverseEntities, - attrsCharsRegExp = /[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g, - rawCharsRegExp = /[<>&\"\']/g, - entityRegExp = /&#([a-z0-9]+);?|&([a-z0-9]+);/gi, - asciiMap = { - 128: "\u20AC", 130: "\u201A", 131: "\u0192", 132: "\u201E", 133: "\u2026", 134: "\u2020", - 135: "\u2021", 136: "\u02C6", 137: "\u2030", 138: "\u0160", 139: "\u2039", 140: "\u0152", - 142: "\u017D", 145: "\u2018", 146: "\u2019", 147: "\u201C", 148: "\u201D", 149: "\u2022", - 150: "\u2013", 151: "\u2014", 152: "\u02DC", 153: "\u2122", 154: "\u0161", 155: "\u203A", - 156: "\u0153", 158: "\u017E", 159: "\u0178" + /** + * Returns a map with boolean attributes. + * + * @method getBoolAttrs + * @return {Object} Name/value lookup map for boolean attributes. + */ + self.getBoolAttrs = function () { + return boolAttrMap; }; - // Raw entities - baseEntities = { - '\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code - "'": ''', - '<': '<', - '>': '>', - '&': '&', - '\u0060': '`' - }; - - // Reverse lookup table for raw entities - reverseEntities = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - ''': "'" - }; - - // Decodes text by using the browser - function nativeDecode(text) { - var elm; - - elm = document.createElement("div"); - elm.innerHTML = text; - - return elm.textContent || elm.innerText || text; - } - - // Build a two way lookup table for the entities - function buildEntitiesLookup(items, radix) { - var i, chr, entity, lookup = {}; - - if (items) { - items = items.split(','); - radix = radix || 10; + /** + * Returns a map with block elements. + * + * @method getBlockElements + * @return {Object} Name/value lookup map for block elements. + */ + self.getBlockElements = function () { + return blockElementsMap; + }; - // Build entities lookup table - for (i = 0; i < items.length; i += 2) { - chr = String.fromCharCode(parseInt(items[i], radix)); + /** + * Returns a map with text block elements. Such as: p,h1-h6,div,address + * + * @method getTextBlockElements + * @return {Object} Name/value lookup map for block elements. + */ + self.getTextBlockElements = function () { + return textBlockElementsMap; + }; - // Only add non base entities - if (!baseEntities[chr]) { - entity = '&' + items[i + 1] + ';'; - lookup[chr] = entity; - lookup[entity] = chr; - } - } + /** + * Returns a map of inline text format nodes for example strong/span or ins. + * + * @method getTextInlineElements + * @return {Object} Name/value lookup map for text format elements. + */ + self.getTextInlineElements = function () { + return textInlineElementsMap; + }; - return lookup; - } - } + /** + * Returns a map with short ended elements such as BR or IMG. + * + * @method getShortEndedElements + * @return {Object} Name/value lookup map for short ended elements. + */ + self.getShortEndedElements = function () { + return shortEndedElementsMap; + }; - // Unpack entities lookup where the numbers are in radix 32 to reduce the size - namedEntities = buildEntitiesLookup( - '50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,' + - '5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,' + - '5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,' + - '5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,' + - '68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,' + - '6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,' + - '6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,' + - '75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,' + - '7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,' + - '7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,' + - 'sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,' + - 'st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,' + - 't9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,' + - 'tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,' + - 'u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,' + - '81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,' + - '8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,' + - '8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,' + - '8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,' + - '8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,' + - 'nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,' + - 'rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,' + - 'Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,' + - '80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,' + - '811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro', 32); + /** + * Returns a map with self closing tags such as
  • . + * + * @method getSelfClosingElements + * @return {Object} Name/value lookup map for self closing tags elements. + */ + self.getSelfClosingElements = function () { + return selfClosingElementsMap; + }; - var Entities = { /** - * Encodes the specified string using raw entities. This means only the required XML base entities will be encoded. + * Returns a map with elements that should be treated as contents regardless if it has text + * content in them or not such as TD, VIDEO or IMG. * - * @method encodeRaw - * @param {String} text Text to encode. - * @param {Boolean} attr Optional flag to specify if the text is attribute contents. - * @return {String} Entity encoded text. + * @method getNonEmptyElements + * @return {Object} Name/value lookup map for non empty elements. */ - encodeRaw: function (text, attr) { - return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { - return baseEntities[chr] || chr; - }); - }, + self.getNonEmptyElements = function () { + return nonEmptyElementsMap; + }; /** - * Encoded the specified text with both the attributes and text entities. This function will produce larger text contents - * since it doesn't know if the context is within a attribute or text node. This was added for compatibility - * and is exposed as the DOMUtils.encode function. + * Returns a map with elements that the caret should be moved in front of after enter is + * pressed * - * @method encodeAllRaw - * @param {String} text Text to encode. - * @return {String} Entity encoded text. + * @method getMoveCaretBeforeOnEnterElements + * @return {Object} Name/value lookup map for elements to place the caret in front of. */ - encodeAllRaw: function (text) { - return ('' + text).replace(rawCharsRegExp, function (chr) { - return baseEntities[chr] || chr; - }); - }, + self.getMoveCaretBeforeOnEnterElements = function () { + return moveCaretBeforeOnEnterElementsMap; + }; /** - * Encodes the specified string using numeric entities. The core entities will be - * encoded as named ones but all non lower ascii characters will be encoded into numeric entities. + * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. * - * @method encodeNumeric - * @param {String} text Text to encode. - * @param {Boolean} attr Optional flag to specify if the text is attribute contents. - * @return {String} Entity encoded text. + * @method getWhiteSpaceElements + * @return {Object} Name/value lookup map for white space elements. */ - encodeNumeric: function (text, attr) { - return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { - // Multi byte sequence convert it to a single entity - if (chr.length > 1) { - return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; - } + self.getWhiteSpaceElements = function () { + return whiteSpaceElementsMap; + }; - return baseEntities[chr] || '&#' + chr.charCodeAt(0) + ';'; - }); - }, + /** + * Returns a map with special elements. These are elements that needs to be parsed + * in a special way such as script, style, textarea etc. The map object values + * are regexps used to find the end of the element. + * + * @method getSpecialElements + * @return {Object} Name/value lookup map for special elements. + */ + self.getSpecialElements = function () { + return specialElements; + }; /** - * Encodes the specified string using named entities. The core entities will be encoded - * as named ones but all non lower ascii characters will be encoded into named entities. + * Returns true/false if the specified element and it's child is valid or not + * according to the schema. * - * @method encodeNamed - * @param {String} text Text to encode. - * @param {Boolean} attr Optional flag to specify if the text is attribute contents. - * @param {Object} entities Optional parameter with entities to use. - * @return {String} Entity encoded text. + * @method isValidChild + * @param {String} name Element name to check for. + * @param {String} child Element child to verify. + * @return {Boolean} True/false if the element is a valid child of the specified parent. */ - encodeNamed: function (text, attr, entities) { - entities = entities || namedEntities; + self.isValidChild = function (name, child) { + var parent = children[name.toLowerCase()]; - return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { - return baseEntities[chr] || entities[chr] || chr; - }); - }, + return !!(parent && parent[child.toLowerCase()]); + }; /** - * Returns an encode function based on the name(s) and it's optional entities. + * Returns true/false if the specified element name and optional attribute is + * valid according to the schema. * - * @method getEncodeFunc - * @param {String} name Comma separated list of encoders for example named,numeric. - * @param {String} entities Optional parameter with entities to use instead of the built in set. - * @return {function} Encode function to be used. + * @method isValid + * @param {String} name Name of element to check. + * @param {String} attr Optional attribute name to check for. + * @return {Boolean} True/false if the element and attribute is valid. */ - getEncodeFunc: function (name, entities) { - entities = buildEntitiesLookup(entities) || namedEntities; - - function encodeNamedAndNumeric(text, attr) { - return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function (chr) { - if (baseEntities[chr] !== undefined) { - return baseEntities[chr]; - } + self.isValid = function (name, attr) { + var attrPatterns, i, rule = getElementRule(name); - if (entities[chr] !== undefined) { - return entities[chr]; + // Check if it's a valid element + if (rule) { + if (attr) { + // Check if attribute name exists + if (rule.attributes[attr]) { + return true; } - // Convert multi-byte sequences to a single entity. - if (chr.length > 1) { - return '&#' + (((chr.charCodeAt(0) - 0xD800) * 0x400) + (chr.charCodeAt(1) - 0xDC00) + 0x10000) + ';'; + // Check if attribute matches a regexp pattern + attrPatterns = rule.attributePatterns; + if (attrPatterns) { + i = attrPatterns.length; + while (i--) { + if (attrPatterns[i].pattern.test(name)) { + return true; + } + } } - - return '&#' + chr.charCodeAt(0) + ';'; - }); - } - - function encodeCustomNamed(text, attr) { - return Entities.encodeNamed(text, attr, entities); - } - - // Replace + with , to be compatible with previous TinyMCE versions - name = makeMap(name.replace(/\+/g, ',')); - - // Named and numeric encoder - if (name.named && name.numeric) { - return encodeNamedAndNumeric; - } - - // Named encoder - if (name.named) { - // Custom names - if (entities) { - return encodeCustomNamed; + } else { + return true; } - - return Entities.encodeNamed; } - // Numeric - if (name.numeric) { - return Entities.encodeNumeric; - } + // No match + return false; + }; - // Raw encoder - return Entities.encodeRaw; - }, + /** + * Returns true/false if the specified element is valid or not + * according to the schema. + * + * @method getElementRule + * @param {String} name Element name to check for. + * @return {Object} Element object or undefined if the element isn't valid. + */ + self.getElementRule = getElementRule; /** - * Decodes the specified string, this will replace entities with raw UTF characters. + * Returns an map object of all custom elements. * - * @method decode - * @param {String} text Text to entity decode. - * @return {String} Entity decoded string. + * @method getCustomElements + * @return {Object} Name/value map object of all custom elements. */ - decode: function (text) { - return text.replace(entityRegExp, function (all, numeric) { - if (numeric) { - if (numeric.charAt(0).toLowerCase() === 'x') { - numeric = parseInt(numeric.substr(1), 16); - } else { - numeric = parseInt(numeric, 10); - } + self.getCustomElements = function () { + return customElementsMap; + }; - // Support upper UTF - if (numeric > 0xFFFF) { - numeric -= 0x10000; + /** + * Parses a valid elements string and adds it to the schema. The valid elements + * format is for example "element[attr=default|otherattr]". + * Existing rules will be replaced with the ones specified, so this extends the schema. + * + * @method addValidElements + * @param {String} valid_elements String in the valid elements format to be parsed. + */ + self.addValidElements = addValidElements; - return String.fromCharCode(0xD800 + (numeric >> 10), 0xDC00 + (numeric & 0x3FF)); - } + /** + * Parses a valid elements string and sets it to the schema. The valid elements + * format is for example "element[attr=default|otherattr]". + * Existing rules will be replaced with the ones specified, so this extends the schema. + * + * @method setValidElements + * @param {String} valid_elements String in the valid elements format to be parsed. + */ + self.setValidElements = setValidElements; - return asciiMap[numeric] || String.fromCharCode(numeric); - } + /** + * Adds custom non HTML elements to the schema. + * + * @method addCustomElements + * @param {String} custom_elements Comma separated list of custom elements to add. + */ + self.addCustomElements = addCustomElements; - return reverseEntities[all] || namedEntities[all] || nativeDecode(all); - }); - } - }; + /** + * Parses a valid children string and adds them to the schema structure. The valid children + * format is for example: "element[child1|child2]". + * + * @method addValidChildren + * @param {String} valid_children Valid children elements string to parse + */ + self.addValidChildren = addValidChildren; - return Entities; + self.elements = elements; + }; } ); /** - * Schema.js + * Styles.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -7976,3279 +8429,3341 @@ define( */ /** - * Schema validator class. + * This class is used to parse CSS styles it also compresses styles to reduce the output size. * - * @class tinymce.html.Schema * @example - * if (tinymce.activeEditor.schema.isValidChild('p', 'span')) - * alert('span is valid child of p.'); + * var Styles = new tinymce.html.Styles({ + * url_converter: function(url) { + * return url; + * } + * }); * - * if (tinymce.activeEditor.schema.getElementRule('p')) - * alert('P is a valid element.'); + * styles = Styles.parse('border: 1px solid red'); + * styles.color = 'red'; * - * @class tinymce.html.Schema + * console.log(new tinymce.html.StyleSerializer().serialize(styles)); + * + * @class tinymce.html.Styles * @version 3.4 */ define( - 'tinymce.core.html.Schema', + 'tinymce.core.html.Styles', [ - "tinymce.core.util.Tools" ], - function (Tools) { - var mapCache = {}, dummyObj = {}; - var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray; + function () { + return function (settings, schema) { + /*jshint maxlen:255 */ + /*eslint max-len:0 */ + var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, + urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, + styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, + trimRightRegExp = /\s+$/, + i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF'; - function split(items, delim) { - items = Tools.trim(items); - return items ? items.split(delim || ' ') : []; - } + settings = settings || {}; - /** - * Builds a schema lookup table - * - * @private - * @param {String} type html4, html5 or html5-strict schema type. - * @return {Object} Schema lookup table. - */ - function compileSchema(type) { - var schema = {}, globalAttributes, blockContent; - var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent; + if (schema) { + validStyles = schema.getValidStyles(); + invalidStyles = schema.getInvalidStyles(); + } - function add(name, attributes, children) { - var ni, attributesOrder, element; + encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); + for (i = 0; i < encodingItems.length; i++) { + encodingLookup[encodingItems[i]] = invisibleChar + i; + encodingLookup[invisibleChar + i] = encodingItems[i]; + } - function arrayToMap(array, obj) { - var map = {}, i, l; + function toHex(match, r, g, b) { + function hex(val) { + val = parseInt(val, 10).toString(16); - for (i = 0, l = array.length; i < l; i++) { - map[array[i]] = obj || {}; + return val.length > 1 ? val : '0' + val; // 0 -> 00 + } + + return '#' + hex(r) + hex(g) + hex(b); + } + + return { + /** + * Parses the specified RGB color value and returns a hex version of that color. + * + * @method toHex + * @param {String} color RGB string value like rgb(1,2,3) + * @return {String} Hex version of that RGB value like #FF00FF. + */ + toHex: function (color) { + return color.replace(rgbRegExp, toHex); + }, + + /** + * Parses the specified style value into an object collection. This parser will also + * merge and remove any redundant items that browsers might have added. It will also convert non hex + * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. + * + * @method parse + * @param {String} css Style value to parse for example: border:1px solid red;. + * @return {Object} Object representation of that style like {border: '1px solid red'} + */ + parse: function (css) { + var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter; + var urlConverterScope = settings.url_converter_scope || this; + + function compress(prefix, suffix, noJoin) { + var top, right, bottom, left; + + top = styles[prefix + '-top' + suffix]; + if (!top) { + return; + } + + right = styles[prefix + '-right' + suffix]; + if (!right) { + return; + } + + bottom = styles[prefix + '-bottom' + suffix]; + if (!bottom) { + return; + } + + left = styles[prefix + '-left' + suffix]; + if (!left) { + return; + } + + var box = [top, right, bottom, left]; + i = box.length - 1; + while (i--) { + if (box[i] !== box[i + 1]) { + break; + } + } + + if (i > -1 && noJoin) { + return; + } + + styles[prefix + suffix] = i == -1 ? box[0] : box.join(' '); + delete styles[prefix + '-top' + suffix]; + delete styles[prefix + '-right' + suffix]; + delete styles[prefix + '-bottom' + suffix]; + delete styles[prefix + '-left' + suffix]; } - return map; - } + /** + * Checks if the specific style can be compressed in other words if all border-width are equal. + */ + function canCompress(key) { + var value = styles[key], i; - children = children || []; - attributes = attributes || ""; + if (!value) { + return; + } - if (typeof children === "string") { - children = split(children); - } + value = value.split(' '); + i = value.length; + while (i--) { + if (value[i] !== value[0]) { + return false; + } + } - name = split(name); - ni = name.length; - while (ni--) { - attributesOrder = split([globalAttributes, attributes].join(' ')); + styles[key] = value[0]; - element = { - attributes: arrayToMap(attributesOrder), - attributesOrder: attributesOrder, - children: arrayToMap(children, dummyObj) - }; + return true; + } - schema[name[ni]] = element; - } - } + /** + * Compresses multiple styles into one style. + */ + function compress2(target, a, b, c) { + if (!canCompress(a)) { + return; + } - function addAttrs(name, attributes) { - var ni, schemaItem, i, l; + if (!canCompress(b)) { + return; + } - name = split(name); - ni = name.length; - attributes = split(attributes); - while (ni--) { - schemaItem = schema[name[ni]]; - for (i = 0, l = attributes.length; i < l; i++) { - schemaItem.attributes[attributes[i]] = {}; - schemaItem.attributesOrder.push(attributes[i]); + if (!canCompress(c)) { + return; + } + + // Compress + styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; + delete styles[a]; + delete styles[b]; + delete styles[c]; } - } - } - // Use cached schema - if (mapCache[type]) { - return mapCache[type]; - } + // Encodes the specified string by replacing all \" \' ; : with _ + function encode(str) { + isEncoded = true; - // Attributes present on all elements - globalAttributes = "id accesskey class dir lang style tabindex title role"; + return encodingLookup[str]; + } - // Event attributes can be opt-in/opt-out - /*eventAttributes = split("onabort onblur oncancel oncanplay oncanplaythrough onchange onclick onclose oncontextmenu oncuechange " + - "ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended " + - "onerror onfocus oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart " + - "onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay onplaying onprogress onratechange " + - "onreset onscroll onseeked onseeking onseeking onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange " + - "onwaiting" - );*/ + // Decodes the specified string by replacing all _ with it's original value \" \' etc + // It will also decode the \" \' if keepSlashes is set to fale or omitted + function decode(str, keepSlashes) { + if (isEncoded) { + str = str.replace(/\uFEFF[0-9]/g, function (str) { + return encodingLookup[str]; + }); + } - // Block content elements - blockContent = - "address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul"; + if (!keepSlashes) { + str = str.replace(/\\([\'\";:])/g, "$1"); + } - // Phrasing content elements from the HTML5 spec (inline) - phrasingContent = - "a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd " + - "label map noscript object q s samp script select small span strong sub sup " + - "textarea u var #text #comment" - ; + return str; + } - // Add HTML5 items to globalAttributes, blockContent, phrasingContent - if (type != "html4") { - globalAttributes += " contenteditable contextmenu draggable dropzone " + - "hidden spellcheck translate"; - blockContent += " article aside details dialog figure header footer hgroup section nav"; - phrasingContent += " audio canvas command datalist mark meter output picture " + - "progress time wbr video ruby bdi keygen"; - } + function decodeSingleHexSequence(escSeq) { + return String.fromCharCode(parseInt(escSeq.slice(1), 16)); + } - // Add HTML4 elements unless it's html5-strict - if (type != "html5-strict") { - globalAttributes += " xml:lang"; + function decodeHexSequences(value) { + return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence); + } - html4PhrasingContent = "acronym applet basefont big font strike tt"; - phrasingContent = [phrasingContent, html4PhrasingContent].join(' '); + function processUrl(match, url, url2, url3, str, str2) { + str = str || str2; - each(split(html4PhrasingContent), function (name) { - add(name, "", phrasingContent); - }); + if (str) { + str = decode(str); - html4BlockContent = "center dir isindex noframes"; - blockContent = [blockContent, html4BlockContent].join(' '); + // Force strings into single quote format + return "'" + str.replace(/\'/g, "\\'") + "'"; + } - // Flow content elements from the HTML5 spec (block+inline) - flowContent = [blockContent, phrasingContent].join(' '); + url = decode(url || url2 || url3); - each(split(html4BlockContent), function (name) { - add(name, "", flowContent); - }); - } + if (!settings.allow_script_urls) { + var scriptUrl = url.replace(/[\s\r\n]+/g, ''); - // Flow content elements from the HTML5 spec (block+inline) - flowContent = flowContent || [blockContent, phrasingContent].join(" "); + if (/(java|vb)script:/i.test(scriptUrl)) { + return ""; + } - // HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement - // Schema items , , - add("html", "manifest", "head body"); - add("head", "", "base command link meta noscript script style title"); - add("title hr noscript br"); - add("base", "href target"); - add("link", "href rel media hreflang type sizes hreflang"); - add("meta", "name http-equiv content charset"); - add("style", "media type scoped"); - add("script", "src async defer type charset"); - add("body", "onafterprint onbeforeprint onbeforeunload onblur onerror onfocus " + - "onhashchange onload onmessage onoffline ononline onpagehide onpageshow " + - "onpopstate onresize onscroll onstorage onunload", flowContent); - add("address dt dd div caption", "", flowContent); - add("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn", "", phrasingContent); - add("blockquote", "cite", flowContent); - add("ol", "reversed start type", "li"); - add("ul", "", "li"); - add("li", "value", flowContent); - add("dl", "", "dt dd"); - add("a", "href target rel media hreflang type", phrasingContent); - add("q", "cite", phrasingContent); - add("ins del", "cite datetime", flowContent); - add("img", "src sizes srcset alt usemap ismap width height"); - add("iframe", "src name width height", flowContent); - add("embed", "src type width height"); - add("object", "data type typemustmatch name usemap form width height", [flowContent, "param"].join(' ')); - add("param", "name value"); - add("map", "name", [flowContent, "area"].join(' ')); - add("area", "alt coords shape href target rel media hreflang type"); - add("table", "border", "caption colgroup thead tfoot tbody tr" + (type == "html4" ? " col" : "")); - add("colgroup", "span", "col"); - add("col", "span"); - add("tbody thead tfoot", "", "tr"); - add("tr", "", "td th"); - add("td", "colspan rowspan headers", flowContent); - add("th", "colspan rowspan headers scope abbr", flowContent); - add("form", "accept-charset action autocomplete enctype method name novalidate target", flowContent); - add("fieldset", "disabled form name", [flowContent, "legend"].join(' ')); - add("label", "form for", phrasingContent); - add("input", "accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate " + - "formtarget height list max maxlength min multiple name pattern readonly required size src step type value width" - ); - add("button", "disabled form formaction formenctype formmethod formnovalidate formtarget name type value", - type == "html4" ? flowContent : phrasingContent); - add("select", "disabled form multiple name required size", "option optgroup"); - add("optgroup", "disabled label", "option"); - add("option", "disabled label selected value"); - add("textarea", "cols dirname disabled form maxlength name readonly required rows wrap"); - add("menu", "type label", [flowContent, "li"].join(' ')); - add("noscript", "", flowContent); + if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { + return ""; + } + } - // Extend with HTML5 elements - if (type != "html4") { - add("wbr"); - add("ruby", "", [phrasingContent, "rt rp"].join(' ')); - add("figcaption", "", flowContent); - add("mark rt rp summary bdi", "", phrasingContent); - add("canvas", "width height", flowContent); - add("video", "src crossorigin poster preload autoplay mediagroup loop " + - "muted controls width height buffered", [flowContent, "track source"].join(' ')); - add("audio", "src crossorigin preload autoplay mediagroup loop muted controls " + - "buffered volume", [flowContent, "track source"].join(' ')); - add("picture", "", "img source"); - add("source", "src srcset type media sizes"); - add("track", "kind src srclang label default"); - add("datalist", "", [phrasingContent, "option"].join(' ')); - add("article section nav aside header footer", "", flowContent); - add("hgroup", "", "h1 h2 h3 h4 h5 h6"); - add("figure", "", [flowContent, "figcaption"].join(' ')); - add("time", "datetime", phrasingContent); - add("dialog", "open", flowContent); - add("command", "type label icon disabled checked radiogroup command"); - add("output", "for form name", phrasingContent); - add("progress", "value max", phrasingContent); - add("meter", "value min max low high optimum", phrasingContent); - add("details", "open", [flowContent, "summary"].join(' ')); - add("keygen", "autofocus challenge disabled form keytype name"); - } + // Convert the URL to relative/absolute depending on config + if (urlConverter) { + url = urlConverter.call(urlConverterScope, url, 'style'); + } - // Extend with HTML4 attributes unless it's html5-strict - if (type != "html5-strict") { - addAttrs("script", "language xml:space"); - addAttrs("style", "xml:space"); - addAttrs("object", "declare classid code codebase codetype archive standby align border hspace vspace"); - addAttrs("embed", "align name hspace vspace"); - addAttrs("param", "valuetype type"); - addAttrs("a", "charset name rev shape coords"); - addAttrs("br", "clear"); - addAttrs("applet", "codebase archive code object alt name width height align hspace vspace"); - addAttrs("img", "name longdesc align border hspace vspace"); - addAttrs("iframe", "longdesc frameborder marginwidth marginheight scrolling align"); - addAttrs("font basefont", "size color face"); - addAttrs("input", "usemap align"); - addAttrs("select", "onchange"); - addAttrs("textarea"); - addAttrs("h1 h2 h3 h4 h5 h6 div p legend caption", "align"); - addAttrs("ul", "type compact"); - addAttrs("li", "type"); - addAttrs("ol dl menu dir", "compact"); - addAttrs("pre", "width xml:space"); - addAttrs("hr", "align noshade size width"); - addAttrs("isindex", "prompt"); - addAttrs("table", "summary width frame rules cellspacing cellpadding align bgcolor"); - addAttrs("col", "width align char charoff valign"); - addAttrs("colgroup", "width align char charoff valign"); - addAttrs("thead", "align char charoff valign"); - addAttrs("tr", "align char charoff valign bgcolor"); - addAttrs("th", "axis align char charoff valign nowrap bgcolor width height"); - addAttrs("form", "accept"); - addAttrs("td", "abbr axis scope align char charoff valign nowrap bgcolor width height"); - addAttrs("tfoot", "align char charoff valign"); - addAttrs("tbody", "align char charoff valign"); - addAttrs("area", "nohref"); - addAttrs("body", "background bgcolor text link vlink alink"); - } + // Output new URL format + return "url('" + url.replace(/\'/g, "\\'") + "')"; + } - // Extend with HTML5 attributes unless it's html4 - if (type != "html4") { - addAttrs("input button select textarea", "autofocus"); - addAttrs("input textarea", "placeholder"); - addAttrs("a", "download"); - addAttrs("link script img", "crossorigin"); - addAttrs("iframe", "sandbox seamless allowfullscreen"); // Excluded: srcdoc - } + if (css) { + css = css.replace(/[\u0000-\u001F]/g, ''); - // Special: iframe, ruby, video, audio, label + // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing + css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function (str) { + return str.replace(/[;:]/g, encode); + }); - // Delete children of the same name from it's parent - // For example: form can't have a child of the name form - each(split('a form meter progress dfn'), function (name) { - if (schema[name]) { - delete schema[name].children[name]; - } - }); + // Parse styles + while ((matches = styleRegExp.exec(css))) { + styleRegExp.lastIndex = matches.index + matches[0].length; + name = matches[1].replace(trimRightRegExp, '').toLowerCase(); + value = matches[2].replace(trimRightRegExp, ''); - // Delete header, footer, sectioning and heading content descendants - /*each('dt th address', function(name) { - delete schema[name].children[name]; - });*/ + if (name && value) { + // Decode escaped sequences like \65 -> e + name = decodeHexSequences(name); + value = decodeHexSequences(value); - // Caption can't have tables - delete schema.caption.children.table; + // Skip properties with double quotes and sequences like \" \' in their names + // See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations' + // https://cure53.de/fp170.pdf + if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) { + continue; + } - // Delete scripts by default due to possible XSS - delete schema.script; + // Don't allow behavior name or expression/comments within the values + if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { + continue; + } - // TODO: LI:s can only have value if parent is OL + // Opera will produce 700 instead of bold in their style values + if (name === 'font-weight' && value === '700') { + value = 'bold'; + } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED + value = value.toLowerCase(); + } - // TODO: Handle transparent elements - // a ins del canvas map + // Convert RGB colors to HEX + value = value.replace(rgbRegExp, toHex); - mapCache[type] = schema; + // Convert URLs and force them into url('value') format + value = value.replace(urlOrStrRegExp, processUrl); + styles[name] = isEncoded ? decode(value, true) : value; + } + } + // Compress the styles to reduce it's size for example IE will expand styles + compress("border", "", true); + compress("border", "-width"); + compress("border", "-color"); + compress("border", "-style"); + compress("padding", ""); + compress("margin", ""); + compress2('border', 'border-width', 'border-style', 'border-color'); - return schema; - } + // Remove pointless border, IE produces these + if (styles.border === 'medium none') { + delete styles.border; + } - function compileElementMap(value, mode) { - var styles; + // IE 11 will produce a border-image: none when getting the style attribute from

    + // So let us assume it shouldn't be there + if (styles['border-image'] === 'none') { + delete styles['border-image']; + } + } - if (value) { - styles = {}; + return styles; + }, - if (typeof value == 'string') { - value = { - '*': value - }; - } + /** + * Serializes the specified style object into a string. + * + * @method serialize + * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} + * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized. + * @return {String} String representation of the style object for example: border: 1px solid red. + */ + serialize: function (styles, elementName) { + var css = '', name, value; - // Convert styles into a rule list - each(value, function (value, key) { - styles[key] = styles[key.toUpperCase()] = mode == 'map' ? makeMap(value, /[, ]/) : explode(value, /[, ]/); - }); - } + function serializeStyles(name) { + var styleList, i, l, value; - return styles; - } + styleList = validStyles[name]; + if (styleList) { + for (i = 0, l = styleList.length; i < l; i++) { + name = styleList[i]; + value = styles[name]; - /** - * Constructs a new Schema instance. - * - * @constructor - * @method Schema - * @param {Object} settings Name/value settings object. - */ - return function (settings) { - var self = this, elements = {}, children = {}, patternElements = [], validStyles, invalidStyles, schemaItems; - var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap, validClasses; - var blockElementsMap, nonEmptyElementsMap, moveCaretBeforeOnEnterElementsMap, textBlockElementsMap, textInlineElementsMap; - var customElementsMap = {}, specialElements = {}; + if (value) { + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } + } + } - // Creates an lookup table map object for the specified option or the default value - function createLookupTable(option, defaultValue, extendWith) { - var value = settings[option]; + function isValid(name, elementName) { + var styleMap; - if (!value) { - // Get cached default map or make it if needed - value = mapCache[option]; + styleMap = invalidStyles['*']; + if (styleMap && styleMap[name]) { + return false; + } - if (!value) { - value = makeMap(defaultValue, ' ', makeMap(defaultValue.toUpperCase(), ' ')); - value = extend(value, extendWith); + styleMap = invalidStyles[elementName]; + if (styleMap && styleMap[name]) { + return false; + } - mapCache[option] = value; + return true; } - } else { - // Create custom map - value = makeMap(value, /[, ]/, makeMap(value.toUpperCase(), /[, ]/)); - } - - return value; - } - - settings = settings || {}; - schemaItems = compileSchema(settings.schema); - - // Allow all elements and attributes if verify_html is set to false - if (settings.verify_html === false) { - settings.valid_elements = '*[*]'; - } - validStyles = compileElementMap(settings.valid_styles); - invalidStyles = compileElementMap(settings.invalid_styles, 'map'); - validClasses = compileElementMap(settings.valid_classes, 'map'); + // Serialize styles according to schema + if (elementName && validStyles) { + // Serialize global styles and element specific styles + serializeStyles('*'); + serializeStyles(elementName); + } else { + // Output the styles in the order they are inside the object + for (name in styles) { + value = styles[name]; - // Setup map objects - whiteSpaceElementsMap = createLookupTable( - 'whitespace_elements', - 'pre script noscript style textarea video audio iframe object code' - ); - selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); - shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' + - 'meta param embed source wbr track'); - boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize ' + - 'noshade nowrap readonly selected autoplay loop controls'); - nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object ' + - 'script pre code', shortEndedElementsMap); - moveCaretBeforeOnEnterElementsMap = createLookupTable('move_caret_before_on_enter_elements', 'table', nonEmptyElementsMap); - textBlockElementsMap = createLookupTable('text_block_elements', 'h1 h2 h3 h4 h5 h6 p div address pre form ' + - 'blockquote center dir fieldset header footer article section hgroup aside nav figure'); - blockElementsMap = createLookupTable('block_elements', 'hr table tbody thead tfoot ' + - 'th tr td li ol ul caption dl dt dd noscript menu isindex option ' + - 'datalist select optgroup figcaption', textBlockElementsMap); - textInlineElementsMap = createLookupTable('text_inline_elements', 'span strong b em i font strike u var cite ' + - 'dfn code mark q sup sub samp'); + if (value && (!invalidStyles || isValid(name, elementName))) { + css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; + } + } + } - each((settings.special || 'script noscript noframes noembed title style textarea xmp').split(' '), function (name) { - specialElements[name] = new RegExp('<\/' + name + '[^>]*>', 'gi'); - }); + return css; + } + }; + }; + } +); - // Converts a wildcard expression string to a regexp for example *a will become /.*a/. - function patternToRegExp(str) { - return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$'); - } +/** + * 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 + */ - // Parses the specified valid_elements string and adds to the current rules - // This function is a bit hard to read since it's heavily optimized for speed - function addValidElements(validElements) { - var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder, - prefix, outputName, globalAttributes, globalAttributesOrder, key, value, - elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/, - attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/, - hasPatternsRegExp = /[*?+]/; +/** + * Utility class for various DOM manipulation and retrieval functions. + * + * @class tinymce.dom.DOMUtils + * @example + * // Add a class to an element by id in the page + * tinymce.DOM.addClass('someid', 'someclass'); + * + * // Add a class to an element by id inside the editor + * tinymce.activeEditor.dom.addClass('someid', 'someclass'); + */ +define( + 'tinymce.core.dom.DOMUtils', + [ + 'global!document', + 'global!window', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.dom.EventUtils', + 'tinymce.core.dom.Sizzle', + 'tinymce.core.dom.StyleSheetLoader', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.Env', + 'tinymce.core.html.Entities', + 'tinymce.core.html.Schema', + 'tinymce.core.html.Styles', + 'tinymce.core.util.Tools' + ], + function (document, window, DomQuery, EventUtils, Sizzle, StyleSheetLoader, TreeWalker, Env, Entities, Schema, Styles, Tools) { + // Shorten names + var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim; + var isIE = Env.ie; + var simpleSelectorRe = /^([a-z0-9],?)+$/i; + var whiteSpaceRegExp = /^[ \t\r\n]*$/; - if (validElements) { - // Split valid elements into an array with rules - validElements = split(validElements, ','); + function setupAttrHooks(domUtils, settings) { + var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook; - if (elements['@']) { - globalAttributes = elements['@'].attributes; - globalAttributesOrder = elements['@'].attributesOrder; + keepUrlHook = { + set: function ($elm, value, name) { + if (settings.url_converter) { + value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]); } - // Loop all rules - for (ei = 0, el = validElements.length; ei < el; ei++) { - // Parse element rule - matches = elementRuleRegExp.exec(validElements[ei]); - if (matches) { - // Setup local names for matches - prefix = matches[1]; - elementName = matches[2]; - outputName = matches[3]; - attrData = matches[5]; - - // Create new attributes and attributesOrder - attributes = {}; - attributesOrder = []; - - // Create the new element - element = { - attributes: attributes, - attributesOrder: attributesOrder - }; + $elm.attr('data-mce-' + name, value).attr(name, value); + }, - // Padd empty elements prefix - if (prefix === '#') { - element.paddEmpty = true; - } + get: function ($elm, name) { + return $elm.attr('data-mce-' + name) || $elm.attr(name); + } + }; - // Remove empty elements prefix - if (prefix === '-') { - element.removeEmpty = true; - } + attrHooks = { + style: { + set: function ($elm, value) { + if (value !== null && typeof value === 'object') { + $elm.css(value); + return; + } - if (matches[4] === '!') { - element.removeEmptyAttrs = true; - } + if (keepValues) { + $elm.attr('data-mce-style', value); + } - // Copy attributes from global rule into current rule - if (globalAttributes) { - for (key in globalAttributes) { - attributes[key] = globalAttributes[key]; - } + $elm.attr('style', value); + }, - attributesOrder.push.apply(attributesOrder, globalAttributesOrder); - } + get: function ($elm) { + var value = $elm.attr('data-mce-style') || $elm.attr('style'); - // Attributes defined - if (attrData) { - attrData = split(attrData, '|'); - for (ai = 0, al = attrData.length; ai < al; ai++) { - matches = attrRuleRegExp.exec(attrData[ai]); - if (matches) { - attr = {}; - attrType = matches[1]; - attrName = matches[2].replace(/::/g, ':'); - prefix = matches[3]; - value = matches[4]; + value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName); - // Required - if (attrType === '!') { - element.attributesRequired = element.attributesRequired || []; - element.attributesRequired.push(attrName); - attr.required = true; - } + return value; + } + } + }; - // Denied from global - if (attrType === '-') { - delete attributes[attrName]; - attributesOrder.splice(inArray(attributesOrder, attrName), 1); - continue; - } + if (keepValues) { + attrHooks.href = attrHooks.src = keepUrlHook; + } - // Default value - if (prefix) { - // Default value - if (prefix === '=') { - element.attributesDefault = element.attributesDefault || []; - element.attributesDefault.push({ name: attrName, value: value }); - attr.defaultValue = value; - } + return attrHooks; + } - // Forced value - if (prefix === ':') { - element.attributesForced = element.attributesForced || []; - element.attributesForced.push({ name: attrName, value: value }); - attr.forcedValue = value; - } + function updateInternalStyleAttr(domUtils, $elm) { + var value = $elm.attr('style'); - // Required values - if (prefix === '<') { - attr.validValues = makeMap(value, '?'); - } - } + value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName); - // Check for attribute patterns - if (hasPatternsRegExp.test(attrName)) { - element.attributePatterns = element.attributePatterns || []; - attr.pattern = patternToRegExp(attrName); - element.attributePatterns.push(attr); - } else { - // Add attribute to order list if it doesn't already exist - if (!attributes[attrName]) { - attributesOrder.push(attrName); - } + if (!value) { + value = null; + } - attributes[attrName] = attr; - } - } - } - } + $elm.attr('data-mce-style', value); + } - // Global rule, store away these for later usage - if (!globalAttributes && elementName == '@') { - globalAttributes = attributes; - globalAttributesOrder = attributesOrder; - } + function nodeIndex(node, normalized) { + var idx = 0, lastNodeType, nodeType; - // Handle substitute elements such as b/strong - if (outputName) { - element.outputName = elementName; - elements[outputName] = element; - } + if (node) { + for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) { + nodeType = node.nodeType; - // Add pattern or exact element - if (hasPatternsRegExp.test(elementName)) { - element.pattern = patternToRegExp(elementName); - patternElements.push(element); - } else { - elements[elementName] = element; - } + // Normalize text nodes + if (normalized && nodeType == 3) { + if (nodeType == lastNodeType || !node.nodeValue.length) { + continue; } } + idx++; + lastNodeType = nodeType; } } - function setValidElements(validElements) { - elements = {}; - patternElements = []; - - addValidElements(validElements); - - each(schemaItems, function (element, name) { - children[name] = element.children; - }); - } - - // Adds custom non HTML elements to the schema - function addCustomElements(customElements) { - var customElementRegExp = /^(~)?(.+)$/; + return idx; + } - if (customElements) { - // Flush cached items since we are altering the default maps - mapCache.text_block_elements = mapCache.block_elements = null; + /** + * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class. + * + * @constructor + * @method DOMUtils + * @param {Document} doc Document reference to bind the utility class to. + * @param {settings} settings Optional settings collection. + */ + function DOMUtils(doc, settings) { + var self = this, blockElementsMap; - each(split(customElements, ','), function (rule) { - var matches = customElementRegExp.exec(rule), - inline = matches[1] === '~', - cloneName = inline ? 'span' : 'div', - name = matches[2]; - - children[name] = children[cloneName]; - customElementsMap[name] = cloneName; - - // If it's not marked as inline then add it to valid block elements - if (!inline) { - blockElementsMap[name.toUpperCase()] = {}; - blockElementsMap[name] = {}; - } - - // Add elements clone if needed - if (!elements[name]) { - var customRule = elements[cloneName]; - - customRule = extend({}, customRule); - delete customRule.removeEmptyAttrs; - delete customRule.removeEmpty; - - elements[name] = customRule; - } - - // Add custom elements at span/div positions - each(children, function (element, elmName) { - if (element[cloneName]) { - children[elmName] = element = extend({}, children[elmName]); - element[name] = element[cloneName]; - } - }); - }); - } - } - - // Adds valid children to the schema object - function addValidChildren(validChildren) { - var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/; - - // Invalidate the schema cache if the schema is mutated - mapCache[settings.schema] = null; - - if (validChildren) { - each(split(validChildren, ','), function (rule) { - var matches = childRuleRegExp.exec(rule), parent, prefix; - - if (matches) { - prefix = matches[1]; - - // Add/remove items from default - if (prefix) { - parent = children[matches[2]]; - } else { - parent = children[matches[2]] = { '#comment': {} }; - } + self.doc = doc; + self.win = window; + self.files = {}; + self.counter = 0; + self.stdMode = !isIE || doc.documentMode >= 8; + self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode; + self.styleSheetLoader = new StyleSheetLoader(doc); + self.boundEvents = []; + self.settings = settings = settings || {}; + self.schema = settings.schema ? settings.schema : new Schema({}); + self.styles = new Styles({ + url_converter: settings.url_converter, + url_converter_scope: settings.url_converter_scope + }, settings.schema); - parent = children[matches[2]]; + self.fixDoc(doc); + self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event; + self.attrHooks = setupAttrHooks(self, settings); + blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {}; + self.$ = DomQuery.overrideDefaults(function () { + return { + context: doc, + element: self.getRoot() + }; + }); - each(split(matches[3], '|'), function (child) { - if (prefix === '-') { - delete parent[child]; - } else { - parent[child] = {}; - } - }); - } - }); + /** + * Returns true/false if the specified element is a block element or not. + * + * @method isBlock + * @param {Node/String} node Element/Node to check. + * @return {Boolean} True/False state if the node is a block element or not. + */ + self.isBlock = function (node) { + // Fix for #5446 + if (!node) { + return false; } - } - function getElementRule(name) { - var element = elements[name], i; + // This function is called in module pattern style since it might be executed with the wrong this scope + var type = node.nodeType; - // Exact match found - if (element) { - return element; + // If it's a node then check the type and use the nodeName + if (type) { + return !!(type === 1 && blockElementsMap[node.nodeName]); } - // No exact match then try the patterns - i = patternElements.length; - while (i--) { - element = patternElements[i]; + return !!blockElementsMap[node]; + }; + } - if (element.pattern.test(name)) { - return element; - } + DOMUtils.prototype = { + $$: function (elm) { + if (typeof elm == 'string') { + elm = this.get(elm); } - } - if (!settings.valid_elements) { - // No valid elements defined then clone the elements from the schema spec - each(schemaItems, function (element, name) { - elements[name] = { - attributes: element.attributes, - attributesOrder: element.attributesOrder - }; + return this.$(elm); + }, - children[name] = element.children; - }); + root: null, - // Switch these on HTML4 - if (settings.schema != "html5") { - each(split('strong/b em/i'), function (item) { - item = split(item, '/'); - elements[item[1]].outputName = item[0]; - }); - } + fixDoc: function (doc) { + var settings = this.settings, name; - // Add default alt attribute for images, removed since alt="" is treated as presentational. - // elements.img.attributesDefault = [{name: 'alt', value: ''}]; + if (isIE && settings.schema) { + // Add missing HTML 4/5 elements to IE + ('abbr article aside audio canvas ' + + 'details figcaption figure footer ' + + 'header hgroup mark menu meter nav ' + + 'output progress section summary ' + + 'time video').replace(/\w+/g, function (name) { + doc.createElement(name); + }); - // Remove these if they are empty by default - each(split('ol ul sub sup blockquote span font a table tbody tr strong em b i'), function (name) { - if (elements[name]) { - elements[name].removeEmpty = true; + // Create all custom elements + for (name in settings.schema.getCustomElements()) { + doc.createElement(name); } - }); - - // Padd these by default - each(split('p h1 h2 h3 h4 h5 h6 th td pre div address caption'), function (name) { - elements[name].paddEmpty = true; - }); + } + }, - // Remove these if they have no attributes - each(split('span'), function (name) { - elements[name].removeEmptyAttrs = true; - }); + clone: function (node, deep) { + var self = this, clone, doc; - // Remove these by default - // TODO: Reenable in 4.1 - /*each(split('script style'), function(name) { - delete elements[name]; - });*/ - } else { - setValidElements(settings.valid_elements); - } + // TODO: Add feature detection here in the future + if (!isIE || node.nodeType !== 1 || deep) { + return node.cloneNode(deep); + } - addCustomElements(settings.custom_elements); - addValidChildren(settings.valid_children); - addValidElements(settings.extended_valid_elements); + doc = self.doc; - // Todo: Remove this when we fix list handling to be valid - addValidChildren('+ol[ul|ol],+ul[ul|ol]'); + // Make a HTML5 safe shallow copy + if (!deep) { + clone = doc.createElement(node.nodeName); + // Copy attribs + each(self.getAttribs(node), function (attr) { + self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); + }); - // Some elements are not valid by themselves - require parents - each({ - dd: 'dl', - dt: 'dl', - li: 'ul ol', - td: 'tr', - th: 'tr', - tr: 'tbody thead tfoot', - tbody: 'table', - thead: 'table', - tfoot: 'table', - legend: 'fieldset', - area: 'map', - param: 'video audio object' - }, function (parents, item) { - if (elements[item]) { - elements[item].parentsRequired = split(parents); + return clone; } - }); - - // Delete invalid elements - if (settings.invalid_elements) { - each(explode(settings.invalid_elements), function (item) { - if (elements[item]) { - delete elements[item]; - } - }); - } - - // If the user didn't allow span only allow internal spans - if (!getElementRule('span')) { - addValidElements('span[!data-mce-type|*]'); - } + return clone.firstChild; + }, /** - * Name/value map object with valid parents and children to those parents. + * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not + * go above the point of this root node. * - * @example - * children = { - * div:{p:{}, h1:{}} - * }; - * @field children - * @type Object + * @method getRoot + * @return {Element} Root element for the utility class. */ - self.children = children; + getRoot: function () { + var self = this; - /** - * Name/value map object with valid styles for each element. - * - * @method getValidStyles - * @type Object - */ - self.getValidStyles = function () { - return validStyles; - }; + return self.settings.root_element || self.doc.body; + }, /** - * Name/value map object with valid styles for each element. + * Returns the viewport of the window. * - * @method getInvalidStyles - * @type Object + * @method getViewPort + * @param {Window} win Optional window to get viewport of. + * @return {Object} Viewport object with fields x, y, w and h. */ - self.getInvalidStyles = function () { - return invalidStyles; - }; + getViewPort: function (win) { + var doc, rootElm; + + win = !win ? this.win : win; + doc = win.document; + rootElm = this.boxModel ? doc.documentElement : doc.body; + + // Returns viewport size excluding scrollbars + return { + x: win.pageXOffset || rootElm.scrollLeft, + y: win.pageYOffset || rootElm.scrollTop, + w: win.innerWidth || rootElm.clientWidth, + h: win.innerHeight || rootElm.clientHeight + }; + }, /** - * Name/value map object with valid classes for each element. + * Returns the rectangle for a specific element. * - * @method getValidClasses - * @type Object + * @method getRect + * @param {Element/String} elm Element object or element ID to get rectangle from. + * @return {object} Rectangle for specified element object with x, y, w, h fields. */ - self.getValidClasses = function () { - return validClasses; - }; + getRect: function (elm) { + var self = this, pos, size; + + elm = self.get(elm); + pos = self.getPos(elm); + size = self.getSize(elm); + + return { + x: pos.x, y: pos.y, + w: size.w, h: size.h + }; + }, /** - * Returns a map with boolean attributes. + * Returns the size dimensions of the specified element. * - * @method getBoolAttrs - * @return {Object} Name/value lookup map for boolean attributes. + * @method getSize + * @param {Element/String} elm Element object or element ID to get rectangle from. + * @return {object} Rectangle for specified element object with w, h fields. */ - self.getBoolAttrs = function () { - return boolAttrMap; - }; + getSize: function (elm) { + var self = this, w, h; + + elm = self.get(elm); + w = self.getStyle(elm, 'width'); + h = self.getStyle(elm, 'height'); + + // Non pixel value, then force offset/clientWidth + if (w.indexOf('px') === -1) { + w = 0; + } + + // Non pixel value, then force offset/clientWidth + if (h.indexOf('px') === -1) { + h = 0; + } + + return { + w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth, + h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight + }; + }, /** - * Returns a map with block elements. + * Returns a node by the specified selector function. This function will + * loop through all parent nodes and call the specified function for each node. + * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end + * and the node it found will be returned. * - * @method getBlockElements - * @return {Object} Name/value lookup map for block elements. + * @method getParent + * @param {Node/String} node DOM node to search parents on or ID string. + * @param {function} selector Selection function or CSS selector to execute on each node. + * @param {Node} root Optional root element, never go beyond this point. + * @return {Node} DOM Node or null if it wasn't found. */ - self.getBlockElements = function () { - return blockElementsMap; - }; + getParent: function (node, selector, root) { + return this.getParents(node, selector, root, false); + }, /** - * Returns a map with text block elements. Such as: p,h1-h6,div,address + * Returns a node list of all parents matching the specified selector function or pattern. + * If the function then returns true indicating that it has found what it was looking for and that node will be collected. * - * @method getTextBlockElements - * @return {Object} Name/value lookup map for block elements. + * @method getParents + * @param {Node/String} node DOM node to search parents on or ID string. + * @param {function} selector Selection function to execute on each node or CSS pattern. + * @param {Node} root Optional root element, never go beyond this point. + * @return {Array} Array of nodes or null if it wasn't found. */ - self.getTextBlockElements = function () { - return textBlockElementsMap; - }; + getParents: function (node, selector, root, collect) { + var self = this, selectorVal, result = []; + + node = self.get(node); + collect = collect === undefined; + + // Default root on inline mode + root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null); + + // Wrap node name as func + if (is(selector, 'string')) { + selectorVal = selector; + + if (selector === '*') { + selector = function (node) { + return node.nodeType == 1; + }; + } else { + selector = function (node) { + return self.is(node, selectorVal); + }; + } + } + + while (node) { + if (node == root || !node.nodeType || node.nodeType === 9) { + break; + } + + if (!selector || selector(node)) { + if (collect) { + result.push(node); + } else { + return node; + } + } + + node = node.parentNode; + } + + return collect ? result : null; + }, /** - * Returns a map of inline text format nodes for example strong/span or ins. + * Returns the specified element by ID or the input element if it isn't a string. * - * @method getTextInlineElements - * @return {Object} Name/value lookup map for text format elements. + * @method get + * @param {String/Element} n Element id to look for or element to just pass though. + * @return {Element} Element matching the specified id or null if it wasn't found. */ - self.getTextInlineElements = function () { - return textInlineElementsMap; - }; + get: function (elm) { + var name; + + if (elm && this.doc && typeof elm == 'string') { + name = elm; + elm = this.doc.getElementById(elm); + + // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick + if (elm && elm.id !== name) { + return this.doc.getElementsByName(name)[1]; + } + } + + return elm; + }, /** - * Returns a map with short ended elements such as BR or IMG. + * Returns the next node that matches selector or function * - * @method getShortEndedElements - * @return {Object} Name/value lookup map for short ended elements. + * @method getNext + * @param {Node} node Node to find siblings from. + * @param {String/function} selector Selector CSS expression or function. + * @return {Node} Next node item matching the selector or null if it wasn't found. */ - self.getShortEndedElements = function () { - return shortEndedElementsMap; - }; + getNext: function (node, selector) { + return this._findSib(node, selector, 'nextSibling'); + }, /** - * Returns a map with self closing tags such as
  • . + * Returns the previous node that matches selector or function * - * @method getSelfClosingElements - * @return {Object} Name/value lookup map for self closing tags elements. + * @method getPrev + * @param {Node} node Node to find siblings from. + * @param {String/function} selector Selector CSS expression or function. + * @return {Node} Previous node item matching the selector or null if it wasn't found. */ - self.getSelfClosingElements = function () { - return selfClosingElementsMap; - }; + getPrev: function (node, selector) { + return this._findSib(node, selector, 'previousSibling'); + }, + + // #ifndef jquery /** - * Returns a map with elements that should be treated as contents regardless if it has text - * content in them or not such as TD, VIDEO or IMG. + * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test". + * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough + * on more complex patterns. * - * @method getNonEmptyElements - * @return {Object} Name/value lookup map for non empty elements. + * @method select + * @param {String} selector CSS level 3 pattern to select/find elements by. + * @param {Object} scope Optional root element/scope element to search in. + * @return {Array} Array with all matched elements. + * @example + * // Adds a class to all paragraphs in the currently active editor + * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); + * + * // Adds a class to all spans that have the test class in the currently active editor + * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') */ - self.getNonEmptyElements = function () { - return nonEmptyElementsMap; - }; + select: function (selector, scope) { + var self = this; + + /*eslint new-cap:0 */ + return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []); + }, /** - * Returns a map with elements that the caret should be moved in front of after enter is - * pressed + * Returns true/false if the specified element matches the specified css pattern. * - * @method getMoveCaretBeforeOnEnterElements - * @return {Object} Name/value lookup map for elements to place the caret in front of. + * @method is + * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. + * @param {String} selector CSS pattern to match the element against. */ - self.getMoveCaretBeforeOnEnterElements = function () { - return moveCaretBeforeOnEnterElementsMap; - }; + is: function (elm, selector) { + var i; - /** - * Returns a map with elements where white space is to be preserved like PRE or SCRIPT. + if (!elm) { + return false; + } + + // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance + if (elm.length === undefined) { + // Simple all selector + if (selector === '*') { + return elm.nodeType == 1; + } + + // Simple selector just elements + if (simpleSelectorRe.test(selector)) { + selector = selector.toLowerCase().split(/,/); + elm = elm.nodeName.toLowerCase(); + + for (i = selector.length - 1; i >= 0; i--) { + if (selector[i] == elm) { + return true; + } + } + + return false; + } + } + + // Is non element + if (elm.nodeType && elm.nodeType != 1) { + return false; + } + + var elms = elm.nodeType ? [elm] : elm; + + /*eslint new-cap:0 */ + return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; + }, + + // #endif + + /** + * Adds the specified element to another element or elements. * - * @method getWhiteSpaceElements - * @return {Object} Name/value lookup map for white space elements. + * @method add + * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. + * @param {String/Element} name Name of new element to add or existing element to add. + * @param {Object} attrs Optional object collection with arguments to add to the new element(s). + * @param {String} html Optional inner HTML contents to add for each element. + * @param {Boolean} create Optional flag if the element should be created or added. + * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements + * were passed in. + * @example + * // Adds a new paragraph to the end of the active editor + * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content'); */ - self.getWhiteSpaceElements = function () { - return whiteSpaceElementsMap; - }; + add: function (parentElm, name, attrs, html, create) { + var self = this; + + return this.run(parentElm, function (parentElm) { + var newElm; + + newElm = is(name, 'string') ? self.doc.createElement(name) : name; + self.setAttribs(newElm, attrs); + + if (html) { + if (html.nodeType) { + newElm.appendChild(html); + } else { + self.setHTML(newElm, html); + } + } + + return !create ? parentElm.appendChild(newElm) : newElm; + }); + }, /** - * Returns a map with special elements. These are elements that needs to be parsed - * in a special way such as script, style, textarea etc. The map object values - * are regexps used to find the end of the element. + * Creates a new element. * - * @method getSpecialElements - * @return {Object} Name/value lookup map for special elements. + * @method create + * @param {String} name Name of new element. + * @param {Object} attrs Optional object name/value collection with element attributes. + * @param {String} html Optional HTML string to set as inner HTML of the element. + * @return {Element} HTML DOM node element that got created. + * @example + * // Adds an element where the caret/selection is in the active editor + * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content'); + * tinymce.activeEditor.selection.setNode(el); */ - self.getSpecialElements = function () { - return specialElements; - }; + create: function (name, attrs, html) { + return this.add(this.doc.createElement(name), name, attrs, html, 1); + }, /** - * Returns true/false if the specified element and it's child is valid or not - * according to the schema. + * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. * - * @method isValidChild - * @param {String} name Element name to check for. - * @param {String} child Element child to verify. - * @return {Boolean} True/false if the element is a valid child of the specified parent. + * @method createHTML + * @param {String} name Name of new element. + * @param {Object} attrs Optional object name/value collection with element attributes. + * @param {String} html Optional HTML string to set as inner HTML of the element. + * @return {String} String with new HTML element, for example: test. + * @example + * // Creates a html chunk and inserts it at the current selection/caret location + * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line')); */ - self.isValidChild = function (name, child) { - var parent = children[name.toLowerCase()]; + createHTML: function (name, attrs, html) { + var outHtml = '', key; - return !!(parent && parent[child.toLowerCase()]); - }; + outHtml += '<' + name; + + for (key in attrs) { + if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') { + outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"'; + } + } + + // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime + if (typeof html != "undefined") { + return outHtml + '>' + html + ''; + } + + return outHtml + ' />'; + }, /** - * Returns true/false if the specified element name and optional attribute is - * valid according to the schema. + * Creates a document fragment out of the specified HTML string. * - * @method isValid - * @param {String} name Name of element to check. - * @param {String} attr Optional attribute name to check for. - * @return {Boolean} True/false if the element and attribute is valid. + * @method createFragment + * @param {String} html Html string to create fragment from. + * @return {DocumentFragment} Document fragment node. */ - self.isValid = function (name, attr) { - var attrPatterns, i, rule = getElementRule(name); + createFragment: function (html) { + var frag, node, doc = this.doc, container; - // Check if it's a valid element - if (rule) { - if (attr) { - // Check if attribute name exists - if (rule.attributes[attr]) { - return true; - } + container = doc.createElement("div"); + frag = doc.createDocumentFragment(); - // Check if attribute matches a regexp pattern - attrPatterns = rule.attributePatterns; - if (attrPatterns) { - i = attrPatterns.length; - while (i--) { - if (attrPatterns[i].pattern.test(name)) { - return true; - } - } - } - } else { - return true; - } + if (html) { + container.innerHTML = html; } - // No match - return false; - }; + while ((node = container.firstChild)) { + frag.appendChild(node); + } + + return frag; + }, /** - * Returns true/false if the specified element is valid or not - * according to the schema. + * Removes/deletes the specified element(s) from the DOM. * - * @method getElementRule - * @param {String} name Element name to check for. - * @return {Object} Element object or undefined if the element isn't valid. + * @method remove + * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. + * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be + * placed at the location of the removed element. + * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements + * were passed in. + * @example + * // Removes all paragraphs in the active editor + * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); + * + * // Removes an element by id in the document + * tinymce.DOM.remove('mydiv'); */ - self.getElementRule = getElementRule; + remove: function (node, keepChildren) { + node = this.$$(node); + + if (keepChildren) { + node.each(function () { + var child; + + while ((child = this.firstChild)) { + if (child.nodeType == 3 && child.data.length === 0) { + this.removeChild(child); + } else { + this.parentNode.insertBefore(child, this); + } + } + }).remove(); + } else { + node.remove(); + } + + return node.length > 1 ? node.toArray() : node[0]; + }, /** - * Returns an map object of all custom elements. + * Sets the CSS style value on a HTML element. The name can be a camelcase string + * or the CSS style name like background-color. * - * @method getCustomElements - * @return {Object} Name/value map object of all custom elements. + * @method setStyle + * @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on. + * @param {String} name Name of the style value to set. + * @param {String} value Value to set on the style. + * @example + * // Sets a style value on all paragraphs in the currently active editor + * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); + * + * // Sets a style value to an element by id in the current document + * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); */ - self.getCustomElements = function () { - return customElementsMap; - }; + setStyle: function (elm, name, value) { + elm = this.$$(elm).css(name, value); + + if (this.settings.update_styles) { + updateInternalStyleAttr(this, elm); + } + }, /** - * Parses a valid elements string and adds it to the schema. The valid elements - * format is for example "element[attr=default|otherattr]". - * Existing rules will be replaced with the ones specified, so this extends the schema. + * Returns the current style or runtime/computed value of an element. * - * @method addValidElements - * @param {String} valid_elements String in the valid elements format to be parsed. + * @method getStyle + * @param {String/Element} elm HTML element or element id string to get style from. + * @param {String} name Style name to return. + * @param {Boolean} computed Computed style. + * @return {String} Current style or computed style value of an element. */ - self.addValidElements = addValidElements; + getStyle: function (elm, name, computed) { + elm = this.$$(elm); + + if (computed) { + return elm.css(name); + } + + // Camelcase it, if needed + name = name.replace(/-(\D)/g, function (a, b) { + return b.toUpperCase(); + }); + + if (name == 'float') { + name = Env.ie && Env.ie < 12 ? 'styleFloat' : 'cssFloat'; + } + + return elm[0] && elm[0].style ? elm[0].style[name] : undefined; + }, /** - * Parses a valid elements string and sets it to the schema. The valid elements - * format is for example "element[attr=default|otherattr]". - * Existing rules will be replaced with the ones specified, so this extends the schema. + * Sets multiple styles on the specified element(s). * - * @method setValidElements - * @param {String} valid_elements String in the valid elements format to be parsed. + * @method setStyles + * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on. + * @param {Object} styles Name/Value collection of style items to add to the element(s). + * @example + * // Sets styles on all paragraphs in the currently active editor + * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'}); + * + * // Sets styles to an element by id in the current document + * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'}); */ - self.setValidElements = setValidElements; + setStyles: function (elm, styles) { + elm = this.$$(elm).css(styles); + + if (this.settings.update_styles) { + updateInternalStyleAttr(this, elm); + } + }, /** - * Adds custom non HTML elements to the schema. + * Removes all attributes from an element or elements. * - * @method addCustomElements - * @param {String} custom_elements Comma separated list of custom elements to add. + * @method removeAllAttribs + * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. */ - self.addCustomElements = addCustomElements; + removeAllAttribs: function (e) { + return this.run(e, function (e) { + var i, attrs = e.attributes; + for (i = attrs.length - 1; i >= 0; i--) { + e.removeAttributeNode(attrs.item(i)); + } + }); + }, /** - * Parses a valid children string and adds them to the schema structure. The valid children - * format is for example: "element[child1|child2]". + * Sets the specified attribute of an element or elements. * - * @method addValidChildren - * @param {String} valid_children Valid children elements string to parse + * @method setAttrib + * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on. + * @param {String} name Name of attribute to set. + * @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove + * the attribute instead. + * @example + * // Sets class attribute on all paragraphs in the active editor + * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); + * + * // Sets class attribute on a specific element in the current page + * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); */ - self.addValidChildren = addValidChildren; + setAttrib: function (elm, name, value) { + var self = this, originalValue, hook, settings = self.settings; - self.elements = elements; - }; - } -); + if (value === '') { + value = null; + } -/** - * Styles.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 - */ + elm = self.$$(elm); + originalValue = elm.attr(name); -/** - * This class is used to parse CSS styles it also compresses styles to reduce the output size. - * - * @example - * var Styles = new tinymce.html.Styles({ - * url_converter: function(url) { - * return url; - * } - * }); - * - * styles = Styles.parse('border: 1px solid red'); - * styles.color = 'red'; - * - * console.log(new tinymce.html.StyleSerializer().serialize(styles)); - * - * @class tinymce.html.Styles - * @version 3.4 - */ -define( - 'tinymce.core.html.Styles', - [ - ], - function () { - return function (settings, schema) { - /*jshint maxlen:255 */ - /*eslint max-len:0 */ - var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi, - urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi, - styleRegExp = /\s*([^:]+):\s*([^;]+);?/g, - trimRightRegExp = /\s+$/, - i, encodingLookup = {}, encodingItems, validStyles, invalidStyles, invisibleChar = '\uFEFF'; + if (!elm.length) { + return; + } - settings = settings || {}; + hook = self.attrHooks[name]; + if (hook && hook.set) { + hook.set(elm, value, name); + } else { + elm.attr(name, value); + } - if (schema) { - validStyles = schema.getValidStyles(); - invalidStyles = schema.getInvalidStyles(); - } + if (originalValue != value && settings.onSetAttrib) { + settings.onSetAttrib({ + attrElm: elm, + attrName: name, + attrValue: value + }); + } + }, - encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); - for (i = 0; i < encodingItems.length; i++) { - encodingLookup[encodingItems[i]] = invisibleChar + i; - encodingLookup[invisibleChar + i] = encodingItems[i]; - } + /** + * Sets two or more specified attributes of an element or elements. + * + * @method setAttribs + * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. + * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). + * @example + * // Sets class and title attributes on all paragraphs in the active editor + * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'}); + * + * // Sets class and title attributes on a specific element in the current page + * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'}); + */ + setAttribs: function (elm, attrs) { + var self = this; - function toHex(match, r, g, b) { - function hex(val) { - val = parseInt(val, 10).toString(16); + self.$$(elm).each(function (i, node) { + each(attrs, function (value, name) { + self.setAttrib(node, name, value); + }); + }); + }, - return val.length > 1 ? val : '0' + val; // 0 -> 00 - } + /** + * Returns the specified attribute by name. + * + * @method getAttrib + * @param {String/Element} elm Element string id or DOM element to get attribute from. + * @param {String} name Name of attribute to get. + * @param {String} defaultVal Optional default value to return if the attribute didn't exist. + * @return {String} Attribute value string, default value or null if the attribute wasn't found. + */ + getAttrib: function (elm, name, defaultVal) { + var self = this, hook, value; - return '#' + hex(r) + hex(g) + hex(b); - } + elm = self.$$(elm); - return { - /** - * Parses the specified RGB color value and returns a hex version of that color. - * - * @method toHex - * @param {String} color RGB string value like rgb(1,2,3) - * @return {String} Hex version of that RGB value like #FF00FF. - */ - toHex: function (color) { - return color.replace(rgbRegExp, toHex); - }, + if (elm.length) { + hook = self.attrHooks[name]; - /** - * Parses the specified style value into an object collection. This parser will also - * merge and remove any redundant items that browsers might have added. It will also convert non hex - * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. - * - * @method parse - * @param {String} css Style value to parse for example: border:1px solid red;. - * @return {Object} Object representation of that style like {border: '1px solid red'} - */ - parse: function (css) { - var styles = {}, matches, name, value, isEncoded, urlConverter = settings.url_converter; - var urlConverterScope = settings.url_converter_scope || this; + if (hook && hook.get) { + value = hook.get(elm, name); + } else { + value = elm.attr(name); + } + } - function compress(prefix, suffix, noJoin) { - var top, right, bottom, left; + if (typeof value == 'undefined') { + value = defaultVal || ''; + } - top = styles[prefix + '-top' + suffix]; - if (!top) { - return; - } + return value; + }, - right = styles[prefix + '-right' + suffix]; - if (!right) { - return; - } + /** + * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. + * + * @method getPos + * @param {Element/String} elm HTML element or element id to get x, y position from. + * @param {Element} rootElm Optional root element to stop calculations at. + * @return {object} Absolute position of the specified element object with x, y fields. + */ + getPos: function (elm, rootElm) { + var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos; - bottom = styles[prefix + '-bottom' + suffix]; - if (!bottom) { - return; - } + elm = self.get(elm); + rootElm = rootElm || body; - left = styles[prefix + '-left' + suffix]; - if (!left) { - return; - } - - var box = [top, right, bottom, left]; - i = box.length - 1; - while (i--) { - if (box[i] !== box[i + 1]) { - break; - } - } + if (elm) { + // Use getBoundingClientRect if it exists since it's faster than looping offset nodes + // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root + if (rootElm === body && elm.getBoundingClientRect && DomQuery(body).css('position') === 'static') { + pos = elm.getBoundingClientRect(); + rootElm = self.boxModel ? doc.documentElement : body; - if (i > -1 && noJoin) { - return; - } + // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit + // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position + x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft; + y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop; - styles[prefix + suffix] = i == -1 ? box[0] : box.join(' '); - delete styles[prefix + '-top' + suffix]; - delete styles[prefix + '-right' + suffix]; - delete styles[prefix + '-bottom' + suffix]; - delete styles[prefix + '-left' + suffix]; + return { x: x, y: y }; } - /** - * Checks if the specific style can be compressed in other words if all border-width are equal. - */ - function canCompress(key) { - var value = styles[key], i; - - if (!value) { - return; - } - - value = value.split(' '); - i = value.length; - while (i--) { - if (value[i] !== value[0]) { - return false; - } - } - - styles[key] = value[0]; + offsetParent = elm; + while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { + x += offsetParent.offsetLeft || 0; + y += offsetParent.offsetTop || 0; + offsetParent = offsetParent.offsetParent; + } - return true; + offsetParent = elm.parentNode; + while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { + x -= offsetParent.scrollLeft || 0; + y -= offsetParent.scrollTop || 0; + offsetParent = offsetParent.parentNode; } + } - /** - * Compresses multiple styles into one style. - */ - function compress2(target, a, b, c) { - if (!canCompress(a)) { - return; - } + return { x: x, y: y }; + }, - if (!canCompress(b)) { - return; - } + /** + * Parses the specified style value into an object collection. This parser will also + * merge and remove any redundant items that browsers might have added. It will also convert non-hex + * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. + * + * @method parseStyle + * @param {String} cssText Style value to parse, for example: border:1px solid red;. + * @return {Object} Object representation of that style, for example: {border: '1px solid red'} + */ + parseStyle: function (cssText) { + return this.styles.parse(cssText); + }, - if (!canCompress(c)) { - return; - } + /** + * Serializes the specified style object into a string. + * + * @method serializeStyle + * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} + * @param {String} name Optional element name. + * @return {String} String representation of the style object, for example: border: 1px solid red. + */ + serializeStyle: function (styles, name) { + return this.styles.serialize(styles, name); + }, - // Compress - styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; - delete styles[a]; - delete styles[b]; - delete styles[c]; - } + /** + * Adds a style element at the top of the document with the specified cssText content. + * + * @method addStyle + * @param {String} cssText CSS Text style to add to top of head of document. + */ + addStyle: function (cssText) { + var self = this, doc = self.doc, head, styleElm; - // Encodes the specified string by replacing all \" \' ; : with _ - function encode(str) { - isEncoded = true; + // Prevent inline from loading the same styles twice + if (self !== DOMUtils.DOM && doc === document) { + var addedStyles = DOMUtils.DOM.addedStyles; - return encodingLookup[str]; + addedStyles = addedStyles || []; + if (addedStyles[cssText]) { + return; } - // Decodes the specified string by replacing all _ with it's original value \" \' etc - // It will also decode the \" \' if keepSlashes is set to fale or omitted - function decode(str, keepSlashes) { - if (isEncoded) { - str = str.replace(/\uFEFF[0-9]/g, function (str) { - return encodingLookup[str]; - }); - } + addedStyles[cssText] = true; + DOMUtils.DOM.addedStyles = addedStyles; + } - if (!keepSlashes) { - str = str.replace(/\\([\'\";:])/g, "$1"); - } + // Create style element if needed + styleElm = doc.getElementById('mceDefaultStyles'); + if (!styleElm) { + styleElm = doc.createElement('style'); + styleElm.id = 'mceDefaultStyles'; + styleElm.type = 'text/css'; - return str; + head = doc.getElementsByTagName('head')[0]; + if (head.firstChild) { + head.insertBefore(styleElm, head.firstChild); + } else { + head.appendChild(styleElm); } + } - function decodeSingleHexSequence(escSeq) { - return String.fromCharCode(parseInt(escSeq.slice(1), 16)); - } + // Append style data to old or new style element + if (styleElm.styleSheet) { + styleElm.styleSheet.cssText += cssText; + } else { + styleElm.appendChild(doc.createTextNode(cssText)); + } + }, - function decodeHexSequences(value) { - return value.replace(/\\[0-9a-f]+/gi, decodeSingleHexSequence); - } + /** + * Imports/loads the specified CSS file into the document bound to the class. + * + * @method loadCSS + * @param {String} url URL to CSS file to load. + * @example + * // Loads a CSS file dynamically into the current document + * tinymce.DOM.loadCSS('somepath/some.css'); + * + * // Loads a CSS file into the currently active editor instance + * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); + * + * // Loads a CSS file into an editor instance by id + * tinymce.get('someid').dom.loadCSS('somepath/some.css'); + * + * // Loads multiple CSS files into the current document + * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); + */ + loadCSS: function (url) { + var self = this, doc = self.doc, head; - function processUrl(match, url, url2, url3, str, str2) { - str = str || str2; + // Prevent inline from loading the same CSS file twice + if (self !== DOMUtils.DOM && doc === document) { + DOMUtils.DOM.loadCSS(url); + return; + } - if (str) { - str = decode(str); + if (!url) { + url = ''; + } - // Force strings into single quote format - return "'" + str.replace(/\'/g, "\\'") + "'"; - } + head = doc.getElementsByTagName('head')[0]; - url = decode(url || url2 || url3); + each(url.split(','), function (url) { + var link; - if (!settings.allow_script_urls) { - var scriptUrl = url.replace(/[\s\r\n]+/g, ''); + url = Tools._addCacheSuffix(url); - if (/(java|vb)script:/i.test(scriptUrl)) { - return ""; - } + if (self.files[url]) { + return; + } - if (!settings.allow_svg_data_urls && /^data:image\/svg/i.test(scriptUrl)) { - return ""; - } - } + self.files[url] = true; + link = self.create('link', { rel: 'stylesheet', href: url }); - // Convert the URL to relative/absolute depending on config - if (urlConverter) { - url = urlConverter.call(urlConverterScope, url, 'style'); - } + // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug + // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading + // It's ugly but it seems to work fine. + if (isIE && doc.documentMode && doc.recalc) { + link.onload = function () { + if (doc.recalc) { + doc.recalc(); + } - // Output new URL format - return "url('" + url.replace(/\'/g, "\\'") + "')"; + link.onload = null; + }; } - if (css) { - css = css.replace(/[\u0000-\u001F]/g, ''); + head.appendChild(link); + }); + }, - // Encode \" \' % and ; and : inside strings so they don't interfere with the style parsing - css = css.replace(/\\[\"\';:\uFEFF]/g, encode).replace(/\"[^\"]+\"|\'[^\']+\'/g, function (str) { - return str.replace(/[;:]/g, encode); - }); + /** + * Adds a class to the specified element or elements. + * + * @method addClass + * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. + * @param {String} cls Class name to add to each element. + * @return {String/Array} String with new class value or array with new class values for all elements. + * @example + * // Adds a class to all paragraphs in the active editor + * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); + * + * // Adds a class to a specific element in the current page + * tinymce.DOM.addClass('mydiv', 'myclass'); + */ + addClass: function (elm, cls) { + this.$$(elm).addClass(cls); + }, - // Parse styles - while ((matches = styleRegExp.exec(css))) { - styleRegExp.lastIndex = matches.index + matches[0].length; - name = matches[1].replace(trimRightRegExp, '').toLowerCase(); - value = matches[2].replace(trimRightRegExp, ''); + /** + * Removes a class from the specified element or elements. + * + * @method removeClass + * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. + * @param {String} cls Class name to remove from each element. + * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements + * were passed in. + * @example + * // Removes a class from all paragraphs in the active editor + * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); + * + * // Removes a class from a specific element in the current page + * tinymce.DOM.removeClass('mydiv', 'myclass'); + */ + removeClass: function (elm, cls) { + this.toggleClass(elm, cls, false); + }, - if (name && value) { - // Decode escaped sequences like \65 -> e - name = decodeHexSequences(name); - value = decodeHexSequences(value); + /** + * Returns true if the specified element has the specified class. + * + * @method hasClass + * @param {String/Element} elm HTML element or element id string to check CSS class on. + * @param {String} cls CSS class to check for. + * @return {Boolean} true/false if the specified element has the specified class. + */ + hasClass: function (elm, cls) { + return this.$$(elm).hasClass(cls); + }, - // Skip properties with double quotes and sequences like \" \' in their names - // See 'mXSS Attacks: Attacking well-secured Web-Applications by using innerHTML Mutations' - // https://cure53.de/fp170.pdf - if (name.indexOf(invisibleChar) !== -1 || name.indexOf('"') !== -1) { - continue; - } + /** + * Toggles the specified class on/off. + * + * @method toggleClass + * @param {Element} elm Element to toggle class on. + * @param {[type]} cls Class to toggle on/off. + * @param {[type]} state Optional state to set. + */ + toggleClass: function (elm, cls, state) { + this.$$(elm).toggleClass(cls, state).each(function () { + if (this.className === '') { + DomQuery(this).attr('class', null); + } + }); + }, - // Don't allow behavior name or expression/comments within the values - if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(|\/\*|\*\//.test(value))) { - continue; - } + /** + * Shows the specified element(s) by ID by setting the "display" style. + * + * @method show + * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. + */ + show: function (elm) { + this.$$(elm).show(); + }, - // Opera will produce 700 instead of bold in their style values - if (name === 'font-weight' && value === '700') { - value = 'bold'; - } else if (name === 'color' || name === 'background-color') { // Lowercase colors like RED - value = value.toLowerCase(); - } + /** + * Hides the specified element(s) by ID by setting the "display" style. + * + * @method hide + * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide. + * @example + * // Hides an element by id in the document + * tinymce.DOM.hide('myid'); + */ + hide: function (elm) { + this.$$(elm).hide(); + }, - // Convert RGB colors to HEX - value = value.replace(rgbRegExp, toHex); + /** + * Returns true/false if the element is hidden or not by checking the "display" style. + * + * @method isHidden + * @param {String/Element} elm Id or element to check display state on. + * @return {Boolean} true/false if the element is hidden or not. + */ + isHidden: function (elm) { + return this.$$(elm).css('display') == 'none'; + }, - // Convert URLs and force them into url('value') format - value = value.replace(urlOrStrRegExp, processUrl); - styles[name] = isEncoded ? decode(value, true) : value; - } + /** + * Returns a unique id. This can be useful when generating elements on the fly. + * This method will not check if the element already exists. + * + * @method uniqueId + * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". + * @return {String} Unique id. + */ + uniqueId: function (prefix) { + return (!prefix ? 'mce_' : prefix) + (this.counter++); + }, + + /** + * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means + * URLs will get converted, hex color values fixed etc. Check processHTML for details. + * + * @method setHTML + * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of. + * @param {String} html HTML content to set as inner HTML of the element. + * @example + * // Sets the inner HTML of all paragraphs in the active editor + * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); + * + * // Sets the inner HTML of an element by id in the document + * tinymce.DOM.setHTML('mydiv', 'some inner html'); + */ + setHTML: function (elm, html) { + elm = this.$$(elm); + + if (isIE) { + elm.each(function (i, target) { + if (target.canHaveHTML === false) { + return; } - // Compress the styles to reduce it's size for example IE will expand styles - compress("border", "", true); - compress("border", "-width"); - compress("border", "-color"); - compress("border", "-style"); - compress("padding", ""); - compress("margin", ""); - compress2('border', 'border-width', 'border-style', 'border-color'); - // Remove pointless border, IE produces these - if (styles.border === 'medium none') { - delete styles.border; + // Remove all child nodes, IE keeps empty text nodes in DOM + while (target.firstChild) { + target.removeChild(target.firstChild); } - // IE 11 will produce a border-image: none when getting the style attribute from

    - // So let us assume it shouldn't be there - if (styles['border-image'] === 'none') { - delete styles['border-image']; + try { + // IE will remove comments from the beginning + // unless you padd the contents with something + target.innerHTML = '
    ' + html; + target.removeChild(target.firstChild); + } catch (ex) { + // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p + DomQuery('
    ').html('
    ' + html).contents().slice(1).appendTo(target); } - } - return styles; - }, + return html; + }); + } else { + elm.html(html); + } + }, - /** - * Serializes the specified style object into a string. - * - * @method serialize - * @param {Object} styles Object to serialize as string for example: {border: '1px solid red'} - * @param {String} elementName Optional element name, if specified only the styles that matches the schema will be serialized. - * @return {String} String representation of the style object for example: border: 1px solid red. - */ - serialize: function (styles, elementName) { - var css = '', name, value; + /** + * Returns the outer HTML of an element. + * + * @method getOuterHTML + * @param {String/Element} elm Element ID or element object to get outer HTML from. + * @return {String} Outer HTML string. + * @example + * tinymce.DOM.getOuterHTML(editorElement); + * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); + */ + getOuterHTML: function (elm) { + elm = this.get(elm); - function serializeStyles(name) { - var styleList, i, l, value; + // Older FF doesn't have outerHTML 3.6 is still used by some orgaizations + return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : DomQuery('
    ').append(DomQuery(elm).clone()).html(); + }, - styleList = validStyles[name]; - if (styleList) { - for (i = 0, l = styleList.length; i < l; i++) { - name = styleList[i]; - value = styles[name]; + /** + * Sets the specified outer HTML on an element or elements. + * + * @method setOuterHTML + * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. + * @param {Object} html HTML code to set as outer value for the element. + * @example + * // Sets the outer HTML of all paragraphs in the active editor + * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '
    some html
    '); + * + * // Sets the outer HTML of an element by id in the document + * tinymce.DOM.setOuterHTML('mydiv', '
    some html
    '); + */ + setOuterHTML: function (elm, html) { + var self = this; - if (value) { - css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; - } - } + self.$$(elm).each(function () { + try { + // Older FF doesn't have outerHTML 3.6 is still used by some organizations + if ("outerHTML" in this) { + this.outerHTML = html; + return; } + } catch (ex) { + // Ignore } - function isValid(name, elementName) { - var styleMap; + // OuterHTML for IE it sometimes produces an "unknown runtime error" + self.remove(DomQuery(this).html(html), true); + }); + }, - styleMap = invalidStyles['*']; - if (styleMap && styleMap[name]) { - return false; - } + /** + * Entity decodes a string. This method decodes any HTML entities, such as å. + * + * @method decode + * @param {String} s String to decode entities on. + * @return {String} Entity decoded string. + */ + decode: Entities.decode, - styleMap = invalidStyles[elementName]; - if (styleMap && styleMap[name]) { - return false; - } - - return true; - } - - // Serialize styles according to schema - if (elementName && validStyles) { - // Serialize global styles and element specific styles - serializeStyles('*'); - serializeStyles(elementName); - } else { - // Output the styles in the order they are inside the object - for (name in styles) { - value = styles[name]; - - if (value && (!invalidStyles || isValid(name, elementName))) { - css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';'; - } - } - } - - return css; - } - }; - }; - } -); + /** + * Entity encodes a string. This method encodes the most common entities, such as <>"&. + * + * @method encode + * @param {String} text String to encode with entities. + * @return {String} Entity encoded string. + */ + encode: Entities.encodeAllRaw, -/** - * 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 - */ + /** + * Inserts an element after the reference element. + * + * @method insertAfter + * @param {Element} node Element to insert after the reference. + * @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after. + * @return {Element/Array} Element that got added or an array with elements. + */ + insertAfter: function (node, referenceNode) { + referenceNode = this.get(referenceNode); -/** - * Utility class for various DOM manipulation and retrieval functions. - * - * @class tinymce.dom.DOMUtils - * @example - * // Add a class to an element by id in the page - * tinymce.DOM.addClass('someid', 'someclass'); - * - * // Add a class to an element by id inside the editor - * tinymce.activeEditor.dom.addClass('someid', 'someclass'); - */ -define( - 'tinymce.core.dom.DOMUtils', - [ - 'tinymce.core.dom.DomQuery', - 'tinymce.core.dom.EventUtils', - 'tinymce.core.dom.Range', - 'tinymce.core.dom.Sizzle', - 'tinymce.core.dom.StyleSheetLoader', - 'tinymce.core.dom.TreeWalker', - 'tinymce.core.Env', - 'tinymce.core.html.Entities', - 'tinymce.core.html.Schema', - 'tinymce.core.html.Styles', - 'tinymce.core.util.Tools' - ], - function (DomQuery, EventUtils, Range, Sizzle, StyleSheetLoader, TreeWalker, Env, Entities, Schema, Styles, Tools) { - // Shorten names - var each = Tools.each, is = Tools.is, grep = Tools.grep, trim = Tools.trim; - var isIE = Env.ie; - var simpleSelectorRe = /^([a-z0-9],?)+$/i; - var whiteSpaceRegExp = /^[ \t\r\n]*$/; + return this.run(node, function (node) { + var parent, nextSibling; - function setupAttrHooks(domUtils, settings) { - var attrHooks = {}, keepValues = settings.keep_values, keepUrlHook; + parent = referenceNode.parentNode; + nextSibling = referenceNode.nextSibling; - keepUrlHook = { - set: function ($elm, value, name) { - if (settings.url_converter) { - value = settings.url_converter.call(settings.url_converter_scope || domUtils, value, name, $elm[0]); + if (nextSibling) { + parent.insertBefore(node, nextSibling); + } else { + parent.appendChild(node); } - $elm.attr('data-mce-' + name, value).attr(name, value); - }, - - get: function ($elm, name) { - return $elm.attr('data-mce-' + name) || $elm.attr(name); - } - }; - - attrHooks = { - style: { - set: function ($elm, value) { - if (value !== null && typeof value === 'object') { - $elm.css(value); - return; - } - - if (keepValues) { - $elm.attr('data-mce-style', value); - } - - $elm.attr('style', value); - }, - - get: function ($elm) { - var value = $elm.attr('data-mce-style') || $elm.attr('style'); + return node; + }); + }, - value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName); + /** + * Replaces the specified element or elements with the new element specified. The new element will + * be cloned if multiple input elements are passed in. + * + * @method replace + * @param {Element} newElm New element to replace old ones with. + * @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace. + * @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added + * to new ones. + */ + replace: function (newElm, oldElm, keepChildren) { + var self = this; - return value; + return self.run(oldElm, function (oldElm) { + if (is(oldElm, 'array')) { + newElm = newElm.cloneNode(true); } - } - }; - - if (keepValues) { - attrHooks.href = attrHooks.src = keepUrlHook; - } - - return attrHooks; - } - - function updateInternalStyleAttr(domUtils, $elm) { - var value = $elm.attr('style'); - - value = domUtils.serializeStyle(domUtils.parseStyle(value), $elm[0].nodeName); - - if (!value) { - value = null; - } - - $elm.attr('data-mce-style', value); - } - - function nodeIndex(node, normalized) { - var idx = 0, lastNodeType, nodeType; - - if (node) { - for (lastNodeType = node.nodeType, node = node.previousSibling; node; node = node.previousSibling) { - nodeType = node.nodeType; - // Normalize text nodes - if (normalized && nodeType == 3) { - if (nodeType == lastNodeType || !node.nodeValue.length) { - continue; - } + if (keepChildren) { + each(grep(oldElm.childNodes), function (node) { + newElm.appendChild(node); + }); } - idx++; - lastNodeType = nodeType; - } - } - - return idx; - } - - /** - * Constructs a new DOMUtils instance. Consult the Wiki for more details on settings etc for this class. - * - * @constructor - * @method DOMUtils - * @param {Document} doc Document reference to bind the utility class to. - * @param {settings} settings Optional settings collection. - */ - function DOMUtils(doc, settings) { - var self = this, blockElementsMap; - - self.doc = doc; - self.win = window; - self.files = {}; - self.counter = 0; - self.stdMode = !isIE || doc.documentMode >= 8; - self.boxModel = !isIE || doc.compatMode == "CSS1Compat" || self.stdMode; - self.styleSheetLoader = new StyleSheetLoader(doc); - self.boundEvents = []; - self.settings = settings = settings || {}; - self.schema = settings.schema ? settings.schema : new Schema({}); - self.styles = new Styles({ - url_converter: settings.url_converter, - url_converter_scope: settings.url_converter_scope - }, settings.schema); - self.fixDoc(doc); - self.events = settings.ownEvents ? new EventUtils(settings.proxy) : EventUtils.Event; - self.attrHooks = setupAttrHooks(self, settings); - blockElementsMap = settings.schema ? settings.schema.getBlockElements() : {}; - self.$ = DomQuery.overrideDefaults(function () { - return { - context: doc, - element: self.getRoot() - }; - }); + return oldElm.parentNode.replaceChild(newElm, oldElm); + }); + }, /** - * Returns true/false if the specified element is a block element or not. + * Renames the specified element and keeps its attributes and children. * - * @method isBlock - * @param {Node/String} node Element/Node to check. - * @return {Boolean} True/False state if the node is a block element or not. + * @method rename + * @param {Element} elm Element to rename. + * @param {String} name Name of the new element. + * @return {Element} New element or the old element if it needed renaming. */ - self.isBlock = function (node) { - // Fix for #5446 - if (!node) { - return false; - } - - // This function is called in module pattern style since it might be executed with the wrong this scope - var type = node.nodeType; + rename: function (elm, name) { + var self = this, newElm; - // If it's a node then check the type and use the nodeName - if (type) { - return !!(type === 1 && blockElementsMap[node.nodeName]); - } + if (elm.nodeName != name.toUpperCase()) { + // Rename block element + newElm = self.create(name); - return !!blockElementsMap[node]; - }; - } + // Copy attribs to new block + each(self.getAttribs(elm), function (attrNode) { + self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName)); + }); - DOMUtils.prototype = { - $$: function (elm) { - if (typeof elm == 'string') { - elm = this.get(elm); + // Replace block + self.replace(newElm, elm, 1); } - return this.$(elm); + return newElm || elm; }, - root: null, - - fixDoc: function (doc) { - var settings = this.settings, name; + /** + * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. + * + * @method findCommonAncestor + * @param {Element} a Element to find common ancestor of. + * @param {Element} b Element to find common ancestor of. + * @return {Element} Common ancestor element of the two input elements. + */ + findCommonAncestor: function (a, b) { + var ps = a, pe; - if (isIE && settings.schema) { - // Add missing HTML 4/5 elements to IE - ('abbr article aside audio canvas ' + - 'details figcaption figure footer ' + - 'header hgroup mark menu meter nav ' + - 'output progress section summary ' + - 'time video').replace(/\w+/g, function (name) { - doc.createElement(name); - }); + while (ps) { + pe = b; - // Create all custom elements - for (name in settings.schema.getCustomElements()) { - doc.createElement(name); + while (pe && ps != pe) { + pe = pe.parentNode; } - } - }, - clone: function (node, deep) { - var self = this, clone, doc; + if (ps == pe) { + break; + } - // TODO: Add feature detection here in the future - if (!isIE || node.nodeType !== 1 || deep) { - return node.cloneNode(deep); + ps = ps.parentNode; } - doc = self.doc; - - // Make a HTML5 safe shallow copy - if (!deep) { - clone = doc.createElement(node.nodeName); - - // Copy attribs - each(self.getAttribs(node), function (attr) { - self.setAttrib(clone, attr.nodeName, self.getAttrib(node, attr.nodeName)); - }); - - return clone; + if (!ps && a.ownerDocument) { + return a.ownerDocument.documentElement; } - return clone.firstChild; - }, - - /** - * Returns the root node of the document. This is normally the body but might be a DIV. Parents like getParent will not - * go above the point of this root node. - * - * @method getRoot - * @return {Element} Root element for the utility class. - */ - getRoot: function () { - var self = this; - - return self.settings.root_element || self.doc.body; + return ps; }, /** - * Returns the viewport of the window. + * Parses the specified RGB color value and returns a hex version of that color. * - * @method getViewPort - * @param {Window} win Optional window to get viewport of. - * @return {Object} Viewport object with fields x, y, w and h. + * @method toHex + * @param {String} rgbVal RGB string value like rgb(1,2,3) + * @return {String} Hex version of that RGB value like #FF00FF. */ - getViewPort: function (win) { - var doc, rootElm; - - win = !win ? this.win : win; - doc = win.document; - rootElm = this.boxModel ? doc.documentElement : doc.body; - - // Returns viewport size excluding scrollbars - return { - x: win.pageXOffset || rootElm.scrollLeft, - y: win.pageYOffset || rootElm.scrollTop, - w: win.innerWidth || rootElm.clientWidth, - h: win.innerHeight || rootElm.clientHeight - }; + toHex: function (rgbVal) { + return this.styles.toHex(Tools.trim(rgbVal)); }, /** - * Returns the rectangle for a specific element. + * Executes the specified function on the element by id or dom element node or array of elements/id. * - * @method getRect - * @param {Element/String} elm Element object or element ID to get rectangle from. - * @return {object} Rectangle for specified element object with x, y, w, h fields. + * @method run + * @param {String/Element/Array} elm ID or DOM element object or array with ids or elements. + * @param {function} func Function to execute for each item. + * @param {Object} scope Optional scope to execute the function in. + * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. */ - getRect: function (elm) { - var self = this, pos, size; + run: function (elm, func, scope) { + var self = this, result; - elm = self.get(elm); - pos = self.getPos(elm); - size = self.getSize(elm); + if (typeof elm === 'string') { + elm = self.get(elm); + } - return { - x: pos.x, y: pos.y, - w: size.w, h: size.h - }; - }, + if (!elm) { + return false; + } - /** - * Returns the size dimensions of the specified element. - * - * @method getSize - * @param {Element/String} elm Element object or element ID to get rectangle from. - * @return {object} Rectangle for specified element object with w, h fields. - */ - getSize: function (elm) { - var self = this, w, h; + scope = scope || this; + if (!elm.nodeType && (elm.length || elm.length === 0)) { + result = []; - elm = self.get(elm); - w = self.getStyle(elm, 'width'); - h = self.getStyle(elm, 'height'); + each(elm, function (elm, i) { + if (elm) { + if (typeof elm == 'string') { + elm = self.get(elm); + } - // Non pixel value, then force offset/clientWidth - if (w.indexOf('px') === -1) { - w = 0; - } + result.push(func.call(scope, elm, i)); + } + }); - // Non pixel value, then force offset/clientWidth - if (h.indexOf('px') === -1) { - h = 0; + return result; } - return { - w: parseInt(w, 10) || elm.offsetWidth || elm.clientWidth, - h: parseInt(h, 10) || elm.offsetHeight || elm.clientHeight - }; - }, - - /** - * Returns a node by the specified selector function. This function will - * loop through all parent nodes and call the specified function for each node. - * If the function then returns true indicating that it has found what it was looking for, the loop execution will then end - * and the node it found will be returned. - * - * @method getParent - * @param {Node/String} node DOM node to search parents on or ID string. - * @param {function} selector Selection function or CSS selector to execute on each node. - * @param {Node} root Optional root element, never go beyond this point. - * @return {Node} DOM Node or null if it wasn't found. - */ - getParent: function (node, selector, root) { - return this.getParents(node, selector, root, false); + return func.call(scope, elm); }, /** - * Returns a node list of all parents matching the specified selector function or pattern. - * If the function then returns true indicating that it has found what it was looking for and that node will be collected. + * Returns a NodeList with attributes for the element. * - * @method getParents - * @param {Node/String} node DOM node to search parents on or ID string. - * @param {function} selector Selection function to execute on each node or CSS pattern. - * @param {Node} root Optional root element, never go beyond this point. - * @return {Array} Array of nodes or null if it wasn't found. + * @method getAttribs + * @param {HTMLElement/string} elm Element node or string id to get attributes from. + * @return {NodeList} NodeList with attributes. */ - getParents: function (node, selector, root, collect) { - var self = this, selectorVal, result = []; + getAttribs: function (elm) { + var attrs; - node = self.get(node); - collect = collect === undefined; + elm = this.get(elm); - // Default root on inline mode - root = root || (self.getRoot().nodeName != 'BODY' ? self.getRoot().parentNode : null); + if (!elm) { + return []; + } - // Wrap node name as func - if (is(selector, 'string')) { - selectorVal = selector; + if (isIE) { + attrs = []; - if (selector === '*') { - selector = function (node) { - return node.nodeType == 1; - }; - } else { - selector = function (node) { - return self.is(node, selectorVal); - }; + // Object will throw exception in IE + if (elm.nodeName == 'OBJECT') { + return elm.attributes; } - } - while (node) { - if (node == root || !node.nodeType || node.nodeType === 9) { - break; + // IE doesn't keep the selected attribute if you clone option elements + if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) { + attrs.push({ specified: 1, nodeName: 'selected' }); } - if (!selector || selector(node)) { - if (collect) { - result.push(node); - } else { - return node; - } - } + // It's crazy that this is faster in IE but it's because it returns all attributes all the time + var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi; + elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function (a) { + attrs.push({ specified: 1, nodeName: a }); + }); - node = node.parentNode; + return attrs; } - return collect ? result : null; + return elm.attributes; }, /** - * Returns the specified element by ID or the input element if it isn't a string. + * Returns true/false if the specified node is to be considered empty or not. * - * @method get - * @param {String/Element} n Element id to look for or element to just pass though. - * @return {Element} Element matching the specified id or null if it wasn't found. - */ - get: function (elm) { - var name; + * @example + * tinymce.DOM.isEmpty(node, {img: true}); + * @method isEmpty + * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. + * @return {Boolean} true/false if the node is empty or not. + */ + isEmpty: function (node, elements) { + var self = this, i, attributes, type, whitespace, walker, name, brCount = 0; - if (elm && this.doc && typeof elm == 'string') { - name = elm; - elm = this.doc.getElementById(elm); + node = node.firstChild; + if (node) { + walker = new TreeWalker(node, node.parentNode); + elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null); + whitespace = self.schema ? self.schema.getWhiteSpaceElements() : {}; - // IE and Opera returns meta elements when they match the specified input ID, but getElementsByName seems to do the trick - if (elm && elm.id !== name) { - return this.doc.getElementsByName(name)[1]; - } + do { + type = node.nodeType; + + if (type === 1) { + // Ignore bogus elements + var bogusVal = node.getAttribute('data-mce-bogus'); + if (bogusVal) { + node = walker.next(bogusVal === 'all'); + continue; + } + + // Keep empty elements like + name = node.nodeName.toLowerCase(); + if (elements && elements[name]) { + // Ignore single BR elements in blocks like


    or


    + if (name === 'br') { + brCount++; + node = walker.next(); + continue; + } + + return false; + } + + // Keep elements with data-bookmark attributes or name attribute like + attributes = self.getAttribs(node); + i = attributes.length; + while (i--) { + name = attributes[i].nodeName; + if (name === "name" || name === 'data-mce-bookmark') { + return false; + } + } + } + + // Keep comment nodes + if (type == 8) { + return false; + } + + // Keep non whitespace text nodes + if (type === 3 && !whiteSpaceRegExp.test(node.nodeValue)) { + return false; + } + + // Keep whitespace preserve elements + if (type === 3 && node.parentNode && whitespace[node.parentNode.nodeName] && whiteSpaceRegExp.test(node.nodeValue)) { + return false; + } + + node = walker.next(); + } while (node); } - return elm; + return brCount <= 1; }, /** - * Returns the next node that matches selector or function + * Creates a new DOM Range object. This will use the native DOM Range API if it's + * available. If it's not, it will fall back to the custom TinyMCE implementation. * - * @method getNext - * @param {Node} node Node to find siblings from. - * @param {String/function} selector Selector CSS expression or function. - * @return {Node} Next node item matching the selector or null if it wasn't found. + * @method createRng + * @return {DOMRange} DOM Range object. + * @example + * var rng = tinymce.DOM.createRng(); + * alert(rng.startContainer + "," + rng.startOffset); */ - getNext: function (node, selector) { - return this._findSib(node, selector, 'nextSibling'); + createRng: function () { + return this.doc.createRange(); }, /** - * Returns the previous node that matches selector or function + * Returns the index of the specified node within its parent. * - * @method getPrev - * @param {Node} node Node to find siblings from. - * @param {String/function} selector Selector CSS expression or function. - * @return {Node} Previous node item matching the selector or null if it wasn't found. + * @method nodeIndex + * @param {Node} node Node to look for. + * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization. + * @return {Number} Index of the specified node. */ - getPrev: function (node, selector) { - return this._findSib(node, selector, 'previousSibling'); - }, - - // #ifndef jquery + nodeIndex: nodeIndex, /** - * Selects specific elements by a CSS level 3 pattern. For example "div#a1 p.test". - * This function is optimized for the most common patterns needed in TinyMCE but it also performs well enough - * on more complex patterns. - * - * @method select - * @param {String} selector CSS level 3 pattern to select/find elements by. - * @param {Object} scope Optional root element/scope element to search in. - * @return {Array} Array with all matched elements. - * @example - * // Adds a class to all paragraphs in the currently active editor - * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); + * Splits an element into two new elements and places the specified split + * element or elements between the new ones. For example splitting the paragraph at the bold element in + * this example

    abcabc123

    would produce

    abc

    abc

    123

    . * - * // Adds a class to all spans that have the test class in the currently active editor - * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('span.test'), 'someclass') + * @method split + * @param {Element} parentElm Parent element to split. + * @param {Element} splitElm Element to split at. + * @param {Element} replacementElm Optional replacement element to replace the split element with. + * @return {Element} Returns the split element or the replacement element if that is specified. */ - select: function (selector, scope) { - var self = this; + split: function (parentElm, splitElm, replacementElm) { + var self = this, r = self.createRng(), bef, aft, pa; - /*eslint new-cap:0 */ - return Sizzle(selector, self.get(scope) || self.settings.root_element || self.doc, []); - }, + // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense + // but we don't want that in our code since it serves no purpose for the end user + // For example splitting this html at the bold element: + //

    text 1CHOPtext 2

    + // would produce: + //

    text 1

    CHOP

    text 2

    + // this function will then trim off empty edges and produce: + //

    text 1

    CHOP

    text 2

    + function trimNode(node) { + var i, children = node.childNodes, type = node.nodeType; - /** - * Returns true/false if the specified element matches the specified css pattern. - * - * @method is - * @param {Node/NodeList} elm DOM node to match or an array of nodes to match. - * @param {String} selector CSS pattern to match the element against. - */ - is: function (elm, selector) { - var i; + function surroundedBySpans(node) { + var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; + var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; + return previousIsSpan && nextIsSpan; + } - if (!elm) { - return false; - } + if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { + return; + } - // If it isn't an array then try to do some simple selectors instead of Sizzle for to boost performance - if (elm.length === undefined) { - // Simple all selector - if (selector === '*') { - return elm.nodeType == 1; + for (i = children.length - 1; i >= 0; i--) { + trimNode(children[i]); } - // Simple selector just elements - if (simpleSelectorRe.test(selector)) { - selector = selector.toLowerCase().split(/,/); - elm = elm.nodeName.toLowerCase(); + if (type != 9) { + // Keep non whitespace text nodes + if (type == 3 && node.nodeValue.length > 0) { + // If parent element isn't a block or there isn't any useful contents for example "

    " + // Also keep text nodes with only spaces if surrounded by spans. + // eg. "

    a b

    " should keep space between a and b + var trimmedLength = trim(node.nodeValue).length; + if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { + return; + } + } else if (type == 1) { + // If the only child is a bookmark then move it up + children = node.childNodes; - for (i = selector.length - 1; i >= 0; i--) { - if (selector[i] == elm) { - return true; + // TODO fix this complex if + if (children.length == 1 && children[0] && children[0].nodeType == 1 && + children[0].getAttribute('data-mce-type') == 'bookmark') { + node.parentNode.insertBefore(children[0], node); + } + + // Keep non empty elements or img, hr etc + if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { + return; } } - return false; + self.remove(node); } - } - // Is non element - if (elm.nodeType && elm.nodeType != 1) { - return false; + return node; } - var elms = elm.nodeType ? [elm] : elm; + if (parentElm && splitElm) { + // Get before chunk + r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); + r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); + bef = r.extractContents(); - /*eslint new-cap:0 */ - return Sizzle(selector, elms[0].ownerDocument || elms[0], null, elms).length > 0; - }, + // Get after chunk + r = self.createRng(); + r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); + r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); + aft = r.extractContents(); - // #endif + // Insert before chunk + pa = parentElm.parentNode; + pa.insertBefore(trimNode(bef), parentElm); + + // Insert middle chunk + if (replacementElm) { + pa.insertBefore(replacementElm, parentElm); + //pa.replaceChild(replacementElm, splitElm); + } else { + pa.insertBefore(splitElm, parentElm); + } + + // Insert after chunk + pa.insertBefore(trimNode(aft), parentElm); + self.remove(parentElm); + + return replacementElm || splitElm; + } + }, /** - * Adds the specified element to another element or elements. + * Adds an event handler to the specified object. * - * @method add - * @param {String/Element/Array} parentElm Element id string, DOM node element or array of ids or elements to add to. - * @param {String/Element} name Name of new element to add or existing element to add. - * @param {Object} attrs Optional object collection with arguments to add to the new element(s). - * @param {String} html Optional inner HTML contents to add for each element. - * @param {Boolean} create Optional flag if the element should be created or added. - * @return {Element/Array} Element that got created, or an array of created elements if multiple input elements - * were passed in. - * @example - * // Adds a new paragraph to the end of the active editor - * tinymce.activeEditor.dom.add(tinymce.activeEditor.getBody(), 'p', {title: 'my title'}, 'Some content'); + * @method bind + * @param {Element/Document/Window/Array} target Target element to bind events to. + * handler to or an array of elements/ids/documents. + * @param {String} name Name of event handler to add, for example: click. + * @param {function} func Function to execute when the event occurs. + * @param {Object} scope Optional scope to execute the function in. + * @return {function} Function callback handler the same as the one passed in. */ - add: function (parentElm, name, attrs, html, create) { + bind: function (target, name, func, scope) { var self = this; - return this.run(parentElm, function (parentElm) { - var newElm; - - newElm = is(name, 'string') ? self.doc.createElement(name) : name; - self.setAttribs(newElm, attrs); + if (Tools.isArray(target)) { + var i = target.length; - if (html) { - if (html.nodeType) { - newElm.appendChild(html); - } else { - self.setHTML(newElm, html); - } + while (i--) { + target[i] = self.bind(target[i], name, func, scope); } - return !create ? parentElm.appendChild(newElm) : newElm; - }); - }, + return target; + } - /** - * Creates a new element. - * - * @method create - * @param {String} name Name of new element. - * @param {Object} attrs Optional object name/value collection with element attributes. - * @param {String} html Optional HTML string to set as inner HTML of the element. - * @return {Element} HTML DOM node element that got created. - * @example - * // Adds an element where the caret/selection is in the active editor - * var el = tinymce.activeEditor.dom.create('div', {id: 'test', 'class': 'myclass'}, 'some content'); - * tinymce.activeEditor.selection.setNode(el); - */ - create: function (name, attrs, html) { - return this.add(this.doc.createElement(name), name, attrs, html, 1); + // Collect all window/document events bound by editor instance + if (self.settings.collect && (target === self.doc || target === self.win)) { + self.boundEvents.push([target, name, func, scope]); + } + + return self.events.bind(target, name, func, scope || self); }, /** - * Creates HTML string for element. The element will be closed unless an empty inner HTML string is passed in. + * Removes the specified event handler by name and function from an element or collection of elements. * - * @method createHTML - * @param {String} name Name of new element. - * @param {Object} attrs Optional object name/value collection with element attributes. - * @param {String} html Optional HTML string to set as inner HTML of the element. - * @return {String} String with new HTML element, for example: test. - * @example - * // Creates a html chunk and inserts it at the current selection/caret location - * tinymce.activeEditor.selection.setContent(tinymce.activeEditor.dom.createHTML('a', {href: 'test.html'}, 'some line')); + * @method unbind + * @param {Element/Document/Window/Array} target Target element to unbind events on. + * @param {String} name Event handler name, for example: "click" + * @param {function} func Function to remove. + * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements + * were passed in. */ - createHTML: function (name, attrs, html) { - var outHtml = '', key; + unbind: function (target, name, func) { + var self = this, i; - outHtml += '<' + name; + if (Tools.isArray(target)) { + i = target.length; - for (key in attrs) { - if (attrs.hasOwnProperty(key) && attrs[key] !== null && typeof attrs[key] != 'undefined') { - outHtml += ' ' + key + '="' + this.encode(attrs[key]) + '"'; + while (i--) { + target[i] = self.unbind(target[i], name, func); } + + return target; } - // A call to tinymce.is doesn't work for some odd reason on IE9 possible bug inside their JS runtime - if (typeof html != "undefined") { - return outHtml + '>' + html + ''; + // Remove any bound events matching the input + if (self.boundEvents && (target === self.doc || target === self.win)) { + i = self.boundEvents.length; + + while (i--) { + var item = self.boundEvents[i]; + + if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { + this.events.unbind(item[0], item[1], item[2]); + } + } } - return outHtml + ' />'; + return this.events.unbind(target, name, func); }, /** - * Creates a document fragment out of the specified HTML string. + * Fires the specified event name with object on target. * - * @method createFragment - * @param {String} html Html string to create fragment from. - * @return {DocumentFragment} Document fragment node. + * @method fire + * @param {Node/Document/Window} target Target element or object to fire event on. + * @param {String} name Name of the event to fire. + * @param {Object} evt Event object to send. + * @return {Event} Event object. */ - createFragment: function (html) { - var frag, node, doc = this.doc, container; + fire: function (target, name, evt) { + return this.events.fire(target, name, evt); + }, - container = doc.createElement("div"); - frag = doc.createDocumentFragment(); + // Returns the content editable state of a node + getContentEditable: function (node) { + var contentEditable; - if (html) { - container.innerHTML = html; + // Check type + if (!node || node.nodeType != 1) { + return null; } - while ((node = container.firstChild)) { - frag.appendChild(node); + // Check for fake content editable + contentEditable = node.getAttribute("data-mce-contenteditable"); + if (contentEditable && contentEditable !== "inherit") { + return contentEditable; } - return frag; + // Check for real content editable + return node.contentEditable !== "inherit" ? node.contentEditable : null; }, - /** - * Removes/deletes the specified element(s) from the DOM. - * - * @method remove - * @param {String/Element/Array} node ID of element or DOM element object or array containing multiple elements/ids. - * @param {Boolean} keepChildren Optional state to keep children or not. If set to true all children will be - * placed at the location of the removed element. - * @return {Element/Array} HTML DOM element that got removed, or an array of removed elements if multiple input elements - * were passed in. - * @example - * // Removes all paragraphs in the active editor - * tinymce.activeEditor.dom.remove(tinymce.activeEditor.dom.select('p')); - * - * // Removes an element by id in the document - * tinymce.DOM.remove('mydiv'); - */ - remove: function (node, keepChildren) { - node = this.$$(node); + getContentEditableParent: function (node) { + var root = this.getRoot(), state = null; - if (keepChildren) { - node.each(function () { - var child; + for (; node && node !== root; node = node.parentNode) { + state = this.getContentEditable(node); - while ((child = this.firstChild)) { - if (child.nodeType == 3 && child.data.length === 0) { - this.removeChild(child); - } else { - this.parentNode.insertBefore(child, this); - } - } - }).remove(); - } else { - node.remove(); + if (state !== null) { + break; + } } - return node.length > 1 ? node.toArray() : node[0]; + return state; }, /** - * Sets the CSS style value on a HTML element. The name can be a camelcase string - * or the CSS style name like background-color. - * - * @method setStyle - * @param {String/Element/Array} elm HTML element/Array of elements to set CSS style value on. - * @param {String} name Name of the style value to set. - * @param {String} value Value to set on the style. - * @example - * // Sets a style value on all paragraphs in the currently active editor - * tinymce.activeEditor.dom.setStyle(tinymce.activeEditor.dom.select('p'), 'background-color', 'red'); + * Destroys all internal references to the DOM to solve IE leak issues. * - * // Sets a style value to an element by id in the current document - * tinymce.DOM.setStyle('mydiv', 'background-color', 'red'); + * @method destroy */ - setStyle: function (elm, name, value) { - elm = this.$$(elm).css(name, value); + destroy: function () { + var self = this; - if (this.settings.update_styles) { - updateInternalStyleAttr(this, elm); - } - }, + // Unbind all events bound to window/document by editor instance + if (self.boundEvents) { + var i = self.boundEvents.length; - /** - * Returns the current style or runtime/computed value of an element. - * - * @method getStyle - * @param {String/Element} elm HTML element or element id string to get style from. - * @param {String} name Style name to return. - * @param {Boolean} computed Computed style. - * @return {String} Current style or computed style value of an element. - */ - getStyle: function (elm, name, computed) { - elm = this.$$(elm); + while (i--) { + var item = self.boundEvents[i]; + this.events.unbind(item[0], item[1], item[2]); + } - if (computed) { - return elm.css(name); + self.boundEvents = null; } - // Camelcase it, if needed - name = name.replace(/-(\D)/g, function (a, b) { - return b.toUpperCase(); - }); - - if (name == 'float') { - name = Env.ie && Env.ie < 12 ? 'styleFloat' : 'cssFloat'; + // Restore sizzle document to window.document + // Since the current document might be removed producing "Permission denied" on IE see #6325 + if (Sizzle.setDocument) { + Sizzle.setDocument(); } - return elm[0] && elm[0].style ? elm[0].style[name] : undefined; + self.win = self.doc = self.root = self.events = self.frag = null; }, - /** - * Sets multiple styles on the specified element(s). - * - * @method setStyles - * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set styles on. - * @param {Object} styles Name/Value collection of style items to add to the element(s). - * @example - * // Sets styles on all paragraphs in the currently active editor - * tinymce.activeEditor.dom.setStyles(tinymce.activeEditor.dom.select('p'), {'background-color': 'red', 'color': 'green'}); - * - * // Sets styles to an element by id in the current document - * tinymce.DOM.setStyles('mydiv', {'background-color': 'red', 'color': 'green'}); - */ - setStyles: function (elm, styles) { - elm = this.$$(elm).css(styles); + isChildOf: function (node, parent) { + while (node) { + if (parent === node) { + return true; + } - if (this.settings.update_styles) { - updateInternalStyleAttr(this, elm); + node = node.parentNode; } + + return false; }, - /** - * Removes all attributes from an element or elements. - * - * @method removeAllAttribs - * @param {Element/String/Array} e DOM element, element id string or array of elements/ids to remove attributes from. - */ - removeAllAttribs: function (e) { - return this.run(e, function (e) { - var i, attrs = e.attributes; - for (i = attrs.length - 1; i >= 0; i--) { - e.removeAttributeNode(attrs.item(i)); - } - }); - }, - - /** - * Sets the specified attribute of an element or elements. - * - * @method setAttrib - * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attribute on. - * @param {String} name Name of attribute to set. - * @param {String} value Value to set on the attribute - if this value is falsy like null, 0 or '' it will remove - * the attribute instead. - * @example - * // Sets class attribute on all paragraphs in the active editor - * tinymce.activeEditor.dom.setAttrib(tinymce.activeEditor.dom.select('p'), 'class', 'myclass'); - * - * // Sets class attribute on a specific element in the current page - * tinymce.dom.setAttrib('mydiv', 'class', 'myclass'); - */ - setAttrib: function (elm, name, value) { - var self = this, originalValue, hook, settings = self.settings; + // #ifdef debug - if (value === '') { - value = null; - } + dumpRng: function (r) { + return ( + 'startContainer: ' + r.startContainer.nodeName + + ', startOffset: ' + r.startOffset + + ', endContainer: ' + r.endContainer.nodeName + + ', endOffset: ' + r.endOffset + ); + }, - elm = self.$$(elm); - originalValue = elm.attr(name); + // #endif - if (!elm.length) { - return; - } + _findSib: function (node, selector, name) { + var self = this, func = selector; - hook = self.attrHooks[name]; - if (hook && hook.set) { - hook.set(elm, value, name); - } else { - elm.attr(name, value); - } + if (node) { + // If expression make a function of it using is + if (typeof func == 'string') { + func = function (node) { + return self.is(node, selector); + }; + } - if (originalValue != value && settings.onSetAttrib) { - settings.onSetAttrib({ - attrElm: elm, - attrName: name, - attrValue: value - }); + // Loop all siblings + for (node = node[name]; node; node = node[name]) { + if (func(node)) { + return node; + } + } } - }, - /** - * Sets two or more specified attributes of an element or elements. - * - * @method setAttribs - * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set attributes on. - * @param {Object} attrs Name/Value collection of attribute items to add to the element(s). - * @example - * // Sets class and title attributes on all paragraphs in the active editor - * tinymce.activeEditor.dom.setAttribs(tinymce.activeEditor.dom.select('p'), {'class': 'myclass', title: 'some title'}); - * - * // Sets class and title attributes on a specific element in the current page - * tinymce.DOM.setAttribs('mydiv', {'class': 'myclass', title: 'some title'}); - */ - setAttribs: function (elm, attrs) { - var self = this; + return null; + } + }; - self.$$(elm).each(function (i, node) { - each(attrs, function (value, name) { - self.setAttrib(node, name, value); - }); - }); - }, + /** + * Instance of DOMUtils for the current document. + * + * @static + * @property DOM + * @type tinymce.dom.DOMUtils + * @example + * // Example of how to add a class to some element by id + * tinymce.DOM.addClass('someid', 'someclass'); + */ + DOMUtils.DOM = new DOMUtils(document); + DOMUtils.nodeIndex = nodeIndex; - /** - * Returns the specified attribute by name. - * - * @method getAttrib - * @param {String/Element} elm Element string id or DOM element to get attribute from. - * @param {String} name Name of attribute to get. - * @param {String} defaultVal Optional default value to return if the attribute didn't exist. - * @return {String} Attribute value string, default value or null if the attribute wasn't found. - */ - getAttrib: function (elm, name, defaultVal) { - var self = this, hook, value; + return DOMUtils; + } +); - elm = self.$$(elm); +/** + * ScriptLoader.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 + */ - if (elm.length) { - hook = self.attrHooks[name]; +/*globals console*/ - if (hook && hook.get) { - value = hook.get(elm, name); - } else { - value = elm.attr(name); - } - } +/** + * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks + * when various items gets loaded. This class is useful to load external JavaScript files. + * + * @class tinymce.dom.ScriptLoader + * @example + * // Load a script from a specific URL using the global script loader + * tinymce.ScriptLoader.load('somescript.js'); + * + * // Load a script using a unique instance of the script loader + * var scriptLoader = new tinymce.dom.ScriptLoader(); + * + * scriptLoader.load('somescript.js'); + * + * // Load multiple scripts + * var scriptLoader = new tinymce.dom.ScriptLoader(); + * + * scriptLoader.add('somescript1.js'); + * scriptLoader.add('somescript2.js'); + * scriptLoader.add('somescript3.js'); + * + * scriptLoader.loadQueue(function() { + * alert('All scripts are now loaded.'); + * }); + */ +define( + 'tinymce.core.dom.ScriptLoader', + [ + 'global!document', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.util.Tools' + ], + function (document, DOMUtils, Tools) { + var DOM = DOMUtils.DOM; + var each = Tools.each, grep = Tools.grep; - if (typeof value == 'undefined') { - value = defaultVal || ''; - } + var isFunction = function (f) { + return typeof f === 'function'; + }; - return value; - }, + function ScriptLoader() { + var QUEUED = 0, + LOADING = 1, + LOADED = 2, + FAILED = 3, + states = {}, + queue = [], + scriptLoadedCallbacks = {}, + queueLoadedCallbacks = [], + loading = 0, + undef; /** - * Returns the absolute x, y position of a node. The position will be returned in an object with x, y fields. + * Loads a specific script directly without adding it to the load queue. * - * @method getPos - * @param {Element/String} elm HTML element or element id to get x, y position from. - * @param {Element} rootElm Optional root element to stop calculations at. - * @return {object} Absolute position of the specified element object with x, y fields. + * @method load + * @param {String} url Absolute URL to script to add. + * @param {function} callback Optional success callback function when the script loaded successfully. + * @param {function} callback Optional failure callback function when the script failed to load. */ - getPos: function (elm, rootElm) { - var self = this, x = 0, y = 0, offsetParent, doc = self.doc, body = doc.body, pos; + function loadScript(url, success, failure) { + var dom = DOM, elm, id; - elm = self.get(elm); - rootElm = rootElm || body; + // Execute callback when script is loaded + function done() { + dom.remove(id); - if (elm) { - // Use getBoundingClientRect if it exists since it's faster than looping offset nodes - // Fallback to offsetParent calculations if the body isn't static better since it stops at the body root - if (rootElm === body && elm.getBoundingClientRect && DomQuery(body).css('position') === 'static') { - pos = elm.getBoundingClientRect(); - rootElm = self.boxModel ? doc.documentElement : body; + if (elm) { + elm.onreadystatechange = elm.onload = elm = null; + } - // Add scroll offsets from documentElement or body since IE with the wrong box model will use d.body and so do WebKit - // Also remove the body/documentelement clientTop/clientLeft on IE 6, 7 since they offset the position - x = pos.left + (doc.documentElement.scrollLeft || body.scrollLeft) - rootElm.clientLeft; - y = pos.top + (doc.documentElement.scrollTop || body.scrollTop) - rootElm.clientTop; + success(); + } - return { x: x, y: y }; - } + function error() { + /*eslint no-console:0 */ - offsetParent = elm; - while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { - x += offsetParent.offsetLeft || 0; - y += offsetParent.offsetTop || 0; - offsetParent = offsetParent.offsetParent; - } + // We can't mark it as done if there is a load error since + // A) We don't want to produce 404 errors on the server and + // B) the onerror event won't fire on all browsers. + // done(); - offsetParent = elm.parentNode; - while (offsetParent && offsetParent != rootElm && offsetParent.nodeType) { - x -= offsetParent.scrollLeft || 0; - y -= offsetParent.scrollTop || 0; - offsetParent = offsetParent.parentNode; + if (isFunction(failure)) { + failure(); + } else { + // Report the error so it's easier for people to spot loading errors + if (typeof console !== "undefined" && console.log) { + console.log("Failed to load script: " + url); + } } } - return { x: x, y: y }; - }, + id = dom.uniqueId(); + + // Create new script element + elm = document.createElement('script'); + elm.id = id; + elm.type = 'text/javascript'; + elm.src = Tools._addCacheSuffix(url); + + // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly + if ("onreadystatechange" in elm) { + elm.onreadystatechange = function () { + if (/loaded|complete/.test(elm.readyState)) { + done(); + } + }; + } else { + elm.onload = done; + } + + // Add onerror event will get fired on some browsers but not all of them + elm.onerror = error; + + // Add script to document + (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); + } /** - * Parses the specified style value into an object collection. This parser will also - * merge and remove any redundant items that browsers might have added. It will also convert non-hex - * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. + * Returns true/false if a script has been loaded or not. * - * @method parseStyle - * @param {String} cssText Style value to parse, for example: border:1px solid red;. - * @return {Object} Object representation of that style, for example: {border: '1px solid red'} + * @method isDone + * @param {String} url URL to check for. + * @return {Boolean} true/false if the URL is loaded. */ - parseStyle: function (cssText) { - return this.styles.parse(cssText); - }, + this.isDone = function (url) { + return states[url] == LOADED; + }; /** - * Serializes the specified style object into a string. + * Marks a specific script to be loaded. This can be useful if a script got loaded outside + * the script loader or to skip it from loading some script. * - * @method serializeStyle - * @param {Object} styles Object to serialize as string, for example: {border: '1px solid red'} - * @param {String} name Optional element name. - * @return {String} String representation of the style object, for example: border: 1px solid red. + * @method markDone + * @param {string} url Absolute URL to the script to mark as loaded. */ - serializeStyle: function (styles, name) { - return this.styles.serialize(styles, name); - }, + this.markDone = function (url) { + states[url] = LOADED; + }; /** - * Adds a style element at the top of the document with the specified cssText content. + * Adds a specific script to the load queue of the script loader. * - * @method addStyle - * @param {String} cssText CSS Text style to add to top of head of document. + * @method add + * @param {String} url Absolute URL to script to add. + * @param {function} success Optional success callback function to execute when the script loades successfully. + * @param {Object} scope Optional scope to execute callback in. + * @param {function} failure Optional failure callback function to execute when the script failed to load. */ - addStyle: function (cssText) { - var self = this, doc = self.doc, head, styleElm; - - // Prevent inline from loading the same styles twice - if (self !== DOMUtils.DOM && doc === document) { - var addedStyles = DOMUtils.DOM.addedStyles; - - addedStyles = addedStyles || []; - if (addedStyles[cssText]) { - return; - } + this.add = this.load = function (url, success, scope, failure) { + var state = states[url]; - addedStyles[cssText] = true; - DOMUtils.DOM.addedStyles = addedStyles; + // Add url to load queue + if (state == undef) { + queue.push(url); + states[url] = QUEUED; } - // Create style element if needed - styleElm = doc.getElementById('mceDefaultStyles'); - if (!styleElm) { - styleElm = doc.createElement('style'); - styleElm.id = 'mceDefaultStyles'; - styleElm.type = 'text/css'; - - head = doc.getElementsByTagName('head')[0]; - if (head.firstChild) { - head.insertBefore(styleElm, head.firstChild); - } else { - head.appendChild(styleElm); + if (success) { + // Store away callback for later execution + if (!scriptLoadedCallbacks[url]) { + scriptLoadedCallbacks[url] = []; } - } - // Append style data to old or new style element - if (styleElm.styleSheet) { - styleElm.styleSheet.cssText += cssText; - } else { - styleElm.appendChild(doc.createTextNode(cssText)); + scriptLoadedCallbacks[url].push({ + success: success, + failure: failure, + scope: scope || this + }); } - }, + }; + + this.remove = function (url) { + delete states[url]; + delete scriptLoadedCallbacks[url]; + }; /** - * Imports/loads the specified CSS file into the document bound to the class. - * - * @method loadCSS - * @param {String} url URL to CSS file to load. - * @example - * // Loads a CSS file dynamically into the current document - * tinymce.DOM.loadCSS('somepath/some.css'); - * - * // Loads a CSS file into the currently active editor instance - * tinymce.activeEditor.dom.loadCSS('somepath/some.css'); + * Starts the loading of the queue. * - * // Loads a CSS file into an editor instance by id - * tinymce.get('someid').dom.loadCSS('somepath/some.css'); + * @method loadQueue + * @param {function} success Optional callback to execute when all queued items are loaded. + * @param {function} failure Optional callback to execute when queued items failed to load. + * @param {Object} scope Optional scope to execute the callback in. + */ + this.loadQueue = function (success, scope, failure) { + this.loadScripts(queue, success, scope, failure); + }; + + /** + * Loads the specified queue of files and executes the callback ones they are loaded. + * This method is generally not used outside this class but it might be useful in some scenarios. * - * // Loads multiple CSS files into the current document - * tinymce.DOM.loadCSS('somepath/some.css,somepath/someother.css'); + * @method loadScripts + * @param {Array} scripts Array of queue items to load. + * @param {function} callback Optional callback to execute when scripts is loaded successfully. + * @param {Object} scope Optional scope to execute callback in. + * @param {function} callback Optional callback to execute if scripts failed to load. */ - loadCSS: function (url) { - var self = this, doc = self.doc, head; + this.loadScripts = function (scripts, success, scope, failure) { + var loadScripts, failures = []; - // Prevent inline from loading the same CSS file twice - if (self !== DOMUtils.DOM && doc === document) { - DOMUtils.DOM.loadCSS(url); - return; - } + function execCallbacks(name, url) { + // Execute URL callback functions + each(scriptLoadedCallbacks[url], function (callback) { + if (isFunction(callback[name])) { + callback[name].call(callback.scope); + } + }); - if (!url) { - url = ''; + scriptLoadedCallbacks[url] = undef; } - head = doc.getElementsByTagName('head')[0]; + queueLoadedCallbacks.push({ + success: success, + failure: failure, + scope: scope || this + }); - each(url.split(','), function (url) { - var link; + loadScripts = function () { + var loadingScripts = grep(scripts); - url = Tools._addCacheSuffix(url); + // Current scripts has been handled + scripts.length = 0; - if (self.files[url]) { - return; - } + // Load scripts that needs to be loaded + each(loadingScripts, function (url) { + // Script is already loaded then execute script callbacks directly + if (states[url] === LOADED) { + execCallbacks('success', url); + return; + } - self.files[url] = true; - link = self.create('link', { rel: 'stylesheet', href: url }); + if (states[url] === FAILED) { + execCallbacks('failure', url); + return; + } - // IE 8 has a bug where dynamically loading stylesheets would produce a 1 item remaining bug - // This fix seems to resolve that issue by recalcing the document once a stylesheet finishes loading - // It's ugly but it seems to work fine. - if (isIE && doc.documentMode && doc.recalc) { - link.onload = function () { - if (doc.recalc) { - doc.recalc(); - } + // Is script not loading then start loading it + if (states[url] !== LOADING) { + states[url] = LOADING; + loading++; - link.onload = null; - }; - } + loadScript(url, function () { + states[url] = LOADED; + loading--; - head.appendChild(link); - }); - }, + execCallbacks('success', url); - /** - * Adds a class to the specified element or elements. - * - * @method addClass - * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. - * @param {String} cls Class name to add to each element. - * @return {String/Array} String with new class value or array with new class values for all elements. - * @example - * // Adds a class to all paragraphs in the active editor - * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'myclass'); - * - * // Adds a class to a specific element in the current page - * tinymce.DOM.addClass('mydiv', 'myclass'); - */ - addClass: function (elm, cls) { - this.$$(elm).addClass(cls); - }, + // Load more scripts if they where added by the recently loaded script + loadScripts(); + }, function () { + states[url] = FAILED; + loading--; - /** - * Removes a class from the specified element or elements. - * - * @method removeClass - * @param {String/Element/Array} elm Element ID string or DOM element or array with elements or IDs. - * @param {String} cls Class name to remove from each element. - * @return {String/Array} String of remaining class name(s), or an array of strings if multiple input elements - * were passed in. - * @example - * // Removes a class from all paragraphs in the active editor - * tinymce.activeEditor.dom.removeClass(tinymce.activeEditor.dom.select('p'), 'myclass'); - * - * // Removes a class from a specific element in the current page - * tinymce.DOM.removeClass('mydiv', 'myclass'); - */ - removeClass: function (elm, cls) { - this.toggleClass(elm, cls, false); - }, + failures.push(url); + execCallbacks('failure', url); - /** - * Returns true if the specified element has the specified class. - * - * @method hasClass - * @param {String/Element} elm HTML element or element id string to check CSS class on. - * @param {String} cls CSS class to check for. - * @return {Boolean} true/false if the specified element has the specified class. - */ - hasClass: function (elm, cls) { - return this.$$(elm).hasClass(cls); - }, + // Load more scripts if they where added by the recently loaded script + loadScripts(); + }); + } + }); - /** - * Toggles the specified class on/off. - * - * @method toggleClass - * @param {Element} elm Element to toggle class on. - * @param {[type]} cls Class to toggle on/off. - * @param {[type]} state Optional state to set. - */ - toggleClass: function (elm, cls, state) { - this.$$(elm).toggleClass(cls, state).each(function () { - if (this.className === '') { - DomQuery(this).attr('class', null); + // No scripts are currently loading then execute all pending queue loaded callbacks + if (!loading) { + // We need to clone the notifications and empty the pending callbacks so that callbacks can load more resources + var notifyCallbacks = queueLoadedCallbacks.slice(0); + queueLoadedCallbacks.length = 0; + + each(notifyCallbacks, function (callback) { + if (failures.length === 0) { + if (isFunction(callback.success)) { + callback.success.call(callback.scope); + } + } else { + if (isFunction(callback.failure)) { + callback.failure.call(callback.scope, failures); + } + } + }); } - }); - }, + }; - /** - * Shows the specified element(s) by ID by setting the "display" style. - * - * @method show - * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to show. - */ - show: function (elm) { - this.$$(elm).show(); - }, + loadScripts(); + }; + } - /** - * Hides the specified element(s) by ID by setting the "display" style. - * - * @method hide - * @param {String/Element/Array} elm ID of DOM element or DOM element or array with elements or IDs to hide. - * @example - * // Hides an element by id in the document - * tinymce.DOM.hide('myid'); - */ - hide: function (elm) { - this.$$(elm).hide(); - }, + ScriptLoader.ScriptLoader = new ScriptLoader(); - /** - * Returns true/false if the element is hidden or not by checking the "display" style. - * - * @method isHidden - * @param {String/Element} elm Id or element to check display state on. - * @return {Boolean} true/false if the element is hidden or not. - */ - isHidden: function (elm) { - return this.$$(elm).css('display') == 'none'; - }, + return ScriptLoader; + } +); - /** - * Returns a unique id. This can be useful when generating elements on the fly. - * This method will not check if the element already exists. - * - * @method uniqueId - * @param {String} prefix Optional prefix to add in front of all ids - defaults to "mce_". - * @return {String} Unique id. - */ - uniqueId: function (prefix) { - return (!prefix ? 'mce_' : prefix) + (this.counter++); - }, +/** + * AddOnManager.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 loading of themes/plugins or other add-ons and their language packs. + * + * @class tinymce.AddOnManager + */ +define( + 'tinymce.core.AddOnManager', + [ + 'ephox.katamari.api.Arr', + 'tinymce.core.dom.ScriptLoader', + 'tinymce.core.util.Tools' + ], + function (Arr, ScriptLoader, Tools) { + var each = Tools.each; + + function AddOnManager() { + var self = this; + + self.items = []; + self.urls = {}; + self.lookup = {}; + self._listeners = []; + } + AddOnManager.prototype = { /** - * Sets the specified HTML content inside the element or elements. The HTML will first be processed. This means - * URLs will get converted, hex color values fixed etc. Check processHTML for details. - * - * @method setHTML - * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set HTML inside of. - * @param {String} html HTML content to set as inner HTML of the element. - * @example - * // Sets the inner HTML of all paragraphs in the active editor - * tinymce.activeEditor.dom.setHTML(tinymce.activeEditor.dom.select('p'), 'some inner html'); + * Returns the specified add on by the short name. * - * // Sets the inner HTML of an element by id in the document - * tinymce.DOM.setHTML('mydiv', 'some inner html'); + * @method get + * @param {String} name Add-on to look for. + * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. */ - setHTML: function (elm, html) { - elm = this.$$(elm); - - if (isIE) { - elm.each(function (i, target) { - if (target.canHaveHTML === false) { - return; - } + get: function (name) { + if (this.lookup[name]) { + return this.lookup[name].instance; + } - // Remove all child nodes, IE keeps empty text nodes in DOM - while (target.firstChild) { - target.removeChild(target.firstChild); - } + return undefined; + }, - try { - // IE will remove comments from the beginning - // unless you padd the contents with something - target.innerHTML = '
    ' + html; - target.removeChild(target.firstChild); - } catch (ex) { - // IE sometimes produces an unknown runtime error on innerHTML if it's a div inside a p - DomQuery('
    ').html('
    ' + html).contents().slice(1).appendTo(target); - } + dependencies: function (name) { + var result; - return html; - }); - } else { - elm.html(html); + if (this.lookup[name]) { + result = this.lookup[name].dependencies; } - }, - - /** - * Returns the outer HTML of an element. - * - * @method getOuterHTML - * @param {String/Element} elm Element ID or element object to get outer HTML from. - * @return {String} Outer HTML string. - * @example - * tinymce.DOM.getOuterHTML(editorElement); - * tinymce.activeEditor.getOuterHTML(tinymce.activeEditor.getBody()); - */ - getOuterHTML: function (elm) { - elm = this.get(elm); - // Older FF doesn't have outerHTML 3.6 is still used by some orgaizations - return elm.nodeType == 1 && "outerHTML" in elm ? elm.outerHTML : DomQuery('
    ').append(DomQuery(elm).clone()).html(); + return result || []; }, /** - * Sets the specified outer HTML on an element or elements. - * - * @method setOuterHTML - * @param {Element/String/Array} elm DOM element, element id string or array of elements/ids to set outer HTML on. - * @param {Object} html HTML code to set as outer value for the element. - * @example - * // Sets the outer HTML of all paragraphs in the active editor - * tinymce.activeEditor.dom.setOuterHTML(tinymce.activeEditor.dom.select('p'), '
    some html
    '); + * Loads a language pack for the specified add-on. * - * // Sets the outer HTML of an element by id in the document - * tinymce.DOM.setOuterHTML('mydiv', '
    some html
    '); + * @method requireLangPack + * @param {String} name Short name of the add-on. + * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. */ - setOuterHTML: function (elm, html) { - var self = this; + requireLangPack: function (name, languages) { + var language = AddOnManager.language; - self.$$(elm).each(function () { - try { - // Older FF doesn't have outerHTML 3.6 is still used by some organizations - if ("outerHTML" in this) { - this.outerHTML = html; + if (language && AddOnManager.languageLoad !== false) { + if (languages) { + languages = ',' + languages + ','; + + // Load short form sv.js or long form sv_SE.js + if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { + language = language.substr(0, 2); + } else if (languages.indexOf(',' + language + ',') == -1) { return; } - } catch (ex) { - // Ignore } - // OuterHTML for IE it sometimes produces an "unknown runtime error" - self.remove(DomQuery(this).html(html), true); - }); + ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); + } }, /** - * Entity decodes a string. This method decodes any HTML entities, such as å. + * Adds a instance of the add-on by it's short name. * - * @method decode - * @param {String} s String to decode entities on. - * @return {String} Entity decoded string. - */ - decode: Entities.decode, - - /** - * Entity encodes a string. This method encodes the most common entities, such as <>"&. + * @method add + * @param {String} id Short name/id for the add-on. + * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. + * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. + * @example + * // Create a simple plugin + * tinymce.create('tinymce.plugins.TestPlugin', { + * TestPlugin: function(ed, url) { + * ed.on('click', function(e) { + * ed.windowManager.alert('Hello World!'); + * }); + * } + * }); * - * @method encode - * @param {String} text String to encode with entities. - * @return {String} Entity encoded string. - */ - encode: Entities.encodeAllRaw, - - /** - * Inserts an element after the reference element. + * // Register plugin using the add method + * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); * - * @method insertAfter - * @param {Element} node Element to insert after the reference. - * @param {Element/String/Array} referenceNode Reference element, element id or array of elements to insert after. - * @return {Element/Array} Element that got added or an array with elements. + * // Initialize TinyMCE + * tinymce.init({ + * ... + * plugins: '-test' // Init the plugin but don't try to load it + * }); */ - insertAfter: function (node, referenceNode) { - referenceNode = this.get(referenceNode); - - return this.run(node, function (node) { - var parent, nextSibling; - - parent = referenceNode.parentNode; - nextSibling = referenceNode.nextSibling; + add: function (id, addOn, dependencies) { + this.items.push(addOn); + this.lookup[id] = { instance: addOn, dependencies: dependencies }; + var result = Arr.partition(this._listeners, function (listener) { + return listener.name === id; + }); - if (nextSibling) { - parent.insertBefore(node, nextSibling); - } else { - parent.appendChild(node); - } + this._listeners = result.fail; - return node; + each(result.pass, function (listener) { + listener.callback(); }); - }, - /** - * Replaces the specified element or elements with the new element specified. The new element will - * be cloned if multiple input elements are passed in. - * - * @method replace - * @param {Element} newElm New element to replace old ones with. - * @param {Element/String/Array} oldElm Element DOM node, element id or array of elements or ids to replace. - * @param {Boolean} keepChildren Optional keep children state, if set to true child nodes from the old object will be added - * to new ones. - */ - replace: function (newElm, oldElm, keepChildren) { - var self = this; + return addOn; + }, - return self.run(oldElm, function (oldElm) { - if (is(oldElm, 'array')) { - newElm = newElm.cloneNode(true); - } + remove: function (name) { + delete this.urls[name]; + delete this.lookup[name]; + }, - if (keepChildren) { - each(grep(oldElm.childNodes), function (node) { - newElm.appendChild(node); - }); - } + createUrl: function (baseUrl, dep) { + if (typeof dep === "object") { + return dep; + } - return oldElm.parentNode.replaceChild(newElm, oldElm); - }); + return { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix }; }, /** - * Renames the specified element and keeps its attributes and children. + * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. + * This should be used in development mode. A new compressor/javascript munger process will ensure that the + * components are put together into the plugin.js file and compressed correctly. * - * @method rename - * @param {Element} elm Element to rename. - * @param {String} name Name of the new element. - * @return {Element} New element or the old element if it needed renaming. + * @method addComponents + * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). + * @param {Array} scripts Array containing the names of the scripts to load. */ - rename: function (elm, name) { - var self = this, newElm; - - if (elm.nodeName != name.toUpperCase()) { - // Rename block element - newElm = self.create(name); - - // Copy attribs to new block - each(self.getAttribs(elm), function (attrNode) { - self.setAttrib(newElm, attrNode.nodeName, self.getAttrib(elm, attrNode.nodeName)); - }); - - // Replace block - self.replace(newElm, elm, 1); - } + addComponents: function (pluginName, scripts) { + var pluginUrl = this.urls[pluginName]; - return newElm || elm; + each(scripts, function (script) { + ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); + }); }, /** - * Find the common ancestor of two elements. This is a shorter method than using the DOM Range logic. + * Loads an add-on from a specific url. * - * @method findCommonAncestor - * @param {Element} a Element to find common ancestor of. - * @param {Element} b Element to find common ancestor of. - * @return {Element} Common ancestor element of the two input elements. + * @method load + * @param {String} name Short name of the add-on that gets loaded. + * @param {String} addOnUrl URL to the add-on that will get loaded. + * @param {function} success Optional success callback to execute when an add-on is loaded. + * @param {Object} scope Optional scope to execute the callback in. + * @param {function} failure Optional failure callback to execute when an add-on failed to load. + * @example + * // Loads a plugin from an external URL + * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); + * + * // Initialize TinyMCE + * tinymce.init({ + * ... + * plugins: '-myplugin' // Don't try to load it again + * }); */ - findCommonAncestor: function (a, b) { - var ps = a, pe; + load: function (name, addOnUrl, success, scope, failure) { + var self = this, url = addOnUrl; - while (ps) { - pe = b; + function loadDependencies() { + var dependencies = self.dependencies(name); - while (pe && ps != pe) { - pe = pe.parentNode; - } + each(dependencies, function (dep) { + var newUrl = self.createUrl(addOnUrl, dep); - if (ps == pe) { - break; - } + self.load(newUrl.resource, newUrl, undefined, undefined); + }); - ps = ps.parentNode; + if (success) { + if (scope) { + success.call(scope); + } else { + success.call(ScriptLoader); + } + } } - if (!ps && a.ownerDocument) { - return a.ownerDocument.documentElement; + if (self.urls[name]) { + return; } - return ps; - }, - - /** - * Parses the specified RGB color value and returns a hex version of that color. - * - * @method toHex - * @param {String} rgbVal RGB string value like rgb(1,2,3) - * @return {String} Hex version of that RGB value like #FF00FF. - */ - toHex: function (rgbVal) { - return this.styles.toHex(Tools.trim(rgbVal)); - }, - - /** - * Executes the specified function on the element by id or dom element node or array of elements/id. - * - * @method run - * @param {String/Element/Array} elm ID or DOM element object or array with ids or elements. - * @param {function} func Function to execute for each item. - * @param {Object} scope Optional scope to execute the function in. - * @return {Object/Array} Single object, or an array of objects if multiple input elements were passed in. - */ - run: function (elm, func, scope) { - var self = this, result; - - if (typeof elm === 'string') { - elm = self.get(elm); + if (typeof addOnUrl === "object") { + url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; } - if (!elm) { - return false; + if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { + url = AddOnManager.baseURL + '/' + url; } - scope = scope || this; - if (!elm.nodeType && (elm.length || elm.length === 0)) { - result = []; - - each(elm, function (elm, i) { - if (elm) { - if (typeof elm == 'string') { - elm = self.get(elm); - } - - result.push(func.call(scope, elm, i)); - } - }); + self.urls[name] = url.substring(0, url.lastIndexOf('/')); - return result; + if (self.lookup[name]) { + loadDependencies(); + } else { + ScriptLoader.ScriptLoader.add(url, loadDependencies, scope, failure); } - - return func.call(scope, elm); }, - /** - * Returns a NodeList with attributes for the element. - * - * @method getAttribs - * @param {HTMLElement/string} elm Element node or string id to get attributes from. - * @return {NodeList} NodeList with attributes. - */ - getAttribs: function (elm) { - var attrs; - - elm = this.get(elm); - - if (!elm) { - return []; + waitFor: function (name, callback) { + if (this.lookup.hasOwnProperty(name)) { + callback(); + } else { + this._listeners.push({ name: name, callback: callback }); } + } + }; - if (isIE) { - attrs = []; - - // Object will throw exception in IE - if (elm.nodeName == 'OBJECT') { - return elm.attributes; - } + AddOnManager.PluginManager = new AddOnManager(); + AddOnManager.ThemeManager = new AddOnManager(); - // IE doesn't keep the selected attribute if you clone option elements - if (elm.nodeName === 'OPTION' && this.getAttrib(elm, 'selected')) { - attrs.push({ specified: 1, nodeName: 'selected' }); - } + return AddOnManager; + } +); - // It's crazy that this is faster in IE but it's because it returns all attributes all the time - var attrRegExp = /<\/?[\w:\-]+ ?|=[\"][^\"]+\"|=\'[^\']+\'|=[\w\-]+|>/gi; - elm.cloneNode(false).outerHTML.replace(attrRegExp, '').replace(/[\w:\-]+/gi, function (a) { - attrs.push({ specified: 1, nodeName: a }); - }); +/** + * TinyMCE theme class. + * + * @class tinymce.Theme + */ - return attrs; - } - - return elm.attributes; - }, - - /** - * Returns true/false if the specified node is to be considered empty or not. - * - * @example - * tinymce.DOM.isEmpty(node, {img: true}); - * @method isEmpty - * @param {Object} elements Optional name/value object with elements that are automatically treated as non-empty elements. - * @return {Boolean} true/false if the node is empty or not. - */ - isEmpty: function (node, elements) { - var self = this, i, attributes, type, whitespace, walker, name, brCount = 0; - - node = node.firstChild; - if (node) { - walker = new TreeWalker(node, node.parentNode); - elements = elements || (self.schema ? self.schema.getNonEmptyElements() : null); - whitespace = self.schema ? self.schema.getWhiteSpaceElements() : {}; - - do { - type = node.nodeType; +/** + * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. + * + * @method renderUI + * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. + * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. + */ - if (type === 1) { - // Ignore bogus elements - var bogusVal = node.getAttribute('data-mce-bogus'); - if (bogusVal) { - node = walker.next(bogusVal === 'all'); - continue; - } +/** + * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. + * + * @class tinymce.Plugin + * @example + * tinymce.PluginManager.add('example', function(editor, url) { + * // Add a button that opens a window + * editor.addButton('example', { + * text: 'My button', + * icon: false, + * onclick: function() { + * // Open window + * editor.windowManager.open({ + * title: 'Example plugin', + * body: [ + * {type: 'textbox', name: 'title', label: 'Title'} + * ], + * onsubmit: function(e) { + * // Insert content when the window form is submitted + * editor.insertContent('Title: ' + e.data.title); + * } + * }); + * } + * }); + * + * // Adds a menu item to the tools menu + * editor.addMenuItem('example', { + * text: 'Example plugin', + * context: 'tools', + * onclick: function() { + * // Open window with a specific url + * editor.windowManager.open({ + * title: 'TinyMCE site', + * url: 'http://www.tinymce.com', + * width: 800, + * height: 600, + * buttons: [{ + * text: 'Close', + * onclick: 'close' + * }] + * }); + * } + * }); + * }); + */ - // Keep empty elements like - name = node.nodeName.toLowerCase(); - if (elements && elements[name]) { - // Ignore single BR elements in blocks like


    or


    - if (name === 'br') { - brCount++; - node = walker.next(); - continue; - } +define( + 'ephox.katamari.api.Cell', - return false; - } + [ + ], - // Keep elements with data-bookmark attributes or name attribute like - attributes = self.getAttribs(node); - i = attributes.length; - while (i--) { - name = attributes[i].nodeName; - if (name === "name" || name === 'data-mce-bookmark') { - return false; - } - } - } + function () { + var Cell = function (initial) { + var value = initial; - // Keep comment nodes - if (type == 8) { - return false; - } + var get = function () { + return value; + }; - // Keep non whitespace text nodes - if (type === 3 && !whiteSpaceRegExp.test(node.nodeValue)) { - return false; - } + var set = function (v) { + value = v; + }; - // Keep whitespace preserve elements - if (type === 3 && node.parentNode && whitespace[node.parentNode.nodeName] && whiteSpaceRegExp.test(node.nodeValue)) { - return false; - } + var clone = function () { + return Cell(get()); + }; - node = walker.next(); - } while (node); - } + return { + get: get, + set: set, + clone: clone + }; + }; - return brCount <= 1; - }, + return Cell; + } +); - /** - * Creates a new DOM Range object. This will use the native DOM Range API if it's - * available. If it's not, it will fall back to the custom TinyMCE implementation. - * - * @method createRng - * @return {DOMRange} DOM Range object. - * @example - * var rng = tinymce.DOM.createRng(); - * alert(rng.startContainer + "," + rng.startOffset); - */ - createRng: function () { - var doc = this.doc; +/** + * NodeType.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 + */ - return doc.createRange ? doc.createRange() : new Range(this); - }, +/** + * Contains various node validation functions. + * + * @private + * @class tinymce.dom.NodeType + */ +define( + 'tinymce.core.dom.NodeType', + [ + ], + function () { + function isNodeType(type) { + return function (node) { + return !!node && node.nodeType == type; + }; + } - /** - * Returns the index of the specified node within its parent. - * - * @method nodeIndex - * @param {Node} node Node to look for. - * @param {boolean} normalized Optional true/false state if the index is what it would be after a normalization. - * @return {Number} Index of the specified node. - */ - nodeIndex: nodeIndex, + var isElement = isNodeType(1); - /** - * Splits an element into two new elements and places the specified split - * element or elements between the new ones. For example splitting the paragraph at the bold element in - * this example

    abcabc123

    would produce

    abc

    abc

    123

    . - * - * @method split - * @param {Element} parentElm Parent element to split. - * @param {Element} splitElm Element to split at. - * @param {Element} replacementElm Optional replacement element to replace the split element with. - * @return {Element} Returns the split element or the replacement element if that is specified. - */ - split: function (parentElm, splitElm, replacementElm) { - var self = this, r = self.createRng(), bef, aft, pa; + function matchNodeNames(names) { + names = names.toLowerCase().split(' '); - // W3C valid browsers tend to leave empty nodes to the left/right side of the contents - this makes sense - // but we don't want that in our code since it serves no purpose for the end user - // For example splitting this html at the bold element: - //

    text 1CHOPtext 2

    - // would produce: - //

    text 1

    CHOP

    text 2

    - // this function will then trim off empty edges and produce: - //

    text 1

    CHOP

    text 2

    - function trimNode(node) { - var i, children = node.childNodes, type = node.nodeType; + return function (node) { + var i, name; - function surroundedBySpans(node) { - var previousIsSpan = node.previousSibling && node.previousSibling.nodeName == 'SPAN'; - var nextIsSpan = node.nextSibling && node.nextSibling.nodeName == 'SPAN'; - return previousIsSpan && nextIsSpan; - } + if (node && node.nodeType) { + name = node.nodeName.toLowerCase(); - if (type == 1 && node.getAttribute('data-mce-type') == 'bookmark') { - return; + for (i = 0; i < names.length; i++) { + if (name === names[i]) { + return true; + } } + } - for (i = children.length - 1; i >= 0; i--) { - trimNode(children[i]); - } + return false; + }; + } - if (type != 9) { - // Keep non whitespace text nodes - if (type == 3 && node.nodeValue.length > 0) { - // If parent element isn't a block or there isn't any useful contents for example "

    " - // Also keep text nodes with only spaces if surrounded by spans. - // eg. "

    a b

    " should keep space between a and b - var trimmedLength = trim(node.nodeValue).length; - if (!self.isBlock(node.parentNode) || trimmedLength > 0 || trimmedLength === 0 && surroundedBySpans(node)) { - return; - } - } else if (type == 1) { - // If the only child is a bookmark then move it up - children = node.childNodes; + function matchStyleValues(name, values) { + values = values.toLowerCase().split(' '); - // TODO fix this complex if - if (children.length == 1 && children[0] && children[0].nodeType == 1 && - children[0].getAttribute('data-mce-type') == 'bookmark') { - node.parentNode.insertBefore(children[0], node); - } + return function (node) { + var i, cssValue; - // Keep non empty elements or img, hr etc - if (children.length || /^(br|hr|input|img)$/i.test(node.nodeName)) { - return; - } + if (isElement(node)) { + for (i = 0; i < values.length; i++) { + cssValue = node.ownerDocument.defaultView.getComputedStyle(node, null).getPropertyValue(name); + if (cssValue === values[i]) { + return true; } - - self.remove(node); } - - return node; } - if (parentElm && splitElm) { - // Get before chunk - r.setStart(parentElm.parentNode, self.nodeIndex(parentElm)); - r.setEnd(splitElm.parentNode, self.nodeIndex(splitElm)); - bef = r.extractContents(); - - // Get after chunk - r = self.createRng(); - r.setStart(splitElm.parentNode, self.nodeIndex(splitElm) + 1); - r.setEnd(parentElm.parentNode, self.nodeIndex(parentElm) + 1); - aft = r.extractContents(); - - // Insert before chunk - pa = parentElm.parentNode; - pa.insertBefore(trimNode(bef), parentElm); - - // Insert middle chunk - if (replacementElm) { - pa.insertBefore(replacementElm, parentElm); - //pa.replaceChild(replacementElm, splitElm); - } else { - pa.insertBefore(splitElm, parentElm); - } + return false; + }; + } - // Insert after chunk - pa.insertBefore(trimNode(aft), parentElm); - self.remove(parentElm); + function hasPropValue(propName, propValue) { + return function (node) { + return isElement(node) && node[propName] === propValue; + }; + } - return replacementElm || splitElm; - } - }, + function hasAttribute(attrName, attrValue) { + return function (node) { + return isElement(node) && node.hasAttribute(attrName); + }; + } - /** - * Adds an event handler to the specified object. - * - * @method bind - * @param {Element/Document/Window/Array} target Target element to bind events to. - * handler to or an array of elements/ids/documents. - * @param {String} name Name of event handler to add, for example: click. - * @param {function} func Function to execute when the event occurs. - * @param {Object} scope Optional scope to execute the function in. - * @return {function} Function callback handler the same as the one passed in. - */ - bind: function (target, name, func, scope) { - var self = this; + function hasAttributeValue(attrName, attrValue) { + return function (node) { + return isElement(node) && node.getAttribute(attrName) === attrValue; + }; + } - if (Tools.isArray(target)) { - var i = target.length; + function isBogus(node) { + return isElement(node) && node.hasAttribute('data-mce-bogus'); + } - while (i--) { - target[i] = self.bind(target[i], name, func, scope); + function hasContentEditableState(value) { + return function (node) { + if (isElement(node)) { + if (node.contentEditable === value) { + return true; } - return target; - } - - // Collect all window/document events bound by editor instance - if (self.settings.collect && (target === self.doc || target === self.win)) { - self.boundEvents.push([target, name, func, scope]); + if (node.getAttribute('data-mce-contenteditable') === value) { + return true; + } } - return self.events.bind(target, name, func, scope || self); - }, + return false; + }; + } - /** - * Removes the specified event handler by name and function from an element or collection of elements. - * - * @method unbind - * @param {Element/Document/Window/Array} target Target element to unbind events on. - * @param {String} name Event handler name, for example: "click" - * @param {function} func Function to remove. - * @return {bool/Array} Bool state of true if the handler was removed, or an array of states if multiple input elements - * were passed in. - */ - unbind: function (target, name, func) { - var self = this, i; + return { + isText: isNodeType(3), + isElement: isElement, + isComment: isNodeType(8), + isBr: matchNodeNames('br'), + isContentEditableTrue: hasContentEditableState('true'), + isContentEditableFalse: hasContentEditableState('false'), + matchNodeNames: matchNodeNames, + hasPropValue: hasPropValue, + hasAttribute: hasAttribute, + hasAttributeValue: hasAttributeValue, + matchStyleValues: matchStyleValues, + isBogus: isBogus + }; + } +); +/** + * Fun.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 + */ - if (Tools.isArray(target)) { - i = target.length; +/** + * Functional utility class. + * + * @private + * @class tinymce.util.Fun + */ +define( + 'tinymce.core.util.Fun', + [ + ], + function () { + var slice = [].slice; - while (i--) { - target[i] = self.unbind(target[i], name, func); - } + function constant(value) { + return function () { + return value; + }; + } - return target; - } + function negate(predicate) { + return function (x) { + return !predicate(x); + }; + } - // Remove any bound events matching the input - if (self.boundEvents && (target === self.doc || target === self.win)) { - i = self.boundEvents.length; + function compose(f, g) { + return function (x) { + return f(g(x)); + }; + } - while (i--) { - var item = self.boundEvents[i]; + function or() { + var args = slice.call(arguments); - if (target == item[0] && (!name || name == item[1]) && (!func || func == item[2])) { - this.events.unbind(item[0], item[1], item[2]); - } + return function (x) { + for (var i = 0; i < args.length; i++) { + if (args[i](x)) { + return true; } } - return this.events.unbind(target, name, func); - }, - - /** - * Fires the specified event name with object on target. - * - * @method fire - * @param {Node/Document/Window} target Target element or object to fire event on. - * @param {String} name Name of the event to fire. - * @param {Object} evt Event object to send. - * @return {Event} Event object. - */ - fire: function (target, name, evt) { - return this.events.fire(target, name, evt); - }, + return false; + }; + } - // Returns the content editable state of a node - getContentEditable: function (node) { - var contentEditable; + function and() { + var args = slice.call(arguments); - // Check type - if (!node || node.nodeType != 1) { - return null; + return function (x) { + for (var i = 0; i < args.length; i++) { + if (!args[i](x)) { + return false; + } } - // Check for fake content editable - contentEditable = node.getAttribute("data-mce-contenteditable"); - if (contentEditable && contentEditable !== "inherit") { - return contentEditable; - } + return true; + }; + } - // Check for real content editable - return node.contentEditable !== "inherit" ? node.contentEditable : null; - }, + function curry(fn) { + var args = slice.call(arguments); - getContentEditableParent: function (node) { - var root = this.getRoot(), state = null; + if (args.length - 1 >= fn.length) { + return fn.apply(this, args.slice(1)); + } - for (; node && node !== root; node = node.parentNode) { - state = this.getContentEditable(node); + return function () { + var tempArgs = args.concat([].slice.call(arguments)); + return curry.apply(this, tempArgs); + }; + } - if (state !== null) { - break; - } - } + function noop() { + } - return state; - }, + return { + constant: constant, + negate: negate, + and: and, + or: or, + curry: curry, + compose: compose, + noop: noop + }; + } +); +/** + * Zwsp.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 + */ - /** - * Destroys all internal references to the DOM to solve IE leak issues. - * - * @method destroy - */ - destroy: function () { - var self = this; +/** + * Utility functions for working with zero width space + * characters used as character containers etc. + * + * @private + * @class tinymce.text.Zwsp + * @example + * var isZwsp = Zwsp.isZwsp('\uFEFF'); + * var abc = Zwsp.trim('a\uFEFFc'); + */ +define( + 'tinymce.core.text.Zwsp', + [ + ], + function () { + // This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP + var ZWSP = '\uFEFF'; - // Unbind all events bound to window/document by editor instance - if (self.boundEvents) { - var i = self.boundEvents.length; + var isZwsp = function (chr) { + return chr === ZWSP; + }; - while (i--) { - var item = self.boundEvents[i]; - this.events.unbind(item[0], item[1], item[2]); - } + var trim = function (text) { + return text.replace(new RegExp(ZWSP, 'g'), ''); + }; - self.boundEvents = null; - } + return { + isZwsp: isZwsp, + ZWSP: ZWSP, + trim: trim + }; + } +); +/** + * CaretContainer.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 + */ - // Restore sizzle document to window.document - // Since the current document might be removed producing "Permission denied" on IE see #6325 - if (Sizzle.setDocument) { - Sizzle.setDocument(); - } +/** + * This module handles caret containers. A caret container is a node that + * holds the caret for positional purposes. + * + * @private + * @class tinymce.caret.CaretContainer + */ +define( + 'tinymce.core.caret.CaretContainer', + [ + 'global!document', + 'tinymce.core.dom.NodeType', + 'tinymce.core.text.Zwsp' + ], + function (document, NodeType, Zwsp) { + var isElement = NodeType.isElement, + isText = NodeType.isText; - self.win = self.doc = self.root = self.events = self.frag = null; - }, + function isCaretContainerBlock(node) { + if (isText(node)) { + node = node.parentNode; + } - isChildOf: function (node, parent) { - while (node) { - if (parent === node) { - return true; - } + return isElement(node) && node.hasAttribute('data-mce-caret'); + } - node = node.parentNode; - } + function isCaretContainerInline(node) { + return isText(node) && Zwsp.isZwsp(node.data); + } - return false; - }, + function isCaretContainer(node) { + return isCaretContainerBlock(node) || isCaretContainerInline(node); + } - // #ifdef debug + var hasContent = function (node) { + return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild); + }; - dumpRng: function (r) { - return ( - 'startContainer: ' + r.startContainer.nodeName + - ', startOffset: ' + r.startOffset + - ', endContainer: ' + r.endContainer.nodeName + - ', endOffset: ' + r.endOffset - ); - }, + function insertInline(node, before) { + var doc, sibling, textNode, parentNode; - // #endif + doc = node.ownerDocument; + textNode = doc.createTextNode(Zwsp.ZWSP); + parentNode = node.parentNode; - _findSib: function (node, selector, name) { - var self = this, func = selector; + if (!before) { + sibling = node.nextSibling; + if (isText(sibling)) { + if (isCaretContainer(sibling)) { + return sibling; + } - if (node) { - // If expression make a function of it using is - if (typeof func == 'string') { - func = function (node) { - return self.is(node, selector); - }; + if (startsWithCaretContainer(sibling)) { + sibling.splitText(1); + return sibling; } + } - // Loop all siblings - for (node = node[name]; node; node = node[name]) { - if (func(node)) { - return node; - } + if (node.nextSibling) { + parentNode.insertBefore(textNode, node.nextSibling); + } else { + parentNode.appendChild(textNode); + } + } else { + sibling = node.previousSibling; + if (isText(sibling)) { + if (isCaretContainer(sibling)) { + return sibling; + } + + if (endsWithCaretContainer(sibling)) { + return sibling.splitText(sibling.data.length - 1); } } + parentNode.insertBefore(textNode, node); + } + + return textNode; + } + + var prependInline = function (node) { + if (NodeType.isText(node)) { + var data = node.data; + if (data.length > 0 && data.charAt(0) !== Zwsp.ZWSP) { + node.insertData(0, Zwsp.ZWSP); + } + return node; + } else { return null; } }; - /** - * Instance of DOMUtils for the current document. - * - * @static - * @property DOM - * @type tinymce.dom.DOMUtils - * @example - * // Example of how to add a class to some element by id - * tinymce.DOM.addClass('someid', 'someclass'); - */ - DOMUtils.DOM = new DOMUtils(document); - DOMUtils.nodeIndex = nodeIndex; + var appendInline = function (node) { + if (NodeType.isText(node)) { + var data = node.data; + if (data.length > 0 && data.charAt(data.length - 1) !== Zwsp.ZWSP) { + node.insertData(data.length, Zwsp.ZWSP); + } + return node; + } else { + return null; + } + }; - return DOMUtils; + var isBeforeInline = function (pos) { + return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset()) === Zwsp.ZWSP; + }; + + var isAfterInline = function (pos) { + return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset() - 1) === Zwsp.ZWSP; + }; + + function createBogusBr() { + var br = document.createElement('br'); + br.setAttribute('data-mce-bogus', '1'); + return br; + } + + function insertBlock(blockName, node, before) { + var doc, blockNode, parentNode; + + doc = node.ownerDocument; + blockNode = doc.createElement(blockName); + blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after'); + blockNode.setAttribute('data-mce-bogus', 'all'); + blockNode.appendChild(createBogusBr()); + parentNode = node.parentNode; + + if (!before) { + if (node.nextSibling) { + parentNode.insertBefore(blockNode, node.nextSibling); + } else { + parentNode.appendChild(blockNode); + } + } else { + parentNode.insertBefore(blockNode, node); + } + + return blockNode; + } + + function startsWithCaretContainer(node) { + return isText(node) && node.data[0] == Zwsp.ZWSP; + } + + function endsWithCaretContainer(node) { + return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP; + } + + function trimBogusBr(elm) { + var brs = elm.getElementsByTagName('br'); + var lastBr = brs[brs.length - 1]; + if (NodeType.isBogus(lastBr)) { + lastBr.parentNode.removeChild(lastBr); + } + } + + function showCaretContainerBlock(caretContainer) { + if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) { + trimBogusBr(caretContainer); + caretContainer.removeAttribute('data-mce-caret'); + caretContainer.removeAttribute('data-mce-bogus'); + caretContainer.removeAttribute('style'); + caretContainer.removeAttribute('_moz_abspos'); + return caretContainer; + } + + return null; + } + + return { + isCaretContainer: isCaretContainer, + isCaretContainerBlock: isCaretContainerBlock, + isCaretContainerInline: isCaretContainerInline, + showCaretContainerBlock: showCaretContainerBlock, + insertInline: insertInline, + prependInline: prependInline, + appendInline: appendInline, + isBeforeInline: isBeforeInline, + isAfterInline: isAfterInline, + insertBlock: insertBlock, + hasContent: hasContent, + startsWithCaretContainer: startsWithCaretContainer, + endsWithCaretContainer: endsWithCaretContainer + }; } ); - /** - * ScriptLoader.js + * RangeUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -11257,732 +11772,665 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/*globals console*/ - /** - * This class handles asynchronous/synchronous loading of JavaScript files it will execute callbacks - * when various items gets loaded. This class is useful to load external JavaScript files. - * - * @class tinymce.dom.ScriptLoader - * @example - * // Load a script from a specific URL using the global script loader - * tinymce.ScriptLoader.load('somescript.js'); - * - * // Load a script using a unique instance of the script loader - * var scriptLoader = new tinymce.dom.ScriptLoader(); - * - * scriptLoader.load('somescript.js'); - * - * // Load multiple scripts - * var scriptLoader = new tinymce.dom.ScriptLoader(); - * - * scriptLoader.add('somescript1.js'); - * scriptLoader.add('somescript2.js'); - * scriptLoader.add('somescript3.js'); + * This class contains a few utility methods for ranges. * - * scriptLoader.loadQueue(function() { - * alert('All scripts are now loaded.'); - * }); + * @class tinymce.dom.RangeUtils */ define( - 'tinymce.core.dom.ScriptLoader', + 'tinymce.core.dom.RangeUtils', [ - "tinymce.core.dom.DOMUtils", - "tinymce.core.util.Tools" + 'tinymce.core.util.Tools', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.dom.NodeType', + 'tinymce.core.caret.CaretContainer' ], - function (DOMUtils, Tools) { - var DOM = DOMUtils.DOM; - var each = Tools.each, grep = Tools.grep; - - var isFunction = function (f) { - return typeof f === 'function'; - }; - - function ScriptLoader() { - var QUEUED = 0, - LOADING = 1, - LOADED = 2, - FAILED = 3, - states = {}, - queue = [], - scriptLoadedCallbacks = {}, - queueLoadedCallbacks = [], - loading = 0, - undef; - - /** - * Loads a specific script directly without adding it to the load queue. - * - * @method load - * @param {String} url Absolute URL to script to add. - * @param {function} callback Optional success callback function when the script loaded successfully. - * @param {function} callback Optional failure callback function when the script failed to load. - */ - function loadScript(url, success, failure) { - var dom = DOM, elm, id; + function (Tools, TreeWalker, NodeType, CaretContainer) { + var each = Tools.each, + isContentEditableTrue = NodeType.isContentEditableTrue, + isContentEditableFalse = NodeType.isContentEditableFalse, + isCaretContainer = CaretContainer.isCaretContainer; - // Execute callback when script is loaded - function done() { - dom.remove(id); + function hasCeProperty(node) { + return isContentEditableTrue(node) || isContentEditableFalse(node); + } - if (elm) { - elm.onreadystatechange = elm.onload = elm = null; - } + function getEndChild(container, index) { + var childNodes = container.childNodes; - success(); - } + index--; - function error() { - /*eslint no-console:0 */ + if (index > childNodes.length - 1) { + index = childNodes.length - 1; + } else if (index < 0) { + index = 0; + } - // We can't mark it as done if there is a load error since - // A) We don't want to produce 404 errors on the server and - // B) the onerror event won't fire on all browsers. - // done(); + return childNodes[index] || container; + } - if (isFunction(failure)) { - failure(); - } else { - // Report the error so it's easier for people to spot loading errors - if (typeof console !== "undefined" && console.log) { - console.log("Failed to load script: " + url); - } - } + function findParent(node, rootNode, predicate) { + while (node && node !== rootNode) { + if (predicate(node)) { + return node; } - id = dom.uniqueId(); + node = node.parentNode; + } - // Create new script element - elm = document.createElement('script'); - elm.id = id; - elm.type = 'text/javascript'; - elm.src = Tools._addCacheSuffix(url); + return null; + } - // Seems that onreadystatechange works better on IE 10 onload seems to fire incorrectly - if ("onreadystatechange" in elm) { - elm.onreadystatechange = function () { - if (/loaded|complete/.test(elm.readyState)) { - done(); - } - }; - } else { - elm.onload = done; - } + function hasParent(node, rootNode, predicate) { + return findParent(node, rootNode, predicate) !== null; + } - // Add onerror event will get fired on some browsers but not all of them - elm.onerror = error; + function hasParentWithName(node, rootNode, name) { + return hasParent(node, rootNode, function (node) { + return node.nodeName === name; + }); + } - // Add script to document - (document.getElementsByTagName('head')[0] || document.body).appendChild(elm); - } + function isFormatterCaret(node) { + return node.id === '_mce_caret'; + } - /** - * Returns true/false if a script has been loaded or not. - * - * @method isDone - * @param {String} url URL to check for. - * @return {Boolean} true/false if the URL is loaded. - */ - this.isDone = function (url) { - return states[url] == LOADED; - }; + function isCeFalseCaretContainer(node, rootNode) { + return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false; + } + function RangeUtils(dom) { /** - * Marks a specific script to be loaded. This can be useful if a script got loaded outside - * the script loader or to skip it from loading some script. + * Walks the specified range like object and executes the callback for each sibling collection it finds. * - * @method markDone - * @param {string} url Absolute URL to the script to mark as loaded. + * @private + * @method walk + * @param {Object} rng Range like object. + * @param {function} callback Callback function to execute for each sibling collection. */ - this.markDone = function (url) { - states[url] = LOADED; - }; + this.walk = function (rng, callback) { + var startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset, + ancestor, startPoint, + endPoint, node, parent, siblings, nodes; - /** - * Adds a specific script to the load queue of the script loader. - * - * @method add - * @param {String} url Absolute URL to script to add. - * @param {function} success Optional success callback function to execute when the script loades successfully. - * @param {Object} scope Optional scope to execute callback in. - * @param {function} failure Optional failure callback function to execute when the script failed to load. - */ - this.add = this.load = function (url, success, scope, failure) { - var state = states[url]; + // Handle table cell selection the table plugin enables + // you to fake select table cells and perform formatting actions on them + nodes = dom.select('td[data-mce-selected],th[data-mce-selected]'); + if (nodes.length > 0) { + each(nodes, function (node) { + callback([node]); + }); - // Add url to load queue - if (state == undef) { - queue.push(url); - states[url] = QUEUED; + return; } - if (success) { - // Store away callback for later execution - if (!scriptLoadedCallbacks[url]) { - scriptLoadedCallbacks[url] = []; + /** + * Excludes start/end text node if they are out side the range + * + * @private + * @param {Array} nodes Nodes to exclude items from. + * @return {Array} Array with nodes excluding the start/end container if needed. + */ + function exclude(nodes) { + var node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); } - scriptLoadedCallbacks[url].push({ - success: success, - failure: failure, - scope: scope || this - }); + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; } - }; - this.remove = function (url) { - delete states[url]; - delete scriptLoadedCallbacks[url]; - }; + /** + * Collects siblings + * + * @private + * @param {Node} node Node to collect siblings from. + * @param {String} name Name of the sibling to check for. + * @param {Node} endNode + * @return {Array} Array of collected siblings. + */ + function collectSiblings(node, name, endNode) { + var siblings = []; - /** - * Starts the loading of the queue. - * - * @method loadQueue - * @param {function} success Optional callback to execute when all queued items are loaded. - * @param {function} failure Optional callback to execute when queued items failed to load. - * @param {Object} scope Optional scope to execute the callback in. - */ - this.loadQueue = function (success, scope, failure) { - this.loadScripts(queue, success, scope, failure); - }; + for (; node && node != endNode; node = node[name]) { + siblings.push(node); + } - /** - * Loads the specified queue of files and executes the callback ones they are loaded. - * This method is generally not used outside this class but it might be useful in some scenarios. - * - * @method loadScripts - * @param {Array} scripts Array of queue items to load. - * @param {function} callback Optional callback to execute when scripts is loaded successfully. - * @param {Object} scope Optional scope to execute callback in. - * @param {function} callback Optional callback to execute if scripts failed to load. - */ - this.loadScripts = function (scripts, success, scope, failure) { - var loadScripts, failures = []; + return siblings; + } - function execCallbacks(name, url) { - // Execute URL callback functions - each(scriptLoadedCallbacks[url], function (callback) { - if (isFunction(callback[name])) { - callback[name].call(callback.scope); + /** + * Find an end point this is the node just before the common ancestor root. + * + * @private + * @param {Node} node Node to start at. + * @param {Node} root Root/ancestor element to stop just before. + * @return {Node} Node just before the root element. + */ + function findEndPoint(node, root) { + do { + if (node.parentNode == root) { + return node; } - }); - scriptLoadedCallbacks[url] = undef; + node = node.parentNode; + } while (node); } - queueLoadedCallbacks.push({ - success: success, - failure: failure, - scope: scope || this - }); - - loadScripts = function () { - var loadingScripts = grep(scripts); + function walkBoundary(startNode, endNode, next) { + var siblingName = next ? 'nextSibling' : 'previousSibling'; - // Current scripts has been handled - scripts.length = 0; + for (node = startNode, parent = node.parentNode; node && node != endNode; node = parent) { + parent = node.parentNode; + siblings = collectSiblings(node == startNode ? node : node[siblingName], siblingName); - // Load scripts that needs to be loaded - each(loadingScripts, function (url) { - // Script is already loaded then execute script callbacks directly - if (states[url] === LOADED) { - execCallbacks('success', url); - return; - } + if (siblings.length) { + if (!next) { + siblings.reverse(); + } - if (states[url] === FAILED) { - execCallbacks('failure', url); - return; + callback(exclude(siblings)); } + } + } - // Is script not loading then start loading it - if (states[url] !== LOADING) { - states[url] = LOADING; - loading++; - - loadScript(url, function () { - states[url] = LOADED; - loading--; - - execCallbacks('success', url); + // If index based start position then resolve it + if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { + startContainer = startContainer.childNodes[startOffset]; + } - // Load more scripts if they where added by the recently loaded script - loadScripts(); - }, function () { - states[url] = FAILED; - loading--; + // If index based end position then resolve it + if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { + endContainer = getEndChild(endContainer, endOffset); + } - failures.push(url); - execCallbacks('failure', url); + // Same container + if (startContainer == endContainer) { + return callback(exclude([startContainer])); + } - // Load more scripts if they where added by the recently loaded script - loadScripts(); - }); - } - }); + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); - // No scripts are currently loading then execute all pending queue loaded callbacks - if (!loading) { - each(queueLoadedCallbacks, function (callback) { - if (failures.length === 0) { - if (isFunction(callback.success)) { - callback.success.call(callback.scope); - } - } else { - if (isFunction(callback.failure)) { - callback.failure.call(callback.scope, failures); - } - } - }); + // Process left side + for (node = startContainer; node; node = node.parentNode) { + if (node === endContainer) { + return walkBoundary(startContainer, ancestor, true); + } - queueLoadedCallbacks.length = 0; + if (node === ancestor) { + break; } - }; + } - loadScripts(); - }; - } + // Process right side + for (node = endContainer; node; node = node.parentNode) { + if (node === startContainer) { + return walkBoundary(endContainer, ancestor); + } - ScriptLoader.ScriptLoader = new ScriptLoader(); + if (node === ancestor) { + break; + } + } - return ScriptLoader; - } -); + // Find start/end point + startPoint = findEndPoint(startContainer, ancestor) || startContainer; + endPoint = findEndPoint(endContainer, ancestor) || endContainer; -/** - * AddOnManager.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 - */ + // Walk left leaf + walkBoundary(startContainer, startPoint, true); -/** - * This class handles the loading of themes/plugins or other add-ons and their language packs. - * - * @class tinymce.AddOnManager - */ -define( - 'tinymce.core.AddOnManager', - [ - "tinymce.core.dom.ScriptLoader", - "tinymce.core.util.Tools" - ], - function (ScriptLoader, Tools) { - var each = Tools.each; + // Walk the middle from start to end point + siblings = collectSiblings( + startPoint == startContainer ? startPoint : startPoint.nextSibling, + 'nextSibling', + endPoint == endContainer ? endPoint.nextSibling : endPoint + ); - function AddOnManager() { - var self = this; + if (siblings.length) { + callback(exclude(siblings)); + } - self.items = []; - self.urls = {}; - self.lookup = {}; - } + // Walk right leaf + walkBoundary(endContainer, endPoint); + }; - AddOnManager.prototype = { /** - * Returns the specified add on by the short name. + * Splits the specified range at it's start/end points. * - * @method get - * @param {String} name Add-on to look for. - * @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined. + * @private + * @param {Range/RangeObject} rng Range to split. + * @return {Object} Range position object. */ - get: function (name) { - if (this.lookup[name]) { - return this.lookup[name].instance; + this.split = function (rng) { + var startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset; + + function splitText(node, offset) { + return node.splitText(offset); } - return undefined; - }, + // Handle single text node + if (startContainer == endContainer && startContainer.nodeType == 3) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; - dependencies: function (name) { - var result; + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } + } + } else { + // Split startContainer text node if needed + if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { + startContainer = splitText(startContainer, startOffset); + startOffset = 0; + } - if (this.lookup[name]) { - result = this.lookup[name].dependencies; + // Split endContainer text node if needed + if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { + endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + } } - return result || []; - }, + return { + startContainer: startContainer, + startOffset: startOffset, + endContainer: endContainer, + endOffset: endOffset + }; + }; /** - * Loads a language pack for the specified add-on. + * Normalizes the specified range by finding the closest best suitable caret location. * - * @method requireLangPack - * @param {String} name Short name of the add-on. - * @param {String} languages Optional comma or space separated list of languages to check if it matches the name. + * @private + * @param {Range} rng Range to normalize. + * @return {Boolean} True/false if the specified range was normalized or not. */ - requireLangPack: function (name, languages) { - var language = AddOnManager.language; + this.normalize = function (rng) { + var normalized = false, collapsed; - if (language && AddOnManager.languageLoad !== false) { - if (languages) { - languages = ',' + languages + ','; + function normalizeEndPoint(start) { + var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; + var directionLeft, isAfterNode; - // Load short form sv.js or long form sv_SE.js - if (languages.indexOf(',' + language.substr(0, 2) + ',') != -1) { - language = language.substr(0, 2); - } else if (languages.indexOf(',' + language + ',') == -1) { - return; + function isTableCell(node) { + return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); + } + + function hasBrBeforeAfter(node, left) { + var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); + + while ((node = walker[left ? 'prev' : 'next']())) { + if (node.nodeName === "BR") { + return true; + } } } - ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + language + '.js'); - } - }, + function hasContentEditableFalseParent(node) { + while (node && node != body) { + if (isContentEditableFalse(node)) { + return true; + } - /** - * Adds a instance of the add-on by it's short name. - * - * @method add - * @param {String} id Short name/id for the add-on. - * @param {tinymce.Theme/tinymce.Plugin} addOn Theme or plugin to add. - * @return {tinymce.Theme/tinymce.Plugin} The same theme or plugin instance that got passed in. - * @example - * // Create a simple plugin - * tinymce.create('tinymce.plugins.TestPlugin', { - * TestPlugin: function(ed, url) { - * ed.on('click', function(e) { - * ed.windowManager.alert('Hello World!'); - * }); - * } - * }); - * - * // Register plugin using the add method - * tinymce.PluginManager.add('test', tinymce.plugins.TestPlugin); - * - * // Initialize TinyMCE - * tinymce.init({ - * ... - * plugins: '-test' // Init the plugin but don't try to load it - * }); - */ - add: function (id, addOn, dependencies) { - this.items.push(addOn); - this.lookup[id] = { instance: addOn, dependencies: dependencies }; + node = node.parentNode; + } - return addOn; - }, + return false; + } - remove: function (name) { - delete this.urls[name]; - delete this.lookup[name]; - }, + function isPrevNode(node, name) { + return node.previousSibling && node.previousSibling.nodeName == name; + } - createUrl: function (baseUrl, dep) { - if (typeof dep === "object") { - return dep; - } + // Walks the dom left/right to find a suitable text node to move the endpoint into + // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG + function findTextNodeRelative(left, startNode) { + var walker, lastInlineElement, parentBlockContainer; - return { prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix }; - }, + startNode = startNode || container; + parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; - /** - * Add a set of components that will make up the add-on. Using the url of the add-on name as the base url. - * This should be used in development mode. A new compressor/javascript munger process will ensure that the - * components are put together into the plugin.js file and compressed correctly. - * - * @method addComponents - * @param {String} pluginName name of the plugin to load scripts from (will be used to get the base url for the plugins). - * @param {Array} scripts Array containing the names of the scripts to load. - */ - addComponents: function (pluginName, scripts) { - var pluginUrl = this.urls[pluginName]; + // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 + // This:


    |

    becomes

    |

    + if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) { + container = startNode.parentNode; + offset = dom.nodeIndex(startNode); + normalized = true; + return; + } - each(scripts, function (script) { - ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script); - }); - }, + // Walk left until we hit a text node we can move to or a block/br/img + walker = new TreeWalker(startNode, parentBlockContainer); + while ((node = walker[left ? 'prev' : 'next']())) { + // Break if we hit a non content editable node + if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, dom.getRoot())) { + return; + } - /** - * Loads an add-on from a specific url. - * - * @method load - * @param {String} name Short name of the add-on that gets loaded. - * @param {String} addOnUrl URL to the add-on that will get loaded. - * @param {function} success Optional success callback to execute when an add-on is loaded. - * @param {Object} scope Optional scope to execute the callback in. - * @param {function} failure Optional failure callback to execute when an add-on failed to load. - * @example - * // Loads a plugin from an external URL - * tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js'); - * - * // Initialize TinyMCE - * tinymce.init({ - * ... - * plugins: '-myplugin' // Don't try to load it again - * }); - */ - load: function (name, addOnUrl, success, scope, failure) { - var self = this, url = addOnUrl; + // Found text node that has a length + if (node.nodeType === 3 && node.nodeValue.length > 0) { + if (hasParentWithName(node, body, 'A') === false) { + container = node; + offset = left ? node.nodeValue.length : 0; + normalized = true; + } - function loadDependencies() { - var dependencies = self.dependencies(name); + return; + } - each(dependencies, function (dep) { - var newUrl = self.createUrl(addOnUrl, dep); + // Break if we find a block or a BR/IMG/INPUT etc + if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + return; + } - self.load(newUrl.resource, newUrl, undefined, undefined); - }); + lastInlineElement = node; + } - if (success) { - if (scope) { - success.call(scope); - } else { - success.call(ScriptLoader); + // Only fetch the last inline element when in caret mode for now + if (collapsed && lastInlineElement) { + container = lastInlineElement; + normalized = true; + offset = 0; } } - } - - if (self.urls[name]) { - return; - } - if (typeof addOnUrl === "object") { - url = addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix; - } + container = rng[(start ? 'start' : 'end') + 'Container']; + offset = rng[(start ? 'start' : 'end') + 'Offset']; + isAfterNode = container.nodeType == 1 && offset === container.childNodes.length; + nonEmptyElementsMap = dom.schema.getNonEmptyElements(); + directionLeft = start; - if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) { - url = AddOnManager.baseURL + '/' + url; - } + if (isCaretContainer(container)) { + return; + } - self.urls[name] = url.substring(0, url.lastIndexOf('/')); + if (container.nodeType == 1 && offset > container.childNodes.length - 1) { + directionLeft = false; + } - if (self.lookup[name]) { - loadDependencies(); - } else { - ScriptLoader.ScriptLoader.add(url, loadDependencies, scope, failure); - } - } - }; + // If the container is a document move it to the body element + if (container.nodeType === 9) { + container = dom.getRoot(); + offset = 0; + } - AddOnManager.PluginManager = new AddOnManager(); - AddOnManager.ThemeManager = new AddOnManager(); + // If the container is body try move it into the closest text node or position + if (container === body) { + // If start is before/after a image, table etc + if (directionLeft) { + node = container.childNodes[offset > 0 ? offset - 1 : 0]; + if (node) { + if (isCaretContainer(node)) { + return; + } - return AddOnManager; - } -); + if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { + return; + } + } + } -/** - * TinyMCE theme class. - * - * @class tinymce.Theme - */ + // Resolve the index + if (container.hasChildNodes()) { + offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); + container = container.childNodes[offset]; + offset = 0; -/** - * This method is responsible for rendering/generating the overall user interface with toolbars, buttons, iframe containers etc. - * - * @method renderUI - * @param {Object} obj Object parameter containing the targetNode DOM node that will be replaced visually with an editor instance. - * @return {Object} an object with items like iframeContainer, editorContainer, sizeContainer, deltaWidth, deltaHeight. - */ + // Don't normalize non collapsed selections like

    [a

    ] + if (!collapsed && container === body.lastChild && container.nodeName === 'TABLE') { + return; + } -/** - * Plugin base class, this is a pseudo class that describes how a plugin is to be created for TinyMCE. The methods below are all optional. - * - * @class tinymce.Plugin - * @example - * tinymce.PluginManager.add('example', function(editor, url) { - * // Add a button that opens a window - * editor.addButton('example', { - * text: 'My button', - * icon: false, - * onclick: function() { - * // Open window - * editor.windowManager.open({ - * title: 'Example plugin', - * body: [ - * {type: 'textbox', name: 'title', label: 'Title'} - * ], - * onsubmit: function(e) { - * // Insert content when the window form is submitted - * editor.insertContent('Title: ' + e.data.title); - * } - * }); - * } - * }); - * - * // Adds a menu item to the tools menu - * editor.addMenuItem('example', { - * text: 'Example plugin', - * context: 'tools', - * onclick: function() { - * // Open window with a specific url - * editor.windowManager.open({ - * title: 'TinyMCE site', - * url: 'http://www.tinymce.com', - * width: 800, - * height: 600, - * buttons: [{ - * text: 'Close', - * onclick: 'close' - * }] - * }); - * } - * }); - * }); - */ + if (hasContentEditableFalseParent(container) || isCaretContainer(container)) { + return; + } -define( - 'ephox.katamari.api.Cell', + // Don't walk into elements that doesn't have any child nodes like a IMG + if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { + // Walk the DOM to find a text node to place the caret at or a BR + node = container; + walker = new TreeWalker(container, body); - [ - ], + do { + if (isContentEditableFalse(node) || isCaretContainer(node)) { + normalized = false; + break; + } - function () { - var Cell = function (initial) { - var value = initial; + // Found a text node use that position + if (node.nodeType === 3 && node.nodeValue.length > 0) { + offset = directionLeft ? 0 : node.nodeValue.length; + container = node; + normalized = true; + break; + } - var get = function () { - return value; - }; + // Found a BR/IMG/PRE element that we can place the caret before + if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) { + offset = dom.nodeIndex(node); + container = node.parentNode; - var set = function (v) { - value = v; - }; + // Put caret after image and pre tag when moving the end point + if ((node.nodeName === "IMG" || node.nodeName === "PRE") && !directionLeft) { + offset++; + } - var clone = function () { - return Cell(get()); - }; + normalized = true; + break; + } + } while ((node = (directionLeft ? walker.next() : walker.prev()))); + } + } + } - return { - get: get, - set: set, - clone: clone - }; - }; + // Lean the caret to the left if possible + if (collapsed) { + // So this: x|x + // Becomes: x|x + // Seems that only gecko has issues with this + if (container.nodeType === 3 && offset === 0) { + findTextNodeRelative(true); + } - return Cell; - } -); + // Lean left into empty inline elements when the caret is before a BR + // So this: |
    + // Becomes: |
    + // Seems that only gecko has issues with this. + // Special edge case for

    x|

    since we don't want

    x|

    + if (container.nodeType === 1) { + node = container.childNodes[offset]; -/** - * NodeType.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 - */ + // Offset is after the containers last child + // then use the previous child for normalization + if (!node) { + node = container.childNodes[offset - 1]; + } -/** - * Contains various node validation functions. - * - * @private - * @class tinymce.dom.NodeType - */ -define( - 'tinymce.core.dom.NodeType', - [ - ], - function () { - function isNodeType(type) { - return function (node) { - return !!node && node.nodeType == type; - }; - } + if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') && + !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { + findTextNodeRelative(true, node); + } + } + } - var isElement = isNodeType(1); + // Lean the start of the selection right if possible + // So this: x[x] + // Becomes: x[x] + if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { + findTextNodeRelative(false); + } - function matchNodeNames(names) { - names = names.toLowerCase().split(' '); + // Set endpoint if it was normalized + if (normalized) { + rng['set' + (start ? 'Start' : 'End')](container, offset); + } + } - return function (node) { - var i, name; + collapsed = rng.collapsed; - if (node && node.nodeType) { - name = node.nodeName.toLowerCase(); + normalizeEndPoint(true); - for (i = 0; i < names.length; i++) { - if (name === names[i]) { - return true; - } - } + if (!collapsed) { + normalizeEndPoint(); } - return false; + // If it was collapsed then make sure it still is + if (normalized && collapsed) { + rng.collapse(true); + } + + return normalized; }; } - function matchStyleValues(name, values) { - values = values.toLowerCase().split(' '); + /** + * Compares two ranges and checks if they are equal. + * + * @static + * @method compareRanges + * @param {DOMRange} rng1 First range to compare. + * @param {DOMRange} rng2 First range to compare. + * @return {Boolean} true/false if the ranges are equal. + */ + RangeUtils.compareRanges = function (rng1, rng2) { + return rng1 && rng2 && + (rng1.startContainer === rng2.startContainer && rng1.startOffset === rng2.startOffset) && + (rng1.endContainer === rng2.endContainer && rng1.endOffset === rng2.endOffset); + }; - return function (node) { - var i, cssValue; + /** + * Finds the closest selection rect tries to get the range from that. + */ + function findClosestIeRange(clientX, clientY, doc) { + var element, rng, rects; - if (isElement(node)) { - for (i = 0; i < values.length; i++) { - cssValue = node.ownerDocument.defaultView.getComputedStyle(node, null).getPropertyValue(name); - if (cssValue === values[i]) { - return true; - } - } + element = doc.elementFromPoint(clientX, clientY); + rng = doc.body.createTextRange(); + + if (!element || element.tagName == 'HTML') { + element = doc.body; + } + + rng.moveToElementText(element); + rects = Tools.toArray(rng.getClientRects()); + + rects = rects.sort(function (a, b) { + a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY)); + b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY)); + + return a - b; + }); + + if (rects.length > 0) { + clientY = (rects[0].bottom + rects[0].top) / 2; + + try { + rng.moveToPoint(clientX, clientY); + rng.collapse(true); + + return rng; + } catch (ex) { + // At least we tried } + } - return false; - }; + return null; } - function hasPropValue(propName, propValue) { - return function (node) { - return isElement(node) && node[propName] === propValue; - }; + function moveOutOfContentEditableFalse(rng, rootNode) { + var parentElement = rng && rng.parentElement ? rng.parentElement() : null; + return isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng; } - function hasAttribute(attrName, attrValue) { - return function (node) { - return isElement(node) && node.hasAttribute(attrName); - }; - } + /** + * Gets the caret range for the given x/y location. + * + * @static + * @method getCaretRangeFromPoint + * @param {Number} clientX X coordinate for range + * @param {Number} clientY Y coordinate for range + * @param {Document} doc Document that x/y are relative to + * @returns {Range} caret range + */ + RangeUtils.getCaretRangeFromPoint = function (clientX, clientY, doc) { + var rng, point; - function hasAttributeValue(attrName, attrValue) { - return function (node) { - return isElement(node) && node.getAttribute(attrName) === attrValue; - }; - } + if (doc.caretPositionFromPoint) { + point = doc.caretPositionFromPoint(clientX, clientY); + rng = doc.createRange(); + rng.setStart(point.offsetNode, point.offset); + rng.collapse(true); + } else if (doc.caretRangeFromPoint) { + rng = doc.caretRangeFromPoint(clientX, clientY); + } else if (doc.body.createTextRange) { + rng = doc.body.createTextRange(); - function isBogus(node) { - return isElement(node) && node.hasAttribute('data-mce-bogus'); - } + try { + rng.moveToPoint(clientX, clientY); + rng.collapse(true); + } catch (ex) { + rng = findClosestIeRange(clientX, clientY, doc); + } - function hasContentEditableState(value) { - return function (node) { - if (isElement(node)) { - if (node.contentEditable === value) { - return true; - } + return moveOutOfContentEditableFalse(rng, doc.body); + } - if (node.getAttribute('data-mce-contenteditable') === value) { - return true; - } + return rng; + }; + + RangeUtils.getSelectedNode = function (range) { + var startContainer = range.startContainer, + startOffset = range.startOffset; + + if (startContainer.hasChildNodes() && range.endOffset == startOffset + 1) { + return startContainer.childNodes[startOffset]; + } + + return null; + }; + + RangeUtils.getNode = function (container, offset) { + if (container.nodeType === 1 && container.hasChildNodes()) { + if (offset >= container.childNodes.length) { + offset = container.childNodes.length - 1; } - return false; - }; - } + container = container.childNodes[offset]; + } - return { - isText: isNodeType(3), - isElement: isElement, - isComment: isNodeType(8), - isBr: matchNodeNames('br'), - isContentEditableTrue: hasContentEditableState('true'), - isContentEditableFalse: hasContentEditableState('false'), - matchNodeNames: matchNodeNames, - hasPropValue: hasPropValue, - hasAttribute: hasAttribute, - hasAttributeValue: hasAttributeValue, - matchStyleValues: matchStyleValues, - isBogus: isBogus + return container; }; + + return RangeUtils; } ); + /** - * Fun.js + * CaretCandidate.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -11992,93 +12440,87 @@ define( */ /** - * Functional utility class. + * This module contains logic for handling caret candidates. A caret candidate is + * for example text nodes, images, input elements, cE=false elements etc. * * @private - * @class tinymce.util.Fun + * @class tinymce.caret.CaretCandidate */ define( - 'tinymce.core.util.Fun', + 'tinymce.core.caret.CaretCandidate', [ + "tinymce.core.dom.NodeType", + "tinymce.core.util.Arr", + "tinymce.core.caret.CaretContainer" ], - function () { - var slice = [].slice; - - function constant(value) { - return function () { - return value; - }; - } - - function negate(predicate) { - return function (x) { - return !predicate(x); - }; - } - - function compose(f, g) { - return function (x) { - return f(g(x)); - }; - } + function (NodeType, Arr, CaretContainer) { + var isContentEditableTrue = NodeType.isContentEditableTrue, + isContentEditableFalse = NodeType.isContentEditableFalse, + isBr = NodeType.isBr, + isText = NodeType.isText, + isInvalidTextElement = NodeType.matchNodeNames('script style textarea'), + isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'), + isTable = NodeType.matchNodeNames('table'), + isCaretContainer = CaretContainer.isCaretContainer; - function or() { - var args = slice.call(arguments); + function isCaretCandidate(node) { + if (isCaretContainer(node)) { + return false; + } - return function (x) { - for (var i = 0; i < args.length; i++) { - if (args[i](x)) { - return true; - } + if (isText(node)) { + if (isInvalidTextElement(node.parentNode)) { + return false; } - return false; - }; + return true; + } + + return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node); } - function and() { - var args = slice.call(arguments); + function isInEditable(node, rootNode) { + for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { + if (isContentEditableFalse(node)) { + return false; + } - return function (x) { - for (var i = 0; i < args.length; i++) { - if (!args[i](x)) { - return false; - } + if (isContentEditableTrue(node)) { + return true; } + } - return true; - }; + return true; } - function curry(fn) { - var args = slice.call(arguments); - - if (args.length - 1 >= fn.length) { - return fn.apply(this, args.slice(1)); + function isAtomicContentEditableFalse(node) { + if (!isContentEditableFalse(node)) { + return false; } - return function () { - var tempArgs = args.concat([].slice.call(arguments)); - return curry.apply(this, tempArgs); - }; + return Arr.reduce(node.getElementsByTagName('*'), function (result, elm) { + return result || isContentEditableTrue(elm); + }, false) !== true; } - function noop() { + function isAtomic(node) { + return isAtomicInline(node) || isAtomicContentEditableFalse(node); + } + + function isEditableCaretCandidate(node, rootNode) { + return isCaretCandidate(node) && isInEditable(node, rootNode); } return { - constant: constant, - negate: negate, - and: and, - or: or, - curry: curry, - compose: compose, - noop: noop + isCaretCandidate: isCaretCandidate, + isInEditable: isInEditable, + isAtomic: isAtomic, + isEditableCaretCandidate: isEditableCaretCandidate }; } ); /** - * Zwsp.js + * ClientRect.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -12088,237 +12530,197 @@ define( */ /** - * Utility functions for working with zero width space - * characters used as character containers etc. + * Utility functions for working with client rects. * * @private - * @class tinymce.text.Zwsp - * @example - * var isZwsp = Zwsp.isZwsp('\uFEFF'); - * var abc = Zwsp.trim('a\uFEFFc'); + * @class tinymce.geom.ClientRect */ define( - 'tinymce.core.text.Zwsp', + 'tinymce.core.geom.ClientRect', [ ], function () { - // This is technically not a ZWSP but a ZWNBSP or a BYTE ORDER MARK it used to be a ZWSP - var ZWSP = '\uFEFF'; - - var isZwsp = function (chr) { - return chr === ZWSP; - }; + var round = Math.round; - var trim = function (text) { - return text.replace(new RegExp(ZWSP, 'g'), ''); - }; + function clone(rect) { + if (!rect) { + return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 }; + } - return { - isZwsp: isZwsp, - ZWSP: ZWSP, - trim: trim - }; - } -); -/** - * CaretContainer.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 - */ + return { + left: round(rect.left), + top: round(rect.top), + bottom: round(rect.bottom), + right: round(rect.right), + width: round(rect.width), + height: round(rect.height) + }; + } -/** - * This module handles caret containers. A caret container is a node that - * holds the caret for positional purposes. - * - * @private - * @class tinymce.caret.CaretContainer - */ -define( - 'tinymce.core.caret.CaretContainer', - [ - "tinymce.core.dom.NodeType", - "tinymce.core.text.Zwsp" - ], - function (NodeType, Zwsp) { - var isElement = NodeType.isElement, - isText = NodeType.isText; + function collapse(clientRect, toStart) { + clientRect = clone(clientRect); - function isCaretContainerBlock(node) { - if (isText(node)) { - node = node.parentNode; + if (toStart) { + clientRect.right = clientRect.left; + } else { + clientRect.left = clientRect.left + clientRect.width; + clientRect.right = clientRect.left; } - return isElement(node) && node.hasAttribute('data-mce-caret'); - } + clientRect.width = 0; - function isCaretContainerInline(node) { - return isText(node) && Zwsp.isZwsp(node.data); + return clientRect; } - function isCaretContainer(node) { - return isCaretContainerBlock(node) || isCaretContainerInline(node); + function isEqual(rect1, rect2) { + return ( + rect1.left === rect2.left && + rect1.top === rect2.top && + rect1.bottom === rect2.bottom && + rect1.right === rect2.right + ); } - var hasContent = function (node) { - return node.firstChild !== node.lastChild || !NodeType.isBr(node.firstChild); - }; - - function insertInline(node, before) { - var doc, sibling, textNode, parentNode; - - doc = node.ownerDocument; - textNode = doc.createTextNode(Zwsp.ZWSP); - parentNode = node.parentNode; - - if (!before) { - sibling = node.nextSibling; - if (isText(sibling)) { - if (isCaretContainer(sibling)) { - return sibling; - } - - if (startsWithCaretContainer(sibling)) { - sibling.splitText(1); - return sibling; - } - } + function isValidOverflow(overflowY, clientRect1, clientRect2) { + return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2; - if (node.nextSibling) { - parentNode.insertBefore(textNode, node.nextSibling); - } else { - parentNode.appendChild(textNode); - } - } else { - sibling = node.previousSibling; - if (isText(sibling)) { - if (isCaretContainer(sibling)) { - return sibling; - } + } - if (endsWithCaretContainer(sibling)) { - return sibling.splitText(sibling.data.length - 1); - } - } + function isAbove(clientRect1, clientRect2) { + if ((clientRect1.bottom - clientRect1.height / 2) < clientRect2.top) { + return true; + } - parentNode.insertBefore(textNode, node); + if (clientRect1.top > clientRect2.bottom) { + return false; } - return textNode; + return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2); } - var prependInline = function (node) { - if (NodeType.isText(node)) { - var data = node.data; - if (data.length > 0 && data.charAt(0) !== Zwsp.ZWSP) { - node.insertData(0, Zwsp.ZWSP); - } - return node; - } else { - return null; + function isBelow(clientRect1, clientRect2) { + if (clientRect1.top > clientRect2.bottom) { + return true; } - }; - var appendInline = function (node) { - if (NodeType.isText(node)) { - var data = node.data; - if (data.length > 0 && data.charAt(data.length - 1) !== Zwsp.ZWSP) { - node.insertData(data.length, Zwsp.ZWSP); - } - return node; - } else { - return null; + if (clientRect1.bottom < clientRect2.top) { + return false; } - }; - var isBeforeInline = function (pos) { - return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset()) === Zwsp.ZWSP; - }; + return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2); + } - var isAfterInline = function (pos) { - return pos && NodeType.isText(pos.container()) && pos.container().data.charAt(pos.offset() - 1) === Zwsp.ZWSP; - }; + function isLeft(clientRect1, clientRect2) { + return clientRect1.left < clientRect2.left; + } - function createBogusBr() { - var br = document.createElement('br'); - br.setAttribute('data-mce-bogus', '1'); - return br; + function isRight(clientRect1, clientRect2) { + return clientRect1.right > clientRect2.right; } - function insertBlock(blockName, node, before) { - var doc, blockNode, parentNode; + function compare(clientRect1, clientRect2) { + if (isAbove(clientRect1, clientRect2)) { + return -1; + } - doc = node.ownerDocument; - blockNode = doc.createElement(blockName); - blockNode.setAttribute('data-mce-caret', before ? 'before' : 'after'); - blockNode.setAttribute('data-mce-bogus', 'all'); - blockNode.appendChild(createBogusBr()); - parentNode = node.parentNode; + if (isBelow(clientRect1, clientRect2)) { + return 1; + } - if (!before) { - if (node.nextSibling) { - parentNode.insertBefore(blockNode, node.nextSibling); - } else { - parentNode.appendChild(blockNode); - } - } else { - parentNode.insertBefore(blockNode, node); + if (isLeft(clientRect1, clientRect2)) { + return -1; } - return blockNode; - } + if (isRight(clientRect1, clientRect2)) { + return 1; + } - function startsWithCaretContainer(node) { - return isText(node) && node.data[0] == Zwsp.ZWSP; + return 0; } - function endsWithCaretContainer(node) { - return isText(node) && node.data[node.data.length - 1] == Zwsp.ZWSP; + function containsXY(clientRect, clientX, clientY) { + return ( + clientX >= clientRect.left && + clientX <= clientRect.right && + clientY >= clientRect.top && + clientY <= clientRect.bottom + ); } - function trimBogusBr(elm) { - var brs = elm.getElementsByTagName('br'); - var lastBr = brs[brs.length - 1]; - if (NodeType.isBogus(lastBr)) { - lastBr.parentNode.removeChild(lastBr); - } - } + return { + clone: clone, + collapse: collapse, + isEqual: isEqual, + isAbove: isAbove, + isBelow: isBelow, + isLeft: isLeft, + isRight: isRight, + compare: compare, + containsXY: containsXY + }; + } +); - function showCaretContainerBlock(caretContainer) { - if (caretContainer && caretContainer.hasAttribute('data-mce-caret')) { - trimBogusBr(caretContainer); - caretContainer.removeAttribute('data-mce-caret'); - caretContainer.removeAttribute('data-mce-bogus'); - caretContainer.removeAttribute('style'); - caretContainer.removeAttribute('_moz_abspos'); - return caretContainer; - } +/** + * ExtendingChar.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 + */ - return null; +/** + * This class contains logic for detecting extending characters. + * + * @private + * @class tinymce.text.ExtendingChar + * @example + * var isExtending = ExtendingChar.isExtendingChar('a'); + */ +define( + 'tinymce.core.text.ExtendingChar', + [ + ], + function () { + // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt + // Only includes the characters in that fit into UCS-2 16 bit + var extendingChars = new RegExp( + "[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" + + "\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" + + "\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" + + "\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" + + "\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" + + "\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" + + "\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" + + "\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" + + "\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" + + "\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" + + "\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" + + "\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" + + "\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" + + "\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" + + "\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" + + "\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" + + "\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" + + "\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" + + "\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" + + "\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" + + "\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]" + ); + + function isExtendingChar(ch) { + return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } return { - isCaretContainer: isCaretContainer, - isCaretContainerBlock: isCaretContainerBlock, - isCaretContainerInline: isCaretContainerInline, - showCaretContainerBlock: showCaretContainerBlock, - insertInline: insertInline, - prependInline: prependInline, - appendInline: appendInline, - isBeforeInline: isBeforeInline, - isAfterInline: isAfterInline, - insertBlock: insertBlock, - hasContent: hasContent, - startsWithCaretContainer: startsWithCaretContainer, - endsWithCaretContainer: endsWithCaretContainer + isExtendingChar: isExtendingChar }; } ); /** - * RangeUtils.js + * CaretPosition.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -12328,771 +12730,667 @@ define( */ /** - * This class contains a few utility methods for ranges. + * This module contains logic for creating caret positions within a document a caretposition + * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight + * since it's now updated live when the DOM changes. * - * @class tinymce.dom.RangeUtils + * @private + * @class tinymce.caret.CaretPosition + * @example + * var caretPos1 = new CaretPosition(container, offset); + * var caretPos2 = CaretPosition.fromRangeStart(someRange); */ define( - 'tinymce.core.dom.RangeUtils', + 'tinymce.core.caret.CaretPosition', [ - "tinymce.core.util.Tools", - "tinymce.core.dom.TreeWalker", + "tinymce.core.util.Fun", "tinymce.core.dom.NodeType", - "tinymce.core.dom.Range", - "tinymce.core.caret.CaretContainer" + "tinymce.core.dom.DOMUtils", + "tinymce.core.dom.RangeUtils", + "tinymce.core.caret.CaretCandidate", + "tinymce.core.geom.ClientRect", + "tinymce.core.text.ExtendingChar" ], - function (Tools, TreeWalker, NodeType, Range, CaretContainer) { - var each = Tools.each, - isContentEditableTrue = NodeType.isContentEditableTrue, - isContentEditableFalse = NodeType.isContentEditableFalse, - isCaretContainer = CaretContainer.isCaretContainer; + function (Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) { + var isElement = NodeType.isElement, + isCaretCandidate = CaretCandidate.isCaretCandidate, + isBlock = NodeType.matchStyleValues('display', 'block table'), + isFloated = NodeType.matchStyleValues('float', 'left right'), + isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)), + isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')), + isText = NodeType.isText, + isBr = NodeType.isBr, + nodeIndex = DOMUtils.nodeIndex, + resolveIndex = RangeUtils.getNode; - function hasCeProperty(node) { - return isContentEditableTrue(node) || isContentEditableFalse(node); + function createRange(doc) { + return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng(); } - function getEndChild(container, index) { - var childNodes = container.childNodes; + function isWhiteSpace(chr) { + return chr && /[\r\n\t ]/.test(chr); + } - index--; + function isHiddenWhiteSpaceRange(range) { + var container = range.startContainer, + offset = range.startOffset, + text; - if (index > childNodes.length - 1) { - index = childNodes.length - 1; - } else if (index < 0) { - index = 0; + if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) { + text = container.data; + + if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) { + return true; + } } - return childNodes[index] || container; + return false; } - function findParent(node, rootNode, predicate) { - while (node && node !== rootNode) { - if (predicate(node)) { - return node; - } + function getCaretPositionClientRects(caretPosition) { + var clientRects = [], beforeNode, node; - node = node.parentNode; - } + // Hack for older WebKit versions that doesn't + // support getBoundingClientRect on BR elements + function getBrClientRect(brNode) { + var doc = brNode.ownerDocument, + rng = createRange(doc), + nbsp = doc.createTextNode('\u00a0'), + parentNode = brNode.parentNode, + clientRect; - return null; - } + parentNode.insertBefore(nbsp, brNode); + rng.setStart(nbsp, 0); + rng.setEnd(nbsp, 1); + clientRect = ClientRect.clone(rng.getBoundingClientRect()); + parentNode.removeChild(nbsp); - function hasParent(node, rootNode, predicate) { - return findParent(node, rootNode, predicate) !== null; - } + return clientRect; + } - function hasParentWithName(node, rootNode, name) { - return hasParent(node, rootNode, function (node) { - return node.nodeName === name; - }); - } + function getBoundingClientRect(item) { + var clientRect, clientRects; - function isFormatterCaret(node) { - return node.id === '_mce_caret'; - } + clientRects = item.getClientRects(); + if (clientRects.length > 0) { + clientRect = ClientRect.clone(clientRects[0]); + } else { + clientRect = ClientRect.clone(item.getBoundingClientRect()); + } - function isCeFalseCaretContainer(node, rootNode) { - return isCaretContainer(node) && hasParent(node, rootNode, isFormatterCaret) === false; - } + if (isBr(item) && clientRect.left === 0) { + return getBrClientRect(item); + } - function RangeUtils(dom) { - /** - * Walks the specified range like object and executes the callback for each sibling collection it finds. - * - * @private - * @method walk - * @param {Object} rng Range like object. - * @param {function} callback Callback function to execute for each sibling collection. - */ - this.walk = function (rng, callback) { - var startContainer = rng.startContainer, - startOffset = rng.startOffset, - endContainer = rng.endContainer, - endOffset = rng.endOffset, - ancestor, startPoint, - endPoint, node, parent, siblings, nodes; + return clientRect; + } - // Handle table cell selection the table plugin enables - // you to fake select table cells and perform formatting actions on them - nodes = dom.select('td[data-mce-selected],th[data-mce-selected]'); - if (nodes.length > 0) { - each(nodes, function (node) { - callback([node]); - }); + function collapseAndInflateWidth(clientRect, toStart) { + clientRect = ClientRect.collapse(clientRect, toStart); + clientRect.width = 1; + clientRect.right = clientRect.left + 1; + + return clientRect; + } + function addUniqueAndValidRect(clientRect) { + if (clientRect.height === 0) { return; } - /** - * Excludes start/end text node if they are out side the range - * - * @private - * @param {Array} nodes Nodes to exclude items from. - * @return {Array} Array with nodes excluding the start/end container if needed. - */ - function exclude(nodes) { - var node; - - // First node is excluded - node = nodes[0]; - if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { - nodes.splice(0, 1); + if (clientRects.length > 0) { + if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) { + return; } + } - // Last node is excluded - node = nodes[nodes.length - 1]; - if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { - nodes.splice(nodes.length - 1, 1); + clientRects.push(clientRect); + } + + function addCharacterOffset(container, offset) { + var range = createRange(container.ownerDocument); + + if (offset < container.data.length) { + if (ExtendingChar.isExtendingChar(container.data[offset])) { + return clientRects; } - return nodes; + // WebKit returns two client rects for a position after an extending + // character a\uxxx|b so expand on "b" and collapse to start of "b" box + if (ExtendingChar.isExtendingChar(container.data[offset - 1])) { + range.setStart(container, offset); + range.setEnd(container, offset + 1); + + if (!isHiddenWhiteSpaceRange(range)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false)); + return clientRects; + } + } } - /** - * Collects siblings - * - * @private - * @param {Node} node Node to collect siblings from. - * @param {String} name Name of the sibling to check for. - * @param {Node} endNode - * @return {Array} Array of collected siblings. - */ - function collectSiblings(node, name, endNode) { - var siblings = []; + if (offset > 0) { + range.setStart(container, offset - 1); + range.setEnd(container, offset); - for (; node && node != endNode; node = node[name]) { - siblings.push(node); + if (!isHiddenWhiteSpaceRange(range)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false)); } + } - return siblings; + if (offset < container.data.length) { + range.setStart(container, offset); + range.setEnd(container, offset + 1); + + if (!isHiddenWhiteSpaceRange(range)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true)); + } } + } - /** - * Find an end point this is the node just before the common ancestor root. - * - * @private - * @param {Node} node Node to start at. - * @param {Node} root Root/ancestor element to stop just before. - * @return {Node} Node just before the root element. - */ - function findEndPoint(node, root) { - do { - if (node.parentNode == root) { - return node; - } - - node = node.parentNode; - } while (node); - } + if (isText(caretPosition.container())) { + addCharacterOffset(caretPosition.container(), caretPosition.offset()); + return clientRects; + } - function walkBoundary(startNode, endNode, next) { - var siblingName = next ? 'nextSibling' : 'previousSibling'; + if (isElement(caretPosition.container())) { + if (caretPosition.isAtEnd()) { + node = resolveIndex(caretPosition.container(), caretPosition.offset()); + if (isText(node)) { + addCharacterOffset(node, node.data.length); + } - for (node = startNode, parent = node.parentNode; node && node != endNode; node = parent) { - parent = node.parentNode; - siblings = collectSiblings(node == startNode ? node : node[siblingName], siblingName); + if (isValidElementCaretCandidate(node) && !isBr(node)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false)); + } + } else { + node = resolveIndex(caretPosition.container(), caretPosition.offset()); + if (isText(node)) { + addCharacterOffset(node, 0); + } - if (siblings.length) { - if (!next) { - siblings.reverse(); - } + if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false)); + return clientRects; + } - callback(exclude(siblings)); + beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1); + if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) { + if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false)); } } - } - // If index based start position then resolve it - if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { - startContainer = startContainer.childNodes[startOffset]; + if (isValidElementCaretCandidate(node)) { + addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true)); + } } + } - // If index based end position then resolve it - if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { - endContainer = getEndChild(endContainer, endOffset); - } + return clientRects; + } - // Same container - if (startContainer == endContainer) { - return callback(exclude([startContainer])); + /** + * Represents a location within the document by a container and an offset. + * + * @constructor + * @param {Node} container Container node. + * @param {Number} offset Offset within that container node. + * @param {Array} clientRects Optional client rects array for the position. + */ + function CaretPosition(container, offset, clientRects) { + function isAtStart() { + if (isText(container)) { + return offset === 0; } - // Find common ancestor and end points - ancestor = dom.findCommonAncestor(startContainer, endContainer); - - // Process left side - for (node = startContainer; node; node = node.parentNode) { - if (node === endContainer) { - return walkBoundary(startContainer, ancestor, true); - } + return offset === 0; + } - if (node === ancestor) { - break; - } + function isAtEnd() { + if (isText(container)) { + return offset >= container.data.length; } - // Process right side - for (node = endContainer; node; node = node.parentNode) { - if (node === startContainer) { - return walkBoundary(endContainer, ancestor); - } - - if (node === ancestor) { - break; - } - } + return offset >= container.childNodes.length; + } - // Find start/end point - startPoint = findEndPoint(startContainer, ancestor) || startContainer; - endPoint = findEndPoint(endContainer, ancestor) || endContainer; + function toRange() { + var range; - // Walk left leaf - walkBoundary(startContainer, startPoint, true); + range = createRange(container.ownerDocument); + range.setStart(container, offset); + range.setEnd(container, offset); - // Walk the middle from start to end point - siblings = collectSiblings( - startPoint == startContainer ? startPoint : startPoint.nextSibling, - 'nextSibling', - endPoint == endContainer ? endPoint.nextSibling : endPoint - ); + return range; + } - if (siblings.length) { - callback(exclude(siblings)); + function getClientRects() { + if (!clientRects) { + clientRects = getCaretPositionClientRects(new CaretPosition(container, offset)); } - // Walk right leaf - walkBoundary(endContainer, endPoint); - }; + return clientRects; + } - /** - * Splits the specified range at it's start/end points. - * - * @private - * @param {Range/RangeObject} rng Range to split. - * @return {Object} Range position object. - */ - this.split = function (rng) { - var startContainer = rng.startContainer, - startOffset = rng.startOffset, - endContainer = rng.endContainer, - endOffset = rng.endOffset; + function isVisible() { + return getClientRects().length > 0; + } - function splitText(node, offset) { - return node.splitText(offset); - } + function isEqual(caretPosition) { + return caretPosition && container === caretPosition.container() && offset === caretPosition.offset(); + } - // Handle single text node - if (startContainer == endContainer && startContainer.nodeType == 3) { - if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { - endContainer = splitText(startContainer, startOffset); - startContainer = endContainer.previousSibling; + function getNode(before) { + return resolveIndex(container, before ? offset - 1 : offset); + } - if (endOffset > startOffset) { - endOffset = endOffset - startOffset; - startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; - endOffset = endContainer.nodeValue.length; - startOffset = 0; - } else { - endOffset = 0; - } - } - } else { - // Split startContainer text node if needed - if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) { - startContainer = splitText(startContainer, startOffset); - startOffset = 0; - } + return { + /** + * Returns the container node. + * + * @method container + * @return {Node} Container node. + */ + container: Fun.constant(container), - // Split endContainer text node if needed - if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) { - endContainer = splitText(endContainer, endOffset).previousSibling; - endOffset = endContainer.nodeValue.length; - } - } + /** + * Returns the offset within the container node. + * + * @method offset + * @return {Number} Offset within the container node. + */ + offset: Fun.constant(offset), - return { - startContainer: startContainer, - startOffset: startOffset, - endContainer: endContainer, - endOffset: endOffset - }; - }; + /** + * Returns a range out of a the caret position. + * + * @method toRange + * @return {DOMRange} range for the caret position. + */ + toRange: toRange, - /** - * Normalizes the specified range by finding the closest best suitable caret location. - * - * @private - * @param {Range} rng Range to normalize. - * @return {Boolean} True/false if the specified range was normalized or not. - */ - this.normalize = function (rng) { - var normalized = false, collapsed; + /** + * Returns the client rects for the caret position. Might be multiple rects between + * block elements. + * + * @method getClientRects + * @return {Array} Array of client rects. + */ + getClientRects: getClientRects, - function normalizeEndPoint(start) { - var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap; - var directionLeft, isAfterNode; + /** + * Returns true if the caret location is visible/displayed on screen. + * + * @method isVisible + * @return {Boolean} true/false if the position is visible or not. + */ + isVisible: isVisible, - function isTableCell(node) { - return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); - } + /** + * Returns true if the caret location is at the beginning of text node or container. + * + * @method isVisible + * @return {Boolean} true/false if the position is at the beginning. + */ + isAtStart: isAtStart, - function hasBrBeforeAfter(node, left) { - var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body); + /** + * Returns true if the caret location is at the end of text node or container. + * + * @method isVisible + * @return {Boolean} true/false if the position is at the end. + */ + isAtEnd: isAtEnd, - while ((node = walker[left ? 'prev' : 'next']())) { - if (node.nodeName === "BR") { - return true; - } - } - } + /** + * Compares the caret position to another caret position. This will only compare the + * container and offset not it's visual position. + * + * @method isEqual + * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with. + * @return {Boolean} true if the caret positions are equal. + */ + isEqual: isEqual, - function hasContentEditableFalseParent(node) { - while (node && node != body) { - if (isContentEditableFalse(node)) { - return true; - } + /** + * Returns the closest resolved node from a node index. That means if you have an offset after the + * last node in a container it will return that last node. + * + * @method getNode + * @return {Node} Node that is closest to the index. + */ + getNode: getNode + }; + } - node = node.parentNode; - } + /** + * Creates a caret position from the start of a range. + * + * @method fromRangeStart + * @param {DOMRange} range DOM Range to create caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range. + */ + CaretPosition.fromRangeStart = function (range) { + return new CaretPosition(range.startContainer, range.startOffset); + }; - return false; - } + /** + * Creates a caret position from the end of a range. + * + * @method fromRangeEnd + * @param {DOMRange} range DOM Range to create caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range. + */ + CaretPosition.fromRangeEnd = function (range) { + return new CaretPosition(range.endContainer, range.endOffset); + }; - function isPrevNode(node, name) { - return node.previousSibling && node.previousSibling.nodeName == name; - } + /** + * Creates a caret position from a node and places the offset after it. + * + * @method after + * @param {Node} node Node to get caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the node. + */ + CaretPosition.after = function (node) { + return new CaretPosition(node.parentNode, nodeIndex(node) + 1); + }; - // Walks the dom left/right to find a suitable text node to move the endpoint into - // It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG - function findTextNodeRelative(left, startNode) { - var walker, lastInlineElement, parentBlockContainer; + /** + * Creates a caret position from a node and places the offset before it. + * + * @method before + * @param {Node} node Node to get caret position from. + * @return {tinymce.caret.CaretPosition} Caret position from the node. + */ + CaretPosition.before = function (node) { + return new CaretPosition(node.parentNode, nodeIndex(node)); + }; - startNode = startNode || container; - parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body; + CaretPosition.isAtStart = function (pos) { + return pos ? pos.isAtStart() : false; + }; - // Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680 - // This:


    |

    becomes

    |

    - if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) { - container = startNode.parentNode; - offset = dom.nodeIndex(startNode); - normalized = true; - return; - } + CaretPosition.isAtEnd = function (pos) { + return pos ? pos.isAtEnd() : false; + }; - // Walk left until we hit a text node we can move to or a block/br/img - walker = new TreeWalker(startNode, parentBlockContainer); - while ((node = walker[left ? 'prev' : 'next']())) { - // Break if we hit a non content editable node - if (dom.getContentEditableParent(node) === "false" || isCeFalseCaretContainer(node, dom.getRoot())) { - return; - } + CaretPosition.isTextPosition = function (pos) { + return pos ? NodeType.isText(pos.container()) : false; + }; - // Found text node that has a length - if (node.nodeType === 3 && node.nodeValue.length > 0) { - if (hasParentWithName(node, body, 'A') === false) { - container = node; - offset = left ? node.nodeValue.length : 0; - normalized = true; - } + return CaretPosition; + } +); +/** + * CaretBookmark.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 + */ - return; - } +/** + * This module creates or resolves xpath like string representation of a CaretPositions. + * + * The format is a / separated list of chunks with: + * [index|after|before] + * + * For example: + * p[0]/b[0]/text()[0],1 =

    a|c

    + * p[0]/img[0],before =

    |

    + * p[0]/img[0],after =

    |

    + * + * @private + * @static + * @class tinymce.caret.CaretBookmark + * @example + * var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild)); + * var caretPosition = CaretBookmark.resolve(bookmark); + */ +define( + 'tinymce.core.caret.CaretBookmark', + [ + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.util.Fun', + 'tinymce.core.util.Arr', + 'tinymce.core.caret.CaretPosition' + ], + function (NodeType, DomUtils, Fun, Arr, CaretPosition) { + var isText = NodeType.isText, + isBogus = NodeType.isBogus, + nodeIndex = DomUtils.nodeIndex; - // Break if we find a block or a BR/IMG/INPUT etc - if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) { - return; - } + function normalizedParent(node) { + var parentNode = node.parentNode; - lastInlineElement = node; - } + if (isBogus(parentNode)) { + return normalizedParent(parentNode); + } - // Only fetch the last inline element when in caret mode for now - if (collapsed && lastInlineElement) { - container = lastInlineElement; - normalized = true; - offset = 0; - } - } + return parentNode; + } - container = rng[(start ? 'start' : 'end') + 'Container']; - offset = rng[(start ? 'start' : 'end') + 'Offset']; - isAfterNode = container.nodeType == 1 && offset === container.childNodes.length; - nonEmptyElementsMap = dom.schema.getNonEmptyElements(); - directionLeft = start; + function getChildNodes(node) { + if (!node) { + return []; + } - if (isCaretContainer(container)) { - return; - } + return Arr.reduce(node.childNodes, function (result, node) { + if (isBogus(node) && node.nodeName != 'BR') { + result = result.concat(getChildNodes(node)); + } else { + result.push(node); + } - if (container.nodeType == 1 && offset > container.childNodes.length - 1) { - directionLeft = false; - } + return result; + }, []); + } - // If the container is a document move it to the body element - if (container.nodeType === 9) { - container = dom.getRoot(); - offset = 0; - } + function normalizedTextOffset(textNode, offset) { + while ((textNode = textNode.previousSibling)) { + if (!isText(textNode)) { + break; + } - // If the container is body try move it into the closest text node or position - if (container === body) { - // If start is before/after a image, table etc - if (directionLeft) { - node = container.childNodes[offset > 0 ? offset - 1 : 0]; - if (node) { - if (isCaretContainer(node)) { - return; - } + offset += textNode.data.length; + } - if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") { - return; - } - } - } + return offset; + } - // Resolve the index - if (container.hasChildNodes()) { - offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1); - container = container.childNodes[offset]; - offset = 0; + function equal(targetValue) { + return function (value) { + return targetValue === value; + }; + } - // Don't normalize non collapsed selections like

    [a

    ] - if (!collapsed && container === body.lastChild && container.nodeName === 'TABLE') { - return; - } + function normalizedNodeIndex(node) { + var nodes, index, numTextFragments; - if (hasContentEditableFalseParent(container) || isCaretContainer(container)) { - return; - } - - // Don't walk into elements that doesn't have any child nodes like a IMG - if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) { - // Walk the DOM to find a text node to place the caret at or a BR - node = container; - walker = new TreeWalker(container, body); - - do { - if (isContentEditableFalse(node) || isCaretContainer(node)) { - normalized = false; - break; - } - - // Found a text node use that position - if (node.nodeType === 3 && node.nodeValue.length > 0) { - offset = directionLeft ? 0 : node.nodeValue.length; - container = node; - normalized = true; - break; - } - - // Found a BR/IMG/PRE element that we can place the caret before - if (nonEmptyElementsMap[node.nodeName.toLowerCase()] && !isTableCell(node)) { - offset = dom.nodeIndex(node); - container = node.parentNode; - - // Put caret after image and pre tag when moving the end point - if ((node.nodeName === "IMG" || node.nodeName === "PRE") && !directionLeft) { - offset++; - } - - normalized = true; - break; - } - } while ((node = (directionLeft ? walker.next() : walker.prev()))); - } - } - } - - // Lean the caret to the left if possible - if (collapsed) { - // So this: x|x - // Becomes: x|x - // Seems that only gecko has issues with this - if (container.nodeType === 3 && offset === 0) { - findTextNodeRelative(true); - } - - // Lean left into empty inline elements when the caret is before a BR - // So this: |
    - // Becomes: |
    - // Seems that only gecko has issues with this. - // Special edge case for

    x|

    since we don't want

    x|

    - if (container.nodeType === 1) { - node = container.childNodes[offset]; - - // Offset is after the containers last child - // then use the previous child for normalization - if (!node) { - node = container.childNodes[offset - 1]; - } - - if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') && - !hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) { - findTextNodeRelative(true, node); - } - } - } - - // Lean the start of the selection right if possible - // So this: x[x] - // Becomes: x[x] - if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) { - findTextNodeRelative(false); - } - - // Set endpoint if it was normalized - if (normalized) { - rng['set' + (start ? 'Start' : 'End')](container, offset); - } + nodes = getChildNodes(normalizedParent(node)); + index = Arr.findIndex(nodes, equal(node), node); + nodes = nodes.slice(0, index + 1); + numTextFragments = Arr.reduce(nodes, function (result, node, i) { + if (isText(node) && isText(nodes[i - 1])) { + result++; } - collapsed = rng.collapsed; - - normalizeEndPoint(true); - - if (!collapsed) { - normalizeEndPoint(); - } + return result; + }, 0); - // If it was collapsed then make sure it still is - if (normalized && collapsed) { - rng.collapse(true); - } + nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName)); + index = Arr.findIndex(nodes, equal(node), node); - return normalized; - }; + return index - numTextFragments; } - /** - * Compares two ranges and checks if they are equal. - * - * @static - * @method compareRanges - * @param {DOMRange} rng1 First range to compare. - * @param {DOMRange} rng2 First range to compare. - * @return {Boolean} true/false if the ranges are equal. - */ - RangeUtils.compareRanges = function (rng1, rng2) { - if (rng1 && rng2) { - // Compare native IE ranges - if (rng1.item || rng1.duplicate) { - // Both are control ranges and the selected element matches - if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) { - return true; - } + function createPathItem(node) { + var name; - // Both are text ranges and the range matches - if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) { - return true; - } - } else { - // Compare w3c ranges - return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset; - } + if (isText(node)) { + name = 'text()'; + } else { + name = node.nodeName.toLowerCase(); } - return false; - }; + return name + '[' + normalizedNodeIndex(node) + ']'; + } - /** - * Finds the closest selection rect tries to get the range from that. - */ - function findClosestIeRange(clientX, clientY, doc) { - var element, rng, rects; + function parentsUntil(rootNode, node, predicate) { + var parents = []; - element = doc.elementFromPoint(clientX, clientY); - rng = doc.body.createTextRange(); + for (node = node.parentNode; node != rootNode; node = node.parentNode) { + if (predicate && predicate(node)) { + break; + } - if (!element || element.tagName == 'HTML') { - element = doc.body; + parents.push(node); } - rng.moveToElementText(element); - rects = Tools.toArray(rng.getClientRects()); - - rects = rects.sort(function (a, b) { - a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY)); - b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY)); - - return a - b; - }); + return parents; + } - if (rects.length > 0) { - clientY = (rects[0].bottom + rects[0].top) / 2; + function create(rootNode, caretPosition) { + var container, offset, path = [], + outputOffset, childNodes, parents; - try { - rng.moveToPoint(clientX, clientY); - rng.collapse(true); + container = caretPosition.container(); + offset = caretPosition.offset(); - return rng; - } catch (ex) { - // At least we tried + if (isText(container)) { + outputOffset = normalizedTextOffset(container, offset); + } else { + childNodes = container.childNodes; + if (offset >= childNodes.length) { + outputOffset = 'after'; + offset = childNodes.length - 1; + } else { + outputOffset = 'before'; } + + container = childNodes[offset]; } - return null; - } + path.push(createPathItem(container)); + parents = parentsUntil(rootNode, container); + parents = Arr.filter(parents, Fun.negate(NodeType.isBogus)); + path = path.concat(Arr.map(parents, function (node) { + return createPathItem(node); + })); - function moveOutOfContentEditableFalse(rng, rootNode) { - var parentElement = rng && rng.parentElement ? rng.parentElement() : null; - return isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng; + return path.reverse().join('/') + ',' + outputOffset; } - /** - * Gets the caret range for the given x/y location. - * - * @static - * @method getCaretRangeFromPoint - * @param {Number} clientX X coordinate for range - * @param {Number} clientY Y coordinate for range - * @param {Document} doc Document that x/y are relative to - * @returns {Range} caret range - */ - RangeUtils.getCaretRangeFromPoint = function (clientX, clientY, doc) { - var rng, point; - - if (doc.caretPositionFromPoint) { - point = doc.caretPositionFromPoint(clientX, clientY); - rng = doc.createRange(); - rng.setStart(point.offsetNode, point.offset); - rng.collapse(true); - } else if (doc.caretRangeFromPoint) { - rng = doc.caretRangeFromPoint(clientX, clientY); - } else if (doc.body.createTextRange) { - rng = doc.body.createTextRange(); - - try { - rng.moveToPoint(clientX, clientY); - rng.collapse(true); - } catch (ex) { - rng = findClosestIeRange(clientX, clientY, doc); - } + function resolvePathItem(node, name, index) { + var nodes = getChildNodes(node); - return moveOutOfContentEditableFalse(rng, doc.body); - } + nodes = Arr.filter(nodes, function (node, index) { + return !isText(node) || !isText(nodes[index - 1]); + }); - return rng; - }; + nodes = Arr.filter(nodes, NodeType.matchNodeNames(name)); + return nodes[index]; + } - RangeUtils.getSelectedNode = function (range) { - var startContainer = range.startContainer, - startOffset = range.startOffset; + function findTextPosition(container, offset) { + var node = container, targetOffset = 0, dataLen; - if (startContainer.hasChildNodes() && range.endOffset == startOffset + 1) { - return startContainer.childNodes[startOffset]; - } + while (isText(node)) { + dataLen = node.data.length; - return null; - }; + if (offset >= targetOffset && offset <= targetOffset + dataLen) { + container = node; + offset = offset - targetOffset; + break; + } - RangeUtils.getNode = function (container, offset) { - if (container.nodeType === 1 && container.hasChildNodes()) { - if (offset >= container.childNodes.length) { - offset = container.childNodes.length - 1; + if (!isText(node.nextSibling)) { + container = node; + offset = dataLen; + break; } - container = container.childNodes[offset]; + targetOffset += dataLen; + node = node.nextSibling; } - return container; - }; - - return RangeUtils; - } -); - -/** - * CaretCandidate.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 handling caret candidates. A caret candidate is - * for example text nodes, images, input elements, cE=false elements etc. - * - * @private - * @class tinymce.caret.CaretCandidate - */ -define( - 'tinymce.core.caret.CaretCandidate', - [ - "tinymce.core.dom.NodeType", - "tinymce.core.util.Arr", - "tinymce.core.caret.CaretContainer" - ], - function (NodeType, Arr, CaretContainer) { - var isContentEditableTrue = NodeType.isContentEditableTrue, - isContentEditableFalse = NodeType.isContentEditableFalse, - isBr = NodeType.isBr, - isText = NodeType.isText, - isInvalidTextElement = NodeType.matchNodeNames('script style textarea'), - isAtomicInline = NodeType.matchNodeNames('img input textarea hr iframe video audio object'), - isTable = NodeType.matchNodeNames('table'), - isCaretContainer = CaretContainer.isCaretContainer; - - function isCaretCandidate(node) { - if (isCaretContainer(node)) { - return false; + if (offset > container.data.length) { + offset = container.data.length; } - if (isText(node)) { - if (isInvalidTextElement(node.parentNode)) { - return false; - } + return new CaretPosition(container, offset); + } - return true; + function resolve(rootNode, path) { + var parts, container, offset; + + if (!path) { + return null; } - return isAtomicInline(node) || isBr(node) || isTable(node) || isContentEditableFalse(node); - } + parts = path.split(','); + path = parts[0].split('/'); + offset = parts.length > 1 ? parts[1] : 'before'; - function isInEditable(node, rootNode) { - for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { - if (isContentEditableFalse(node)) { - return false; + container = Arr.reduce(path, function (result, value) { + value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value); + if (!value) { + return null; } - if (isContentEditableTrue(node)) { - return true; + if (value[1] === 'text()') { + value[1] = '#text'; } - } - return true; - } + return resolvePathItem(result, value[1], parseInt(value[2], 10)); + }, rootNode); - function isAtomicContentEditableFalse(node) { - if (!isContentEditableFalse(node)) { - return false; + if (!container) { + return null; } - return Arr.reduce(node.getElementsByTagName('*'), function (result, elm) { - return result || isContentEditableTrue(elm); - }, false) !== true; - } + if (!isText(container)) { + if (offset === 'after') { + offset = nodeIndex(container) + 1; + } else { + offset = nodeIndex(container); + } - function isAtomic(node) { - return isAtomicInline(node) || isAtomicContentEditableFalse(node); - } + return new CaretPosition(container.parentNode, offset); + } - function isEditableCaretCandidate(node, rootNode) { - return isCaretCandidate(node) && isInEditable(node, rootNode); + return findTextPosition(container, parseInt(offset, 10)); } return { - isCaretCandidate: isCaretCandidate, - isInEditable: isInEditable, - isAtomic: isAtomic, - isEditableCaretCandidate: isEditableCaretCandidate + /** + * Create a xpath bookmark location for the specified caret position. + * + * @method create + * @param {Node} rootNode Root node to create bookmark within. + * @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node. + * @return {String} String xpath like location of caret position. + */ + create: create, + + /** + * Resolves a xpath like bookmark location to the a caret position. + * + * @method resolve + * @param {Node} rootNode Root node to resolve xpath bookmark within. + * @param {String} bookmark Bookmark string to resolve. + * @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark. + */ + resolve: resolve }; } ); /** - * ClientRect.js + * BookmarkManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -13102,600 +13400,435 @@ define( */ /** - * Utility functions for working with client rects. + * This class handles selection bookmarks. * - * @private - * @class tinymce.geom.ClientRect + * @class tinymce.dom.BookmarkManager */ define( - 'tinymce.core.geom.ClientRect', + 'tinymce.core.dom.BookmarkManager', [ + 'tinymce.core.caret.CaretBookmark', + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.Env', + 'tinymce.core.text.Zwsp', + 'tinymce.core.util.Tools' ], - function () { - var round = Math.round; + function (CaretBookmark, CaretContainer, CaretPosition, NodeType, RangeUtils, Env, Zwsp, Tools) { + var isContentEditableFalse = NodeType.isContentEditableFalse; - function clone(rect) { - if (!rect) { - return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0 }; - } + var getNormalizedTextOffset = function (container, offset) { + var node, trimmedOffset; - return { - left: round(rect.left), - top: round(rect.top), - bottom: round(rect.bottom), - right: round(rect.right), - width: round(rect.width), - height: round(rect.height) - }; - } + trimmedOffset = Zwsp.trim(container.data.slice(0, offset)).length; + for (node = container.previousSibling; node && node.nodeType === 3; node = node.previousSibling) { + trimmedOffset += Zwsp.trim(node.data).length; + } - function collapse(clientRect, toStart) { - clientRect = clone(clientRect); + return trimmedOffset; + }; - if (toStart) { - clientRect.right = clientRect.left; - } else { - clientRect.left = clientRect.left + clientRect.width; - clientRect.right = clientRect.left; + var trimEmptyTextNode = function (node) { + if (NodeType.isText(node) && node.data.length === 0) { + node.parentNode.removeChild(node); } + }; - clientRect.width = 0; + /** + * Constructs a new BookmarkManager instance for a specific selection instance. + * + * @constructor + * @method BookmarkManager + * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for. + */ + function BookmarkManager(selection) { + var dom = selection.dom; - return clientRect; - } + /** + * Returns a bookmark location for the current selection. This bookmark object + * can then be used to restore the selection after some content modification to the document. + * + * @method getBookmark + * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. + * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. + * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. + * @example + * // Stores a bookmark of the current selection + * var bm = tinymce.activeEditor.selection.getBookmark(); + * + * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinymce.activeEditor.selection.moveToBookmark(bm); + */ + this.getBookmark = function (type, normalized) { + var rng, rng2, id, collapsed, name, element, chr = '', styles; - function isEqual(rect1, rect2) { - return ( - rect1.left === rect2.left && - rect1.top === rect2.top && - rect1.bottom === rect2.bottom && - rect1.right === rect2.right - ); - } + function findIndex(name, element) { + var count = 0; - function isValidOverflow(overflowY, clientRect1, clientRect2) { - return overflowY >= 0 && overflowY <= Math.min(clientRect1.height, clientRect2.height) / 2; + Tools.each(dom.select(name), function (node) { + if (node.getAttribute('data-mce-bogus') === 'all') { + return; + } - } + if (node == element) { + return false; + } - function isAbove(clientRect1, clientRect2) { - if ((clientRect1.bottom - clientRect1.height / 2) < clientRect2.top) { - return true; - } + count++; + }); - if (clientRect1.top > clientRect2.bottom) { - return false; - } + return count; + } - return isValidOverflow(clientRect2.top - clientRect1.bottom, clientRect1, clientRect2); - } + function normalizeTableCellSelection(rng) { + function moveEndPoint(start) { + var container, offset, childNodes, prefix = start ? 'start' : 'end'; - function isBelow(clientRect1, clientRect2) { - if (clientRect1.top > clientRect2.bottom) { - return true; - } - - if (clientRect1.bottom < clientRect2.top) { - return false; - } - - return isValidOverflow(clientRect2.bottom - clientRect1.top, clientRect1, clientRect2); - } + container = rng[prefix + 'Container']; + offset = rng[prefix + 'Offset']; - function isLeft(clientRect1, clientRect2) { - return clientRect1.left < clientRect2.left; - } + if (container.nodeType == 1 && container.nodeName == "TR") { + childNodes = container.childNodes; + container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; + if (container) { + offset = start ? 0 : container.childNodes.length; + rng['set' + (start ? 'Start' : 'End')](container, offset); + } + } + } - function isRight(clientRect1, clientRect2) { - return clientRect1.right > clientRect2.right; - } + moveEndPoint(true); + moveEndPoint(); - function compare(clientRect1, clientRect2) { - if (isAbove(clientRect1, clientRect2)) { - return -1; - } + return rng; + } - if (isBelow(clientRect1, clientRect2)) { - return 1; - } + function getLocation(rng) { + var root = dom.getRoot(), bookmark = {}; - if (isLeft(clientRect1, clientRect2)) { - return -1; - } + function getPoint(rng, start) { + var container = rng[start ? 'startContainer' : 'endContainer'], + offset = rng[start ? 'startOffset' : 'endOffset'], point = [], childNodes, after = 0; - if (isRight(clientRect1, clientRect2)) { - return 1; - } + if (container.nodeType === 3) { + point.push(normalized ? getNormalizedTextOffset(container, offset) : offset); + } else { + childNodes = container.childNodes; - return 0; - } + if (offset >= childNodes.length && childNodes.length) { + after = 1; + offset = Math.max(0, childNodes.length - 1); + } - function containsXY(clientRect, clientX, clientY) { - return ( - clientX >= clientRect.left && - clientX <= clientRect.right && - clientY >= clientRect.top && - clientY <= clientRect.bottom - ); - } + point.push(dom.nodeIndex(childNodes[offset], normalized) + after); + } - return { - clone: clone, - collapse: collapse, - isEqual: isEqual, - isAbove: isAbove, - isBelow: isBelow, - isLeft: isLeft, - isRight: isRight, - compare: compare, - containsXY: containsXY - }; - } -); + for (; container && container != root; container = container.parentNode) { + point.push(dom.nodeIndex(container, normalized)); + } -/** - * ExtendingChar.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 - */ + return point; + } -/** - * This class contains logic for detecting extending characters. - * - * @private - * @class tinymce.text.ExtendingChar - * @example - * var isExtending = ExtendingChar.isExtendingChar('a'); - */ -define( - 'tinymce.core.text.ExtendingChar', - [ - ], - function () { - // Generated from: http://www.unicode.org/Public/UNIDATA/DerivedCoreProperties.txt - // Only includes the characters in that fit into UCS-2 16 bit - var extendingChars = new RegExp( - "[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A" + - "\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0" + - "\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C" + - "\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3" + - "\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC" + - "\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B57" + - "\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56" + - "\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D01\u0D3E\u0D41-\u0D44" + - "\u0D4D\u0D57\u0D62-\u0D63\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9" + - "\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97" + - "\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074" + - "\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5" + - "\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18" + - "\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1B00-\u1B03\u1B34" + - "\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9" + - "\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9" + - "\u1DC0-\u1DF5\u1DFC-\u1DFF\u200C-\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1" + - "\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1" + - "\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC" + - "\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1" + - "\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F]" - ); + bookmark.start = getPoint(rng, true); - function isExtendingChar(ch) { - return typeof ch == "string" && ch.charCodeAt(0) >= 768 && extendingChars.test(ch); - } + if (!selection.isCollapsed()) { + bookmark.end = getPoint(rng); + } - return { - isExtendingChar: isExtendingChar - }; - } -); -/** - * CaretPosition.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 - */ + return bookmark; + } -/** - * This module contains logic for creating caret positions within a document a caretposition - * is similar to a DOMRange object but it doesn't have two endpoints and is also more lightweight - * since it's now updated live when the DOM changes. - * - * @private - * @class tinymce.caret.CaretPosition - * @example - * var caretPos1 = new CaretPosition(container, offset); - * var caretPos2 = CaretPosition.fromRangeStart(someRange); - */ -define( - 'tinymce.core.caret.CaretPosition', - [ - "tinymce.core.util.Fun", - "tinymce.core.dom.NodeType", - "tinymce.core.dom.DOMUtils", - "tinymce.core.dom.RangeUtils", - "tinymce.core.caret.CaretCandidate", - "tinymce.core.geom.ClientRect", - "tinymce.core.text.ExtendingChar" - ], - function (Fun, NodeType, DOMUtils, RangeUtils, CaretCandidate, ClientRect, ExtendingChar) { - var isElement = NodeType.isElement, - isCaretCandidate = CaretCandidate.isCaretCandidate, - isBlock = NodeType.matchStyleValues('display', 'block table'), - isFloated = NodeType.matchStyleValues('float', 'left right'), - isValidElementCaretCandidate = Fun.and(isElement, isCaretCandidate, Fun.negate(isFloated)), - isNotPre = Fun.negate(NodeType.matchStyleValues('white-space', 'pre pre-line pre-wrap')), - isText = NodeType.isText, - isBr = NodeType.isBr, - nodeIndex = DOMUtils.nodeIndex, - resolveIndex = RangeUtils.getNode; + function findAdjacentContentEditableFalseElm(rng) { + function findSibling(node, offset) { + var sibling; - function createRange(doc) { - return "createRange" in doc ? doc.createRange() : DOMUtils.DOM.createRng(); - } + if (NodeType.isElement(node)) { + node = RangeUtils.getNode(node, offset); + if (isContentEditableFalse(node)) { + return node; + } + } - function isWhiteSpace(chr) { - return chr && /[\r\n\t ]/.test(chr); - } + if (CaretContainer.isCaretContainer(node)) { + if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) { + node = node.parentNode; + } - function isHiddenWhiteSpaceRange(range) { - var container = range.startContainer, - offset = range.startOffset, - text; + sibling = node.previousSibling; + if (isContentEditableFalse(sibling)) { + return sibling; + } - if (isWhiteSpace(range.toString()) && isNotPre(container.parentNode)) { - text = container.data; + sibling = node.nextSibling; + if (isContentEditableFalse(sibling)) { + return sibling; + } + } + } - if (isWhiteSpace(text[offset - 1]) || isWhiteSpace(text[offset + 1])) { - return true; + return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset); } - } - return false; - } + if (type == 2) { + element = selection.getNode(); + name = element ? element.nodeName : null; + rng = selection.getRng(); - function getCaretPositionClientRects(caretPosition) { - var clientRects = [], beforeNode, node; + if (isContentEditableFalse(element) || name == 'IMG') { + return { name: name, index: findIndex(name, element) }; + } - // Hack for older WebKit versions that doesn't - // support getBoundingClientRect on BR elements - function getBrClientRect(brNode) { - var doc = brNode.ownerDocument, - rng = createRange(doc), - nbsp = doc.createTextNode('\u00a0'), - parentNode = brNode.parentNode, - clientRect; + element = findAdjacentContentEditableFalseElm(rng); + if (element) { + name = element.tagName; + return { name: name, index: findIndex(name, element) }; + } - parentNode.insertBefore(nbsp, brNode); - rng.setStart(nbsp, 0); - rng.setEnd(nbsp, 1); - clientRect = ClientRect.clone(rng.getBoundingClientRect()); - parentNode.removeChild(nbsp); + return getLocation(rng); + } - return clientRect; - } + if (type == 3) { + rng = selection.getRng(); - function getBoundingClientRect(item) { - var clientRect, clientRects; + return { + start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)), + end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng)) + }; + } - clientRects = item.getClientRects(); - if (clientRects.length > 0) { - clientRect = ClientRect.clone(clientRects[0]); - } else { - clientRect = ClientRect.clone(item.getBoundingClientRect()); + // Handle simple range + if (type) { + return { rng: selection.getRng() }; } - if (isBr(item) && clientRect.left === 0) { - return getBrClientRect(item); + rng = selection.getRng(); + id = dom.uniqueId(); + collapsed = selection.isCollapsed(); + styles = 'overflow:hidden;line-height:0px'; + element = selection.getNode(); + name = element.nodeName; + if (name == 'IMG') { + return { name: name, index: findIndex(name, element) }; } - return clientRect; - } + // W3C method + rng2 = normalizeTableCellSelection(rng.cloneRange()); - function collapseAndInflateWidth(clientRect, toStart) { - clientRect = ClientRect.collapse(clientRect, toStart); - clientRect.width = 1; - clientRect.right = clientRect.left + 1; + // Insert end marker + if (!collapsed) { + rng2.collapse(false); + var endBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_end', style: styles }, chr); + rng2.insertNode(endBookmarkNode); + trimEmptyTextNode(endBookmarkNode.nextSibling); + } - return clientRect; - } + rng = normalizeTableCellSelection(rng); + rng.collapse(true); + var startBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_start', style: styles }, chr); + rng.insertNode(startBookmarkNode); + trimEmptyTextNode(startBookmarkNode.previousSibling); - function addUniqueAndValidRect(clientRect) { - if (clientRect.height === 0) { - return; - } + selection.moveToBookmark({ id: id, keep: 1 }); - if (clientRects.length > 0) { - if (ClientRect.isEqual(clientRect, clientRects[clientRects.length - 1])) { - return; - } - } + return { id: id }; + }; - clientRects.push(clientRect); - } + /** + * Restores the selection to the specified bookmark. + * + * @method moveToBookmark + * @param {Object} bookmark Bookmark to restore selection from. + * @return {Boolean} true/false if it was successful or not. + * @example + * // Stores a bookmark of the current selection + * var bm = tinymce.activeEditor.selection.getBookmark(); + * + * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinymce.activeEditor.selection.moveToBookmark(bm); + */ + this.moveToBookmark = function (bookmark) { + var rng, root, startContainer, endContainer, startOffset, endOffset; - function addCharacterOffset(container, offset) { - var range = createRange(container.ownerDocument); + function setEndPoint(start) { + var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; - if (offset < container.data.length) { - if (ExtendingChar.isExtendingChar(container.data[offset])) { - return clientRects; - } + if (point) { + offset = point[0]; - // WebKit returns two client rects for a position after an extending - // character a\uxxx|b so expand on "b" and collapse to start of "b" box - if (ExtendingChar.isExtendingChar(container.data[offset - 1])) { - range.setStart(container, offset); - range.setEnd(container, offset + 1); + // Find container node + for (node = root, i = point.length - 1; i >= 1; i--) { + children = node.childNodes; - if (!isHiddenWhiteSpaceRange(range)) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false)); - return clientRects; + if (point[i] > children.length - 1) { + return; + } + + node = children[point[i]]; } - } - } - if (offset > 0) { - range.setStart(container, offset - 1); - range.setEnd(container, offset); + // Move text offset to best suitable location + if (node.nodeType === 3) { + offset = Math.min(point[0], node.nodeValue.length); + } - if (!isHiddenWhiteSpaceRange(range)) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), false)); + // Move element offset to best suitable location + if (node.nodeType === 1) { + offset = Math.min(point[0], node.childNodes.length); + } + + // Set offset within container node + if (start) { + rng.setStart(node, offset); + } else { + rng.setEnd(node, offset); + } } + + return true; } - if (offset < container.data.length) { - range.setStart(container, offset); - range.setEnd(container, offset + 1); + function restoreEndPoint(suffix) { + var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; - if (!isHiddenWhiteSpaceRange(range)) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(range), true)); - } - } - } + if (marker) { + node = marker.parentNode; - if (isText(caretPosition.container())) { - addCharacterOffset(caretPosition.container(), caretPosition.offset()); - return clientRects; - } + if (suffix == 'start') { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } - if (isElement(caretPosition.container())) { - if (caretPosition.isAtEnd()) { - node = resolveIndex(caretPosition.container(), caretPosition.offset()); - if (isText(node)) { - addCharacterOffset(node, node.data.length); - } + startContainer = endContainer = node; + startOffset = endOffset = idx; + } else { + if (!keep) { + idx = dom.nodeIndex(marker); + } else { + node = marker.firstChild; + idx = 1; + } - if (isValidElementCaretCandidate(node) && !isBr(node)) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false)); - } - } else { - node = resolveIndex(caretPosition.container(), caretPosition.offset()); - if (isText(node)) { - addCharacterOffset(node, 0); - } + endContainer = node; + endOffset = idx; + } - if (isValidElementCaretCandidate(node) && caretPosition.isAtEnd()) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), false)); - return clientRects; - } + if (!keep) { + prev = marker.previousSibling; + next = marker.nextSibling; - beforeNode = resolveIndex(caretPosition.container(), caretPosition.offset() - 1); - if (isValidElementCaretCandidate(beforeNode) && !isBr(beforeNode)) { - if (isBlock(beforeNode) || isBlock(node) || !isValidElementCaretCandidate(node)) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(beforeNode), false)); + // Remove all marker text nodes + Tools.each(Tools.grep(marker.childNodes), function (node) { + if (node.nodeType == 3) { + node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); + } + }); + + // Remove marker but keep children if for example contents where inserted into the marker + // Also remove duplicated instances of the marker for example by a + // split operation or by WebKit auto split on paste feature + while ((marker = dom.get(bookmark.id + '_' + suffix))) { + dom.remove(marker, 1); + } + + // If siblings are text nodes then merge them unless it's Opera since it some how removes the node + // and we are sniffing since adding a lot of detection code for a browser with 3% of the market + // isn't worth the effort. Sorry, Opera but it's just a fact + if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { + idx = prev.nodeValue.length; + prev.appendData(next.nodeValue); + dom.remove(next); + + if (suffix == 'start') { + startContainer = endContainer = prev; + startOffset = endOffset = idx; + } else { + endContainer = prev; + endOffset = idx; + } + } } } + } - if (isValidElementCaretCandidate(node)) { - addUniqueAndValidRect(collapseAndInflateWidth(getBoundingClientRect(node), true)); + function addBogus(node) { + // Adds a bogus BR element for empty block elements + if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { + node.innerHTML = '
    '; } + + return node; } - } - return clientRects; - } + function resolveCaretPositionBookmark() { + var rng, pos; - /** - * Represents a location within the document by a container and an offset. - * - * @constructor - * @param {Node} container Container node. - * @param {Number} offset Offset within that container node. - * @param {Array} clientRects Optional client rects array for the position. - */ - function CaretPosition(container, offset, clientRects) { - function isAtStart() { - if (isText(container)) { - return offset === 0; - } + rng = dom.createRng(); + pos = CaretBookmark.resolve(dom.getRoot(), bookmark.start); + rng.setStart(pos.container(), pos.offset()); - return offset === 0; - } + pos = CaretBookmark.resolve(dom.getRoot(), bookmark.end); + rng.setEnd(pos.container(), pos.offset()); - function isAtEnd() { - if (isText(container)) { - return offset >= container.data.length; + return rng; } - return offset >= container.childNodes.length; - } - - function toRange() { - var range; + if (bookmark) { + if (Tools.isArray(bookmark.start)) { + rng = dom.createRng(); + root = dom.getRoot(); - range = createRange(container.ownerDocument); - range.setStart(container, offset); - range.setEnd(container, offset); - - return range; - } + if (setEndPoint(true) && setEndPoint()) { + selection.setRng(rng); + } + } else if (typeof bookmark.start == 'string') { + selection.setRng(resolveCaretPositionBookmark(bookmark)); + } else if (bookmark.id) { + // Restore start/end points + restoreEndPoint('start'); + restoreEndPoint('end'); - function getClientRects() { - if (!clientRects) { - clientRects = getCaretPositionClientRects(new CaretPosition(container, offset)); + if (startContainer) { + rng = dom.createRng(); + rng.setStart(addBogus(startContainer), startOffset); + rng.setEnd(addBogus(endContainer), endOffset); + selection.setRng(rng); + } + } else if (bookmark.name) { + selection.select(dom.select(bookmark.name)[bookmark.index]); + } else if (bookmark.rng) { + selection.setRng(bookmark.rng); + } } - - return clientRects; - } - - function isVisible() { - return getClientRects().length > 0; - } - - function isEqual(caretPosition) { - return caretPosition && container === caretPosition.container() && offset === caretPosition.offset(); - } - - function getNode(before) { - return resolveIndex(container, before ? offset - 1 : offset); - } - - return { - /** - * Returns the container node. - * - * @method container - * @return {Node} Container node. - */ - container: Fun.constant(container), - - /** - * Returns the offset within the container node. - * - * @method offset - * @return {Number} Offset within the container node. - */ - offset: Fun.constant(offset), - - /** - * Returns a range out of a the caret position. - * - * @method toRange - * @return {DOMRange} range for the caret position. - */ - toRange: toRange, - - /** - * Returns the client rects for the caret position. Might be multiple rects between - * block elements. - * - * @method getClientRects - * @return {Array} Array of client rects. - */ - getClientRects: getClientRects, - - /** - * Returns true if the caret location is visible/displayed on screen. - * - * @method isVisible - * @return {Boolean} true/false if the position is visible or not. - */ - isVisible: isVisible, - - /** - * Returns true if the caret location is at the beginning of text node or container. - * - * @method isVisible - * @return {Boolean} true/false if the position is at the beginning. - */ - isAtStart: isAtStart, - - /** - * Returns true if the caret location is at the end of text node or container. - * - * @method isVisible - * @return {Boolean} true/false if the position is at the end. - */ - isAtEnd: isAtEnd, - - /** - * Compares the caret position to another caret position. This will only compare the - * container and offset not it's visual position. - * - * @method isEqual - * @param {tinymce.caret.CaretPosition} caretPosition Caret position to compare with. - * @return {Boolean} true if the caret positions are equal. - */ - isEqual: isEqual, - - /** - * Returns the closest resolved node from a node index. That means if you have an offset after the - * last node in a container it will return that last node. - * - * @method getNode - * @return {Node} Node that is closest to the index. - */ - getNode: getNode }; } /** - * Creates a caret position from the start of a range. - * - * @method fromRangeStart - * @param {DOMRange} range DOM Range to create caret position from. - * @return {tinymce.caret.CaretPosition} Caret position from the start of DOM range. - */ - CaretPosition.fromRangeStart = function (range) { - return new CaretPosition(range.startContainer, range.startOffset); - }; - - /** - * Creates a caret position from the end of a range. - * - * @method fromRangeEnd - * @param {DOMRange} range DOM Range to create caret position from. - * @return {tinymce.caret.CaretPosition} Caret position from the end of DOM range. - */ - CaretPosition.fromRangeEnd = function (range) { - return new CaretPosition(range.endContainer, range.endOffset); - }; - - /** - * Creates a caret position from a node and places the offset after it. - * - * @method after - * @param {Node} node Node to get caret position from. - * @return {tinymce.caret.CaretPosition} Caret position from the node. - */ - CaretPosition.after = function (node) { - return new CaretPosition(node.parentNode, nodeIndex(node) + 1); - }; - - /** - * Creates a caret position from a node and places the offset before it. + * Returns true/false if the specified node is a bookmark node or not. * - * @method before - * @param {Node} node Node to get caret position from. - * @return {tinymce.caret.CaretPosition} Caret position from the node. + * @static + * @method isBookmarkNode + * @param {DOMNode} node DOM Node to check if it's a bookmark node or not. + * @return {Boolean} true/false if the node is a bookmark node or not. */ - CaretPosition.before = function (node) { - return new CaretPosition(node.parentNode, nodeIndex(node)); - }; - - CaretPosition.isAtStart = function (pos) { - return pos ? pos.isAtStart() : false; - }; - - CaretPosition.isAtEnd = function (pos) { - return pos ? pos.isAtEnd() : false; - }; - - CaretPosition.isTextPosition = function (pos) { - return pos ? NodeType.isText(pos.container()) : false; + BookmarkManager.isBookmarkNode = function (node) { + return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark'; }; - return CaretPosition; + return BookmarkManager; } ); /** - * CaretBookmark.js + * CaretUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -13705,264 +13838,317 @@ define( */ /** - * This module creates or resolves xpath like string representation of a CaretPositions. - * - * The format is a / separated list of chunks with: - * [index|after|before] - * - * For example: - * p[0]/b[0]/text()[0],1 =

    a|c

    - * p[0]/img[0],before =

    |

    - * p[0]/img[0],after =

    |

    + * Utility functions shared by the caret logic. * * @private - * @static - * @class tinymce.caret.CaretBookmark - * @example - * var bookmark = CaretBookmark.create(rootElm, CaretPosition.before(rootElm.firstChild)); - * var caretPosition = CaretBookmark.resolve(bookmark); + * @class tinymce.caret.CaretUtils */ define( - 'tinymce.core.caret.CaretBookmark', + 'tinymce.core.caret.CaretUtils', [ - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.DOMUtils', - 'tinymce.core.util.Fun', - 'tinymce.core.util.Arr', - 'tinymce.core.caret.CaretPosition' + "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 (NodeType, DomUtils, Fun, Arr, CaretPosition) { - var isText = NodeType.isText, - isBogus = NodeType.isBogus, - nodeIndex = DomUtils.nodeIndex; + 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 normalizedParent(node) { - var parentNode = node.parentNode; + function isForwards(direction) { + return direction > 0; + } - if (isBogus(parentNode)) { - return normalizedParent(parentNode); + function isBackwards(direction) { + return direction < 0; + } + + function skipCaretContainers(walk, shallow) { + var node; + + while ((node = walk(shallow))) { + if (!isCaretContainerBlock(node)) { + return node; + } } - return parentNode; + return null; } - function getChildNodes(node) { - if (!node) { - return []; - } + function findNode(node, direction, predicateFn, rootNode, shallow) { + var walker = new TreeWalker(node, rootNode); - return Arr.reduce(node.childNodes, function (result, node) { - if (isBogus(node) && node.nodeName != 'BR') { - result = result.concat(getChildNodes(node)); - } else { - result.push(node); + if (isBackwards(direction)) { + if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { + node = skipCaretContainers(walker.prev, true); + if (predicateFn(node)) { + return node; + } } - return result; - }, []); - } + while ((node = skipCaretContainers(walker.prev, shallow))) { + if (predicateFn(node)) { + return node; + } + } + } - function normalizedTextOffset(textNode, offset) { - while ((textNode = textNode.previousSibling)) { - if (!isText(textNode)) { - break; + if (isForwards(direction)) { + if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { + node = skipCaretContainers(walker.next, true); + if (predicateFn(node)) { + return node; + } } - offset += textNode.data.length; + while ((node = skipCaretContainers(walker.next, shallow))) { + if (predicateFn(node)) { + return node; + } + } } - return offset; + return null; } - function equal(targetValue) { - return function (value) { - return targetValue === value; - }; - } + function getEditingHost(node, rootNode) { + for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { + if (isContentEditableTrue(node)) { + return node; + } + } - function normalizedNodeIndex(node) { - var nodes, index, numTextFragments; + return rootNode; + } - nodes = getChildNodes(normalizedParent(node)); - index = Arr.findIndex(nodes, equal(node), node); - nodes = nodes.slice(0, index + 1); - numTextFragments = Arr.reduce(nodes, function (result, node, i) { - if (isText(node) && isText(nodes[i - 1])) { - result++; + function getParentBlock(node, rootNode) { + while (node && node != rootNode) { + if (isBlockLike(node)) { + return node; } - return result; - }, 0); - - nodes = Arr.filter(nodes, NodeType.matchNodeNames(node.nodeName)); - index = Arr.findIndex(nodes, equal(node), node); + node = node.parentNode; + } - return index - numTextFragments; + return null; } - function createPathItem(node) { - var name; - - if (isText(node)) { - name = 'text()'; - } else { - name = node.nodeName.toLowerCase(); - } + function isInSameBlock(caretPosition1, caretPosition2, rootNode) { + return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode); + } - return name + '[' + normalizedNodeIndex(node) + ']'; + function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) { + return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode); } - function parentsUntil(rootNode, node, predicate) { - var parents = []; + function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) { + var container, offset; - for (node = node.parentNode; node != rootNode; node = node.parentNode) { - if (predicate && predicate(node)) { - break; - } + if (!caretPosition) { + return null; + } - parents.push(node); + container = caretPosition.container(); + offset = caretPosition.offset(); + + if (!isElement(container)) { + return null; } - return parents; + return container.childNodes[offset + relativeOffset]; } - function create(rootNode, caretPosition) { - var container, offset, path = [], - outputOffset, childNodes, parents; - - container = caretPosition.container(); - offset = caretPosition.offset(); + function beforeAfter(before, node) { + var range = node.ownerDocument.createRange(); - if (isText(container)) { - outputOffset = normalizedTextOffset(container, offset); + if (before) { + range.setStartBefore(node); + range.setEndBefore(node); } else { - childNodes = container.childNodes; - if (offset >= childNodes.length) { - outputOffset = 'after'; - offset = childNodes.length - 1; - } else { - outputOffset = 'before'; - } - - container = childNodes[offset]; + range.setStartAfter(node); + range.setEndAfter(node); } - path.push(createPathItem(container)); - parents = parentsUntil(rootNode, container); - parents = Arr.filter(parents, Fun.negate(NodeType.isBogus)); - path = path.concat(Arr.map(parents, function (node) { - return createPathItem(node); - })); + return range; + } - return path.reverse().join('/') + ',' + outputOffset; + function isNodesInSameBlock(rootNode, node1, node2) { + return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode); } - function resolvePathItem(node, name, index) { - var nodes = getChildNodes(node); + function lean(left, rootNode, node) { + var sibling, siblingName; - nodes = Arr.filter(nodes, function (node, index) { - return !isText(node) || !isText(nodes[index - 1]); - }); + if (left) { + siblingName = 'previousSibling'; + } else { + siblingName = 'nextSibling'; + } - nodes = Arr.filter(nodes, NodeType.matchNodeNames(name)); - return nodes[index]; - } + while (node && node != rootNode) { + sibling = node[siblingName]; - function findTextPosition(container, offset) { - var node = container, targetOffset = 0, dataLen; + if (isCaretContainer(sibling)) { + sibling = sibling[siblingName]; + } - while (isText(node)) { - dataLen = node.data.length; + if (isContentEditableFalse(sibling)) { + if (isNodesInSameBlock(rootNode, sibling, node)) { + return sibling; + } - if (offset >= targetOffset && offset <= targetOffset + dataLen) { - container = node; - offset = offset - targetOffset; break; } - if (!isText(node.nextSibling)) { - container = node; - offset = dataLen; + if (isCaretCandidate(sibling)) { break; } - targetOffset += dataLen; - node = node.nextSibling; - } - - if (offset > container.data.length) { - offset = container.data.length; + node = node.parentNode; } - return new CaretPosition(container, offset); + return null; } - function resolve(rootNode, path) { - var parts, container, offset; - - if (!path) { - return null; - } + var before = curry(beforeAfter, true); + var after = curry(beforeAfter, false); - parts = path.split(','); - path = parts[0].split('/'); - offset = parts.length > 1 ? parts[1] : 'before'; + function normalizeRange(direction, rootNode, range) { + var node, container, offset, location; + var leanLeft = curry(lean, true, rootNode); + var leanRight = curry(lean, false, rootNode); - container = Arr.reduce(path, function (result, value) { - value = /([\w\-\(\)]+)\[([0-9]+)\]/.exec(value); - if (!value) { - return null; - } + container = range.startContainer; + offset = range.startOffset; - if (value[1] === 'text()') { - value[1] = '#text'; + if (CaretContainer.isCaretContainerBlock(container)) { + if (!isElement(container)) { + container = container.parentNode; } - return resolvePathItem(result, value[1], parseInt(value[2], 10)); - }, rootNode); + location = container.getAttribute('data-mce-caret'); - if (!container) { - return null; - } + if (location == 'before') { + node = container.nextSibling; + if (isContentEditableFalse(node)) { + return before(node); + } + } - if (!isText(container)) { - if (offset === 'after') { - offset = nodeIndex(container) + 1; - } else { - offset = nodeIndex(container); + if (location == 'after') { + node = container.previousSibling; + if (isContentEditableFalse(node)) { + return after(node); + } } + } - return new CaretPosition(container.parentNode, offset); + if (!range.collapsed) { + return range; } - return findTextPosition(container, parseInt(offset, 10)); - } + if (NodeType.isText(container)) { + if (isCaretContainer(container)) { + if (direction === 1) { + node = leanRight(container); + if (node) { + return before(node); + } - return { - /** - * Create a xpath bookmark location for the specified caret position. - * - * @method create - * @param {Node} rootNode Root node to create bookmark within. - * @param {tinymce.caret.CaretPosition} caretPosition Caret position within the root node. - * @return {String} String xpath like location of caret position. - */ - create: create, + node = leanLeft(container); + if (node) { + return after(node); + } + } - /** - * Resolves a xpath like bookmark location to the a caret position. - * - * @method resolve - * @param {Node} rootNode Root node to resolve xpath bookmark within. - * @param {String} bookmark Bookmark string to resolve. - * @return {tinymce.caret.CaretPosition} Caret position resolved from xpath like bookmark. - */ - resolve: resolve + 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 }; } ); + /** - * BookmarkManager.js + * CaretWalker.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -13972,3193 +14158,2897 @@ define( */ /** - * This class handles selection bookmarks. + * This module contains logic for moving around a virtual caret in logical order within a DOM element. * - * @class tinymce.dom.BookmarkManager + * 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.dom.BookmarkManager', + 'tinymce.core.caret.CaretWalker', [ - 'tinymce.core.caret.CaretBookmark', - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangeUtils', - 'tinymce.core.Env', - 'tinymce.core.text.Zwsp', - 'tinymce.core.util.Tools' + "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 (CaretBookmark, CaretContainer, CaretPosition, NodeType, RangeUtils, Env, Zwsp, Tools) { - var isContentEditableFalse = NodeType.isContentEditableFalse; + 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; - var getNormalizedTextOffset = function (container, offset) { - var node, trimmedOffset; + function getParents(node, rootNode) { + var parents = []; - trimmedOffset = Zwsp.trim(container.data.slice(0, offset)).length; - for (node = container.previousSibling; node && node.nodeType === 3; node = node.previousSibling) { - trimmedOffset += Zwsp.trim(node.data).length; + while (node && node != rootNode) { + parents.push(node); + node = node.parentNode; } - return trimmedOffset; - }; + return parents; + } - var trimEmptyTextNode = function (node) { - if (NodeType.isText(node) && node.data.length === 0) { - node.parentNode.removeChild(node); + function nodeAtIndex(container, offset) { + if (container.hasChildNodes() && offset < container.childNodes.length) { + return container.childNodes[offset]; } - }; - - /** - * Constructs a new BookmarkManager instance for a specific selection instance. - * - * @constructor - * @method BookmarkManager - * @param {tinymce.dom.Selection} selection Selection instance to handle bookmarks for. - */ - function BookmarkManager(selection) { - var dom = selection.dom; - /** - * Returns a bookmark location for the current selection. This bookmark object - * can then be used to restore the selection after some content modification to the document. - * - * @method getBookmark - * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. - * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. - * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. - * @example - * // Stores a bookmark of the current selection - * var bm = tinymce.activeEditor.selection.getBookmark(); - * - * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); - * - * // Restore the selection bookmark - * tinymce.activeEditor.selection.moveToBookmark(bm); - */ - this.getBookmark = function (type, normalized) { - var rng, rng2, id, collapsed, name, element, chr = '', styles; + return null; + } - function findIndex(name, element) { - var count = 0; + function getCaretCandidatePosition(direction, node) { + if (isForwards(direction)) { + if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { + return CaretPosition.before(node); + } - Tools.each(dom.select(name), function (node) { - if (node.getAttribute('data-mce-bogus') === 'all') { - return; - } + if (isText(node)) { + return CaretPosition(node, 0); + } + } - if (node == element) { - return false; - } + if (isBackwards(direction)) { + if (isCaretCandidate(node.nextSibling) && !isText(node.nextSibling)) { + return CaretPosition.after(node); + } - count++; - }); + if (isText(node)) { + return CaretPosition(node, node.data.length); + } + } - return count; + if (isBackwards(direction)) { + if (isBr(node)) { + return CaretPosition.before(node); } - function normalizeTableCellSelection(rng) { - function moveEndPoint(start) { - var container, offset, childNodes, prefix = start ? 'start' : 'end'; + return CaretPosition.after(node); + } - container = rng[prefix + 'Container']; - offset = rng[prefix + 'Offset']; + return CaretPosition.before(node); + } - if (container.nodeType == 1 && container.nodeName == "TR") { - childNodes = container.childNodes; - container = childNodes[Math.min(start ? offset : offset - 1, childNodes.length - 1)]; - if (container) { - offset = start ? 0 : container.childNodes.length; - rng['set' + (start ? 'Start' : 'End')](container, offset); - } - } - } + // Jumps over BR elements

    |

    a

    ->


    |a

    + function isBrBeforeBlock(node, rootNode) { + var next; - moveEndPoint(true); - moveEndPoint(); + if (!NodeType.isBr(node)) { + return false; + } - return rng; + 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; + } - function getLocation(rng) { - var root = dom.getRoot(), bookmark = {}; + container = caretPosition.container(); + offset = caretPosition.offset(); - function getPoint(rng, start) { - var container = rng[start ? 'startContainer' : 'endContainer'], - offset = rng[start ? 'startOffset' : 'endOffset'], point = [], childNodes, after = 0; + if (isText(container)) { + if (isBackwards(direction) && offset > 0) { + return CaretPosition(container, --offset); + } - if (container.nodeType === 3) { - point.push(normalized ? getNormalizedTextOffset(container, offset) : offset); - } else { - childNodes = container.childNodes; + if (isForwards(direction) && offset < container.length) { + return CaretPosition(container, ++offset); + } - if (offset >= childNodes.length && childNodes.length) { - after = 1; - offset = Math.max(0, childNodes.length - 1); - } + 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); + } - point.push(dom.nodeIndex(childNodes[offset], normalized) + after); + return CaretPosition.after(innerNode); + } } - for (; container && container != root; container = container.parentNode) { - point.push(dom.nodeIndex(container, normalized)); + if (isText(nextNode)) { + return CaretPosition(nextNode, nextNode.data.length); } - return point; - } - - bookmark.start = getPoint(rng, true); - - if (!selection.isCollapsed()) { - bookmark.end = getPoint(rng); + return CaretPosition.before(nextNode); } - - return bookmark; } - function findAdjacentContentEditableFalseElm(rng) { - function findSibling(node, offset) { - var sibling; - - if (NodeType.isElement(node)) { - node = RangeUtils.getNode(node, offset); - if (isContentEditableFalse(node)) { - return node; - } + 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 (CaretContainer.isCaretContainer(node)) { - if (NodeType.isText(node) && CaretContainer.isCaretContainerBlock(node)) { - node = node.parentNode; - } + if (!isAtomic(nextNode)) { + innerNode = CaretUtils.findNode(nextNode, direction, isEditableCaretCandidate, nextNode); + if (innerNode) { + if (isText(innerNode)) { + return CaretPosition(innerNode, 0); + } - sibling = node.previousSibling; - if (isContentEditableFalse(sibling)) { - return sibling; + return CaretPosition.before(innerNode); } + } - sibling = node.nextSibling; - if (isContentEditableFalse(sibling)) { - return sibling; - } + if (isText(nextNode)) { + return CaretPosition(nextNode, 0); } - } - return findSibling(rng.startContainer, rng.startOffset) || findSibling(rng.endContainer, rng.endOffset); + return CaretPosition.after(nextNode); + } } - if (type == 2) { - element = selection.getNode(); - name = element ? element.nodeName : null; - rng = selection.getRng(); - - if (isContentEditableFalse(element) || name == 'IMG') { - return { name: name, index: findIndex(name, element) }; - } + node = caretPosition.getNode(); + } - if (selection.tridentSel) { - return selection.tridentSel.getBookmark(type); - } + 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); + } + } - element = findAdjacentContentEditableFalseElm(rng); - if (element) { - name = element.tagName; - return { name: name, index: findIndex(name, element) }; - } + nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); - return getLocation(rng); + 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); } - if (type == 3) { - rng = selection.getRng(); + return caretPosition; + } - return { - start: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeStart(rng)), - end: CaretBookmark.create(dom.getRoot(), CaretPosition.fromRangeEnd(rng)) - }; - } + if (nextNode) { + return getCaretCandidatePosition(direction, nextNode); + } - // Handle simple range - if (type) { - return { rng: selection.getRng() }; - } + return null; + } - rng = selection.getRng(); - id = dom.uniqueId(); - collapsed = selection.isCollapsed(); - styles = 'overflow:hidden;line-height:0px'; + 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); + }, - // Explorer method - if (rng.duplicate || rng.item) { - // Text selection - if (!rng.item) { - rng2 = rng.duplicate(); + /** + * 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 + */ - try { - // Insert start marker - rng.collapse(); - rng.pasteHTML('' + chr + ''); - - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - - // Detect the empty space after block elements in IE and move the - // end back one character

    ] becomes

    ]

    - rng.moveToElementText(rng2.parentElement()); - if (rng.compareEndPoints('StartToEnd', rng2) === 0) { - rng2.move('character', -1); - } +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); + }; - rng2.pasteHTML('' + chr + ''); - } - } catch (ex) { - // IE might throw unspecified error so lets ignore it - return null; - } - } else { - // Control selection - element = rng.item(0); - name = element.nodeName; + var afterElement = function (node) { + return NodeType.isBr(node) ? CaretPosition.before(node) : CaretPosition.after(node); + }; - return { name: name, index: findIndex(name, element) }; - } - } else { - element = selection.getNode(); - name = element.nodeName; - if (name == 'IMG') { - return { name: name, index: findIndex(name, element) }; - } + var isBeforeOrStart = function (position) { + if (CaretPosition.isTextPosition(position)) { + return position.offset() === 0; + } else { + return CaretCandidate.isCaretCandidate(position.getNode()); + } + }; - // W3C method - rng2 = normalizeTableCellSelection(rng.cloneRange()); + var isAfterOrEnd = function (position) { + if (CaretPosition.isTextPosition(position)) { + return position.offset() === position.container().data.length; + } else { + return CaretCandidate.isCaretCandidate(position.getNode(true)); + } + }; - // Insert end marker - if (!collapsed) { - rng2.collapse(false); - var endBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_end', style: styles }, chr); - rng2.insertNode(endBookmarkNode); - trimEmptyTextNode(endBookmarkNode.nextSibling); - } + var isBeforeAfterSameElement = function (from, to) { + return !CaretPosition.isTextPosition(from) && !CaretPosition.isTextPosition(to) && from.getNode() === to.getNode(true); + }; - rng = normalizeTableCellSelection(rng); - rng.collapse(true); - var startBookmarkNode = dom.create('span', { 'data-mce-type': "bookmark", id: id + '_start', style: styles }, chr); - rng.insertNode(startBookmarkNode); - trimEmptyTextNode(startBookmarkNode.previousSibling); - } + var isAtBr = function (position) { + return !CaretPosition.isTextPosition(position) && NodeType.isBr(position.getNode()); + }; - selection.moveToBookmark({ id: id, keep: 1 }); + 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); + } + }; - return { id: id }; - }; + // 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)); + }; - /** - * Restores the selection to the specified bookmark. - * - * @method moveToBookmark - * @param {Object} bookmark Bookmark to restore selection from. - * @return {Boolean} true/false if it was successful or not. - * @example - * // Stores a bookmark of the current selection - * var bm = tinymce.activeEditor.selection.getBookmark(); - * - * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); - * - * // Restore the selection bookmark - * tinymce.activeEditor.selection.moveToBookmark(bm); - */ - this.moveToBookmark = function (bookmark) { - var rng, root, startContainer, endContainer, startOffset, endOffset; + // 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); + } + }); + }; - function setEndPoint(start) { - var point = bookmark[start ? 'start' : 'end'], i, node, offset, children; + 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(); + } + }; - if (point) { - offset = point[0]; + 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) + }; + } +); - // Find container node - for (node = root, i = point.length - 1; i >= 1; i--) { - children = node.childNodes; +/** + * 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 + */ - if (point[i] > children.length - 1) { - return; - } +define( + 'tinymce.core.dom.RangeNormalizer', + [ + 'global!document', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.caret.CaretUtils' + ], + function (document, CaretFinder, CaretPosition, CaretUtils) { + var createRange = function (sc, so, ec, eo) { + var rng = document.createRange(); + rng.setStart(sc, so); + rng.setEnd(ec, eo); + return rng; + }; - node = children[point[i]]; - } + // 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; - // Move text offset to best suitable location - if (node.nodeType === 3) { - offset = Math.min(point[0], node.nodeValue.length); - } + 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); + }; - // Move element offset to best suitable location - if (node.nodeType === 1) { - offset = Math.min(point[0], node.childNodes.length); - } + var normalizeBlockSelection = function (rng) { + return rng.collapsed ? rng : normalizeBlockSelectionRange(rng); + }; - // Set offset within container node - if (start) { - rng.setStart(node, offset); - } else { - rng.setEnd(node, offset); - } - } + var normalize = function (rng) { + return normalizeBlockSelection(rng); + }; - return true; - } + return { + normalize: normalize + }; + } +); +define( + 'ephox.katamari.api.Type', - function restoreEndPoint(suffix) { - var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep; + [ + 'global!Array', + 'global!String' + ], - if (marker) { - node = marker.parentNode; + function (Array, String) { + var typeOf = function(x) { + if (x === null) return 'null'; + var t = typeof x; + if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array'; + if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string'; + return t; + }; - if (suffix == 'start') { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + var isType = function (type) { + return function (value) { + return typeOf(value) === type; + }; + }; - startContainer = endContainer = node; - startOffset = endOffset = idx; - } else { - if (!keep) { - idx = dom.nodeIndex(marker); - } else { - node = marker.firstChild; - idx = 1; - } + return { + isString: isType('string'), + isObject: isType('object'), + isArray: isType('array'), + isNull: isType('null'), + isBoolean: isType('boolean'), + isUndefined: isType('undefined'), + isFunction: isType('function'), + isNumber: isType('number') + }; + } +); - endContainer = node; - endOffset = idx; - } - if (!keep) { - prev = marker.previousSibling; - next = marker.nextSibling; +define( + 'ephox.katamari.data.Immutable', - // Remove all marker text nodes - Tools.each(Tools.grep(marker.childNodes), function (node) { - if (node.nodeType == 3) { - node.nodeValue = node.nodeValue.replace(/\uFEFF/g, ''); - } - }); + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'global!Array', + 'global!Error' + ], - // Remove marker but keep children if for example contents where inserted into the marker - // Also remove duplicated instances of the marker for example by a - // split operation or by WebKit auto split on paste feature - while ((marker = dom.get(bookmark.id + '_' + suffix))) { - dom.remove(marker, 1); - } + function (Arr, Fun, Array, Error) { + return function () { + var fields = arguments; + return function(/* values */) { + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var values = new Array(arguments.length); + for (var i = 0; i < values.length; i++) values[i] = arguments[i]; - // If siblings are text nodes then merge them unless it's Opera since it some how removes the node - // and we are sniffing since adding a lot of detection code for a browser with 3% of the market - // isn't worth the effort. Sorry, Opera but it's just a fact - if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3 && !Env.opera) { - idx = prev.nodeValue.length; - prev.appendData(next.nodeValue); - dom.remove(next); + if (fields.length !== values.length) + throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments'); - if (suffix == 'start') { - startContainer = endContainer = prev; - startOffset = endOffset = idx; - } else { - endContainer = prev; - endOffset = idx; - } - } - } - } - } + var struct = {}; + Arr.each(fields, function (name, i) { + struct[name] = Fun.constant(values[i]); + }); + return struct; + }; + }; + } +); - function addBogus(node) { - // Adds a bogus BR element for empty block elements - if (dom.isBlock(node) && !node.innerHTML && !Env.ie) { - node.innerHTML = '
    '; - } +define( + 'ephox.katamari.api.Obj', - return node; + [ + 'ephox.katamari.api.Option', + 'global!Object' + ], + + function (Option, Object) { + // There are many variations of Object iteration that are faster than the 'for-in' style: + // http://jsperf.com/object-keys-iteration/107 + // + // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering + var keys = (function () { + var fastKeys = Object.keys; + + // This technically means that 'each' and 'find' on IE8 iterate through the object twice. + // This code doesn't run on IE8 much, so it's an acceptable tradeoff. + // If it becomes a problem we can always duplicate the feature detection inside each and find as well. + var slowKeys = function (o) { + var r = []; + for (var i in o) { + if (o.hasOwnProperty(i)) { + r.push(i); + } } + return r; + }; - function resolveCaretPositionBookmark() { - var rng, pos; + return fastKeys === undefined ? slowKeys : fastKeys; + })(); - rng = dom.createRng(); - pos = CaretBookmark.resolve(dom.getRoot(), bookmark.start); - rng.setStart(pos.container(), pos.offset()); - pos = CaretBookmark.resolve(dom.getRoot(), bookmark.end); - rng.setEnd(pos.container(), pos.offset()); + var each = function (obj, f) { + var props = keys(obj); + for (var k = 0, len = props.length; k < len; k++) { + var i = props[k]; + var x = obj[i]; + f(x, i, obj); + } + }; - return rng; - } + /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */ + var objectMap = function (obj, f) { + return tupleMap(obj, function (x, i, obj) { + return { + k: i, + v: f(x, i, obj) + }; + }); + }; - if (bookmark) { - if (Tools.isArray(bookmark.start)) { - rng = dom.createRng(); - root = dom.getRoot(); + /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */ + var tupleMap = function (obj, f) { + var r = {}; + each(obj, function (x, i) { + var tuple = f(x, i, obj); + r[tuple.k] = tuple.v; + }); + return r; + }; - if (selection.tridentSel) { - return selection.tridentSel.moveToBookmark(bookmark); - } + /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */ + var bifilter = function (obj, pred) { + var t = {}; + var f = {}; + each(obj, function(x, i) { + var branch = pred(x, i) ? t : f; + branch[i] = x; + }); + return { + t: t, + f: f + }; + }; - if (setEndPoint(true) && setEndPoint()) { - selection.setRng(rng); - } - } else if (typeof bookmark.start == 'string') { - selection.setRng(resolveCaretPositionBookmark(bookmark)); - } else if (bookmark.id) { - // Restore start/end points - restoreEndPoint('start'); - restoreEndPoint('end'); + /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */ + var mapToArray = function (obj, f) { + var r = []; + each(obj, function(value, name) { + r.push(f(value, name)); + }); + return r; + }; - if (startContainer) { - rng = dom.createRng(); - rng.setStart(addBogus(startContainer), startOffset); - rng.setEnd(addBogus(endContainer), endOffset); - selection.setRng(rng); - } - } else if (bookmark.name) { - selection.select(dom.select(bookmark.name)[bookmark.index]); - } else if (bookmark.rng) { - selection.setRng(bookmark.rng); - } + /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */ + var find = function (obj, pred) { + var props = keys(obj); + for (var k = 0, len = props.length; k < len; k++) { + var i = props[k]; + var x = obj[i]; + if (pred(x, i, obj)) { + return Option.some(x); } - }; - } + } + return Option.none(); + }; - /** - * Returns true/false if the specified node is a bookmark node or not. - * - * @static - * @method isBookmarkNode - * @param {DOMNode} node DOM Node to check if it's a bookmark node or not. - * @return {Boolean} true/false if the node is a bookmark node or not. - */ - BookmarkManager.isBookmarkNode = function (node) { - return node && node.tagName === 'SPAN' && node.getAttribute('data-mce-type') === 'bookmark'; + /** values :: JsObj(k, v) -> [v] */ + var values = function (obj) { + return mapToArray(obj, function (v) { + return v; + }); }; - return BookmarkManager; + var size = function (obj) { + return values(obj).length; + }; + + return { + bifilter: bifilter, + each: each, + map: objectMap, + mapToArray: mapToArray, + tupleMap: tupleMap, + find: find, + keys: keys, + values: values, + size: size + }; } ); -/** - * 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', + 'ephox.katamari.util.BagUtils', + [ - "tinymce.core.util.Fun", - "tinymce.core.dom.TreeWalker", - "tinymce.core.dom.NodeType", - "tinymce.core.caret.CaretPosition", - "tinymce.core.caret.CaretContainer", - "tinymce.core.caret.CaretCandidate" + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Type', + 'global!Error' ], - 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 (Arr, Type, Error) { + var sort = function (arr) { + return arr.slice(0).sort(); + }; - function isBackwards(direction) { - return direction < 0; - } + var reqMessage = function (required, keys) { + throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.'); + }; - function skipCaretContainers(walk, shallow) { - var node; + var unsuppMessage = function (unsupported) { + throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', ')); + }; - while ((node = walk(shallow))) { - if (!isCaretContainerBlock(node)) { - return node; - } - } + var validateStrArr = function (label, array) { + if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.'); + Arr.each(array, function (a) { + if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.'); + }); + }; - return null; - } + var invalidTypeMessage = function (incorrect, type) { + throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.'); + }; - function findNode(node, direction, predicateFn, rootNode, shallow) { - var walker = new TreeWalker(node, rootNode); + var checkDupes = function (everything) { + var sorted = sort(everything); + var dupe = Arr.find(sorted, function (s, i) { + return i < sorted.length -1 && s === sorted[i + 1]; + }); - if (isBackwards(direction)) { - if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { - node = skipCaretContainers(walker.prev, true); - if (predicateFn(node)) { - return node; - } - } + dupe.each(function (d) { + throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].'); + }); + }; - while ((node = skipCaretContainers(walker.prev, shallow))) { - if (predicateFn(node)) { - return node; - } - } - } + return { + sort: sort, + reqMessage: reqMessage, + unsuppMessage: unsuppMessage, + validateStrArr: validateStrArr, + invalidTypeMessage: invalidTypeMessage, + checkDupes: checkDupes + }; + } +); +define( + 'ephox.katamari.data.MixedBag', - if (isForwards(direction)) { - if (isContentEditableFalse(node) || isCaretContainerBlock(node)) { - node = skipCaretContainers(walker.next, true); - if (predicateFn(node)) { - return node; - } - } + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.util.BagUtils', + 'global!Error', + 'global!Object' + ], - while ((node = skipCaretContainers(walker.next, shallow))) { - if (predicateFn(node)) { - return node; - } - } - } + function (Arr, Fun, Obj, Option, BagUtils, Error, Object) { + + return function (required, optional) { + var everything = required.concat(optional); + if (everything.length === 0) throw new Error('You must specify at least one required or optional field.'); - return null; - } + BagUtils.validateStrArr('required', required); + BagUtils.validateStrArr('optional', optional); - function getEditingHost(node, rootNode) { - for (node = node.parentNode; node && node != rootNode; node = node.parentNode) { - if (isContentEditableTrue(node)) { - return node; - } - } + BagUtils.checkDupes(everything); - return rootNode; - } + return function (obj) { + var keys = Obj.keys(obj); - function getParentBlock(node, rootNode) { - while (node && node != rootNode) { - if (isBlockLike(node)) { - return node; - } + // Ensure all required keys are present. + var allReqd = Arr.forall(required, function (req) { + return Arr.contains(keys, req); + }); - node = node.parentNode; - } + if (! allReqd) BagUtils.reqMessage(required, keys); - return null; - } + var unsupported = Arr.filter(keys, function (key) { + return !Arr.contains(everything, key); + }); - function isInSameBlock(caretPosition1, caretPosition2, rootNode) { - return getParentBlock(caretPosition1.container(), rootNode) == getParentBlock(caretPosition2.container(), rootNode); - } + if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); - function isInSameEditingHost(caretPosition1, caretPosition2, rootNode) { - return getEditingHost(caretPosition1.container(), rootNode) == getEditingHost(caretPosition2.container(), rootNode); - } + var r = {}; + Arr.each(required, function (req) { + r[req] = Fun.constant(obj[req]); + }); - function getChildNodeAtRelativeOffset(relativeOffset, caretPosition) { - var container, offset; + Arr.each(optional, function (opt) { + r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none()); + }); - if (!caretPosition) { - return null; - } + return r; + }; + }; + } +); +define( + 'ephox.katamari.api.Struct', - container = caretPosition.container(); - offset = caretPosition.offset(); + [ + 'ephox.katamari.data.Immutable', + 'ephox.katamari.data.MixedBag' + ], - if (!isElement(container)) { - return null; - } + function (Immutable, MixedBag) { + return { + immutable: Immutable, + immutableBag: MixedBag + }; + } +); - return container.childNodes[offset + relativeOffset]; - } +define( + 'ephox.sugar.alien.Recurse', - 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 () { + /** + * Applies f repeatedly until it completes (by returning Option.none()). + * + * Normally would just use recursion, but JavaScript lacks tail call optimisation. + * + * This is what recursion looks like when manually unravelled :) + */ + var toArray = function (target, f) { + var r = []; - function isNodesInSameBlock(rootNode, node1, node2) { - return getParentBlock(node1, rootNode) == getParentBlock(node2, rootNode); - } + var recurse = function (e) { + r.push(e); + return f(e); + }; - function lean(left, rootNode, node) { - var sibling, siblingName; + var cur = f(target); + do { + cur = cur.bind(recurse); + } while (cur.isSome()); - if (left) { - siblingName = 'previousSibling'; - } else { - siblingName = 'nextSibling'; - } + return r; + }; - while (node && node != rootNode) { - sibling = node[siblingName]; + return { + toArray: toArray + }; + } +); +define( + 'ephox.sand.api.Node', - if (isCaretContainer(sibling)) { - sibling = sibling[siblingName]; - } + [ + 'ephox.sand.util.Global' + ], - if (isContentEditableFalse(sibling)) { - if (isNodesInSameBlock(rootNode, sibling, node)) { - return sibling; - } + function (Global) { + /* + * MDN says (yes) for IE, but it's undefined on IE8 + */ + var node = function () { + var f = Global.getOrDie('Node'); + return f; + }; - break; - } + /* + * Most of numerosity doesn't alter the methods on the object. + * We're making an exception for Node, because bitwise and is so easy to get wrong. + * + * Might be nice to ADT this at some point instead of having individual methods. + */ - if (isCaretCandidate(sibling)) { - break; - } + var compareDocumentPosition = function (a, b, match) { + // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions + // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation + // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition + return (a.compareDocumentPosition(b) & match) !== 0; + }; - node = node.parentNode; - } + var documentPositionPreceding = function (a, b) { + return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING); + }; - return null; - } + var documentPositionContainedBy = function (a, b) { + return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY); + }; - var before = curry(beforeAfter, true); - var after = curry(beforeAfter, false); + return { + documentPositionPreceding: documentPositionPreceding, + documentPositionContainedBy: documentPositionContainedBy + }; + } +); +define( + 'ephox.katamari.api.Thunk', - 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; + function () { - if (CaretContainer.isCaretContainerBlock(container)) { - if (!isElement(container)) { - container = container.parentNode; + var cached = function (f) { + var called = false; + var r; + return function() { + if (!called) { + called = true; + r = f.apply(null, arguments); } + return r; + }; + }; - location = container.getAttribute('data-mce-caret'); + return { + cached: cached + }; + } +); - if (location == 'before') { - node = container.nextSibling; - if (isContentEditableFalse(node)) { - return before(node); - } - } +defineGlobal("global!Number", Number); +define( + 'ephox.sand.detect.Version', - if (location == 'after') { - node = container.previousSibling; - if (isContentEditableFalse(node)) { - return after(node); - } - } - } + [ + 'ephox.katamari.api.Arr', + 'global!Number', + 'global!String' + ], - if (!range.collapsed) { - return range; + function (Arr, Number, String) { + var firstMatch = function (regexes, s) { + for (var i = 0; i < regexes.length; i++) { + var x = regexes[i]; + if (x.test(s)) return x; } + return undefined; + }; - 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); - } + var find = function (regexes, agent) { + var r = firstMatch(regexes, agent); + if (!r) return { major : 0, minor : 0 }; + var group = function(i) { + return Number(agent.replace(r, '$' + i)); + }; + return nu(group(1), group(2)); + }; - node = leanRight(container); - if (node) { - return before(node); - } - } + var detect = function (versionRegexes, agent) { + var cleanedAgent = String(agent).toLowerCase(); - return range; - } + if (versionRegexes.length === 0) return unknown(); + return find(versionRegexes, cleanedAgent); + }; - if (CaretContainer.endsWithCaretContainer(container) && offset >= container.data.length - 1) { - if (direction === 1) { - node = leanRight(container); - if (node) { - return before(node); - } - } + var unknown = function () { + return nu(0, 0); + }; - return range; - } + var nu = function (major, minor) { + return { major: major, minor: minor }; + }; - if (CaretContainer.startsWithCaretContainer(container) && offset <= 1) { - if (direction === -1) { - node = leanLeft(container); - if (node) { - return after(node); - } - } + return { + nu: nu, + detect: detect, + unknown: unknown + }; + } +); +define( + 'ephox.sand.core.Browser', - return range; - } + [ + 'ephox.katamari.api.Fun', + 'ephox.sand.detect.Version' + ], - if (offset === container.data.length) { - node = leanRight(container); - if (node) { - return before(node); - } + function (Fun, Version) { + var edge = 'Edge'; + var chrome = 'Chrome'; + var ie = 'IE'; + var opera = 'Opera'; + var firefox = 'Firefox'; + var safari = 'Safari'; - return range; - } + var isBrowser = function (name, current) { + return function () { + return current === name; + }; + }; - if (offset === 0) { - node = leanLeft(container); - if (node) { - return after(node); - } + var unknown = function () { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; - return range; - } - } + var nu = function (info) { + var current = info.current; + var version = info.version; - return range; - } + return { + current: current, + version: version, - function isNextToContentEditableFalse(relativeOffset, caretPosition) { - return isContentEditableFalse(getChildNodeAtRelativeOffset(relativeOffset, caretPosition)); - } + // INVESTIGATE: Rename to Edge ? + isEdge: isBrowser(edge, current), + isChrome: isBrowser(chrome, current), + // NOTE: isIe just looks too weird + isIE: isBrowser(ie, current), + isOpera: isBrowser(opera, current), + isFirefox: isBrowser(firefox, current), + isSafari: isBrowser(safari, current) + }; + }; 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 + unknown: unknown, + nu: nu, + edge: Fun.constant(edge), + chrome: Fun.constant(chrome), + ie: Fun.constant(ie), + opera: Fun.constant(opera), + firefox: Fun.constant(firefox), + safari: Fun.constant(safari) }; } ); - -/** - * 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', + 'ephox.sand.core.OperatingSystem', + [ - "tinymce.core.dom.NodeType", - "tinymce.core.caret.CaretCandidate", - "tinymce.core.caret.CaretPosition", - "tinymce.core.caret.CaretUtils", - "tinymce.core.util.Arr", - "tinymce.core.util.Fun" + 'ephox.katamari.api.Fun', + 'ephox.sand.detect.Version' ], - 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; - } + function (Fun, Version) { + var windows = 'Windows'; + var ios = 'iOS'; + var android = 'Android'; + var linux = 'Linux'; + var osx = 'OSX'; + var solaris = 'Solaris'; + var freebsd = 'FreeBSD'; - return parents; - } + // Though there is a bit of dupe with this and Browser, trying to + // reuse code makes it much harder to follow and change. + var isOS = function (name, current) { + return function () { + return current === name; + }; + }; - function nodeAtIndex(container, offset) { - if (container.hasChildNodes() && offset < container.childNodes.length) { - return container.childNodes[offset]; - } + var unknown = function () { + return nu({ + current: undefined, + version: Version.unknown() + }); + }; - return null; - } + var nu = function (info) { + var current = info.current; + var version = info.version; - function getCaretCandidatePosition(direction, node) { - if (isForwards(direction)) { - if (isCaretCandidate(node.previousSibling) && !isText(node.previousSibling)) { - return CaretPosition.before(node); - } + return { + current: current, + version: version, - 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); - } - } + isWindows: isOS(windows, current), + // TODO: Fix capitalisation + isiOS: isOS(ios, current), + isAndroid: isOS(android, current), + isOSX: isOS(osx, current), + isLinux: isOS(linux, current), + isSolaris: isOS(solaris, current), + isFreeBSD: isOS(freebsd, current) + }; + }; - nextNode = CaretUtils.findNode(node, direction, isEditableCaretCandidate, rootNode); + return { + unknown: unknown, + nu: nu, - 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); - } + windows: Fun.constant(windows), + ios: Fun.constant(ios), + android: Fun.constant(android), + linux: Fun.constant(linux), + osx: Fun.constant(osx), + solaris: Fun.constant(solaris), + freebsd: Fun.constant(freebsd) + }; + } +); +define( + 'ephox.sand.detect.DeviceType', - return caretPosition; - } + [ + 'ephox.katamari.api.Fun' + ], - if (nextNode) { - return getCaretCandidatePosition(direction, nextNode); - } + function (Fun) { + return function (os, browser, userAgent) { + var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; + var isiPhone = os.isiOS() && !isiPad; + var isAndroid3 = os.isAndroid() && os.version.major === 3; + var isAndroid4 = os.isAndroid() && os.version.major === 4; + var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true ); + var isTouch = os.isiOS() || os.isAndroid(); + var isPhone = isTouch && !isTablet; - return null; - } + var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; - 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); - } + isiPad : Fun.constant(isiPad), + isiPhone: Fun.constant(isiPhone), + isTablet: Fun.constant(isTablet), + isPhone: Fun.constant(isPhone), + isTouch: Fun.constant(isTouch), + isAndroid: os.isAndroid, + isiOS: os.isiOS, + isWebView: Fun.constant(iOSwebview) }; }; } ); -/** - * 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.sand.detect.UaString', + [ - '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' + 'ephox.katamari.api.Arr', + 'ephox.sand.detect.Version', + 'global!String' ], - 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)); + function (Arr, Version, String) { + var detect = function (candidates, userAgent) { + var agent = String(userAgent).toLowerCase(); + return Arr.find(candidates, function (candidate) { + return candidate.search(agent); + }); }; - // 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); - } + // They (browser and os) are the same at the moment, but they might + // not stay that way. + var detectBrowser = function (browsers, userAgent) { + return detect(browsers, userAgent).map(function (browser) { + var version = Version.detect(browser.versionRegexes, userAgent); + return { + current: browser.name, + version: version + }; }); }; - 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(); - } + var detectOs = function (oses, userAgent) { + return detect(oses, userAgent).map(function (os) { + var version = Version.detect(os.versionRegexes, userAgent); + return { + current: os.name, + version: version + }; + }); }; 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) + detectBrowser: detectBrowser, + detectOs: detectOs }; } ); - -/** - * 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', + 'ephox.katamari.str.StrAppend', + [ - '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); + function () { + var addToStart = function (str, prefix) { + return prefix + str; }; - var normalizeBlockSelection = function (rng) { - return rng.collapsed ? rng : normalizeBlockSelectionRange(rng); + var addToEnd = function (str, suffix) { + return str + suffix; }; - var normalize = function (rng) { - return normalizeBlockSelection(rng); + var removeFromStart = function (str, numChars) { + return str.substring(numChars); }; + var removeFromEnd = function (str, numChars) { + return str.substring(0, str.length - numChars); + }; + return { - normalize: normalize + addToStart: addToStart, + addToEnd: addToEnd, + removeFromStart: removeFromStart, + removeFromEnd: removeFromEnd }; } ); -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.str.StringParts', [ - 'ephox.katamari.api.Fun', - 'global!Error', - 'global!console', - 'global!document' + 'ephox.katamari.api.Option', + 'global!Error' ], - 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]); + function (Option, Error) { + /** Return the first 'count' letters from 'str'. +- * e.g. first("abcde", 2) === "ab" +- */ + var first = function(str, count) { + return str.substr(0, count); }; - var fromTag = function (tag, scope) { - var doc = scope || document; - var node = doc.createElement(tag); - return fromDom(node); + /** Return the last 'count' letters from 'str'. + * e.g. last("abcde", 2) === "de" + */ + var last = function(str, count) { + return str.substr(str.length - count, str.length); }; - var fromText = function (text, scope) { - var doc = scope || document; - var node = doc.createTextNode(text); - return fromDom(node); + var head = function(str) { + return str === '' ? Option.none() : Option.some(str.substr(0, 1)); }; - var fromDom = function (node) { - if (node === null || node === undefined) throw new Error('Node cannot be null or undefined'); - return { - dom: Fun.constant(node) - }; + var tail = function(str) { + return str === '' ? Option.none() : Option.some(str.substring(1)); }; return { - fromHtml: fromHtml, - fromTag: fromTag, - fromText: fromText, - fromDom: fromDom + first: first, + last: last, + head: head, + tail: tail }; } ); - define( - 'ephox.katamari.api.Type', + 'ephox.katamari.api.Strings', [ - 'global!Array', - 'global!String' + 'ephox.katamari.str.StrAppend', + 'ephox.katamari.str.StringParts', + 'global!Error' ], - function (Array, String) { - var typeOf = function(x) { - if (x === null) return 'null'; - var t = typeof x; - if (t === 'object' && Array.prototype.isPrototypeOf(x)) return 'array'; - if (t === 'object' && String.prototype.isPrototypeOf(x)) return 'string'; - return t; + function (StrAppend, StringParts, Error) { + var checkRange = function(str, substr, start) { + if (substr === '') return true; + if (str.length < substr.length) return false; + var x = str.substr(start, start + substr.length); + return x === substr; }; - var isType = function (type) { - return function (value) { - return typeOf(value) === type; + /** Given a string and object, perform template-replacements on the string, as specified by the object. + * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"] + * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format. + */ + var supplant = function(str, obj) { + var isStringOrNumber = function(a) { + var t = typeof a; + return t === 'string' || t === 'number'; }; + + return str.replace(/\${([^{}]*)}/g, + function (a, b) { + var value = obj[b]; + return isStringOrNumber(value) ? value : a; + } + ); }; - return { - isString: isType('string'), - isObject: isType('object'), - isArray: isType('array'), - isNull: isType('null'), - isBoolean: isType('boolean'), - isUndefined: isType('undefined'), - isFunction: isType('function'), - isNumber: isType('number') + var removeLeading = function (str, prefix) { + return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str; }; - } -); + var removeTrailing = function (str, prefix) { + return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str; + }; -define( - 'ephox.katamari.data.Immutable', - - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'global!Array', - 'global!Error' - ], - - function (Arr, Fun, Array, Error) { - return function () { - var fields = arguments; - return function(/* values */) { - // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome - var values = new Array(arguments.length); - for (var i = 0; i < values.length; i++) values[i] = arguments[i]; - - if (fields.length !== values.length) - throw new Error('Wrong number of arguments to struct. Expected "[' + fields.length + ']", got ' + values.length + ' arguments'); - - var struct = {}; - Arr.each(fields, function (name, i) { - struct[name] = Fun.constant(values[i]); - }); - return struct; - }; + var ensureLeading = function (str, prefix) { + return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix); }; - } -); - -define( - 'ephox.katamari.api.Obj', - - [ - 'ephox.katamari.api.Option', - 'global!Object' - ], - - function (Option, Object) { - // There are many variations of Object iteration that are faster than the 'for-in' style: - // http://jsperf.com/object-keys-iteration/107 - // - // Use the native keys if it is available (IE9+), otherwise fall back to manually filtering - var keys = (function () { - var fastKeys = Object.keys; - - // This technically means that 'each' and 'find' on IE8 iterate through the object twice. - // This code doesn't run on IE8 much, so it's an acceptable tradeoff. - // If it becomes a problem we can always duplicate the feature detection inside each and find as well. - var slowKeys = function (o) { - var r = []; - for (var i in o) { - if (o.hasOwnProperty(i)) { - r.push(i); - } - } - return r; - }; - - return fastKeys === undefined ? slowKeys : fastKeys; - })(); - - var each = function (obj, f) { - var props = keys(obj); - for (var k = 0, len = props.length; k < len; k++) { - var i = props[k]; - var x = obj[i]; - f(x, i, obj); - } + var ensureTrailing = function (str, prefix) { + return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix); }; - - /** objectMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> x)) -> JsObj(k, x) */ - var objectMap = function (obj, f) { - return tupleMap(obj, function (x, i, obj) { - return { - k: i, - v: f(x, i, obj) - }; - }); + + var contains = function(str, substr) { + return str.indexOf(substr) !== -1; }; - /** tupleMap :: (JsObj(k, v), (v, k, JsObj(k, v) -> { k: x, v: y })) -> JsObj(x, y) */ - var tupleMap = function (obj, f) { - var r = {}; - each(obj, function (x, i) { - var tuple = f(x, i, obj); - r[tuple.k] = tuple.v; - }); - return r; + var capitalize = function(str) { + return StringParts.head(str).bind(function (head) { + return StringParts.tail(str).map(function (tail) { + return head.toUpperCase() + tail; + }); + }).getOr(str); }; - /** bifilter :: (JsObj(k, v), (v, k -> Bool)) -> { t: JsObj(k, v), f: JsObj(k, v) } */ - var bifilter = function (obj, pred) { - var t = {}; - var f = {}; - each(obj, function(x, i) { - var branch = pred(x, i) ? t : f; - branch[i] = x; - }); - return { - t: t, - f: f - }; + /** Does 'str' start with 'prefix'? + * Note: all strings start with the empty string. + * More formally, for all strings x, startsWith(x, ""). + * This is so that for all strings x and y, startsWith(y + x, y) + */ + var startsWith = function(str, prefix) { + return checkRange(str, prefix, 0); }; - /** mapToArray :: (JsObj(k, v), (v, k -> a)) -> [a] */ - var mapToArray = function (obj, f) { - var r = []; - each(obj, function(value, name) { - r.push(f(value, name)); - }); - return r; + /** Does 'str' end with 'suffix'? + * Note: all strings end with the empty string. + * More formally, for all strings x, endsWith(x, ""). + * This is so that for all strings x and y, endsWith(x + y, y) + */ + var endsWith = function(str, suffix) { + return checkRange(str, suffix, str.length - suffix.length); }; - /** find :: (JsObj(k, v), (v, k, JsObj(k, v) -> Bool)) -> Option v */ - var find = function (obj, pred) { - var props = keys(obj); - for (var k = 0, len = props.length; k < len; k++) { - var i = props[k]; - var x = obj[i]; - if (pred(x, i, obj)) { - return Option.some(x); - } - } - return Option.none(); + + /** removes all leading and trailing spaces */ + var trim = function(str) { + return str.replace(/^\s+|\s+$/g, ''); }; - /** values :: JsObj(k, v) -> [v] */ - var values = function (obj) { - return mapToArray(obj, function (v) { - return v; - }); + var lTrim = function(str) { + return str.replace(/^\s+/g, ''); }; - var size = function (obj) { - return values(obj).length; + var rTrim = function(str) { + return str.replace(/\s+$/g, ''); }; return { - bifilter: bifilter, - each: each, - map: objectMap, - mapToArray: mapToArray, - tupleMap: tupleMap, - find: find, - keys: keys, - values: values, - size: size + supplant: supplant, + startsWith: startsWith, + removeLeading: removeLeading, + removeTrailing: removeTrailing, + ensureLeading: ensureLeading, + ensureTrailing: ensureTrailing, + endsWith: endsWith, + contains: contains, + trim: trim, + lTrim: lTrim, + rTrim: rTrim, + capitalize: capitalize }; } ); + define( - 'ephox.katamari.util.BagUtils', + 'ephox.sand.info.PlatformInfo', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Type', - 'global!Error' + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Strings' ], - function (Arr, Type, Error) { - var sort = function (arr) { - return arr.slice(0).sort(); - }; - - var reqMessage = function (required, keys) { - throw new Error('All required keys (' + sort(required).join(', ') + ') were not specified. Specified keys were: ' + sort(keys).join(', ') + '.'); - }; - - var unsuppMessage = function (unsupported) { - throw new Error('Unsupported keys for object: ' + sort(unsupported).join(', ')); - }; - - var validateStrArr = function (label, array) { - if (!Type.isArray(array)) throw new Error('The ' + label + ' fields must be an array. Was: ' + array + '.'); - Arr.each(array, function (a) { - if (!Type.isString(a)) throw new Error('The value ' + a + ' in the ' + label + ' fields was not a string.'); - }); - }; + function (Fun, Strings) { + var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; - var invalidTypeMessage = function (incorrect, type) { - throw new Error('All values need to be of type: ' + type + '. Keys (' + sort(incorrect).join(', ') + ') were not.'); + var checkContains = function (target) { + return function (uastring) { + return Strings.contains(uastring, target); + }; }; - var checkDupes = function (everything) { - var sorted = sort(everything); - var dupe = Arr.find(sorted, function (s, i) { - return i < sorted.length -1 && s === sorted[i + 1]; - }); + var browsers = [ + { + name : 'Edge', + versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], + search: function (uastring) { + var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit'); + return monstrosity; + } + }, + { + name : 'Chrome', + versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], + search : function (uastring) { + return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe'); + } + }, + { + name : 'IE', + versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], + search: function (uastring) { + return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident'); + } + }, + // INVESTIGATE: Is this still the Opera user agent? + { + name : 'Opera', + versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], + search : checkContains('opera') + }, + { + name : 'Firefox', + versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], + search : checkContains('firefox') + }, + { + name : 'Safari', + versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], + search : function (uastring) { + return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit'); + } + } + ]; - dupe.each(function (d) { - throw new Error('The field: ' + d + ' occurs more than once in the combined fields: [' + sorted.join(', ') + '].'); - }); - }; + var oses = [ + { + name : 'Windows', + search : checkContains('win'), + versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name : 'iOS', + search : function (uastring) { + return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad'); + }, + versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] + }, + { + name : 'Android', + search : checkContains('android'), + versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] + }, + { + name : 'OSX', + search : checkContains('os x'), + versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/] + }, + { + name : 'Linux', + search : checkContains('linux'), + versionRegexes: [ ] + }, + { name : 'Solaris', + search : checkContains('sunos'), + versionRegexes: [ ] + }, + { + name : 'FreeBSD', + search : checkContains('freebsd'), + versionRegexes: [ ] + } + ]; return { - sort: sort, - reqMessage: reqMessage, - unsuppMessage: unsuppMessage, - validateStrArr: validateStrArr, - invalidTypeMessage: invalidTypeMessage, - checkDupes: checkDupes + browsers: Fun.constant(browsers), + oses: Fun.constant(oses) }; } ); define( - 'ephox.katamari.data.MixedBag', + 'ephox.sand.core.PlatformDetection', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Obj', - 'ephox.katamari.api.Option', - 'ephox.katamari.util.BagUtils', - 'global!Error', - 'global!Object' + 'ephox.sand.core.Browser', + 'ephox.sand.core.OperatingSystem', + 'ephox.sand.detect.DeviceType', + 'ephox.sand.detect.UaString', + 'ephox.sand.info.PlatformInfo' ], - function (Arr, Fun, Obj, Option, BagUtils, Error, Object) { - - return function (required, optional) { - var everything = required.concat(optional); - if (everything.length === 0) throw new Error('You must specify at least one required or optional field.'); - - BagUtils.validateStrArr('required', required); - BagUtils.validateStrArr('optional', optional); - - BagUtils.checkDupes(everything); - - return function (obj) { - var keys = Obj.keys(obj); - - // Ensure all required keys are present. - var allReqd = Arr.forall(required, function (req) { - return Arr.contains(keys, req); - }); - - if (! allReqd) BagUtils.reqMessage(required, keys); - - var unsupported = Arr.filter(keys, function (key) { - return !Arr.contains(everything, key); - }); - - if (unsupported.length > 0) BagUtils.unsuppMessage(unsupported); - - var r = {}; - Arr.each(required, function (req) { - r[req] = Fun.constant(obj[req]); - }); + function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) { + var detect = function (userAgent) { + var browsers = PlatformInfo.browsers(); + var oses = PlatformInfo.oses(); - Arr.each(optional, function (opt) { - r[opt] = Fun.constant(Object.prototype.hasOwnProperty.call(obj, opt) ? Option.some(obj[opt]): Option.none()); - }); + var browser = UaString.detectBrowser(browsers, userAgent).fold( + Browser.unknown, + Browser.nu + ); + var os = UaString.detectOs(oses, userAgent).fold( + OperatingSystem.unknown, + OperatingSystem.nu + ); + var deviceType = DeviceType(os, browser, userAgent); - return r; + return { + browser: browser, + os: os, + deviceType: deviceType }; }; + + return { + detect: detect + }; } ); define( - 'ephox.katamari.api.Struct', + 'ephox.sand.api.PlatformDetection', [ - 'ephox.katamari.data.Immutable', - 'ephox.katamari.data.MixedBag' + 'ephox.katamari.api.Thunk', + 'ephox.sand.core.PlatformDetection', + 'global!navigator' ], - function (Immutable, MixedBag) { + function (Thunk, PlatformDetection, navigator) { + var detect = Thunk.cached(function () { + var userAgent = navigator.userAgent; + return PlatformDetection.detect(userAgent); + }); + return { - immutable: Immutable, - immutableBag: MixedBag + detect: detect }; } ); - define( - 'ephox.sugar.alien.Recurse', + 'ephox.sugar.api.node.NodeTypes', [ ], function () { - /** - * Applies f repeatedly until it completes (by returning Option.none()). - * - * Normally would just use recursion, but JavaScript lacks tail call optimisation. - * - * This is what recursion looks like when manually unravelled :) - */ - var toArray = function (target, f) { - var r = []; - - var recurse = function (e) { - r.push(e); - return f(e); - }; - - var cur = f(target); - do { - cur = cur.bind(recurse); - } while (cur.isSome()); - - return r; - }; - return { - toArray: toArray + ATTRIBUTE: 2, + CDATA_SECTION: 4, + COMMENT: 8, + DOCUMENT: 9, + DOCUMENT_TYPE: 10, + DOCUMENT_FRAGMENT: 11, + ELEMENT: 1, + TEXT: 3, + PROCESSING_INSTRUCTION: 7, + ENTITY_REFERENCE: 5, + ENTITY: 6, + NOTATION: 12 }; } ); define( - 'ephox.katamari.api.Global', + 'ephox.sugar.api.search.Selectors', [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.NodeTypes', + 'global!Error', + 'global!document' ], - function () { - // Use window object as the global if it's available since CSP will block script evals - if (typeof window !== 'undefined') { - return window; - } else { - return Function('return this;')(); - } - } -); + function (Arr, Option, Element, NodeTypes, Error, document) { + /* + * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons, + * instead of doing object lookup feature detection on every call + */ + var STANDARD = 0; + var MSSTANDARD = 1; + var WEBKITSTANDARD = 2; + var FIREFOXSTANDARD = 3; + var selectorType = (function () { + var test = document.createElement('span'); + // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. + // Still check for the others, but do it last. + return test.matches !== undefined ? STANDARD : + test.msMatchesSelector !== undefined ? MSSTANDARD : + test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD : + test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD : + -1; + })(); -define( - 'ephox.katamari.api.Resolve', - [ - 'ephox.katamari.api.Global' - ], + var ELEMENT = NodeTypes.ELEMENT; + var DOCUMENT = NodeTypes.DOCUMENT; - function (Global) { - /** path :: ([String], JsObj?) -> JsObj */ - var path = function (parts, scope) { - var o = scope !== undefined ? scope : Global; - for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i) - o = o[parts[i]]; - return o; - }; + var is = function (element, selector) { + var elem = element.dom(); + if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches - /** resolve :: (String, JsObj?) -> JsObj */ - var resolve = function (p, scope) { - var parts = p.split('.'); - return path(parts, scope); + // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. + // Still check for the others, but do it last. + else if (selectorType === STANDARD) return elem.matches(selector); + else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector); + else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector); + else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector); + else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :( }; - /** step :: (JsObj, String) -> JsObj */ - var step = function (o, part) { - if (o[part] === undefined || o[part] === null) - o[part] = {}; - return o[part]; + var bypassSelector = function (dom) { + // Only elements and documents support querySelector + return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT || + // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ + dom.childElementCount === 0; }; - /** forge :: ([String], JsObj?) -> JsObj */ - var forge = function (parts, target) { - var o = target !== undefined ? target : Global; - for (var i = 0; i < parts.length; ++i) - o = step(o, parts[i]); - return o; + var all = function (selector, scope) { + var base = scope === undefined ? document : scope.dom(); + return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom); }; - /** namespace :: (String, JsObj?) -> JsObj */ - var namespace = function (name, target) { - var parts = name.split('.'); - return forge(parts, target); + var one = function (selector, scope) { + var base = scope === undefined ? document : scope.dom(); + return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom); }; return { - path: path, - resolve: resolve, - forge: forge, - namespace: namespace + all: all, + is: is, + one: one }; } ); - define( - 'ephox.sand.util.Global', + 'ephox.sugar.api.dom.Compare', [ - 'ephox.katamari.api.Resolve' + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sand.api.Node', + 'ephox.sand.api.PlatformDetection', + 'ephox.sugar.api.search.Selectors' ], - function (Resolve) { - var unsafe = function (name, scope) { - return Resolve.resolve(name, scope); - }; - - var getOrDie = function (name, scope) { - var actual = unsafe(name, scope); + function (Arr, Fun, Node, PlatformDetection, Selectors) { - if (actual === undefined) throw name + ' not available on this browser'; - return actual; + var eq = function (e1, e2) { + return e1.dom() === e2.dom(); }; - return { - getOrDie: getOrDie + var isEqualNode = function (e1, e2) { + return e1.dom().isEqualNode(e2.dom()); }; - } -); -define( - 'ephox.sand.api.Node', - - [ - 'ephox.sand.util.Global' - ], - function (Global) { - /* - * MDN says (yes) for IE, but it's undefined on IE8 - */ - var node = function () { - var f = Global.getOrDie('Node'); - return f; + var member = function (element, elements) { + return Arr.exists(elements, Fun.curry(eq, element)); }; - /* - * Most of numerosity doesn't alter the methods on the object. - * We're making an exception for Node, because bitwise and is so easy to get wrong. - * - * Might be nice to ADT this at some point instead of having individual methods. - */ - - var compareDocumentPosition = function (a, b, match) { - // Returns: 0 if e1 and e2 are the same node, or a bitmask comparing the positions - // of nodes e1 and e2 in their documents. See the URL below for bitmask interpretation - // https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition - return (a.compareDocumentPosition(b) & match) !== 0; + // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself). + var regularContains = function (e1, e2) { + var d1 = e1.dom(), d2 = e2.dom(); + return d1 === d2 ? false : d1.contains(d2); }; - var documentPositionPreceding = function (a, b) { - return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_PRECEDING); + var ieContains = function (e1, e2) { + // IE only implements the contains() method for Element nodes. + // It fails for Text nodes, so implement it using compareDocumentPosition() + // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect + // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1': + // Also, compareDocumentPosition defines a node containing itself as false. + return Node.documentPositionContainedBy(e1.dom(), e2.dom()); }; - var documentPositionContainedBy = function (a, b) { - return compareDocumentPosition(a, b, node().DOCUMENT_POSITION_CONTAINED_BY); - }; + var browser = PlatformDetection.detect().browser; + + // Returns: true if node e1 contains e2, otherwise false. + // (returns false if e1===e2: A node does not contain itself). + var contains = browser.isIE() ? ieContains : regularContains; return { - documentPositionPreceding: documentPositionPreceding, - documentPositionContainedBy: documentPositionContainedBy + eq: eq, + isEqualNode: isEqualNode, + member: member, + contains: contains, + + // Only used by DomUniverse. Remove (or should Selectors.is move here?) + is: Selectors.is }; } ); + define( - 'ephox.katamari.api.Thunk', + 'ephox.sugar.api.search.Traverse', [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Struct', + 'ephox.sugar.alien.Recurse', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element' ], - function () { + function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) { + // The document associated with the current element + var owner = function (element) { + return Element.fromDom(element.dom().ownerDocument); + }; - var cached = function (f) { - var called = false; - var r; - return function() { - if (!called) { - called = true; - r = f.apply(null, arguments); - } - return r; - }; + var documentElement = function (element) { + // TODO: Avoid unnecessary wrap/unwrap here + var doc = owner(element); + return Element.fromDom(doc.dom().documentElement); }; - return { - cached: cached + // The window element associated with the element + var defaultView = function (element) { + var el = element.dom(); + var defaultView = el.ownerDocument.defaultView; + return Element.fromDom(defaultView); }; - } -); -defineGlobal("global!Number", Number); -define( - 'ephox.sand.detect.Version', + var parent = function (element) { + var dom = element.dom(); + return Option.from(dom.parentNode).map(Element.fromDom); + }; - [ - 'ephox.katamari.api.Arr', - 'global!Number', - 'global!String' - ], + var findIndex = function (element) { + return parent(element).bind(function (p) { + // TODO: Refactor out children so we can avoid the constant unwrapping + var kin = children(p); + return Arr.findIndex(kin, function (elem) { + return Compare.eq(element, elem); + }); + }); + }; - function (Arr, Number, String) { - var firstMatch = function (regexes, s) { - for (var i = 0; i < regexes.length; i++) { - var x = regexes[i]; - if (x.test(s)) return x; + var parents = function (element, isRoot) { + var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); + + // This is used a *lot* so it needs to be performant, not recursive + var dom = element.dom(); + var ret = []; + + while (dom.parentNode !== null && dom.parentNode !== undefined) { + var rawParent = dom.parentNode; + var parent = Element.fromDom(rawParent); + ret.push(parent); + + if (stop(parent) === true) break; + else dom = rawParent; } - return undefined; + return ret; }; - var find = function (regexes, agent) { - var r = firstMatch(regexes, agent); - if (!r) return { major : 0, minor : 0 }; - var group = function(i) { - return Number(agent.replace(r, '$' + i)); + var siblings = function (element) { + // TODO: Refactor out children so we can just not add self instead of filtering afterwards + var filterSelf = function (elements) { + return Arr.filter(elements, function (x) { + return !Compare.eq(element, x); + }); }; - return nu(group(1), group(2)); - }; - var detect = function (versionRegexes, agent) { - var cleanedAgent = String(agent).toLowerCase(); + return parent(element).map(children).map(filterSelf).getOr([]); + }; - if (versionRegexes.length === 0) return unknown(); - return find(versionRegexes, cleanedAgent); + var offsetParent = function (element) { + var dom = element.dom(); + return Option.from(dom.offsetParent).map(Element.fromDom); }; - var unknown = function () { - return nu(0, 0); + var prevSibling = function (element) { + var dom = element.dom(); + return Option.from(dom.previousSibling).map(Element.fromDom); }; - var nu = function (major, minor) { - return { major: major, minor: minor }; + var nextSibling = function (element) { + var dom = element.dom(); + return Option.from(dom.nextSibling).map(Element.fromDom); }; - return { - nu: nu, - detect: detect, - unknown: unknown + var prevSiblings = function (element) { + // This one needs to be reversed, so they're still in DOM order + return Arr.reverse(Recurse.toArray(element, prevSibling)); }; - } -); -define( - 'ephox.sand.core.Browser', - [ - 'ephox.katamari.api.Fun', - 'ephox.sand.detect.Version' - ], + var nextSiblings = function (element) { + return Recurse.toArray(element, nextSibling); + }; - function (Fun, Version) { - var edge = 'Edge'; - var chrome = 'Chrome'; - var ie = 'IE'; - var opera = 'Opera'; - var firefox = 'Firefox'; - var safari = 'Safari'; + var children = function (element) { + var dom = element.dom(); + return Arr.map(dom.childNodes, Element.fromDom); + }; - var isBrowser = function (name, current) { - return function () { - return current === name; - }; + var child = function (element, index) { + var children = element.dom().childNodes; + return Option.from(children[index]).map(Element.fromDom); }; - var unknown = function () { - return nu({ - current: undefined, - version: Version.unknown() - }); + var firstChild = function (element) { + return child(element, 0); }; - var nu = function (info) { - var current = info.current; - var version = info.version; + var lastChild = function (element) { + return child(element, element.dom().childNodes.length - 1); + }; - return { - current: current, - version: version, + var childNodesCount = function (element, index) { + return element.dom().childNodes.length; + }; - // INVESTIGATE: Rename to Edge ? - isEdge: isBrowser(edge, current), - isChrome: isBrowser(chrome, current), - // NOTE: isIe just looks too weird - isIE: isBrowser(ie, current), - isOpera: isBrowser(opera, current), - isFirefox: isBrowser(firefox, current), - isSafari: isBrowser(safari, current) - }; + var spot = Struct.immutable('element', 'offset'); + var leaf = function (element, offset) { + var cs = children(element); + return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); }; return { - unknown: unknown, - nu: nu, - edge: Fun.constant(edge), - chrome: Fun.constant(chrome), - ie: Fun.constant(ie), - opera: Fun.constant(opera), - firefox: Fun.constant(firefox), - safari: Fun.constant(safari) + owner: owner, + defaultView: defaultView, + documentElement: documentElement, + parent: parent, + findIndex: findIndex, + parents: parents, + siblings: siblings, + prevSibling: prevSibling, + offsetParent: offsetParent, + prevSiblings: prevSiblings, + nextSibling: nextSibling, + nextSiblings: nextSiblings, + children: children, + child: child, + firstChild: firstChild, + lastChild: lastChild, + childNodesCount: childNodesCount, + leaf: leaf }; } ); + define( - 'ephox.sand.core.OperatingSystem', + 'ephox.sugar.api.dom.Insert', [ - 'ephox.katamari.api.Fun', - 'ephox.sand.detect.Version' + 'ephox.sugar.api.search.Traverse' ], - function (Fun, Version) { - var windows = 'Windows'; - var ios = 'iOS'; - var android = 'Android'; - var linux = 'Linux'; - var osx = 'OSX'; - var solaris = 'Solaris'; - var freebsd = 'FreeBSD'; + function (Traverse) { + var before = function (marker, element) { + var parent = Traverse.parent(marker); + parent.each(function (v) { + v.dom().insertBefore(element.dom(), marker.dom()); + }); + }; - // Though there is a bit of dupe with this and Browser, trying to - // reuse code makes it much harder to follow and change. - var isOS = function (name, current) { - return function () { - return current === name; - }; + var after = function (marker, element) { + var sibling = Traverse.nextSibling(marker); + sibling.fold(function () { + var parent = Traverse.parent(marker); + parent.each(function (v) { + append(v, element); + }); + }, function (v) { + before(v, element); + }); }; - var unknown = function () { - return nu({ - current: undefined, - version: Version.unknown() + var prepend = function (parent, element) { + var firstChild = Traverse.firstChild(parent); + firstChild.fold(function () { + append(parent, element); + }, function (v) { + parent.dom().insertBefore(element.dom(), v.dom()); }); }; - var nu = function (info) { - var current = info.current; - var version = info.version; + var append = function (parent, element) { + parent.dom().appendChild(element.dom()); + }; - return { - current: current, - version: version, + var appendAt = function (parent, element, index) { + Traverse.child(parent, index).fold(function () { + append(parent, element); + }, function (v) { + before(v, element); + }); + }; - isWindows: isOS(windows, current), - // TODO: Fix capitalisation - isiOS: isOS(ios, current), - isAndroid: isOS(android, current), - isOSX: isOS(osx, current), - isLinux: isOS(linux, current), - isSolaris: isOS(solaris, current), - isFreeBSD: isOS(freebsd, current) - }; + var wrap = function (element, wrapper) { + before(element, wrapper); + append(wrapper, element); }; return { - unknown: unknown, - nu: nu, - - windows: Fun.constant(windows), - ios: Fun.constant(ios), - android: Fun.constant(android), - linux: Fun.constant(linux), - osx: Fun.constant(osx), - solaris: Fun.constant(solaris), - freebsd: Fun.constant(freebsd) + before: before, + after: after, + prepend: prepend, + append: append, + appendAt: appendAt, + wrap: wrap }; } ); -define( - 'ephox.sand.detect.DeviceType', - - [ - 'ephox.katamari.api.Fun' - ], - - function (Fun) { - return function (os, browser, userAgent) { - var isiPad = os.isiOS() && /ipad/i.test(userAgent) === true; - var isiPhone = os.isiOS() && !isiPad; - var isAndroid3 = os.isAndroid() && os.version.major === 3; - var isAndroid4 = os.isAndroid() && os.version.major === 4; - var isTablet = isiPad || isAndroid3 || ( isAndroid4 && /mobile/i.test(userAgent) === true ); - var isTouch = os.isiOS() || os.isAndroid(); - var isPhone = isTouch && !isTablet; - - var iOSwebview = browser.isSafari() && os.isiOS() && /safari/i.test(userAgent) === false; - return { - isiPad : Fun.constant(isiPad), - isiPhone: Fun.constant(isiPhone), - isTablet: Fun.constant(isTablet), - isPhone: Fun.constant(isPhone), - isTouch: Fun.constant(isTouch), - isAndroid: os.isAndroid, - isiOS: os.isiOS, - isWebView: Fun.constant(iOSwebview) - }; - }; - } -); define( - 'ephox.sand.detect.UaString', + 'ephox.sugar.api.dom.InsertAll', [ 'ephox.katamari.api.Arr', - 'ephox.sand.detect.Version', - 'global!String' + 'ephox.sugar.api.dom.Insert' ], - function (Arr, Version, String) { - var detect = function (candidates, userAgent) { - var agent = String(userAgent).toLowerCase(); - return Arr.find(candidates, function (candidate) { - return candidate.search(agent); + function (Arr, Insert) { + var before = function (marker, elements) { + Arr.each(elements, function (x) { + Insert.before(marker, x); }); }; - // They (browser and os) are the same at the moment, but they might - // not stay that way. - var detectBrowser = function (browsers, userAgent) { - return detect(browsers, userAgent).map(function (browser) { - var version = Version.detect(browser.versionRegexes, userAgent); - return { - current: browser.name, - version: version - }; + var after = function (marker, elements) { + Arr.each(elements, function (x, i) { + var e = i === 0 ? marker : elements[i - 1]; + Insert.after(e, x); }); }; - var detectOs = function (oses, userAgent) { - return detect(oses, userAgent).map(function (os) { - var version = Version.detect(os.versionRegexes, userAgent); - return { - current: os.name, - version: version - }; + var prepend = function (parent, elements) { + Arr.each(elements.slice().reverse(), function (x) { + Insert.prepend(parent, x); + }); + }; + + var append = function (parent, elements) { + Arr.each(elements, function (x) { + Insert.append(parent, x); }); }; return { - detectBrowser: detectBrowser, - detectOs: detectOs + before: before, + after: after, + prepend: prepend, + append: append }; } ); + define( - 'ephox.katamari.str.StrAppend', + 'ephox.sugar.api.dom.Remove', [ - + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.InsertAll', + 'ephox.sugar.api.search.Traverse' ], - function () { - var addToStart = function (str, prefix) { - return prefix + str; - }; + function (Arr, InsertAll, Traverse) { + var empty = function (element) { + // shortcut "empty node" trick. Requires IE 9. + element.dom().textContent = ''; - var addToEnd = function (str, suffix) { - return str + suffix; + // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general + // than removing every child node manually. + // The following is (probably) safe for performance as 99.9% of the time the trick works and + // Traverse.children will return an empty array. + Arr.each(Traverse.children(element), function (rogue) { + remove(rogue); + }); }; - var removeFromStart = function (str, numChars) { - return str.substring(numChars); + var remove = function (element) { + var dom = element.dom(); + if (dom.parentNode !== null) + dom.parentNode.removeChild(dom); }; - var removeFromEnd = function (str, numChars) { - return str.substring(0, str.length - numChars); + var unwrap = function (wrapper) { + var children = Traverse.children(wrapper); + if (children.length > 0) + InsertAll.before(wrapper, children); + remove(wrapper); }; - + return { - addToStart: addToStart, - addToEnd: addToEnd, - removeFromStart: removeFromStart, - removeFromEnd: removeFromEnd + empty: empty, + remove: remove, + unwrap: unwrap }; } ); + define( - 'ephox.katamari.str.StringParts', + 'ephox.sugar.api.node.Node', [ - 'ephox.katamari.api.Option', - 'global!Error' + 'ephox.sugar.api.node.NodeTypes' ], - function (Option, Error) { - /** Return the first 'count' letters from 'str'. -- * e.g. first("abcde", 2) === "ab" -- */ - var first = function(str, count) { - return str.substr(0, count); + function (NodeTypes) { + var name = function (element) { + var r = element.dom().nodeName; + return r.toLowerCase(); }; - /** Return the last 'count' letters from 'str'. - * e.g. last("abcde", 2) === "de" - */ - var last = function(str, count) { - return str.substr(str.length - count, str.length); + var type = function (element) { + return element.dom().nodeType; }; - var head = function(str) { - return str === '' ? Option.none() : Option.some(str.substr(0, 1)); + var value = function (element) { + return element.dom().nodeValue; }; - var tail = function(str) { - return str === '' ? Option.none() : Option.some(str.substring(1)); + 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 { - first: first, - last: last, - head: head, - tail: tail + name: name, + type: type, + value: value, + isElement: isElement, + isText: isText, + isDocument: isDocument, + isComment: isComment }; } ); + define( - 'ephox.katamari.api.Strings', + 'ephox.sugar.impl.NodeValue', [ - 'ephox.katamari.str.StrAppend', - 'ephox.katamari.str.StringParts', + 'ephox.sand.api.PlatformDetection', + 'ephox.katamari.api.Option', 'global!Error' ], - function (StrAppend, StringParts, Error) { - var checkRange = function(str, substr, start) { - if (substr === '') return true; - if (str.length < substr.length) return false; - var x = str.substr(start, start + substr.length); - return x === substr; - }; - - /** Given a string and object, perform template-replacements on the string, as specified by the object. - * Any template fields of the form ${name} are replaced by the string or number specified as obj["name"] - * Based on Douglas Crockford's 'supplant' method for template-replace of strings. Uses different template format. - */ - var supplant = function(str, obj) { - var isStringOrNumber = function(a) { - var t = typeof a; - return t === 'string' || t === 'number'; + 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(''); }; - return str.replace(/\${([^{}]*)}/g, - function (a, b) { - var value = obj[b]; - return isStringOrNumber(value) ? value : a; + 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 removeLeading = function (str, prefix) { - return startsWith(str, prefix) ? StrAppend.removeFromStart(str, prefix.length) : str; - }; + }; - var removeTrailing = function (str, prefix) { - return endsWith(str, prefix) ? StrAppend.removeFromEnd(str, prefix.length) : str; - }; + var getOptionSafe = function (element) { + return is(element) ? Option.from(element.dom().nodeValue) : Option.none(); + }; - var ensureLeading = function (str, prefix) { - return startsWith(str, prefix) ? str : StrAppend.addToStart(str, prefix); - }; + var browser = PlatformDetection.detect().browser; + var getOption = browser.isIE() && browser.version.major === 10 ? getOptionIE10 : getOptionSafe; - var ensureTrailing = function (str, prefix) { - return endsWith(str, prefix) ? str : StrAppend.addToEnd(str, prefix); - }; - - var contains = function(str, substr) { - return str.indexOf(substr) !== -1; - }; + 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; + }; - var capitalize = function(str) { - return StringParts.head(str).bind(function (head) { - return StringParts.tail(str).map(function (tail) { - return head.toUpperCase() + tail; - }); - }).getOr(str); + return { + get: get, + getOption: getOption, + set: set + }; }; + } +); +define( + 'ephox.sugar.api.node.Text', - /** Does 'str' start with 'prefix'? - * Note: all strings start with the empty string. - * More formally, for all strings x, startsWith(x, ""). - * This is so that for all strings x and y, startsWith(y + x, y) - */ - var startsWith = function(str, prefix) { - return checkRange(str, prefix, 0); - }; + [ + 'ephox.sugar.api.node.Node', + 'ephox.sugar.impl.NodeValue' + ], - /** Does 'str' end with 'suffix'? - * Note: all strings end with the empty string. - * More formally, for all strings x, endsWith(x, ""). - * This is so that for all strings x and y, endsWith(x + y, y) - */ - var endsWith = function(str, suffix) { - return checkRange(str, suffix, str.length - suffix.length); - }; + function (Node, NodeValue) { + var api = NodeValue(Node.isText, 'text'); - - /** removes all leading and trailing spaces */ - var trim = function(str) { - return str.replace(/^\s+|\s+$/g, ''); + var get = function (element) { + return api.get(element); }; - var lTrim = function(str) { - return str.replace(/^\s+/g, ''); + var getOption = function (element) { + return api.getOption(element); }; - var rTrim = function(str) { - return str.replace(/\s+$/g, ''); + var set = function (element, value) { + api.set(element, value); }; return { - supplant: supplant, - startsWith: startsWith, - removeLeading: removeLeading, - removeTrailing: removeTrailing, - ensureLeading: ensureLeading, - ensureTrailing: ensureTrailing, - endsWith: endsWith, - contains: contains, - trim: trim, - lTrim: lTrim, - rTrim: rTrim, - capitalize: capitalize + get: get, + getOption: getOption, + set: set }; } ); define( - 'ephox.sand.info.PlatformInfo', + 'ephox.sugar.api.node.Body', [ - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Strings' + 'ephox.katamari.api.Thunk', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'global!document' ], - function (Fun, Strings) { - var normalVersionRegex = /.*?version\/\ ?([0-9]+)\.([0-9]+).*/; + function (Thunk, Element, Node, document) { - var checkContains = function (target) { - return function (uastring) { - return Strings.contains(uastring, target); - }; + // 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 browsers = [ - { - name : 'Edge', - versionRegexes: [/.*?edge\/ ?([0-9]+)\.([0-9]+)$/], - search: function (uastring) { - var monstrosity = Strings.contains(uastring, 'edge/') && Strings.contains(uastring, 'chrome') && Strings.contains(uastring, 'safari') && Strings.contains(uastring, 'applewebkit'); - return monstrosity; - } - }, - { - name : 'Chrome', - versionRegexes: [/.*?chrome\/([0-9]+)\.([0-9]+).*/, normalVersionRegex], - search : function (uastring) { - return Strings.contains(uastring, 'chrome') && !Strings.contains(uastring, 'chromeframe'); - } - }, - { - name : 'IE', - versionRegexes: [/.*?msie\ ?([0-9]+)\.([0-9]+).*/, /.*?rv:([0-9]+)\.([0-9]+).*/], - search: function (uastring) { - return Strings.contains(uastring, 'msie') || Strings.contains(uastring, 'trident'); - } - }, - // INVESTIGATE: Is this still the Opera user agent? - { - name : 'Opera', - versionRegexes: [normalVersionRegex, /.*?opera\/([0-9]+)\.([0-9]+).*/], - search : checkContains('opera') - }, - { - name : 'Firefox', - versionRegexes: [/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/], - search : checkContains('firefox') - }, - { - name : 'Safari', - versionRegexes: [normalVersionRegex, /.*?cpu os ([0-9]+)_([0-9]+).*/], - search : function (uastring) { - return (Strings.contains(uastring, 'safari') || Strings.contains(uastring, 'mobile/')) && Strings.contains(uastring, 'applewebkit'); - } - } - ]; + var body = Thunk.cached(function() { + return getBody(Element.fromDom(document)); + }); - var oses = [ - { - name : 'Windows', - search : checkContains('win'), - versionRegexes: [/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/] - }, - { - name : 'iOS', - search : function (uastring) { - return Strings.contains(uastring, 'iphone') || Strings.contains(uastring, 'ipad'); - }, - versionRegexes: [/.*?version\/\ ?([0-9]+)\.([0-9]+).*/, /.*cpu os ([0-9]+)_([0-9]+).*/, /.*cpu iphone os ([0-9]+)_([0-9]+).*/] - }, - { - name : 'Android', - search : checkContains('android'), - versionRegexes: [/.*?android\ ?([0-9]+)\.([0-9]+).*/] - }, - { - name : 'OSX', - search : checkContains('os x'), - versionRegexes: [/.*?os\ x\ ?([0-9]+)_([0-9]+).*/] - }, - { - name : 'Linux', - search : checkContains('linux'), - versionRegexes: [ ] - }, - { name : 'Solaris', - search : checkContains('sunos'), - versionRegexes: [ ] - }, - { - name : 'FreeBSD', - search : checkContains('freebsd'), - versionRegexes: [ ] - } - ]; + 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 { - browsers: Fun.constant(browsers), - oses: Fun.constant(oses) + body: body, + getBody: getBody, + inBody: inBody }; } ); + define( - 'ephox.sand.core.PlatformDetection', + 'ephox.sugar.api.search.PredicateFilter', [ - 'ephox.sand.core.Browser', - 'ephox.sand.core.OperatingSystem', - 'ephox.sand.detect.DeviceType', - 'ephox.sand.detect.UaString', - 'ephox.sand.info.PlatformInfo' + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Body', + 'ephox.sugar.api.search.Traverse' ], - function (Browser, OperatingSystem, DeviceType, UaString, PlatformInfo) { - var detect = function (userAgent) { - var browsers = PlatformInfo.browsers(); - var oses = PlatformInfo.oses(); - - var browser = UaString.detectBrowser(browsers, userAgent).fold( - Browser.unknown, - Browser.nu - ); - var os = UaString.detectOs(oses, userAgent).fold( - OperatingSystem.unknown, - OperatingSystem.nu - ); - var deviceType = DeviceType(os, browser, userAgent); + function (Arr, Body, Traverse) { + // maybe TraverseWith, similar to traverse but with a predicate? - return { - browser: browser, - os: os, - deviceType: deviceType - }; + var all = function (predicate) { + return descendants(Body.body(), predicate); }; - return { - detect: detect + var ancestors = function (scope, predicate, isRoot) { + return Arr.filter(Traverse.parents(scope, isRoot), predicate); }; - } -); -defineGlobal("global!navigator", navigator); -define( - 'ephox.sand.api.PlatformDetection', - - [ - 'ephox.katamari.api.Thunk', - 'ephox.sand.core.PlatformDetection', - 'global!navigator' - ], - function (Thunk, PlatformDetection, navigator) { - var detect = Thunk.cached(function () { - var userAgent = navigator.userAgent; - return PlatformDetection.detect(userAgent); - }); + var siblings = function (scope, predicate) { + return Arr.filter(Traverse.siblings(scope), predicate); + }; - return { - detect: detect + var children = function (scope, predicate) { + return Arr.filter(Traverse.children(scope), predicate); }; - } -); -define( - 'ephox.sugar.api.node.NodeTypes', - [ + 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; + }; - function () { return { - ATTRIBUTE: 2, - CDATA_SECTION: 4, - COMMENT: 8, - DOCUMENT: 9, - DOCUMENT_TYPE: 10, - DOCUMENT_FRAGMENT: 11, - ELEMENT: 1, - TEXT: 3, - PROCESSING_INSTRUCTION: 7, - ENTITY_REFERENCE: 5, - ENTITY: 6, - NOTATION: 12 + all: all, + ancestors: ancestors, + siblings: siblings, + children: children, + descendants: descendants }; } ); + define( - 'ephox.sugar.api.search.Selectors', + 'ephox.sugar.api.search.SelectorFilter', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Option', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.NodeTypes', - 'global!Error', - 'global!document' + 'ephox.sugar.api.search.PredicateFilter', + 'ephox.sugar.api.search.Selectors' ], - function (Arr, Option, Element, NodeTypes, Error, document) { - /* - * There's a lot of code here; the aim is to allow the browser to optimise constant comparisons, - * instead of doing object lookup feature detection on every call - */ - var STANDARD = 0; - var MSSTANDARD = 1; - var WEBKITSTANDARD = 2; - var FIREFOXSTANDARD = 3; - - var selectorType = (function () { - var test = document.createElement('span'); - // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. - // Still check for the others, but do it last. - return test.matches !== undefined ? STANDARD : - test.msMatchesSelector !== undefined ? MSSTANDARD : - test.webkitMatchesSelector !== undefined ? WEBKITSTANDARD : - test.mozMatchesSelector !== undefined ? FIREFOXSTANDARD : - -1; - })(); - - - var ELEMENT = NodeTypes.ELEMENT; - var DOCUMENT = NodeTypes.DOCUMENT; + function (PredicateFilter, Selectors) { + var all = function (selector) { + return Selectors.all(selector); + }; - var is = function (element, selector) { - var elem = element.dom(); - if (elem.nodeType !== ELEMENT) return false; // documents have querySelector but not matches + // 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). + // - // As of Chrome 34 / Safari 7.1 / FireFox 34, everyone except IE has the unprefixed function. - // Still check for the others, but do it last. - else if (selectorType === STANDARD) return elem.matches(selector); - else if (selectorType === MSSTANDARD) return elem.msMatchesSelector(selector); - else if (selectorType === WEBKITSTANDARD) return elem.webkitMatchesSelector(selector); - else if (selectorType === FIREFOXSTANDARD) return elem.mozMatchesSelector(selector); - else throw new Error('Browser lacks native selectors'); // unfortunately we can't throw this on startup :( + 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 bypassSelector = function (dom) { - // Only elements and documents support querySelector - return dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT || - // IE fix for complex queries on empty nodes: http://jsfiddle.net/spyder/fv9ptr5L/ - dom.childElementCount === 0; + 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 all = function (selector, scope) { - var base = scope === undefined ? document : scope.dom(); - return bypassSelector(base) ? [] : Arr.map(base.querySelectorAll(selector), Element.fromDom); + 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 one = function (selector, scope) { - var base = scope === undefined ? document : scope.dom(); - return bypassSelector(base) ? Option.none() : Option.from(base.querySelector(selector)).map(Element.fromDom); + var descendants = function (scope, selector) { + return Selectors.all(selector, scope); }; return { all: all, - is: is, - one: one + ancestors: ancestors, + siblings: siblings, + children: children, + descendants: descendants }; } ); -define( - 'ephox.sugar.api.dom.Compare', - - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.sand.api.Node', - 'ephox.sand.api.PlatformDetection', - 'ephox.sugar.api.search.Selectors' +/** + * ElementType.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.ElementType', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Node' ], + function (Arr, Fun, Node) { + var blocks = [ + 'article', 'aside', 'details', 'div', 'dt', 'figcaption', 'footer', + 'form', 'fieldset', 'header', 'hgroup', 'html', 'main', 'nav', + 'section', 'summary', 'body', 'p', 'dl', 'multicol', 'dd', 'figure', + 'address', 'center', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'listing', 'xmp', 'pre', 'plaintext', 'menu', 'dir', 'ul', 'ol', 'li', 'hr', + 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'caption' + ]; - function (Arr, Fun, Node, PlatformDetection, Selectors) { + var voids = [ + 'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', + 'isindex', 'link', 'meta', 'param', 'embed', 'source', 'wbr', 'track' + ]; - var eq = function (e1, e2) { - return e1.dom() === e2.dom(); - }; + var tableCells = ['td', 'th']; + var tableSections = ['thead', 'tbody', 'tfoot']; - var isEqualNode = function (e1, e2) { - return e1.dom().isEqualNode(e2.dom()); - }; + var textBlocks = [ + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', + 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article', + 'section', 'hgroup', 'aside', 'nav', 'figure' + ]; - var member = function (element, elements) { - return Arr.exists(elements, Fun.curry(eq, element)); - }; + var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; + var listItems = ['li', 'dd', 'dt']; + var lists = ['ul', 'ol', 'dl']; - // DOM contains() method returns true if e1===e2, we define our contains() to return false (a node does not contain itself). - var regularContains = function (e1, e2) { - var d1 = e1.dom(), d2 = e2.dom(); - return d1 === d2 ? false : d1.contains(d2); + var lazyLookup = function (items) { + var lookup; + return function (node) { + lookup = lookup ? lookup : Arr.mapToObject(items, Fun.constant(true)); + return lookup.hasOwnProperty(Node.name(node)); + }; }; - var ieContains = function (e1, e2) { - // IE only implements the contains() method for Element nodes. - // It fails for Text nodes, so implement it using compareDocumentPosition() - // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect - // Note that compareDocumentPosition returns CONTAINED_BY if 'e2 *is_contained_by* e1': - // Also, compareDocumentPosition defines a node containing itself as false. - return Node.documentPositionContainedBy(e1.dom(), e2.dom()); - }; + var isHeading = lazyLookup(headings); - var browser = PlatformDetection.detect().browser; + var isBlock = lazyLookup(blocks); - // Returns: true if node e1 contains e2, otherwise false. - // (returns false if e1===e2: A node does not contain itself). - var contains = browser.isIE() ? ieContains : regularContains; + var isInline = function (node) { + return Node.isElement(node) && !isBlock(node); + }; - return { - eq: eq, - isEqualNode: isEqualNode, - member: member, - contains: contains, + var isBr = function (node) { + return Node.isElement(node) && Node.name(node) === 'br'; + }; - // Only used by DomUniverse. Remove (or should Selectors.is move here?) - is: Selectors.is + return { + isBlock: isBlock, + isInline: isInline, + isHeading: isHeading, + isTextBlock: lazyLookup(textBlocks), + isList: lazyLookup(lists), + isListItem: lazyLookup(listItems), + isVoid: lazyLookup(voids), + isTableSection: lazyLookup(tableSections), + isTableCell: lazyLookup(tableCells), + isBr: isBr }; } ); -define( - 'ephox.sugar.api.search.Traverse', +/** + * PaddingBr.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.PaddingBr', [ - 'ephox.katamari.api.Type', 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Struct', - 'ephox.sugar.alien.Recurse', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element' + 'ephox.sugar.api.dom.Insert', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.node.Text', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.Traverse', + 'tinymce.core.dom.ElementType' ], + function (Arr, Insert, Remove, Element, Node, Text, SelectorFilter, Traverse, ElementType) { + var getLastChildren = function (elm) { + var children = [], rawNode = elm.dom(); - function (Type, Arr, Fun, Option, Struct, Recurse, Compare, Element) { - // The document associated with the current element - var owner = function (element) { - return Element.fromDom(element.dom().ownerDocument); + while (rawNode) { + children.push(Element.fromDom(rawNode)); + rawNode = rawNode.lastChild; + } + + return children; }; - var documentElement = function (element) { - // TODO: Avoid unnecessary wrap/unwrap here - var doc = owner(element); - return Element.fromDom(doc.dom().documentElement); + 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); + } }; - // The window element associated with the element - var defaultView = function (element) { - var el = element.dom(); - var defaultView = el.ownerDocument.defaultView; - return Element.fromDom(defaultView); + var fillWithPaddingBr = function (elm) { + Remove.empty(elm); + Insert.append(elm, Element.fromHtml('
    ')); }; - var parent = function (element) { - var dom = element.dom(); - return Option.from(dom.parentNode).map(Element.fromDom); + var isPaddingContents = function (elm) { + return Node.isText(elm) ? Text.get(elm) === '\u00a0' : ElementType.isBr(elm); }; - var findIndex = function (element) { - return parent(element).bind(function (p) { - // TODO: Refactor out children so we can avoid the constant unwrapping - var kin = children(p); - return Arr.findIndex(kin, function (elem) { - return Compare.eq(element, elem); + var isPaddedElement = function (elm) { + return Arr.filter(Traverse.children(elm), isPaddingContents).length === 1; + }; + + var trimBlockTrailingBr = function (elm) { + Traverse.lastChild(elm).each(function (lastChild) { + Traverse.prevSibling(lastChild).each(function (lastChildPrevSibling) { + if (ElementType.isBlock(elm) && ElementType.isBr(lastChild) && ElementType.isBlock(lastChildPrevSibling)) { + Remove.remove(lastChild); + } }); }); }; - var parents = function (element, isRoot) { - var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); + return { + removeTrailingBr: removeTrailingBr, + fillWithPaddingBr: fillWithPaddingBr, + isPaddedElement: isPaddedElement, + trimBlockTrailingBr: trimBlockTrailingBr + }; + } +); +/** + * FormatUtils.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 is used a *lot* so it needs to be performant, not recursive - var dom = element.dom(); - var ret = []; +define( + 'tinymce.core.fmt.FormatUtils', + [ + 'tinymce.core.dom.TreeWalker' + ], + function (TreeWalker) { + var isInlineBlock = function (node) { + return node && /^(IMG)$/.test(node.nodeName); + }; - while (dom.parentNode !== null && dom.parentNode !== undefined) { - var rawParent = dom.parentNode; - var parent = Element.fromDom(rawParent); - ret.push(parent); + var moveStart = function (dom, selection, rng) { + var container = rng.startContainer, + offset = rng.startOffset, + walker, node, nodes; - if (stop(parent) === true) break; - else dom = rawParent; + if (rng.startContainer === rng.endContainer) { + if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { + return; + } } - return ret; - }; - var siblings = function (element) { - // TODO: Refactor out children so we can just not add self instead of filtering afterwards - var filterSelf = function (elements) { - return Arr.filter(elements, function (x) { - return !Compare.eq(element, x); - }); - }; + // 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 = dom.nodeIndex(container); + container = container.parentNode; + } - return parent(element).map(children).map(filterSelf).getOr([]); - }; + // 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); + } - var offsetParent = function (element) { - var dom = element.dom(); - return Option.from(dom.offsetParent).map(Element.fromDom); - }; + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3 && !isWhiteSpaceNode(node)) { + rng.setStart(node, 0); + selection.setRng(rng); - var prevSibling = function (element) { - var dom = element.dom(); - return Option.from(dom.previousSibling).map(Element.fromDom); + return; + } + } + } }; - var nextSibling = function (element) { - var dom = element.dom(); - return Option.from(dom.nextSibling).map(Element.fromDom); - }; + /** + * Returns the next/previous non whitespace node. + * + * @private + * @param {Node} node Node to start at. + * @param {boolean} next (Optional) Include next or previous node defaults to previous. + * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false. + * @return {Node} Next or previous node or undefined if it wasn't found. + */ + var getNonWhiteSpaceSibling = function (node, next, inc) { + if (node) { + next = next ? 'nextSibling' : 'previousSibling'; - var prevSiblings = function (element) { - // This one needs to be reversed, so they're still in DOM order - return Arr.reverse(Recurse.toArray(element, prevSibling)); + for (node = inc ? node : node[next]; node; node = node[next]) { + if (node.nodeType === 1 || !isWhiteSpaceNode(node)) { + return node; + } + } + } }; - var nextSiblings = function (element) { - return Recurse.toArray(element, nextSibling); - }; + var isTextBlock = function (editor, name) { + if (name.nodeType) { + name = name.nodeName; + } - var children = function (element) { - var dom = element.dom(); - return Arr.map(dom.childNodes, Element.fromDom); + return !!editor.schema.getTextBlockElements()[name.toLowerCase()]; }; - var child = function (element, index) { - var children = element.dom().childNodes; - return Option.from(children[index]).map(Element.fromDom); + var isValid = function (ed, parent, child) { + return ed.schema.isValidChild(parent, child); }; - var firstChild = function (element) { - return child(element, 0); + var isWhiteSpaceNode = function (node) { + return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); }; - var lastChild = function (element) { - return child(element, element.dom().childNodes.length - 1); - }; + /** + * 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. + */ + var replaceVars = function (value, vars) { + if (typeof value !== "string") { + value = value(vars); + } else if (vars) { + value = value.replace(/%(\w+)/g, function (str, name) { + return vars[name] || str; + }); + } - var spot = Struct.immutable('element', 'offset'); - var leaf = function (element, offset) { - var cs = children(element); - return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset); + return value; }; - return { - owner: owner, - defaultView: defaultView, - documentElement: documentElement, - parent: parent, - findIndex: findIndex, - parents: parents, - siblings: siblings, - prevSibling: prevSibling, - offsetParent: offsetParent, - prevSiblings: prevSiblings, - nextSibling: nextSibling, - nextSiblings: nextSiblings, - children: children, - child: child, - firstChild: firstChild, - lastChild: lastChild, - leaf: leaf + /** + * 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. + */ + var isEq = function (str1, str2) { + str1 = str1 || ''; + str2 = str2 || ''; + + str1 = '' + (str1.nodeName || str1); + str2 = '' + (str2.nodeName || str2); + + return str1.toLowerCase() === str2.toLowerCase(); }; - } -); -define( - 'ephox.sugar.api.dom.Insert', + var normalizeStyleValue = function (dom, value, name) { + // Force the format to hex + if (name === 'color' || name === 'backgroundColor') { + value = dom.toHex(value); + } - [ - 'ephox.sugar.api.search.Traverse' - ], + // Opera will return bold as 700 + if (name === 'fontWeight' && value === 700) { + value = 'bold'; + } - function (Traverse) { - var before = function (marker, element) { - var parent = Traverse.parent(marker); - parent.each(function (v) { - v.dom().insertBefore(element.dom(), marker.dom()); - }); - }; + // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" + if (name === 'fontFamily') { + value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); + } - var after = function (marker, element) { - var sibling = Traverse.nextSibling(marker); - sibling.fold(function () { - var parent = Traverse.parent(marker); - parent.each(function (v) { - append(v, element); - }); - }, function (v) { - before(v, element); - }); + return '' + value; }; - var prepend = function (parent, element) { - var firstChild = Traverse.firstChild(parent); - firstChild.fold(function () { - append(parent, element); - }, function (v) { - parent.dom().insertBefore(element.dom(), v.dom()); - }); + var getStyle = function (dom, node, name) { + return normalizeStyleValue(dom, dom.getStyle(node, name), name); }; - var append = function (parent, element) { - parent.dom().appendChild(element.dom()); - }; + var getTextDecoration = function (dom, node) { + var decoration; - var appendAt = function (parent, element, index) { - Traverse.child(parent, index).fold(function () { - append(parent, element); - }, function (v) { - before(v, element); + dom.getParent(node, function (n) { + decoration = dom.getStyle(n, 'text-decoration'); + return decoration && decoration !== 'none'; }); + + return decoration; }; - var wrap = function (element, wrapper) { - before(element, wrapper); - append(wrapper, element); + var getParents = function (dom, node, selector) { + return dom.getParents(node, selector, dom.getRoot()); }; return { - before: before, - after: after, - prepend: prepend, - append: append, - appendAt: appendAt, - wrap: wrap + isInlineBlock: isInlineBlock, + moveStart: moveStart, + getNonWhiteSpaceSibling: getNonWhiteSpaceSibling, + isTextBlock: isTextBlock, + isValid: isValid, + isWhiteSpaceNode: isWhiteSpaceNode, + replaceVars: replaceVars, + isEq: isEq, + normalizeStyleValue: normalizeStyleValue, + getStyle: getStyle, + getTextDecoration: getTextDecoration, + getParents: getParents }; } ); +/** + * ExpandRange.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( - 'ephox.sugar.api.dom.InsertAll', - + 'tinymce.core.fmt.ExpandRange', [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.dom.Insert' + 'tinymce.core.dom.BookmarkManager', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.fmt.FormatUtils' ], + function (BookmarkManager, TreeWalker, FormatUtils) { + var isBookmarkNode = BookmarkManager.isBookmarkNode; + var getParents = FormatUtils.getParents, isWhiteSpaceNode = FormatUtils.isWhiteSpaceNode, isTextBlock = FormatUtils.isTextBlock; - function (Arr, Insert) { - var before = function (marker, elements) { - Arr.each(elements, function (x) { - Insert.before(marker, x); - }); - }; - - var after = function (marker, elements) { - Arr.each(elements, function (x, i) { - var e = i === 0 ? marker : elements[i - 1]; - Insert.after(e, x); - }); - }; - - var prepend = function (parent, elements) { - Arr.each(elements.slice().reverse(), function (x) { - Insert.prepend(parent, x); - }); - }; + // 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. + var findLeaf = function (node, offset) { + if (typeof offset === 'undefined') { + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } - var append = function (parent, elements) { - Arr.each(elements, function (x) { - Insert.append(parent, x); - }); + while (node && node.hasChildNodes()) { + node = node.childNodes[offset]; + if (node) { + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + } + return { node: node, offset: offset }; }; - return { - before: before, - after: after, - prepend: prepend, - append: append - }; - } -); - -define( - 'ephox.sugar.api.dom.Remove', + var excludeTrailingWhitespace = function (endContainer, endOffset) { + // Avoid applying formatting to a trailing space, + // but remove formatting from trailing space + var leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { + leaf = findLeaf(leaf.node.previousSibling); + } - [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.dom.InsertAll', - 'ephox.sugar.api.search.Traverse' - ], + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { - function (Arr, InsertAll, Traverse) { - var empty = function (element) { - // shortcut "empty node" trick. Requires IE 9. - element.dom().textContent = ''; + if (leaf.offset > 1) { + endContainer = leaf.node; + endContainer.splitText(leaf.offset - 1); + } + } + } - // If the contents was a single empty text node, the above doesn't remove it. But, it's still faster in general - // than removing every child node manually. - // The following is (probably) safe for performance as 99.9% of the time the trick works and - // Traverse.children will return an empty array. - Arr.each(Traverse.children(element), function (rogue) { - remove(rogue); - }); + return endContainer; }; - var remove = function (element) { - var dom = element.dom(); - if (dom.parentNode !== null) - dom.parentNode.removeChild(dom); + var isBogusBr = function (node) { + return node.nodeName === "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; }; - var unwrap = function (wrapper) { - var children = Traverse.children(wrapper); - if (children.length > 0) - InsertAll.before(wrapper, children); - remove(wrapper); - }; + var expandRng = function (editor, rng, format, remove) { + var lastIdx, endPoint, + startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset, + dom = editor.dom; - return { - empty: empty, - remove: remove, - unwrap: unwrap - }; - } -); + // This function walks up the tree if there is no siblings before/after the node + var findParentContainer = function (start) { + var container, parent, sibling, siblingName, root; -define( - 'ephox.sugar.api.node.Node', + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); - [ - 'ephox.sugar.api.node.NodeTypes' - ], + // 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; + } + } - function (NodeTypes) { - var name = function (element) { - var r = element.dom().nodeName; - return r.toLowerCase(); - }; + /*eslint no-constant-condition:0 */ + while (true) { + // Stop expanding on block elements + if (!format[0].block_expand && dom.isBlock(parent)) { + return parent; + } - var type = function (element) { - return element.dom().nodeType; - }; + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { + return parent; + } + } - var value = function (element) { - return element.dom().nodeValue; - }; + // Check if we can move up are we at root level or body level + if (parent === root || parent.parentNode === root) { + container = parent; + break; + } - var isType = function (t) { - return function (element) { - return type(element) === t; + parent = parent.parentNode; + } + + return container; }; - }; - var isComment = function (element) { - return type(element) === NodeTypes.COMMENT || name(element) === '#comment'; - }; + // 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]; - var isElement = isType(NodeTypes.ELEMENT); - var isText = isType(NodeTypes.TEXT); - var isDocument = isType(NodeTypes.DOCUMENT); + if (startContainer.nodeType === 3) { + startOffset = 0; + } + } - return { - name: name, - type: type, - value: value, - isElement: isElement, - isText: isText, - isDocument: isDocument, - isComment: isComment - }; - } -); + // 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]; -define( - 'ephox.sugar.impl.NodeValue', + if (endContainer.nodeType === 3) { + endOffset = endContainer.nodeValue.length; + } + } - [ - 'ephox.sand.api.PlatformDetection', - 'ephox.katamari.api.Option', - 'global!Error' - ], + // Expands the node to the closes contentEditable false element if it exists + var findParentContentEditable = function (node) { + var parent = node; - 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(''); - }; + while (parent) { + if (parent.nodeType === 1 && dom.getContentEditable(parent)) { + return dom.getContentEditable(parent) === "false" ? parent : node; + } - 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(); + parent = parent.parentNode; } - }; - var getOptionSafe = function (element) { - return is(element) ? Option.from(element.dom().nodeValue) : Option.none(); + return node; }; - var browser = PlatformDetection.detect().browser; - var getOption = browser.isIE() && browser.version.major === 10 ? getOptionIE10 : getOptionSafe; + var findWordEndPoint = function (container, offset, start) { + var walker, node, pos, lastTextNode; - 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; - }; + var findSpace = function (node, offset) { + var pos, pos2, str = node.nodeValue; - return { - get: get, - getOption: getOption, - set: set - }; - }; - } -); -define( - 'ephox.sugar.api.node.Text', + if (typeof offset === "undefined") { + offset = start ? str.length : 0; + } - [ - 'ephox.sugar.api.node.Node', - 'ephox.sugar.impl.NodeValue' - ], + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; - function (Node, NodeValue) { - var api = NodeValue(Node.isText, 'text'); + // 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; + } - var get = function (element) { - return api.get(element); - }; + return pos; + }; - var getOption = function (element) { - return api.getOption(element); - }; + if (container.nodeType === 3) { + pos = findSpace(container, offset); - var set = function (element, value) { - api.set(element, value); - }; + if (pos !== -1) { + return { container: container, offset: pos }; + } - return { - get: get, - getOption: getOption, - set: set - }; - } -); + lastTextNode = container; + } -define( - 'ephox.sugar.api.node.Body', + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, dom.isBlock) || editor.getBody()); + while ((node = walker[start ? 'prev' : 'next']())) { + if (node.nodeType === 3) { + lastTextNode = node; + pos = findSpace(node); - [ - 'ephox.katamari.api.Thunk', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'global!document' - ], + if (pos !== -1) { + return { container: node, offset: pos }; + } + } else if (dom.isBlock(node)) { + break; + } + } - function (Thunk, Element, Node, document) { + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } - // 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(); + return { container: lastTextNode, offset: offset }; + } + }; - // 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 findSelectorEndPoint = function (container, siblingName) { + var parents, i, y, curFormat; - var body = Thunk.cached(function() { - return getBody(Element.fromDom(document)); - }); + if (container.nodeType === 3 && container.nodeValue.length === 0 && container[siblingName]) { + container = container[siblingName]; + } - var getBody = function (doc) { - var body = doc.dom().body; - if (body === null || body === undefined) throw 'Body is not available yet'; - return Element.fromDom(body); - }; + parents = getParents(dom, container); + for (i = 0; i < parents.length; i++) { + for (y = 0; y < format.length; y++) { + curFormat = format[y]; - return { - body: body, - getBody: getBody, - inBody: inBody - }; - } -); + // If collapsed state is set then skip formats that doesn't match that + if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { + continue; + } -define( - 'ephox.sugar.api.search.PredicateFilter', + if (dom.is(parents[i], curFormat.selector)) { + return parents[i]; + } + } + } - [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.node.Body', - 'ephox.sugar.api.search.Traverse' - ], + return container; + }; - function (Arr, Body, Traverse) { - // maybe TraverseWith, similar to traverse but with a predicate? + var findBlockEndPoint = function (container, siblingName) { + var node, root = dom.getRoot(); - var all = function (predicate) { - return descendants(Body.body(), predicate); - }; + // Expand to block of similar type + if (!format[0].wrapper) { + node = dom.getParent(container, format[0].block, root); + } - var ancestors = function (scope, predicate, isRoot) { - return Arr.filter(Traverse.parents(scope, isRoot), predicate); - }; + // Expand to first wrappable block element or any block element + if (!node) { + var scopeRoot = dom.getParent(container, 'LI,TD,TH'); + 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(editor, node); + }, scopeRoot); + } - var siblings = function (scope, predicate) { - return Arr.filter(Traverse.siblings(scope), predicate); - }; + // Exclude inner lists from wrapping + if (node && format[0].wrapper) { + node = getParents(dom, node, 'ul,ol').reverse()[0] || node; + } - var children = function (scope, predicate) { - return Arr.filter(Traverse.children(scope), predicate); - }; + // Didn't find a block element look for first/last wrappable element + if (!node) { + node = container; - var descendants = function (scope, predicate) { - var result = []; + while (node[siblingName] && !dom.isBlock(node[siblingName])) { + node = node[siblingName]; - // Recurse.toArray() might help here - Arr.each(Traverse.children(scope), function (x) { - if (predicate(x)) { - result = result.concat([ x ]); + // 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 (FormatUtils.isEq(node, 'br')) { + break; + } + } } - result = result.concat(descendants(x, predicate)); - }); - return result; - }; - return { - all: all, - ancestors: ancestors, - siblings: siblings, - children: children, - descendants: descendants - }; - } -); + return node || container; + }; -define( - 'ephox.sugar.api.search.SelectorFilter', + // Expand to closest contentEditable element + startContainer = findParentContentEditable(startContainer); + endContainer = findParentContentEditable(endContainer); - [ - 'ephox.sugar.api.search.PredicateFilter', - 'ephox.sugar.api.search.Selectors' - ], + // Exclude bookmark nodes if possible + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; + startContainer = startContainer.nextSibling || startContainer; - function (PredicateFilter, Selectors) { - var all = function (selector) { - return Selectors.all(selector); - }; + if (startContainer.nodeType === 3) { + startOffset = 0; + } + } - // 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). - // + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; + endContainer = endContainer.previousSibling || endContainer; - 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); - }; + if (endContainer.nodeType === 3) { + endOffset = endContainer.length; + } + } - 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); - }); - }; + 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; + } - 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); - }); - }; + // Expand right to closest word boundary + endPoint = findWordEndPoint(endContainer, endOffset); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } - var descendants = function (scope, selector) { - return Selectors.all(selector, scope); - }; + endContainer = remove ? endContainer : excludeTrailingWhitespace(endContainer, endOffset); + } - return { - all: all, - ancestors: ancestors, - siblings: siblings, - children: children, - descendants: descendants - }; - } -); + // 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); + } -/** - * ElementType.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 - */ + if (!format[0].inline || (endContainer.nodeType !== 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(); + } + } -define( - 'tinymce.core.dom.ElementType', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.sugar.api.node.Node' - ], - function (Arr, Fun, Node) { - var blocks = [ - 'article', 'aside', 'details', 'div', 'dt', 'figcaption', 'footer', - 'form', 'fieldset', 'header', 'hgroup', 'html', 'main', 'nav', - 'section', 'summary', 'body', 'p', 'dl', 'multicol', 'dd', 'figure', - 'address', 'center', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', - 'listing', 'xmp', 'pre', 'plaintext', 'menu', 'dir', 'ul', 'ol', 'li', 'hr', - 'table', 'tbody', 'thead', 'tfoot', 'th', 'tr', 'td', 'caption' - ]; - - var voids = [ - 'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', - 'isindex', 'link', 'meta', 'param', 'embed', 'source', 'wbr', 'track' - ]; - - var tableCells = ['td', 'th']; - var tableSections = ['thead', 'tbody', 'tfoot']; - - var textBlocks = [ - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'div', 'address', 'pre', 'form', - 'blockquote', 'center', 'dir', 'fieldset', 'header', 'footer', 'article', - 'section', 'hgroup', 'aside', 'nav', 'figure' - ]; + // 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'); + } - var headings = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - var listItems = ['li', 'dd', 'dt']; - var lists = ['ul', 'ol', 'dl']; + // 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'); - var lazyLookup = function (items) { - var lookup; - return function (node) { - lookup = lookup ? lookup : Arr.mapToObject(items, Fun.constant(true)); - return lookup.hasOwnProperty(Node.name(node)); - }; - }; + // Non block element then try to expand up the leaf + if (format[0].block) { + if (!dom.isBlock(startContainer)) { + startContainer = findParentContainer(true); + } - var isHeading = lazyLookup(headings); + if (!dom.isBlock(endContainer)) { + endContainer = findParentContainer(); + } + } + } - var isBlock = lazyLookup(blocks); + // Setup index for startContainer + if (startContainer.nodeType === 1) { + startOffset = dom.nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } - var isInline = function (node) { - return Node.isElement(node) && !isBlock(node); - }; + // Setup index for endContainer + if (endContainer.nodeType === 1) { + endOffset = dom.nodeIndex(endContainer) + 1; + endContainer = endContainer.parentNode; + } - var isBr = function (node) { - return Node.isElement(node) && Node.name(node) === 'br'; + // Return new range like object + return { + startContainer: startContainer, + startOffset: startOffset, + endContainer: endContainer, + endOffset: endOffset + }; }; return { - isBlock: isBlock, - isInline: isInline, - isHeading: isHeading, - isTextBlock: lazyLookup(textBlocks), - isList: lazyLookup(lists), - isListItem: lazyLookup(listItems), - isVoid: lazyLookup(voids), - isTableSection: lazyLookup(tableSections), - isTableCell: lazyLookup(tableCells), - isBr: isBr + expandRng: expandRng }; } ); - /** - * PaddingBr.js + * MatchFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -17168,261 +17058,216 @@ define( */ define( - 'tinymce.core.dom.PaddingBr', + 'tinymce.core.fmt.MatchFormat', [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.dom.Insert', - 'ephox.sugar.api.dom.Remove', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'ephox.sugar.api.node.Text', - 'ephox.sugar.api.search.SelectorFilter', - 'ephox.sugar.api.search.Traverse', - 'tinymce.core.dom.ElementType' + 'tinymce.core.fmt.FormatUtils' ], - 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; - } + function (FormatUtils) { + var isEq = FormatUtils.isEq; - return children; - }; + var matchesUnInheritedFormatSelector = function (ed, node, name) { + var formatList = ed.formatter.get(name); - 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); + if (formatList) { + for (var i = 0; i < formatList.length; i++) { + if (formatList[i].inherit === false && ed.dom.is(node, formatList[i].selector)) { + return true; + } + } } - }; - var fillWithPaddingBr = function (elm) { - Remove.empty(elm); - Insert.append(elm, Element.fromHtml('
    ')); + return false; }; - var isPaddingContents = function (elm) { - return Node.isText(elm) ? Text.get(elm) === '\u00a0' : ElementType.isBr(elm); - }; + var matchParents = function (editor, node, name, vars) { + var root = editor.dom.getRoot(); - var isPaddedElement = function (elm) { - return Arr.filter(Traverse.children(elm), isPaddingContents).length === 1; - }; + if (node === root) { + return false; + } - var trimBlockTrailingBr = function (elm) { - Traverse.lastChild(elm).each(function (lastChild) { - Traverse.prevSibling(lastChild).each(function (lastChildPrevSibling) { - if (ElementType.isBlock(elm) && ElementType.isBr(lastChild) && ElementType.isBlock(lastChildPrevSibling)) { - Remove.remove(lastChild); - } - }); + // 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); }); - }; - return { - removeTrailingBr: removeTrailingBr, - fillWithPaddingBr: fillWithPaddingBr, - isPaddedElement: isPaddedElement, - trimBlockTrailingBr: trimBlockTrailingBr + // Do an exact check on the similar format element + return matchNode(editor, node, name, vars); }; - } -); -/** - * FormatUtils.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.FormatUtils', - [ - 'tinymce.core.dom.TreeWalker' - ], - function (TreeWalker) { - var isInlineBlock = function (node) { - return node && /^(IMG)$/.test(node.nodeName); - }; + var matchName = function (dom, node, format) { + // Check for inline match + if (isEq(node, format.inline)) { + return true; + } - var moveStart = function (dom, selection, rng) { - var container = rng.startContainer, - offset = rng.startOffset, - walker, node, nodes; + // Check for block match + if (isEq(node, format.block)) { + return true; + } - if (rng.startContainer === rng.endContainer) { - if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { - return; - } + // Check for selector match + if (format.selector) { + return node.nodeType === 1 && dom.is(node, format.selector); } + }; - // 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 = dom.nodeIndex(container); - container = container.parentNode; + 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); } - // 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); - } + // 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); + } - for (node = walker.current(); node; node = walker.next()) { - if (node.nodeType === 3 && !isWhiteSpaceNode(node)) { - rng.setStart(node, 0); - selection.setRng(rng); + if (similar && !value && !format.exact) { + return; + } - 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; }; - /** - * Returns the next/previous non whitespace node. - * - * @private - * @param {Node} node Node to start at. - * @param {boolean} next (Optional) Include next or previous node defaults to previous. - * @param {boolean} inc (Optional) Include the current node in checking. Defaults to false. - * @return {Node} Next or previous node or undefined if it wasn't found. - */ - var getNonWhiteSpaceSibling = function (node, next, inc) { - if (node) { - next = next ? 'nextSibling' : 'previousSibling'; + var matchNode = function (ed, node, name, vars, similar) { + var formatList = ed.formatter.get(name), format, i, x, classes, dom = ed.dom; - for (node = inc ? node : node[next]; node; node = node[next]) { - if (node.nodeType === 1 || !isWhiteSpaceNode(node)) { - return node; + 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 isTextBlock = function (editor, name) { - if (name.nodeType) { - name = name.nodeName; - } - - return !!editor.schema.getTextBlockElements()[name.toLowerCase()]; - }; + var match = function (editor, name, vars, node) { + var startNode; - var isValid = function (ed, parent, child) { - return ed.schema.isValidChild(parent, child); - }; + // Check specified node + if (node) { + return matchParents(editor, node, name, vars); + } - var isWhiteSpaceNode = function (node) { - return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); - }; + // Check selected node + node = editor.selection.getNode(); + if (matchParents(editor, node, name, vars)) { + return true; + } - /** - * 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. - */ - var replaceVars = function (value, vars) { - if (typeof value !== "string") { - value = value(vars); - } else if (vars) { - value = value.replace(/%(\w+)/g, function (str, name) { - return vars[name] || str; - }); + // Check start node if it's different + startNode = editor.selection.getStart(); + if (startNode !== node) { + if (matchParents(editor, startNode, name, vars)) { + return true; + } } - return value; + return false; }; - /** - * 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. - */ - var isEq = function (str1, str2) { - str1 = str1 || ''; - str2 = str2 || ''; - - str1 = '' + (str1.nodeName || str1); - str2 = '' + (str2.nodeName || str2); - - return str1.toLowerCase() === str2.toLowerCase(); - }; + var matchAll = function (editor, names, vars) { + var startElement, matchedFormatNames = [], checkedMap = {}; - var normalizeStyleValue = function (dom, value, name) { - // Force the format to hex - if (name === 'color' || name === 'backgroundColor') { - value = dom.toHex(value); - } + // Check start of selection for formats + startElement = editor.selection.getStart(); + editor.dom.getParent(startElement, function (node) { + var i, name; - // Opera will return bold as 700 - if (name === 'fontWeight' && value === 700) { - value = 'bold'; - } + for (i = 0; i < names.length; i++) { + name = names[i]; - // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" - if (name === 'fontFamily') { - value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); - } + if (!checkedMap[name] && matchNode(editor, node, name, vars)) { + checkedMap[name] = true; + matchedFormatNames.push(name); + } + } + }, editor.dom.getRoot()); - return '' + value; + return matchedFormatNames; }; - var getStyle = function (dom, node, name) { - return normalizeStyleValue(dom, dom.getStyle(node, name), name); - }; + var canApply = function (editor, name) { + var formatList = editor.formatter.get(name), startNode, parents, i, x, selector, dom = editor.dom; - var getTextDecoration = function (dom, node) { - var decoration; + if (formatList) { + startNode = editor.selection.getStart(); + parents = FormatUtils.getParents(dom, startNode); - dom.getParent(node, function (n) { - decoration = dom.getStyle(n, 'text-decoration'); - return decoration && decoration !== 'none'; - }); + for (x = formatList.length - 1; x >= 0; x--) { + selector = formatList[x].selector; - return decoration; - }; + // 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; + } - var getParents = function (dom, node, selector) { - return dom.getParents(node, selector, dom.getRoot()); + for (i = parents.length - 1; i >= 0; i--) { + if (dom.is(parents[i], selector)) { + return true; + } + } + } + } + + return false; }; return { - isInlineBlock: isInlineBlock, - moveStart: moveStart, - getNonWhiteSpaceSibling: getNonWhiteSpaceSibling, - isTextBlock: isTextBlock, - isValid: isValid, - isWhiteSpaceNode: isWhiteSpaceNode, - replaceVars: replaceVars, - isEq: isEq, - normalizeStyleValue: normalizeStyleValue, - getStyle: getStyle, - getTextDecoration: getTextDecoration, - getParents: getParents + matchNode: matchNode, + matchName: matchName, + match: match, + matchAll: matchAll, + canApply: canApply, + matchesUnInheritedFormatSelector: matchesUnInheritedFormatSelector }; } ); /** - * ExpandRange.js + * CaretFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -17432,382 +17277,380 @@ define( */ define( - 'tinymce.core.fmt.ExpandRange', + 'tinymce.core.fmt.CaretFormat', [ - 'tinymce.core.dom.BookmarkManager', + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'tinymce.core.dom.PaddingBr', + 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', - 'tinymce.core.fmt.FormatUtils' + '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 (BookmarkManager, TreeWalker, FormatUtils) { - var isBookmarkNode = BookmarkManager.isBookmarkNode; - var getParents = FormatUtils.getParents, isWhiteSpaceNode = FormatUtils.isWhiteSpaceNode, isTextBlock = FormatUtils.isTextBlock; + function (Arr, Element, PaddingBr, RangeUtils, TreeWalker, ExpandRange, FormatUtils, MatchFormat, Zwsp, Fun, Tools) { + var ZWSP = Zwsp.ZWSP, CARET_ID = '_mce_caret', DEBUG = false; - // 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. - var findLeaf = function (node, offset) { - if (typeof offset === 'undefined') { - offset = node.nodeType === 3 ? node.length : node.childNodes.length; - } + var isCaretNode = function (node) { + return node.nodeType === 1 && node.id === CARET_ID; + }; - while (node && node.hasChildNodes()) { - node = node.childNodes[offset]; - if (node) { - offset = node.nodeType === 3 ? node.length : node.childNodes.length; + 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 { node: node, offset: offset }; + + return true; }; - var excludeTrailingWhitespace = function (endContainer, endOffset) { - // Avoid applying formatting to a trailing space, - // but remove formatting from trailing space - var leaf = findLeaf(endContainer, endOffset); - if (leaf.node) { - while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { - leaf = findLeaf(leaf.node.previousSibling); - } + var findFirstTextNode = function (node) { + var walker; - if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && - leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + if (node) { + walker = new TreeWalker(node, node); - if (leaf.offset > 1) { - endContainer = leaf.node; - endContainer.splitText(leaf.offset - 1); + for (node = walker.current(); node; node = walker.next()) { + if (node.nodeType === 3) { + return node; } } } - return endContainer; + return null; }; - var isBogusBr = function (node) { - return node.nodeName === "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; - }; - - var expandRng = function (editor, rng, format, remove) { - var lastIdx, endPoint, - startContainer = rng.startContainer, - startOffset = rng.startOffset, - endContainer = rng.endContainer, - endOffset = rng.endOffset, - dom = editor.dom; + var createCaretContainer = function (dom, fill) { + var caretContainer = dom.create('span', { id: CARET_ID, 'data-mce-bogus': '1', style: DEBUG ? 'color:red' : '' }); - // This function walks up the tree if there is no siblings before/after the node - var findParentContainer = function (start) { - var container, parent, sibling, siblingName, root; + if (fill) { + caretContainer.appendChild(dom.doc.createTextNode(ZWSP)); + } - container = parent = start ? startContainer : endContainer; - siblingName = start ? 'previousSibling' : 'nextSibling'; - root = dom.getRoot(); + return caretContainer; + }; - // 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; - } + var getParentCaretContainer = function (node) { + while (node) { + if (node.id === CARET_ID) { + return node; } - /*eslint no-constant-condition:0 */ - while (true) { - // Stop expanding on block elements - if (!format[0].block_expand && dom.isBlock(parent)) { - return parent; - } + node = node.parentNode; + } + }; - // Walk left/right - for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { - if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { - return parent; - } - } + // 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; - // Check if we can move up are we at root level or body level - if (parent === root || parent.parentNode === root) { - container = parent; - break; + 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'); + } + }; - parent = parent.parentNode; - } + var trimZwspFromCaretContainer = function (caretContainerNode) { + var textNode = findFirstTextNode(caretContainerNode); + if (textNode && textNode.nodeValue.charAt(0) === ZWSP) { + textNode.deleteData(0, 1); + } - return container; - }; + return textNode; + }; - // 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]; + var removeCaretContainerNode = function (dom, selection, node, moveCaret) { + var rng, block, textNode; - if (startContainer.nodeType === 3) { - startOffset = 0; + rng = selection.getRng(true); + block = dom.getParent(node, dom.isBlock); + + if (isCaretContainerEmpty(node)) { + if (moveCaret !== false) { + rng.setStartBefore(node); + rng.setEndBefore(node); } - } - // 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]; + dom.remove(node); + } else { + textNode = trimZwspFromCaretContainer(node); + if (rng.startContainer === textNode && rng.startOffset > 0) { + rng.setStart(textNode, rng.startOffset - 1); + } - if (endContainer.nodeType === 3) { - endOffset = endContainer.nodeValue.length; + if (rng.endContainer === textNode && rng.endOffset > 0) { + rng.setEnd(textNode, rng.endOffset - 1); } + + dom.remove(node, true); } - // Expands the node to the closes contentEditable false element if it exists - var findParentContentEditable = function (node) { - var parent = node; + if (block && dom.isEmpty(block)) { + PaddingBr.fillWithPaddingBr(Element.fromDom(block)); + } - while (parent) { - if (parent.nodeType === 1 && dom.getContentEditable(parent)) { - return dom.getContentEditable(parent) === "false" ? parent : node; - } + selection.setRng(rng); + }; - parent = parent.parentNode; + // 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); + } + }; - return node; - }; + var insertCaretContainerNode = function (editor, caretContainer, formatNode) { + var dom = editor.dom, block = dom.getParent(formatNode, Fun.curry(FormatUtils.isTextBlock, editor)); - var findWordEndPoint = function (container, offset, start) { - var walker, node, pos, lastTextNode; + 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 findSpace = function (node, offset) { - var pos, pos2, str = node.nodeValue; + var appendNode = function (parentNode, node) { + parentNode.appendChild(node); + return node; + }; - if (typeof offset === "undefined") { - offset = start ? str.length : 0; - } + var insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) { + var innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) { + return appendNode(parentNode, formatNode.cloneNode(false)); + }, caretContainer); - if (start) { - pos = str.lastIndexOf(' ', offset); - pos2 = str.lastIndexOf('\u00a0', offset); - pos = pos > pos2 ? pos : pos2; + return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP)); + }; - // 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; - } + var setupCaretEvents = function (editor) { + if (!editor._hasCaretEvents) { + bindEvents(editor); + editor._hasCaretEvents = true; + } + }; - return pos; - }; + var applyCaretFormat = function (editor, name, vars) { + var rng, caretContainer, textNode, offset, bookmark, container, text; + var dom = editor.dom, selection = editor.selection; - if (container.nodeType === 3) { - pos = findSpace(container, offset); + setupCaretEvents(editor); - if (pos !== -1) { - return { container: container, offset: pos }; - } + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; - lastTextNode = container; - } + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } - // Walk the nodes inside the block - walker = new TreeWalker(container, dom.getParent(container, dom.isBlock) || editor.getBody()); - while ((node = walker[start ? 'prev' : 'next']())) { - if (node.nodeType === 3) { - lastTextNode = node; - pos = findSpace(node); + // 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(); - if (pos !== -1) { - return { container: node, offset: pos }; - } - } else if (dom.isBlock(node)) { - break; - } - } + // Collapse bookmark range (WebKit) + rng.collapse(true); - if (lastTextNode) { - if (start) { - offset = 0; - } else { - offset = lastTextNode.length; - } + // 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); - return { container: lastTextNode, offset: offset }; - } - }; + // Apply the format to the range + editor.formatter.apply(name, vars, rng); - var findSelectorEndPoint = function (container, siblingName) { - var parents, i, y, curFormat; + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== ZWSP) { + caretContainer = createCaretContainer(dom, true); + textNode = caretContainer.firstChild; - if (container.nodeType === 3 && container.nodeValue.length === 0 && container[siblingName]) { - container = container[siblingName]; - } + rng.insertNode(caretContainer); + offset = 1; - parents = getParents(dom, container); - for (i = 0; i < parents.length; i++) { - for (y = 0; y < format.length; y++) { - curFormat = format[y]; + editor.formatter.apply(name, vars, caretContainer); + } else { + editor.formatter.apply(name, vars, caretContainer); + } - // If collapsed state is set then skip formats that doesn't match that - if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { - continue; - } + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } + }; - if (dom.is(parents[i], curFormat.selector)) { - return parents[i]; - } - } - } + 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; - return container; - }; + setupCaretEvents(editor); - var findBlockEndPoint = function (container, siblingName) { - var node, root = dom.getRoot(); + container = rng.startContainer; + offset = rng.startOffset; + node = container; - // Expand to block of similar type - if (!format[0].wrapper) { - node = dom.getParent(container, format[0].block, root); + if (container.nodeType === 3) { + if (offset !== container.nodeValue.length) { + hasContentAfter = true; } - // Expand to first wrappable block element or any block element - if (!node) { - var scopeRoot = dom.getParent(container, 'LI,TD,TH'); - 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(editor, node); - }, scopeRoot); + node = node.parentNode; + } + + while (node) { + if (MatchFormat.matchNode(editor, node, name, vars, similar)) { + formatNode = node; + break; } - // Exclude inner lists from wrapping - if (node && format[0].wrapper) { - node = getParents(dom, node, 'ul,ol').reverse()[0] || node; + if (node.nextSibling) { + hasContentAfter = true; } - // Didn't find a block element look for first/last wrappable element - if (!node) { - node = container; + parents.push(node); + node = node.parentNode; + } - while (node[siblingName] && !dom.isBlock(node[siblingName])) { - node = node[siblingName]; + // Node doesn't have the specified format + if (!formatNode) { + return; + } - // 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 (FormatUtils.isEq(node, 'br')) { - break; - } - } - } + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + bookmark = selection.getBookmark(); - return node || container; - }; + // Collapse bookmark range (WebKit) + rng.collapse(true); - // Expand to closest contentEditable element - startContainer = findParentContentEditable(startContainer); - endContainer = findParentContentEditable(endContainer); + // 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); - // Exclude bookmark nodes if possible - if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { - startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; - startContainer = startContainer.nextSibling || startContainer; + editor.formatter.remove(name, vars, rng); + selection.moveToBookmark(bookmark); + } else { + caretContainer = getParentCaretContainer(formatNode); + var newCaretContainer = createCaretContainer(dom, false); + var caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer); - if (startContainer.nodeType === 3) { - startOffset = 0; + if (caretContainer) { + insertCaretContainerNode(editor, newCaretContainer, caretContainer); + } else { + insertCaretContainerNode(editor, newCaretContainer, formatNode); } - } - if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { - endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; - endContainer = endContainer.previousSibling || endContainer; + removeCaretContainerNode(dom, selection, caretContainer, false); + selection.setCursorLocation(caretNode, 1); - if (endContainer.nodeType === 3) { - endOffset = endContainer.length; + if (dom.isEmpty(formatNode)) { + dom.remove(formatNode); } } + }; - 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; - } + var bindEvents = function (editor) { + var dom = editor.dom, selection = editor.selection; - // Expand right to closest word boundary - endPoint = findWordEndPoint(endContainer, endOffset); - if (endPoint) { - endContainer = endPoint.container; - endOffset = endPoint.offset; + if (!editor._hasCaretEvents) { + var markCaretContainersBogus, disableCaretContainer; + + editor.on('BeforeGetContent', function (e) { + if (markCaretContainersBogus && e.format !== 'raw') { + markCaretContainersBogus(); } - } + }); - endContainer = remove ? endContainer : excludeTrailingWhitespace(endContainer, endOffset); - } + editor.on('mouseup keydown', function (e) { + if (disableCaretContainer) { + disableCaretContainer(e); + } + }); - // 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); - } + // 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 (!format[0].inline || (endContainer.nodeType !== 3 || endOffset === endContainer.nodeValue.length)) { - endContainer = findParentContainer(); - } - } + if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { + // Mark children + i = nodes.length; + while (i--) { + dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + } + } + }; - // 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'); - } + disableCaretContainer = function (e) { + var keyCode = e.keyCode; - // 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'); + removeCaretContainer(dom, selection, null, false); - // Non block element then try to expand up the leaf - if (format[0].block) { - if (!dom.isBlock(startContainer)) { - startContainer = findParentContainer(true); + // Remove caret container if it's empty + if (keyCode === 8 && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) { + removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); } - if (!dom.isBlock(endContainer)) { - endContainer = findParentContainer(); + // Remove caret container on keydown and it's left/right arrow keys + if (keyCode === 37 || keyCode === 39) { + removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); } - } - } - // Setup index for startContainer - if (startContainer.nodeType === 1) { - startOffset = dom.nodeIndex(startContainer); - startContainer = startContainer.parentNode; - } + unmarkBogusCaretParents(dom, selection); + }; - // Setup index for endContainer - if (endContainer.nodeType === 1) { - endOffset = dom.nodeIndex(endContainer) + 1; - endContainer = endContainer.parentNode; + // 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 new range like object - return { - startContainer: startContainer, - startOffset: startOffset, - endContainer: endContainer, - endOffset: endOffset - }; }; return { - expandRng: expandRng + applyCaretFormat: applyCaretFormat, + removeCaretFormat: removeCaretFormat, + isCaretNode: isCaretNode }; } ); /** - * MatchFormat.js + * Hooks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -17816,217 +17659,192 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Internal class for overriding formatting. + * + * @private + * @class tinymce.fmt.Hooks + */ define( - 'tinymce.core.fmt.MatchFormat', + 'tinymce.core.fmt.Hooks', [ - 'tinymce.core.fmt.FormatUtils' + "tinymce.core.util.Arr", + "tinymce.core.dom.NodeType", + "tinymce.core.dom.DomQuery" ], - function (FormatUtils) { - var isEq = FormatUtils.isEq; + function (Arr, NodeType, $) { + var postProcessHooks = {}, filter = Arr.filter, each = Arr.each; - var matchesUnInheritedFormatSelector = function (ed, node, name) { - var formatList = ed.formatter.get(name); + function addPostProcessHook(name, hook) { + var hooks = postProcessHooks[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; - } - } + if (!hooks) { + postProcessHooks[name] = hooks = []; } - 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; - } + postProcessHooks[name].push(hook); + } - return node.parentNode === root || !!matchNode(editor, node, name, vars, true); + function postProcess(name, editor) { + each(postProcessHooks[name], function (hook) { + hook(editor); }); + } - // 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; - } + addPostProcessHook("pre", function (editor) { + var rng = editor.selection.getRng(), isPre, blocks; - // Check for selector match - if (format.selector) { - return node.nodeType === 1 && dom.is(node, format.selector); + function hasPreSibling(pre) { + return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) !== -1; } - }; - - 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); + function joinPre(pre1, pre2) { + $(pre2).remove(); + $(pre1).append('

    ').append(pre2.childNodes); } - // 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); - } + isPre = NodeType.matchNodeNames('pre'); - if (similar && !value && !format.exact) { - return; - } + if (!rng.collapsed) { + blocks = editor.selection.getSelectedBlocks(); - 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; - } - } - } + each(filter(filter(blocks, isPre), hasPreSibling), function (pre) { + joinPre(pre.previousSibling, pre); + }); } + }); - return format; + return { + postProcess: postProcess }; + } +); - 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]; +/** + * ElementUtils.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 + */ - // 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; - } - } - } +/** + * Utility class for various element specific functions. + * + * @private + * @class tinymce.dom.ElementUtils + */ +define( + 'tinymce.core.dom.ElementUtils', + [ + "tinymce.core.dom.BookmarkManager", + "tinymce.core.util.Tools" + ], + function (BookmarkManager, Tools) { + var each = Tools.each; - return format; - } + function ElementUtils(dom) { + /** + * Compares two nodes and checks if it's attributes and styles matches. + * This doesn't compare classes as items since their order is significant. + * + * @method compare + * @param {Node} node1 First node to compare with. + * @param {Node} node2 Second node to compare with. + * @return {boolean} True/false if the nodes are the same or not. + */ + this.compare = function (node1, node2) { + // Not the same name + if (node1.nodeName != node2.nodeName) { + return false; } - } - }; - var match = function (editor, name, vars, node) { - var startNode; + /** + * Returns all the nodes attributes excluding internal ones, styles and classes. + * + * @private + * @param {Node} node Node to get attributes from. + * @return {Object} Name/value object with attributes and attribute values. + */ + function getAttribs(node) { + var attribs = {}; - // Check specified node - if (node) { - return matchParents(editor, node, name, vars); - } + each(dom.getAttribs(node), function (attr) { + var name = attr.nodeName.toLowerCase(); - // Check selected node - node = editor.selection.getNode(); - if (matchParents(editor, node, name, vars)) { - return true; - } + // Don't compare internal attributes or style + if (name.indexOf('_') !== 0 && name !== 'style' && name.indexOf('data-') !== 0) { + attribs[name] = dom.getAttrib(node, name); + } + }); - // Check start node if it's different - startNode = editor.selection.getStart(); - if (startNode !== node) { - if (matchParents(editor, startNode, name, vars)) { - return true; + return attribs; } - } - return false; - }; + /** + * Compares two objects checks if it's key + value exists in the other one. + * + * @private + * @param {Object} obj1 First object to compare. + * @param {Object} obj2 Second object to compare. + * @return {boolean} True/false if the objects matches or not. + */ + function compareObjects(obj1, obj2) { + var value, name; - var matchAll = function (editor, names, vars) { - var startElement, matchedFormatNames = [], checkedMap = {}; + for (name in obj1) { + // Obj1 has item obj2 doesn't have + if (obj1.hasOwnProperty(name)) { + value = obj2[name]; - // Check start of selection for formats - startElement = editor.selection.getStart(); - editor.dom.getParent(startElement, function (node) { - var i, name; + // Obj2 doesn't have obj1 item + if (typeof value == "undefined") { + return false; + } - for (i = 0; i < names.length; i++) { - name = names[i]; + // Obj2 item has a different value + if (obj1[name] != value) { + return false; + } - if (!checkedMap[name] && matchNode(editor, node, name, vars)) { - checkedMap[name] = true; - matchedFormatNames.push(name); + // Delete similar value + delete obj2[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); + // Check if obj 2 has something obj 1 doesn't have + for (name in obj2) { + // Obj2 has item obj1 doesn't have + if (obj2.hasOwnProperty(name)) { + return false; + } + } - for (x = formatList.length - 1; x >= 0; x--) { - selector = formatList[x].selector; + return true; + } - // 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; - } + // Attribs are not the same + if (!compareObjects(getAttribs(node1), getAttribs(node2))) { + return false; + } - for (i = parents.length - 1; i >= 0; i--) { - if (dom.is(parents[i], selector)) { - return true; - } - } + // Styles are not the same + if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { + return false; } - } - return false; - }; + return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2); + }; + } - return { - matchNode: matchNode, - matchName: matchName, - match: match, - matchAll: matchAll, - canApply: canApply, - matchesUnInheritedFormatSelector: matchesUnInheritedFormatSelector - }; + return ElementUtils; } ); + /** - * CaretFormat.js + * RemoveFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -18036,451 +17854,544 @@ define( */ define( - 'tinymce.core.fmt.CaretFormat', + 'tinymce.core.fmt.RemoveFormat', [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.node.Element', - 'tinymce.core.dom.PaddingBr', + 'ephox.katamari.api.Fun', + 'tinymce.core.dom.BookmarkManager', 'tinymce.core.dom.RangeUtils', 'tinymce.core.dom.TreeWalker', + 'tinymce.core.fmt.CaretFormat', '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; + function (Fun, BookmarkManager, RangeUtils, TreeWalker, CaretFormat, ExpandRange, FormatUtils, MatchFormat, Tools) { + var MCE_ATTR_RE = /^(src|href|style)$/; + var each = Tools.each; + var isEq = FormatUtils.isEq; - var isCaretNode = function (node) { - return node.nodeType === 1 && node.id === CARET_ID; + var isTableCell = function (node) { + return /^(TH|TD)$/.test(node.nodeName); }; - var isCaretContainerEmpty = function (node, nodes) { - while (node) { - if ((node.nodeType === 3 && node.nodeValue !== ZWSP) || node.childNodes.length > 1) { - return false; - } + var getContainer = function (ed, rng, start) { + var container, offset, lastIdx; - // Collect nodes - if (nodes && node.nodeType === 1) { - nodes.push(node); - } + container = rng[start ? 'startContainer' : 'endContainer']; + offset = rng[start ? 'startOffset' : 'endOffset']; - node = node.firstChild; - } + if (container.nodeType === 1) { + lastIdx = container.childNodes.length - 1; - return true; - }; + if (!start && offset) { + offset--; + } - var findFirstTextNode = function (node) { - var walker; + container = container.childNodes[offset > lastIdx ? lastIdx : offset]; + } - if (node) { - walker = new TreeWalker(node, node); + // If start text node is excluded then walk to the next node + if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { + container = new TreeWalker(container, ed.getBody()).next() || container; + } - for (node = walker.current(); node; node = walker.next()) { - if (node.nodeType === 3) { - return node; - } - } + // If end text node is excluded then walk to the previous node + if (container.nodeType === 3 && !start && offset === 0) { + container = new TreeWalker(container, ed.getBody()).prev() || container; } - return null; + return container; }; - var createCaretContainer = function (dom, fill) { - var caretContainer = dom.create('span', { id: CARET_ID, 'data-mce-bogus': '1', style: DEBUG ? 'color:red' : '' }); + var wrap = function (dom, node, name, attrs) { + var wrapper = dom.create(name, attrs); - if (fill) { - caretContainer.appendChild(dom.doc.createTextNode(ZWSP)); - } + node.parentNode.insertBefore(wrapper, node); + wrapper.appendChild(node); - return caretContainer; + return wrapper; }; - var getParentCaretContainer = function (node) { - while (node) { - if (node.id === CARET_ID) { - return node; - } - - node = node.parentNode; + /** + * 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. + */ + var matchName = function (dom, node, format) { + // Check for inline match + if (isEq(node, format.inline)) { + return true; } - }; - - // 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'); + // Check for block match + if (isEq(node, format.block)) { + return true; } - }; - var trimZwspFromCaretContainer = function (caretContainerNode) { - var textNode = findFirstTextNode(caretContainerNode); - if (textNode && textNode.nodeValue.charAt(0) === ZWSP) { - textNode.deleteData(0, 1); + // Check for selector match + if (format.selector) { + return node.nodeType === 1 && dom.is(node, format.selector); } - - return textNode; }; - var removeCaretContainerNode = function (dom, selection, node, moveCaret) { - var rng, block, textNode; + var isColorFormatAndAnchor = function (node, format) { + return format.links && node.tagName === 'A'; + }; - rng = selection.getRng(true); - block = dom.getParent(node, dom.isBlock); + var find = function (dom, node, next, inc) { + node = FormatUtils.getNonWhiteSpaceSibling(node, next, inc); + return !node || (node.nodeName === 'BR' || dom.isBlock(node)); + }; - if (isCaretContainerEmpty(node)) { - if (moveCaret !== false) { - rng.setStartBefore(node); - rng.setEndBefore(node); - } + /** + * 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
    text
    text + * + * Output becomes: + * text

    text
    text + * + * So when the div is removed the result is: + * text
    text
    text + * + * @private + * @param {Node} node Node to remove + apply BR/P elements to. + * @param {Object} format Format rule. + * @return {Node} Input node. + */ + var removeNode = function (ed, node, format) { + var parentNode = node.parentNode, rootBlockElm; + var dom = ed.dom, forcedRootBlock = ed.settings.forced_root_block; - dom.remove(node); - } else { - textNode = trimZwspFromCaretContainer(node); - if (rng.startContainer === textNode && rng.startOffset > 0) { - rng.setStart(textNode, rng.startOffset - 1); - } + if (format.block) { + if (!forcedRootBlock) { + // Append BR elements if needed before we remove the block + if (dom.isBlock(node) && !dom.isBlock(parentNode)) { + if (!find(dom, node, false) && !find(dom, node.firstChild, true, 1)) { + node.insertBefore(dom.create('br'), node.firstChild); + } - if (rng.endContainer === textNode && rng.endOffset > 0) { - rng.setEnd(textNode, rng.endOffset - 1); + if (!find(dom, node, true) && !find(dom, node.lastChild, false, 1)) { + node.appendChild(dom.create('br')); + } + } + } else { + // Wrap the block in a forcedRootBlock if we are at the root of document + if (parentNode === dom.getRoot()) { + if (!format.list_block || !isEq(node, format.list_block)) { + each(Tools.grep(node.childNodes), function (node) { + if (FormatUtils.isValid(ed, forcedRootBlock, node.nodeName.toLowerCase())) { + if (!rootBlockElm) { + rootBlockElm = wrap(dom, node, forcedRootBlock); + dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs); + } else { + rootBlockElm.appendChild(node); + } + } else { + rootBlockElm = 0; + } + }); + } + } } - - dom.remove(node, true); } - if (block && dom.isEmpty(block)) { - PaddingBr.fillWithPaddingBr(Element.fromDom(block)); + // Never remove nodes that isn't the specified inline element if a selector is specified too + if (format.selector && format.inline && !isEq(format.inline, node)) { + return; } - selection.setRng(rng); + dom.remove(node, 1); }; - // 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()); + /** + * 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. + */ + var removeFormat = function (ed, format, vars, node, compareNode) { + var i, attrs, stylesModified, dom = ed.dom; - if (!node) { - while ((node = dom.get(CARET_ID))) { - removeCaretContainerNode(dom, selection, node, false); - } - } - } else { - removeCaretContainerNode(dom, selection, node, moveCaret); + // Check if node matches format + if (!matchName(dom, node, format) && !isColorFormatAndAnchor(node, format)) { + return false; } - }; - - 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); - } - } - }; + // Should we compare with format attribs and styles + if (format.remove !== 'all') { + // Remove styles + each(format.styles, function (value, name) { + value = FormatUtils.normalizeStyleValue(dom, FormatUtils.replaceVars(value, vars), name); - var appendNode = function (parentNode, node) { - parentNode.appendChild(node); - return node; - }; + // Indexed array + if (typeof name === 'number') { + name = value; + compareNode = 0; + } - var insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) { - var innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) { - return appendNode(parentNode, formatNode.cloneNode(false)); - }, caretContainer); + if (format.remove_similar || (!compareNode || isEq(FormatUtils.getStyle(dom, compareNode, name), value))) { + dom.setStyle(node, name, ''); + } - return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP)); - }; + stylesModified = 1; + }); - var setupCaretEvents = function (editor) { - if (!editor._hasCaretEvents) { - bindEvents(editor); - editor._hasCaretEvents = true; - } - }; + // Remove style attribute if it's empty + if (stylesModified && dom.getAttrib(node, 'style') === '') { + node.removeAttribute('style'); + node.removeAttribute('data-mce-style'); + } - var applyCaretFormat = function (editor, name, vars) { - var rng, caretContainer, textNode, offset, bookmark, container, text; - var dom = editor.dom, selection = editor.selection; + // Remove attributes + each(format.attributes, function (value, name) { + var valueOut; - setupCaretEvents(editor); + value = FormatUtils.replaceVars(value, vars); - rng = selection.getRng(true); - offset = rng.startOffset; - container = rng.startContainer; - text = container.nodeValue; + // Indexed array + if (typeof name === 'number') { + name = value; + compareNode = 0; + } - caretContainer = getParentCaretContainer(selection.getStart()); - if (caretContainer) { - textNode = findFirstTextNode(caretContainer); - } + 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; + } + }); - // 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(); + // We got some internal classes left + if (valueOut) { + dom.setAttrib(node, name, valueOut); + return; + } + } + } - // Collapse bookmark range (WebKit) - rng.collapse(true); + // IE6 has a bug where the attribute doesn't get removed correctly + if (name === "class") { + node.removeAttribute('className'); + } - // 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); + // Remove mce prefixed attributes + if (MCE_ATTR_RE.test(name)) { + node.removeAttribute('data-mce-' + name); + } - // Apply the format to the range - editor.formatter.apply(name, vars, rng); + node.removeAttribute(name); + } + }); - // Move selection back to caret position - selection.moveToBookmark(bookmark); - } else { - if (!caretContainer || textNode.nodeValue !== ZWSP) { - caretContainer = createCaretContainer(dom, true); - textNode = caretContainer.firstChild; + // Remove classes + each(format.classes, function (value) { + value = FormatUtils.replaceVars(value, vars); - rng.insertNode(caretContainer); - offset = 1; + if (!compareNode || dom.hasClass(compareNode, value)) { + dom.removeClass(node, value); + } + }); - editor.formatter.apply(name, vars, caretContainer); - } else { - editor.formatter.apply(name, vars, caretContainer); + // 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; + } } + } - // Move selection to text node - selection.setCursorLocation(textNode, offset); + // Remove the inline child if it's empty for example or + if (format.remove !== 'none') { + removeNode(ed, node, format); + return true; } }; - 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; - } + var findFormatRoot = function (editor, container, name, vars, similar) { + var formatRoot; - node = node.parentNode; - } + // Find format root + each(FormatUtils.getParents(editor.dom, container.parentNode).reverse(), function (parent) { + var format; - while (node) { - if (MatchFormat.matchNode(editor, node, name, vars, similar)) { - formatNode = node; - break; + // Find format root element + if (!formatRoot && parent.id !== '_start' && parent.id !== '_end') { + // Is the node matching the format we are looking for + format = MatchFormat.matchNode(editor, parent, name, vars, similar); + if (format && format.split !== false) { + formatRoot = parent; + } } + }); - if (node.nextSibling) { - hasContentAfter = true; - } + return formatRoot; + }; - parents.push(node); - node = node.parentNode; - } + var wrapAndSplit = function (editor, formatList, formatRoot, container, target, split, format, vars) { + var parent, clone, lastClone, firstClone, i, formatRootParent, dom = editor.dom; - // Node doesn't have the specified format - if (!formatNode) { - return; - } + // Format root found then clone formats and split it + if (formatRoot) { + formatRootParent = formatRoot.parentNode; - // Is there contents after the caret then remove the format on the element - if (hasContentAfter) { - bookmark = selection.getBookmark(); + for (parent = container.parentNode; parent && parent !== formatRootParent; parent = parent.parentNode) { + clone = dom.clone(parent, false); - // Collapse bookmark range (WebKit) - rng.collapse(true); + for (i = 0; i < formatList.length; i++) { + if (removeFormat(editor, formatList[i], vars, clone, clone)) { + clone = 0; + break; + } + } - // 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); + // Build wrapper node + if (clone) { + if (lastClone) { + clone.appendChild(lastClone); + } - editor.formatter.remove(name, vars, rng); - selection.moveToBookmark(bookmark); - } else { - caretContainer = getParentCaretContainer(formatNode); - var newCaretContainer = createCaretContainer(dom, false); - var caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer); + if (!firstClone) { + firstClone = clone; + } - if (caretContainer) { - insertCaretContainerNode(editor, newCaretContainer, caretContainer); - } else { - insertCaretContainerNode(editor, newCaretContainer, formatNode); + lastClone = clone; + } } - removeCaretContainerNode(dom, selection, caretContainer, false); - selection.setCursorLocation(caretNode, 1); + // Never split block elements if the format is mixed + if (split && (!format.mixed || !dom.isBlock(formatRoot))) { + container = dom.split(formatRoot, container); + } - if (dom.isEmpty(formatNode)) { - dom.remove(formatNode); + // Wrap container in cloned formats + if (lastClone) { + target.parentNode.insertBefore(lastClone, target); + firstClone.appendChild(target); } } + + return container; }; - var bindEvents = function (editor) { - var dom = editor.dom, selection = editor.selection; + var remove = function (ed, name, vars, node, similar) { + var formatList = ed.formatter.get(name), format = formatList[0]; + var bookmark, rng, contentEditable = true, dom = ed.dom, selection = ed.selection; - if (!editor._hasCaretEvents) { - var markCaretContainersBogus, disableCaretContainer; + var splitToFormatRoot = function (container) { + var formatRoot = findFormatRoot(ed, container, name, vars, similar); + return wrapAndSplit(ed, formatList, formatRoot, container, container, true, format, vars); + }; - editor.on('BeforeGetContent', function (e) { - if (markCaretContainersBogus && e.format !== 'raw') { - markCaretContainersBogus(); - } - }); + // Merges the styles for each node + var process = function (node) { + var children, i, l, lastContentEditable, hasContentEditableState; - editor.on('mouseup keydown', function (e) { - if (disableCaretContainer) { - disableCaretContainer(e); + // Node has a contentEditable value + if (node.nodeType === 1 && dom.getContentEditable(node)) { + lastContentEditable = contentEditable; + contentEditable = dom.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 = Tools.grep(node.childNodes); + + // Process current node + if (contentEditable && !hasContentEditableState) { + for (i = 0, l = formatList.length; i < l; i++) { + if (removeFormat(ed, formatList[i], vars, node, node)) { + break; + } } - }); + } - // 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; + // Process the children + if (format.deep) { + if (children.length) { + for (i = 0, l = children.length; i < l; i++) { + process(children[i]); + } - if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { - // Mark children - i = nodes.length; - while (i--) { - dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack } } - }; + } + }; - disableCaretContainer = function (e) { - var keyCode = e.keyCode; + var unwrap = function (start) { + var node = dom.get(start ? '_start' : '_end'), + out = node[start ? 'firstChild' : 'lastChild']; - removeCaretContainer(dom, selection, null, false); + // 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 (BookmarkManager.isBookmarkNode(out)) { + out = out[start ? 'firstChild' : 'lastChild']; + } - // Remove caret container if it's empty - if (keyCode === 8 && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) { - removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); - } + // 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; + } - // Remove caret container on keydown and it's left/right arrow keys - if (keyCode === 37 || keyCode === 39) { - removeCaretContainer(dom, selection, getParentCaretContainer(selection.getStart())); - } + dom.remove(node, true); - unmarkBogusCaretParents(dom, selection); - }; + return out; + }; - // 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; - } - }; + var removeRngStyle = function (rng) { + var startContainer, endContainer; + var commonAncestorContainer = rng.commonAncestorContainer; - 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 - */ + rng = ExpandRange.expandRng(ed, rng, formatList, true); -/** - * 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; + if (format.split) { + startContainer = getContainer(ed, rng, true); + endContainer = getContainer(ed, rng); - function addPostProcessHook(name, hook) { - var hooks = postProcessHooks[name]; + 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; + } + } - if (!hooks) { - postProcessHooks[name] = hooks = []; - } + // 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; + } - postProcessHooks[name].push(hook); - } + if (dom.isChildOf(startContainer, endContainer) && !dom.isBlock(endContainer) && + !isTableCell(startContainer) && !isTableCell(endContainer)) { + startContainer = wrap(dom, startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); + splitToFormatRoot(startContainer); + startContainer = unwrap(true); + return; + } - function postProcess(name, editor) { - each(postProcessHooks[name], function (hook) { - hook(editor); - }); - } + // Wrap start/end nodes in span element since these might be cloned/moved + startContainer = wrap(dom, startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); + endContainer = wrap(dom, endContainer, 'span', { id: '_end', 'data-mce-type': 'bookmark' }); - addPostProcessHook("pre", function (editor) { - var rng = editor.selection.getRng(), isPre, blocks; + // Split start/end + splitToFormatRoot(startContainer); + splitToFormatRoot(endContainer); - function hasPreSibling(pre) { - return isPre(pre.previousSibling) && Arr.indexOf(blocks, pre.previousSibling) !== -1; + // 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 = dom.nodeIndex(startContainer); + rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; + rng.endOffset = dom.nodeIndex(endContainer) + 1; + } + + // Remove items between start/end + new RangeUtils(dom).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 && FormatUtils.getTextDecoration(dom, node.parentNode) === 'underline') { + removeFormat(ed, { + '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; } - function joinPre(pre1, pre2) { - $(pre2).remove(); - $(pre1).append('

    ').append(pre2.childNodes); + if (dom.getContentEditable(selection.getNode()) === "false") { + node = selection.getNode(); + for (var i = 0, l = formatList.length; i < l; i++) { + if (formatList[i].ceFalseOverride) { + if (removeFormat(ed, formatList[i], vars, node, node)) { + break; + } + } + } + + return; } - isPre = NodeType.matchNodeNames('pre'); + 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); - if (!rng.collapsed) { - blocks = editor.selection.getSelectedBlocks(); + // 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 && MatchFormat.match(ed, name, vars, selection.getStart())) { + FormatUtils.moveStart(dom, selection, selection.getRng(true)); + } - each(filter(filter(blocks, isPre), hasPreSibling), function (pre) { - joinPre(pre.previousSibling, pre); - }); + ed.nodeChanged(); + } else { + CaretFormat.removeCaretFormat(ed, name, vars, similar); } - }); + }; return { - postProcess: postProcess + removeFormat: removeFormat, + remove: remove }; } ); - /** - * ElementUtils.js + * MergeFormats.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -18489,121 +18400,222 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Utility class for various element specific functions. - * - * @private - * @class tinymce.dom.ElementUtils - */ define( - 'tinymce.core.dom.ElementUtils', + 'tinymce.core.fmt.MergeFormats', [ - "tinymce.core.dom.BookmarkManager", - "tinymce.core.util.Tools" + 'ephox.katamari.api.Fun', + 'tinymce.core.dom.BookmarkManager', + 'tinymce.core.dom.ElementUtils', + 'tinymce.core.dom.NodeType', + 'tinymce.core.fmt.CaretFormat', + 'tinymce.core.fmt.FormatUtils', + 'tinymce.core.fmt.MatchFormat', + 'tinymce.core.fmt.RemoveFormat', + 'tinymce.core.util.Tools' ], - function (BookmarkManager, Tools) { + function (Fun, BookmarkManager, ElementUtils, NodeType, CaretFormat, FormatUtils, MatchFormat, RemoveFormat, Tools) { var each = Tools.each; - function ElementUtils(dom) { - /** - * Compares two nodes and checks if it's attributes and styles matches. - * This doesn't compare classes as items since their order is significant. - * - * @method compare - * @param {Node} node1 First node to compare with. - * @param {Node} node2 Second node to compare with. - * @return {boolean} True/false if the nodes are the same or not. - */ - this.compare = function (node1, node2) { - // Not the same name - if (node1.nodeName != node2.nodeName) { - return false; + var isElementNode = function (node) { + return node && node.nodeType === 1 && !BookmarkManager.isBookmarkNode(node) && !CaretFormat.isCaretNode(node) && !NodeType.isBogus(node); + }; + + var findElementSibling = function (node, siblingName) { + var sibling; + + for (sibling = node; sibling; sibling = sibling[siblingName]) { + if (sibling.nodeType === 3 && sibling.nodeValue.length !== 0) { + return node; } - /** - * Returns all the nodes attributes excluding internal ones, styles and classes. - * - * @private - * @param {Node} node Node to get attributes from. - * @return {Object} Name/value object with attributes and attribute values. - */ - function getAttribs(node) { - var attribs = {}; + if (sibling.nodeType === 1 && !BookmarkManager.isBookmarkNode(sibling)) { + return sibling; + } + } - each(dom.getAttribs(node), function (attr) { - var name = attr.nodeName.toLowerCase(); + return node; + }; - // Don't compare internal attributes or style - if (name.indexOf('_') !== 0 && name !== 'style' && name.indexOf('data-') !== 0) { - attribs[name] = dom.getAttrib(node, name); - } + var mergeSiblingsNodes = function (dom, prev, next) { + var sibling, tmpSibling, elementUtils = new ElementUtils(dom); + + // Check if next/prev exists and that they are elements + if (prev && next) { + // If previous sibling is empty then jump over it + prev = findElementSibling(prev, 'previousSibling'); + next = findElementSibling(next, 'nextSibling'); + + // Compare next and previous nodes + if (elementUtils.compare(prev, next)) { + // Append nodes between + for (sibling = prev.nextSibling; sibling && sibling !== next;) { + tmpSibling = sibling; + sibling = sibling.nextSibling; + prev.appendChild(tmpSibling); + } + + dom.remove(next); + + Tools.each(Tools.grep(next.childNodes), function (node) { + prev.appendChild(node); }); - return attribs; + return prev; } + } - /** - * Compares two objects checks if it's key + value exists in the other one. - * - * @private - * @param {Object} obj1 First object to compare. - * @param {Object} obj2 Second object to compare. - * @return {boolean} True/false if the objects matches or not. - */ - function compareObjects(obj1, obj2) { - var value, name; + return next; + }; - for (name in obj1) { - // Obj1 has item obj2 doesn't have - if (obj1.hasOwnProperty(name)) { - value = obj2[name]; + var processChildElements = function (node, filter, process) { + each(node.childNodes, function (node) { + if (isElementNode(node)) { + if (filter(node)) { + process(node); + } + if (node.hasChildNodes()) { + processChildElements(node, filter, process); + } + } + }); + }; - // Obj2 doesn't have obj1 item - if (typeof value == "undefined") { - return false; - } + var hasStyle = function (dom, name) { + return Fun.curry(function (name, node) { + return !!(node && FormatUtils.getStyle(dom, node, name)); + }, name); + }; - // Obj2 item has a different value - if (obj1[name] != value) { - return false; - } + var applyStyle = function (dom, name, value) { + return Fun.curry(function (name, value, node) { + dom.setStyle(node, name, value); - // Delete similar value - delete obj2[name]; - } + if (node.getAttribute('style') === '') { + node.removeAttribute('style'); + } + + unwrapEmptySpan(dom, node); + }, name, value); + }; + + var unwrapEmptySpan = function (dom, node) { + if (node.nodeName === 'SPAN' && dom.getAttribs(node).length === 0) { + dom.remove(node, true); + } + }; + + var processUnderlineAndColor = function (dom, node) { + var textDecoration; + if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { + textDecoration = FormatUtils.getTextDecoration(dom, node.parentNode); + if (dom.getStyle(node, 'color') && textDecoration) { + dom.setStyle(node, 'text-decoration', textDecoration); + } else if (dom.getStyle(node, 'text-decoration') === textDecoration) { + dom.setStyle(node, 'text-decoration', null); + } + } + }; + + var mergeUnderlineAndColor = function (dom, format, vars, node) { + // Colored nodes should be underlined so that the color of the underline matches the text color. + if (format.styles.color || format.styles.textDecoration) { + Tools.walk(node, Fun.curry(processUnderlineAndColor, dom), 'childNodes'); + processUnderlineAndColor(dom, node); + } + }; + + var mergeBackgroundColorAndFontSize = function (dom, format, vars, node) { + // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882) + if (format.styles && format.styles.backgroundColor) { + processChildElements(node, + hasStyle(dom, 'fontSize'), + applyStyle(dom, 'backgroundColor', FormatUtils.replaceVars(format.styles.backgroundColor, vars)) + ); + } + }; + + var mergeSubSup = function (dom, format, vars, node) { + // Remove font size on all chilren of a sub/sup and remove the inverse element + if (format.inline === 'sub' || format.inline === 'sup') { + processChildElements(node, + hasStyle(dom, 'fontSize'), + applyStyle(dom, 'fontSize', '') + ); + + dom.remove(dom.select(format.inline === 'sup' ? 'sub' : 'sup', node), true); + } + }; + + var mergeSiblings = function (dom, format, vars, node) { + // Merge next and previous siblings if they are similar texttext becomes texttext + if (node && format.merge_siblings !== false) { + node = mergeSiblingsNodes(dom, FormatUtils.getNonWhiteSpaceSibling(node), node); + node = mergeSiblingsNodes(dom, node, FormatUtils.getNonWhiteSpaceSibling(node, true)); + } + }; + + var clearChildStyles = function (dom, format, node) { + if (format.clear_child_styles) { + var selector = format.links ? '*:not(a)' : '*'; + each(dom.select(selector, node), function (node) { + if (isElementNode(node)) { + each(format.styles, function (value, name) { + dom.setStyle(node, name, ''); + }); } + }); + } + }; - // Check if obj 2 has something obj 1 doesn't have - for (name in obj2) { - // Obj2 has item obj1 doesn't have - if (obj2.hasOwnProperty(name)) { - return false; - } + var mergeWithChildren = function (editor, formatList, vars, 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(editor.dom.select(format.inline, node), function (child) { + if (!isElementNode(child)) { + return; } - return true; - } + RemoveFormat.removeFormat(editor, format, vars, child, format.exact ? child : null); + }); - // Attribs are not the same - if (!compareObjects(getAttribs(node1), getAttribs(node2))) { - return false; - } + clearChildStyles(editor.dom, format, node); + }); + }; - // Styles are not the same - if (!compareObjects(dom.parseStyle(dom.getAttrib(node1, 'style')), dom.parseStyle(dom.getAttrib(node2, 'style')))) { - return false; + var mergeWithParents = function (editor, format, name, vars, node) { + // Remove format if direct parent already has the same format + if (MatchFormat.matchNode(editor, node.parentNode, name, vars)) { + if (RemoveFormat.removeFormat(editor, format, vars, node)) { + return; } + } - return !BookmarkManager.isBookmarkNode(node1) && !BookmarkManager.isBookmarkNode(node2); - }; - } + // Remove format if any ancestor already has the same format + if (format.merge_with_parents) { + editor.dom.getParent(node.parentNode, function (parent) { + if (MatchFormat.matchNode(editor, parent, name, vars)) { + RemoveFormat.removeFormat(editor, format, vars, node); + return true; + } + }); + } + }; - return ElementUtils; + return { + mergeWithChildren: mergeWithChildren, + mergeUnderlineAndColor: mergeUnderlineAndColor, + mergeBackgroundColorAndFontSize: mergeBackgroundColorAndFontSize, + mergeSubSup: mergeSubSup, + mergeSiblings: mergeSiblings, + mergeWithParents: mergeWithParents + }; } ); - /** - * RemoveFormat.js + * ApplyFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -18613,544 +18625,356 @@ define( */ define( - 'tinymce.core.fmt.RemoveFormat', + 'tinymce.core.fmt.ApplyFormat', [ - 'ephox.katamari.api.Fun', 'tinymce.core.dom.BookmarkManager', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeNormalizer', 'tinymce.core.dom.RangeUtils', - 'tinymce.core.dom.TreeWalker', 'tinymce.core.fmt.CaretFormat', 'tinymce.core.fmt.ExpandRange', 'tinymce.core.fmt.FormatUtils', + 'tinymce.core.fmt.Hooks', 'tinymce.core.fmt.MatchFormat', + 'tinymce.core.fmt.MergeFormats', 'tinymce.core.util.Tools' ], - function (Fun, BookmarkManager, RangeUtils, TreeWalker, CaretFormat, ExpandRange, FormatUtils, MatchFormat, Tools) { - var MCE_ATTR_RE = /^(src|href|style)$/; + function (BookmarkManager, NodeType, RangeNormalizer, RangeUtils, CaretFormat, ExpandRange, FormatUtils, Hooks, MatchFormat, MergeFormats, Tools) { var each = Tools.each; - var isEq = FormatUtils.isEq; - var isTableCell = function (node) { - return /^(TH|TD)$/.test(node.nodeName); + var isElementNode = function (node) { + return node && node.nodeType === 1 && !BookmarkManager.isBookmarkNode(node) && !CaretFormat.isCaretNode(node) && !NodeType.isBogus(node); }; - var getContainer = function (ed, rng, start) { - var container, offset, lastIdx; - - container = rng[start ? 'startContainer' : 'endContainer']; - offset = rng[start ? 'startOffset' : 'endOffset']; - - if (container.nodeType === 1) { - lastIdx = container.childNodes.length - 1; - - if (!start && offset) { - offset--; + var processChildElements = function (node, filter, process) { + each(node.childNodes, function (node) { + if (isElementNode(node)) { + if (filter(node)) { + process(node); + } + if (node.hasChildNodes()) { + processChildElements(node, filter, process); + } } - - container = container.childNodes[offset > lastIdx ? lastIdx : offset]; - } - - // If start text node is excluded then walk to the next node - if (container.nodeType === 3 && start && offset >= container.nodeValue.length) { - container = new TreeWalker(container, ed.getBody()).next() || container; - } - - // If end text node is excluded then walk to the previous node - if (container.nodeType === 3 && !start && offset === 0) { - container = new TreeWalker(container, ed.getBody()).prev() || container; - } - - return container; - }; - - var wrap = function (dom, node, name, attrs) { - var wrapper = dom.create(name, attrs); - - node.parentNode.insertBefore(wrapper, node); - wrapper.appendChild(node); - - return wrapper; + }); }; - /** - * 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. - */ - 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); - } - }; + var applyFormat = function (ed, name, vars, node) { + var formatList = ed.formatter.get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && ed.selection.isCollapsed(); + var dom = ed.dom, selection = ed.selection; - var isColorFormatAndAnchor = function (node, format) { - return format.links && node.tagName === 'A'; - }; + var setElementFormat = function (elm, fmt) { + fmt = fmt || format; - var find = function (dom, node, next, inc) { - node = FormatUtils.getNonWhiteSpaceSibling(node, next, inc); - return !node || (node.nodeName === 'BR' || dom.isBlock(node)); - }; + if (elm) { + if (fmt.onformat) { + fmt.onformat(elm, fmt, vars, node); + } - /** - * 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
    text
    text - * - * Output becomes: - * text

    text
    text - * - * So when the div is removed the result is: - * text
    text
    text - * - * @private - * @param {Node} node Node to remove + apply BR/P elements to. - * @param {Object} format Format rule. - * @return {Node} Input node. - */ - var removeNode = function (ed, node, format) { - var parentNode = node.parentNode, rootBlockElm; - var dom = ed.dom, forcedRootBlock = ed.settings.forced_root_block; + each(fmt.styles, function (value, name) { + dom.setStyle(elm, name, FormatUtils.replaceVars(value, vars)); + }); - if (format.block) { - if (!forcedRootBlock) { - // Append BR elements if needed before we remove the block - if (dom.isBlock(node) && !dom.isBlock(parentNode)) { - if (!find(dom, node, false) && !find(dom, node.firstChild, true, 1)) { - node.insertBefore(dom.create('br'), node.firstChild); - } + // Needed for the WebKit span spam bug + // TODO: Remove this once WebKit/Blink fixes this + if (fmt.styles) { + var styleVal = dom.getAttrib(elm, 'style'); - if (!find(dom, node, true) && !find(dom, node.lastChild, false, 1)) { - node.appendChild(dom.create('br')); - } - } - } else { - // Wrap the block in a forcedRootBlock if we are at the root of document - if (parentNode === dom.getRoot()) { - if (!format.list_block || !isEq(node, format.list_block)) { - each(Tools.grep(node.childNodes), function (node) { - if (FormatUtils.isValid(ed, forcedRootBlock, node.nodeName.toLowerCase())) { - if (!rootBlockElm) { - rootBlockElm = wrap(dom, node, forcedRootBlock); - dom.setAttribs(rootBlockElm, ed.settings.forced_root_block_attrs); - } else { - rootBlockElm.appendChild(node); - } - } else { - rootBlockElm = 0; - } - }); + if (styleVal) { + elm.setAttribute('data-mce-style', styleVal); } } - } - } - // Never remove nodes that isn't the specified inline element if a selector is specified too - if (format.selector && format.inline && !isEq(format.inline, node)) { - return; - } + each(fmt.attributes, function (value, name) { + dom.setAttrib(elm, name, FormatUtils.replaceVars(value, vars)); + }); - dom.remove(node, 1); - }; + each(fmt.classes, function (value) { + value = FormatUtils.replaceVars(value, vars); - /** - * 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. - */ - var removeFormat = function (ed, format, vars, node, compareNode) { - var i, attrs, stylesModified, dom = ed.dom; + if (!dom.hasClass(elm, value)) { + dom.addClass(elm, value); + } + }); + } + }; - // Check if node matches format - if (!matchName(dom, node, format) && !isColorFormatAndAnchor(node, format)) { - return false; - } + var applyNodeStyle = function (formatList, node) { + var found = false; - // Should we compare with format attribs and styles - if (format.remove !== 'all') { - // Remove styles - each(format.styles, function (value, name) { - value = FormatUtils.normalizeStyleValue(dom, FormatUtils.replaceVars(value, vars), name); + if (!format.selector) { + return false; + } - // Indexed array - if (typeof name === 'number') { - name = value; - compareNode = 0; + // Look for matching formats + each(formatList, function (format) { + // Check collapsed state if it exists + if ('collapsed' in format && format.collapsed !== isCollapsed) { + return; } - if (format.remove_similar || (!compareNode || isEq(FormatUtils.getStyle(dom, compareNode, name), value))) { - dom.setStyle(node, name, ''); + if (dom.is(node, format.selector) && !CaretFormat.isCaretNode(node)) { + setElementFormat(node, format); + found = true; + return false; } - - stylesModified = 1; }); - // Remove style attribute if it's empty - if (stylesModified && dom.getAttrib(node, 'style') === '') { - node.removeAttribute('style'); - node.removeAttribute('data-mce-style'); - } + return found; + }; - // Remove attributes - each(format.attributes, function (value, name) { - var valueOut; + var applyRngStyle = function (dom, rng, bookmark, nodeSpecific) { + var newWrappers = [], wrapName, wrapElm, contentEditable = true; - value = FormatUtils.replaceVars(value, vars); + // Setup wrapper element + wrapName = format.inline || format.block; + wrapElm = dom.create(wrapName); + setElementFormat(wrapElm); - // 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; - } - }); + new RangeUtils(dom).walk(rng, function (nodes) { + var currentWrapElm; - // We got some internal classes left - if (valueOut) { - dom.setAttrib(node, name, valueOut); - return; - } - } - } + /** + * Process a list of nodes wrap them. + */ + var process = function (node) { + var nodeName, parentName, hasContentEditableState, lastContentEditable; - // IE6 has a bug where the attribute doesn't get removed correctly - if (name === "class") { - node.removeAttribute('className'); - } + lastContentEditable = contentEditable; + nodeName = node.nodeName.toLowerCase(); + parentName = node.parentNode.nodeName.toLowerCase(); - // Remove mce prefixed attributes - if (MCE_ATTR_RE.test(name)) { - node.removeAttribute('data-mce-' + name); + // Node has a contentEditable value + if (node.nodeType === 1 && dom.getContentEditable(node)) { + lastContentEditable = contentEditable; + contentEditable = dom.getContentEditable(node) === "true"; + hasContentEditableState = true; // We don't want to wrap the container only it's children } - node.removeAttribute(name); - } - }); - - // Remove classes - each(format.classes, function (value) { - value = FormatUtils.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(ed, node, format); - return true; - } - }; - - var findFormatRoot = function (editor, container, name, vars, similar) { - var formatRoot; - - // Find format root - each(FormatUtils.getParents(editor.dom, 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 = MatchFormat.matchNode(editor, parent, name, vars, similar); - if (format && format.split !== false) { - formatRoot = parent; - } - } - }); - - return formatRoot; - }; - - var wrapAndSplit = function (editor, formatList, formatRoot, container, target, split, format, vars) { - var parent, clone, lastClone, firstClone, i, formatRootParent, dom = editor.dom; - - // Format root found then clone formats and split it - if (formatRoot) { - formatRootParent = formatRoot.parentNode; + // Stop wrapping on br elements + if (FormatUtils.isEq(nodeName, 'br')) { + currentWrapElm = 0; - for (parent = container.parentNode; parent && parent !== formatRootParent; parent = parent.parentNode) { - clone = dom.clone(parent, false); + // Remove any br elements when we wrap things + if (format.block) { + dom.remove(node); + } - for (i = 0; i < formatList.length; i++) { - if (removeFormat(editor, formatList[i], vars, clone, clone)) { - clone = 0; - break; + return; } - } - // Build wrapper node - if (clone) { - if (lastClone) { - clone.appendChild(lastClone); + // If node is wrapper type + if (format.wrapper && MatchFormat.matchNode(ed, node, name, vars)) { + currentWrapElm = 0; + return; } - if (!firstClone) { - firstClone = clone; + // Can we rename the block + // TODO: Break this if up, too complex + if (contentEditable && !hasContentEditableState && format.block && + !format.wrapper && FormatUtils.isTextBlock(ed, nodeName) && FormatUtils.isValid(ed, parentName, wrapName)) { + node = dom.rename(node, wrapName); + setElementFormat(node); + newWrappers.push(node); + currentWrapElm = 0; + return; } - lastClone = clone; - } - } - - // Never split block elements if the format is mixed - if (split && (!format.mixed || !dom.isBlock(formatRoot))) { - container = dom.split(formatRoot, container); - } - - // Wrap container in cloned formats - if (lastClone) { - target.parentNode.insertBefore(lastClone, target); - firstClone.appendChild(target); - } - } - - return container; - }; + // Handle selector patterns + if (format.selector) { + var found = applyNodeStyle(formatList, node); - var remove = function (ed, name, vars, node, similar) { - var formatList = ed.formatter.get(name), format = formatList[0]; - var bookmark, rng, contentEditable = true, dom = ed.dom, selection = ed.selection; + // Continue processing if a selector match wasn't found and a inline element is defined + if (!format.inline || found) { + currentWrapElm = 0; + return; + } + } - var splitToFormatRoot = function (container) { - var formatRoot = findFormatRoot(ed, container, name, vars, similar); - return wrapAndSplit(ed, formatList, formatRoot, container, container, true, format, vars); - }; + // Is it valid to wrap this item + // TODO: Break this if up, too complex + if (contentEditable && !hasContentEditableState && FormatUtils.isValid(ed, wrapName, nodeName) && FormatUtils.isValid(ed, parentName, wrapName) && + !(!nodeSpecific && node.nodeType === 3 && + node.nodeValue.length === 1 && + node.nodeValue.charCodeAt(0) === 65279) && + !CaretFormat.isCaretNode(node) && + (!format.inline || !dom.isBlock(node))) { + // Start wrapping + if (!currentWrapElm) { + // Wrap the node + currentWrapElm = dom.clone(wrapElm, false); + node.parentNode.insertBefore(currentWrapElm, node); + newWrappers.push(currentWrapElm); + } - // Merges the styles for each node - var process = function (node) { - var children, i, l, lastContentEditable, hasContentEditableState; + currentWrapElm.appendChild(node); + } else { + // Start a new wrapper for possible children + currentWrapElm = 0; - // Node has a contentEditable value - if (node.nodeType === 1 && dom.getContentEditable(node)) { - lastContentEditable = contentEditable; - contentEditable = dom.getContentEditable(node) === "true"; - hasContentEditableState = true; // We don't want to wrap the container only it's children - } + each(Tools.grep(node.childNodes), process); - // Grab the children first since the nodelist might be changed - children = Tools.grep(node.childNodes); + if (hasContentEditableState) { + contentEditable = lastContentEditable; // Restore last contentEditable state from stack + } - // Process current node - if (contentEditable && !hasContentEditableState) { - for (i = 0, l = formatList.length; i < l; i++) { - if (removeFormat(ed, formatList[i], vars, node, node)) { - break; + // End the last wrapper + currentWrapElm = 0; } - } - } + }; - // Process the children - if (format.deep) { - if (children.length) { - for (i = 0, l = children.length; i < l; i++) { - process(children[i]); - } + // Process siblings from range + each(nodes, process); + }); - if (hasContentEditableState) { - contentEditable = lastContentEditable; // Restore last contentEditable state from stack - } - } - } - }; + // 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) { + var process = function (node) { + if (node.nodeName === 'A') { + setElementFormat(node, format); + } - var unwrap = function (start) { - var node = dom.get(start ? '_start' : '_end'), - out = node[start ? 'firstChild' : 'lastChild']; + each(Tools.grep(node.childNodes), process); + }; - // 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 (BookmarkManager.isBookmarkNode(out)) { - out = out[start ? 'firstChild' : 'lastChild']; + process(node); + }); } - // 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; - } + // Cleanup + each(newWrappers, function (node) { + var childCount; - dom.remove(node, true); + var getChildCount = function (node) { + var count = 0; - return out; - }; + each(node.childNodes, function (node) { + if (!FormatUtils.isWhiteSpaceNode(node) && !BookmarkManager.isBookmarkNode(node)) { + count++; + } + }); - var removeRngStyle = function (rng) { - var startContainer, endContainer; - var commonAncestorContainer = rng.commonAncestorContainer; + return count; + }; - rng = ExpandRange.expandRng(ed, rng, formatList, true); + var getChildElementNode = function (root) { + var child = false; + each(root.childNodes, function (node) { + if (isElementNode(node)) { + child = node; + return false; // break loop + } + }); + return child; + }; - if (format.split) { - startContainer = getContainer(ed, rng, true); - endContainer = getContainer(ed, rng); + var mergeStyles = function (node) { + var child, clone; - 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; - } - } + child = getChildElementNode(node); - // 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 child was found and of the same type as the current node + if (child && !BookmarkManager.isBookmarkNode(child) && MatchFormat.matchName(dom, child, format)) { + clone = dom.clone(child, false); + setElementFormat(clone); - if (dom.isChildOf(startContainer, endContainer) && !dom.isBlock(endContainer) && - !isTableCell(startContainer) && !isTableCell(endContainer)) { - startContainer = wrap(dom, startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); - splitToFormatRoot(startContainer); - startContainer = unwrap(true); - return; + dom.replace(clone, node, true); + dom.remove(child, 1); } - // Wrap start/end nodes in span element since these might be cloned/moved - startContainer = wrap(dom, startContainer, 'span', { id: '_start', 'data-mce-type': 'bookmark' }); - endContainer = wrap(dom, endContainer, 'span', { id: '_end', 'data-mce-type': 'bookmark' }); + return clone || node; + }; - // Split start/end - splitToFormatRoot(startContainer); - splitToFormatRoot(endContainer); + childCount = getChildCount(node); - // Unwrap start/end to get real elements again - startContainer = unwrap(true); - endContainer = unwrap(); - } else { - startContainer = endContainer = splitToFormatRoot(startContainer); + // 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 || !dom.isBlock(node)) && childCount === 0) { + dom.remove(node, 1); + return; } - // Update range positions since they might have changed after the split operations - rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; - rng.startOffset = dom.nodeIndex(startContainer); - rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; - rng.endOffset = dom.nodeIndex(endContainer) + 1; - } - - // Remove items between start/end - new RangeUtils(dom).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 && FormatUtils.getTextDecoration(dom, node.parentNode) === 'underline') { - removeFormat(ed, { - 'deep': false, - 'exact': true, - 'inline': 'span', - 'styles': { - 'textDecoration': 'underline' - } - }, null, node); + 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); } - }); + + MergeFormats.mergeWithChildren(ed, formatList, vars, node); + MergeFormats.mergeWithParents(ed, format, name, vars, node); + MergeFormats.mergeBackgroundColorAndFontSize(dom, format, vars, node); + MergeFormats.mergeSubSup(dom, format, vars, node); + MergeFormats.mergeSiblings(dom, format, vars, node); + } }); }; - // Handle node - if (node) { - if (node.nodeType) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - removeRngStyle(rng); - } else { - removeRngStyle(node); - } - - return; - } - if (dom.getContentEditable(selection.getNode()) === "false") { node = selection.getNode(); for (var i = 0, l = formatList.length; i < l; i++) { - if (formatList[i].ceFalseOverride) { - if (removeFormat(ed, formatList[i], vars, node, node)) { - break; - } + if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) { + setElementFormat(node, formatList[i]); + return; } } 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); + if (format) { + if (node) { + if (node.nodeType) { + if (!applyNodeStyle(formatList, node)) { + rng = dom.createRng(); + rng.setStartBefore(node); + rng.setEndAfter(node); + applyRngStyle(dom, ExpandRange.expandRng(ed, rng, formatList), null, true); + } + } else { + applyRngStyle(dom, 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(); - // 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 && MatchFormat.match(ed, name, vars, selection.getStart())) { - FormatUtils.moveStart(dom, selection, selection.getRng(true)); + // 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 (!ed.settings.forced_root_block && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { + applyFormat(ed, formatList[0].defaultBlock); + } + + // Apply formatting to selection + ed.selection.setRng(RangeNormalizer.normalize(ed.selection.getRng())); + bookmark = selection.getBookmark(); + applyRngStyle(dom, ExpandRange.expandRng(ed, selection.getRng(true), formatList), bookmark); + + if (format.styles) { + MergeFormats.mergeUnderlineAndColor(dom, format, vars, curSelNode); + } + + selection.moveToBookmark(bookmark); + FormatUtils.moveStart(dom, selection, selection.getRng(true)); + ed.nodeChanged(); + } else { + CaretFormat.applyCaretFormat(ed, name, vars); + } } - ed.nodeChanged(); - } else { - CaretFormat.removeCaretFormat(ed, name, vars, similar); + Hooks.postProcess(name, ed); } }; return { - removeFormat: removeFormat, - remove: remove + applyFormat: applyFormat }; } ); /** - * MergeFormats.js + * FormatChanged.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -19160,221 +18984,300 @@ define( */ define( - 'tinymce.core.fmt.MergeFormats', + 'tinymce.core.fmt.FormatChanged', [ - 'ephox.katamari.api.Fun', - 'tinymce.core.dom.BookmarkManager', - 'tinymce.core.dom.ElementUtils', - 'tinymce.core.dom.NodeType', - 'tinymce.core.fmt.CaretFormat', + 'ephox.katamari.api.Cell', 'tinymce.core.fmt.FormatUtils', 'tinymce.core.fmt.MatchFormat', - 'tinymce.core.fmt.RemoveFormat', 'tinymce.core.util.Tools' ], - function (Fun, BookmarkManager, ElementUtils, NodeType, CaretFormat, FormatUtils, MatchFormat, RemoveFormat, Tools) { + function (Cell, FormatUtils, MatchFormat, Tools) { var each = Tools.each; - var isElementNode = function (node) { - return node && node.nodeType === 1 && !BookmarkManager.isBookmarkNode(node) && !CaretFormat.isCaretNode(node) && !NodeType.isBogus(node); - }; - - var findElementSibling = function (node, siblingName) { - var sibling; + var setup = function (formatChangeData, editor) { + var currentFormats = {}; - for (sibling = node; sibling; sibling = sibling[siblingName]) { - if (sibling.nodeType === 3 && sibling.nodeValue.length !== 0) { - return node; - } + formatChangeData.set({}); - if (sibling.nodeType === 1 && !BookmarkManager.isBookmarkNode(sibling)) { - return sibling; - } - } + editor.on('NodeChange', function (e) { + var parents = FormatUtils.getParents(editor.dom, e.element), matchedFormats = {}; - return node; - }; + // Ignore bogus nodes like the tag created by moveStart() + parents = Tools.grep(parents, function (node) { + return node.nodeType === 1 && !node.getAttribute('data-mce-bogus'); + }); - var mergeSiblingsNodes = function (dom, prev, next) { - var sibling, tmpSibling, elementUtils = new ElementUtils(dom); + // Check for new formats + each(formatChangeData.get(), function (callbacks, format) { + each(parents, function (node) { + if (editor.formatter.matchNode(node, format, {}, callbacks.similar)) { + if (!currentFormats[format]) { + // Execute callbacks + each(callbacks, function (callback) { + callback(true, { node: node, format: format, parents: parents }); + }); - // Check if next/prev exists and that they are elements - if (prev && next) { - // If previous sibling is empty then jump over it - prev = findElementSibling(prev, 'previousSibling'); - next = findElementSibling(next, 'nextSibling'); + currentFormats[format] = callbacks; + } - // Compare next and previous nodes - if (elementUtils.compare(prev, next)) { - // Append nodes between - for (sibling = prev.nextSibling; sibling && sibling !== next;) { - tmpSibling = sibling; - sibling = sibling.nextSibling; - prev.appendChild(tmpSibling); - } + matchedFormats[format] = callbacks; + return false; + } - dom.remove(next); - - Tools.each(Tools.grep(next.childNodes), function (node) { - prev.appendChild(node); + if (MatchFormat.matchesUnInheritedFormatSelector(editor, node, format)) { + return false; + } }); + }); - return prev; - } - } - - return next; - }; + // Check if current formats still match + each(currentFormats, function (callbacks, format) { + if (!matchedFormats[format]) { + delete currentFormats[format]; - var processChildElements = function (node, filter, process) { - each(node.childNodes, function (node) { - if (isElementNode(node)) { - if (filter(node)) { - process(node); - } - if (node.hasChildNodes()) { - processChildElements(node, filter, process); + each(callbacks, function (callback) { + callback(false, { node: e.element, format: format, parents: parents }); + }); } - } + }); }); }; - var hasStyle = function (dom, name) { - return Fun.curry(function (name, node) { - return !!(node && FormatUtils.getStyle(dom, node, name)); - }, name); - }; - - var applyStyle = function (dom, name, value) { - return Fun.curry(function (name, value, node) { - dom.setStyle(node, name, value); + var addListeners = function (formatChangeData, formats, callback, similar) { + var formatChangeItems = formatChangeData.get(); - if (node.getAttribute('style') === '') { - node.removeAttribute('style'); + each(formats.split(','), function (format) { + if (!formatChangeItems[format]) { + formatChangeItems[format] = []; + formatChangeItems[format].similar = similar; } - unwrapEmptySpan(dom, node); - }, name, value); - }; + formatChangeItems[format].push(callback); + }); - var unwrapEmptySpan = function (dom, node) { - if (node.nodeName === 'SPAN' && dom.getAttribs(node).length === 0) { - dom.remove(node, true); - } + formatChangeData.set(formatChangeItems); }; - var processUnderlineAndColor = function (dom, node) { - var textDecoration; - if (node.nodeType === 1 && node.parentNode && node.parentNode.nodeType === 1) { - textDecoration = FormatUtils.getTextDecoration(dom, node.parentNode); - if (dom.getStyle(node, 'color') && textDecoration) { - dom.setStyle(node, 'text-decoration', textDecoration); - } else if (dom.getStyle(node, 'text-decoration') === textDecoration) { - dom.setStyle(node, 'text-decoration', null); - } + var formatChanged = function (editor, formatChangeState, formats, callback, similar) { + if (formatChangeState.get() === null) { + setup(formatChangeState, editor); } - }; - var mergeUnderlineAndColor = function (dom, format, vars, node) { - // Colored nodes should be underlined so that the color of the underline matches the text color. - if (format.styles.color || format.styles.textDecoration) { - Tools.walk(node, Fun.curry(processUnderlineAndColor, dom), 'childNodes'); - processUnderlineAndColor(dom, node); - } + addListeners(formatChangeState, formats, callback, similar); }; - var mergeBackgroundColorAndFontSize = function (dom, format, vars, node) { - // nodes with font-size should have their own background color as well to fit the line-height (see TINY-882) - if (format.styles && format.styles.backgroundColor) { - processChildElements(node, - hasStyle(dom, 'fontSize'), - applyStyle(dom, 'backgroundColor', FormatUtils.replaceVars(format.styles.backgroundColor, vars)) - ); - } + return { + formatChanged: formatChanged }; + } +); +/** + * DefaultFormats.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 + */ - var mergeSubSup = function (dom, format, vars, node) { - // Remove font size on all chilren of a sub/sup and remove the inverse element - if (format.inline === 'sub' || format.inline === 'sup') { - processChildElements(node, - hasStyle(dom, 'fontSize'), - applyStyle(dom, 'fontSize', '') - ); +define( + 'tinymce.core.fmt.DefaultFormats', + [ + 'tinymce.core.util.Tools' + ], + function (Tools) { + var get = function (dom) { + var formats = { + valigntop: [ + { selector: 'td,th', styles: { 'verticalAlign': 'top' } } + ], - dom.remove(dom.select(format.inline === 'sup' ? 'sub' : 'sup', node), true); - } - }; + valignmiddle: [ + { selector: 'td,th', styles: { 'verticalAlign': 'middle' } } + ], - var mergeSiblings = function (dom, format, vars, node) { - // Merge next and previous siblings if they are similar texttext becomes texttext - if (node && format.merge_siblings !== false) { - node = mergeSiblingsNodes(dom, FormatUtils.getNonWhiteSpaceSibling(node), node); - node = mergeSiblingsNodes(dom, node, FormatUtils.getNonWhiteSpaceSibling(node, true)); - } - }; + valignbottom: [ + { selector: 'td,th', styles: { 'verticalAlign': 'bottom' } } + ], - var clearChildStyles = function (dom, format, node) { - if (format.clear_child_styles) { - var selector = format.links ? '*:not(a)' : '*'; - each(dom.select(selector, node), function (node) { - if (isElementNode(node)) { - each(format.styles, function (value, name) { - dom.setStyle(node, name, ''); - }); + alignleft: [ + { + selector: 'figure.image', + collapsed: false, + classes: 'align-left', + ceFalseOverride: true, + preview: 'font-family font-size' + }, + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'left' + }, + inherit: false, + preview: false, + defaultBlock: 'div' + }, + { + selector: 'img,table', + collapsed: false, + styles: { + 'float': 'left' + }, + preview: 'font-family font-size' } - }); - } - }; + ], - var mergeWithChildren = function (editor, formatList, vars, 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(editor.dom.select(format.inline, node), function (child) { - if (!isElementNode(child)) { - return; + aligncenter: [ + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'center' + }, + inherit: false, + preview: 'font-family font-size', + defaultBlock: 'div' + }, + { + selector: 'figure.image', + collapsed: false, + classes: 'align-center', + ceFalseOverride: true, + preview: 'font-family font-size' + }, + { + selector: 'img', + collapsed: false, + styles: { + display: 'block', + marginLeft: 'auto', + marginRight: 'auto' + }, + preview: false + }, + { + selector: 'table', + collapsed: false, + styles: { + marginLeft: 'auto', + marginRight: 'auto' + }, + preview: 'font-family font-size' } + ], - RemoveFormat.removeFormat(editor, format, vars, child, format.exact ? child : null); - }); + alignright: [ + { + selector: 'figure.image', + collapsed: false, + classes: 'align-right', + ceFalseOverride: true, + preview: 'font-family font-size' + }, + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'right' + }, + inherit: false, + preview: 'font-family font-size', + defaultBlock: 'div' + }, + { + selector: 'img,table', + collapsed: false, + styles: { + 'float': 'right' + }, + preview: 'font-family font-size' + } + ], - clearChildStyles(editor.dom, format, node); - }); - }; + alignjustify: [ + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'justify' + }, + inherit: false, + defaultBlock: 'div', + preview: 'font-family font-size' + } + ], - var mergeWithParents = function (editor, format, name, vars, node) { - // Remove format if direct parent already has the same format - if (MatchFormat.matchNode(editor, node.parentNode, name, vars)) { - if (RemoveFormat.removeFormat(editor, format, vars, node)) { - return; - } - } + bold: [ + { inline: 'strong', remove: 'all' }, + { inline: 'span', styles: { fontWeight: 'bold' } }, + { inline: 'b', remove: 'all' } + ], - // Remove format if any ancestor already has the same format - if (format.merge_with_parents) { - editor.dom.getParent(node.parentNode, function (parent) { - if (MatchFormat.matchNode(editor, parent, name, vars)) { - RemoveFormat.removeFormat(editor, format, vars, node); + italic: [ + { inline: 'em', remove: 'all' }, + { inline: 'span', styles: { fontStyle: 'italic' } }, + { inline: 'i', remove: 'all' } + ], + + underline: [ + { inline: 'span', styles: { textDecoration: 'underline' }, exact: true }, + { inline: 'u', remove: 'all' } + ], + + strikethrough: [ + { inline: 'span', styles: { textDecoration: 'line-through' }, exact: true }, + { inline: 'strike', remove: 'all' } + ], + + forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, + hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, + fontname: { inline: 'span', styles: { fontFamily: '%value' }, clear_child_styles: true }, + fontsize: { inline: 'span', styles: { fontSize: '%value' }, clear_child_styles: true }, + fontsize_class: { inline: 'span', attributes: { 'class': '%value' } }, + blockquote: { block: 'blockquote', wrapper: 1, remove: 'all' }, + subscript: { inline: 'sub' }, + superscript: { inline: 'sup' }, + code: { inline: 'code' }, + + link: { + inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, + onmatch: function () { return true; + }, + + onformat: function (elm, fmt, vars) { + Tools.each(vars, function (value, key) { + dom.setAttrib(elm, key, value); + }); } - }); - } + }, + + removeformat: [ + { + selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins', + remove: 'all', + split: true, + expand: false, + block_expand: true, + deep: true + }, + { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true }, + { selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true } + ] + }; + + Tools.each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function (name) { + formats[name] = { block: name, remove: 'all' }; + }); + + return formats; }; return { - mergeWithChildren: mergeWithChildren, - mergeUnderlineAndColor: mergeUnderlineAndColor, - mergeBackgroundColorAndFontSize: mergeBackgroundColorAndFontSize, - mergeSubSup: mergeSubSup, - mergeSiblings: mergeSiblings, - mergeWithParents: mergeWithParents + get: get }; } ); /** - * ApplyFormat.js + * FormatRegistry.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -19384,454 +19287,433 @@ define( */ define( - 'tinymce.core.fmt.ApplyFormat', + 'tinymce.core.fmt.FormatRegistry', [ - 'tinymce.core.dom.BookmarkManager', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangeNormalizer', - 'tinymce.core.dom.RangeUtils', - 'tinymce.core.fmt.CaretFormat', - 'tinymce.core.fmt.ExpandRange', - 'tinymce.core.fmt.FormatUtils', - 'tinymce.core.fmt.Hooks', - 'tinymce.core.fmt.MatchFormat', - 'tinymce.core.fmt.MergeFormats', + 'tinymce.core.fmt.DefaultFormats', 'tinymce.core.util.Tools' ], - function (BookmarkManager, NodeType, RangeNormalizer, RangeUtils, CaretFormat, ExpandRange, FormatUtils, Hooks, MatchFormat, MergeFormats, Tools) { - var each = Tools.each; + function (DefaultFormats, Tools) { + return function (editor) { + var formats = {}; - var isElementNode = function (node) { - return node && node.nodeType === 1 && !BookmarkManager.isBookmarkNode(node) && !CaretFormat.isCaretNode(node) && !NodeType.isBogus(node); - }; + var get = function (name) { + return name ? formats[name] : formats; + }; - var processChildElements = function (node, filter, process) { - each(node.childNodes, function (node) { - if (isElementNode(node)) { - if (filter(node)) { - process(node); - } - if (node.hasChildNodes()) { - processChildElements(node, filter, process); - } - } - }); - }; + var register = function (name, format) { + if (name) { + if (typeof name !== 'string') { + Tools.each(name, function (format, name) { + register(name, format); + }); + } else { + // Force format into array and add it to internal collection + format = format.length ? format : [format]; - var applyFormat = function (ed, name, vars, node) { - var formatList = ed.formatter.get(name), format = formatList[0], bookmark, rng, isCollapsed = !node && ed.selection.isCollapsed(); - var dom = ed.dom, selection = ed.selection; + Tools.each(format, function (format) { + // Set deep to false by default on selector formats this to avoid removing + // alignment on images inside paragraphs when alignment is changed on paragraphs + if (typeof format.deep === 'undefined') { + format.deep = !format.selector; + } - var setElementFormat = function (elm, fmt) { - fmt = fmt || format; + // Default to true + if (typeof format.split === 'undefined') { + format.split = !format.selector || format.inline; + } - if (elm) { - if (fmt.onformat) { - fmt.onformat(elm, fmt, vars, node); - } + // Default to true + if (typeof format.remove === 'undefined' && format.selector && !format.inline) { + format.remove = 'none'; + } - each(fmt.styles, function (value, name) { - dom.setStyle(elm, name, FormatUtils.replaceVars(value, vars)); - }); + // Mark format as a mixed format inline + block level + if (format.selector && format.inline) { + format.mixed = true; + format.block_expand = true; + } - // Needed for the WebKit span spam bug - // TODO: Remove this once WebKit/Blink fixes this - if (fmt.styles) { - var styleVal = dom.getAttrib(elm, 'style'); + // Split classes if needed + if (typeof format.classes === 'string') { + format.classes = format.classes.split(/\s+/); + } + }); - if (styleVal) { - elm.setAttribute('data-mce-style', styleVal); - } + formats[name] = format; } - - each(fmt.attributes, function (value, name) { - dom.setAttrib(elm, name, FormatUtils.replaceVars(value, vars)); - }); - - each(fmt.classes, function (value) { - value = FormatUtils.replaceVars(value, vars); - - if (!dom.hasClass(elm, value)) { - dom.addClass(elm, value); - } - }); } }; - var applyNodeStyle = function (formatList, node) { - var found = false; - - if (!format.selector) { - return false; + var unregister = function (name) { + if (name && formats[name]) { + delete formats[name]; } - // Look for matching formats - each(formatList, function (format) { - // Check collapsed state if it exists - if ('collapsed' in format && format.collapsed !== isCollapsed) { - return; - } + return formats; + }; - if (dom.is(node, format.selector) && !CaretFormat.isCaretNode(node)) { - setElementFormat(node, format); - found = true; - return false; - } - }); + register(DefaultFormats.get(editor.dom)); + register(editor.settings.formats); - return found; + return { + get: get, + register: register, + unregister: unregister }; + }; + } +); - var applyRngStyle = function (dom, rng, bookmark, nodeSpecific) { - var newWrappers = [], wrapName, wrapElm, contentEditable = true; +/** + * Preview.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 + */ - // Setup wrapper element - wrapName = format.inline || format.block; - wrapElm = dom.create(wrapName); - setElementFormat(wrapElm); +/** + * Internal class for generating previews styles for formats. + * + * Example: + * Preview.getCssText(editor, 'bold'); + * + * @private + * @class tinymce.fmt.Preview + */ +define( + 'tinymce.core.fmt.Preview', + [ + "tinymce.core.dom.DOMUtils", + "tinymce.core.util.Tools", + "tinymce.core.html.Schema" + ], + function (DOMUtils, Tools, Schema) { + var each = Tools.each; + var dom = DOMUtils.DOM; - new RangeUtils(dom).walk(rng, function (nodes) { - var currentWrapElm; + function parsedSelectorToHtml(ancestry, editor) { + var elm, item, fragment; + var schema = editor && editor.schema || new Schema({}); - /** - * Process a list of nodes wrap them. - */ - var process = function (node) { - var nodeName, parentName, hasContentEditableState, lastContentEditable; + function decorate(elm, item) { + if (item.classes.length) { + dom.addClass(elm, item.classes.join(' ')); + } + dom.setAttribs(elm, item.attrs); + } - lastContentEditable = contentEditable; - nodeName = node.nodeName.toLowerCase(); - parentName = node.parentNode.nodeName.toLowerCase(); + function createElement(sItem) { + var elm; - // Node has a contentEditable value - if (node.nodeType === 1 && dom.getContentEditable(node)) { - lastContentEditable = contentEditable; - contentEditable = dom.getContentEditable(node) === "true"; - hasContentEditableState = true; // We don't want to wrap the container only it's children - } + item = typeof sItem === 'string' ? { + name: sItem, + classes: [], + attrs: {} + } : sItem; - // Stop wrapping on br elements - if (FormatUtils.isEq(nodeName, 'br')) { - currentWrapElm = 0; + elm = dom.create(item.name); + decorate(elm, item); + return elm; + } - // Remove any br elements when we wrap things - if (format.block) { - dom.remove(node); - } + function getRequiredParent(elm, candidate) { + var name = typeof elm !== 'string' ? elm.nodeName.toLowerCase() : elm; + var elmRule = schema.getElementRule(name); + var parentsRequired = elmRule && elmRule.parentsRequired; - return; - } + if (parentsRequired && parentsRequired.length) { + return candidate && Tools.inArray(parentsRequired, candidate) !== -1 ? candidate : parentsRequired[0]; + } else { + return false; + } + } - // If node is wrapper type - if (format.wrapper && MatchFormat.matchNode(ed, node, name, vars)) { - currentWrapElm = 0; - return; - } + function wrapInHtml(elm, ancestry, siblings) { + var parent, parentCandidate, parentRequired; + var ancestor = ancestry.length > 0 && ancestry[0]; + var ancestorName = ancestor && ancestor.name; - // Can we rename the block - // TODO: Break this if up, too complex - if (contentEditable && !hasContentEditableState && format.block && - !format.wrapper && FormatUtils.isTextBlock(ed, nodeName) && FormatUtils.isValid(ed, parentName, wrapName)) { - node = dom.rename(node, wrapName); - setElementFormat(node); - newWrappers.push(node); - currentWrapElm = 0; - return; - } + parentRequired = getRequiredParent(elm, ancestorName); - // Handle selector patterns - if (format.selector) { - var found = applyNodeStyle(formatList, node); + if (parentRequired) { + if (ancestorName === parentRequired) { + parentCandidate = ancestry[0]; + ancestry = ancestry.slice(1); + } else { + parentCandidate = parentRequired; + } + } else if (ancestor) { + parentCandidate = ancestry[0]; + ancestry = ancestry.slice(1); + } else if (!siblings) { + return elm; + } - // Continue processing if a selector match wasn't found and a inline element is defined - if (!format.inline || found) { - currentWrapElm = 0; - return; - } - } + if (parentCandidate) { + parent = createElement(parentCandidate); + parent.appendChild(elm); + } - // Is it valid to wrap this item - // TODO: Break this if up, too complex - if (contentEditable && !hasContentEditableState && FormatUtils.isValid(ed, wrapName, nodeName) && FormatUtils.isValid(ed, parentName, wrapName) && - !(!nodeSpecific && node.nodeType === 3 && - node.nodeValue.length === 1 && - node.nodeValue.charCodeAt(0) === 65279) && - !CaretFormat.isCaretNode(node) && - (!format.inline || !dom.isBlock(node))) { - // Start wrapping - if (!currentWrapElm) { - // Wrap the node - currentWrapElm = dom.clone(wrapElm, false); - node.parentNode.insertBefore(currentWrapElm, node); - newWrappers.push(currentWrapElm); - } + if (siblings) { + if (!parent) { + // if no more ancestry, wrap in generic div + parent = dom.create('div'); + parent.appendChild(elm); + } - currentWrapElm.appendChild(node); - } else { - // Start a new wrapper for possible children - currentWrapElm = 0; + Tools.each(siblings, function (sibling) { + var siblingElm = createElement(sibling); + parent.insertBefore(siblingElm, elm); + }); + } - each(Tools.grep(node.childNodes), process); + return wrapInHtml(parent, ancestry, parentCandidate && parentCandidate.siblings); + } - if (hasContentEditableState) { - contentEditable = lastContentEditable; // Restore last contentEditable state from stack - } + if (ancestry && ancestry.length) { + item = ancestry[0]; + elm = createElement(item); + fragment = dom.create('div'); + fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), item.siblings)); + return fragment; + } else { + return ''; + } + } - // End the last wrapper - currentWrapElm = 0; - } - }; - // Process siblings from range - each(nodes, process); - }); + function selectorToHtml(selector, editor) { + return parsedSelectorToHtml(parseSelector(selector), editor); + } - // 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) { - var process = function (node) { - if (node.nodeName === 'A') { - setElementFormat(node, format); - } - each(Tools.grep(node.childNodes), process); - }; + function parseSelectorItem(item) { + var tagName; + var obj = { + classes: [], + attrs: {} + }; - process(node); - }); - } + item = obj.selector = Tools.trim(item); - // Cleanup - each(newWrappers, function (node) { - var childCount; + if (item !== '*') { + // matching IDs, CLASSes, ATTRIBUTES and PSEUDOs + tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, function ($0, $1, $2, $3, $4) { + switch ($1) { + case '#': + obj.attrs.id = $2; + break; - var getChildCount = function (node) { - var count = 0; + case '.': + obj.classes.push($2); + break; - each(node.childNodes, function (node) { - if (!FormatUtils.isWhiteSpaceNode(node) && !BookmarkManager.isBookmarkNode(node)) { - count++; + case ':': + if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) { + obj.attrs[$2] = $2; } - }); + break; + } - return count; - }; + // atribute matched + if ($3 === '[') { + var m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/); + if (m) { + obj.attrs[m[1]] = m[2]; + } + } - var getChildElementNode = function (root) { - var child = false; - each(root.childNodes, function (node) { - if (isElementNode(node)) { - child = node; - return false; // break loop - } - }); - return child; - }; + return ''; + }); + } - var mergeStyles = function (node) { - var child, clone; + obj.name = tagName || 'div'; + return obj; + } - child = getChildElementNode(node); - // If child was found and of the same type as the current node - if (child && !BookmarkManager.isBookmarkNode(child) && MatchFormat.matchName(dom, child, format)) { - clone = dom.clone(child, false); - setElementFormat(clone); + function parseSelector(selector) { + if (!selector || typeof selector !== 'string') { + return []; + } - dom.replace(clone, node, true); - dom.remove(child, 1); - } + // take into account only first one + selector = selector.split(/\s*,\s*/)[0]; - return clone || node; - }; + // tighten + selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1'); - childCount = getChildCount(node); + // split either on > or on space, but not the one inside brackets + return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), function (item) { + // process each sibling selector separately + var siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem); + var obj = siblings.pop(); // the last one is our real target - // 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 || !dom.isBlock(node)) && childCount === 0) { - dom.remove(node, 1); - return; - } + if (siblings.length) { + obj.siblings = siblings; + } + return obj; + }).reverse(); + } - 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); - } - MergeFormats.mergeWithChildren(ed, formatList, vars, node); - MergeFormats.mergeWithParents(ed, format, name, vars, node); - MergeFormats.mergeBackgroundColorAndFontSize(dom, format, vars, node); - MergeFormats.mergeSubSup(dom, format, vars, node); - MergeFormats.mergeSiblings(dom, format, vars, node); - } - }); - }; + function getCssText(editor, format) { + var name, previewFrag, previewElm, items; + var previewCss = '', parentFontSize, previewStyles; - if (dom.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; - } - } + previewStyles = editor.settings.preview_styles; - return; + // No preview forced + if (previewStyles === false) { + return ''; } - if (format) { - if (node) { - if (node.nodeType) { - if (!applyNodeStyle(formatList, node)) { - rng = dom.createRng(); - rng.setStartBefore(node); - rng.setEndAfter(node); - applyRngStyle(dom, ExpandRange.expandRng(ed, rng, formatList), null, true); - } - } else { - applyRngStyle(dom, 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(); + // Default preview + if (typeof previewStyles !== 'string') { + previewStyles = 'font-family font-size font-weight font-style text-decoration ' + + 'text-transform color background-color border border-radius outline text-shadow'; + } - // 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 (!ed.settings.forced_root_block && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { - applyFormat(ed, formatList[0].defaultBlock); - } + // Removes any variables since these can't be previewed + function removeVars(val) { + return val.replace(/%(\w+)/g, ''); + } - // Apply formatting to selection - ed.selection.setRng(RangeNormalizer.normalize(ed.selection.getRng())); - bookmark = selection.getBookmark(); - applyRngStyle(dom, ExpandRange.expandRng(ed, selection.getRng(true), formatList), bookmark); + // Create block/inline element to use for preview + if (typeof format === "string") { + format = editor.formatter.get(format); + if (!format) { + return; + } - if (format.styles) { - MergeFormats.mergeUnderlineAndColor(dom, format, vars, curSelNode); - } + format = format[0]; + } - selection.moveToBookmark(bookmark); - FormatUtils.moveStart(dom, selection, selection.getRng(true)); - ed.nodeChanged(); - } else { - CaretFormat.applyCaretFormat(ed, name, vars); - } + // Format specific preview override + // TODO: This should probably be further reduced by the previewStyles option + if ('preview' in format) { + previewStyles = format.preview; + if (previewStyles === false) { + return ''; } + } - Hooks.postProcess(name, ed); + name = format.block || format.inline || 'span'; + + items = parseSelector(format.selector); + if (items.length) { + if (!items[0].name) { // e.g. something like ul > .someClass was provided + items[0].name = name; + } + name = format.selector; + previewFrag = parsedSelectorToHtml(items, editor); + } else { + previewFrag = parsedSelectorToHtml([name], editor); } - }; - return { - applyFormat: applyFormat - }; - } -); -/** - * FormatChanged.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 - */ + previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild; -define( - 'tinymce.core.fmt.FormatChanged', - [ - 'ephox.katamari.api.Cell', - 'tinymce.core.fmt.FormatUtils', - 'tinymce.core.fmt.MatchFormat', - 'tinymce.core.util.Tools' - ], - function (Cell, FormatUtils, MatchFormat, Tools) { - var each = Tools.each; + // Add format styles to preview element + each(format.styles, function (value, name) { + value = removeVars(value); - var setup = function (formatChangeData, editor) { - var currentFormats = {}; + if (value) { + dom.setStyle(previewElm, name, value); + } + }); - formatChangeData.set({}); + // Add attributes to preview element + each(format.attributes, function (value, name) { + value = removeVars(value); - editor.on('NodeChange', function (e) { - var parents = FormatUtils.getParents(editor.dom, e.element), matchedFormats = {}; + if (value) { + dom.setAttrib(previewElm, name, value); + } + }); - // Ignore bogus nodes like the
    tag created by moveStart() - parents = Tools.grep(parents, function (node) { - return node.nodeType === 1 && !node.getAttribute('data-mce-bogus'); - }); + // Add classes to preview element + each(format.classes, function (value) { + value = removeVars(value); - // Check for new formats - each(formatChangeData.get(), function (callbacks, format) { - each(parents, function (node) { - if (editor.formatter.matchNode(node, format, {}, callbacks.similar)) { - if (!currentFormats[format]) { - // Execute callbacks - each(callbacks, function (callback) { - callback(true, { node: node, format: format, parents: parents }); - }); + if (!dom.hasClass(previewElm, value)) { + dom.addClass(previewElm, value); + } + }); - currentFormats[format] = callbacks; - } + editor.fire('PreviewFormats'); - matchedFormats[format] = callbacks; - return false; - } + // Add the previewElm outside the visual area + dom.setStyles(previewFrag, { position: 'absolute', left: -0xFFFF }); + editor.getBody().appendChild(previewFrag); - if (MatchFormat.matchesUnInheritedFormatSelector(editor, node, format)) { - return false; - } - }); - }); + // Get parent container font size so we can compute px values out of em/% for older IE:s + parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); + parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; - // Check if current formats still match - each(currentFormats, function (callbacks, format) { - if (!matchedFormats[format]) { - delete currentFormats[format]; + each(previewStyles.split(' '), function (name) { + var value = dom.getStyle(previewElm, name, true); - each(callbacks, function (callback) { - callback(false, { node: e.element, format: format, parents: parents }); - }); + // If background is transparent then check if the body has a background color we can use + if (name === 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { + value = dom.getStyle(editor.getBody(), name, true); + + // Ignore white since it's the default color, not the nicest fix + // TODO: Fix this by detecting runtime style + if (dom.toHex(value).toLowerCase() === '#ffffff') { + return; } - }); - }); - }; + } - var addListeners = function (formatChangeData, formats, callback, similar) { - var formatChangeItems = formatChangeData.get(); + if (name === 'color') { + // Ignore black since it's the default color, not the nicest fix + // TODO: Fix this by detecting runtime style + if (dom.toHex(value).toLowerCase() === '#000000') { + return; + } + } - each(formats.split(','), function (format) { - if (!formatChangeItems[format]) { - formatChangeItems[format] = []; - formatChangeItems[format].similar = similar; + // Old IE won't calculate the font size so we need to do that manually + if (name === 'font-size') { + if (/em|%$/.test(value)) { + if (parentFontSize === 0) { + return; + } + + // Convert font size from em/% to px + value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); + value = (value * parentFontSize) + 'px'; + } } - formatChangeItems[format].push(callback); + if (name === "border" && value) { + previewCss += 'padding:0 2px;'; + } + + previewCss += name + ':' + value + ';'; }); - formatChangeData.set(formatChangeItems); - }; + editor.fire('AfterPreviewFormats'); - var formatChanged = function (editor, formatChangeState, formats, callback, similar) { - if (formatChangeState.get() === null) { - setup(formatChangeState, editor); - } + //previewCss += 'line-height:normal'; - addListeners(formatChangeState, formats, callback, similar); - }; + dom.remove(previewFrag); + + return previewCss; + } return { - formatChanged: formatChanged + getCssText: getCssText, + parseSelector: parseSelector, + selectorToHtml: selectorToHtml }; } ); + /** - * DefaultFormats.js + * ToggleFormat.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -19841,202 +19723,31 @@ define( */ define( - 'tinymce.core.fmt.DefaultFormats', + 'tinymce.core.fmt.ToggleFormat', [ - 'tinymce.core.util.Tools' + 'tinymce.core.fmt.ApplyFormat', + 'tinymce.core.fmt.MatchFormat', + 'tinymce.core.fmt.RemoveFormat' ], - function (Tools) { - var get = function (dom) { - var formats = { - valigntop: [ - { selector: 'td,th', styles: { 'verticalAlign': 'top' } } - ], - - valignmiddle: [ - { selector: 'td,th', styles: { 'verticalAlign': 'middle' } } - ], - - valignbottom: [ - { selector: 'td,th', styles: { 'verticalAlign': 'bottom' } } - ], - - alignleft: [ - { - selector: 'figure.image', - collapsed: false, - classes: 'align-left', - ceFalseOverride: true, - preview: 'font-family font-size' - }, - { - selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', - styles: { - textAlign: 'left' - }, - inherit: false, - preview: false, - defaultBlock: 'div' - }, - { - selector: 'img,table', - collapsed: false, - styles: { - 'float': 'left' - }, - preview: 'font-family font-size' - } - ], - - aligncenter: [ - { - selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', - styles: { - textAlign: 'center' - }, - inherit: false, - preview: 'font-family font-size', - defaultBlock: 'div' - }, - { - selector: 'figure.image', - collapsed: false, - classes: 'align-center', - ceFalseOverride: true, - preview: 'font-family font-size' - }, - { - selector: 'img', - collapsed: false, - styles: { - display: 'block', - marginLeft: 'auto', - marginRight: 'auto' - }, - preview: false - }, - { - selector: 'table', - collapsed: false, - styles: { - marginLeft: 'auto', - marginRight: 'auto' - }, - preview: 'font-family font-size' - } - ], - - alignright: [ - { - selector: 'figure.image', - collapsed: false, - classes: 'align-right', - ceFalseOverride: true, - preview: 'font-family font-size' - }, - { - selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', - styles: { - textAlign: 'right' - }, - inherit: false, - preview: 'font-family font-size', - defaultBlock: 'div' - }, - { - selector: 'img,table', - collapsed: false, - styles: { - 'float': 'right' - }, - preview: 'font-family font-size' - } - ], - - alignjustify: [ - { - selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', - styles: { - textAlign: 'justify' - }, - inherit: false, - defaultBlock: 'div', - preview: 'font-family font-size' - } - ], - - bold: [ - { inline: 'strong', remove: 'all' }, - { inline: 'span', styles: { fontWeight: 'bold' } }, - { inline: 'b', remove: 'all' } - ], - - italic: [ - { inline: 'em', remove: 'all' }, - { inline: 'span', styles: { fontStyle: 'italic' } }, - { inline: 'i', remove: 'all' } - ], - - underline: [ - { inline: 'span', styles: { textDecoration: 'underline' }, exact: true }, - { inline: 'u', remove: 'all' } - ], - - strikethrough: [ - { inline: 'span', styles: { textDecoration: 'line-through' }, exact: true }, - { inline: 'strike', remove: 'all' } - ], - - forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, - hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, - fontname: { inline: 'span', styles: { fontFamily: '%value' }, clear_child_styles: true }, - fontsize: { inline: 'span', styles: { fontSize: '%value' }, clear_child_styles: true }, - fontsize_class: { inline: 'span', attributes: { 'class': '%value' } }, - blockquote: { block: 'blockquote', wrapper: 1, remove: 'all' }, - subscript: { inline: 'sub' }, - superscript: { inline: 'sup' }, - code: { inline: 'code' }, - - link: { - inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, - onmatch: function () { - return true; - }, - - onformat: function (elm, fmt, vars) { - Tools.each(vars, function (value, key) { - dom.setAttrib(elm, key, value); - }); - } - }, - - removeformat: [ - { - selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins', - remove: 'all', - split: true, - expand: false, - block_expand: true, - deep: true - }, - { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true }, - { selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true } - ] - }; - - Tools.each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function (name) { - formats[name] = { block: name, remove: 'all' }; - }); + function (ApplyFormat, MatchFormat, RemoveFormat) { + var toggle = function (editor, formats, name, vars, node) { + var fmt = formats.get(name); - return formats; + if (MatchFormat.match(editor, name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { + RemoveFormat.remove(editor, name, vars, node); + } else { + ApplyFormat.applyFormat(editor, name, vars, node); + } }; return { - get: get + toggle: toggle }; } ); + /** - * FormatRegistry.js + * FormatShortcuts.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -20046,85 +19757,34 @@ define( */ define( - 'tinymce.core.fmt.FormatRegistry', + 'tinymce.core.keyboard.FormatShortcuts', [ - 'tinymce.core.fmt.DefaultFormats', - 'tinymce.core.util.Tools' ], - function (DefaultFormats, Tools) { - return function (editor) { - var formats = {}; - - var get = function (name) { - return name ? formats[name] : formats; - }; - - var register = function (name, format) { - if (name) { - if (typeof name !== 'string') { - Tools.each(name, function (format, name) { - register(name, format); - }); - } else { - // Force format into array and add it to internal collection - format = format.length ? format : [format]; - - Tools.each(format, function (format) { - // Set deep to false by default on selector formats this to avoid removing - // alignment on images inside paragraphs when alignment is changed on paragraphs - if (typeof format.deep === 'undefined') { - format.deep = !format.selector; - } - - // Default to true - if (typeof format.split === 'undefined') { - format.split = !format.selector || format.inline; - } - - // Default to true - if (typeof format.remove === 'undefined' && format.selector && !format.inline) { - format.remove = 'none'; - } - - // Mark format as a mixed format inline + block level - if (format.selector && format.inline) { - format.mixed = true; - format.block_expand = true; - } - - // Split classes if needed - if (typeof format.classes === 'string') { - format.classes = format.classes.split(/\s+/); - } - }); - - formats[name] = format; - } - } - }; - - var unregister = function (name) { - if (name && formats[name]) { - delete formats[name]; - } + function () { + var setup = function (editor) { + // Add some inline shortcuts + editor.addShortcut('meta+b', '', 'Bold'); + editor.addShortcut('meta+i', '', 'Italic'); + editor.addShortcut('meta+u', '', 'Underline'); - return formats; - }; + // BlockFormat shortcuts keys + for (var i = 1; i <= 6; i++) { + editor.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]); + } - register(DefaultFormats.get(editor.dom)); - register(editor.settings.formats); + editor.addShortcut('access+7', '', ['FormatBlock', false, 'p']); + editor.addShortcut('access+8', '', ['FormatBlock', false, 'div']); + editor.addShortcut('access+9', '', ['FormatBlock', false, 'address']); + }; - return { - get: get, - register: register, - unregister: unregister - }; + return { + setup: setup }; } ); /** - * Preview.js + * Formatter.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -20134,591 +19794,172 @@ define( */ /** - * Internal class for generating previews styles for formats. + * Text formatter engine class. This class is used to apply formats like bold, italic, font size + * etc to the current selection or specific nodes. This engine was built to replace the browser's + * default formatting logic for execCommand due to its inconsistent and buggy behavior. * - * Example: - * Preview.getCssText(editor, 'bold'); + * @class tinymce.Formatter + * @example + * tinymce.activeEditor.formatter.register('mycustomformat', { + * inline: 'span', + * styles: {color: '#ff0000'} + * }); * - * @private - * @class tinymce.fmt.Preview + * tinymce.activeEditor.formatter.apply('mycustomformat'); */ define( - 'tinymce.core.fmt.Preview', + 'tinymce.core.api.Formatter', [ - "tinymce.core.dom.DOMUtils", - "tinymce.core.util.Tools", - "tinymce.core.html.Schema" + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'tinymce.core.fmt.ApplyFormat', + 'tinymce.core.fmt.FormatChanged', + 'tinymce.core.fmt.FormatRegistry', + 'tinymce.core.fmt.MatchFormat', + 'tinymce.core.fmt.Preview', + 'tinymce.core.fmt.RemoveFormat', + 'tinymce.core.fmt.ToggleFormat', + 'tinymce.core.keyboard.FormatShortcuts' ], - function (DOMUtils, Tools, Schema) { - var each = Tools.each; - var dom = DOMUtils.DOM; + function (Cell, Fun, ApplyFormat, FormatChanged, FormatRegistry, MatchFormat, Preview, RemoveFormat, ToggleFormat, FormatShortcuts) { + /** + * Constructs a new formatter instance. + * + * @constructor Formatter + * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. + */ + return function (editor) { + var formats = FormatRegistry(editor); + var formatChangeState = Cell(null); - function parsedSelectorToHtml(ancestry, editor) { - var elm, item, fragment; - var schema = editor && editor.schema || new Schema({}); + FormatShortcuts.setup(editor); - function decorate(elm, item) { - if (item.classes.length) { - dom.addClass(elm, item.classes.join(' ')); - } - dom.setAttribs(elm, item.attrs); - } + return { + /** + * Returns the format by name or all formats if no name is specified. + * + * @method get + * @param {String} name Optional name to retrieve by. + * @return {Array/Object} Array/Object with all registered formats or a specific format. + */ + get: formats.get, - function createElement(sItem) { - var elm; + /** + * Registers a specific format by name. + * + * @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. + */ + register: formats.register, - item = typeof sItem === 'string' ? { - name: sItem, - classes: [], - attrs: {} - } : sItem; + /** + * Unregister a specific format by name. + * + * @method unregister + * @param {String} name Name of the format for example "bold". + */ + unregister: formats.unregister, - elm = dom.create(item.name); - decorate(elm, item); - return elm; - } + /** + * 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), - function getRequiredParent(elm, candidate) { - var name = typeof elm !== 'string' ? elm.nodeName.toLowerCase() : elm; - var elmRule = schema.getElementRule(name); - var parentsRequired = elmRule && elmRule.parentsRequired; + /** + * 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), - if (parentsRequired && parentsRequired.length) { - return candidate && Tools.inArray(parentsRequired, candidate) !== -1 ? candidate : parentsRequired[0]; - } else { - return false; - } - } + /** + * 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), - function wrapInHtml(elm, ancestry, siblings) { - var parent, parentCandidate, parentRequired; - var ancestor = ancestry.length > 0 && ancestry[0]; - var ancestorName = ancestor && ancestor.name; + /** + * 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), - parentRequired = getRequiredParent(elm, ancestorName); + /** + * 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), - if (parentRequired) { - if (ancestorName === parentRequired) { - parentCandidate = ancestry[0]; - ancestry = ancestry.slice(1); - } else { - parentCandidate = parentRequired; - } - } else if (ancestor) { - parentCandidate = ancestry[0]; - ancestry = ancestry.slice(1); - } else if (!siblings) { - return elm; - } + /** + * 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), - if (parentCandidate) { - parent = createElement(parentCandidate); - parent.appendChild(elm); - } + /** + * 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), - if (siblings) { - if (!parent) { - // if no more ancestry, wrap in generic div - parent = dom.create('div'); - parent.appendChild(elm); - } + /** + * 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), - Tools.each(siblings, function (sibling) { - var siblingElm = createElement(sibling); - parent.insertBefore(siblingElm, elm); - }); - } - - return wrapInHtml(parent, ancestry, parentCandidate && parentCandidate.siblings); - } - - if (ancestry && ancestry.length) { - item = ancestry[0]; - elm = createElement(item); - fragment = dom.create('div'); - fragment.appendChild(wrapInHtml(elm, ancestry.slice(1), item.siblings)); - return fragment; - } else { - return ''; - } - } - - - function selectorToHtml(selector, editor) { - return parsedSelectorToHtml(parseSelector(selector), editor); - } - - - function parseSelectorItem(item) { - var tagName; - var obj = { - classes: [], - attrs: {} - }; - - item = obj.selector = Tools.trim(item); - - if (item !== '*') { - // matching IDs, CLASSes, ATTRIBUTES and PSEUDOs - tagName = item.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g, function ($0, $1, $2, $3, $4) { - switch ($1) { - case '#': - obj.attrs.id = $2; - break; - - case '.': - obj.classes.push($2); - break; - - case ':': - if (Tools.inArray('checked disabled enabled read-only required'.split(' '), $2) !== -1) { - obj.attrs[$2] = $2; - } - break; - } - - // atribute matched - if ($3 === '[') { - var m = $4.match(/([\w\-]+)(?:\=\"([^\"]+))?/); - if (m) { - obj.attrs[m[1]] = m[2]; - } - } - - return ''; - }); - } - - obj.name = tagName || 'div'; - return obj; - } - - - function parseSelector(selector) { - if (!selector || typeof selector !== 'string') { - return []; - } - - // take into account only first one - selector = selector.split(/\s*,\s*/)[0]; - - // tighten - selector = selector.replace(/\s*(~\+|~|\+|>)\s*/g, '$1'); - - // split either on > or on space, but not the one inside brackets - return Tools.map(selector.split(/(?:>|\s+(?![^\[\]]+\]))/), function (item) { - // process each sibling selector separately - var siblings = Tools.map(item.split(/(?:~\+|~|\+)/), parseSelectorItem); - var obj = siblings.pop(); // the last one is our real target - - if (siblings.length) { - obj.siblings = siblings; - } - return obj; - }).reverse(); - } - - - function getCssText(editor, format) { - var name, previewFrag, previewElm, items; - var previewCss = '', parentFontSize, previewStyles; - - previewStyles = editor.settings.preview_styles; - - // No preview forced - if (previewStyles === false) { - return ''; - } - - // Default preview - if (typeof previewStyles !== 'string') { - previewStyles = 'font-family font-size font-weight font-style text-decoration ' + - 'text-transform color background-color border border-radius outline text-shadow'; - } - - // Removes any variables since these can't be previewed - function removeVars(val) { - return val.replace(/%(\w+)/g, ''); - } - - // Create block/inline element to use for preview - if (typeof format === "string") { - format = editor.formatter.get(format); - if (!format) { - return; - } - - format = format[0]; - } - - // Format specific preview override - // TODO: This should probably be further reduced by the previewStyles option - if ('preview' in format) { - previewStyles = format.preview; - if (previewStyles === false) { - return ''; - } - } - - name = format.block || format.inline || 'span'; - - items = parseSelector(format.selector); - if (items.length) { - if (!items[0].name) { // e.g. something like ul > .someClass was provided - items[0].name = name; - } - name = format.selector; - previewFrag = parsedSelectorToHtml(items, editor); - } else { - previewFrag = parsedSelectorToHtml([name], editor); - } - - previewElm = dom.select(name, previewFrag)[0] || previewFrag.firstChild; - - // Add format styles to preview element - each(format.styles, function (value, name) { - value = removeVars(value); - - if (value) { - dom.setStyle(previewElm, name, value); - } - }); - - // Add attributes to preview element - each(format.attributes, function (value, name) { - value = removeVars(value); - - if (value) { - dom.setAttrib(previewElm, name, value); - } - }); - - // Add classes to preview element - each(format.classes, function (value) { - value = removeVars(value); - - if (!dom.hasClass(previewElm, value)) { - dom.addClass(previewElm, value); - } - }); - - editor.fire('PreviewFormats'); - - // Add the previewElm outside the visual area - dom.setStyles(previewFrag, { position: 'absolute', left: -0xFFFF }); - editor.getBody().appendChild(previewFrag); - - // Get parent container font size so we can compute px values out of em/% for older IE:s - parentFontSize = dom.getStyle(editor.getBody(), 'fontSize', true); - parentFontSize = /px$/.test(parentFontSize) ? parseInt(parentFontSize, 10) : 0; - - each(previewStyles.split(' '), function (name) { - var value = dom.getStyle(previewElm, name, true); - - // If background is transparent then check if the body has a background color we can use - if (name === 'background-color' && /transparent|rgba\s*\([^)]+,\s*0\)/.test(value)) { - value = dom.getStyle(editor.getBody(), name, true); - - // Ignore white since it's the default color, not the nicest fix - // TODO: Fix this by detecting runtime style - if (dom.toHex(value).toLowerCase() === '#ffffff') { - return; - } - } - - if (name === 'color') { - // Ignore black since it's the default color, not the nicest fix - // TODO: Fix this by detecting runtime style - if (dom.toHex(value).toLowerCase() === '#000000') { - return; - } - } - - // Old IE won't calculate the font size so we need to do that manually - if (name === 'font-size') { - if (/em|%$/.test(value)) { - if (parentFontSize === 0) { - return; - } - - // Convert font size from em/% to px - value = parseFloat(value, 10) / (/%$/.test(value) ? 100 : 1); - value = (value * parentFontSize) + 'px'; - } - } - - if (name === "border" && value) { - previewCss += 'padding:0 2px;'; - } - - previewCss += name + ':' + value + ';'; - }); - - editor.fire('AfterPreviewFormats'); - - //previewCss += 'line-height:normal'; - - dom.remove(previewFrag); - - return previewCss; - } - - return { - getCssText: getCssText, - parseSelector: parseSelector, - selectorToHtml: selectorToHtml - }; - } -); - -/** - * ToggleFormat.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.ToggleFormat', - [ - 'tinymce.core.fmt.ApplyFormat', - 'tinymce.core.fmt.MatchFormat', - 'tinymce.core.fmt.RemoveFormat' - ], - function (ApplyFormat, MatchFormat, RemoveFormat) { - var toggle = function (editor, formats, name, vars, node) { - var fmt = formats.get(name); - - if (MatchFormat.match(editor, name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { - RemoveFormat.remove(editor, name, vars, node); - } else { - ApplyFormat.applyFormat(editor, name, vars, node); - } - }; - - return { - toggle: toggle - }; - } -); - -/** - * FormatShortcuts.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.FormatShortcuts', - [ - ], - function () { - var setup = function (editor) { - // Add some inline shortcuts - editor.addShortcut('meta+b', '', 'Bold'); - editor.addShortcut('meta+i', '', 'Italic'); - editor.addShortcut('meta+u', '', 'Underline'); - - // BlockFormat shortcuts keys - for (var i = 1; i <= 6; i++) { - editor.addShortcut('access+' + i, '', ['FormatBlock', false, 'h' + i]); - } - - editor.addShortcut('access+7', '', ['FormatBlock', false, 'p']); - editor.addShortcut('access+8', '', ['FormatBlock', false, 'div']); - editor.addShortcut('access+9', '', ['FormatBlock', false, 'address']); - }; - - return { - setup: setup - }; - } -); - -/** - * Formatter.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 - */ - -/** - * Text formatter engine class. This class is used to apply formats like bold, italic, font size - * etc to the current selection or specific nodes. This engine was built to replace the browser's - * default formatting logic for execCommand due to its inconsistent and buggy behavior. - * - * @class tinymce.Formatter - * @example - * tinymce.activeEditor.formatter.register('mycustomformat', { - * inline: 'span', - * styles: {color: '#ff0000'} - * }); - * - * tinymce.activeEditor.formatter.apply('mycustomformat'); - */ -define( - 'tinymce.core.api.Formatter', - [ - 'ephox.katamari.api.Cell', - 'ephox.katamari.api.Fun', - 'tinymce.core.fmt.ApplyFormat', - 'tinymce.core.fmt.FormatChanged', - 'tinymce.core.fmt.FormatRegistry', - 'tinymce.core.fmt.MatchFormat', - 'tinymce.core.fmt.Preview', - 'tinymce.core.fmt.RemoveFormat', - 'tinymce.core.fmt.ToggleFormat', - 'tinymce.core.keyboard.FormatShortcuts' - ], - function (Cell, Fun, ApplyFormat, FormatChanged, FormatRegistry, MatchFormat, Preview, RemoveFormat, ToggleFormat, FormatShortcuts) { - /** - * Constructs a new formatter instance. - * - * @constructor Formatter - * @param {tinymce.Editor} ed Editor instance to construct the formatter engine to. - */ - return function (editor) { - var formats = FormatRegistry(editor); - var formatChangeState = Cell(null); - - FormatShortcuts.setup(editor); - - return { - /** - * Returns the format by name or all formats if no name is specified. - * - * @method get - * @param {String} name Optional name to retrieve by. - * @return {Array/Object} Array/Object with all registered formats or a specific format. - */ - get: formats.get, - - /** - * Registers a specific format by name. - * - * @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. - */ - 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) - }; - }; - } -); + /** + * 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) + }; + }; + } +); define( 'ephox.sugar.api.properties.Attr', @@ -20836,7 +20077,6 @@ define( }; } ); -defineGlobal("global!window", window); define( 'ephox.sugar.api.properties.Css', @@ -21109,7 +20349,7 @@ define( ); /** - * DomUtils.js + * NotificationManagerImpl.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -21118,1838 +20358,1577 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Private UI DomUtils proxy. - * - * @private - * @class tinymce.ui.DomUtils - */ define( - 'tinymce.core.ui.DomUtils', + 'tinymce.core.ui.NotificationManagerImpl', [ - "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++); - }, + function () { + return function () { + var unimplemented = function () { + throw new Error('Theme did not provide a NotificationManager implementation.'); + }; - create: function (name, attrs, children) { - var elm = document.createElement(name); + return { + open: unimplemented, + close: unimplemented, + reposition: unimplemented, + getArgs: unimplemented + }; + }; + } +); - 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; - }, +/** + * NotificationManager.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 + */ - createFragment: function (html) { - return DOMUtils.DOM.createFragment(html); - }, +/** + * This class handles the creation of TinyMCE's notifications. + * + * @class tinymce.NotificationManager + * @example + * // Opens a new notification of type "error" with text "An error occurred." + * tinymce.activeEditor.notificationManager.open({ + * text: 'An error occurred.', + * type: 'error' + * }); + */ +define( + 'tinymce.core.api.NotificationManager', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'tinymce.core.EditorView', + 'tinymce.core.ui.NotificationManagerImpl', + 'tinymce.core.util.Delay' + ], + function (Arr, Option, EditorView, NotificationManagerImpl, Delay) { + return function (editor) { + var notifications = []; - getWindowSize: function () { - return DOMUtils.DOM.getViewPort(); - }, + var getImplementation = function () { + var theme = editor.theme; + return theme && theme.getNotificationManagerImpl ? theme.getNotificationManagerImpl() : NotificationManagerImpl(); + }; - getSize: function (elm) { - var width, height; + var getTopNotification = function () { + return Option.from(notifications[0]); + }; - if (elm.getBoundingClientRect) { - var rect = elm.getBoundingClientRect(); + var isEqual = function (a, b) { + return a.type === b.type && a.text === b.text && !a.progressBar && !a.timeout && !b.progressBar && !b.timeout; + }; - 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; + var reposition = function () { + if (notifications.length > 0) { + getImplementation().reposition(notifications); } + }; - return { width: width, height: height }; - }, + var addNotification = function (notification) { + notifications.push(notification); + }; - getPos: function (elm, root) { - return DOMUtils.DOM.getPos(elm, root || funcs.getContainer()); - }, + var closeNotification = function (notification) { + Arr.findIndex(notifications, function (otherNotification) { + return otherNotification === notification; + }).each(function (index) { + // Mutate here since third party might have stored away the window array + // TODO: Consider breaking this api + notifications.splice(index, 1); + }); + }; - getContainer: function () { - return Env.container ? Env.container : document.body; - }, + var open = function (args) { + // Never open notification if editor has been removed. + if (editor.removed || !EditorView.isEditorAttachedToDom(editor)) { + return; + } - getViewPort: function (win) { - return DOMUtils.DOM.getViewPort(win); - }, + return Arr.find(notifications, function (notification) { + return isEqual(getImplementation().getArgs(notification), args); + }).getOrThunk(function () { + editor.editorManager.setActive(editor); - get: function (id) { - return document.getElementById(id); - }, + var notification = getImplementation().open(args, function () { + closeNotification(notification); + reposition(); + }); - addClass: function (elm, cls) { - return DOMUtils.DOM.addClass(elm, cls); - }, + addNotification(notification); + reposition(); + return notification; + }); + }; - removeClass: function (elm, cls) { - return DOMUtils.DOM.removeClass(elm, cls); - }, + var close = function () { + getTopNotification().each(function (notification) { + getImplementation().close(notification); + closeNotification(notification); + reposition(); + }); + }; - hasClass: function (elm, cls) { - return DOMUtils.DOM.hasClass(elm, cls); - }, + var getNotifications = function () { + return notifications; + }; - toggleClass: function (elm, cls, state) { - return DOMUtils.DOM.toggleClass(elm, cls, state); - }, + var registerEvents = function (editor) { + editor.on('SkinLoaded', function () { + var serviceMessage = editor.settings.service_message; - css: function (elm, name, value) { - return DOMUtils.DOM.setStyle(elm, name, value); - }, + if (serviceMessage) { + open({ + text: serviceMessage, + type: 'warning', + timeout: 0, + icon: '' + }); + } + }); - getRuntimeStyle: function (elm, name) { - return DOMUtils.DOM.getStyle(elm, name, true); - }, + editor.on('ResizeEditor ResizeWindow', function () { + Delay.requestAnimationFrame(reposition); + }); - on: function (target, name, callback, scope) { - return DOMUtils.DOM.bind(target, name, callback, scope); - }, + editor.on('remove', function () { + Arr.each(notifications, function (notification) { + getImplementation().close(notification); + }); + }); + }; - off: function (target, name, callback) { - return DOMUtils.DOM.unbind(target, name, callback); - }, + registerEvents(editor); - fire: function (target, name, args) { - return DOMUtils.DOM.fire(target, name, args); - }, + return { + /** + * Opens a new notification. + * + * @method open + * @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc. + */ + open: open, - innerHtml: function (elm, html) { - // Workaround for
    in

    bug on IE 8 #6178 - DOMUtils.DOM.setHTML(elm, html); - } - }; + /** + * Closes the top most notification. + * + * @method close + */ + close: close, - return funcs; + /** + * Returns the currently opened notification objects. + * + * @method getNotifications + * @return {Array} Array of the currently opened notifications. + */ + getNotifications: getNotifications + }; + }; } ); -/** - * Class.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 utilitiy class is used for easier inheritance. - * - * Features: - * * Exposed super functions: this._super(); - * * Mixins - * * Dummy functions - * * Property functions: var value = object.value(); and object.value(newValue); - * * Static functions - * * Defaults settings - */ define( - 'tinymce.core.util.Class', + 'ephox.katamari.api.Adt', + [ - "tinymce.core.util.Tools" + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Type', + 'global!Array', + 'global!Error', + 'global!console' ], - function (Tools) { - var each = Tools.each, extend = Tools.extend; - - var extendClass, initializing; - - function Class() { - } - // Provides classical inheritance, based on code made by John Resig - Class.extend = extendClass = function (prop) { - var self = this, _super = self.prototype, prototype, name, member; + 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'); + } + if (cases.length === 0) { + throw new Error('there must be at least one case'); + } - // The dummy class constructor - function Class() { - var i, mixins, mixin, self = this; + var constructors = [ ]; - // All construction is actually done in the init method - if (!initializing) { - // Run class constuctor - if (self.init) { - self.init.apply(self, arguments); - } + // adt is mutated to add the individual cases + var adt = {}; + Arr.each(cases, function (acase, count) { + var keys = Obj.keys(acase); - // Run mixin constructors - mixins = self.Mixins; - if (mixins) { - i = mixins.length; - while (i--) { - mixin = mixins[i]; - if (mixin.init) { - mixin.init.apply(self, arguments); - } - } - } + // validation + if (keys.length !== 1) { + throw new Error('one and only one name per case'); } - } - // Dummy function, needs to be extended in order to provide functionality - function dummy() { - return this; - } + var key = keys[0]; + var value = acase[key]; - // Creates a overloaded method for the class - // this enables you to use this._super(); to call the super function - function createMethod(name, fn) { - return function () { - var self = this, tmp = self._super, ret; + // 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'); + } - self._super = _super[name]; - ret = fn.apply(self, arguments); - self._super = tmp; + constructors.push(key); + // + // constructor for key + // + adt[key] = function () { + var argLength = arguments.length; - return ret; - }; - } + // validation + if (argLength !== value.length) { + throw new Error('Wrong number of arguments to case ' + key + '. Expected ' + value.length + ' (' + value + '), got ' + argLength); + } - // Instantiate a base class (but only create the instance, - // don't run the init constructor) - initializing = true; + // 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]; - /*eslint new-cap:0 */ - prototype = new self(); - initializing = false; - // Add mixins - if (prop.Mixins) { - each(prop.Mixins, function (mixin) { - for (var name in mixin) { - if (name !== "init") { - prop[name] = mixin[name]; + 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(',')); } - } - }); - if (_super.Mixins) { - prop.Mixins = _super.Mixins.concat(prop.Mixins); - } - } - - // Generate dummy methods - if (prop.Methods) { - each(prop.Methods.split(','), function (name) { - prop[name] = dummy; - }); - } + var allReqd = Arr.forall(constructors, function (reqKey) { + return Arr.contains(branchKeys, reqKey); + }); - // Generate property methods - if (prop.Properties) { - each(prop.Properties.split(','), function (name) { - var fieldName = '_' + name; + if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); - prop[name] = function (value) { - var self = this, undef; + return branches[key].apply(null, args); + }; - // Set value - if (value !== undef) { - self[fieldName] = value; + // + // 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, - return self; + // NOTE: Only for debugging. + log: function (label) { + console.log(label, { + constructors: constructors, + constructor: key, + params: args + }); } - - // Get value - return self[fieldName]; }; - }); - } - - // Static functions - if (prop.Statics) { - each(prop.Statics, function (func, name) { - Class[name] = func; - }); - } + }; + }); - // Default settings - if (prop.Defaults && _super.Defaults) { - prop.Defaults = extend({}, _super.Defaults, prop.Defaults); - } + return adt; + }; + return { + generate: generate + }; + } +); +define( + 'ephox.sugar.api.selection.Selection', - // Copy the properties over onto the new prototype - for (name in prop) { - member = prop[name]; + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Struct' + ], - if (typeof member == "function" && _super[name]) { - prototype[name] = createMethod(name, member); - } else { - prototype[name] = member; - } - } + function (Adt, Struct) { + // Consider adding a type for "element" + var type = Adt.generate([ + { domRange: [ 'rng' ] }, + { relative: [ 'startSitu', 'finishSitu' ] }, + { exact: [ 'start', 'soffset', 'finish', 'foffset' ] } + ]); - // Populate our constructed prototype object - Class.prototype = prototype; + var range = Struct.immutable( + 'start', + 'soffset', + 'finish', + 'foffset' + ); - // Enforce the constructor to be what we expect - Class.constructor = Class; + var exactFromRange = function (simRange) { + return type.exact(simRange.start(), simRange.soffset(), simRange.finish(), simRange.foffset()); + }; - // And make this class extendible - Class.extend = extendClass; + return { + domRange: type.domRange, + relative: type.relative, + exact: type.exact, - return Class; + exactFromRange: exactFromRange, + range: range }; - - return Class; } ); -/** - * EventDispatcher.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 lets you add/remove and fire events by name on the specified scope. This makes - * it easy to add event listener logic to any class. - * - * @class tinymce.util.EventDispatcher - * @example - * var eventDispatcher = new EventDispatcher(); - * - * eventDispatcher.on('click', function() {console.log('data');}); - * eventDispatcher.fire('click', {data: 123}); - */ define( - 'tinymce.core.util.EventDispatcher', + 'ephox.sugar.api.dom.DocumentPosition', + [ - "tinymce.core.util.Tools" + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse' ], - function (Tools) { - var nativeEvents = Tools.makeMap( - "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + - "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + - "draggesture dragdrop drop drag submit " + - "compositionstart compositionend compositionupdate touchstart touchmove touchend", - ' ' - ); - function Dispatcher(settings) { - var self = this, scope, bindings = {}, toggleEvent; + function (Compare, Element, Traverse) { + var makeRange = function (start, soffset, finish, foffset) { + var doc = Traverse.owner(start); - function returnFalse() { - return false; - } + // TODO: We need to think about a better place to put native range creation code. Does it even belong in sugar? + // Could the `Compare` checks (node.compareDocumentPosition) handle these situations better? + var rng = doc.dom().createRange(); + rng.setStart(start.dom(), soffset); + rng.setEnd(finish.dom(), foffset); + return rng; + }; - function returnTrue() { - return true; - } + // Return the deepest - or furthest down the document tree - Node that contains both boundary points + // of the range (start:soffset, finish:foffset). + var commonAncestorContainer = function (start, soffset, finish, foffset) { + var r = makeRange(start, soffset, finish, foffset); + return Element.fromDom(r.commonAncestorContainer); + }; - settings = settings || {}; - scope = settings.scope || self; - toggleEvent = settings.toggleEvent || returnFalse; + var after = function (start, soffset, finish, foffset) { + var r = makeRange(start, soffset, finish, foffset); - /** - * Fires the specified event by name. - * - * @method fire - * @param {String} name Name of the event to fire. - * @param {Object?} args Event arguments. - * @return {Object} Event args instance passed in. - * @example - * instance.fire('event', {...}); - */ - function fire(name, args) { - var handlers, i, l, callback; + var same = Compare.eq(start, finish) && soffset === foffset; + return r.collapsed && !same; + }; - name = name.toLowerCase(); - args = args || {}; - args.type = name; + return { + after: after, + commonAncestorContainer: commonAncestorContainer + }; + } +); +define( + 'ephox.sugar.api.node.Fragment', - // Setup target is there isn't one - if (!args.target) { - args.target = scope; - } + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'global!document' + ], - // Add event delegation methods if they are missing - if (!args.preventDefault) { - // Add preventDefault method - args.preventDefault = function () { - args.isDefaultPrevented = returnTrue; - }; + 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); + }; - // Add stopPropagation - args.stopPropagation = function () { - args.isPropagationStopped = returnTrue; - }; + return { + fromElements: fromElements + }; + } +); - // Add stopImmediatePropagation - args.stopImmediatePropagation = function () { - args.isImmediatePropagationStopped = returnTrue; - }; +define( + 'ephox.sugar.api.selection.Situ', - // Add event delegation states - args.isDefaultPrevented = returnFalse; - args.isPropagationStopped = returnFalse; - args.isImmediatePropagationStopped = returnFalse; - } - - if (settings.beforeFire) { - settings.beforeFire(args); - } - - handlers = bindings[name]; - if (handlers) { - for (i = 0, l = handlers.length; i < l; i++) { - callback = handlers[i]; - - // Unbind handlers marked with "once" - if (callback.once) { - off(name, callback.func); - } - - // Stop immediate propagation if needed - if (args.isImmediatePropagationStopped()) { - args.stopPropagation(); - return args; - } - - // If callback returns false then prevent default and stop all propagation - if (callback.func.call(scope, args) === false) { - args.preventDefault(); - return args; - } - } - } + [ + 'ephox.katamari.api.Adt' + ], - return args; - } + function (Adt) { + var adt = Adt.generate([ + { 'before': [ 'element' ] }, + { 'on': [ 'element', 'offset' ] }, + { after: [ 'element' ] } + ]); - /** - * Binds an event listener to a specific event by name. - * - * @method on - * @param {String} name Event name or space separated list of events to bind. - * @param {callback} callback Callback to be executed when the event occurs. - * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. - * @return {Object} Current class instance. - * @example - * instance.on('event', function(e) { - * // Callback logic - * }); - */ - function on(name, callback, prepend, extra) { - var handlers, names, i; + // Probably don't need this given that we now have "match" + var cata = function (subject, onBefore, onOn, onAfter) { + return subject.fold(onBefore, onOn, onAfter); + }; - if (callback === false) { - callback = returnFalse; - } + return { + before: adt.before, + on: adt.on, + after: adt.after, + cata: cata + }; + } +); - if (callback) { - callback = { - func: callback - }; +define( + 'ephox.sugar.selection.core.NativeRange', - if (extra) { - Tools.extend(callback, extra); - } + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element' + ], - names = name.toLowerCase().split(' '); - i = names.length; - while (i--) { - name = names[i]; - handlers = bindings[name]; - if (!handlers) { - handlers = bindings[name] = []; - toggleEvent(name, true); - } + function (Fun, Option, Compare, Element) { + var selectNodeContents = function (win, element) { + var rng = win.document.createRange(); + selectNodeContentsUsing(rng, element); + return rng; + }; - if (prepend) { - handlers.unshift(callback); - } else { - handlers.push(callback); - } - } - } + var selectNodeContentsUsing = function (rng, element) { + rng.selectNodeContents(element.dom()); + }; - return self; - } + var isWithin = function (outerRange, innerRange) { + // Adapted from: http://stackoverflow.com/questions/5605401/insert-link-in-contenteditable-element + return innerRange.compareBoundaryPoints(outerRange.END_TO_START, outerRange) < 1 && + innerRange.compareBoundaryPoints(outerRange.START_TO_END, outerRange) > -1; + }; - /** - * Unbinds an event listener to a specific event by name. - * - * @method off - * @param {String?} name Name of the event to unbind. - * @param {callback?} callback Callback to unbind. - * @return {Object} Current class instance. - * @example - * // Unbind specific callback - * instance.off('event', handler); - * - * // Unbind all listeners by name - * instance.off('event'); - * - * // Unbind all events - * instance.off(); - */ - function off(name, callback) { - var i, handlers, bindingName, names, hi; + var create = function (win) { + return win.document.createRange(); + }; - if (name) { - names = name.toLowerCase().split(' '); - i = names.length; - while (i--) { - name = names[i]; - handlers = bindings[name]; + // NOTE: Mutates the range. + var setStart = function (rng, situ) { + situ.fold(function (e) { + rng.setStartBefore(e.dom()); + }, function (e, o) { + rng.setStart(e.dom(), o); + }, function (e) { + rng.setStartAfter(e.dom()); + }); + }; - // Unbind all handlers - if (!name) { - for (bindingName in bindings) { - toggleEvent(bindingName, false); - delete bindings[bindingName]; - } + var setFinish = function (rng, situ) { + situ.fold(function (e) { + rng.setEndBefore(e.dom()); + }, function (e, o) { + rng.setEnd(e.dom(), o); + }, function (e) { + rng.setEndAfter(e.dom()); + }); + }; - return self; - } + var replaceWith = function (rng, fragment) { + // Note: this document fragment approach may not work on IE9. + deleteContents(rng); + rng.insertNode(fragment.dom()); + }; - if (handlers) { - // Unbind all by name - if (!callback) { - handlers.length = 0; - } else { - // Unbind specific ones - hi = handlers.length; - while (hi--) { - if (handlers[hi].func === callback) { - handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1)); - bindings[name] = handlers; - } - } - } + var isCollapsed = function (start, soffset, finish, foffset) { + return Compare.eq(start, finish) && soffset === foffset; + }; - if (!handlers.length) { - toggleEvent(name, false); - delete bindings[name]; - } - } - } - } else { - for (name in bindings) { - toggleEvent(name, false); - } + var relativeToNative = function (win, startSitu, finishSitu) { + var range = win.document.createRange(); + setStart(range, startSitu); + setFinish(range, finishSitu); + return range; + }; - bindings = {}; - } + var exactToNative = function (win, start, soffset, finish, foffset) { + var rng = win.document.createRange(); + rng.setStart(start.dom(), soffset); + rng.setEnd(finish.dom(), foffset); + return rng; + }; - return self; - } + var deleteContents = function (rng) { + rng.deleteContents(); + }; - /** - * Binds an event listener to a specific event by name - * and automatically unbind the event once the callback fires. - * - * @method once - * @param {String} name Event name or space separated list of events to bind. - * @param {callback} callback Callback to be executed when the event occurs. - * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. - * @return {Object} Current class instance. - * @example - * instance.once('event', function(e) { - * // Callback logic - * }); - */ - function once(name, callback, prepend) { - return on(name, callback, prepend, { once: true }); - } + var cloneFragment = function (rng) { + var fragment = rng.cloneContents(); + return Element.fromDom(fragment); + }; - /** - * Returns true/false if the dispatcher has a event of the specified name. - * - * @method has - * @param {String} name Name of the event to check for. - * @return {Boolean} true/false if the event exists or not. - */ - function has(name) { - name = name.toLowerCase(); - return !(!bindings[name] || bindings[name].length === 0); - } + var toRect = function (rect) { + return { + left: Fun.constant(rect.left), + top: Fun.constant(rect.top), + right: Fun.constant(rect.right), + bottom: Fun.constant(rect.bottom), + width: Fun.constant(rect.width), + height: Fun.constant(rect.height) + }; + }; + + var getFirstRect = function (rng) { + var rects = rng.getClientRects(); + // ASSUMPTION: The first rectangle is the start of the selection + var rect = rects.length > 0 ? rects[0] : rng.getBoundingClientRect(); + return rect.width > 0 || rect.height > 0 ? Option.some(rect).map(toRect) : Option.none(); + }; - // Expose - self.fire = fire; - self.on = on; - self.off = off; - self.once = once; - self.has = has; - } + var getBounds = function (rng) { + var rect = rng.getBoundingClientRect(); + return rect.width > 0 || rect.height > 0 ? Option.some(rect).map(toRect) : Option.none(); + }; - /** - * Returns true/false if the specified event name is a native browser event or not. - * - * @method isNative - * @param {String} name Name to check if it's native. - * @return {Boolean} true/false if the event is native or not. - * @static - */ - Dispatcher.isNative = function (name) { - return !!nativeEvents[name.toLowerCase()]; + var toString = function (rng) { + return rng.toString(); }; - return Dispatcher; + return { + create: create, + replaceWith: replaceWith, + selectNodeContents: selectNodeContents, + selectNodeContentsUsing: selectNodeContentsUsing, + isCollapsed: isCollapsed, + relativeToNative: relativeToNative, + exactToNative: exactToNative, + deleteContents: deleteContents, + cloneFragment: cloneFragment, + getFirstRect: getFirstRect, + getBounds: getBounds, + isWithin: isWithin, + toString: toString + }; } ); -/** - * 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', + 'ephox.sugar.selection.core.SelectionDirection', + [ + 'ephox.katamari.api.Adt', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Thunk', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.selection.core.NativeRange' ], - 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; + function (Adt, Fun, Option, Thunk, Element, NativeRange) { + var adt = Adt.generate([ + { ltr: [ 'start', 'soffset', 'finish', 'foffset' ] }, + { rtl: [ 'start', 'soffset', 'finish', 'foffset' ] } + ]); - if (!bindings) { - bindings = otherModel._bindings = []; + var fromRange = function (win, type, range) { + return type(Element.fromDom(range.startContainer), range.startOffset, Element.fromDom(range.endContainer), range.endOffset); + }; - otherModel.on('destroy', function () { - var i = bindings.length; + var getRanges = function (win, selection) { + return selection.match({ + domRange: function (rng) { + return { + ltr: Fun.constant(rng), + rtl: Option.none + }; + }, + relative: function (startSitu, finishSitu) { + return { + ltr: Thunk.cached(function () { + return NativeRange.relativeToNative(win, startSitu, finishSitu); + }), + rtl: Thunk.cached(function () { + return Option.some( + NativeRange.relativeToNative(win, finishSitu, startSitu) + ); + }) + }; + }, + exact: function (start, soffset, finish, foffset) { + return { + ltr: Thunk.cached(function () { + return NativeRange.exactToNative(win, start, soffset, finish, foffset); + }), + rtl: Thunk.cached(function () { + return Option.some( + NativeRange.exactToNative(win, finish, foffset, start, soffset) + ); + }) + }; + } + }); + }; - while (i--) { - bindings[i](); - } - }); - } + var doDiagnose = function (win, ranges) { + // If we cannot create a ranged selection from start > finish, it could be RTL + var rng = ranges.ltr(); + if (rng.collapsed) { + // Let's check if it's RTL ... if it is, then reversing the direction will not be collapsed + var reversed = ranges.rtl().filter(function (rev) { + return rev.collapsed === false; + }); + + return reversed.map(function (rev) { + // We need to use "reversed" here, because the original only has one point (collapsed) + return adt.rtl( + Element.fromDom(rev.endContainer), rev.endOffset, + Element.fromDom(rev.startContainer), rev.startOffset + ); + }).getOrThunk(function () { + return fromRange(win, adt.ltr, rng); + }); + } else { + return fromRange(win, adt.ltr, rng); + } + }; - bindings.push(function () { - model.off('change:' + name, fromSelfToOther); - }); + var diagnose = function (win, selection) { + var ranges = getRanges(win, selection); + return doDiagnose(win, ranges); + }; - return model.get(name); + var asLtrRange = function (win, selection) { + var diagnosis = diagnose(win, selection); + return diagnosis.match({ + ltr: function (start, soffset, finish, foffset) { + var rng = win.document.createRange(); + rng.setStart(start.dom(), soffset); + rng.setEnd(finish.dom(), foffset); + return rng; + }, + rtl: function (start, soffset, finish, foffset) { + // NOTE: Reversing start and finish + var rng = win.document.createRange(); + rng.setStart(finish.dom(), foffset); + rng.setEnd(start.dom(), soffset); + return rng; } }); }; - return Binding; + return { + ltr: adt.ltr, + rtl: adt.rtl, + diagnose: diagnose, + asLtrRange: asLtrRange + }; } ); -/** - * Observable.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 mixin will add event binding logic to classes. - * - * @mixin tinymce.util.Observable - */ define( - 'tinymce.core.util.Observable', + 'ephox.katamari.api.Options', + [ - "tinymce.core.util.EventDispatcher" + 'ephox.katamari.api.Option' ], - function (EventDispatcher) { - function getEventDispatcher(obj) { - if (!obj._eventDispatcher) { - obj._eventDispatcher = new EventDispatcher({ - scope: obj, - toggleEvent: function (name, state) { - if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { - obj.toggleNativeEvent(name, state); - } - } - }); - } - return obj._eventDispatcher; - } + 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; + }; - return { - /** - * Fires the specified event by name. Consult the - * event reference for more details on each event. - * - * @method fire - * @param {String} name Name of the event to fire. - * @param {Object?} args Event arguments. - * @param {Boolean?} bubble True/false if the event is to be bubbled. - * @return {Object} Event args instance passed in. - * @example - * instance.fire('event', {...}); - */ - fire: function (name, args, bubble) { - var self = this; + /** 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(); + }; - // Prevent all events except the remove event after the instance has been removed - if (self.removed && name !== "remove") { - return args; + /** + * 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)); + }; - args = getEventDispatcher(self).fire(name, args, bubble); + return { + cat: cat, + findMap: findMap, + liftN: liftN + }; + } +); - // Bubble event up to parents - if (bubble !== false && self.parent) { - var parent = self.parent(); - while (parent && !args.isPropagationStopped()) { - parent.fire(name, args, false); - parent = parent.parent(); - } - } +defineGlobal("global!Math", Math); +define( + 'ephox.sugar.selection.alien.Geometry', - return args; - }, + [ + 'global!Math' + ], - /** - * Binds an event listener to a specific event by name. Consult the - * event reference for more details on each event. - * - * @method on - * @param {String} name Event name or space separated list of events to bind. - * @param {callback} callback Callback to be executed when the event occurs. - * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. - * @return {Object} Current class instance. - * @example - * instance.on('event', function(e) { - * // Callback logic - * }); - */ - on: function (name, callback, prepend) { - return getEventDispatcher(this).on(name, callback, prepend); - }, + function (Math) { + var searchForPoint = function (rectForOffset, x, y, maxX, length) { + // easy cases + if (length === 0) return 0; + else if (x === maxX) return length - 1; - /** - * Unbinds an event listener to a specific event by name. Consult the - * event reference for more details on each event. - * - * @method off - * @param {String?} name Name of the event to unbind. - * @param {callback?} callback Callback to unbind. - * @return {Object} Current class instance. - * @example - * // Unbind specific callback - * instance.off('event', handler); - * - * // Unbind all listeners by name - * instance.off('event'); - * - * // Unbind all events - * instance.off(); - */ - off: function (name, callback) { - return getEventDispatcher(this).off(name, callback); - }, + var xDelta = maxX; - /** - * Bind the event callback and once it fires the callback is removed. Consult the - * event reference for more details on each event. - * - * @method once - * @param {String} name Name of the event to bind. - * @param {callback} callback Callback to bind only once. - * @return {Object} Current class instance. - */ - once: function (name, callback) { - return getEventDispatcher(this).once(name, callback); - }, + // start at 1, zero is the fallback + for (var i = 1; i < length; i++) { + var rect = rectForOffset(i); + var curDeltaX = Math.abs(x - rect.left); - /** - * Returns true/false if the object has a event of the specified name. - * - * @method hasEventListeners - * @param {String} name Name of the event to check for. - * @return {Boolean} true/false if the event exists or not. - */ - hasEventListeners: function (name) { - return getEventDispatcher(this).has(name); + if (y > rect.bottom) { + // range is too high, above drop point, do nothing + } else if (y < rect.top || curDeltaX > xDelta) { + // if the search winds up on the line below the drop point, + // or we pass the best X offset, + // wind back to the previous (best) delta + return i - 1; + } else { + // update current search delta + xDelta = curDeltaX; + } } + return 0; // always return something, even if it's not the exact offset it'll be better than nothing + }; + + var inRect = function (rect, x, y) { + return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; + }; + + return { + inRect: inRect, + searchForPoint: searchForPoint }; + } ); -/** - * ObservableObject.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 a object that is observable when properties changes a change event gets emitted. - * - * @private - * @class tinymce.data.ObservableObject - */ define( - 'tinymce.core.data.ObservableObject', - [ - 'tinymce.core.data.Binding', - 'tinymce.core.util.Class', - 'tinymce.core.util.Observable', - 'tinymce.core.util.Tools' - ], function (Binding, Class, Observable, Tools) { - function isNode(node) { - return node.nodeType > 0; - } + 'ephox.sugar.selection.query.TextPoint', - // Todo: Maybe this should be shallow compare since it might be huge object references - function isEqual(a, b) { - var k, checked; + [ + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.node.Text', + 'ephox.sugar.selection.alien.Geometry', + 'global!Math' + ], - // Strict equals - if (a === b) { - return true; - } + function (Option, Options, Text, Geometry, Math) { + var locateOffset = function (doc, textnode, x, y, rect) { + var rangeForOffset = function (offset) { + var r = doc.dom().createRange(); + r.setStart(textnode.dom(), offset); + r.collapse(true); + return r; + }; - // Compare null - if (a === null || b === null) { - return a === b; - } + var rectForOffset = function (offset) { + var r = rangeForOffset(offset); + return r.getBoundingClientRect(); + }; - // Compare number, boolean, string, undefined - if (typeof a !== "object" || typeof b !== "object") { - return a === b; - } + var length = Text.get(textnode).length; + var offset = Geometry.searchForPoint(rectForOffset, x, y, rect.right, length); + return rangeForOffset(offset); + }; - // Compare arrays - if (Tools.isArray(b)) { - if (a.length !== b.length) { - return false; - } + var locate = function (doc, node, x, y) { + var r = doc.dom().createRange(); + r.selectNode(node.dom()); + var rects = r.getClientRects(); + var foundRect = Options.findMap(rects, function (rect) { + return Geometry.inRect(rect, x, y) ? Option.some(rect) : Option.none(); + }); - k = a.length; - while (k--) { - if (!isEqual(a[k], b[k])) { - return false; - } - } - } + return foundRect.map(function (rect) { + return locateOffset(doc, node, x, y, rect); + }); + }; - // Shallow compare nodes - if (isNode(a) || isNode(b)) { - return a === b; - } + return { + locate: locate + }; + } +); - // Compare objects - checked = {}; - for (k in b) { - if (!isEqual(a[k], b[k])) { - return false; - } +define( + 'ephox.sugar.selection.query.ContainerPoint', - checked[k] = true; - } + [ + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.selection.alien.Geometry', + 'ephox.sugar.selection.query.TextPoint', + 'global!Math' + ], - for (k in a) { - if (!checked[k] && !isEqual(a[k], b[k])) { - return false; - } - } + function (Option, Options, Node, Traverse, Geometry, TextPoint, Math) { + /** + * Future idea: + * + * This code requires the drop point to be contained within the nodes array somewhere. If it isn't, + * we fall back to the extreme start or end of the node array contents. + * This isn't really what the user intended. + * + * In theory, we could just find the range point closest to the boxes representing the node + * (repartee does something similar). + */ - return true; - } + var searchInChildren = function (doc, node, x, y) { + var r = doc.dom().createRange(); + var nodes = Traverse.children(node); + return Options.findMap(nodes, function (n) { + // slight mutation because we assume creating ranges is expensive + r.selectNode(n.dom()); + return Geometry.inRect(r.getBoundingClientRect(), x, y) ? + locateNode(doc, n, x, y) : + Option.none(); + }); + }; - return Class.extend({ - Mixins: [Observable], + var locateNode = function (doc, node, x, y) { + var locator = Node.isText(node) ? TextPoint.locate : searchInChildren; + return locator(doc, node, x, y); + }; - /** - * Constructs a new observable object instance. - * - * @constructor - * @param {Object} data Initial data for the object. - */ - init: function (data) { - var name, value; + var locate = function (doc, node, x, y) { + var r = doc.dom().createRange(); + r.selectNode(node.dom()); + var rect = r.getBoundingClientRect(); + // Clamp x,y at the bounds of the node so that the locate function has SOME chance + var boundedX = Math.max(rect.left, Math.min(rect.right, x)); + var boundedY = Math.max(rect.top, Math.min(rect.bottom, y)); - data = data || {}; + return locateNode(doc, node, boundedX, boundedY); + }; - for (name in data) { - value = data[name]; + return { + locate: locate + }; + } +); - if (value instanceof Binding) { - data[name] = value.create(this, name); - } - } +define( + 'ephox.sugar.impl.ClosestOrAncestor', - this.data = data; - }, + [ + 'ephox.katamari.api.Type', + 'ephox.katamari.api.Option' + ], - /** - * Sets a property on the value this will call - * observers if the value is a change from the current value. - * - * @method set - * @param {String/object} name Name of the property to set or a object of items to set. - * @param {Object} value Value to set for the property. - * @return {tinymce.data.ObservableObject} Observable object instance. - */ - set: function (name, value) { - var key, args, oldValue = this.data[name]; + 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', - if (value instanceof Binding) { - value = value.create(this, name); - } + [ + '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' + ], - if (typeof name === "object") { - for (key in name) { - this.set(key, name[key]); - } + function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) { + var first = function (predicate) { + return descendant(Body.body(), predicate); + }; - return this; - } + var ancestor = function (scope, predicate, isRoot) { + var element = scope.dom(); + var stop = Type.isFunction(isRoot) ? isRoot : Fun.constant(false); - if (!isEqual(oldValue, value)) { - this.data[name] = value; + while (element.parentNode) { + element = element.parentNode; + var el = Element.fromDom(element); - args = { - target: this, - name: name, - value: value, - oldValue: oldValue - }; + if (predicate(el)) return Option.some(el); + else if (stop(el)) break; + } + return Option.none(); + }; - this.fire('change:' + name, args); - this.fire('change', args); - } + 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); + }; - return this; - }, + var sibling = function (scope, predicate) { + var element = scope.dom(); + if (!element.parentNode) return Option.none(); - /** - * Gets a property by name. - * - * @method get - * @param {String} name Name of the property to get. - * @return {Object} Object value of propery. - */ - get: function (name) { - return this.data[name]; - }, + return child(Element.fromDom(element.parentNode), function (x) { + return !Compare.eq(scope, x) && predicate(x); + }); + }; - /** - * Returns true/false if the specified property exists. - * - * @method has - * @param {String} name Name of the property to check for. - * @return {Boolean} true/false if the item exists. - */ - has: function (name) { - return name in this.data; - }, + var child = function (scope, predicate) { + var result = Arr.find(scope.dom().childNodes, + Fun.compose(predicate, Element.fromDom)); + return result.map(Element.fromDom); + }; - /** - * Returns a dynamic property binding for the specified property name. This makes - * it possible to sync the state of two properties in two ObservableObject instances. - * - * @method bind - * @param {String} name Name of the property to sync with the property it's inserted to. - * @return {tinymce.data.Binding} Data binding instance. - */ - bind: function (name) { - return Binding.create(this, name); - }, + 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])); - /** - * Destroys the observable object and fires the "destroy" - * event and clean up any internal resources. - * - * @method destroy - */ - destroy: function () { - this.fire('destroy'); - } - }); + 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 + }; } ); -/** - * Selector.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 no-nested-ternary:0 */ - -/** - * Selector engine, enables you to select controls by using CSS like expressions. - * We currently only support basic CSS expressions to reduce the size of the core - * and the ones we support should be enough for most cases. - * - * @example - * Supported expressions: - * element - * element#name - * element.class - * element[attr] - * element[attr*=value] - * element[attr~=value] - * element[attr!=value] - * element[attr^=value] - * element[attr$=value] - * element: - * element:not() - * element:first - * element:last - * element:odd - * element:even - * element element - * element > element - * - * @class tinymce.ui.Selector - */ define( - 'tinymce.core.ui.Selector', + 'ephox.sugar.api.selection.Awareness', + [ - "tinymce.core.util.Class" + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.node.Text', + 'ephox.sugar.api.search.Traverse' ], - function (Class) { - "use strict"; - /** - * Produces an array with a unique set of objects. It will not compare the values - * but the references of the objects. - * - * @private - * @method unqiue - * @param {Array} array Array to make into an array with unique items. - * @return {Array} Array with unique items. - */ - function unique(array) { - var uniqueItems = [], i = array.length, item; + function (Arr, Node, Text, Traverse) { + var getEnd = function (element) { + return Node.name(element) === 'img' ? 1 : Text.getOption(element).fold(function () { + return Traverse.children(element).length; + }, function (v) { + return v.length; + }); + }; - while (i--) { - item = array[i]; + var isEnd = function (element, offset) { + return getEnd(element) === offset; + }; - if (!item.__checked) { - uniqueItems.push(item); - item.__checked = 1; - } - } + var isStart = function (element, offset) { + return offset === 0; + }; - i = uniqueItems.length; - while (i--) { - delete uniqueItems[i].__checked; - } + var NBSP = '\u00A0'; - return uniqueItems; - } + var isTextNodeWithCursorPosition = function (el) { + return Text.getOption(el).filter(function (text) { + // For the purposes of finding cursor positions only allow text nodes with content, + // but trim removes   and that's allowed + return text.trim().length !== 0 || text.indexOf(NBSP) > -1; + }).isSome(); + }; - var expression = /^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i; + var elementsWithCursorPosition = [ 'img', 'br' ]; + var isCursorPosition = function (elem) { + var hasCursorPosition = isTextNodeWithCursorPosition(elem); + return hasCursorPosition || Arr.contains(elementsWithCursorPosition, Node.name(elem)); + }; + + return { + getEnd: getEnd, + isEnd: isEnd, + isStart: isStart, + isCursorPosition: isCursorPosition + }; + } +); - /*jshint maxlen:255 */ - /*eslint max-len:0 */ - var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - whiteSpace = /^\s*|\s*$/g, - Collection; +define( + 'ephox.sugar.api.selection.CursorPosition', - var Selector = Class.extend({ - /** - * Constructs a new Selector instance. - * - * @constructor - * @method init - * @param {String} selector CSS like selector expression. - */ - init: function (selector) { - var match = this.match; + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.search.PredicateFind', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.Awareness' + ], - function compileNameFilter(name) { - if (name) { - name = name.toLowerCase(); + function (Option, PredicateFind, Traverse, Awareness) { + var first = function (element) { + return PredicateFind.descendant(element, Awareness.isCursorPosition); + }; - return function (item) { - return name === '*' || item.type === name; - }; - } - } + var last = function (element) { + return descendantRtl(element, Awareness.isCursorPosition); + }; - function compileIdFilter(id) { - if (id) { - return function (item) { - return item._name === id; - }; - } + // Note, sugar probably needs some RTL traversals. + var descendantRtl = function (scope, predicate) { + var descend = function (element) { + var children = Traverse.children(element); + for (var i = children.length - 1; i >= 0; i--) { + var child = children[i]; + if (predicate(child)) return Option.some(child); + var res = descend(child); + if (res.isSome()) return res; } - function compileClassesFilter(classes) { - if (classes) { - classes = classes.split('.'); + return Option.none(); + }; + + return descend(scope); + }; - return function (item) { - var i = classes.length; + return { + first: first, + last: last + }; + } +); - while (i--) { - if (!item.classes.contains(classes[i])) { - return false; - } - } +define( + 'ephox.sugar.selection.query.EdgePoint', - return true; - }; - } - } + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.CursorPosition' + ], - function compileAttrFilter(name, cmp, check) { - if (name) { - return function (item) { - var value = item[name] ? item[name]() : ''; + function (Option, Traverse, CursorPosition) { + /* + * When a node has children, we return either the first or the last cursor + * position, whichever is closer horizontally + * + * When a node has no children, we return the start of end of the element, + * depending on which is closer horizontally + * */ - return !cmp ? !!check : - cmp === "=" ? value === check : - cmp === "*=" ? value.indexOf(check) >= 0 : - cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 : - cmp === "!=" ? value != check : - cmp === "^=" ? value.indexOf(check) === 0 : - cmp === "$=" ? value.substr(value.length - check.length) === check : - false; - }; - } - } + // TODO: Make this RTL compatible + var COLLAPSE_TO_LEFT = true; + var COLLAPSE_TO_RIGHT = false; - function compilePsuedoFilter(name) { - var notSelectors; + var getCollapseDirection = function (rect, x) { + return x - rect.left < rect.right - x ? COLLAPSE_TO_LEFT : COLLAPSE_TO_RIGHT; + }; - if (name) { - name = /(?:not\((.+)\))|(.+)/i.exec(name); + var createCollapsedNode = function (doc, target, collapseDirection) { + var r = doc.dom().createRange(); + r.selectNode(target.dom()); + r.collapse(collapseDirection); + return r; + }; - if (!name[1]) { - name = name[2]; + var locateInElement = function (doc, node, x) { + var cursorRange = doc.dom().createRange(); + cursorRange.selectNode(node.dom()); + var rect = cursorRange.getBoundingClientRect(); + var collapseDirection = getCollapseDirection(rect, x); - return function (item, index, length) { - return name === 'first' ? index === 0 : - name === 'last' ? index === length - 1 : - name === 'even' ? index % 2 === 0 : - name === 'odd' ? index % 2 === 1 : - item[name] ? item[name]() : - false; - }; - } + var f = collapseDirection === COLLAPSE_TO_LEFT ? CursorPosition.first : CursorPosition.last; + return f(node).map(function (target) { + return createCollapsedNode(doc, target, collapseDirection); + }); + }; - // Compile not expression - notSelectors = parseChunks(name[1], []); + var locateInEmpty = function (doc, node, x) { + var rect = node.dom().getBoundingClientRect(); + var collapseDirection = getCollapseDirection(rect, x); + return Option.some(createCollapsedNode(doc, node, collapseDirection)); + }; - return function (item) { - return !match(item, notSelectors); - }; - } - } + var search = function (doc, node, x) { + var f = Traverse.children(node).length === 0 ? locateInEmpty : locateInElement; + return f(doc, node, x); + }; - function compile(selector, filters, direct) { - var parts; + return { + search: search + }; + } +); - function add(filter) { - if (filter) { - filters.push(filter); - } - } +define( + 'ephox.sugar.selection.query.CaretRange', - // Parse expression into parts - parts = expression.exec(selector.replace(whiteSpace, '')); + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.selection.query.ContainerPoint', + 'ephox.sugar.selection.query.EdgePoint', + 'global!document', + 'global!Math' + ], + + function (Option, Element, Traverse, Selection, ContainerPoint, EdgePoint, document, Math) { + var caretPositionFromPoint = function (doc, x, y) { + return Option.from(doc.dom().caretPositionFromPoint(x, y)).bind(function (pos) { + // It turns out that Firefox can return null for pos.offsetNode + if (pos.offsetNode === null) return Option.none(); + var r = doc.dom().createRange(); + r.setStart(pos.offsetNode, pos.offset); + r.collapse(); + return Option.some(r); + }); + }; - add(compileNameFilter(parts[1])); - add(compileIdFilter(parts[2])); - add(compileClassesFilter(parts[3])); - add(compileAttrFilter(parts[4], parts[5], parts[6])); - add(compilePsuedoFilter(parts[7])); + var caretRangeFromPoint = function (doc, x, y) { + return Option.from(doc.dom().caretRangeFromPoint(x, y)); + }; - // Mark the filter with pseudo for performance - filters.pseudo = !!parts[7]; - filters.direct = direct; + var searchTextNodes = function (doc, node, x, y) { + var r = doc.dom().createRange(); + r.selectNode(node.dom()); + var rect = r.getBoundingClientRect(); + // Clamp x,y at the bounds of the node so that the locate function has SOME chance + var boundedX = Math.max(rect.left, Math.min(rect.right, x)); + var boundedY = Math.max(rect.top, Math.min(rect.bottom, y)); - return filters; - } + return ContainerPoint.locate(doc, node, boundedX, boundedY); + }; - // Parser logic based on Sizzle by John Resig - function parseChunks(selector, selectors) { - var parts = [], extra, matches, i; + var searchFromPoint = function (doc, x, y) { + // elementFromPoint is defined to return null when there is no element at the point + // This often happens when using IE10 event.y instead of event.clientY + return Option.from(doc.dom().elementFromPoint(x, y)).map(Element.fromDom).bind(function (elem) { + // used when the x,y position points to an image, or outside the bounds + var fallback = function () { + return EdgePoint.search(doc, elem, x); + }; - do { - chunker.exec(""); - matches = chunker.exec(selector); + return Traverse.children(elem).length === 0 ? fallback() : + // if we have children, search for the right text node and then get the offset out of it + searchTextNodes(doc, elem, x, y).orThunk(fallback); + }); + }; - if (matches) { - selector = matches[3]; - parts.push(matches[1]); + var availableSearch = document.caretPositionFromPoint ? caretPositionFromPoint : // defined standard + document.caretRangeFromPoint ? caretRangeFromPoint : // webkit implementation + searchFromPoint; // fallback - if (matches[2]) { - extra = matches[3]; - break; - } - } - } while (matches); - if (extra) { - parseChunks(extra, selectors); - } + var fromPoint = function (win, x, y) { + var doc = Element.fromDom(win.document); + return availableSearch(doc, x, y).map(function (rng) { + return Selection.range( + Element.fromDom(rng.startContainer), + rng.startOffset, + Element.fromDom(rng.endContainer), + rng.endOffset + ); + }); + }; - selector = []; - for (i = 0; i < parts.length; i++) { - if (parts[i] != '>') { - selector.push(compile(parts[i], [], parts[i - 1] === '>')); - } - } + return { + fromPoint: fromPoint + }; + } +); - selectors.push(selector); +define( + 'ephox.sugar.selection.query.Within', - return selectors; - } + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.search.SelectorFilter', + 'ephox.sugar.api.search.Selectors', + 'ephox.sugar.selection.core.NativeRange', + 'ephox.sugar.selection.core.SelectionDirection' + ], + + function (Arr, Element, Node, SelectorFilter, Selectors, NativeRange, SelectionDirection) { + var withinContainer = function (win, ancestor, outerRange, selector) { + var innerRange = NativeRange.create(win); + var self = Selectors.is(ancestor, selector) ? [ ancestor ] : []; + var elements = self.concat(SelectorFilter.descendants(ancestor, selector)); + return Arr.filter(elements, function (elem) { + // Mutate the selection to save creating new ranges each time + NativeRange.selectNodeContentsUsing(innerRange, elem); + return NativeRange.isWithin(outerRange, innerRange); + }); + }; - this._selectors = parseChunks(selector, []); - }, + var find = function (win, selection, selector) { + // Reverse the selection if it is RTL when doing the comparison + var outerRange = SelectionDirection.asLtrRange(win, selection); + var ancestor = Element.fromDom(outerRange.commonAncestorContainer); + // Note, this might need to change when we have to start looking for non elements. + return Node.isElement(ancestor) ? + withinContainer(win, ancestor, outerRange, selector) : []; + }; - /** - * Returns true/false if the selector matches the specified control. - * - * @method match - * @param {tinymce.ui.Control} control Control to match against the selector. - * @param {Array} selectors Optional array of selectors, mostly used internally. - * @return {Boolean} true/false state if the control matches or not. - */ - match: function (control, selectors) { - var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item; + return { + find: find + }; + } +); +define( + 'ephox.sugar.selection.quirks.Prefilter', + + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.api.selection.Situ' + ], - selectors = selectors || this._selectors; - for (i = 0, l = selectors.length; i < l; i++) { - selector = selectors[i]; - sl = selector.length; - item = control; - count = 0; + function (Arr, Node, Selection, Situ) { + var beforeSpecial = function (element, offset) { + // From memory, we don't want to use
    directly on Firefox because it locks the keyboard input. + // It turns out that directly on IE locks the keyboard as well. + // If the offset is 0, use before. If the offset is 1, use after. + // TBIO-3889: Firefox Situ.on results in a child of the ; Situ.before results in platform inconsistencies + var name = Node.name(element); + if ('input' === name) return Situ.after(element); + else if (!Arr.contains([ 'br', 'img' ], name)) return Situ.on(element, offset); + else return offset === 0 ? Situ.before(element) : Situ.after(element); + }; - for (si = sl - 1; si >= 0; si--) { - filters = selector[si]; + var preprocess = function (startSitu, finishSitu) { + var start = startSitu.fold(Situ.before, beforeSpecial, Situ.after); + var finish = finishSitu.fold(Situ.before, beforeSpecial, Situ.after); + return Selection.relative(start, finish); + }; - while (item) { - // Find the index and length since a pseudo filter like :first needs it - if (filters.pseudo) { - siblings = item.parent().items(); - index = length = siblings.length; - while (index--) { - if (siblings[index] === item) { - break; - } - } - } + return { + beforeSpecial: beforeSpecial, + preprocess: preprocess + }; + } +); - for (fi = 0, fl = filters.length; fi < fl; fi++) { - if (!filters[fi](item, index, length)) { - fi = fl + 1; - break; - } - } +define( + 'ephox.sugar.api.selection.WindowSelection', - if (fi === fl) { - count++; - break; - } else { - // If it didn't match the right most expression then - // break since it's no point looking at the parents - if (si === sl - 1) { - break; - } - } + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.DocumentPosition', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Fragment', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.api.selection.Situ', + 'ephox.sugar.selection.core.NativeRange', + 'ephox.sugar.selection.core.SelectionDirection', + 'ephox.sugar.selection.query.CaretRange', + 'ephox.sugar.selection.query.Within', + 'ephox.sugar.selection.quirks.Prefilter' + ], - item = item.parent(); - } - } + function (Option, DocumentPosition, Element, Fragment, Selection, Situ, NativeRange, SelectionDirection, CaretRange, Within, Prefilter) { + var doSetNativeRange = function (win, rng) { + var selection = win.getSelection(); + selection.removeAllRanges(); + selection.addRange(rng); + }; - // If we found all selectors then return true otherwise continue looking - if (count === sl) { - return true; + var doSetRange = function (win, start, soffset, finish, foffset) { + var rng = NativeRange.exactToNative(win, start, soffset, finish, foffset); + doSetNativeRange(win, rng); + }; + + var findWithin = function (win, selection, selector) { + return Within.find(win, selection, selector); + }; + + var setExact = function (win, start, soffset, finish, foffset) { + setRelative(win, Situ.on(start, soffset), Situ.on(finish, foffset)); + }; + + var setRelative = function (win, startSitu, finishSitu) { + var relative = Prefilter.preprocess(startSitu, finishSitu); + + return SelectionDirection.diagnose(win, relative).match({ + ltr: function (start, soffset, finish, foffset) { + doSetRange(win, start, soffset, finish, foffset); + }, + rtl: function (start, soffset, finish, foffset) { + var selection = win.getSelection(); + // If this selection is backwards, then we need to use extend. + if (selection.extend) { + selection.collapse(start.dom(), soffset); + selection.extend(finish.dom(), foffset); + } else { + doSetRange(win, finish, foffset, start, soffset); } } + }); + }; - return false; - }, + // NOTE: We are still reading the range because it gives subtly different behaviour + // than using the anchorNode and focusNode. I'm not sure if this behaviour is any + // better or worse; it's just different. + var readRange = function (selection) { + var rng = Option.from(selection.getRangeAt(0)); + return rng.map(function (r) { + return Selection.range(Element.fromDom(r.startContainer), r.startOffset, Element.fromDom(r.endContainer), r.endOffset); + }); + }; - /** - * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container. - * - * @method find - * @param {tinymce.ui.Control} container Container to look for items in. - * @return {tinymce.ui.Collection} Collection with matched elements. - */ - find: function (container) { - var matches = [], i, l, selectors = this._selectors; + var doGetExact = function (selection) { + var anchorNode = Element.fromDom(selection.anchorNode); + var focusNode = Element.fromDom(selection.focusNode); + return DocumentPosition.after(anchorNode, selection.anchorOffset, focusNode, selection.focusOffset) ? Option.some( + Selection.range( + Element.fromDom(selection.anchorNode), + selection.anchorOffset, + Element.fromDom(selection.focusNode), + selection.focusOffset + ) + ) : readRange(selection); + }; - function collect(items, selector, index) { - var i, l, fi, fl, item, filters = selector[index]; + var setToElement = function (win, element) { + var rng = NativeRange.selectNodeContents(win, element); + doSetNativeRange(win, rng); + }; - for (i = 0, l = items.length; i < l; i++) { - item = items[i]; + var forElement = function (win, element) { + var rng = NativeRange.selectNodeContents(win, element); + return Selection.range( + Element.fromDom(rng.startContainer), rng.startOffset, + Element.fromDom(rng.endContainer), rng.endOffset + ); + }; - // Run each filter against the item - for (fi = 0, fl = filters.length; fi < fl; fi++) { - if (!filters[fi](item, i, l)) { - fi = fl + 1; - break; - } - } + var getExact = function (win) { + // We want to retrieve the selection as it is. + var selection = win.getSelection(); + return selection.rangeCount > 0 ? doGetExact(selection) : Option.none(); + }; - // All filters matched the item - if (fi === fl) { - // Matched item is on the last expression like: panel toolbar [button] - if (index == selector.length - 1) { - matches.push(item); - } else { - // Collect next expression type - if (item.items) { - collect(item.items(), selector, index + 1); - } - } - } else if (filters.direct) { - return; - } + // TODO: Test this. + var get = function (win) { + return getExact(win).map(function (range) { + return Selection.exact(range.start(), range.soffset(), range.finish(), range.foffset()); + }); + }; - // Collect child items - if (item.items) { - collect(item.items(), selector, index); - } - } - } + var getFirstRect = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.getFirstRect(rng); + }; - if (container.items) { - for (i = 0, l = selectors.length; i < l; i++) { - collect(container.items(), selectors[i], 0); - } + var getBounds = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.getBounds(rng); + }; - // Unique the matches if needed - if (l > 1) { - matches = unique(matches); - } - } + var getAtPoint = function (win, x, y) { + return CaretRange.fromPoint(win, x, y); + }; - // Fix for circular reference - if (!Collection) { - // TODO: Fix me! - Collection = Selector.Collection; - } + var getAsString = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.toString(rng); + }; - return new Collection(matches); - } - }); + var clear = function (win) { + var selection = win.getSelection(); + selection.removeAllRanges(); + }; + + var clone = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + return NativeRange.cloneFragment(rng); + }; + + var replace = function (win, selection, elements) { + var rng = SelectionDirection.asLtrRange(win, selection); + var fragment = Fragment.fromElements(elements); + NativeRange.replaceWith(rng, fragment); + }; + + var deleteAt = function (win, selection) { + var rng = SelectionDirection.asLtrRange(win, selection); + NativeRange.deleteContents(rng); + }; - return Selector; + return { + setExact: setExact, + getExact: getExact, + get: get, + setRelative: setRelative, + setToElement: setToElement, + clear: clear, + + clone: clone, + replace: replace, + deleteAt: deleteAt, + + forElement: forElement, + + getFirstRect: getFirstRect, + getBounds: getBounds, + getAtPoint: getAtPoint, + + findWithin: findWithin, + getAsString: getAsString + }; } ); -/** - * Collection.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 - */ - -/** - * Control collection, this class contains control instances and it enables you to - * perform actions on all the contained items. This is very similar to how jQuery works. - * - * @example - * someCollection.show().disabled(true); - * - * @class tinymce.ui.Collection - */ define( - 'tinymce.core.ui.Collection', + 'tinymce.core.selection.SelectionBookmark', + [ - "tinymce.core.util.Tools", - "tinymce.core.ui.Selector", - "tinymce.core.util.Class" + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.node.Node', + 'ephox.sugar.api.node.Text', + 'ephox.sugar.api.search.Traverse', + 'ephox.sugar.api.selection.Selection', + 'ephox.sugar.api.selection.WindowSelection', + 'global!document', + 'tinymce.core.caret.CaretFinder' ], - function (Tools, Selector, Class) { - "use strict"; - var Collection, proto, push = Array.prototype.push, slice = Array.prototype.slice; + function (Fun, Option, Compare, Element, Node, Text, Traverse, Selection, WindowSelection, document, CaretFinder) { + var clamp = function (offset, element) { + var max = Node.isText(element) ? Text.get(element).length : Traverse.children(element).length + 1; - proto = { - /** - * Current number of contained control instances. - * - * @field length - * @type Number - */ - length: 0, + if (offset > max) { + return max; + } else if (offset < 0) { + return 0; + } - /** - * Constructor for the collection. - * - * @constructor - * @method init - * @param {Array} items Optional array with items to add. - */ - init: function (items) { - if (items) { - this.add(items); - } - }, + return offset; + }; - /** - * Adds new items to the control collection. - * - * @method add - * @param {Array} items Array if items to add to collection. - * @return {tinymce.ui.Collection} Current collection instance. - */ - add: function (items) { - var self = this; + var normalizeRng = function (rng) { + return Selection.range( + rng.start(), + clamp(rng.soffset(), rng.start()), + rng.finish(), + clamp(rng.foffset(), rng.finish()) + ); + }; - // Force single item into array - if (!Tools.isArray(items)) { - if (items instanceof Collection) { - self.add(items.toArray()); - } else { - push.call(self, items); - } - } else { - push.apply(self, items); - } + var isOrContains = function (root, elm) { + return Compare.contains(root, elm) || Compare.eq(root, elm); + }; - return self; - }, + var isRngInRoot = function (root) { + return function (rng) { + return isOrContains(root, rng.start()) && isOrContains(root, rng.finish()); + }; + }; - /** - * Sets the contents of the collection. This will remove any existing items - * and replace them with the ones specified in the input array. - * - * @method set - * @param {Array} items Array with items to set into the Collection. - * @return {tinymce.ui.Collection} Collection instance. - */ - set: function (items) { - var self = this, len = self.length, i; + // var dumpRng = function (rng) { + // console.log('start', rng.start().dom()); + // console.log('soffset', rng.soffset()); + // console.log('finish', rng.finish().dom()); + // console.log('foffset', rng.foffset()); + // return rng; + // }; - self.length = 0; - self.add(items); + var getBookmark = function (root) { + var win = Traverse.defaultView(root); - // Remove old entries - for (i = self.length; i < len; i++) { - delete self[i]; - } + return WindowSelection.getExact(win.dom()) + .filter(isRngInRoot(root)); + }; - return self; - }, + var validate = function (root, bookmark) { + return Option.from(bookmark) + .filter(isRngInRoot(root)) + .map(normalizeRng); + }; - /** - * Filters the collection item based on the specified selector expression or selector function. - * - * @method filter - * @param {String} selector Selector expression to filter items by. - * @return {tinymce.ui.Collection} Collection containing the filtered items. - */ - filter: function (selector) { - var self = this, i, l, matches = [], item, match; + var bookmarkToNativeRng = function (bookmark) { + var rng = document.createRange(); + rng.setStart(bookmark.start().dom(), bookmark.soffset()); + rng.setEnd(bookmark.finish().dom(), bookmark.foffset()); - // Compile string into selector expression - if (typeof selector === "string") { - selector = new Selector(selector); + return Option.some(rng); + }; - match = function (item) { - return selector.match(item); - }; - } else { - // Use selector as matching function - match = selector; - } + var store = function (editor) { + var newBookmark = getBookmark(Element.fromDom(editor.getBody())); - for (i = 0, l = self.length; i < l; i++) { - item = self[i]; + editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark; + }; - if (match(item)) { - matches.push(item); - } - } + var getRng = function (editor) { + var bookmark = editor.bookmark ? editor.bookmark : Option.none(); - return new Collection(matches); - }, + return bookmark + .bind(Fun.curry(validate, Element.fromDom(editor.getBody()))) + .bind(bookmarkToNativeRng); + }; - /** - * Slices the items within the collection. - * - * @method slice - * @param {Number} index Index to slice at. - * @param {Number} len Optional length to slice. - * @return {tinymce.ui.Collection} Current collection. - */ - slice: function () { - return new Collection(slice.apply(this, arguments)); - }, + var restore = function (editor) { + getRng(editor).each(function (rng) { + editor.selection.setRng(rng); + }); + }; - /** - * Makes the current collection equal to the specified index. - * - * @method eq - * @param {Number} index Index of the item to set the collection to. - * @return {tinymce.ui.Collection} Current collection. - */ - eq: function (index) { - return index === -1 ? this.slice(index) : this.slice(index, +index + 1); - }, - - /** - * Executes the specified callback on each item in collection. - * - * @method each - * @param {function} callback Callback to execute for each item in collection. - * @return {tinymce.ui.Collection} Current collection instance. - */ - each: function (callback) { - Tools.each(this, callback); - - return this; - }, - - /** - * Returns an JavaScript array object of the contents inside the collection. - * - * @method toArray - * @return {Array} Array with all items from collection. - */ - toArray: function () { - return Tools.toArray(this); - }, - - /** - * Finds the index of the specified control or return -1 if it isn't in the collection. - * - * @method indexOf - * @param {Control} ctrl Control instance to look for. - * @return {Number} Index of the specified control or -1. - */ - indexOf: function (ctrl) { - var self = this, i = self.length; - - while (i--) { - if (self[i] === ctrl) { - break; - } - } - - return i; - }, - - /** - * Returns a new collection of the contents in reverse order. - * - * @method reverse - * @return {tinymce.ui.Collection} Collection instance with reversed items. - */ - reverse: function () { - return new Collection(Tools.toArray(this).reverse()); - }, - - /** - * Returns true/false if the class exists or not. - * - * @method hasClass - * @param {String} cls Class to check for. - * @return {Boolean} true/false state if the class exists or not. - */ - hasClass: function (cls) { - return this[0] ? this[0].classes.contains(cls) : false; - }, - - /** - * Sets/gets the specific property on the items in the collection. The same as executing control.(); - * - * @method prop - * @param {String} name Property name to get/set. - * @param {Object} value Optional object value to set. - * @return {tinymce.ui.Collection} Current collection instance or value of the first item on a get operation. - */ - prop: function (name, value) { - var self = this, undef, item; - - if (value !== undef) { - self.each(function (item) { - if (item[name]) { - item[name](value); - } - }); - - return self; - } - - item = self[0]; - - if (item && item[name]) { - return item[name](); - } - }, - - /** - * Executes the specific function name with optional arguments an all items in collection if it exists. - * - * @example collection.exec("myMethod", arg1, arg2, arg3); - * @method exec - * @param {String} name Name of the function to execute. - * @param {Object} ... Multiple arguments to pass to each function. - * @return {tinymce.ui.Collection} Current collection. - */ - exec: function (name) { - var self = this, args = Tools.toArray(arguments).slice(1); - - self.each(function (item) { - if (item[name]) { - item[name].apply(item, args); - } - }); - - return self; - }, - - /** - * Remove all items from collection and DOM. - * - * @method remove - * @return {tinymce.ui.Collection} Current collection. - */ - remove: function () { - var i = this.length; - - while (i--) { - this[i].remove(); - } - - return this; - }, - - /** - * Adds a class to all items in the collection. - * - * @method addClass - * @param {String} cls Class to add to each item. - * @return {tinymce.ui.Collection} Current collection instance. - */ - addClass: function (cls) { - return this.each(function (item) { - item.classes.add(cls); - }); - }, - - /** - * Removes the specified class from all items in collection. - * - * @method removeClass - * @param {String} cls Class to remove from each item. - * @return {tinymce.ui.Collection} Current collection instance. - */ - removeClass: function (cls) { - return this.each(function (item) { - item.classes.remove(cls); - }); - } - - /** - * Fires the specified event by name and arguments on the control. This will execute all - * bound event handlers. - * - * @method fire - * @param {String} name Name of the event to fire. - * @param {Object} args Optional arguments to pass to the event. - * @return {tinymce.ui.Collection} Current collection instance. - */ - // fire: function(event, args) {}, -- Generated by code below - - /** - * Binds a callback to the specified event. This event can both be - * native browser events like "click" or custom ones like PostRender. - * - * The callback function will have two parameters the first one being the control that received the event - * the second one will be the event object either the browsers native event object or a custom JS object. - * - * @method on - * @param {String} name Name of the event to bind. For example "click". - * @param {String/function} callback Callback function to execute ones the event occurs. - * @return {tinymce.ui.Collection} Current collection instance. - */ - // on: function(name, callback) {}, -- Generated by code below - - /** - * Unbinds the specified event and optionally a specific callback. If you omit the name - * parameter all event handlers will be removed. If you omit the callback all event handles - * by the specified name will be removed. - * - * @method off - * @param {String} name Optional name for the event to unbind. - * @param {function} callback Optional callback function to unbind. - * @return {tinymce.ui.Collection} Current collection instance. - */ - // off: function(name, callback) {}, -- Generated by code below - - /** - * Shows the items in the current collection. - * - * @method show - * @return {tinymce.ui.Collection} Current collection instance. - */ - // show: function() {}, -- Generated by code below - - /** - * Hides the items in the current collection. - * - * @method hide - * @return {tinymce.ui.Collection} Current collection instance. - */ - // hide: function() {}, -- Generated by code below - - /** - * Sets/gets the text contents of the items in the current collection. - * - * @method text - * @return {tinymce.ui.Collection} Current collection instance or text value of the first item on a get operation. - */ - // text: function(value) {}, -- Generated by code below - - /** - * Sets/gets the name contents of the items in the current collection. - * - * @method name - * @return {tinymce.ui.Collection} Current collection instance or name value of the first item on a get operation. - */ - // name: function(value) {}, -- Generated by code below - - /** - * Sets/gets the disabled state on the items in the current collection. - * - * @method disabled - * @return {tinymce.ui.Collection} Current collection instance or disabled state of the first item on a get operation. - */ - // disabled: function(state) {}, -- Generated by code below - - /** - * Sets/gets the active state on the items in the current collection. - * - * @method active - * @return {tinymce.ui.Collection} Current collection instance or active state of the first item on a get operation. - */ - // active: function(state) {}, -- Generated by code below - - /** - * Sets/gets the selected state on the items in the current collection. - * - * @method selected - * @return {tinymce.ui.Collection} Current collection instance or selected state of the first item on a get operation. - */ - // selected: function(state) {}, -- Generated by code below - - /** - * Sets/gets the selected state on the items in the current collection. - * - * @method visible - * @return {tinymce.ui.Collection} Current collection instance or visible state of the first item on a get operation. - */ - // visible: function(state) {}, -- Generated by code below + return { + store: store, + restore: restore, + getRng: getRng, + getBookmark: getBookmark, + validate: validate }; - - // Extend tinymce.ui.Collection prototype with some generated control specific methods - Tools.each('fire on off show hide append prepend before after reflow'.split(' '), function (name) { - proto[name] = function () { - var args = Tools.toArray(arguments); - - this.each(function (ctrl) { - if (name in ctrl) { - ctrl[name].apply(ctrl, args); - } - }); - - return this; - }; - }); - - // Extend tinymce.ui.Collection prototype with some property methods - Tools.each('text name disabled active selected checked visible parent value data'.split(' '), function (name) { - proto[name] = function (value) { - return this.prop(name, value); - }; - }); - - // Create class based on the new prototype - Collection = Class.extend(proto); - - // Stick Collection into Selector to prevent circual references - Selector.Collection = Collection; - - return Collection; } ); + /** - * BoxUtils.js + * WindowManagerImpl.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -22958,101 +21937,30 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Utility class for box parsing and measuring. - * - * @private - * @class tinymce.ui.BoxUtils - */ define( - 'tinymce.core.ui.BoxUtils', + 'tinymce.core.ui.WindowManagerImpl', [ ], function () { - "use strict"; - - return { - /** - * Parses the specified box value. A box value contains 1-4 properties in clockwise order. - * - * @method parseBox - * @param {String/Number} value Box value "0 1 2 3" or "0" etc. - * @return {Object} Object with top/right/bottom/left properties. - * @private - */ - parseBox: function (value) { - var len, radix = 10; - - if (!value) { - return; - } - - if (typeof value === "number") { - value = value || 0; - - return { - top: value, - left: value, - bottom: value, - right: value - }; - } - - value = value.split(' '); - len = value.length; - - if (len === 1) { - value[1] = value[2] = value[3] = value[0]; - } else if (len === 2) { - value[2] = value[0]; - value[3] = value[1]; - } else if (len === 3) { - value[3] = value[1]; - } - - return { - top: parseInt(value[0], radix) || 0, - right: parseInt(value[1], radix) || 0, - bottom: parseInt(value[2], radix) || 0, - left: parseInt(value[3], radix) || 0 - }; - }, - - measureBox: function (elm, prefix) { - function getStyle(name) { - var defaultView = document.defaultView; - - if (defaultView) { - // Remove camelcase - name = name.replace(/[A-Z]/g, function (a) { - return '-' + a; - }); - - return defaultView.getComputedStyle(elm, null).getPropertyValue(name); - } - - return elm.currentStyle[name]; - } - - function getSide(name) { - var val = parseFloat(getStyle(name), 10); - - return isNaN(val) ? 0 : val; - } + return function () { + var unimplemented = function () { + throw new Error('Theme did not provide a WindowManager implementation.'); + }; - return { - top: getSide(prefix + "TopWidth"), - right: getSide(prefix + "RightWidth"), - bottom: getSide(prefix + "BottomWidth"), - left: getSide(prefix + "LeftWidth") - }; - } + return { + open: unimplemented, + alert: unimplemented, + confirm: unimplemented, + close: unimplemented, + getParams: unimplemented, + setParams: unimplemented + }; }; } ); /** - * ClassList.js + * WindowManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -23062,150 +21970,226 @@ define( */ /** - * Handles adding and removal of classes. + * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs. * - * @private - * @class tinymce.ui.ClassList + * @class tinymce.WindowManager + * @example + * // Opens a new dialog with the file.htm file and the size 320x240 + * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. + * tinymce.activeEditor.windowManager.open({ + * url: 'file.htm', + * width: 320, + * height: 240 + * }, { + * custom_param: 1 + * }); + * + * // Displays an alert box using the active editors window manager instance + * tinymce.activeEditor.windowManager.alert('Hello world!'); + * + * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm + * }); */ define( - 'tinymce.core.ui.ClassList', + 'tinymce.core.api.WindowManager', [ - "tinymce.core.util.Tools" + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'tinymce.core.selection.SelectionBookmark', + 'tinymce.core.ui.WindowManagerImpl' ], - function (Tools) { - "use strict"; + function (Arr, Option, SelectionBookmark, WindowManagerImpl) { + return function (editor) { + var windows = []; - function noop() { - } + var getImplementation = function () { + var theme = editor.theme; + return theme && theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl(); + }; - /** - * Constructs a new class list the specified onchange - * callback will be executed when the class list gets modifed. - * - * @constructor ClassList - * @param {function} onchange Onchange callback to be executed. - */ - function ClassList(onchange) { - this.cls = []; - this.cls._map = {}; - this.onchange = onchange || noop; - this.prefix = ''; - } + var funcBind = function (scope, f) { + return function () { + return f ? f.apply(scope, arguments) : undefined; + }; + }; - Tools.extend(ClassList.prototype, { - /** - * Adds a new class to the class list. - * - * @method add - * @param {String} cls Class to be added. - * @return {tinymce.ui.ClassList} Current class list instance. - */ - add: function (cls) { - if (cls && !this.contains(cls)) { - this.cls._map[cls] = true; - this.cls.push(cls); - this._change(); - } + var fireOpenEvent = function (win) { + editor.fire('OpenWindow', { + win: win + }); + }; - return this; - }, + var fireCloseEvent = function (win) { + editor.fire('CloseWindow', { + win: win + }); + }; - /** - * Removes the specified class from the class list. - * - * @method remove - * @param {String} cls Class to be removed. - * @return {tinymce.ui.ClassList} Current class list instance. - */ - remove: function (cls) { - if (this.contains(cls)) { - for (var i = 0; i < this.cls.length; i++) { - if (this.cls[i] === cls) { - break; - } + var addWindow = function (win) { + windows.push(win); + fireOpenEvent(win); + }; + + var closeWindow = function (win) { + Arr.findIndex(windows, function (otherWindow) { + return otherWindow === win; + }).each(function (index) { + // Mutate here since third party might have stored away the window array, consider breaking this api + windows.splice(index, 1); + + fireCloseEvent(win); + + // Move focus back to editor when the last window is closed + if (windows.length === 0) { + editor.focus(); } + }); + }; - this.cls.splice(i, 1); - delete this.cls._map[cls]; - this._change(); - } + var getTopWindow = function () { + return Option.from(windows[windows.length - 1]); + }; - return this; - }, + var open = function (args, params) { + editor.editorManager.setActive(editor); + SelectionBookmark.store(editor); - /** - * Toggles a class in the class list. - * - * @method toggle - * @param {String} cls Class to be added/removed. - * @param {Boolean} state Optional state if it should be added/removed. - * @return {tinymce.ui.ClassList} Current class list instance. - */ - toggle: function (cls, state) { - var curState = this.contains(cls); + var win = getImplementation().open(args, params, closeWindow); + addWindow(win); + return win; + }; - if (curState !== state) { - if (curState) { - this.remove(cls); - } else { - this.add(cls); - } + var alert = function (message, callback, scope) { + var win = getImplementation().alert(message, funcBind(scope ? scope : this, callback), closeWindow); + addWindow(win); + }; - this._change(); - } + var confirm = function (message, callback, scope) { + var win = getImplementation().confirm(message, funcBind(scope ? scope : this, callback), closeWindow); + addWindow(win); + }; - return this; - }, + var close = function () { + getTopWindow().each(function (win) { + getImplementation().close(win); + closeWindow(win); + }); + }; - /** - * Returns true if the class list has the specified class. - * - * @method contains - * @param {String} cls Class to look for. - * @return {Boolean} true/false if the class exists or not. - */ - contains: function (cls) { - return !!this.cls._map[cls]; - }, + var getParams = function () { + return getTopWindow().map(getImplementation().getParams).getOr(null); + }; - /** - * Returns a space separated list of classes. - * - * @method toString - * @return {String} Space separated list of classes. - */ + var setParams = function (params) { + getTopWindow().each(function (win) { + getImplementation().setParams(win, params); + }); + }; - _change: function () { - delete this.clsValue; - this.onchange.call(this); - } - }); + var getWindows = function () { + return windows; + }; - // IE 8 compatibility - ClassList.prototype.toString = function () { - var value; + editor.on('remove', function () { + Arr.each(windows.slice(0), function (win) { + getImplementation().close(win); + }); + }); - if (this.clsValue) { - return this.clsValue; - } + return { + // Used by the legacy3x compat layer and possible third party + // TODO: Deprecate this, and possible switch to a immutable window array for getWindows + windows: windows, - value = ''; - for (var i = 0; i < this.cls.length; i++) { - if (i > 0) { - value += ' '; - } + /** + * Opens a new window. + * + * @method open + * @param {Object} args Optional name/value settings collection contains things like width/height/url etc. + * @param {Object} params Options like title, file, width, height etc. + * @option {String} title Window title. + * @option {String} file URL of the file to open in the window. + * @option {Number} width Width in pixels. + * @option {Number} height Height in pixels. + * @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content + * larger than the popup size specified). + */ + open: open, - value += this.prefix + this.cls[i]; - } + /** + * Creates a alert dialog. Please don't use the blocking behavior of this + * native version use the callback method instead then it can be extended. + * + * @method alert + * @param {String} message Text to display in the new alert dialog. + * @param {function} callback Callback function to be executed after the user has selected ok. + * @param {Object} scope Optional scope to execute the callback in. + * @example + * // Displays an alert box using the active editors window manager instance + * tinymce.activeEditor.windowManager.alert('Hello world!'); + */ + alert: alert, - return value; - }; + /** + * Creates a confirm dialog. Please don't use the blocking behavior of this + * native version use the callback method instead then it can be extended. + * + * @method confirm + * @param {String} message Text to display in the new confirm dialog. + * @param {function} callback Callback function to be executed after the user has selected ok or cancel. + * @param {Object} scope Optional scope to execute the callback in. + * @example + * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm + * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { + * if (s) + * tinymce.activeEditor.windowManager.alert("Ok"); + * else + * tinymce.activeEditor.windowManager.alert("Cancel"); + * }); + */ + confirm: confirm, + + /** + * Closes the top most window. + * + * @method close + */ + close: close, + + /** + * Returns the params of the last window open call. This can be used in iframe based + * dialog to get params passed from the tinymce plugin. + * + * @example + * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams(); + * + * @method getParams + * @return {Object} Name/value object with parameters passed from windowManager.open call. + */ + getParams: getParams, + + /** + * Sets the params of the last opened window. + * + * @method setParams + * @param {Object} params Params object to set for the last opened window. + */ + setParams: setParams, - return ClassList; + /** + * Returns the currently opened window objects. + * + * @method getWindows + * @return {Array} Array of the currently opened windows. + */ + getWindows: getWindows + }; + }; } ); + /** - * ReflowQueue.js + * RangePoint.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -23214,82 +22198,30 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class will automatically reflow controls on the next animation frame within a few milliseconds on older browsers. - * If the user manually reflows then the automatic reflow will be cancelled. This class is used internally when various control states - * changes that triggers a reflow. - * - * @class tinymce.ui.ReflowQueue - * @static - */ define( - 'tinymce.core.ui.ReflowQueue', + 'tinymce.core.dom.RangePoint', [ - "tinymce.core.util.Delay" + 'ephox.katamari.api.Arr', + 'tinymce.core.geom.ClientRect' ], - function (Delay) { - var dirtyCtrls = {}, animationFrameRequested; - - return { - /** - * Adds a control to the next automatic reflow call. This is the control that had a state - * change for example if the control was hidden/shown. - * - * @method add - * @param {tinymce.ui.Control} ctrl Control to add to queue. - */ - add: function (ctrl) { - var parent = ctrl.parent(); - - if (parent) { - if (!parent._layout || parent._layout.isNative()) { - return; - } - - if (!dirtyCtrls[parent._id]) { - dirtyCtrls[parent._id] = parent; - } - - if (!animationFrameRequested) { - animationFrameRequested = true; - - Delay.requestAnimationFrame(function () { - var id, ctrl; - - animationFrameRequested = false; - - for (id in dirtyCtrls) { - ctrl = dirtyCtrls[id]; - - if (ctrl.state.get('rendered')) { - ctrl.reflow(); - } - } + function (Arr, ClientRect) { + var isXYWithinRange = function (clientX, clientY, range) { + if (range.collapsed) { + return false; + } - dirtyCtrls = {}; - }, document.body); - } - } - }, + return Arr.foldl(range.getClientRects(), function (state, rect) { + return state || ClientRect.containsXY(rect, clientX, clientY); + }, false); + }; - /** - * Removes the specified control from the automatic reflow. This will happen when for example the user - * manually triggers a reflow. - * - * @method remove - * @param {tinymce.ui.Control} ctrl Control to remove from queue. - */ - remove: function (ctrl) { - if (dirtyCtrls[ctrl._id]) { - delete dirtyCtrls[ctrl._id]; - } - } + return { + isXYWithinRange: isXYWithinRange }; } ); - /** - * Control.js + * VK.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -23298,1304 +22230,981 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/*eslint consistent-this:0 */ - /** - * This is the base class for all controls and containers. All UI control instances inherit - * from this one as it has the base logic needed by all of them. - * - * @class tinymce.ui.Control + * This file exposes a set of the common KeyCodes for use. Please grow it as needed. */ define( - 'tinymce.core.ui.Control', + 'tinymce.core.util.VK', [ - "tinymce.core.util.Class", - "tinymce.core.util.Tools", - "tinymce.core.util.EventDispatcher", - "tinymce.core.data.ObservableObject", - "tinymce.core.ui.Collection", - "tinymce.core.ui.DomUtils", - "tinymce.core.dom.DomQuery", - "tinymce.core.ui.BoxUtils", - "tinymce.core.ui.ClassList", - "tinymce.core.ui.ReflowQueue" + "tinymce.core.Env" ], - function (Class, Tools, EventDispatcher, ObservableObject, Collection, DomUtils, $, BoxUtils, ClassList, ReflowQueue) { - "use strict"; - - var hasMouseWheelEventSupport = "onmousewheel" in document; - var hasWheelEventSupport = false; - var classPrefix = "mce-"; - var Control, idCounter = 0; - - var proto = { - Statics: { - classPrefix: classPrefix - }, + function (Env) { + return { + BACKSPACE: 8, + DELETE: 46, + DOWN: 40, + ENTER: 13, + LEFT: 37, + RIGHT: 39, + SPACEBAR: 32, + TAB: 9, + UP: 38, - isRtl: function () { - return Control.rtl; + modifierPressed: function (e) { + return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e); }, - /** - * Class/id prefix to use for all controls. - * - * @final - * @field {String} classPrefix - */ - classPrefix: classPrefix, + metaKeyPressed: function (e) { + // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states + return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey); + } + }; + } +); - /** - * Constructs a new control instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {String} style Style CSS properties to add. - * @setting {String} border Border box values example: 1 1 1 1 - * @setting {String} padding Padding box values example: 1 1 1 1 - * @setting {String} margin Margin box values example: 1 1 1 1 - * @setting {Number} minWidth Minimal width for the control. - * @setting {Number} minHeight Minimal height for the control. - * @setting {String} classes Space separated list of classes to add. - * @setting {String} role WAI-ARIA role to use for control. - * @setting {Boolean} hidden Is the control hidden by default. - * @setting {Boolean} disabled Is the control disabled by default. - * @setting {String} name Name of the control instance. - */ - init: function (settings) { - var self = this, classes, defaultClasses; +/** + * ControlSelection.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 + */ - function applyClasses(classes) { - var i; +/** + * This class handles control selection of elements. Controls are elements + * that can be resized and needs to be selected as a whole. It adds custom resize handles + * to all browser engines that support properly disabling the built in resize logic. + * + * @class tinymce.dom.ControlSelection + */ +define( + 'tinymce.core.dom.ControlSelection', + [ + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Selectors', + 'global!document', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangePoint', + 'tinymce.core.Env', + 'tinymce.core.util.Delay', + 'tinymce.core.util.Tools', + 'tinymce.core.util.VK' + ], + function (Fun, Element, Selectors, document, NodeType, RangePoint, Env, Delay, Tools, VK) { + var isContentEditableFalse = NodeType.isContentEditableFalse; + var isContentEditableTrue = NodeType.isContentEditableTrue; - classes = classes.split(' '); - for (i = 0; i < classes.length; i++) { - self.classes.add(classes[i]); - } + function getContentEditableRoot(root, node) { + while (node && node != root) { + if (isContentEditableTrue(node) || isContentEditableFalse(node)) { + return node; } - self.settings = settings = Tools.extend({}, self.Defaults, settings); - - // Initial states - self._id = settings.id || ('mceu_' + (idCounter++)); - self._aria = { role: settings.role }; - self._elmCache = {}; - self.$ = $; - - self.state = new ObservableObject({ - visible: true, - active: false, - disabled: false, - value: '' - }); + node = node.parentNode; + } - self.data = new ObservableObject(settings.data); + return null; + } - self.classes = new ClassList(function () { - if (self.state.get('rendered')) { - self.getEl().className = this.toString(); - } - }); - self.classes.prefix = self.classPrefix; + var isImage = function (elm) { + return elm && elm.nodeName === 'IMG'; + }; - // Setup classes - classes = settings.classes; - if (classes) { - if (self.Defaults) { - defaultClasses = self.Defaults.classes; + var isEventOnImageOutsideRange = function (evt, range) { + return isImage(evt.target) && !RangePoint.isXYWithinRange(evt.clientX, evt.clientY, range); + }; - if (defaultClasses && classes != defaultClasses) { - applyClasses(defaultClasses); - } - } + var contextMenuSelectImage = function (editor, evt) { + var target = evt.target; - applyClasses(classes); - } + if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) { + evt.preventDefault(); + editor.selection.select(target); + } + }; - Tools.each('title text name visible disabled active value'.split(' '), function (name) { - if (name in settings) { - self[name](settings[name]); - } - }); + return function (selection, editor) { + var dom = editor.dom, each = Tools.each; + var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent; + var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; + var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; + var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight; - self.on('click', function () { - if (self.disabled()) { - return false; - } - }); + // Details about each resize handle how to scale etc + resizeHandles = { + // Name: x multiplier, y multiplier, delta size x, delta size y + /*n: [0.5, 0, 0, -1], + e: [1, 0.5, 1, 0], + s: [0.5, 1, 0, 1], + w: [0, 0.5, -1, 0],*/ + nw: [0, 0, -1, -1], + ne: [1, 0, 1, -1], + se: [1, 1, 1, 1], + sw: [0, 1, -1, 1] + }; - /** - * Name/value object with settings for the current control. - * - * @field {Object} settings - */ - self.settings = settings; + // Add CSS for resize handles, cloned element and selected + var rootClass = '.mce-content-body'; + editor.contentStyles.push( + rootClass + ' div.mce-resizehandle {' + + 'position: absolute;' + + 'border: 1px solid black;' + + 'box-sizing: box-sizing;' + + 'background: #FFF;' + + 'width: 7px;' + + 'height: 7px;' + + 'z-index: 10000' + + '}' + + rootClass + ' .mce-resizehandle:hover {' + + 'background: #000' + + '}' + + rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' + + 'outline: 1px solid black;' + + 'resize: none' + // Have been talks about implementing this in browsers + '}' + + rootClass + ' .mce-clonedresizable {' + + 'position: absolute;' + + (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing + 'opacity: .5;' + + 'filter: alpha(opacity=50);' + + 'z-index: 10000' + + '}' + + rootClass + ' .mce-resize-helper {' + + 'background: #555;' + + 'background: rgba(0,0,0,0.75);' + + 'border-radius: 3px;' + + 'border: 1px;' + + 'color: white;' + + 'display: none;' + + 'font-family: sans-serif;' + + 'font-size: 12px;' + + 'white-space: nowrap;' + + 'line-height: 14px;' + + 'margin: 5px 10px;' + + 'padding: 5px;' + + 'position: absolute;' + + 'z-index: 10001' + + '}' + ); - self.borderBox = BoxUtils.parseBox(settings.border); - self.paddingBox = BoxUtils.parseBox(settings.padding); - self.marginBox = BoxUtils.parseBox(settings.margin); + function isResizable(elm) { + var selector = editor.settings.object_resizing; - if (settings.hidden) { - self.hide(); + if (selector === false || Env.iOS) { + return false; } - }, - // Will generate getter/setter methods for these properties - Properties: 'parent,name', + if (typeof selector != 'string') { + selector = 'table,img,div'; + } - /** - * Returns the root element to render controls into. - * - * @method getContainerElm - * @return {Element} HTML DOM element to render into. - */ - getContainerElm: function () { - return DomUtils.getContainer(); - }, + if (elm.getAttribute('data-mce-resize') === 'false') { + return false; + } - /** - * Returns a control instance for the current DOM element. - * - * @method getParentCtrl - * @param {Element} elm HTML dom element to get parent control from. - * @return {tinymce.ui.Control} Control instance or undefined. - */ - getParentCtrl: function (elm) { - var ctrl, lookup = this.getRoot().controlIdLookup; + if (elm == editor.getBody()) { + return false; + } - while (elm && lookup) { - ctrl = lookup[elm.id]; - if (ctrl) { - break; - } + return Selectors.is(Element.fromDom(elm), selector); + } - elm = elm.parentNode; - } - - return ctrl; - }, - - /** - * Initializes the current controls layout rect. - * This will be executed by the layout managers to determine the - * default minWidth/minHeight etc. - * - * @method initLayoutRect - * @return {Object} Layout rect instance. - */ - initLayoutRect: function () { - var self = this, settings = self.settings, borderBox, layoutRect; - var elm = self.getEl(), width, height, minWidth, minHeight, autoResize; - var startMinWidth, startMinHeight, initialSize; - - // Measure the current element - borderBox = self.borderBox = self.borderBox || BoxUtils.measureBox(elm, 'border'); - self.paddingBox = self.paddingBox || BoxUtils.measureBox(elm, 'padding'); - self.marginBox = self.marginBox || BoxUtils.measureBox(elm, 'margin'); - initialSize = DomUtils.getSize(elm); - - // Setup minWidth/minHeight and width/height - startMinWidth = settings.minWidth; - startMinHeight = settings.minHeight; - minWidth = startMinWidth || initialSize.width; - minHeight = startMinHeight || initialSize.height; - width = settings.width; - height = settings.height; - autoResize = settings.autoResize; - autoResize = typeof autoResize != "undefined" ? autoResize : !width && !height; - - width = width || minWidth; - height = height || minHeight; - - var deltaW = borderBox.left + borderBox.right; - var deltaH = borderBox.top + borderBox.bottom; - - var maxW = settings.maxWidth || 0xFFFF; - var maxH = settings.maxHeight || 0xFFFF; - - // Setup initial layout rect - self._layoutRect = layoutRect = { - x: settings.x || 0, - y: settings.y || 0, - w: width, - h: height, - deltaW: deltaW, - deltaH: deltaH, - contentW: width - deltaW, - contentH: height - deltaH, - innerW: width - deltaW, - innerH: height - deltaH, - startMinWidth: startMinWidth || 0, - startMinHeight: startMinHeight || 0, - minW: Math.min(minWidth, maxW), - minH: Math.min(minHeight, maxH), - maxW: maxW, - maxH: maxH, - autoResize: autoResize, - scrollW: 0 - }; + function resizeGhostElement(e) { + var deltaX, deltaY, proportional; + var resizeHelperX, resizeHelperY; - self._lastLayoutRect = {}; + // Calc new width/height + deltaX = e.screenX - startX; + deltaY = e.screenY - startY; - return layoutRect; - }, + // Calc new size + width = deltaX * selectedHandle[2] + startW; + height = deltaY * selectedHandle[3] + startH; - /** - * Getter/setter for the current layout rect. - * - * @method layoutRect - * @param {Object} [newRect] Optional new layout rect. - * @return {tinymce.ui.Control/Object} Current control or rect object. - */ - layoutRect: function (newRect) { - var self = this, curRect = self._layoutRect, lastLayoutRect, size, deltaWidth, deltaHeight, undef, repaintControls; + // Never scale down lower than 5 pixels + width = width < 5 ? 5 : width; + height = height < 5 ? 5 : height; - // Initialize default layout rect - if (!curRect) { - curRect = self.initLayoutRect(); + if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) { + proportional = !VK.modifierPressed(e); + } else { + proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0); } - // Set new rect values - if (newRect) { - // Calc deltas between inner and outer sizes - deltaWidth = curRect.deltaW; - deltaHeight = curRect.deltaH; - - // Set x position - if (newRect.x !== undef) { - curRect.x = newRect.x; + // Constrain proportions + if (proportional) { + if (abs(deltaX) > abs(deltaY)) { + height = round(width * ratio); + width = round(height / ratio); + } else { + width = round(height / ratio); + height = round(width * ratio); } + } - // Set y position - if (newRect.y !== undef) { - curRect.y = newRect.y; - } + // Update ghost size + dom.setStyles(selectedElmGhost, { + width: width, + height: height + }); - // Set minW - if (newRect.minW !== undef) { - curRect.minW = newRect.minW; - } + // Update resize helper position + resizeHelperX = selectedHandle.startPos.x + deltaX; + resizeHelperY = selectedHandle.startPos.y + deltaY; + resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0; + resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0; - // Set minH - if (newRect.minH !== undef) { - curRect.minH = newRect.minH; - } + dom.setStyles(resizeHelper, { + left: resizeHelperX, + top: resizeHelperY, + display: 'block' + }); - // Set new width and calculate inner width - size = newRect.w; - if (size !== undef) { - size = size < curRect.minW ? curRect.minW : size; - size = size > curRect.maxW ? curRect.maxW : size; - curRect.w = size; - curRect.innerW = size - deltaWidth; - } + resizeHelper.innerHTML = width + ' × ' + height; - // Set new height and calculate inner height - size = newRect.h; - if (size !== undef) { - size = size < curRect.minH ? curRect.minH : size; - size = size > curRect.maxH ? curRect.maxH : size; - curRect.h = size; - curRect.innerH = size - deltaHeight; - } + // Update ghost X position if needed + if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { + dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); + } - // Set new inner width and calculate width - size = newRect.innerW; - if (size !== undef) { - size = size < curRect.minW - deltaWidth ? curRect.minW - deltaWidth : size; - size = size > curRect.maxW - deltaWidth ? curRect.maxW - deltaWidth : size; - curRect.innerW = size; - curRect.w = size + deltaWidth; - } + // Update ghost Y position if needed + if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { + dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); + } - // Set new height and calculate inner height - size = newRect.innerH; - if (size !== undef) { - size = size < curRect.minH - deltaHeight ? curRect.minH - deltaHeight : size; - size = size > curRect.maxH - deltaHeight ? curRect.maxH - deltaHeight : size; - curRect.innerH = size; - curRect.h = size + deltaHeight; - } + // Calculate how must overflow we got + deltaX = rootElement.scrollWidth - startScrollWidth; + deltaY = rootElement.scrollHeight - startScrollHeight; - // Set new contentW - if (newRect.contentW !== undef) { - curRect.contentW = newRect.contentW; - } + // Re-position the resize helper based on the overflow + if (deltaX + deltaY !== 0) { + dom.setStyles(resizeHelper, { + left: resizeHelperX - deltaX, + top: resizeHelperY - deltaY + }); + } - // Set new contentH - if (newRect.contentH !== undef) { - curRect.contentH = newRect.contentH; - } + if (!resizeStarted) { + editor.fire('ObjectResizeStart', { target: selectedElm, width: startW, height: startH }); + resizeStarted = true; + } + } - // Compare last layout rect with the current one to see if we need to repaint or not - lastLayoutRect = self._lastLayoutRect; - if (lastLayoutRect.x !== curRect.x || lastLayoutRect.y !== curRect.y || - lastLayoutRect.w !== curRect.w || lastLayoutRect.h !== curRect.h) { - repaintControls = Control.repaintControls; + function endGhostResize() { + resizeStarted = false; - if (repaintControls) { - if (repaintControls.map && !repaintControls.map[self._id]) { - repaintControls.push(self); - repaintControls.map[self._id] = true; - } + function setSizeProp(name, value) { + if (value) { + // Resize by using style or attribute + if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { + dom.setStyle(selectedElm, name, value); + } else { + dom.setAttrib(selectedElm, name, value); } - - lastLayoutRect.x = curRect.x; - lastLayoutRect.y = curRect.y; - lastLayoutRect.w = curRect.w; - lastLayoutRect.h = curRect.h; } - - return self; } - return curRect; - }, - - /** - * Repaints the control after a layout operation. - * - * @method repaint - */ - repaint: function () { - var self = this, style, bodyStyle, bodyElm, rect, borderBox; - var borderW, borderH, lastRepaintRect, round, value; - - // Use Math.round on all values on IE < 9 - round = !document.createRange ? Math.round : function (value) { - return value; - }; - - style = self.getEl().style; - rect = self._layoutRect; - lastRepaintRect = self._lastRepaintRect || {}; + // Set width/height properties + setSizeProp('width', width); + setSizeProp('height', height); - borderBox = self.borderBox; - borderW = borderBox.left + borderBox.right; - borderH = borderBox.top + borderBox.bottom; + dom.unbind(editableDoc, 'mousemove', resizeGhostElement); + dom.unbind(editableDoc, 'mouseup', endGhostResize); - if (rect.x !== lastRepaintRect.x) { - style.left = round(rect.x) + 'px'; - lastRepaintRect.x = rect.x; + if (rootDocument != editableDoc) { + dom.unbind(rootDocument, 'mousemove', resizeGhostElement); + dom.unbind(rootDocument, 'mouseup', endGhostResize); } - if (rect.y !== lastRepaintRect.y) { - style.top = round(rect.y) + 'px'; - lastRepaintRect.y = rect.y; - } + // Remove ghost/helper and update resize handle positions + dom.remove(selectedElmGhost); + dom.remove(resizeHelper); - if (rect.w !== lastRepaintRect.w) { - value = round(rect.w - borderW); - style.width = (value >= 0 ? value : 0) + 'px'; - lastRepaintRect.w = rect.w; + if (!isIE || selectedElm.nodeName == "TABLE") { + showResizeRect(selectedElm); } - if (rect.h !== lastRepaintRect.h) { - value = round(rect.h - borderH); - style.height = (value >= 0 ? value : 0) + 'px'; - lastRepaintRect.h = rect.h; - } + editor.fire('ObjectResized', { target: selectedElm, width: width, height: height }); + dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style')); + editor.nodeChanged(); + } + + function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { + var position, targetWidth, targetHeight, e, rect; - // Update body if needed - if (self._hasBody && rect.innerW !== lastRepaintRect.innerW) { - value = round(rect.innerW); + hideResizeRect(); + unbindResizeHandleEvents(); - bodyElm = self.getEl('body'); - if (bodyElm) { - bodyStyle = bodyElm.style; - bodyStyle.width = (value >= 0 ? value : 0) + 'px'; - } + // Get position and size of target + position = dom.getPos(targetElm, rootElement); + selectedElmX = position.x; + selectedElmY = position.y; + rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption + targetWidth = rect.width || (rect.right - rect.left); + targetHeight = rect.height || (rect.bottom - rect.top); - lastRepaintRect.innerW = rect.innerW; + // Reset width/height if user selects a new image/table + if (selectedElm != targetElm) { + detachResizeStartListener(); + selectedElm = targetElm; + width = height = 0; } - if (self._hasBody && rect.innerH !== lastRepaintRect.innerH) { - value = round(rect.innerH); + // Makes it possible to disable resizing + e = editor.fire('ObjectSelected', { target: targetElm }); - bodyElm = bodyElm || self.getEl('body'); - if (bodyElm) { - bodyStyle = bodyStyle || bodyElm.style; - bodyStyle.height = (value >= 0 ? value : 0) + 'px'; - } + if (isResizable(targetElm) && !e.isDefaultPrevented()) { + each(resizeHandles, function (handle, name) { + var handleElm; - lastRepaintRect.innerH = rect.innerH; - } + function startDrag(e) { + startX = e.screenX; + startY = e.screenY; + startW = selectedElm.clientWidth; + startH = selectedElm.clientHeight; + ratio = startH / startW; + selectedHandle = handle; - self._lastRepaintRect = lastRepaintRect; - self.fire('repaint', {}, false); - }, + handle.startPos = { + x: targetWidth * handle[0] + selectedElmX, + y: targetHeight * handle[1] + selectedElmY + }; - /** - * Updates the controls layout rect by re-measuing it. - */ - updateLayoutRect: function () { - var self = this; + startScrollWidth = rootElement.scrollWidth; + startScrollHeight = rootElement.scrollHeight; - self.parent()._lastRect = null; + selectedElmGhost = selectedElm.cloneNode(true); + dom.addClass(selectedElmGhost, 'mce-clonedresizable'); + dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all'); + selectedElmGhost.contentEditable = false; // Hides IE move layer cursor + selectedElmGhost.unSelectabe = true; + dom.setStyles(selectedElmGhost, { + left: selectedElmX, + top: selectedElmY, + margin: 0 + }); - DomUtils.css(self.getEl(), { width: '', height: '' }); + selectedElmGhost.removeAttribute('data-mce-selected'); + rootElement.appendChild(selectedElmGhost); - self._layoutRect = self._lastRepaintRect = self._lastLayoutRect = null; - self.initLayoutRect(); - }, + dom.bind(editableDoc, 'mousemove', resizeGhostElement); + dom.bind(editableDoc, 'mouseup', endGhostResize); - /** - * Binds a callback to the specified event. This event can both be - * native browser events like "click" or custom ones like PostRender. - * - * The callback function will be passed a DOM event like object that enables yout do stop propagation. - * - * @method on - * @param {String} name Name of the event to bind. For example "click". - * @param {String/function} callback Callback function to execute ones the event occurs. - * @return {tinymce.ui.Control} Current control object. - */ - on: function (name, callback) { - var self = this; + if (rootDocument != editableDoc) { + dom.bind(rootDocument, 'mousemove', resizeGhostElement); + dom.bind(rootDocument, 'mouseup', endGhostResize); + } - function resolveCallbackName(name) { - var callback, scope; + resizeHelper = dom.add(rootElement, 'div', { + 'class': 'mce-resize-helper', + 'data-mce-bogus': 'all' + }, startW + ' × ' + startH); + } - if (typeof name != 'string') { - return name; - } + if (mouseDownHandleName) { + // Drag started by IE native resizestart + if (name == mouseDownHandleName) { + startDrag(mouseDownEvent); + } - return function (e) { - if (!callback) { - self.parentsAndSelf().each(function (ctrl) { - var callbacks = ctrl.settings.callbacks; + return; + } - if (callbacks && (callback = callbacks[name])) { - scope = ctrl; - return false; - } - }); + // Get existing or render resize handle + handleElm = dom.get('mceResizeHandle' + name); + if (handleElm) { + dom.remove(handleElm); } - if (!callback) { - e.action = name; - this.fire('execute', e); - return; + handleElm = dom.add(rootElement, 'div', { + id: 'mceResizeHandle' + name, + 'data-mce-bogus': 'all', + 'class': 'mce-resizehandle', + unselectable: true, + style: 'cursor:' + name + '-resize; margin:0; padding:0' + }); + + // Hides IE move layer cursor + // If we set it on Chrome we get this wounderful bug: #6725 + if (Env.ie) { + handleElm.contentEditable = false; } - return callback.call(scope, e); - }; - } + dom.bind(handleElm, 'mousedown', function (e) { + e.stopImmediatePropagation(); + e.preventDefault(); + startDrag(e); + }); - getEventDispatcher(self).on(name, resolveCallbackName(callback)); + handle.elm = handleElm; - return self; - }, + // Position element + dom.setStyles(handleElm, { + left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), + top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) + }); + }); + } else { + hideResizeRect(); + } - /** - * Unbinds the specified event and optionally a specific callback. If you omit the name - * parameter all event handlers will be removed. If you omit the callback all event handles - * by the specified name will be removed. - * - * @method off - * @param {String} [name] Name for the event to unbind. - * @param {function} [callback] Callback function to unbind. - * @return {tinymce.ui.Control} Current control object. - */ - off: function (name, callback) { - getEventDispatcher(this).off(name, callback); - return this; - }, + selectedElm.setAttribute('data-mce-selected', '1'); + } - /** - * Fires the specified event by name and arguments on the control. This will execute all - * bound event handlers. - * - * @method fire - * @param {String} name Name of the event to fire. - * @param {Object} [args] Arguments to pass to the event. - * @param {Boolean} [bubble] Value to control bubbling. Defaults to true. - * @return {Object} Current arguments object. - */ - fire: function (name, args, bubble) { - var self = this; + function hideResizeRect() { + var name, handleElm; - args = args || {}; + unbindResizeHandleEvents(); - if (!args.control) { - args.control = self; + if (selectedElm) { + selectedElm.removeAttribute('data-mce-selected'); } - args = getEventDispatcher(self).fire(name, args); - - // Bubble event up to parents - if (bubble !== false && self.parent) { - var parent = self.parent(); - while (parent && !args.isPropagationStopped()) { - parent.fire(name, args, false); - parent = parent.parent(); + for (name in resizeHandles) { + handleElm = dom.get('mceResizeHandle' + name); + if (handleElm) { + dom.unbind(handleElm); + dom.remove(handleElm); } } + } - return args; - }, - - /** - * Returns true/false if the specified event has any listeners. - * - * @method hasEventListeners - * @param {String} name Name of the event to check for. - * @return {Boolean} True/false state if the event has listeners. - */ - hasEventListeners: function (name) { - return getEventDispatcher(this).has(name); - }, - - /** - * Returns a control collection with all parent controls. - * - * @method parents - * @param {String} selector Optional selector expression to find parents. - * @return {tinymce.ui.Collection} Collection with all parent controls. - */ - parents: function (selector) { - var self = this, ctrl, parents = new Collection(); + function updateResizeRect(e) { + var startElm, controlElm; - // Add each parent to collection - for (ctrl = self.parent(); ctrl; ctrl = ctrl.parent()) { - parents.add(ctrl); + function isChildOrEqual(node, parent) { + if (node) { + do { + if (node === parent) { + return true; + } + } while ((node = node.parentNode)); + } } - // Filter away everything that doesn't match the selector - if (selector) { - parents = parents.filter(selector); + // Ignore all events while resizing or if the editor instance was removed + if (resizeStarted || editor.removed) { + return; } - return parents; - }, + // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v + each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function (img) { + img.removeAttribute('data-mce-selected'); + }); - /** - * Returns the current control and it's parents. - * - * @method parentsAndSelf - * @param {String} selector Optional selector expression to find parents. - * @return {tinymce.ui.Collection} Collection with all parent controls. - */ - parentsAndSelf: function (selector) { - return new Collection(this).add(this.parents(selector)); - }, + controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); + controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0]; - /** - * Returns the control next to the current control. - * - * @method next - * @return {tinymce.ui.Control} Next control instance. - */ - next: function () { - var parentControls = this.parent().items(); + if (isChildOrEqual(controlElm, rootElement)) { + disableGeckoResize(); + startElm = selection.getStart(true); - return parentControls[parentControls.indexOf(this) + 1]; - }, + if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) { + if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) { + showResizeRect(controlElm); + return; + } + } + } - /** - * Returns the control previous to the current control. - * - * @method prev - * @return {tinymce.ui.Control} Previous control instance. - */ - prev: function () { - var parentControls = this.parent().items(); + hideResizeRect(); + } - return parentControls[parentControls.indexOf(this) - 1]; - }, - - /** - * Sets the inner HTML of the control element. - * - * @method innerHtml - * @param {String} html Html string to set as inner html. - * @return {tinymce.ui.Control} Current control object. - */ - innerHtml: function (html) { - this.$el.html(html); - return this; - }, - - /** - * Returns the control DOM element or sub element. - * - * @method getEl - * @param {String} [suffix] Suffix to get element by. - * @return {Element} HTML DOM element for the current control or it's children. - */ - getEl: function (suffix) { - var id = suffix ? this._id + '-' + suffix : this._id; - - if (!this._elmCache[id]) { - this._elmCache[id] = $('#' + id)[0]; + function attachEvent(elm, name, func) { + if (elm && elm.attachEvent) { + elm.attachEvent('on' + name, func); } + } - return this._elmCache[id]; - }, - - /** - * Sets the visible state to true. - * - * @method show - * @return {tinymce.ui.Control} Current control instance. - */ - show: function () { - return this.visible(true); - }, - - /** - * Sets the visible state to false. - * - * @method hide - * @return {tinymce.ui.Control} Current control instance. - */ - hide: function () { - return this.visible(false); - }, - - /** - * Focuses the current control. - * - * @method focus - * @return {tinymce.ui.Control} Current control instance. - */ - focus: function () { - try { - this.getEl().focus(); - } catch (ex) { - // Ignore IE error + function detachEvent(elm, name, func) { + if (elm && elm.detachEvent) { + elm.detachEvent('on' + name, func); } + } - return this; - }, - - /** - * Blurs the current control. - * - * @method blur - * @return {tinymce.ui.Control} Current control instance. - */ - blur: function () { - this.getEl().blur(); - - return this; - }, - - /** - * Sets the specified aria property. - * - * @method aria - * @param {String} name Name of the aria property to set. - * @param {String} value Value of the aria property. - * @return {tinymce.ui.Control} Current control instance. - */ - aria: function (name, value) { - var self = this, elm = self.getEl(self.ariaTarget); - - if (typeof value === "undefined") { - return self._aria[name]; - } + function resizeNativeStart(e) { + var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; - self._aria[name] = value; + pos = target.getBoundingClientRect(); + relativeX = lastMouseDownEvent.clientX - pos.left; + relativeY = lastMouseDownEvent.clientY - pos.top; - if (self.state.get('rendered')) { - elm.setAttribute(name == 'role' ? name : 'aria-' + name, value); - } + // Figure out what corner we are draging on + for (name in resizeHandles) { + corner = resizeHandles[name]; - return self; - }, + cornerX = target.offsetWidth * corner[0]; + cornerY = target.offsetHeight * corner[1]; - /** - * Encodes the specified string with HTML entities. It will also - * translate the string to different languages. - * - * @method encode - * @param {String/Object/Array} text Text to entity encode. - * @param {Boolean} [translate=true] False if the contents shouldn't be translated. - * @return {String} Encoded and possible traslated string. - */ - encode: function (text, translate) { - if (translate !== false) { - text = this.translate(text); + if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) { + selectedHandle = corner; + break; + } } - return (text || '').replace(/[&<>"]/g, function (match) { - return '&#' + match.charCodeAt(0) + ';'; + // Remove native selection and let the magic begin + resizeStarted = true; + editor.fire('ObjectResizeStart', { + target: selectedElm, + width: selectedElm.clientWidth, + height: selectedElm.clientHeight }); - }, - - /** - * Returns the translated string. - * - * @method translate - * @param {String} text Text to translate. - * @return {String} Translated string or the same as the input. - */ - translate: function (text) { - return Control.translate ? Control.translate(text) : text; - }, - - /** - * Adds items before the current control. - * - * @method before - * @param {Array/tinymce.ui.Collection} items Array of items to prepend before this control. - * @return {tinymce.ui.Control} Current control instance. - */ - before: function (items) { - var self = this, parent = self.parent(); + editor.getDoc().selection.empty(); + showResizeRect(target, name, lastMouseDownEvent); + } - if (parent) { - parent.insert(items, parent.items().indexOf(self), true); + function preventDefault(e) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; // IE } + } - return self; - }, + function isWithinContentEditableFalse(elm) { + return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm)); + } - /** - * Adds items after the current control. - * - * @method after - * @param {Array/tinymce.ui.Collection} items Array of items to append after this control. - * @return {tinymce.ui.Control} Current control instance. - */ - after: function (items) { - var self = this, parent = self.parent(); + function nativeControlSelect(e) { + var target = e.srcElement; - if (parent) { - parent.insert(items, parent.items().indexOf(self)); + if (isWithinContentEditableFalse(target)) { + preventDefault(e); + return; } - return self; - }, + if (target != selectedElm) { + editor.fire('ObjectSelected', { target: target }); + detachResizeStartListener(); - /** - * Removes the current control from DOM and from UI collections. - * - * @method remove - * @return {tinymce.ui.Control} Current control instance. - */ - remove: function () { - var self = this, elm = self.getEl(), parent = self.parent(), newItems, i; + if (target.id.indexOf('mceResizeHandle') === 0) { + e.returnValue = false; + return; + } - if (self.items) { - var controls = self.items().toArray(); - i = controls.length; - while (i--) { - controls[i].remove(); + if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { + hideResizeRect(); + selectedElm = target; + attachEvent(target, 'resizestart', resizeNativeStart); } } + } - if (parent && parent.items) { - newItems = []; + function detachResizeStartListener() { + detachEvent(selectedElm, 'resizestart', resizeNativeStart); + } - parent.items().each(function (item) { - if (item !== self) { - newItems.push(item); - } - }); + function unbindResizeHandleEvents() { + for (var name in resizeHandles) { + var handle = resizeHandles[name]; - parent.items().set(newItems); - parent._lastRect = null; + if (handle.elm) { + dom.unbind(handle.elm); + delete handle.elm; + } } + } - if (self._eventsRoot && self._eventsRoot == self) { - $(elm).off(); + function disableGeckoResize() { + try { + // Disable object resizing on Gecko + editor.getDoc().execCommand('enableObjectResizing', false, false); + } catch (ex) { + // Ignore } + } - var lookup = self.getRoot().controlIdLookup; - if (lookup) { - delete lookup[self._id]; - } + function controlSelect(elm) { + var ctrlRng; - if (elm && elm.parentNode) { - elm.parentNode.removeChild(elm); + if (!isIE) { + return; } - self.state.set('rendered', false); - self.state.destroy(); - - self.fire('remove'); - - return self; - }, - - /** - * Renders the control before the specified element. - * - * @method renderBefore - * @param {Element} elm Element to render before. - * @return {tinymce.ui.Control} Current control instance. - */ - renderBefore: function (elm) { - $(elm).before(this.renderHtml()); - this.postRender(); - return this; - }, - - /** - * Renders the control to the specified element. - * - * @method renderBefore - * @param {Element} elm Element to render to. - * @return {tinymce.ui.Control} Current control instance. - */ - renderTo: function (elm) { - $(elm || this.getContainerElm()).append(this.renderHtml()); - this.postRender(); - return this; - }, - - preRender: function () { - }, - - render: function () { - }, - - renderHtml: function () { - return '

    '; - }, - - /** - * Post render method. Called after the control has been rendered to the target. - * - * @method postRender - * @return {tinymce.ui.Control} Current control instance. - */ - postRender: function () { - var self = this, settings = self.settings, elm, box, parent, name, parentEventsRoot; - - self.$el = $(self.getEl()); - self.state.set('rendered', true); + ctrlRng = editableDoc.body.createControlRange(); - // Bind on settings - for (name in settings) { - if (name.indexOf("on") === 0) { - self.on(name.substr(2), settings[name]); - } + try { + ctrlRng.addElement(elm); + ctrlRng.select(); + return true; + } catch (ex) { + // Ignore since the element can't be control selected for example a P tag } + } - if (self._eventsRoot) { - for (parent = self.parent(); !parentEventsRoot && parent; parent = parent.parent()) { - parentEventsRoot = parent._eventsRoot; - } - - if (parentEventsRoot) { - for (name in parentEventsRoot._nativeEvents) { - self._nativeEvents[name] = true; + editor.on('init', function () { + if (isIE) { + // Hide the resize rect on resize and reselect the image + editor.on('ObjectResized', function (e) { + if (e.target.nodeName != 'TABLE') { + hideResizeRect(); + controlSelect(e.target); } - } - } - - bindPendingEvents(self); + }); - if (settings.style) { - elm = self.getEl(); - if (elm) { - elm.setAttribute('style', settings.style); - elm.style.cssText = settings.style; - } - } + attachEvent(rootElement, 'controlselect', nativeControlSelect); - if (self.settings.border) { - box = self.borderBox; - self.$el.css({ - 'border-top-width': box.top, - 'border-right-width': box.right, - 'border-bottom-width': box.bottom, - 'border-left-width': box.left + editor.on('mousedown', function (e) { + lastMouseDownEvent = e; }); - } - - // Add instance to lookup - var root = self.getRoot(); - if (!root.controlIdLookup) { - root.controlIdLookup = {}; - } + } else { + disableGeckoResize(); - root.controlIdLookup[self._id] = self; + // Sniff sniff, hard to feature detect this stuff + if (Env.ie >= 11) { + // Needs to be mousedown for drag/drop to work on IE 11 + // Needs to be click on Edge to properly select images + editor.on('mousedown click', function (e) { + var target = e.target, nodeName = target.nodeName; - for (var key in self._aria) { - self.aria(key, self._aria[key]); - } + if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) { + if (e.button !== 2) { + editor.selection.select(target, nodeName == 'TABLE'); + } - if (self.state.get('visible') === false) { - self.getEl().style.display = 'none'; - } + // Only fire once since nodeChange is expensive + if (e.type == 'mousedown') { + editor.nodeChanged(); + } + } + }); - self.bindStates(); + editor.dom.bind(rootElement, 'mscontrolselect', function (e) { + function delayedSelect(node) { + Delay.setEditorTimeout(editor, function () { + editor.selection.select(node); + }); + } - self.state.on('change:visible', function (e) { - var state = e.value, parentCtrl; + if (isWithinContentEditableFalse(e.target)) { + e.preventDefault(); + delayedSelect(e.target); + return; + } - if (self.state.get('rendered')) { - self.getEl().style.display = state === false ? 'none' : ''; + if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { + e.preventDefault(); - // Need to force a reflow here on IE 8 - self.getEl().getBoundingClientRect(); + // This moves the selection from being a control selection to a text like selection like in WebKit #6753 + // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. + if (e.target.tagName == 'IMG') { + delayedSelect(e.target); + } + } + }); } + } - // Parent container needs to reflow - parentCtrl = self.parent(); - if (parentCtrl) { - parentCtrl._lastRect = null; + var throttledUpdateResizeRect = Delay.throttle(function (e) { + if (!editor.composing) { + updateResizeRect(e); } - - self.fire(state ? 'show' : 'hide'); - - ReflowQueue.add(self); }); - self.fire('postrender', {}, false); - }, - - bindStates: function () { - }, - - /** - * Scrolls the current control into view. - * - * @method scrollIntoView - * @param {String} align Alignment in view top|center|bottom. - * @return {tinymce.ui.Control} Current control instance. - */ - scrollIntoView: function (align) { - function getOffset(elm, rootElm) { - var x, y, parent = elm; + editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect); - x = y = 0; - while (parent && parent != rootElm && parent.nodeType) { - x += parent.offsetLeft || 0; - y += parent.offsetTop || 0; - parent = parent.offsetParent; + // Update resize rect while typing in a table + editor.on('keyup compositionend', function (e) { + // Don't update the resize rect while composing since it blows away the IME see: #2710 + if (selectedElm && selectedElm.nodeName == "TABLE") { + throttledUpdateResizeRect(e); } + }); - return { x: x, y: y }; - } + editor.on('hide blur', hideResizeRect); + editor.on('contextmenu', Fun.curry(contextMenuSelectImage, editor)); - var elm = this.getEl(), parentElm = elm.parentNode; - var x, y, width, height, parentWidth, parentHeight; - var pos = getOffset(elm, parentElm); + // Hide rect on focusout since it would float on top of windows otherwise + //editor.on('focusout', hideResizeRect); + }); + + editor.on('remove', unbindResizeHandleEvents); - x = pos.x; - y = pos.y; - width = elm.offsetWidth; - height = elm.offsetHeight; - parentWidth = parentElm.clientWidth; - parentHeight = parentElm.clientHeight; + function destroy() { + selectedElm = selectedElmGhost = null; - if (align == "end") { - x -= parentWidth - width; - y -= parentHeight - height; - } else if (align == "center") { - x -= (parentWidth / 2) - (width / 2); - y -= (parentHeight / 2) - (height / 2); + if (isIE) { + detachResizeStartListener(); + detachEvent(rootElement, 'controlselect', nativeControlSelect); } + } - parentElm.scrollLeft = x; - parentElm.scrollTop = y; + return { + isResizable: isResizable, + showResizeRect: showResizeRect, + hideResizeRect: hideResizeRect, + updateResizeRect: updateResizeRect, + controlSelect: controlSelect, + destroy: destroy + }; + }; + } +); - return this; - }, +define( + 'ephox.sugar.api.search.PredicateExists', - getRoot: function () { - var ctrl = this, rootControl, parents = []; + [ + 'ephox.sugar.api.search.PredicateFind' + ], - while (ctrl) { - if (ctrl.rootControl) { - rootControl = ctrl.rootControl; - break; - } + function (PredicateFind) { + var any = function (predicate) { + return PredicateFind.first(predicate).isSome(); + }; - parents.push(ctrl); - rootControl = ctrl; - ctrl = ctrl.parent(); - } + var ancestor = function (scope, predicate, isRoot) { + return PredicateFind.ancestor(scope, predicate, isRoot).isSome(); + }; - if (!rootControl) { - rootControl = this; - } + var closest = function (scope, predicate, isRoot) { + return PredicateFind.closest(scope, predicate, isRoot).isSome(); + }; - var i = parents.length; - while (i--) { - parents[i].rootControl = rootControl; - } + var sibling = function (scope, predicate) { + return PredicateFind.sibling(scope, predicate).isSome(); + }; - return rootControl; - }, + var child = function (scope, predicate) { + return PredicateFind.child(scope, predicate).isSome(); + }; - /** - * Reflows the current control and it's parents. - * This should be used after you for example append children to the current control so - * that the layout managers know that they need to reposition everything. - * - * @example - * container.append({type: 'button', text: 'My button'}).reflow(); - * - * @method reflow - * @return {tinymce.ui.Control} Current control instance. - */ - reflow: function () { - ReflowQueue.remove(this); + var descendant = function (scope, predicate) { + return PredicateFind.descendant(scope, predicate).isSome(); + }; - var parent = this.parent(); - if (parent && parent._layout && !parent._layout.isNative()) { - parent.reflow(); - } + return { + any: any, + ancestor: ancestor, + closest: closest, + sibling: sibling, + child: child, + descendant: descendant + }; + } +); - return this; - } +define( + 'ephox.sugar.api.dom.Focus', - /** - * Sets/gets the parent container for the control. - * - * @method parent - * @param {tinymce.ui.Container} parent Optional parent to set. - * @return {tinymce.ui.Control} Parent control or the current control on a set action. - */ - // parent: function(parent) {} -- Generated + [ + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.PredicateExists', + 'ephox.sugar.api.search.Traverse', + 'global!document' + ], - /** - * Sets/gets the text for the control. - * - * @method text - * @param {String} value Value to set to control. - * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. - */ - // text: function(value) {} -- Generated + function (Fun, Option, Compare, Element, PredicateExists, Traverse, document) { + var focus = function (element) { + element.dom().focus(); + }; - /** - * Sets/gets the disabled state on the control. - * - * @method disabled - * @param {Boolean} state Value to set to control. - * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. - */ - // disabled: function(state) {} -- Generated + var blur = function (element) { + element.dom().blur(); + }; - /** - * Sets/gets the active for the control. - * - * @method active - * @param {Boolean} state Value to set to control. - * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. - */ - // active: function(state) {} -- Generated + var hasFocus = function (element) { + var doc = Traverse.owner(element).dom(); + return element.dom() === doc.activeElement; + }; - /** - * Sets/gets the name for the control. - * - * @method name - * @param {String} value Value to set to control. - * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. - */ - // name: function(value) {} -- Generated + var active = function (_doc) { + var doc = _doc !== undefined ? _doc.dom() : document; + return Option.from(doc.activeElement).map(Element.fromDom); + }; - /** - * Sets/gets the title for the control. - * - * @method title - * @param {String} value Value to set to control. - * @return {String/tinymce.ui.Control} Current control on a set operation or current value on a get. - */ - // title: function(value) {} -- Generated + var focusInside = function (element) { + // Only call focus if the focus is not already inside it. + var doc = Traverse.owner(element); + var inside = active(doc).filter(function (a) { + return PredicateExists.closest(a, Fun.curry(Compare.eq, element)); + }); - /** - * Sets/gets the visible for the control. - * - * @method visible - * @param {Boolean} state Value to set to control. - * @return {Boolean/tinymce.ui.Control} Current control on a set operation or current state on a get. - */ - // visible: function(value) {} -- Generated + inside.fold(function () { + focus(element); + }, Fun.noop); }; /** - * Setup state properties. + * Return the descendant element that has focus. + * Use instead of SelectorFind.descendant(container, ':focus') + * because the :focus selector relies on keyboard focus. */ - Tools.each('text title visible disabled active value'.split(' '), function (name) { - proto[name] = function (value) { - if (arguments.length === 0) { - return this.state.get(name); - } + var search = function (element) { + return active(Traverse.owner(element)).filter(function (e) { + return element.dom().contains(e.dom()); + }); + }; - if (typeof value != "undefined") { - this.state.set(name, value); - } + return { + hasFocus: hasFocus, + focus: focus, + blur: blur, + active: active, + search: search, + focusInside: focusInside + }; + } +); +/** + * 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 + */ - return this; - }; - }); +define( + 'tinymce.core.EditorFocus', + [ + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.dom.Focus', + 'ephox.sugar.api.node.Element', + 'tinymce.core.Env', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.dom.ElementType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.selection.SelectionBookmark' + ], + function (Option, Compare, Focus, Element, Env, CaretFinder, ElementType, RangeUtils, SelectionBookmark) { + var getContentEditableHost = function (editor, node) { + return editor.dom.getParent(node, function (node) { + return editor.dom.getContentEditable(node) === "true"; + }); + }; - Control = Class.extend(proto); + var getCollapsedNode = function (rng) { + return rng.collapsed ? Option.from(RangeUtils.getNode(rng.startContainer, rng.startOffset)).map(Element.fromDom) : Option.none(); + }; - function getEventDispatcher(obj) { - if (!obj._eventDispatcher) { - obj._eventDispatcher = new EventDispatcher({ - scope: obj, - toggleEvent: function (name, state) { - if (state && EventDispatcher.isNative(name)) { - if (!obj._nativeEvents) { - obj._nativeEvents = {}; - } + 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(); + } + }); + }; - obj._nativeEvents[name] = true; + 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()); + } + ); + }; - if (obj.state.get('rendered')) { - bindPendingEvents(obj); - } - } - } - }); + 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(); } + }; - return obj._eventDispatcher; - } + var hasElementFocus = function (elm) { + return Focus.hasFocus(elm) || Focus.search(elm).isSome(); + }; - function bindPendingEvents(eventCtrl) { - var i, l, parents, eventRootCtrl, nativeEvents, name; + var hasIframeFocus = function (editor) { + return editor.iframeElement && Focus.hasFocus(Element.fromDom(editor.iframeElement)); + }; - function delegate(e) { - var control = eventCtrl.getParentCtrl(e.target); + var hasInlineFocus = function (editor) { + var rawBody = editor.getBody(); + return rawBody && hasElementFocus(Element.fromDom(rawBody)); + }; - if (control) { - control.fire(e.type, e); - } - } + var hasFocus = function (editor) { + return editor.inline ? hasInlineFocus(editor) : hasIframeFocus(editor); + }; - function mouseLeaveHandler() { - var ctrl = eventRootCtrl._lastHoverCtrl; + var focusEditor = function (editor) { + var selection = editor.selection, contentEditable = editor.settings.content_editable; + var body = editor.getBody(), contentEditableHost, rng = selection.getRng(); - if (ctrl) { - ctrl.fire("mouseleave", { target: ctrl.getEl() }); + editor.quirks.refreshContentEditable(); - ctrl.parents().each(function (ctrl) { - ctrl.fire("mouseleave", { target: ctrl.getEl() }); - }); + // 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; + } - eventRootCtrl._lastHoverCtrl = null; - } + if (editor.bookmark !== undefined && hasFocus(editor) === false) { + SelectionBookmark.getRng(editor).each(function (bookmarkRng) { + editor.selection.setRng(bookmarkRng); + rng = bookmarkRng; + }); } - function mouseEnterHandler(e) { - var ctrl = eventCtrl.getParentCtrl(e.target), lastCtrl = eventRootCtrl._lastHoverCtrl, idx = 0, i, parents, lastParents; + // 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); + } - // Over on a new control - if (ctrl !== lastCtrl) { - eventRootCtrl._lastHoverCtrl = ctrl; + editor.getWin().focus(); + } - parents = ctrl.parents().toArray().reverse(); - parents.push(ctrl); + // Focus the body as well since it's contentEditable + if (Env.gecko || contentEditable) { + focusBody(body); + normalizeSelection(editor, rng); + } - if (lastCtrl) { - lastParents = lastCtrl.parents().toArray().reverse(); - lastParents.push(lastCtrl); + activateEditor(editor); + }; - for (idx = 0; idx < lastParents.length; idx++) { - if (parents[idx] !== lastParents[idx]) { - break; - } - } - - for (i = lastParents.length - 1; i >= idx; i--) { - lastCtrl = lastParents[i]; - lastCtrl.fire("mouseleave", { - target: lastCtrl.getEl() - }); - } - } - - for (i = idx; i < parents.length; i++) { - ctrl = parents[i]; - ctrl.fire("mouseenter", { - target: ctrl.getEl() - }); - } - } - } - - function fixWheelEvent(e) { - e.preventDefault(); - - if (e.type == "mousewheel") { - e.deltaY = -1 / 40 * e.wheelDelta; - - if (e.wheelDeltaX) { - e.deltaX = -1 / 40 * e.wheelDeltaX; - } - } else { - e.deltaX = 0; - e.deltaY = e.detail; - } + var activateEditor = function (editor) { + editor.editorManager.setActive(editor); + }; - e = eventCtrl.fire("wheel", e); + var focus = function (editor, skipFocus) { + if (editor.removed) { + return; } - nativeEvents = eventCtrl._nativeEvents; - if (nativeEvents) { - // Find event root element if it exists - parents = eventCtrl.parents().toArray(); - parents.unshift(eventCtrl); - for (i = 0, l = parents.length; !eventRootCtrl && i < l; i++) { - eventRootCtrl = parents[i]._eventsRoot; - } - - // Event root wasn't found the use the root control - if (!eventRootCtrl) { - eventRootCtrl = parents[parents.length - 1] || eventCtrl; - } - - // Set the eventsRoot property on children that didn't have it - eventCtrl._eventsRoot = eventRootCtrl; - for (l = i, i = 0; i < l; i++) { - parents[i]._eventsRoot = eventRootCtrl; - } - - var eventRootDelegates = eventRootCtrl._delegates; - if (!eventRootDelegates) { - eventRootDelegates = eventRootCtrl._delegates = {}; - } - - // Bind native event delegates - for (name in nativeEvents) { - if (!nativeEvents) { - return false; - } - - if (name === "wheel" && !hasWheelEventSupport) { - if (hasMouseWheelEventSupport) { - $(eventCtrl.getEl()).on("mousewheel", fixWheelEvent); - } else { - $(eventCtrl.getEl()).on("DOMMouseScroll", fixWheelEvent); - } - - continue; - } - - // Special treatment for mousenter/mouseleave since these doesn't bubble - if (name === "mouseenter" || name === "mouseleave") { - // Fake mousenter/mouseleave - if (!eventRootCtrl._hasMouseEnter) { - $(eventRootCtrl.getEl()).on("mouseleave", mouseLeaveHandler).on("mouseover", mouseEnterHandler); - eventRootCtrl._hasMouseEnter = 1; - } - } else if (!eventRootDelegates[name]) { - $(eventRootCtrl.getEl()).on(name, delegate); - eventRootDelegates[name] = true; - } - - // Remove the event once it's bound - nativeEvents[name] = false; - } - } - } + skipFocus ? activateEditor(editor) : focusEditor(editor); + }; - return Control; + return { + focus: focus, + hasFocus: hasFocus + }; } ); /** - * Movable.js + * ScrollIntoView.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -24604,279 +23213,236 @@ define( * 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.dom.ScrollIntoView', [ - "tinymce.core.ui.DomUtils" + 'tinymce.core.dom.NodeType' ], - 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; + function (NodeType) { + var getPos = function (elm) { + var x = 0, y = 0; - if (ctrl.state.get('fixed') && DomUtils.getRuntimeStyle(document.body, 'position') == 'static') { - x -= viewport.x; - y -= viewport.y; + var offsetParent = elm; + while (offsetParent && offsetParent.nodeType) { + x += offsetParent.offsetLeft || 0; + y += offsetParent.offsetTop || 0; + offsetParent = offsetParent.offsetParent; } - // Get size of self - ctrlElm = ctrl.getEl(); - size = DomUtils.getSize(ctrlElm); - selfW = size.width; - selfH = size.height; + return { x: x, y: y }; + }; - // Get size of target - size = DomUtils.getSize(targetElm); - targetW = size.width; - targetH = size.height; + var fireScrollIntoViewEvent = function (editor, elm, alignToTop) { + var scrollEvent = { elm: elm, alignToTop: alignToTop }; + editor.fire('scrollIntoView', scrollEvent); + return scrollEvent.isDefaultPrevented(); + }; - // Parse align string - rel = (rel || '').split(''); + var scrollIntoView = function (editor, elm, alignToTop) { + var y, viewPort, dom = editor.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0; - // Target corners - if (rel[0] === 'b') { - y += targetH; + if (fireScrollIntoViewEvent(editor, elm, alignToTop)) { + return; } - if (rel[1] === 'r') { - x += targetW; + if (!NodeType.isElement(elm)) { + return; } - if (rel[0] === 'c') { - y += Math.round(targetH / 2); + if (alignToTop === false) { + offsetY = elm.offsetHeight; } - if (rel[1] === 'c') { - x += Math.round(targetW / 2); - } + if (root.nodeName !== 'BODY') { + var scrollContainer = editor.selection.getScrollContainer(); + if (scrollContainer) { + y = getPos(elm).y - getPos(scrollContainer).y + offsetY; + viewPortH = scrollContainer.clientHeight; + viewPortY = scrollContainer.scrollTop; + if (y < viewPortY || y + 25 > viewPortY + viewPortH) { + scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25; + } - // Self corners - if (rel[3] === 'b') { - y -= selfH; + return; + } } - if (rel[4] === 'r') { - x -= selfW; + viewPort = dom.getViewPort(editor.getWin()); + y = dom.getPos(elm).y + offsetY; + viewPortY = viewPort.y; + viewPortH = viewPort.h; + if (y < viewPort.y || y + 25 > viewPortY + viewPortH) { + editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25); } + }; - if (rel[3] === 'c') { - y -= Math.round(selfH / 2); - } + return { + scrollIntoView: scrollIntoView + }; + } +); - if (rel[4] === 'c') { - x -= Math.round(selfW / 2); - } +/** + * EventProcessRanges.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 + */ - return { - x: x, - y: y, - w: selfW, - h: selfH - }; - } +define( + 'tinymce.core.selection.EventProcessRanges', + [ + 'ephox.katamari.api.Arr' + ], + function (Arr) { + var processRanges = function (editor, ranges) { + return Arr.map(ranges, function (range) { + var evt = editor.fire('GetSelectionRange', { range: range }); + return evt.range !== range ? evt.range : range; + }); + }; 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(); + processRanges: processRanges + }; + } +); - 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]; - }, +define( + 'ephox.sugar.api.dom.Replication', - /** - * 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); - } + [ + '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' + ], - var pos = calculateRelativePosition(this, elm, rel); - return this.moveTo(pos.x, pos.y); - }, + function (Attr, Element, Insert, InsertAll, Remove, Traverse) { + var clone = function (original, deep) { + return Element.fromDom(original.dom().cloneNode(deep)); + }; - /** - * 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(); + /** Shallow clone - just the tag, no children */ + var shallow = function (original) { + return clone(original, false); + }; - self.moveTo(rect.x + dx, rect.y + dy); + /** Deep clone - everything copied including children */ + var deep = function (original) { + return clone(original, true); + }; - return self; - }, + /** Shallow clone, with a new tag */ + var shallowAs = function (original, tag) { + var nu = Element.fromTag(tag); - /** - * 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; + var attributes = Attr.clone(original); + Attr.setAll(nu, attributes); - // TODO: Move this to some global class - function constrain(value, max, size) { - if (value < 0) { - return 0; - } + return nu; + }; - if (value + size > max) { - value = max - size; - return value < 0 ? 0 : value; - } + /** Deep clone, with a new tag */ + var copy = function (original, tag) { + var nu = shallowAs(original, tag); - return value; - } + // 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. - if (self.settings.constrainToViewport) { - var viewPortRect = DomUtils.getViewPort(window); - var layoutRect = self.layoutRect(); + var cloneChildren = Traverse.children(deep(original)); + InsertAll.append(nu, cloneChildren); - x = constrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w); - y = constrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h); - } + return nu; + }; - if (self.state.get('rendered')) { - self.layoutRect({ x: x, y: y }).repaint(); - } else { - self.settings.x = x; - self.settings.y = y; - } + /** Change the tag name, but keep all children */ + var mutate = function (original, tag) { + var nu = shallowAs(original, tag); - self.fire('move', { x: x, y: y }); + Insert.before(original, nu); + var children = Traverse.children(original); + InsertAll.append(nu, children); + Remove.remove(original); + return nu; + }; - return self; - } + return { + shallow: shallow, + shallowAs: shallowAs, + deep: deep, + copy: copy, + mutate: mutate }; } ); -/** - * 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', + 'ephox.sugar.api.search.SelectorFind', + [ - "tinymce.core.ui.Control", - "tinymce.core.ui.Movable" + 'ephox.sugar.api.search.PredicateFind', + 'ephox.sugar.api.search.Selectors', + 'ephox.sugar.impl.ClosestOrAncestor' ], - 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; + function (PredicateFind, Selectors, ClosestOrAncestor) { + // TODO: An internal SelectorFilter module that doesn't Element.fromDom() everything - return ( - '' - ); - }, + var first = function (selector) { + return Selectors.one(selector); + }; - bindStates: function () { - var self = this; + var ancestor = function (scope, selector, isRoot) { + return PredicateFind.ancestor(scope, function (e) { + return Selectors.is(e, selector); + }, isRoot); + }; - self.state.on('change:text', function (e) { - self.getEl().lastChild.innerHTML = self.encode(e.value); - }); + var sibling = function (scope, selector) { + return PredicateFind.sibling(scope, function (e) { + return Selectors.is(e, selector); + }); + }; - return self._super(); - }, + var child = function (scope, selector) { + return PredicateFind.child(scope, function (e) { + return Selectors.is(e, selector); + }); + }; - /** - * Repaints the control after a layout operation. - * - * @method repaint - */ - repaint: function () { - var self = this, style, rect; + var descendant = function (scope, selector) { + return Selectors.one(selector, scope); + }; - style = self.getEl().style; - rect = self._layoutRect; + // Returns Some(closest ancestor element (sugared)) matching 'selector' up to isRoot, or None() otherwise + var closest = function (scope, selector, isRoot) { + return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot); + }; - style.left = rect.x + 'px'; - style.top = rect.y + 'px'; - style.zIndex = 0xFFFF + 0xFFFF; - } - }); + return { + first: first, + ancestor: ancestor, + sibling: sibling, + child: child, + descendant: descendant, + closest: closest + }; } ); + /** - * Widget.js + * Parents.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -24885,154 +23451,46 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Widget base class a widget is a control that has a tooltip and some basic states. - * - * @class tinymce.ui.Widget - * @extends tinymce.ui.Control - */ define( - 'tinymce.core.ui.Widget', + 'tinymce.core.dom.Parents', [ - "tinymce.core.ui.Control", - "tinymce.core.ui.Tooltip" + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.search.Traverse' ], - function (Control, Tooltip) { - "use strict"; - - var tooltip; - - var Widget = Control.extend({ - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {String} tooltip Tooltip text to display when hovering. - * @setting {Boolean} autofocus True if the control should be focused when rendered. - * @setting {String} text Text to display inside widget. - */ - init: function (settings) { - var self = this; - - self._super(settings); - settings = self.settings; - self.canFocus = true; - - if (settings.tooltip && Widget.tooltips !== false) { - self.on('mouseenter', function (e) { - var tooltip = self.tooltip().moveTo(-0xFFFF); - - if (e.control == self) { - var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']); - - tooltip.classes.toggle('tooltip-n', rel == 'bc-tc'); - tooltip.classes.toggle('tooltip-nw', rel == 'bc-tl'); - tooltip.classes.toggle('tooltip-ne', rel == 'bc-tr'); - - tooltip.moveRel(self.getEl(), rel); - } else { - tooltip.hide(); - } - }); - - self.on('mouseleave mousedown click', function () { - self.tooltip().hide(); - }); - } - - self.aria('label', settings.ariaLabel || settings.tooltip); - }, - - /** - * Returns the current tooltip instance. - * - * @method tooltip - * @return {tinymce.ui.Tooltip} Tooltip instance. - */ - tooltip: function () { - if (!tooltip) { - tooltip = new Tooltip({ type: 'tooltip' }); - tooltip.renderTo(); - } - - return tooltip; - }, - - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this, settings = self.settings; - - self._super(); - - if (!self.parent() && (settings.width || settings.height)) { - self.initLayoutRect(); - self.repaint(); - } - - if (settings.autofocus) { - self.focus(); - } - }, - - bindStates: function () { - var self = this; - - function disable(state) { - self.aria('disabled', state); - self.classes.toggle('disabled', state); - } - - function active(state) { - self.aria('pressed', state); - self.classes.toggle('active', state); - } - - self.state.on('change:disabled', function (e) { - disable(e.value); - }); - - self.state.on('change:active', function (e) { - active(e.value); - }); - - if (self.state.get('disabled')) { - disable(true); - } - - if (self.state.get('active')) { - active(true); - } + function (Fun, Compare, Traverse) { + var dropLast = function (xs) { + return xs.slice(0, -1); + }; - return self._super(); - }, + 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 []; + } + }; - /** - * Removes the current control from DOM and from UI collections. - * - * @method remove - * @return {tinymce.ui.Control} Current control instance. - */ - remove: function () { - this._super(); + var parents = function (startNode, rootElm) { + return parentsUntil(startNode, rootElm, Fun.constant(false)); + }; - if (tooltip) { - tooltip.remove(); - tooltip = null; - } - } - }); + var parentsAndSelf = function (startNode, rootElm) { + return [startNode].concat(parents(startNode, rootElm)); + }; - return Widget; + return { + parentsUntil: parentsUntil, + parents: parents, + parentsAndSelf: parentsAndSelf + }; } ); /** - * Progress.js + * SelectionUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -25041,83 +23499,78 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Progress control. - * - * @-x-less Progress.less - * @class tinymce.ui.Progress - * @extends tinymce.ui.Control - */ define( - 'tinymce.core.ui.Progress', + 'tinymce.core.selection.SelectionUtils', [ - "tinymce.core.ui.Widget" + '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 (Widget) { - "use strict"; - - return Widget.extend({ - Defaults: { - value: 0 - }, - - init: function (settings) { - var self = this; + 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); + } + }; - self._super(settings); - self.classes.add('progress'); + 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); + } + }; - if (!self.settings.filter) { - self.settings.filter = function (value) { - return Math.round(value); - }; + var getFirstChildren = function (node) { + return Traverse.firstChild(node).fold( + Fun.constant([node]), + function (child) { + return [node].concat(getFirstChildren(child)); } - }, - - renderHtml: function () { - var self = this, id = self._id, prefix = this.classPrefix; - - return ( - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    0%
    ' + - '
    ' - ); - }, - - postRender: function () { - var self = this; - - self._super(); - self.value(self.settings.value); - - return self; - }, - - bindStates: function () { - var self = this; + ); + }; - function setValue(value) { - value = self.settings.filter(value); - self.getEl().lastChild.innerHTML = value + '%'; - self.getEl().firstChild.firstChild.style.width = value + '%'; + 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)); + } } + ); + }; - self.state.on('change:value', function (e) { - setValue(e.value); - }); - - setValue(self.state.get('value')); + 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 self._super(); - } - }); + return { + hasAllContentsSelected: hasAllContentsSelected + }; } ); + /** - * Notification.js + * SimpleTableModel.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -25126,170 +23579,164 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Creates a notification instance. - * - * @-x-less Notification.less - * @class tinymce.ui.Notification - * @extends tinymce.ui.Container - * @mixes tinymce.ui.Movable - */ define( - 'tinymce.core.ui.Notification', + 'tinymce.core.selection.SimpleTableModel', [ - "tinymce.core.ui.Control", - "tinymce.core.ui.Movable", - "tinymce.core.ui.Progress", - "tinymce.core.util.Delay" + '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 (Control, Movable, Progress, Delay) { - var updateLiveRegion = function (ctx, text) { - ctx.getEl().lastChild.textContent = text + (ctx.progressBar ? ' ' + ctx.progressBar.value() + '%' : ''); + 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; }; - return Control.extend({ - Mixins: [Movable], + var fillout = function (table, x, y, tr, td) { + var rowspan = getSpan(td, 'rowspan'); + var colspan = getSpan(td, 'colspan'); + var rows = table.rows(); - Defaults: { - classes: 'widget notification' - }, - - init: function (settings) { - var self = this; - - self._super(settings); - - self.maxWidth = settings.maxWidth; - - if (settings.text) { - self.text(settings.text); - } - - if (settings.icon) { - self.icon = settings.icon; + for (var y2 = y; y2 < y + rowspan; y2++) { + if (!rows[y2]) { + rows[y2] = tableRow(Replication.deep(tr), []); } - if (settings.color) { - self.color = settings.color; - } + for (var x2 = x; x2 < x + colspan; x2++) { + var cells = rows[y2].cells(); - if (settings.type) { - self.classes.add('notification-' + settings.type); + // 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); } + } + }; - if (settings.timeout && (settings.timeout < 0 || settings.timeout > 0) && !settings.closeButton) { - self.closeButton = false; - } else { - self.classes.add('has-close'); - self.closeButton = true; - } + var cellExists = function (table, x, y) { + var rows = table.rows(); + var cells = rows[y] ? rows[y].cells() : []; + return !!cells[x]; + }; - if (settings.progressBar) { - self.progressBar = new Progress(); - } + var skipCellsX = function (table, x, y) { + while (cellExists(table, x, y)) { + x++; + } - self.on('click', function (e) { - if (e.target.className.indexOf(self.classPrefix + 'close') != -1) { - self.close(); - } - }); - }, + return x; + }; - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, prefix = self.classPrefix, icon = '', closeButton = '', progressBar = '', notificationStyle = ''; + var getWidth = function (rows) { + return Arr.foldl(rows, function (acc, row) { + return row.cells().length > acc ? row.cells().length : acc; + }, 0); + }; - if (self.icon) { - icon = ''; + 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)); + } } + } - notificationStyle = ' style="max-width: ' + self.maxWidth + 'px;' + (self.color ? 'background-color: ' + self.color + ';"' : '"'); + return Option.none(); + }; - if (self.closeButton) { - closeButton = ''; - } + var extractRows = function (table, sx, sy, ex, ey) { + var newRows = []; + var rows = table.rows(); - if (self.progressBar) { - progressBar = self.progressBar.renderHtml(); - } + 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 ( - '' - ); - }, + return newRows; + }; - postRender: function () { - var self = this; + 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); - Delay.setTimeout(function () { - self.$el.addClass(self.classPrefix + 'in'); - updateLiveRegion(self, self.state.get('text')); - }, 100); + return tableModel(table.element(), getWidth(newRows), newRows); + }; - return self._super(); - }, + var createDomTable = function (table, rows) { + var tableElement = Replication.shallow(table.element()); + var tableBody = Element.fromTag('tbody'); - bindStates: function () { - var self = this; + InsertAll.append(tableBody, rows); + Insert.append(tableElement, tableBody); + + return tableElement; + }; - self.state.on('change:text', function (e) { - self.getEl().firstChild.innerHTML = e.value; - updateLiveRegion(self, e.value); + 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; }); - if (self.progressBar) { - self.progressBar.bindStates(); - self.progressBar.state.on('change:value', function (e) { - updateLiveRegion(self, self.state.get('text')); - }); - } - return self._super(); - }, - close: function () { - var self = this; + var tr = Replication.shallow(row.element()); + InsertAll.append(tr, cells); + return tr; + }); + }; - if (!self.fire('close').isDefaultPrevented()) { - self.remove(); - } + var fromDom = function (tableElm) { + var table = tableModel(Replication.shallow(tableElm), 0, []); - return self; - }, + 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); + }); + }); - /** - * Repaints the control after a layout operation. - * - * @method repaint - */ - repaint: function () { - var self = this, style, rect; + return tableModel(table.element(), getWidth(table.rows()), table.rows()); + }; - style = self.getEl().style; - rect = self._layoutRect; + var toDom = function (table) { + return createDomTable(table, modelRowsToDomRows(table)); + }; - style.left = rect.x + 'px'; - style.top = rect.y + 'px'; + 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); + }); + }); + }; - // Hardcoded arbitrary z-value because we want the - // notifications under the other windows - style.zIndex = 0xFFFF - 1; - } - }); + return { + fromDom: fromDom, + toDom: toDom, + subsection: subsection + }; } ); + /** - * NotificationManagerImpl.js + * MultiRange.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -25299,92 +23746,92 @@ define( */ define( - 'tinymce.core.ui.NotificationManagerImpl', + 'tinymce.core.selection.MultiRange', [ 'ephox.katamari.api.Arr', - 'tinymce.core.ui.DomUtils', - 'tinymce.core.ui.Notification', - 'tinymce.core.util.Tools' + 'ephox.sugar.api.node.Element', + 'tinymce.core.dom.RangeUtils' ], - function (Arr, DomUtils, Notification, Tools) { - return function (editor) { - var getEditorContainer = function (editor) { - return editor.inline ? editor.getElement() : editor.getContentAreaContainer(); - }; - - var getContainerWidth = function () { - var container = getEditorContainer(editor); - return DomUtils.getSize(container).width; - }; + function (Arr, Element, RangeUtils) { + var getRanges = function (selection) { + var ranges = []; - // Since the viewport will change based on the present notifications, we need to move them all to the - // top left of the viewport to give an accurate size measurement so we can position them later. - var prePositionNotifications = function (notifications) { - Arr.each(notifications, function (notification) { - notification.moveTo(0, 0); - }); - }; + for (var i = 0; i < selection.rangeCount; i++) { + ranges.push(selection.getRangeAt(i)); + } - var positionNotifications = function (notifications) { - if (notifications.length > 0) { - var firstItem = notifications.slice(0, 1)[0]; - var container = getEditorContainer(editor); - firstItem.moveRel(container, 'tc-tc'); - Arr.each(notifications, function (notification, index) { - if (index > 0) { - notification.moveRel(notifications[index - 1].getEl(), 'bc-tc'); - } - }); - } - }; + return ranges; + }; - var reposition = function (notifications) { - prePositionNotifications(notifications); - positionNotifications(notifications); - }; + var getSelectedNodes = function (ranges) { + return Arr.bind(ranges, function (range) { + var node = RangeUtils.getSelectedNode(range); + return node ? [ Element.fromDom(node) ] : []; + }); + }; - var open = function (args, closeCallback) { - var extendedArgs = Tools.extend(args, { maxWidth: getContainerWidth() }); - var notif = new Notification(extendedArgs); - notif.args = extendedArgs; + var hasMultipleRanges = function (selection) { + return getRanges(selection).length > 1; + }; - //If we have a timeout value - if (extendedArgs.timeout > 0) { - notif.timer = setTimeout(function () { - notif.close(); - closeCallback(); - }, extendedArgs.timeout); - } + return { + getRanges: getRanges, + getSelectedNodes: getSelectedNodes, + hasMultipleRanges: hasMultipleRanges + }; + } +); - notif.on('close', function () { - closeCallback(); - }); +/** + * 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 + */ - notif.renderTo(); +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); + }; - return notif; - }; + var getCellsFromElement = function (elm) { + var selectedCells = SelectorFilter.descendants(elm, 'td[data-mce-selected],th[data-mce-selected]'); + return selectedCells; + }; - var close = function (notification) { - notification.close(); - }; + var getCellsFromElementOrRanges = function (ranges, element) { + var selectedCells = getCellsFromElement(element); + var rangeCells = getCellsFromRanges(ranges); + return selectedCells.length > 0 ? selectedCells : rangeCells; + }; - var getArgs = function (notification) { - return notification.args; - }; + var getCellsFromEditor = function (editor) { + return getCellsFromElementOrRanges(MultiRange.getRanges(editor.selection.getSel()), Element.fromDom(editor.getBody())); + }; - return { - open: open, - close: close, - reposition: reposition, - getArgs: getArgs - }; + return { + getCellsFromRanges: getCellsFromRanges, + getCellsFromElement: getCellsFromElement, + getCellsFromElementOrRanges: getCellsFromElementOrRanges, + getCellsFromEditor: getCellsFromEditor }; } ); /** - * NotificationManager.js + * FragmentReader.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -25393,152 +23840,200 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class handles the creation of TinyMCE's notifications. - * - * @class tinymce.NotificationManager - * @example - * // Opens a new notification of type "error" with text "An error occurred." - * tinymce.activeEditor.notificationManager.open({ - * text: 'An error occurred.', - * type: 'error' - * }); - */ define( - 'tinymce.core.api.NotificationManager', + 'tinymce.core.selection.FragmentReader', [ 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Option', - 'tinymce.core.EditorView', - 'tinymce.core.ui.NotificationManagerImpl', - 'tinymce.core.util.Delay' + '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, Option, EditorView, NotificationManagerImpl, Delay) { - return function (editor) { - var notifications = []; - - var getImplementation = function () { - var theme = editor.theme; - return theme.getNotificationManagerImpl ? theme.getNotificationManagerImpl() : NotificationManagerImpl(editor); - }; + 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 getTopNotification = function () { - return Option.from(notifications[0]); - }; + 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 isEqual = function (a, b) { - return a.type === b.type && a.text === b.text && !a.progressBar && !a.timeout && !b.progressBar && !b.timeout; - }; + 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 reposition = function () { - getImplementation().reposition(notifications); - }; + 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 addNotification = function (notification) { - notifications.push(notification); - }; + 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 closeNotification = function (notification) { - Arr.findIndex(notifications, function (otherNotification) { - return otherNotification === notification; - }).each(function (index) { - // Mutate here since third party might have stored away the window array - // TODO: Consider breaking this api - notifications.splice(index, 1); - }); - }; + var emptyFragment = function () { + return Fragment.fromElements([]); + }; - var open = function (args) { - // Never open notification if editor has been removed. - if (editor.removed || !EditorView.isEditorAttachedToDom(editor)) { - return; - } + var getFragmentFromRange = function (rootNode, rng) { + return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng)); + }; - return Arr.find(notifications, function (notification) { - return isEqual(getImplementation().getArgs(notification), args); - }).getOrThunk(function () { - editor.editorManager.setActive(editor); + var getParentTable = function (rootElm, cell) { + return SelectorFind.ancestor(cell, 'table', Fun.curry(Compare.eq, rootElm)); + }; - var notification = getImplementation().open(args, function () { - closeNotification(notification); - reposition(); - }); + 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); - addNotification(notification); - reposition(); - return notification; + return SimpleTableModel.subsection(fullTableModel, firstCell, lastCell).map(function (sectionedTableModel) { + return Fragment.fromElements([SimpleTableModel.toDom(sectionedTableModel)]); }); - }; + }).getOrThunk(emptyFragment); + }; - var close = function () { - getTopNotification().each(function (notification) { - getImplementation().close(notification); - closeNotification(notification); - reposition(); - }); - }; + var getSelectionFragment = function (rootNode, ranges) { + return ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0]); + }; - var getNotifications = function () { - return notifications; - }; + var read = function (rootNode, ranges) { + var selectedCells = TableCellSelection.getCellsFromElementOrRanges(ranges, rootNode); + return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges); + }; - var registerEvents = function (editor) { - editor.on('SkinLoaded', function () { - var serviceMessage = editor.settings.service_message; + return { + read: read + }; + } +); - if (serviceMessage) { - open({ - text: serviceMessage, - type: 'warning', - timeout: 0, - icon: '' - }); - } - }); +/** + * GetSelectionContent.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 + */ - editor.on('ResizeEditor ResizeWindow', function () { - Delay.requestAnimationFrame(reposition); - }); +define( + 'tinymce.core.selection.GetSelectionContent', + [ + 'ephox.sugar.api.node.Element', + 'tinymce.core.selection.EventProcessRanges', + 'tinymce.core.selection.FragmentReader', + 'tinymce.core.selection.MultiRange', + 'tinymce.core.text.Zwsp' + ], + function (Element, EventProcessRanges, FragmentReader, MultiRange, Zwsp) { + var getContent = function (editor, args) { + var rng = editor.selection.getRng(), tmpElm = editor.dom.create("body"); + var sel = editor.selection.getSel(), whiteSpaceBefore, whiteSpaceAfter, fragment; + var ranges = EventProcessRanges.processRanges(editor, MultiRange.getRanges(sel)); + + args = args || {}; + whiteSpaceBefore = whiteSpaceAfter = ''; + args.get = true; + args.format = args.format || 'html'; + args.selection = true; + + args = editor.fire('BeforeGetContent', args); + if (args.isDefaultPrevented()) { + editor.fire('GetContent', args); + return args.content; + } - editor.on('remove', function () { - Arr.each(notifications, function (notification) { - getImplementation().close(notification); - }); - }); - }; + if (args.format === 'text') { + return editor.selection.isCollapsed() ? '' : Zwsp.trim(rng.text || (sel.toString ? sel.toString() : '')); + } - registerEvents(editor); + if (rng.cloneContents) { + fragment = args.contextual ? FragmentReader.read(Element.fromDom(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 = '
    ' + (rng.item ? rng.item(0).outerHTML : rng.htmlText); + tmpElm.removeChild(tmpElm.firstChild); + } else { + tmpElm.innerHTML = rng.toString(); + } - return { - /** - * Opens a new notification. - * - * @method open - * @param {Object} args Optional name/value settings collection contains things like timeout/color/message etc. - */ - open: open, + // Keep whitespace before and after + if (/^\s/.test(tmpElm.innerHTML)) { + whiteSpaceBefore = ' '; + } - /** - * Closes the top most notification. - * - * @method close - */ - close: close, + if (/\s+$/.test(tmpElm.innerHTML)) { + whiteSpaceAfter = ' '; + } - /** - * Returns the currently opened notification objects. - * - * @method getNotifications - * @return {Array} Array of the currently opened notifications. - */ - getNotifications: getNotifications - }; + args.getInner = true; + + args.content = editor.selection.isCollapsed() ? '' : whiteSpaceBefore + editor.selection.serializer.serialize(tmpElm, args) + whiteSpaceAfter; + editor.fire('GetContent', args); + + return args.content; + }; + + return { + getContent: getContent }; } ); /** - * Factory.js + * SetSelectionContent.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -25547,112 +24042,107 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class is a factory for control instances. This enables you - * to create instances of controls without having to require the UI controls directly. - * - * It also allow you to override or add new control types. - * - * @class tinymce.ui.Factory - */ define( - 'tinymce.core.ui.Factory', + 'tinymce.core.selection.SetSelectionContent', [ ], function () { - "use strict"; + var setContent = function (editor, content, args) { + var rng = editor.selection.getRng(), caretNode, doc = editor.getDoc(), frag, temp; - var types = {}; + args = args || { format: 'html' }; + args.set = true; + args.selection = true; + args.content = content; - return { - /** - * Adds a new control instance type to the factory. - * - * @method add - * @param {String} type Type name for example "button". - * @param {function} typeClass Class type function. - */ - add: function (type, typeClass) { - types[type.toLowerCase()] = typeClass; - }, + // Dispatch before set content event + if (!args.no_events) { + args = editor.fire('BeforeSetContent', args); + if (args.isDefaultPrevented()) { + editor.fire('SetContent', args); + return; + } + } - /** - * Returns true/false if the specified type exists or not. - * - * @method has - * @param {String} type Type to look for. - * @return {Boolean} true/false if the control by name exists. - */ - has: function (type) { - return !!types[type.toLowerCase()]; - }, + content = args.content; - /** - * Returns ui control module by name. - * - * @method get - * @param {String} type Type get. - * @return {Object} Module or undefined. - */ - get: function (type) { - var lctype = type.toLowerCase(); - var controlType = types.hasOwnProperty(lctype) ? types[lctype] : null; - if (controlType === null) { - throw new Error("Could not find module for type: " + type); - } + if (rng.insertNode) { + // Make caret marker since insertNode places the caret in the beginning of text after insert + content += '_'; - return controlType; - }, + // Delete and insert new node + if (rng.startContainer == doc && rng.endContainer == doc) { + // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents + doc.body.innerHTML = content; + } else { + rng.deleteContents(); - /** - * Creates a new control instance based on the settings provided. The instance created will be - * based on the specified type property it can also create whole structures of components out of - * the specified JSON object. - * - * @example - * tinymce.ui.Factory.create({ - * type: 'button', - * text: 'Hello world!' - * }); - * - * @method create - * @param {Object/String} settings Name/Value object with items used to create the type. - * @return {tinymce.ui.Control} Control instance based on the specified type. - */ - create: function (type, settings) { - var ControlType; + if (doc.body.childNodes.length === 0) { + doc.body.innerHTML = content; + } else { + // createContextualFragment doesn't exists in IE 9 DOMRanges + if (rng.createContextualFragment) { + rng.insertNode(rng.createContextualFragment(content)); + } else { + // Fake createContextualFragment call in IE 9 + frag = doc.createDocumentFragment(); + temp = doc.createElement('div'); - // If string is specified then use it as the type - if (typeof type == 'string') { - settings = settings || {}; - settings.type = type; - } else { - settings = type; - type = settings.type; + frag.appendChild(temp); + temp.outerHTML = content; + + rng.insertNode(frag); + } + } } - // Find control type - type = type.toLowerCase(); - ControlType = types[type]; + // Move to caret marker + caretNode = editor.dom.get('__caret'); - // #if debug + // Make sure we wrap it compleatly, Opera fails with a simple select call + rng = doc.createRange(); + rng.setStartBefore(caretNode); + rng.setEndBefore(caretNode); + editor.selection.setRng(rng); - if (!ControlType) { - throw new Error("Could not find control by type: " + type); - } + // Remove the caret position + editor.dom.remove('__caret'); - // #endif + try { + editor.selection.setRng(rng); + } catch (ex) { + // Might fail on Opera for some odd reason + } + } else { + if (rng.item) { + // Delete content and get caret text selection + doc.execCommand('Delete', false, null); + rng = editor.getRng(); + } - ControlType = new ControlType(settings); - ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine + // Explorer removes spaces from the beginning of pasted contents + if (/^\s+/.test(content)) { + rng.pasteHTML('_' + content); + editor.dom.remove('__mce_tmp'); + } else { + rng.pasteHTML(content); + } + } - return ControlType; + // Dispatch set content event + if (!args.no_events) { + editor.fire('SetContent', args); } }; + + return { + setContent: setContent + }; } ); + /** - * KeyboardNavigation.js + * Selection.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -25662,929 +24152,776 @@ define( */ /** - * This class handles keyboard navigation of controls and elements. + * 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.ui.KeyboardNavigation + * @class tinymce.dom.Selection + * @example + * // Getting the currently selected node for the active editor + * alert(tinymce.activeEditor.selection.getNode().nodeName); */ define( - 'tinymce.core.ui.KeyboardNavigation', + 'tinymce.core.dom.Selection', [ + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'tinymce.core.EditorFocus', + 'tinymce.core.Env', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.BookmarkManager', + 'tinymce.core.dom.ControlSelection', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.dom.ScrollIntoView', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.selection.EventProcessRanges', + 'tinymce.core.selection.GetSelectionContent', + 'tinymce.core.selection.MultiRange', + 'tinymce.core.selection.SelectionBookmark', + 'tinymce.core.selection.SetSelectionContent', + 'tinymce.core.util.Tools' ], - function () { - "use strict"; + function ( + Compare, Element, EditorFocus, Env, CaretPosition, BookmarkManager, ControlSelection, RangeUtils, ScrollIntoView, TreeWalker, EventProcessRanges, GetSelectionContent, + MultiRange, SelectionBookmark, SetSelectionContent, Tools + ) { + var each = Tools.each, trim = Tools.trim; + + var isAttachedToDom = function (node) { + return !!(node && node.ownerDocument) && Compare.contains(Element.fromDom(node.ownerDocument), Element.fromDom(node)); + }; - var hasTabstopData = function (elm) { - return elm.getAttribute('data-mce-tabstop') ? true : false; + 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); + } }; /** - * This class handles all keyboard navigation for WAI-ARIA support. Each root container - * gets an instance of this class. + * 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. */ - return function (settings) { - var root = settings.root, focusedElement, focusedControl; - - function isElement(node) { - return node && node.nodeType === 1; - } - - try { - focusedElement = document.activeElement; - } catch (ex) { - // IE sometimes fails to return a proper element - focusedElement = document.body; - } + function Selection(dom, win, serializer, editor) { + var self = this; - focusedControl = root.getParentCtrl(focusedElement); + self.dom = dom; + self.win = win; + self.serializer = serializer; + self.editor = editor; + self.bookmarkManager = new BookmarkManager(self); + self.controlSelection = new ControlSelection(self, editor); + } + Selection.prototype = { /** - * Returns the currently focused elements wai aria role of the currently - * focused element or specified element. + * 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. * - * @private - * @param {Element} elm Optional element to get role from. - * @return {String} Role of specified element. + * @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. */ - function getRole(elm) { - elm = elm || focusedElement; + setCursorLocation: function (node, offset) { + var self = this, rng = self.dom.createRng(); - if (isElement(elm)) { - return elm.getAttribute('role'); + 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); } - - return null; - } + }, /** - * Returns the wai role of the parent element of the currently - * focused element or specified element. + * Returns the selected contents using the DOM serializer passed in to this class. * - * @private - * @param {Element} elm Optional element to get parent role from. - * @return {String} Role of the first parent that has a role. - */ - function getParentRole(elm) { - var role, parent = elm || focusedElement; - - while ((parent = parent.parentNode)) { - if ((role = getRole(parent))) { - return role; - } - } - } - - /** - * Returns a wai aria property by name for example aria-selected. + * @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()); * - * @private - * @param {String} name Name of the aria property to get for example "disabled". - * @return {String} Aria property value. + * // Alerts the currently selected contents as plain text + * alert(tinymce.activeEditor.selection.getContent({format: 'text'})); */ - function getAriaProp(name) { - var elm = focusedElement; - - if (isElement(elm)) { - return elm.getAttribute('aria-' + name); - } - } + getContent: function (args) { + return GetSelectionContent.getContent(this.editor, args); + }, /** - * Is the element a text input element or not. + * Sets the current selection to the specified content. If any contents is selected it will be replaced + * with the contents passed in to this function. If there is no selection the contents will be inserted + * where the caret is placed in the editor/page. * - * @private - * @param {Element} elm Element to check if it's an text input element or not. - * @return {Boolean} True/false if the element is a text element or not. + * @method setContent + * @param {String} content HTML contents to set could also be other formats depending on settings. + * @param {Object} args Optional settings object with for example data format. + * @example + * // Inserts some HTML contents at the current selection + * tinymce.activeEditor.selection.setContent('Some contents'); */ - function isTextInputElement(elm) { - var tagName = elm.tagName.toUpperCase(); - - // Notice: since type can be "email" etc we don't check the type - // So all input elements gets treated as text input elements - return tagName == "INPUT" || tagName == "TEXTAREA" || tagName == "SELECT"; - } + setContent: function (content, args) { + SetSelectionContent.setContent(this.editor, content, args); + }, /** - * Returns true/false if the specified element can be focused or not. + * Returns the start element of a selection range. If the start is in a text + * node the parent element will be returned. * - * @private - * @param {Element} elm DOM element to check if it can be focused or not. - * @return {Boolean} True/false if the element can have focus. + * @method getStart + * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. + * @return {Element} Start element of selection range. */ - function canFocus(elm) { - if (isTextInputElement(elm) && !elm.hidden) { - return true; - } + getStart: function (real) { + var self = this, rng = self.getRng(), startElement; - if (hasTabstopData(elm)) { - return true; + startElement = rng.startContainer; + + if (startElement.nodeType == 1 && startElement.hasChildNodes()) { + if (!real || !rng.collapsed) { + startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; + } } - if (/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(getRole(elm))) { - return true; + if (startElement && startElement.nodeType == 3) { + return startElement.parentNode; } - return false; - } + return startElement; + }, /** - * Returns an array of focusable visible elements within the specified container element. + * Returns the end element of a selection range. If the end is in a text + * node the parent element will be returned. * - * @private - * @param {Element} elm DOM element to find focusable elements within. - * @return {Array} Array of focusable elements. + * @method getEnd + * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. + * @return {Element} End element of selection range. */ - function getFocusElements(elm) { - var elements = []; - - function collect(elm) { - if (elm.nodeType != 1 || elm.style.display == 'none' || elm.disabled) { - return; - } + getEnd: function (real) { + var self = this, rng = self.getRng(), endElement, endOffset; - if (canFocus(elm)) { - elements.push(elm); - } + endElement = rng.endContainer; + endOffset = rng.endOffset; - for (var i = 0; i < elm.childNodes.length; i++) { - collect(elm.childNodes[i]); + if (endElement.nodeType == 1 && endElement.hasChildNodes()) { + if (!real || !rng.collapsed) { + endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; } } - collect(elm || root.getEl()); - - return elements; - } - - /** - * Returns the navigation root control for the specified control. The navigation root - * is the control that the keyboard navigation gets scoped to for example a menubar or toolbar group. - * It will look for parents of the specified target control or the currently focused control if this option is omitted. - * - * @private - * @param {tinymce.ui.Control} targetControl Optional target control to find root of. - * @return {tinymce.ui.Control} Navigation root control. - */ - function getNavigationRoot(targetControl) { - var navigationRoot, controls; - - targetControl = targetControl || focusedControl; - controls = targetControl.parents().toArray(); - controls.unshift(targetControl); - - for (var i = 0; i < controls.length; i++) { - navigationRoot = controls[i]; - - if (navigationRoot.settings.ariaRoot) { - break; - } + if (endElement && endElement.nodeType == 3) { + return endElement.parentNode; } - return navigationRoot; - } + return endElement; + }, /** - * Focuses the first item in the specified targetControl element or the last aria index if the - * navigation root has the ariaRemember option enabled. + * Returns a bookmark location for the current selection. This bookmark object + * can then be used to restore the selection after some content modification to the document. * - * @private - * @param {tinymce.ui.Control} targetControl Target control to focus the first item in. + * @method getBookmark + * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. + * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. + * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. + * @example + * // Stores a bookmark of the current selection + * var bm = tinymce.activeEditor.selection.getBookmark(); + * + * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinymce.activeEditor.selection.moveToBookmark(bm); */ - function focusFirst(targetControl) { - var navigationRoot = getNavigationRoot(targetControl); - var focusElements = getFocusElements(navigationRoot.getEl()); - - if (navigationRoot.settings.ariaRemember && "lastAriaIndex" in navigationRoot) { - moveFocusToIndex(navigationRoot.lastAriaIndex, focusElements); - } else { - moveFocusToIndex(0, focusElements); - } - } + getBookmark: function (type, normalized) { + return this.bookmarkManager.getBookmark(type, normalized); + }, /** - * Moves the focus to the specified index within the elements list. - * This will scope the index to the size of the element list if it changed. + * Restores the selection to the specified bookmark. * - * @private - * @param {Number} idx Specified index to move to. - * @param {Array} elements Array with dom elements to move focus within. - * @return {Number} Input index or a changed index if it was out of range. + * @method moveToBookmark + * @param {Object} bookmark Bookmark to restore selection from. + * @return {Boolean} true/false if it was successful or not. + * @example + * // Stores a bookmark of the current selection + * var bm = tinymce.activeEditor.selection.getBookmark(); + * + * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); + * + * // Restore the selection bookmark + * tinymce.activeEditor.selection.moveToBookmark(bm); */ - function moveFocusToIndex(idx, elements) { - if (idx < 0) { - idx = elements.length - 1; - } else if (idx >= elements.length) { - idx = 0; - } - - if (elements[idx]) { - elements[idx].focus(); - } - - return idx; - } + moveToBookmark: function (bookmark) { + return this.bookmarkManager.moveToBookmark(bookmark); + }, /** - * Moves the focus forwards or backwards. + * Selects the specified element. This will place the start and end of the selection range around the element. * - * @private - * @param {Number} dir Direction to move in positive means forward, negative means backwards. - * @param {Array} elements Optional array of elements to move within defaults to the current navigation roots elements. + * @method select + * @param {Element} node HTML DOM element to select. + * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. + * @return {Element} Selected element the same element as the one that got passed in. + * @example + * // Select the first paragraph in the active editor + * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); */ - function moveFocus(dir, elements) { - var idx = -1, navigationRoot = getNavigationRoot(); + select: function (node, content) { + var self = this, dom = self.dom, rng = dom.createRng(), idx; + + if (node) { + if (!content && self.controlSelection.controlSelect(node)) { + return; + } - elements = elements || getFocusElements(navigationRoot.getEl()); + idx = dom.nodeIndex(node); + rng.setStart(node.parentNode, idx); + rng.setEnd(node.parentNode, idx + 1); - for (var i = 0; i < elements.length; i++) { - if (elements[i] === focusedElement) { - idx = i; + // Find first/last text node or BR element + if (content) { + self._moveEndPoint(rng, node, true); + self._moveEndPoint(rng, node); } + + self.setRng(rng); } - idx += dir; - navigationRoot.lastAriaIndex = moveFocusToIndex(idx, elements); - } + return node; + }, /** - * Moves the focus to the left this is called by the left key. + * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. * - * @private + * @method isCollapsed + * @return {Boolean} true/false state if the selection range is collapsed or not. + * Collapsed means if it's a caret or a larger selection. */ - function left() { - var parentRole = getParentRole(); + isCollapsed: function () { + var self = this, rng = self.getRng(), sel = self.getSel(); - if (parentRole == "tablist") { - moveFocus(-1, getFocusElements(focusedElement.parentNode)); - } else if (focusedControl.parent().submenu) { - cancel(); - } else { - moveFocus(-1); + if (!rng || rng.item) { + return false; } - } - - /** - * Moves the focus to the right this is called by the right key. - * - * @private - */ - function right() { - var role = getRole(), parentRole = getParentRole(); - if (parentRole == "tablist") { - moveFocus(1, getFocusElements(focusedElement.parentNode)); - } else if (role == "menuitem" && parentRole == "menu" && getAriaProp('haspopup')) { - enter(); - } else { - moveFocus(1); + if (rng.compareEndPoints) { + return rng.compareEndPoints('StartToEnd', rng) === 0; } - } - /** - * Moves the focus to the up this is called by the up key. - * - * @private - */ - function up() { - moveFocus(-1); - } + return !sel || rng.collapsed; + }, /** - * Moves the focus to the up this is called by the down key. + * Collapse the selection to start or end of range. * - * @private + * @method collapse + * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false. */ - function down() { - var role = getRole(), parentRole = getParentRole(); + collapse: function (toStart) { + var self = this, rng = self.getRng(); - if (role == "menuitem" && parentRole == "menubar") { - enter(); - } else if (role == "button" && getAriaProp('haspopup')) { - enter({ key: 'down' }); - } else { - moveFocus(1); - } - } + rng.collapse(!!toStart); + self.setRng(rng); + }, /** - * Moves the focus to the next item or previous item depending on shift key. + * Returns the browsers internal selection object. * - * @private - * @param {DOMEvent} e DOM event object. + * @method getSel + * @return {Selection} Internal browser selection object. */ - function tab(e) { - var parentRole = getParentRole(); - - if (parentRole == "tablist") { - var elm = getFocusElements(focusedControl.getEl('body'))[0]; - - if (elm) { - elm.focus(); - } - } else { - moveFocus(e.shiftKey ? -1 : 1); - } - } + getSel: function () { + var win = this.win; - /** - * Calls the cancel event on the currently focused control. This is normally done using the Esc key. - * - * @private - */ - function cancel() { - focusedControl.fire('cancel'); - } + return win.getSelection ? win.getSelection() : win.document.selection; + }, /** - * Calls the click event on the currently focused control. This is normally done using the Enter/Space keys. + * Returns the browsers internal range object. * - * @private - * @param {Object} aria Optional aria data to pass along with the enter event. + * @method getRng + * @param {Boolean} w3c Forces a compatible W3C range on IE. + * @return {Range} Internal browser range object. + * @see http://www.quirksmode.org/dom/range_intro.html + * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ */ - function enter(aria) { - aria = aria || {}; - focusedControl.fire('click', { target: focusedElement, aria: aria }); - } - - root.on('keydown', function (e) { - function handleNonTabOrEscEvent(e, handler) { - // Ignore non tab keys for text elements - if (isTextInputElement(focusedElement) || hasTabstopData(focusedElement)) { - return; - } - - if (getRole(focusedElement) === 'slider') { - return; - } + getRng: function (w3c) { + var self = this, selection, rng, elm, doc; - if (handler(e) !== false) { - e.preventDefault(); + function tryCompareBoundaryPoints(how, sourceRange, destinationRange) { + try { + return sourceRange.compareBoundaryPoints(how, destinationRange); + } catch (ex) { + // Gecko throws wrong document exception if the range points + // to nodes that where removed from the dom #6690 + // Browsers should mutate existing DOMRange instances so that they always point + // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink + // For performance reasons just return -1 + return -1; } } - if (e.isDefaultPrevented()) { - return; + if (!self.win) { + return null; } - switch (e.keyCode) { - case 37: // DOM_VK_LEFT - handleNonTabOrEscEvent(e, left); - break; - - case 39: // DOM_VK_RIGHT - handleNonTabOrEscEvent(e, right); - break; - - case 38: // DOM_VK_UP - handleNonTabOrEscEvent(e, up); - break; + doc = self.win.document; - case 40: // DOM_VK_DOWN - handleNonTabOrEscEvent(e, down); - break; + if (typeof doc === 'undefined' || doc === null) { + return null; + } - case 27: // DOM_VK_ESCAPE - cancel(); - break; + if (self.editor.bookmark !== undefined && EditorFocus.hasFocus(self.editor) === false) { + var bookmark = SelectionBookmark.getRng(self.editor); - case 14: // DOM_VK_ENTER - case 13: // DOM_VK_RETURN - case 32: // DOM_VK_SPACE - handleNonTabOrEscEvent(e, enter); - break; + if (bookmark.isSome()) { + return bookmark.getOr(doc.createRange()); + } + } - case 9: // DOM_VK_TAB - if (tab(e) !== false) { - e.preventDefault(); + try { + if ((selection = self.getSel())) { + if (selection.rangeCount > 0) { + rng = selection.getRangeAt(0); + } else { + rng = selection.createRange ? selection.createRange() : doc.createRange(); } - break; + } + } catch (ex) { + // IE throws unspecified error here if TinyMCE is placed in a frame/iframe } - }); - root.on('focusin', function (e) { - focusedElement = e.target; - focusedControl = e.control; - }); + rng = EventProcessRanges.processRanges(self.editor, [ rng ])[0]; - return { - focusFirst: focusFirst - }; - }; - } -); -/** - * Container.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 - */ + // No range found then create an empty one + // This can occur when the editor is placed in a hidden container element on Gecko + // Or on IE when there was an exception + if (!rng) { + rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); + } -/** - * Container control. This is extended by all controls that can have - * children such as panels etc. You can also use this class directly as an - * generic container instance. The container doesn't have any specific role or style. - * - * @-x-less Container.less - * @class tinymce.ui.Container - * @extends tinymce.ui.Control - */ -define( - 'tinymce.core.ui.Container', - [ - "tinymce.core.ui.Control", - "tinymce.core.ui.Collection", - "tinymce.core.ui.Selector", - "tinymce.core.ui.Factory", - "tinymce.core.ui.KeyboardNavigation", - "tinymce.core.util.Tools", - "tinymce.core.dom.DomQuery", - "tinymce.core.ui.ClassList", - "tinymce.core.ui.ReflowQueue" - ], - function (Control, Collection, Selector, Factory, KeyboardNavigation, Tools, $, ClassList, ReflowQueue) { - "use strict"; + // If range is at start of document then move it to start of body + if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { + elm = self.dom.getRoot(); + rng.setStart(elm, 0); + rng.setEnd(elm, 0); + } + + if (self.selectedRange && self.explicitRange) { + if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 && + tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) { + // Safari, Opera and Chrome only ever select text which causes the range to change. + // This lets us use the originally set range if the selection hasn't been changed by the user. + rng = self.explicitRange; + } else { + self.selectedRange = null; + self.explicitRange = null; + } + } - var selectorCache = {}; + return rng; + }, - return Control.extend({ /** - * Constructs a new control instance with the specified settings. + * Changes the selection to the specified DOM range. * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Array} items Items to add to container in JSON format or control instances. - * @setting {String} layout Layout manager by name to use. - * @setting {Object} defaults Default settings to apply to all items. + * @method setRng + * @param {Range} rng Range to select. + * @param {Boolean} forward Optional boolean if the selection is forwards or backwards. */ - init: function (settings) { - var self = this; - - self._super(settings); - settings = self.settings; + setRng: function (rng, forward) { + var self = this, sel, node, evt; - if (settings.fixed) { - self.state.set('fixed', true); + if (!isValidRange(rng)) { + return; } - self._items = new Collection(); + // Is IE specific range + if (rng.select) { + self.explicitRange = null; + + try { + rng.select(); + } catch (ex) { + // Needed for some odd IE bug #1843306 + } - if (self.isRtl()) { - self.classes.add('rtl'); + return; } - self.bodyClasses = new ClassList(function () { - if (self.state.get('rendered')) { - self.getEl('body').className = this.toString(); + sel = self.getSel(); + + evt = self.editor.fire('SetSelectionRange', { range: rng, forward: forward }); + rng = evt.range; + + if (sel) { + self.explicitRange = rng; + + try { + sel.removeAllRanges(); + sel.addRange(rng); + } catch (ex) { + // IE might throw errors here if the editor is within a hidden container and selection is changed } - }); - self.bodyClasses.prefix = self.classPrefix; - self.classes.add('container'); - self.bodyClasses.add('container-body'); + // Forward is set to false and we have an extend function + if (forward === false && sel.extend) { + sel.collapse(rng.endContainer, rng.endOffset); + sel.extend(rng.startContainer, rng.startOffset); + } - if (settings.containerCls) { - self.classes.add(settings.containerCls); + // adding range isn't always successful so we need to check range count otherwise an exception can occur + self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null; } - self._layout = Factory.create((settings.layout || '') + 'layout'); + // WebKit egde case selecting images works better using setBaseAndExtent when the image is floated + if (!rng.collapsed && rng.startContainer === rng.endContainer && sel.setBaseAndExtent && !Env.ie) { + if (rng.endOffset - rng.startOffset < 2) { + if (rng.startContainer.hasChildNodes()) { + node = rng.startContainer.childNodes[rng.startOffset]; + if (node && node.tagName === 'IMG') { + sel.setBaseAndExtent( + rng.startContainer, + rng.startOffset, + rng.endContainer, + rng.endOffset + ); - if (self.settings.items) { - self.add(self.settings.items); - } else { - self.add(self.render()); + // Since the setBaseAndExtent is fixed in more recent Blink versions we + // need to detect if it's doing the wrong thing and falling back to the + // crazy incorrect behavior api call since that seems to be the only way + // to get it to work on Safari WebKit as of 2017-02-23 + if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) { + sel.setBaseAndExtent(node, 0, node, 1); + } + } + } + } } - // TODO: Fix this! - self._hasBody = true; + self.editor.fire('AfterSetSelectionRange', { range: rng, forward: forward }); }, /** - * Returns a collection of child items that the container currently have. + * Sets the current selection to the specified DOM element. * - * @method items - * @return {tinymce.ui.Collection} Control collection direct child controls. - */ - items: function () { - return this._items; - }, - - /** - * Find child controls by selector. - * - * @method find - * @param {String} selector Selector CSS pattern to find children by. - * @return {tinymce.ui.Collection} Control collection with child controls. - */ - find: function (selector) { - selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector); - - return selector.find(this); - }, - - /** - * Adds one or many items to the current container. This will create instances of - * the object representations if needed. - * - * @method add - * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container. - * @return {tinymce.ui.Collection} Current collection control. + * @method setNode + * @param {Element} elm Element to set as the contents of the selection. + * @return {Element} Returns the element that got passed in. + * @example + * // Inserts a DOM node at current selection/caret location + * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'})); */ - add: function (items) { + setNode: function (elm) { var self = this; - self.items().add(self.create(items)).parent(self); + self.setContent(self.dom.getOuterHTML(elm)); - return self; + return elm; }, /** - * Focuses the current container instance. This will look - * for the first control in the container and focus that. + * Returns the currently selected element or the common ancestor element for both start and end of the selection. * - * @method focus - * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not. - * @return {tinymce.ui.Collection} Current instance. + * @method getNode + * @return {Element} Currently selected element or common ancestor element. + * @example + * // Alerts the currently selected elements node name + * alert(tinymce.activeEditor.selection.getNode().nodeName); */ - focus: function (keyboard) { - var self = this, focusCtrl, keyboardNav, items; - - if (keyboard) { - keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav; - - if (keyboardNav) { - keyboardNav.focusFirst(self); - return; - } - } - - items = self.find('*'); - - // TODO: Figure out a better way to auto focus alert dialog buttons - if (self.statusbar) { - items.add(self.statusbar.items()); - } + getNode: function () { + var self = this, rng = self.getRng(), elm; + var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot(); - items.each(function (ctrl) { - if (ctrl.settings.autofocus) { - focusCtrl = null; - return false; - } + function skipEmptyTextNodes(node, forwards) { + var orig = node; - if (ctrl.canFocus) { - focusCtrl = focusCtrl || ctrl; + while (node && node.nodeType === 3 && node.length === 0) { + node = forwards ? node.nextSibling : node.previousSibling; } - }); - if (focusCtrl) { - focusCtrl.focus(); - } - - return self; - }, - - /** - * Replaces the specified child control with a new control. - * - * @method replace - * @param {tinymce.ui.Control} oldItem Old item to be replaced. - * @param {tinymce.ui.Control} newItem New item to be inserted. - */ - replace: function (oldItem, newItem) { - var ctrlElm, items = this.items(), i = items.length; - - // Replace the item in collection - while (i--) { - if (items[i] === oldItem) { - items[i] = newItem; - break; - } + return node || orig; } - if (i >= 0) { - // Remove new item from DOM - ctrlElm = newItem.getEl(); - if (ctrlElm) { - ctrlElm.parentNode.removeChild(ctrlElm); - } - - // Remove old item from DOM - ctrlElm = oldItem.getEl(); - if (ctrlElm) { - ctrlElm.parentNode.removeChild(ctrlElm); - } + // Range maybe lost after the editor is made visible again + if (!rng) { + return root; } - // Adopt the item - newItem.parent(this); - }, - - /** - * Creates the specified items. If any of the items is plain JSON style objects - * it will convert these into real tinymce.ui.Control instances. - * - * @method create - * @param {Array} items Array of items to convert into control instances. - * @return {Array} Array with control instances. - */ - create: function (items) { - var self = this, settings, ctrlItems = []; - - // Non array structure, then force it into an array - if (!Tools.isArray(items)) { - items = [items]; - } + startContainer = rng.startContainer; + endContainer = rng.endContainer; + startOffset = rng.startOffset; + endOffset = rng.endOffset; + elm = rng.commonAncestorContainer; - // Add default type to each child control - Tools.each(items, function (item) { - if (item) { - // Construct item if needed - if (!(item instanceof Control)) { - // Name only then convert it to an object - if (typeof item == "string") { - item = { type: item }; + // Handle selection a image or other control like element such as anchors + if (!rng.collapsed) { + if (startContainer == endContainer) { + if (endOffset - startOffset < 2) { + if (startContainer.hasChildNodes()) { + elm = startContainer.childNodes[startOffset]; } - - // Create control instance based on input settings and default settings - settings = Tools.extend({}, self.settings.defaults, item); - item.type = settings.type = settings.type || item.type || self.settings.defaultType || - (settings.defaults ? settings.defaults.type : null); - item = Factory.create(settings); } - - ctrlItems.push(item); } - }); - - return ctrlItems; - }, - - /** - * Renders new control instances. - * - * @private - */ - renderNew: function () { - var self = this; - - // Render any new items - self.items().each(function (ctrl, index) { - var containerElm; - ctrl.parent(self); + // If the anchor node is a element instead of a text node then return this element + //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) + // return sel.anchorNode.childNodes[sel.anchorOffset]; - if (!ctrl.state.get('rendered')) { - containerElm = self.getEl('body'); + // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. + // This happens when you double click an underlined word in FireFox. + if (startContainer.nodeType === 3 && endContainer.nodeType === 3) { + if (startContainer.length === startOffset) { + startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); + } else { + startContainer = startContainer.parentNode; + } - // Insert or append the item - if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) { - $(containerElm.childNodes[index]).before(ctrl.renderHtml()); + if (endOffset === 0) { + endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); } else { - $(containerElm).append(ctrl.renderHtml()); + endContainer = endContainer.parentNode; } - ctrl.postRender(); - ReflowQueue.add(ctrl); + if (startContainer && startContainer === endContainer) { + return startContainer; + } } - }); - - self._layout.applyClasses(self.items().filter(':visible')); - self._lastRect = null; + } - return self; - }, + if (elm && elm.nodeType == 3) { + return elm.parentNode; + } - /** - * Appends new instances to the current container. - * - * @method append - * @param {Array/tinymce.ui.Collection} items Array if controls to append. - * @return {tinymce.ui.Container} Current container instance. - */ - append: function (items) { - return this.add(items).renderNew(); + return elm; }, - /** - * Prepends new instances to the current container. - * - * @method prepend - * @param {Array/tinymce.ui.Collection} items Array if controls to prepend. - * @return {tinymce.ui.Container} Current container instance. - */ - prepend: function (items) { - var self = this; - - self.items().set(self.create(items).concat(self.items().toArray())); + getSelectedBlocks: function (startElm, endElm) { + var self = this, dom = self.dom, node, root, selectedBlocks = []; - return self.renderNew(); - }, + root = dom.getRoot(); + startElm = dom.getParent(startElm || self.getStart(), dom.isBlock); + endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock); - /** - * Inserts an control at a specific index. - * - * @method insert - * @param {Array/tinymce.ui.Collection} items Array if controls to insert. - * @param {Number} index Index to insert controls at. - * @param {Boolean} [before=false] Inserts controls before the index. - */ - insert: function (items, index, before) { - var self = this, curItems, beforeItems, afterItems; + if (startElm && startElm != root) { + selectedBlocks.push(startElm); + } - items = self.create(items); - curItems = self.items(); + if (startElm && endElm && startElm != endElm) { + node = startElm; - if (!before && index < curItems.length - 1) { - index += 1; + var walker = new TreeWalker(startElm, root); + while ((node = walker.next()) && node != endElm) { + if (dom.isBlock(node)) { + selectedBlocks.push(node); + } + } } - if (index >= 0 && index < curItems.length) { - beforeItems = curItems.slice(0, index).toArray(); - afterItems = curItems.slice(index).toArray(); - curItems.set(beforeItems.concat(items, afterItems)); + if (endElm && startElm != endElm && endElm != root) { + selectedBlocks.push(endElm); } - return self.renderNew(); + return selectedBlocks; }, - /** - * Populates the form fields from the specified JSON data object. - * - * Control items in the form that matches the data will have it's value set. - * - * @method fromJSON - * @param {Object} data JSON data object to set control values by. - * @return {tinymce.ui.Container} Current form instance. - */ - fromJSON: function (data) { - var self = this; + isForward: function () { + var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; - for (var name in data) { - self.find('#' + name).value(data[name]); + // No support for selection direction then always return true + if (!sel || !sel.anchorNode || !sel.focusNode) { + return true; } - return self; - }, + anchorRange = dom.createRng(); + anchorRange.setStart(sel.anchorNode, sel.anchorOffset); + anchorRange.collapse(true); - /** - * Serializes the form into a JSON object by getting all items - * that has a name and a value. - * - * @method toJSON - * @return {Object} JSON object with form data. - */ - toJSON: function () { - var self = this, data = {}; + focusRange = dom.createRng(); + focusRange.setStart(sel.focusNode, sel.focusOffset); + focusRange.collapse(true); - self.find('*').each(function (ctrl) { - var name = ctrl.name(), value = ctrl.value(); + return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; + }, - if (name && typeof value != "undefined") { - data[name] = value; - } - }); + normalize: function () { + var self = this, rng = self.getRng(); + + if (new RangeUtils(self.dom).normalize(rng) && !MultiRange.hasMultipleRanges(self.getSel())) { + self.setRng(rng, self.isForward()); + } - return data; + return rng; }, /** - * Renders the control as a HTML string. + * Executes callback when the current selection starts/stops matching the specified selector. The current + * state will be passed to the callback as it's first argument. * - * @method renderHtml - * @return {String} HTML representing the control. + * @method selectorChanged + * @param {String} selector CSS selector to check for. + * @param {function} callback Callback with state and args when the selector is matches or not. */ - renderHtml: function () { - var self = this, layout = self._layout, role = this.settings.role; + selectorChanged: function (selector, callback) { + var self = this, currentSelectors; - self.preRender(); - layout.preRender(self); + if (!self.selectorChangedData) { + self.selectorChangedData = {}; + currentSelectors = {}; - return ( - '
    ' + - '
    ' + - (self.settings.html || '') + layout.renderHtml(self) + - '
    ' + - '
    ' - ); - }, + self.editor.on('NodeChange', function (e) { + var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; - /** - * Post render method. Called after the control has been rendered to the target. - * - * @method postRender - * @return {tinymce.ui.Container} Current combobox instance. - */ - postRender: function () { - var self = this, box; + // Check for new matching selectors + each(self.selectorChangedData, function (callbacks, selector) { + each(parents, function (node) { + if (dom.is(node, selector)) { + if (!currentSelectors[selector]) { + // Execute callbacks + each(callbacks, function (callback) { + callback(true, { node: node, selector: selector, parents: parents }); + }); - self.items().exec('postRender'); - self._super(); + currentSelectors[selector] = callbacks; + } - self._layout.postRender(self); - self.state.set('rendered', true); + matchedSelectors[selector] = callbacks; + return false; + } + }); + }); - if (self.settings.style) { - self.$el.css(self.settings.style); - } + // Check if current selectors still match + each(currentSelectors, function (callbacks, selector) { + if (!matchedSelectors[selector]) { + delete currentSelectors[selector]; - if (self.settings.border) { - box = self.borderBox; - self.$el.css({ - 'border-top-width': box.top, - 'border-right-width': box.right, - 'border-bottom-width': box.bottom, - 'border-left-width': box.left + each(callbacks, function (callback) { + callback(false, { node: node, selector: selector, parents: parents }); + }); + } + }); }); } - if (!self.parent()) { - self.keyboardNav = new KeyboardNavigation({ - root: self - }); + // Add selector listeners + if (!self.selectorChangedData[selector]) { + self.selectorChangedData[selector] = []; } + self.selectorChangedData[selector].push(callback); + return self; }, - /** - * Initializes the current controls layout rect. - * This will be executed by the layout managers to determine the - * default minWidth/minHeight etc. - * - * @method initLayoutRect - * @return {Object} Layout rect instance. - */ - initLayoutRect: function () { - var self = this, layoutRect = self._super(); + getScrollContainer: function () { + var scrollContainer, node = this.dom.getRoot(); + + while (node && node.nodeName != 'BODY') { + if (node.scrollHeight > node.clientHeight) { + scrollContainer = node; + break; + } - // Recalc container size by asking layout manager - self._layout.recalc(self); + node = node.parentNode; + } - return layoutRect; + return scrollContainer; }, - /** - * Recalculates the positions of the controls in the current container. - * This is invoked by the reflow method and shouldn't be called directly. - * - * @method recalc - */ - recalc: function () { - var self = this, rect = self._layoutRect, lastRect = self._lastRect; + scrollIntoView: function (elm, alignToTop) { + ScrollIntoView.scrollIntoView(this.editor, elm, alignToTop); + }, - if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) { - self._layout.recalc(self); - rect = self.layoutRect(); - self._lastRect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h }; - return true; - } + placeCaretAt: function (clientX, clientY) { + this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc())); }, - /** - * Reflows the current container and it's children and possible parents. - * This should be used after you for example append children to the current control so - * that the layout managers know that they need to reposition everything. - * - * @example - * container.append({type: 'button', text: 'My button'}).reflow(); - * - * @method reflow - * @return {tinymce.ui.Container} Current container instance. - */ - reflow: function () { - var i; + _moveEndPoint: function (rng, node, start) { + var root = node, walker = new TreeWalker(node, root); + var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements(); - ReflowQueue.remove(this); + do { + // Text node + if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) { + if (start) { + rng.setStart(node, 0); + } else { + rng.setEnd(node, node.nodeValue.length); + } - if (this.visible()) { - Control.repaintControls = []; - Control.repaintControls.map = {}; + return; + } - this.recalc(); - i = Control.repaintControls.length; + // BR/IMG/INPUT elements but not table cells + if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) { + if (start) { + rng.setStartBefore(node); + } else { + if (node.nodeName == 'BR') { + rng.setEndBefore(node); + } else { + rng.setEndAfter(node); + } + } - while (i--) { - Control.repaintControls[i].repaint(); + return; } - // TODO: Fix me! - if (this.settings.layout !== "flow" && this.settings.layout !== "stack") { - this.repaint(); + // Found empty text block old IE can place the selection inside those + if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) { + if (start) { + rng.setStart(node, 0); + } else { + rng.setEnd(node, 0); + } + + return; } + } while ((node = (start ? walker.next() : walker.prev()))); - Control.repaintControls = []; + // Failed to find any text node or other suitable location then move to the root of body + if (root.nodeName == 'BODY') { + if (start) { + rng.setStart(root, 0); + } else { + rng.setEnd(root, root.childNodes.length); + } } + }, - return this; + getBoundingClientRect: function () { + var rng = this.getRng(); + return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect(); + }, + + destroy: function () { + this.win = null; + this.controlSelection.destroy(); } - }); + }; + + return Selection; } ); + /** - * DragHelper.js + * Node.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -26594,852 +24931,513 @@ define( */ /** - * Drag/drop helper class. + * This class is a minimalistic implementation of a DOM like node used by the DomParser class. * * @example - * var dragHelper = new tinymce.ui.DragHelper('mydiv', { - * start: function(evt) { - * }, - * - * drag: function(evt) { - * }, - * - * end: function(evt) { - * } - * }); + * var node = new tinymce.html.Node('strong', 1); + * someRoot.append(node); * - * @class tinymce.ui.DragHelper + * @class tinymce.html.Node + * @version 3.4 */ define( - 'tinymce.core.ui.DragHelper', + 'tinymce.core.html.Node', [ - "tinymce.core.dom.DomQuery" ], - function ($) { - "use strict"; - - function getDocumentSize(doc) { - var documentElement, body, scrollWidth, clientWidth; - var offsetWidth, scrollHeight, clientHeight, offsetHeight, max = Math.max; + function () { + var whiteSpaceRegExp = /^[ \t\r\n]*$/; + var typeLookup = { + '#text': 3, + '#comment': 8, + '#cdata': 4, + '#pi': 7, + '#doctype': 10, + '#document-fragment': 11 + }; - documentElement = doc.documentElement; - body = doc.body; + // Walks the tree left/right + function walk(node, rootNode, prev) { + var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; - scrollWidth = max(documentElement.scrollWidth, body.scrollWidth); - clientWidth = max(documentElement.clientWidth, body.clientWidth); - offsetWidth = max(documentElement.offsetWidth, body.offsetWidth); + // Walk into nodes if it has a start + if (node[startName]) { + return node[startName]; + } - scrollHeight = max(documentElement.scrollHeight, body.scrollHeight); - clientHeight = max(documentElement.clientHeight, body.clientHeight); - offsetHeight = max(documentElement.offsetHeight, body.offsetHeight); + // Return the sibling if it has one + if (node !== rootNode) { + sibling = node[siblingName]; - return { - width: scrollWidth < offsetWidth ? clientWidth : scrollWidth, - height: scrollHeight < offsetHeight ? clientHeight : scrollHeight - }; - } + if (sibling) { + return sibling; + } - function updateWithTouchData(e) { - var keys, i; + // Walk up the parents to look for siblings + for (parent = node.parent; parent && parent !== rootNode; parent = parent.parent) { + sibling = parent[siblingName]; - if (e.changedTouches) { - keys = "screenX screenY pageX pageY clientX clientY".split(' '); - for (i = 0; i < keys.length; i++) { - e[keys[i]] = e.changedTouches[0][keys[i]]; + if (sibling) { + return sibling; + } } } } - return function (id, settings) { - var $eventOverlay, doc = settings.document || document, downButton, start, stop, drag, startX, startY; - - settings = settings || {}; + /** + * Constructs a new Node instance. + * + * @constructor + * @method Node + * @param {String} name Name of the node type. + * @param {Number} type Numeric type representing the node. + */ + function Node(name, type) { + this.name = name; + this.type = type; - function getHandleElm() { - return doc.getElementById(settings.handle || id); + if (type === 1) { + this.attributes = []; + this.attributes.map = {}; } + } - start = function (e) { - var docSize = getDocumentSize(doc), handleElm, cursor; - - updateWithTouchData(e); + Node.prototype = { + /** + * Replaces the current node with the specified one. + * + * @example + * someNode.replace(someNewNode); + * + * @method replace + * @param {tinymce.html.Node} node Node to replace the current node with. + * @return {tinymce.html.Node} The old node that got replaced. + */ + replace: function (node) { + var self = this; - e.preventDefault(); - downButton = e.button; - handleElm = getHandleElm(); - startX = e.screenX; - startY = e.screenY; - - // Grab cursor from handle so we can place it on overlay - if (window.getComputedStyle) { - cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor"); - } else { - cursor = handleElm.runtimeStyle.cursor; + if (node.parent) { + node.remove(); } - $eventOverlay = $('
    ').css({ - position: "absolute", - top: 0, left: 0, - width: docSize.width, - height: docSize.height, - zIndex: 0x7FFFFFFF, - opacity: 0.0001, - cursor: cursor - }).appendTo(doc.body); + self.insert(node, self); + self.remove(); - $(doc).on('mousemove touchmove', drag).on('mouseup touchend', stop); + return self; + }, - settings.start(e); - }; + /** + * Gets/sets or removes an attribute by name. + * + * @example + * someNode.attr("name", "value"); // Sets an attribute + * console.log(someNode.attr("name")); // Gets an attribute + * someNode.attr("name", null); // Removes an attribute + * + * @method attr + * @param {String} name Attribute name to set or get. + * @param {String} value Optional value to set. + * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. + */ + attr: function (name, value) { + var self = this, attrs, i, undef; - drag = function (e) { - updateWithTouchData(e); + if (typeof name !== "string") { + for (i in name) { + self.attr(i, name[i]); + } - if (e.button !== downButton) { - return stop(e); + return self; } - e.deltaX = e.screenX - startX; - e.deltaY = e.screenY - startY; + if ((attrs = self.attributes)) { + if (value !== undef) { + // Remove attribute + if (value === null) { + if (name in attrs.map) { + delete attrs.map[name]; + + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs = attrs.splice(i, 1); + return self; + } + } + } - e.preventDefault(); - settings.drag(e); - }; + return self; + } - stop = function (e) { - updateWithTouchData(e); + // Set attribute + if (name in attrs.map) { + // Set attribute + i = attrs.length; + while (i--) { + if (attrs[i].name === name) { + attrs[i].value = value; + break; + } + } + } else { + attrs.push({ name: name, value: value }); + } - $(doc).off('mousemove touchmove', drag).off('mouseup touchend', stop); + attrs.map[name] = value; - $eventOverlay.remove(); + return self; + } - if (settings.stop) { - settings.stop(e); + return attrs.map[name]; } - }; + }, /** - * Destroys the drag/drop helper instance. + * Does a shallow clones the node into a new node. It will also exclude id attributes since + * there should only be one id per document. * - * @method destroy + * @example + * var clonedNode = node.clone(); + * + * @method clone + * @return {tinymce.html.Node} New copy of the original node. */ - this.destroy = function () { - $(getHandleElm()).off(); - }; - - $(getHandleElm()).on('mousedown touchstart', start); - }; - } -); -/** - * Scrollable.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 mixin makes controls scrollable using custom scrollbars. - * - * @-x-less Scrollable.less - * @mixin tinymce.ui.Scrollable - */ -define( - 'tinymce.core.ui.Scrollable', - [ - "tinymce.core.dom.DomQuery", - "tinymce.core.ui.DragHelper" - ], - function ($, DragHelper) { - "use strict"; - - return { - init: function () { - var self = this; - self.on('repaint', self.renderScroll); - }, - - renderScroll: function () { - var self = this, margin = 2; - - function repaintScroll() { - var hasScrollH, hasScrollV, bodyElm; - - function repaintAxis(axisName, posName, sizeName, contentSizeName, hasScroll, ax) { - var containerElm, scrollBarElm, scrollThumbElm; - var containerSize, scrollSize, ratio, rect; - var posNameLower, sizeNameLower; - - scrollBarElm = self.getEl('scroll' + axisName); - if (scrollBarElm) { - posNameLower = posName.toLowerCase(); - sizeNameLower = sizeName.toLowerCase(); - - $(self.getEl('absend')).css(posNameLower, self.layoutRect()[contentSizeName] - 1); - - if (!hasScroll) { - $(scrollBarElm).css('display', 'none'); - return; - } + clone: function () { + var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; - $(scrollBarElm).css('display', 'block'); - containerElm = self.getEl('body'); - scrollThumbElm = self.getEl('scroll' + axisName + "t"); - containerSize = containerElm["client" + sizeName] - (margin * 2); - containerSize -= hasScrollH && hasScrollV ? scrollBarElm["client" + ax] : 0; - scrollSize = containerElm["scroll" + sizeName]; - ratio = containerSize / scrollSize; + // Clone element attributes + if ((selfAttrs = self.attributes)) { + cloneAttrs = []; + cloneAttrs.map = {}; - rect = {}; - rect[posNameLower] = containerElm["offset" + posName] + margin; - rect[sizeNameLower] = containerSize; - $(scrollBarElm).css(rect); + for (i = 0, l = selfAttrs.length; i < l; i++) { + selfAttr = selfAttrs[i]; - rect = {}; - rect[posNameLower] = containerElm["scroll" + posName] * ratio; - rect[sizeNameLower] = containerSize * ratio; - $(scrollThumbElm).css(rect); + // Clone everything except id + if (selfAttr.name !== 'id') { + cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value }; + cloneAttrs.map[selfAttr.name] = selfAttr.value; } } - bodyElm = self.getEl('body'); - hasScrollH = bodyElm.scrollWidth > bodyElm.clientWidth; - hasScrollV = bodyElm.scrollHeight > bodyElm.clientHeight; - - repaintAxis("h", "Left", "Width", "contentW", hasScrollH, "Height"); - repaintAxis("v", "Top", "Height", "contentH", hasScrollV, "Width"); + clone.attributes = cloneAttrs; } - function addScroll() { - function addScrollAxis(axisName, posName, sizeName, deltaPosName, ax) { - var scrollStart, axisId = self._id + '-scroll' + axisName, prefix = self.classPrefix; + clone.value = self.value; + clone.shortEnded = self.shortEnded; - $(self.getEl()).append( - '
    ' + - '
    ' + - '
    ' - ); - - self.draghelper = new DragHelper(axisId + 't', { - start: function () { - scrollStart = self.getEl('body')["scroll" + posName]; - $('#' + axisId).addClass(prefix + 'active'); - }, - - drag: function (e) { - var ratio, hasScrollH, hasScrollV, containerSize, layoutRect = self.layoutRect(); - - hasScrollH = layoutRect.contentW > layoutRect.innerW; - hasScrollV = layoutRect.contentH > layoutRect.innerH; - containerSize = self.getEl('body')["client" + sizeName] - (margin * 2); - containerSize -= hasScrollH && hasScrollV ? self.getEl('scroll' + axisName)["client" + ax] : 0; - - ratio = containerSize / self.getEl('body')["scroll" + sizeName]; - self.getEl('body')["scroll" + posName] = scrollStart + (e["delta" + deltaPosName] / ratio); - }, - - stop: function () { - $('#' + axisId).removeClass(prefix + 'active'); - } - }); - } - - self.classes.add('scroll'); - - addScrollAxis("v", "Top", "Height", "Y", "Width"); - addScrollAxis("h", "Left", "Width", "X", "Height"); - } - - if (self.settings.autoScroll) { - if (!self._hasScroll) { - self._hasScroll = true; - addScroll(); - - self.on('wheel', function (e) { - var bodyEl = self.getEl('body'); - - bodyEl.scrollLeft += (e.deltaX || 0) * 10; - bodyEl.scrollTop += e.deltaY * 10; - - repaintScroll(); - }); - - $(self.getEl('body')).on("scroll", repaintScroll); - } - - repaintScroll(); - } - } - }; - } -); -/** - * Panel.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 new panel. - * - * @-x-less Panel.less - * @class tinymce.ui.Panel - * @extends tinymce.ui.Container - * @mixes tinymce.ui.Scrollable - */ -define( - 'tinymce.core.ui.Panel', - [ - "tinymce.core.ui.Container", - "tinymce.core.ui.Scrollable" - ], - function (Container, Scrollable) { - "use strict"; - - return Container.extend({ - Defaults: { - layout: 'fit', - containerCls: 'panel' - }, - - Mixins: [Scrollable], + return clone; + }, /** - * Renders the control as a HTML string. + * Wraps the node in in another node. + * + * @example + * node.wrap(wrapperNode); * - * @method renderHtml - * @return {String} HTML representing the control. + * @method wrap */ - renderHtml: function () { - var self = this, layout = self._layout, innerHtml = self.settings.html; - - self.preRender(); - layout.preRender(self); - - if (typeof innerHtml == "undefined") { - innerHtml = ( - '
    ' + - layout.renderHtml(self) + - '
    ' - ); - } else { - if (typeof innerHtml == 'function') { - innerHtml = innerHtml.call(self); - } - - self._hasBody = false; - } - - return ( - '
    ' + - (self._preBodyHtml || '') + - innerHtml + - '
    ' - ); - } - }); - } -); - -/** - * Resizable.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 - */ + wrap: function (wrapper) { + var self = this; -/** - * Resizable mixin. Enables controls to be resized. - * - * @mixin tinymce.ui.Resizable - */ -define( - 'tinymce.core.ui.Resizable', - [ - "tinymce.core.ui.DomUtils" - ], - function (DomUtils) { - "use strict"; + self.parent.insert(wrapper, self); + wrapper.append(self); - return { - /** - * Resizes the control to contents. - * - * @method resizeToContent - */ - resizeToContent: function () { - this._layoutRect.autoResize = true; - this._lastRect = null; - this.reflow(); + return self; }, /** - * Resizes the control to a specific width/height. + * Unwraps the node in other words it removes the node but keeps the children. + * + * @example + * node.unwrap(); * - * @method resizeTo - * @param {Number} w Control width. - * @param {Number} h Control height. - * @return {tinymce.ui.Control} Current control instance. + * @method unwrap */ - resizeTo: function (w, h) { - // TODO: Fix hack - if (w <= 1 || h <= 1) { - var rect = DomUtils.getWindowSize(); + unwrap: function () { + var self = this, node, next; - w = w <= 1 ? w * rect.w : w; - h = h <= 1 ? h * rect.h : h; + for (node = self.firstChild; node;) { + next = node.next; + self.insert(node, self, true); + node = next; } - this._layoutRect.autoResize = false; - return this.layoutRect({ minW: w, minH: h, w: w, h: h }).reflow(); + self.remove(); }, /** - * Resizes the control to a specific relative width/height. + * Removes the node from it's parent. + * + * @example + * node.remove(); * - * @method resizeBy - * @param {Number} dw Relative control width. - * @param {Number} dh Relative control height. - * @return {tinymce.ui.Control} Current control instance. + * @method remove + * @return {tinymce.html.Node} Current node that got removed. */ - resizeBy: function (dw, dh) { - var self = this, rect = self.layoutRect(); - - return self.resizeTo(rect.w + dw, rect.h + dh); - } - }; - } -); -/** - * FloatPanel.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 creates a floating panel. - * - * @-x-less FloatPanel.less - * @class tinymce.ui.FloatPanel - * @extends tinymce.ui.Panel - * @mixes tinymce.ui.Movable - * @mixes tinymce.ui.Resizable - */ -define( - 'tinymce.core.ui.FloatPanel', - [ - "tinymce.core.ui.Panel", - "tinymce.core.ui.Movable", - "tinymce.core.ui.Resizable", - "tinymce.core.ui.DomUtils", - "tinymce.core.dom.DomQuery", - "tinymce.core.util.Delay" - ], - function (Panel, Movable, Resizable, DomUtils, $, Delay) { - "use strict"; - - var documentClickHandler, documentScrollHandler, windowResizeHandler, visiblePanels = []; - var zOrder = [], hasModal; - - function isChildOf(ctrl, parent) { - while (ctrl) { - if (ctrl == parent) { - return true; - } - - ctrl = ctrl.parent(); - } - } - - function skipOrHidePanels(e) { - // Hide any float panel when a click/focus out is out side that float panel and the - // float panels direct parent for example a click on a menu button - var i = visiblePanels.length; + remove: function () { + var self = this, parent = self.parent, next = self.next, prev = self.prev; - while (i--) { - var panel = visiblePanels[i], clickCtrl = panel.getParentCtrl(e.target); + if (parent) { + if (parent.firstChild === self) { + parent.firstChild = next; - if (panel.settings.autohide) { - if (clickCtrl) { - if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) { - continue; + if (next) { + next.prev = null; } + } else { + prev.next = next; } - e = panel.fire('autohide', { target: e.target }); - if (!e.isDefaultPrevented()) { - panel.hide(); - } - } - } - } - - function bindDocumentClickHandler() { - - if (!documentClickHandler) { - documentClickHandler = function (e) { - // Gecko fires click event and in the wrong order on Mac so lets normalize - if (e.button == 2) { - return; - } - - skipOrHidePanels(e); - }; - - $(document).on('click touchstart', documentClickHandler); - } - } - - function bindDocumentScrollHandler() { - if (!documentScrollHandler) { - documentScrollHandler = function () { - var i; - - i = visiblePanels.length; - while (i--) { - repositionPanel(visiblePanels[i]); - } - }; - - $(window).on('scroll', documentScrollHandler); - } - } - - function bindWindowResizeHandler() { - if (!windowResizeHandler) { - var docElm = document.documentElement, clientWidth = docElm.clientWidth, clientHeight = docElm.clientHeight; - - windowResizeHandler = function () { - // Workaround for #7065 IE 7 fires resize events event though the window wasn't resized - if (!document.all || clientWidth != docElm.clientWidth || clientHeight != docElm.clientHeight) { - clientWidth = docElm.clientWidth; - clientHeight = docElm.clientHeight; - FloatPanel.hideAll(); - } - }; - - $(window).on('resize', windowResizeHandler); - } - } - - /** - * Repositions the panel to the top of page if the panel is outside of the visual viewport. It will - * also reposition all child panels of the current panel. - */ - function repositionPanel(panel) { - var scrollY = DomUtils.getViewPort().y; - - function toggleFixedChildPanels(fixed, deltaY) { - var parent; - - for (var i = 0; i < visiblePanels.length; i++) { - if (visiblePanels[i] != panel) { - parent = visiblePanels[i].parent(); + if (parent.lastChild === self) { + parent.lastChild = prev; - while (parent && (parent = parent.parent())) { - if (parent == panel) { - visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint(); - } + if (prev) { + prev.next = null; } + } else { + next.prev = prev; } - } - } - if (panel.settings.autofix) { - if (!panel.state.get('fixed')) { - panel._autoFixY = panel.layoutRect().y; - - if (panel._autoFixY < scrollY) { - panel.fixed(true).layoutRect({ y: 0 }).repaint(); - toggleFixedChildPanels(true, scrollY - panel._autoFixY); - } - } else { - if (panel._autoFixY > scrollY) { - panel.fixed(false).layoutRect({ y: panel._autoFixY }).repaint(); - toggleFixedChildPanels(false, panel._autoFixY - scrollY); - } + self.parent = self.next = self.prev = null; } - } - } - function addRemove(add, ctrl) { - var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal; + return self; + }, - if (add) { - zOrder.push(ctrl); - } else { - i = zOrder.length; + /** + * Appends a new node as a child of the current node. + * + * @example + * node.append(someNode); + * + * @method append + * @param {tinymce.html.Node} node Node to append as a child of the current one. + * @return {tinymce.html.Node} The node that got appended. + */ + append: function (node) { + var self = this, last; - while (i--) { - if (zOrder[i] === ctrl) { - zOrder.splice(i, 1); - } + if (node.parent) { + node.remove(); } - } - - if (zOrder.length) { - for (i = 0; i < zOrder.length; i++) { - if (zOrder[i].modal) { - zIndex++; - topModal = zOrder[i]; - } - zOrder[i].getEl().style.zIndex = zIndex; - zOrder[i].zIndex = zIndex; - zIndex++; + last = self.lastChild; + if (last) { + last.next = node; + node.prev = last; + self.lastChild = node; + } else { + self.lastChild = self.firstChild = node; } - } - - var modalBlockEl = $('#' + ctrl.classPrefix + 'modal-block', ctrl.getContainerElm())[0]; - - if (topModal) { - $(modalBlockEl).css('z-index', topModal.zIndex - 1); - } else if (modalBlockEl) { - modalBlockEl.parentNode.removeChild(modalBlockEl); - hasModal = false; - } - FloatPanel.currentZIndex = zIndex; - } + node.parent = self; - var FloatPanel = Panel.extend({ - Mixins: [Movable, Resizable], + return node; + }, /** - * Constructs a new control instance with the specified settings. + * Inserts a node at a specific position as a child of the current node. * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} autohide Automatically hide the panel. + * @example + * parentNode.insert(newChildNode, oldChildNode); + * + * @method insert + * @param {tinymce.html.Node} node Node to insert as a child of the current node. + * @param {tinymce.html.Node} refNode Reference node to set node before/after. + * @param {Boolean} before Optional state to insert the node before the reference node. + * @return {tinymce.html.Node} The node that got inserted. */ - init: function (settings) { - var self = this; + insert: function (node, refNode, before) { + var parent; - self._super(settings); - self._eventsRoot = self; + if (node.parent) { + node.remove(); + } - self.classes.add('floatpanel'); + parent = refNode.parent || this; - // Hide floatpanes on click out side the root button - if (settings.autohide) { - bindDocumentClickHandler(); - bindWindowResizeHandler(); - visiblePanels.push(self); - } + if (before) { + if (refNode === parent.firstChild) { + parent.firstChild = node; + } else { + refNode.prev.next = node; + } - if (settings.autofix) { - bindDocumentScrollHandler(); + node.prev = refNode.prev; + node.next = refNode; + refNode.prev = node; + } else { + if (refNode === parent.lastChild) { + parent.lastChild = node; + } else { + refNode.next.prev = node; + } - self.on('move', function () { - repositionPanel(this); - }); + node.next = refNode.next; + node.prev = refNode; + refNode.next = node; } - self.on('postrender show', function (e) { - if (e.control == self) { - var $modalBlockEl, prefix = self.classPrefix; - - if (self.modal && !hasModal) { - $modalBlockEl = $('#' + prefix + 'modal-block', self.getContainerElm()); - if (!$modalBlockEl[0]) { - $modalBlockEl = $( - '
    ' - ).appendTo(self.getContainerElm()); - } + node.parent = parent; - Delay.setTimeout(function () { - $modalBlockEl.addClass(prefix + 'in'); - $(self.getEl()).addClass(prefix + 'in'); - }); + return node; + }, - hasModal = true; - } + /** + * Get all children by name. + * + * @method getAll + * @param {String} name Name of the child nodes to collect. + * @return {Array} Array with child nodes matchin the specified name. + */ + getAll: function (name) { + var self = this, node, collection = []; - addRemove(true, self); + for (node = self.firstChild; node; node = walk(node, self)) { + if (node.name === name) { + collection.push(node); } - }); - - self.on('show', function () { - self.parents().each(function (ctrl) { - if (ctrl.state.get('fixed')) { - self.fixed(true); - return false; - } - }); - }); - - if (settings.popover) { - self._preBodyHtml = '
    '; - self.classes.add('popover').add('bottom').add(self.isRtl() ? 'end' : 'start'); } - self.aria('label', settings.ariaLabel); - self.aria('labelledby', self._id); - self.aria('describedby', self.describedBy || self._id + '-none'); + return collection; }, - fixed: function (state) { - var self = this; + /** + * Removes all children of the current node. + * + * @method empty + * @return {tinymce.html.Node} The current node that got cleared. + */ + empty: function () { + var self = this, nodes, i, node; - if (self.state.get('fixed') != state) { - if (self.state.get('rendered')) { - var viewport = DomUtils.getViewPort(); + // Remove all children + if (self.firstChild) { + nodes = []; - if (state) { - self.layoutRect().y -= viewport.y; - } else { - self.layoutRect().y += viewport.y; - } + // Collect the children + for (node = self.firstChild; node; node = walk(node, self)) { + nodes.push(node); } - self.classes.toggle('fixed', state); - self.state.set('fixed', state); + // Remove the children + i = nodes.length; + while (i--) { + node = nodes[i]; + node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; + } } + self.firstChild = self.lastChild = null; + return self; }, /** - * Shows the current float panel. + * Returns true/false if the node is to be considered empty or not. * - * @method show - * @return {tinymce.ui.FloatPanel} Current floatpanel instance. + * @example + * node.isEmpty({img: true}); + * @method isEmpty + * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. + * @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables. + * @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node. + * @return {Boolean} true/false if the node is empty or not. */ - show: function () { - var self = this, i, state = self._super(); + isEmpty: function (elements, whitespace, predicate) { + var self = this, node = self.firstChild, i, name; - i = visiblePanels.length; - while (i--) { - if (visiblePanels[i] === self) { - break; - } - } + whitespace = whitespace || {}; - if (i === -1) { - visiblePanels.push(self); - } + if (node) { + do { + if (node.type === 1) { + // Ignore bogus elements + if (node.attributes.map['data-mce-bogus']) { + continue; + } - return state; - }, + // Keep empty elements like + if (elements[node.name]) { + return false; + } - /** - * Hides the current float panel. - * - * @method hide - * @return {tinymce.ui.FloatPanel} Current floatpanel instance. - */ - hide: function () { - removeVisiblePanel(this); - addRemove(false, this); + // 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; + } + } + } - return this._super(); - }, + // Keep comments + if (node.type === 8) { + return false; + } - /** - * Hide all visible float panels with he autohide setting enabled. This is for - * manually hiding floating menus or panels. - * - * @method hideAll - */ - hideAll: function () { - FloatPanel.hideAll(); - }, + // Keep non whitespace text nodes + if (node.type === 3 && !whiteSpaceRegExp.test(node.value)) { + return false; + } - /** - * Closes the float panel. This will remove the float panel from page and fire the close event. - * - * @method close - */ - close: function () { - var self = this; + // Keep whitespace preserve elements + if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) { + return false; + } - if (!self.fire('close').isDefaultPrevented()) { - self.remove(); - addRemove(false, self); + // Predicate tells that the node is contents + if (predicate && predicate(node)) { + return false; + } + } while ((node = walk(node, self))); } - return self; + return true; }, /** - * Removes the float panel from page. + * Walks to the next or previous node and returns that node or null if it wasn't found. * - * @method remove + * @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. */ - remove: function () { - removeVisiblePanel(this); - this._super(); - }, - - postRender: function () { - var self = this; - - if (self.settings.bodyRole) { - this.getEl('body').setAttribute('role', self.settings.bodyRole); - } - - return self._super(); + walk: function (prev) { + return walk(this, null, prev); } - }); + }; /** - * Hide all visible float panels with he autohide setting enabled. This is for - * manually hiding floating menus or panels. + * Creates a node of a specific type. * * @static - * @method hideAll + * @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. */ - FloatPanel.hideAll = function () { - var i = visiblePanels.length; - - while (i--) { - var panel = visiblePanels[i]; - - if (panel && panel.settings.autohide) { - panel.hide(); - visiblePanels.splice(i, 1); - } - } - }; + Node.create = function (name, attrs) { + var node, attrName; - function removeVisiblePanel(panel) { - var i; + // Create node + node = new Node(name, typeLookup[name] || 1); - i = visiblePanels.length; - while (i--) { - if (visiblePanels[i] === panel) { - visiblePanels.splice(i, 1); + // Add attributes if needed + if (attrs) { + for (attrName in attrs) { + node.attr(attrName, attrs[attrName]); } } - i = zOrder.length; - while (i--) { - if (zOrder[i] === panel) { - zOrder.splice(i, 1); - } - } - } + return node; + }; - return FloatPanel; + return Node; } ); - /** - * Window.js + * SaxParser.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -27448,483 +25446,498 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/*eslint max-depth:[2, 9] */ + /** - * Creates a new window. + * 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); + * }, * - * @-x-less Window.less - * @class tinymce.ui.Window - * @extends tinymce.ui.FloatPanel + * doctype: function(text) { + * console.log('DocType:', text); + * } + * }, schema); + * @class tinymce.html.SaxParser + * @version 3.4 */ define( - 'tinymce.core.ui.Window', + 'tinymce.core.html.SaxParser', [ - "tinymce.core.ui.FloatPanel", - "tinymce.core.ui.Panel", - "tinymce.core.ui.DomUtils", - "tinymce.core.dom.DomQuery", - "tinymce.core.ui.DragHelper", - "tinymce.core.ui.BoxUtils", - "tinymce.core.Env", - "tinymce.core.util.Delay" + "tinymce.core.html.Schema", + "tinymce.core.html.Entities", + "tinymce.core.util.Tools" ], - function (FloatPanel, Panel, DomUtils, $, DragHelper, BoxUtils, Env, Delay) { - "use strict"; - - var windows = [], oldMetaValue = ''; - - function toggleFullScreenState(state) { - var noScaleMetaValue = 'width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0', - viewport = $("meta[name=viewport]")[0], - contentValue; - - if (Env.overrideViewPort === false) { - return; - } - - if (!viewport) { - viewport = document.createElement('meta'); - viewport.setAttribute('name', 'viewport'); - document.getElementsByTagName('head')[0].appendChild(viewport); - } - - contentValue = viewport.getAttribute('content'); - if (contentValue && typeof oldMetaValue != 'undefined') { - oldMetaValue = contentValue; - } + function (Schema, Entities, Tools) { + var each = Tools.each; - viewport.setAttribute('content', state ? noScaleMetaValue : oldMetaValue); - } + var isValidPrefixAttrName = function (name) { + return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0; + }; - function toggleBodyFullScreenClasses(classPrefix, state) { - if (checkFullscreenWindows() && state === false) { - $([document.documentElement, document.body]).removeClass(classPrefix + 'fullscreen'); - } - } + var trimComments = function (text) { + return text.replace(//g, ''); + }; - function checkFullscreenWindows() { - for (var i = 0; i < windows.length; i++) { - if (windows[i]._fullscreen) { - return true; - } - } - return false; - } - - function handleWindowResize() { - if (!Env.desktop) { - var lastSize = { - w: window.innerWidth, - h: window.innerHeight - }; + /** + * 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; - Delay.setInterval(function () { - var w = window.innerWidth, - h = window.innerHeight; + shortEndedElements = schema.getShortEndedElements(); + tokenRegExp = /<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g; + tokenRegExp.lastIndex = index = startIndex; - if (lastSize.w != w || lastSize.h != h) { - lastSize = { - w: w, - h: h - }; + while ((matches = tokenRegExp.exec(html))) { + index = tokenRegExp.lastIndex; - $(window).trigger('resize'); + if (matches[1] === '/') { // End element + count--; + } else if (!matches[1]) { // Start element + if (matches[2] in shortEndedElements) { + continue; } - }, 100); - } - function reposition() { - var i, rect = DomUtils.getWindowSize(), layoutRect; - - for (i = 0; i < windows.length; i++) { - layoutRect = windows[i].layoutRect(); + count++; + } - windows[i].moveTo( - windows[i].settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2), - windows[i].settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2) - ); + if (count === 0) { + break; } } - $(window).on('resize', reposition); + return index; } - var Window = FloatPanel.extend({ - modal: true, - - Defaults: { - border: 1, - layout: 'flex', - containerCls: 'panel', - role: 'dialog', - callbacks: { - submit: function () { - this.fire('submit', { data: this.toJSON() }); - }, + /** + * 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; - close: function () { - this.close(); - } + 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; } - }, + }); /** - * Constructs a instance with the specified settings. + * Parses the specified HTML string and executes the callbacks for each item it finds. * - * @constructor - * @param {Object} settings Name/value object with settings. + * @example + * new SaxParser({...}).parse('text'); + * @method parse + * @param {String} html Html string to sax parse. */ - init: function (settings) { - var self = this; + 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; - self._super(settings); + function processEndTag(name) { + var pos, i; - if (self.isRtl()) { - self.classes.add('rtl'); - } + // Find position of parent of the same type + pos = stack.length; + while (pos--) { + if (stack[pos].name === name) { + break; + } + } - self.classes.add('window'); - self.bodyClasses.add('window-body'); - self.state.set('fixed', true); + // Found parent + if (pos >= 0) { + // Close all the open elements + for (i = stack.length - 1; i >= pos; i--) { + name = stack[i]; - // Create statusbar - if (settings.buttons) { - self.statusbar = new Panel({ - layout: 'flex', - border: '1 0 0 0', - spacing: 3, - padding: 10, - align: 'center', - pack: self.isRtl() ? 'start' : 'end', - defaults: { - type: 'button' - }, - items: settings.buttons - }); + if (name.valid) { + self.end(name.name); + } + } - self.statusbar.classes.add('foot'); - self.statusbar.parent(self); + // Remove the open elements from the stack + stack.length = pos; + } } - self.on('click', function (e) { - var closeClass = self.classPrefix + 'close'; + function parseAttribute(match, name, value, val2, val3) { + var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g; - if (DomUtils.hasClass(e.target, closeClass) || DomUtils.hasClass(e.target.parentNode, closeClass)) { - self.close(); - } - }); + name = name.toLowerCase(); + value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute - self.on('cancel', function () { - self.close(); - }); + // Validate name and value pass through all data- attributes + if (validate && !isInternalElement && isValidPrefixAttrName(name) === false) { + attrRule = validAttributesMap[name]; - self.aria('describedby', self.describedBy || self._id + '-none'); - self.aria('label', settings.title); - self._fullscreen = false; - }, + // Find rule by pattern matching + if (!attrRule && validAttributePatterns) { + i = validAttributePatterns.length; + while (i--) { + attrRule = validAttributePatterns[i]; + if (attrRule.pattern.test(name)) { + break; + } + } - /** - * Recalculates the positions of the controls in the current container. - * This is invoked by the reflow method and shouldn't be called directly. - * - * @method recalc - */ - recalc: function () { - var self = this, statusbar = self.statusbar, layoutRect, width, x, needsRecalc; + // No rule matched + if (i === -1) { + attrRule = null; + } + } - if (self._fullscreen) { - self.layoutRect(DomUtils.getWindowSize()); - self.layoutRect().contentH = self.layoutRect().innerH; - } + // No attribute rule found + if (!attrRule) { + return; + } + + // Validate value + if (attrRule.validValues && !(value in attrRule.validValues)) { + return; + } + } - self._super(); + // Block any javascript: urls or non image data uris + if (filteredUrlAttrs[name] && !settings.allow_script_urls) { + var uri = value.replace(trimRegExp, ''); - layoutRect = self.layoutRect(); + try { + // Might throw malformed URI sequence + uri = decodeURIComponent(uri); + } catch (ex) { + // Fallback to non UTF-8 decoder + uri = unescape(uri); + } - // Resize window based on title width - if (self.settings.title && !self._fullscreen) { - width = layoutRect.headerW; - if (width > layoutRect.w) { - x = layoutRect.x - Math.max(0, width / 2); - self.layoutRect({ w: width, x: x }); - needsRecalc = true; - } - } + if (scriptUriRegExp.test(uri)) { + return; + } - // Resize window based on statusbar width - if (statusbar) { - statusbar.layoutRect({ w: self.layoutRect().innerW }).recalc(); + if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) { + return; + } + } - width = statusbar.layoutRect().minW + layoutRect.deltaW; - if (width > layoutRect.w) { - x = layoutRect.x - Math.max(0, width - layoutRect.w); - self.layoutRect({ w: width, x: x }); - needsRecalc = true; + // Block data or event attributes on elements marked as internal + if (isInternalElement && (name in filteredUrlAttrs || name.indexOf('on') === 0)) { + return; } - } - // Recalc body and disable auto resize - if (needsRecalc) { - self.recalc(); + // Add attribute to list and map + attrList.map[name] = value; + attrList.push({ + name: name, + value: value + }); } - }, - /** - * Initializes the current controls layout rect. - * This will be executed by the layout managers to determine the - * default minWidth/minHeight etc. - * - * @method initLayoutRect - * @return {Object} Layout rect instance. - */ - initLayoutRect: function () { - var self = this, layoutRect = self._super(), deltaH = 0, headEl; + // 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'); - // Reserve vertical space for title - if (self.settings.title && !self._fullscreen) { - headEl = self.getEl('head'); + attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; - var size = DomUtils.getSize(headEl); + // 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 + '>'; - layoutRect.headerW = size.width; - layoutRect.headerH = size.height; + 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))); + } - deltaH += layoutRect.headerH; - } + if ((value = matches[6])) { // End element + value = value.toLowerCase(); - // Reserve vertical space for statusbar - if (self.statusbar) { - deltaH += self.statusbar.layoutRect().h; - } + // 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); + } - layoutRect.deltaH += deltaH; - layoutRect.minH += deltaH; - //layoutRect.innerH -= deltaH; - layoutRect.h += deltaH; + 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; + } - var rect = DomUtils.getWindowSize(); + value = value.toLowerCase(); - layoutRect.x = self.settings.x || Math.max(0, rect.w / 2 - layoutRect.w / 2); - layoutRect.y = self.settings.y || Math.max(0, rect.h / 2 - layoutRect.h / 2); + // 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); + } - return layoutRect; - }, + isShortEnded = value in shortEndedElements; - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, layout = self._layout, id = self._id, prefix = self.classPrefix; - var settings = self.settings, headerHtml = '', footerHtml = '', html = settings.html; + // Is self closing tag for example an
  • after an open
  • + if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) { + processEndTag(value); + } - self.preRender(); - layout.preRender(self); + // Validate element + if (!validate || (elementRule = schema.getElementRule(value))) { + isValidElement = true; - if (settings.title) { - headerHtml = ( - '
    ' + - '
    ' + self.encode(settings.title) + '
    ' + - '
    ' + - '' + - '
    ' - ); - } + // Grab attributes map and patters when validation is enabled + if (validate) { + validAttributesMap = elementRule.attributes; + validAttributePatterns = elementRule.attributePatterns; + } - if (settings.url) { - html = ''; - } + // Parse attributes + if ((attribsValue = matches[8])) { + isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element - if (typeof html == "undefined") { - html = layout.renderHtml(self); - } + // If the element has internal attributes then remove it if we are told to do so + if (isInternalElement && removeInternalElements) { + isValidElement = false; + } - if (self.statusbar) { - footerHtml = self.statusbar.renderHtml(); - } + attrList = []; + attrList.map = {}; - return ( - '
    ' + - '
    ' + - headerHtml + - '
    ' + - html + - '
    ' + - footerHtml + - '
    ' + - '
    ' - ); - }, + attribsValue.replace(attrRegExp, parseAttribute); + } else { + attrList = []; + attrList.map = {}; + } - /** - * Switches the window fullscreen mode. - * - * @method fullscreen - * @param {Boolean} state True/false state. - * @return {tinymce.ui.Window} Current window instance. - */ - fullscreen: function (state) { - var self = this, documentElement = document.documentElement, slowRendering, prefix = self.classPrefix, layoutRect; + // Process attributes if validation is enabled + if (validate && !isInternalElement) { + attributesRequired = elementRule.attributesRequired; + attributesDefault = elementRule.attributesDefault; + attributesForced = elementRule.attributesForced; + anyAttributesRequired = elementRule.removeEmptyAttrs; - if (state != self._fullscreen) { - $(window).on('resize', function () { - var time; + // Check if any attribute exists + if (anyAttributesRequired && !attrList.length) { + isValidElement = false; + } - if (self._fullscreen) { - // Time the layout time if it's to slow use a timeout to not hog the CPU - if (!slowRendering) { - time = new Date().getTime(); + // Handle forced attributes + if (attributesForced) { + i = attributesForced.length; + while (i--) { + attr = attributesForced[i]; + name = attr.name; + attrValue = attr.value; - var rect = DomUtils.getWindowSize(); - self.moveTo(0, 0).resizeTo(rect.w, rect.h); + if (attrValue === '{$uid}') { + attrValue = 'mce_' + idCount++; + } - if ((new Date().getTime()) - time > 50) { - slowRendering = true; + attrList.map[name] = attrValue; + attrList.push({ name: name, value: attrValue }); + } } - } else { - if (!self._timer) { - self._timer = Delay.setTimeout(function () { - var rect = DomUtils.getWindowSize(); - self.moveTo(0, 0).resizeTo(rect.w, rect.h); - self._timer = 0; - }, 50); - } - } - } - }); + // Handle default attributes + if (attributesDefault) { + i = attributesDefault.length; + while (i--) { + attr = attributesDefault[i]; + name = attr.name; - layoutRect = self.layoutRect(); - self._fullscreen = state; + if (!(name in attrList.map)) { + attrValue = attr.value; - if (!state) { - self.borderBox = BoxUtils.parseBox(self.settings.border); - self.getEl('head').style.display = ''; - layoutRect.deltaH += layoutRect.headerH; - $([documentElement, document.body]).removeClass(prefix + 'fullscreen'); - self.classes.remove('fullscreen'); - self.moveTo(self._initial.x, self._initial.y).resizeTo(self._initial.w, self._initial.h); - } else { - self._initial = { x: layoutRect.x, y: layoutRect.y, w: layoutRect.w, h: layoutRect.h }; + if (attrValue === '{$uid}') { + attrValue = 'mce_' + idCount++; + } - self.borderBox = BoxUtils.parseBox('0'); - self.getEl('head').style.display = 'none'; - layoutRect.deltaH -= layoutRect.headerH + 2; - $([documentElement, document.body]).addClass(prefix + 'fullscreen'); - self.classes.add('fullscreen'); + attrList.map[name] = attrValue; + attrList.push({ name: name, value: attrValue }); + } + } + } - var rect = DomUtils.getWindowSize(); - self.moveTo(0, 0).resizeTo(rect.w, rect.h); - } - } + // Handle required attributes + if (attributesRequired) { + i = attributesRequired.length; + while (i--) { + if (attributesRequired[i] in attrList.map) { + break; + } + } - return self.reflow(); - }, + // None of the required attributes where found + if (i === -1) { + isValidElement = false; + } + } - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this, startPos; + // Invalidate element if it's marked as bogus + if ((attr = attrList.map['data-mce-bogus'])) { + if (attr === 'all') { + index = findEndTag(schema, html, tokenRegExp.lastIndex); + tokenRegExp.lastIndex = index; + continue; + } - setTimeout(function () { - self.classes.add('in'); - self.fire('open'); - }, 0); + isValidElement = false; + } + } - self._super(); + if (isValidElement) { + self.start(value, attrList, isShortEnded); + } + } else { + isValidElement = false; + } - if (self.statusbar) { - self.statusbar.postRender(); - } + // Treat script, noscript and style a bit different since they may include code that looks like elements + if ((endRegExp = specialElements[value])) { + endRegExp.lastIndex = index = matches.index + matches[0].length; - self.focus(); + if ((matches = endRegExp.exec(html))) { + if (isValidElement) { + text = html.substr(index, matches.index - index); + } - this.dragHelper = new DragHelper(self._id + '-dragh', { - start: function () { - startPos = { - x: self.layoutRect().x, - y: self.layoutRect().y - }; - }, + index = matches.index + matches[0].length; + } else { + text = html.substr(index); + index = html.length; + } - drag: function (e) { - self.moveTo(startPos.x + e.deltaX, startPos.y + e.deltaY); - } - }); + if (isValidElement) { + if (text.length > 0) { + self.text(text, true); + } - self.on('submit', function (e) { - if (!e.isDefaultPrevented()) { - self.close(); - } - }); + self.end(value); + } - windows.push(self); - toggleFullScreenState(true); - }, + tokenRegExp.lastIndex = index; + continue; + } - /** - * Fires a submit event with the serialized form. - * - * @method submit - * @return {Object} Event arguments object. - */ - submit: function () { - return this.fire('submit', { data: this.toJSON() }); - }, + // Push value on to stack + if (!isShortEnded) { + if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) { + stack.push({ name: value, valid: isValidElement }); + } else if (isValidElement) { + self.end(value); + } + } + } else if ((value = matches[1])) { // Comment + // Padd comment value to avoid browsers from parsing invalid comments as HTML + if (value.charAt(0) === '>') { + value = ' ' + value; + } - /** - * Removes the current control from DOM and from UI collections. - * - * @method remove - * @return {tinymce.ui.Control} Current control instance. - */ - remove: function () { - var self = this, i; + if (!settings.allow_conditional_comments && value.substr(0, 3).toLowerCase() === '[if') { + value = ' ' + value; + } - self.dragHelper.destroy(); - self._super(); + self.comment(value); + } else if ((value = matches[2])) { // CDATA + self.cdata(trimComments(value)); + } else if ((value = matches[3])) { // DOCTYPE + self.doctype(value); + } else if ((value = matches[4])) { // PI + self.pi(value, matches[5]); + } - if (self.statusbar) { - this.statusbar.remove(); + index = matches.index + matches[0].length; } - toggleBodyFullScreenClasses(self.classPrefix, false); - - i = windows.length; - while (i--) { - if (windows[i] === self) { - windows.splice(i, 1); - } + // Text + if (index < html.length) { + self.text(decode(html.substr(index))); } - toggleFullScreenState(windows.length > 0); - }, + // Close any open elements + for (i = stack.length - 1; i >= 0; i--) { + value = stack[i]; - /** - * Returns the contentWindow object of the iframe if it exists. - * - * @method getContentWindow - * @return {Window} window object or null. - */ - getContentWindow: function () { - var ifr = this.getEl().getElementsByTagName('iframe')[0]; - return ifr ? ifr.contentWindow : null; - } - }); + if (value.valid) { + self.end(value.name); + } + } + }; + } - handleWindowResize(); + SaxParser.findEndTag = findEndTag; - return Window; + return SaxParser; } ); /** - * MessageBox.js + * DomParser.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -27934,1333 +25947,1102 @@ define( */ /** - * This class is used to create MessageBoxes like alerts/confirms etc. + * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make + * sure that the node tree is valid according to the specified schema. + * So for example:

    a

    b

    c

    will become

    a

    b

    c

    + * + * @example + * var parser = new tinymce.html.DomParser({validate: true}, schema); + * var rootNode = parser.parse('

    content

    '); * - * @class tinymce.ui.MessageBox - * @extends tinymce.ui.FloatPanel + * @class tinymce.html.DomParser + * @version 3.4 */ define( - 'tinymce.core.ui.MessageBox', + 'tinymce.core.html.DomParser', [ - "tinymce.core.ui.Window" + "tinymce.core.html.Node", + "tinymce.core.html.Schema", + "tinymce.core.html.SaxParser", + "tinymce.core.util.Tools" ], - function (Window) { - "use strict"; + function (Node, Schema, SaxParser, Tools) { + var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend; - var MessageBox = Window.extend({ - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - */ - init: function (settings) { - settings = { - border: 1, - padding: 20, - layout: 'flex', - pack: "center", - align: "center", - containerCls: 'panel', - autoScroll: true, - buttons: { type: "button", text: "Ok", action: "ok" }, - items: { - type: "label", - multiline: true, - maxWidth: 500, - maxHeight: 200 - } - }; + var paddEmptyNode = function (settings, node) { + if (settings.padd_empty_with_br) { + node.empty().append(new Node('br', '1')).shortEnded = true; + } else { + node.empty().append(new Node('#text', '3')).value = '\u00a0'; + } + }; - this._super(settings); - }, + var hasOnlyChild = function (node, name) { + return node && node.firstChild === node.lastChild && node.firstChild.name === name; + }; - Statics: { - /** - * Ok buttons constant. - * - * @static - * @final - * @field {Number} OK - */ - OK: 1, + var isPadded = function (schema, node) { + var rule = schema.getElementRule(node.name); + return rule && rule.paddEmpty; + }; - /** - * Ok/cancel buttons constant. - * - * @static - * @final - * @field {Number} OK_CANCEL - */ - OK_CANCEL: 2, + var isEmpty = function (schema, nonEmptyElements, whitespaceElements, node) { + return node.isEmpty(nonEmptyElements, whitespaceElements, function (node) { + return isPadded(schema, node); + }); + }; - /** - * yes/no buttons constant. - * - * @static - * @final - * @field {Number} YES_NO - */ - YES_NO: 3, + /** + * Constructs a new DomParser instance. + * + * @constructor + * @method DomParser + * @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. + */ + return function (settings, schema) { + var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; - /** - * yes/no/cancel buttons constant. - * - * @static - * @final - * @field {Number} YES_NO_CANCEL - */ - YES_NO_CANCEL: 4, + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; + settings.root_name = settings.root_name || 'body'; + self.schema = schema = schema || new Schema(); - /** - * Constructs a new message box and renders it to the body element. - * - * @static - * @method msgBox - * @param {Object} settings Name/value object with settings. - */ - msgBox: function (settings) { - var buttons, callback = settings.callback || function () { }; + function fixInvalidChildren(nodes) { + var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i; + var nonEmptyElements, whitespaceElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode; - function createButton(text, status, primary) { - return { - type: "button", - text: text, - subtype: primary ? 'primary' : '', - onClick: function (e) { - e.control.parents()[1].close(); - callback(status); - } - }; - } + nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table'); + nonEmptyElements = schema.getNonEmptyElements(); + whitespaceElements = schema.getWhiteSpaceElements(); + textBlockElements = schema.getTextBlockElements(); + specialElements = schema.getSpecialElements(); - switch (settings.buttons) { - case MessageBox.OK_CANCEL: - buttons = [ - createButton('Ok', true, true), - createButton('Cancel', false) - ]; - break; + for (ni = 0; ni < nodes.length; ni++) { + node = nodes[ni]; - case MessageBox.YES_NO: - case MessageBox.YES_NO_CANCEL: - buttons = [ - createButton('Yes', 1, true), - createButton('No', 0) - ]; + // Already removed or fixed + if (!node.parent || node.fixed) { + continue; + } - if (settings.buttons == MessageBox.YES_NO_CANCEL) { - buttons.push(createButton('Cancel', -1)); + // If the invalid element is a text block and the text block is within a parent LI element + // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office + if (textBlockElements[node.name] && node.parent.name == 'li') { + // Move sibling text blocks after LI element + sibling = node.next; + while (sibling) { + if (textBlockElements[sibling.name]) { + sibling.name = 'li'; + sibling.fixed = true; + node.parent.insert(sibling, node.parent); + } else { + break; } - break; - - default: - buttons = [ - createButton('Ok', true, true) - ]; - break; - } - return new Window({ - padding: 20, - x: settings.x, - y: settings.y, - minWidth: 300, - minHeight: 100, - layout: "flex", - pack: "center", - align: "center", - buttons: buttons, - title: settings.title, - role: 'alertdialog', - items: { - type: "label", - multiline: true, - maxWidth: 500, - maxHeight: 200, - text: settings.text - }, - onPostRender: function () { - this.aria('describedby', this.items()[0]._id); - }, - onClose: settings.onClose, - onCancel: function () { - callback(false); + sibling = sibling.next; } - }).renderTo(document.body).reflow(); - }, - /** - * Creates a new alert dialog. - * - * @method alert - * @param {Object} settings Settings for the alert dialog. - * @param {function} [callback] Callback to execute when the user makes a choice. - */ - alert: function (settings, callback) { - if (typeof settings == "string") { - settings = { text: settings }; + // Unwrap current text block + node.unwrap(node); + continue; } - settings.callback = callback; - return MessageBox.msgBox(settings); - }, - - /** - * Creates a new confirm dialog. - * - * @method confirm - * @param {Object} settings Settings for the confirm dialog. - * @param {function} [callback] Callback to execute when the user makes a choice. - */ - confirm: function (settings, callback) { - if (typeof settings == "string") { - settings = { text: settings }; + // Get list of all parent nodes until we find a valid parent to stick the child into + parents = [node]; + for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && + !nonSplitableElements[parent.name]; parent = parent.parent) { + parents.push(parent); } - settings.callback = callback; - settings.buttons = MessageBox.OK_CANCEL; + // Found a suitable parent + if (parent && parents.length > 1) { + // Reverse the array since it makes looping easier + parents.reverse(); - return MessageBox.msgBox(settings); - } - } - }); + // Clone the related parent and insert that after the moved node + newParent = currentNode = self.filterNode(parents[0].clone()); - return MessageBox; - } -); + // Start cloning and moving children on the left side of the target node + for (i = 0; i < parents.length - 1; i++) { + if (schema.isValidChild(currentNode.name, parents[i].name)) { + tempNode = self.filterNode(parents[i].clone()); + currentNode.append(tempNode); + } else { + tempNode = currentNode; + } -/** - * WindowManagerImpl.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 - */ + for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) { + nextNode = childNode.next; + tempNode.append(childNode); + childNode = nextNode; + } -define( - 'tinymce.core.ui.WindowManagerImpl', - [ - "tinymce.core.ui.Window", - "tinymce.core.ui.MessageBox" - ], - function (Window, MessageBox) { - return function (editor) { - var open = function (args, params, closeCallback) { - var win; - - args.title = args.title || ' '; - - // Handle URL - args.url = args.url || args.file; // Legacy - if (args.url) { - args.width = parseInt(args.width || 320, 10); - args.height = parseInt(args.height || 240, 10); - } - - // Handle body - if (args.body) { - args.items = { - defaults: args.defaults, - type: args.bodyType || 'form', - items: args.body, - data: args.data, - callbacks: args.commands - }; - } + currentNode = tempNode; + } - if (!args.url && !args.buttons) { - args.buttons = [ - { - text: 'Ok', subtype: 'primary', onclick: function () { - win.find('form')[0].submit(); + if (!isEmpty(schema, nonEmptyElements, whitespaceElements, newParent)) { + parent.insert(newParent, parents[0], true); + parent.insert(node, newParent); + } else { + parent.insert(node, parents[0], true); + } + + // Check if the element is empty by looking through it's contents and special treatment for


    + parent = parents[0]; + if (isEmpty(schema, nonEmptyElements, whitespaceElements, parent) || hasOnlyChild(parent, 'br')) { + parent.empty().remove(); + } + } else if (node.parent) { + // If it's an LI try to find a UL/OL for it or wrap it + if (node.name === 'li') { + sibling = node.prev; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.append(node); + continue; } - }, - { - text: 'Cancel', onclick: function () { - win.close(); + sibling = node.next; + if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { + sibling.insert(node, sibling.firstChild, true); + continue; } + + node.wrap(self.filterNode(new Node('ul', 1))); + continue; } - ]; - } - win = new Window(args); + // Try wrapping the element in a DIV + if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { + node.wrap(self.filterNode(new Node('div', 1))); + } else { + // We failed wrapping it, then remove or unwrap it + if (specialElements[node.name]) { + node.empty().remove(); + } else { + node.unwrap(); + } + } + } + } + } - win.on('close', function () { - closeCallback(win); - }); + /** + * Runs the specified node though the element and attributes filters. + * + * @method filterNode + * @param {tinymce.html.Node} Node the node to run filters on. + * @return {tinymce.html.Node} The passed in node. + */ + self.filterNode = function (node) { + var i, name, list; - // Handle data - if (args.data) { - win.on('postRender', function () { - this.find('*').each(function (ctrl) { - var name = ctrl.name(); + // Run element filters + if (name in nodeFilters) { + list = matchedNodes[name]; - if (name in args.data) { - ctrl.value(args.data[name]); - } - }); - }); + if (list) { + list.push(node); + } else { + matchedNodes[name] = [node]; + } } - // store args and parameters - win.features = args || {}; - win.params = params || {}; + // Run attribute filters + i = attributeFilters.length; + while (i--) { + name = attributeFilters[i].name; + + if (name in node.attributes.map) { + list = matchedAttributes[name]; - win = win.renderTo().reflow(); + if (list) { + list.push(node); + } else { + matchedAttributes[name] = [node]; + } + } + } - return win; + return node; }; - var alert = function (message, choiceCallback, closeCallback) { - var win; + /** + * Adds a node filter function to the parser, the parser will collect the specified nodes by name + * and then execute the callback ones it has finished parsing the document. + * + * @example + * parser.addNodeFilter('p,h1', function(nodes, name) { + * for (var i = 0; i < nodes.length; i++) { + * console.log(nodes[i].name); + * } + * }); + * @method addNodeFilter + * @method {String} name Comma separated list of nodes to collect. + * @param {function} callback Callback function to execute once it has collected nodes. + */ + self.addNodeFilter = function (name, callback) { + each(explode(name), function (name) { + var list = nodeFilters[name]; - win = MessageBox.alert(message, function () { - choiceCallback(); - }); + if (!list) { + nodeFilters[name] = list = []; + } - win.on('close', function () { - closeCallback(win); + list.push(callback); }); - - return win; }; - var confirm = function (message, choiceCallback, closeCallback) { - var win; + /** + * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes + * and then execute the callback ones it has finished parsing the document. + * + * @example + * parser.addAttributeFilter('src,href', function(nodes, name) { + * for (var i = 0; i < nodes.length; i++) { + * console.log(nodes[i].name); + * } + * }); + * @method addAttributeFilter + * @method {String} name Comma separated list of nodes to collect. + * @param {function} callback Callback function to execute once it has collected nodes. + */ + self.addAttributeFilter = function (name, callback) { + each(explode(name), function (name) { + var i; - win = MessageBox.confirm(message, function (state) { - choiceCallback(state); - }); + for (i = 0; i < attributeFilters.length; i++) { + if (attributeFilters[i].name === name) { + attributeFilters[i].callbacks.push(callback); + return; + } + } - win.on('close', function () { - closeCallback(win); + attributeFilters.push({ name: name, callbacks: [callback] }); }); - - return win; }; - var close = function (window) { - window.close(); - }; + /** + * Parses the specified HTML string into a DOM like node tree and returns the result. + * + * @example + * var rootNode = new DomParser({...}).parse('text'); + * @method parse + * @param {String} html Html string to sax parse. + * @param {Object} args Optional args object that gets passed to all filter functions. + * @return {tinymce.html.Node} Root node containing the tree. + */ + self.parse = function (html, args) { + var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate; + var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement; + var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements; + var children, nonEmptyElements, rootBlockName; - var getParams = function (window) { - return window.params; - }; + args = args || {}; + matchedNodes = {}; + matchedAttributes = {}; + blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); + nonEmptyElements = schema.getNonEmptyElements(); + children = schema.children; + validate = settings.validate; + rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; - var setParams = function (window, params) { - window.params = params; - }; + whiteSpaceElements = schema.getWhiteSpaceElements(); + startWhiteSpaceRegExp = /^[ \t\r\n]+/; + endWhiteSpaceRegExp = /[ \t\r\n]+$/; + allWhiteSpaceRegExp = /[ \t\r\n]+/g; + isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; - return { - open: open, - alert: alert, - confirm: confirm, - close: close, - getParams: getParams, - setParams: setParams - }; - }; - } -); + function addRootBlocks() { + var node = rootNode.firstChild, next, rootBlockNode; -/** - * WindowManager.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 - */ + // Removes whitespace at beginning and end of block so: + //

    x

    ->

    x

    + function trim(rootBlockNode) { + if (rootBlockNode) { + node = rootBlockNode.firstChild; + if (node && node.type == 3) { + node.value = node.value.replace(startWhiteSpaceRegExp, ''); + } -/** - * This class handles the creation of native windows and dialogs. This class can be extended to provide for example inline dialogs. - * - * @class tinymce.WindowManager - * @example - * // Opens a new dialog with the file.htm file and the size 320x240 - * // It also adds a custom parameter this can be retrieved by using tinyMCEPopup.getWindowArg inside the dialog. - * tinymce.activeEditor.windowManager.open({ - * url: 'file.htm', - * width: 320, - * height: 240 - * }, { - * custom_param: 1 - * }); - * - * // Displays an alert box using the active editors window manager instance - * tinymce.activeEditor.windowManager.alert('Hello world!'); - * - * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm - * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s, ) { - * if (s) - * tinymce.activeEditor.windowManager.alert("Ok"); - * else - * tinymce.activeEditor.windowManager.alert("Cancel"); - * }); - */ -define( - 'tinymce.core.api.WindowManager', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Option', - 'tinymce.core.ui.WindowManagerImpl' - ], - function (Arr, Option, WindowManagerImpl) { - return function (editor) { - var windows = []; + node = rootBlockNode.lastChild; + if (node && node.type == 3) { + node.value = node.value.replace(endWhiteSpaceRegExp, ''); + } + } + } - var getImplementation = function () { - var theme = editor.theme; - return theme.getWindowManagerImpl ? theme.getWindowManagerImpl() : WindowManagerImpl(editor); - }; + // 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; + } - var funcBind = function (scope, f) { - return function () { - return f ? f.apply(scope, arguments) : undefined; - }; - }; + while (node) { + next = node.next; - var fireOpenEvent = function (win) { - editor.fire('OpenWindow', { - win: win - }); - }; + 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; + } - var fireCloseEvent = function (win) { - editor.fire('CloseWindow', { - win: win - }); - }; + node = next; + } - var addWindow = function (win) { - windows.push(win); - fireOpenEvent(win); - }; + trim(rootBlockNode); + } - var closeWindow = function (win) { - Arr.findIndex(windows, function (otherWindow) { - return otherWindow === win; - }).each(function (index) { - // Mutate here since third party might have stored away the window array, consider breaking this api - windows.splice(index, 1); + function createNode(name, type) { + var node = new Node(name, type), list; - fireCloseEvent(win); + if (name in nodeFilters) { + list = matchedNodes[name]; - // Move focus back to editor when the last window is closed - if (windows.length === 0) { - editor.focus(); + if (list) { + list.push(node); + } else { + matchedNodes[name] = [node]; + } } - }); - }; - var getTopWindow = function () { - return Option.from(windows[0]); - }; + return node; + } - var open = function (args, params) { - editor.editorManager.setActive(editor); + function removeWhitespaceBefore(node) { + var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements(); - // Takes a snapshot in the FocusManager of the selection before focus is lost to dialog - if (windows.length === 0) { - editor.nodeChanged(); - } + for (textNode = node.prev; textNode && textNode.type === 3;) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); - var win = getImplementation().open(args, params, closeWindow); - addWindow(win); - return win; - }; + // Found a text node with non whitespace then trim that and break + if (textVal.length > 0) { + textNode.value = textVal; + return; + } - var alert = function (message, callback, scope) { - var win = getImplementation().alert(message, funcBind(scope ? scope : this, callback), closeWindow); - addWindow(win); - }; + textNodeNext = textNode.next; - var confirm = function (message, callback, scope) { - var win = getImplementation().confirm(message, funcBind(scope ? scope : this, callback), closeWindow); - addWindow(win); - }; + // 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; + } - var close = function () { - getTopWindow().each(function (win) { - getImplementation().close(win); - closeWindow(win); - }); - }; + if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') { + textNode = textNode.prev; + continue; + } + } - var getParams = function () { - return getTopWindow().map(getImplementation().getParams).getOr(null); - }; + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + } - var setParams = function (params) { - getTopWindow().each(function (win) { - getImplementation().setParams(win, params); - }); - }; + function cloneAndExcludeBlocks(input) { + var name, output = {}; - var getWindows = function () { - return windows; - }; + for (name in input) { + if (name !== 'li' && name != 'p') { + output[name] = input[name]; + } + } - editor.on('remove', function () { - Arr.each(windows.slice(0), function (win) { - getImplementation().close(win); - }); - }); + return output; + } - return { - // Used by the legacy3x compat layer and possible third party - // TODO: Deprecate this, and possible switch to a immutable window array for getWindows - windows: windows, + parser = new SaxParser({ + validate: validate, + allow_script_urls: settings.allow_script_urls, + allow_conditional_comments: settings.allow_conditional_comments, - /** - * Opens a new window. - * - * @method open - * @param {Object} args Optional name/value settings collection contains things like width/height/url etc. - * @param {Object} params Options like title, file, width, height etc. - * @option {String} title Window title. - * @option {String} file URL of the file to open in the window. - * @option {Number} width Width in pixels. - * @option {Number} height Height in pixels. - * @option {Boolean} autoScroll Specifies whether the popup window can have scrollbars if required (i.e. content - * larger than the popup size specified). - */ - open: open, + // Exclude P and LI from DOM parsing since it's treated better by the DOM parser + self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), - /** - * Creates a alert dialog. Please don't use the blocking behavior of this - * native version use the callback method instead then it can be extended. - * - * @method alert - * @param {String} message Text to display in the new alert dialog. - * @param {function} callback Callback function to be executed after the user has selected ok. - * @param {Object} scope Optional scope to execute the callback in. - * @example - * // Displays an alert box using the active editors window manager instance - * tinymce.activeEditor.windowManager.alert('Hello world!'); - */ - alert: alert, + cdata: function (text) { + node.append(createNode('#cdata', 4)).value = text; + }, - /** - * Creates a confirm dialog. Please don't use the blocking behavior of this - * native version use the callback method instead then it can be extended. - * - * @method confirm - * @param {String} message Text to display in the new confirm dialog. - * @param {function} callback Callback function to be executed after the user has selected ok or cancel. - * @param {Object} scope Optional scope to execute the callback in. - * @example - * // Displays an confirm box and an alert message will be displayed depending on what you choose in the confirm - * tinymce.activeEditor.windowManager.confirm("Do you want to do something", function(s) { - * if (s) - * tinymce.activeEditor.windowManager.alert("Ok"); - * else - * tinymce.activeEditor.windowManager.alert("Cancel"); - * }); - */ - confirm: confirm, + text: function (text, raw) { + var textNode; - /** - * Closes the top most window. - * - * @method close - */ - close: close, + // Trim all redundant whitespace on non white space elements + if (!isInWhiteSpacePreservedElement) { + text = text.replace(allWhiteSpaceRegExp, ' '); - /** - * Returns the params of the last window open call. This can be used in iframe based - * dialog to get params passed from the tinymce plugin. - * - * @example - * var dialogArguments = top.tinymce.activeEditor.windowManager.getParams(); - * - * @method getParams - * @return {Object} Name/value object with parameters passed from windowManager.open call. - */ - getParams: getParams, + if (node.lastChild && blockElements[node.lastChild.name]) { + text = text.replace(startWhiteSpaceRegExp, ''); + } + } - /** - * Sets the params of the last opened window. - * - * @method setParams - * @param {Object} params Params object to set for the last opened window. - */ - setParams: setParams, + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; + } + }, - /** - * Returns the currently opened window objects. - * - * @method getWindows - * @return {Array} Array of the currently opened windows. - */ - getWindows: getWindows - }; - }; - } -); + comment: function (text) { + node.append(createNode('#comment', 8)).value = text; + }, -/** - * RangePoint.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 - */ + pi: function (name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, -define( - 'tinymce.core.dom.RangePoint', - [ - 'ephox.katamari.api.Arr', - 'tinymce.core.geom.ClientRect' - ], - function (Arr, ClientRect) { - var isXYWithinRange = function (clientX, clientY, range) { - if (range.collapsed) { - return false; - } + doctype: function (text) { + var newNode; - return Arr.foldl(range.getClientRects(), function (state, rect) { - return state || ClientRect.containsXY(rect, clientX, clientY); - }, false); - }; + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, - return { - isXYWithinRange: isXYWithinRange - }; - } -); -/** - * VK.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 - */ + start: function (name, attrs, empty) { + var newNode, attrFiltersLen, elementRule, attrName, parent; -/** - * This file exposes a set of the common KeyCodes for use. Please grow it as needed. - */ -define( - 'tinymce.core.util.VK', - [ - "tinymce.core.Env" - ], - function (Env) { - return { - BACKSPACE: 8, - DELETE: 46, - DOWN: 40, - ENTER: 13, - LEFT: 37, - RIGHT: 39, - SPACEBAR: 32, - TAB: 9, - UP: 38, + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; - modifierPressed: function (e) { - return e.shiftKey || e.ctrlKey || e.altKey || this.metaKeyPressed(e); - }, + node.append(newNode); - metaKeyPressed: function (e) { - // Check if ctrl or meta key is pressed. Edge case for AltGr on Windows where it produces ctrlKey+altKey states - return (Env.mac ? e.metaKey : e.ctrlKey && !e.altKey); - } - }; - } -); + // 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); + } -/** - * ControlSelection.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 - */ + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; -/** - * This class handles control selection of elements. Controls are elements - * that can be resized and needs to be selected as a whole. It adds custom resize handles - * to all browser engines that support properly disabling the built in resize logic. - * - * @class tinymce.dom.ControlSelection - */ -define( - 'tinymce.core.dom.ControlSelection', - [ - 'ephox.katamari.api.Fun', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.Selectors', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangePoint', - 'tinymce.core.Env', - 'tinymce.core.util.Delay', - 'tinymce.core.util.Tools', - 'tinymce.core.util.VK' - ], - function (Fun, Element, Selectors, NodeType, RangePoint, Env, Delay, Tools, VK) { - var isContentEditableFalse = NodeType.isContentEditableFalse; - var isContentEditableTrue = NodeType.isContentEditableTrue; + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; - function getContentEditableRoot(root, node) { - while (node && node != root) { - if (isContentEditableTrue(node) || isContentEditableFalse(node)) { - return node; - } + if (list) { + list.push(newNode); + } else { + matchedAttributes[attrName] = [newNode]; + } + } + } - node = node.parentNode; - } + // Trim whitespace before block + if (blockElements[name]) { + removeWhitespaceBefore(newNode); + } - return null; - } + // Change current node if the element wasn't empty i.e not
    or + if (!empty) { + node = newNode; + } - var isImage = function (elm) { - return elm && elm.nodeName === 'IMG'; - }; + // Check if we are inside a whitespace preserved element + if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { + isInWhiteSpacePreservedElement = true; + } + } + }, - var isEventOnImageOutsideRange = function (evt, range) { - return isImage(evt.target) && !RangePoint.isXYWithinRange(evt.clientX, evt.clientY, range); - }; + end: function (name) { + var textNode, elementRule, text, sibling, tempNode; - var contextMenuSelectImage = function (editor, evt) { - var target = evt.target; + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + if (blockElements[name]) { + if (!isInWhiteSpacePreservedElement) { + // Trim whitespace of the first node in a block + textNode = node.firstChild; + if (textNode && textNode.type === 3) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); - if (isEventOnImageOutsideRange(evt, editor.selection.getRng()) && !evt.isDefaultPrevented()) { - evt.preventDefault(); - editor.selection.select(target); - } - }; + // Any characters left after trim or should we remove it + if (text.length > 0) { + textNode.value = text; + textNode = textNode.next; + } else { + sibling = textNode.next; + textNode.remove(); + textNode = sibling; - return function (selection, editor) { - var dom = editor.dom, each = Tools.each; - var selectedElm, selectedElmGhost, resizeHelper, resizeHandles, selectedHandle, lastMouseDownEvent; - var startX, startY, selectedElmX, selectedElmY, startW, startH, ratio, resizeStarted; - var width, height, editableDoc = editor.getDoc(), rootDocument = document, isIE = Env.ie && Env.ie < 11; - var abs = Math.abs, round = Math.round, rootElement = editor.getBody(), startScrollWidth, startScrollHeight; + // Remove any pure whitespace siblings + while (textNode && textNode.type === 3) { + text = textNode.value; + sibling = textNode.next; - // Details about each resize handle how to scale etc - resizeHandles = { - // Name: x multiplier, y multiplier, delta size x, delta size y - /*n: [0.5, 0, 0, -1], - e: [1, 0.5, 1, 0], - s: [0.5, 1, 0, 1], - w: [0, 0.5, -1, 0],*/ - nw: [0, 0, -1, -1], - ne: [1, 0, 1, -1], - se: [1, 1, 1, 1], - sw: [0, 1, -1, 1] - }; + if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { + textNode.remove(); + textNode = sibling; + } - // Add CSS for resize handles, cloned element and selected - var rootClass = '.mce-content-body'; - editor.contentStyles.push( - rootClass + ' div.mce-resizehandle {' + - 'position: absolute;' + - 'border: 1px solid black;' + - 'box-sizing: box-sizing;' + - 'background: #FFF;' + - 'width: 7px;' + - 'height: 7px;' + - 'z-index: 10000' + - '}' + - rootClass + ' .mce-resizehandle:hover {' + - 'background: #000' + - '}' + - rootClass + ' img[data-mce-selected],' + rootClass + ' hr[data-mce-selected] {' + - 'outline: 1px solid black;' + - 'resize: none' + // Have been talks about implementing this in browsers - '}' + - rootClass + ' .mce-clonedresizable {' + - 'position: absolute;' + - (Env.gecko ? '' : 'outline: 1px dashed black;') + // Gecko produces trails while resizing - 'opacity: .5;' + - 'filter: alpha(opacity=50);' + - 'z-index: 10000' + - '}' + - rootClass + ' .mce-resize-helper {' + - 'background: #555;' + - 'background: rgba(0,0,0,0.75);' + - 'border-radius: 3px;' + - 'border: 1px;' + - 'color: white;' + - 'display: none;' + - 'font-family: sans-serif;' + - 'font-size: 12px;' + - 'white-space: nowrap;' + - 'line-height: 14px;' + - 'margin: 5px 10px;' + - 'padding: 5px;' + - 'position: absolute;' + - 'z-index: 10001' + - '}' - ); + textNode = sibling; + } + } + } - function isResizable(elm) { - var selector = editor.settings.object_resizing; + // Trim whitespace of the last node in a block + textNode = node.lastChild; + if (textNode && textNode.type === 3) { + text = textNode.value.replace(endWhiteSpaceRegExp, ''); - if (selector === false || Env.iOS) { - return false; - } + // Any characters left after trim or should we remove it + if (text.length > 0) { + textNode.value = text; + textNode = textNode.prev; + } else { + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; - if (typeof selector != 'string') { - selector = 'table,img,div'; - } + // Remove any pure whitespace siblings + while (textNode && textNode.type === 3) { + text = textNode.value; + sibling = textNode.prev; - if (elm.getAttribute('data-mce-resize') === 'false') { - return false; - } + if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { + textNode.remove(); + textNode = sibling; + } - if (elm == editor.getBody()) { - return false; - } + textNode = sibling; + } + } + } + } - return Selectors.is(Element.fromDom(elm), selector); - } + // Trim start white space + // Removed due to: #5424 + /*textNode = node.prev; + if (textNode && textNode.type === 3) { + text = textNode.value.replace(startWhiteSpaceRegExp, ''); - function resizeGhostElement(e) { - var deltaX, deltaY, proportional; - var resizeHelperX, resizeHelperY; + if (text.length > 0) + textNode.value = text; + else + textNode.remove(); + }*/ + } - // Calc new width/height - deltaX = e.screenX - startX; - deltaY = e.screenY - startY; + // Check if we exited a whitespace preserved element + if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { + isInWhiteSpacePreservedElement = false; + } - // Calc new size - width = deltaX * selectedHandle[2] + startW; - height = deltaY * selectedHandle[3] + startH; + // Handle empty nodes + if (elementRule.removeEmpty || elementRule.paddEmpty) { + if (isEmpty(schema, nonEmptyElements, whiteSpaceElements, node)) { + if (elementRule.paddEmpty) { + paddEmptyNode(settings, node); + } else { + // Leave nodes that have a name like + if (!node.attributes.map.name && !node.attributes.map.id) { + tempNode = node.parent; - // Never scale down lower than 5 pixels - width = width < 5 ? 5 : width; - height = height < 5 ? 5 : height; + if (blockElements[node.name]) { + node.empty().remove(); + } else { + node.unwrap(); + } - if (selectedElm.nodeName == "IMG" && editor.settings.resize_img_proportional !== false) { - proportional = !VK.modifierPressed(e); - } else { - proportional = VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0); - } + node = tempNode; + return; + } + } + } + } - // Constrain proportions - if (proportional) { - if (abs(deltaX) > abs(deltaY)) { - height = round(width * ratio); - width = round(height / ratio); - } else { - width = round(height / ratio); - height = round(width * ratio); + node = node.parent; + } } - } - - // Update ghost size - dom.setStyles(selectedElmGhost, { - width: width, - height: height - }); - - // Update resize helper position - resizeHelperX = selectedHandle.startPos.x + deltaX; - resizeHelperY = selectedHandle.startPos.y + deltaY; - resizeHelperX = resizeHelperX > 0 ? resizeHelperX : 0; - resizeHelperY = resizeHelperY > 0 ? resizeHelperY : 0; - - dom.setStyles(resizeHelper, { - left: resizeHelperX, - top: resizeHelperY, - display: 'block' - }); + }, schema); - resizeHelper.innerHTML = width + ' × ' + height; + rootNode = node = new Node(args.context || settings.root_name, 11); - // Update ghost X position if needed - if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { - dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); - } + parser.parse(html); - // Update ghost Y position if needed - if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { - dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); + // Fix invalid children or report invalid children in a contextual parsing + if (validate && invalidChildren.length) { + if (!args.context) { + fixInvalidChildren(invalidChildren); + } else { + args.invalid = true; + } } - // Calculate how must overflow we got - deltaX = rootElement.scrollWidth - startScrollWidth; - deltaY = rootElement.scrollHeight - startScrollHeight; - - // Re-position the resize helper based on the overflow - if (deltaX + deltaY !== 0) { - dom.setStyles(resizeHelper, { - left: resizeHelperX - deltaX, - top: resizeHelperY - deltaY - }); + // Wrap nodes in the root into block elements if the root is body + if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) { + addRootBlocks(); } - if (!resizeStarted) { - editor.fire('ObjectResizeStart', { target: selectedElm, width: startW, height: startH }); - resizeStarted = true; - } - } + // Run filters only when the contents is valid + if (!args.invalid) { + // Run node filters + for (name in matchedNodes) { + list = nodeFilters[name]; + nodes = matchedNodes[name]; - function endGhostResize() { - resizeStarted = false; + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) { + nodes.splice(fi, 1); + } + } - function setSizeProp(name, value) { - if (value) { - // Resize by using style or attribute - if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { - dom.setStyle(selectedElm, name, value); - } else { - dom.setAttrib(selectedElm, name, value); + for (i = 0, l = list.length; i < l; i++) { + list[i](nodes, name, args); } } - } - // Set width/height properties - setSizeProp('width', width); - setSizeProp('height', height); - - dom.unbind(editableDoc, 'mousemove', resizeGhostElement); - dom.unbind(editableDoc, 'mouseup', endGhostResize); + // Run attribute filters + for (i = 0, l = attributeFilters.length; i < l; i++) { + list = attributeFilters[i]; - if (rootDocument != editableDoc) { - dom.unbind(rootDocument, 'mousemove', resizeGhostElement); - dom.unbind(rootDocument, 'mouseup', endGhostResize); - } + if (list.name in matchedAttributes) { + nodes = matchedAttributes[list.name]; - // Remove ghost/helper and update resize handle positions - dom.remove(selectedElmGhost); - dom.remove(resizeHelper); + // Remove already removed children + fi = nodes.length; + while (fi--) { + if (!nodes[fi].parent) { + nodes.splice(fi, 1); + } + } - if (!isIE || selectedElm.nodeName == "TABLE") { - showResizeRect(selectedElm); + for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) { + list.callbacks[fi](nodes, list.name, args); + } + } + } } - editor.fire('ObjectResized', { target: selectedElm, width: width, height: height }); - dom.setAttrib(selectedElm, 'style', dom.getAttrib(selectedElm, 'style')); - editor.nodeChanged(); - } - - function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) { - var position, targetWidth, targetHeight, e, rect; - - hideResizeRect(); - unbindResizeHandleEvents(); - - // Get position and size of target - position = dom.getPos(targetElm, rootElement); - selectedElmX = position.x; - selectedElmY = position.y; - rect = targetElm.getBoundingClientRect(); // Fix for Gecko offsetHeight for table with caption - targetWidth = rect.width || (rect.right - rect.left); - targetHeight = rect.height || (rect.bottom - rect.top); + return rootNode; + }; - // Reset width/height if user selects a new image/table - if (selectedElm != targetElm) { - detachResizeStartListener(); - selectedElm = targetElm; - width = height = 0; - } + // Remove
    at end of block elements Gecko and WebKit injects BR elements to + // make it possible to place the caret inside empty blocks. This logic tries to remove + // these elements and keep br elements that where intended to be there intact + if (settings.remove_trailing_brs) { + self.addNodeFilter('br', function (nodes) { + var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements()); + var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; + var whiteSpaceElements = schema.getNonEmptyElements(); + var elementRule, textNode; - // Makes it possible to disable resizing - e = editor.fire('ObjectSelected', { target: targetElm }); + // Remove brs from body element as well + blockElements.body = 1; - if (isResizable(targetElm) && !e.isDefaultPrevented()) { - each(resizeHandles, function (handle, name) { - var handleElm; + // Must loop forwards since it will otherwise remove all brs in

    a


    + for (i = 0; i < l; i++) { + node = nodes[i]; + parent = node.parent; - function startDrag(e) { - startX = e.screenX; - startY = e.screenY; - startW = selectedElm.clientWidth; - startH = selectedElm.clientHeight; - ratio = startH / startW; - selectedHandle = handle; + if (blockElements[node.parent.name] && node === parent.lastChild) { + // Loop all nodes to the left of the current node and check for other BR elements + // excluding bookmarks since they are invisible + prev = node.prev; + while (prev) { + prevName = prev.name; - handle.startPos = { - x: targetWidth * handle[0] + selectedElmX, - y: targetHeight * handle[1] + selectedElmY - }; + // Ignore bookmarks + if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { + // Found a non BR element + if (prevName !== "br") { + break; + } - startScrollWidth = rootElement.scrollWidth; - startScrollHeight = rootElement.scrollHeight; + // Found another br it's a

    structure then don't remove anything + if (prevName === 'br') { + node = null; + break; + } + } - selectedElmGhost = selectedElm.cloneNode(true); - dom.addClass(selectedElmGhost, 'mce-clonedresizable'); - dom.setAttrib(selectedElmGhost, 'data-mce-bogus', 'all'); - selectedElmGhost.contentEditable = false; // Hides IE move layer cursor - selectedElmGhost.unSelectabe = true; - dom.setStyles(selectedElmGhost, { - left: selectedElmX, - top: selectedElmY, - margin: 0 - }); + prev = prev.prev; + } - selectedElmGhost.removeAttribute('data-mce-selected'); - rootElement.appendChild(selectedElmGhost); + if (node) { + node.remove(); - dom.bind(editableDoc, 'mousemove', resizeGhostElement); - dom.bind(editableDoc, 'mouseup', endGhostResize); + // Is the parent to be considered empty after we removed the BR + if (isEmpty(schema, nonEmptyElements, whiteSpaceElements, parent)) { + elementRule = schema.getElementRule(parent.name); - if (rootDocument != editableDoc) { - dom.bind(rootDocument, 'mousemove', resizeGhostElement); - dom.bind(rootDocument, 'mouseup', endGhostResize); + // Remove or padd the element depending on schema rule + if (elementRule) { + if (elementRule.removeEmpty) { + parent.remove(); + } else if (elementRule.paddEmpty) { + paddEmptyNode(settings, parent); + } + } + } } + } else { + // Replaces BR elements inside inline elements like


    + // so they become

     

    + lastParent = node; + while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { + lastParent = parent; - resizeHelper = dom.add(rootElement, 'div', { - 'class': 'mce-resize-helper', - 'data-mce-bogus': 'all' - }, startW + ' × ' + startH); - } + if (blockElements[parent.name]) { + break; + } - if (mouseDownHandleName) { - // Drag started by IE native resizestart - if (name == mouseDownHandleName) { - startDrag(mouseDownEvent); + parent = parent.parent; } - return; - } - - // Get existing or render resize handle - handleElm = dom.get('mceResizeHandle' + name); - if (handleElm) { - dom.remove(handleElm); - } - - handleElm = dom.add(rootElement, 'div', { - id: 'mceResizeHandle' + name, - 'data-mce-bogus': 'all', - 'class': 'mce-resizehandle', - unselectable: true, - style: 'cursor:' + name + '-resize; margin:0; padding:0' - }); - - // Hides IE move layer cursor - // If we set it on Chrome we get this wounderful bug: #6725 - if (Env.ie) { - handleElm.contentEditable = false; + if (lastParent === parent && settings.padd_empty_with_br !== true) { + textNode = new Node('#text', 3); + textNode.value = '\u00a0'; + node.replace(textNode); + } } - - dom.bind(handleElm, 'mousedown', function (e) { - e.stopImmediatePropagation(); - e.preventDefault(); - startDrag(e); - }); - - handle.elm = handleElm; - - // Position element - dom.setStyles(handleElm, { - left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), - top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) - }); - }); - } else { - hideResizeRect(); - } - - selectedElm.setAttribute('data-mce-selected', '1'); + } + }); } - function hideResizeRect() { - var name, handleElm; - unbindResizeHandleEvents(); + self.addAttributeFilter('href', function (nodes) { + var i = nodes.length, node; - if (selectedElm) { - selectedElm.removeAttribute('data-mce-selected'); - } + var appendRel = function (rel) { + var parts = rel.split(' ').filter(function (p) { + return p.length > 0; + }); + return parts.concat(['noopener']).sort().join(' '); + }; - for (name in resizeHandles) { - handleElm = dom.get('mceResizeHandle' + name); - if (handleElm) { - dom.unbind(handleElm); - dom.remove(handleElm); + var addNoOpener = function (rel) { + var newRel = rel ? Tools.trim(rel) : ''; + if (!/\b(noopener)\b/g.test(newRel)) { + return appendRel(newRel); + } else { + return newRel; } - } - } - - function updateResizeRect(e) { - var startElm, controlElm; + }; - function isChildOrEqual(node, parent) { - if (node) { - do { - if (node === parent) { - return true; - } - } while ((node = node.parentNode)); + 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'))); + } } } + }); - // Ignore all events while resizing or if the editor instance was removed - if (resizeStarted || editor.removed) { - return; - } + // 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; - // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v - each(dom.select('img[data-mce-selected],hr[data-mce-selected]'), function (img) { - img.removeAttribute('data-mce-selected'); + 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); + } + } }); + } - controlElm = e.type == 'mousedown' ? e.target : selection.getNode(); - controlElm = dom.$(controlElm).closest(isIE ? 'table' : 'table,img,hr')[0]; + if (settings.fix_list_elements) { + self.addNodeFilter('ul,ol', function (nodes) { + var i = nodes.length, node, parentNode; - if (isChildOrEqual(controlElm, rootElement)) { - disableGeckoResize(); - startElm = selection.getStart(true); + while (i--) { + node = nodes[i]; + parentNode = node.parent; - if (isChildOrEqual(startElm, controlElm) && isChildOrEqual(selection.getEnd(true), controlElm)) { - if (!isIE || (controlElm != startElm && startElm.nodeName !== 'IMG')) { - showResizeRect(controlElm); - return; + 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); + } } } - } - - hideResizeRect(); + }); } - function attachEvent(elm, name, func) { - if (elm && elm.attachEvent) { - elm.attachEvent('on' + name, func); - } - } + 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; - function detachEvent(elm, name, func) { - if (elm && elm.detachEvent) { - elm.detachEvent('on' + name, func); - } - } + while (i--) { + node = nodes[i]; + classList = node.attr('class').split(' '); + classValue = ''; - function resizeNativeStart(e) { - var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY; + for (ci = 0; ci < classList.length; ci++) { + className = classList[ci]; + valid = false; - pos = target.getBoundingClientRect(); - relativeX = lastMouseDownEvent.clientX - pos.left; - relativeY = lastMouseDownEvent.clientY - pos.top; + validClassesMap = validClasses['*']; + if (validClassesMap && validClassesMap[className]) { + valid = true; + } - // Figure out what corner we are draging on - for (name in resizeHandles) { - corner = resizeHandles[name]; + validClassesMap = validClasses[node.name]; + if (!valid && validClassesMap && validClassesMap[className]) { + valid = true; + } - cornerX = target.offsetWidth * corner[0]; - cornerY = target.offsetHeight * corner[1]; + if (valid) { + if (classValue) { + classValue += ' '; + } - if (abs(cornerX - relativeX) < 8 && abs(cornerY - relativeY) < 8) { - selectedHandle = corner; - break; - } - } + classValue += className; + } + } - // Remove native selection and let the magic begin - resizeStarted = true; - editor.fire('ObjectResizeStart', { - target: selectedElm, - width: selectedElm.clientWidth, - height: selectedElm.clientHeight + if (!classValue.length) { + classValue = null; + } + + node.attr('class', classValue); + } }); - editor.getDoc().selection.empty(); - showResizeRect(target, name, lastMouseDownEvent); } + }; + } +); +/** + * 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 + */ - function preventDefault(e) { - if (e.preventDefault) { - e.preventDefault(); - } else { - e.returnValue = false; // IE - } - } +/** + * 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('


    '); + * console.log(writer.getContent()); + * + * @class tinymce.html.Writer + * @version 3.4 + */ +define( + 'tinymce.core.html.Writer', + [ + "tinymce.core.html.Entities", + "tinymce.core.util.Tools" + ], + function (Entities, Tools) { + var makeMap = Tools.makeMap; - function isWithinContentEditableFalse(elm) { - return isContentEditableFalse(getContentEditableRoot(editor.getBody(), elm)); - } + /** + * Constructs a new Writer instance. + * + * @constructor + * @method Writer + * @param {Object} settings Name/value settings object. + */ + return function (settings) { + var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; - function nativeControlSelect(e) { - var target = e.srcElement; + settings = settings || {}; + indent = settings.indent; + indentBefore = makeMap(settings.indent_before || ''); + indentAfter = makeMap(settings.indent_after || ''); + encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); + htmlOutput = settings.element_format == "html"; - if (isWithinContentEditableFalse(target)) { - preventDefault(e); - return; - } + return { + /** + * Writes the a start element such as

    . + * + * @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 (target != selectedElm) { - editor.fire('ObjectSelected', { target: target }); - detachResizeStartListener(); + if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; - if (target.id.indexOf('mceResizeHandle') === 0) { - e.returnValue = false; - return; + if (value.length > 0 && value !== '\n') { + html.push('\n'); + } } - if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') { - hideResizeRect(); - selectedElm = target; - attachEvent(target, 'resizestart', resizeNativeStart); + 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), '"'); + } } - } - } - function detachResizeStartListener() { - detachEvent(selectedElm, 'resizestart', resizeNativeStart); - } + if (!empty || htmlOutput) { + html[html.length] = '>'; + } else { + html[html.length] = ' />'; + } - function unbindResizeHandleEvents() { - for (var name in resizeHandles) { - var handle = resizeHandles[name]; + if (empty && indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; - if (handle.elm) { - dom.unbind(handle.elm); - delete handle.elm; + if (value.length > 0 && value !== '\n') { + html.push('\n'); + } } - } - } + }, - function disableGeckoResize() { - try { - // Disable object resizing on Gecko - editor.getDoc().execCommand('enableObjectResizing', false, false); - } catch (ex) { - // Ignore - } - } + /** + * Writes the a end element such as

    . + * + * @method end + * @param {String} name Name of the element. + */ + end: function (name) { + var value; - function controlSelect(elm) { - var ctrlRng; + /*if (indent && indentBefore[name] && html.length > 0) { + value = html[html.length - 1]; - if (!isIE) { - return; - } + if (value.length > 0 && value !== '\n') + html.push('\n'); + }*/ - ctrlRng = editableDoc.body.createControlRange(); + html.push(''); - try { - ctrlRng.addElement(elm); - ctrlRng.select(); - return true; - } catch (ex) { - // Ignore since the element can't be control selected for example a P tag - } - } + if (indent && indentAfter[name] && html.length > 0) { + value = html[html.length - 1]; - editor.on('init', function () { - if (isIE) { - // Hide the resize rect on resize and reselect the image - editor.on('ObjectResized', function (e) { - if (e.target.nodeName != 'TABLE') { - hideResizeRect(); - controlSelect(e.target); + if (value.length > 0 && value !== '\n') { + html.push('\n'); } - }); - - attachEvent(rootElement, 'controlselect', nativeControlSelect); - - editor.on('mousedown', function (e) { - lastMouseDownEvent = e; - }); - } else { - disableGeckoResize(); - - // Sniff sniff, hard to feature detect this stuff - if (Env.ie >= 11) { - // Needs to be mousedown for drag/drop to work on IE 11 - // Needs to be click on Edge to properly select images - editor.on('mousedown click', function (e) { - var target = e.target, nodeName = target.nodeName; - - if (!resizeStarted && /^(TABLE|IMG|HR)$/.test(nodeName) && !isWithinContentEditableFalse(target)) { - if (e.button !== 2) { - editor.selection.select(target, nodeName == 'TABLE'); - } - - // Only fire once since nodeChange is expensive - if (e.type == 'mousedown') { - editor.nodeChanged(); - } - } - }); - - editor.dom.bind(rootElement, 'mscontrolselect', function (e) { - function delayedSelect(node) { - Delay.setEditorTimeout(editor, function () { - editor.selection.select(node); - }); - } - - if (isWithinContentEditableFalse(e.target)) { - e.preventDefault(); - delayedSelect(e.target); - return; - } - - if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) { - e.preventDefault(); - - // This moves the selection from being a control selection to a text like selection like in WebKit #6753 - // TODO: Fix this the day IE works like other browsers without this nasty native ugly control selections. - if (e.target.tagName == 'IMG') { - delayedSelect(e.target); - } - } - }); } - } + }, - var throttledUpdateResizeRect = Delay.throttle(function (e) { - if (!editor.composing) { - updateResizeRect(e); + /** + * Writes a text node. + * + * @method text + * @param {String} text String to write out. + * @param {Boolean} raw Optional raw state if true the contents wont get encoded. + */ + text: function (text, raw) { + if (text.length > 0) { + html[html.length] = raw ? text : encode(text); } - }); + }, - editor.on('nodechange ResizeEditor ResizeWindow drop', throttledUpdateResizeRect); + /** + * Writes a cdata node such as . + * + * @method cdata + * @param {String} text String to write out inside the cdata. + */ + cdata: function (text) { + html.push(''); + }, - // Update resize rect while typing in a table - editor.on('keyup compositionend', function (e) { - // Don't update the resize rect while composing since it blows away the IME see: #2710 - if (selectedElm && selectedElm.nodeName == "TABLE") { - throttledUpdateResizeRect(e); - } - }); + /** + * Writes a comment node such as . + * + * @method cdata + * @param {String} text String to write out inside the comment. + */ + comment: function (text) { + html.push(''); + }, - editor.on('hide blur', hideResizeRect); - editor.on('contextmenu', Fun.curry(contextMenuSelectImage, editor)); + /** + * Writes a PI node such as . + * + * @method pi + * @param {String} name Name of the pi. + * @param {String} text String to write out inside the pi. + */ + pi: function (name, text) { + if (text) { + html.push(''); + } else { + html.push(''); + } - // Hide rect on focusout since it would float on top of windows otherwise - //editor.on('focusout', hideResizeRect); - }); + if (indent) { + html.push('\n'); + } + }, - editor.on('remove', unbindResizeHandleEvents); + /** + * Writes a doctype node such as . + * + * @method doctype + * @param {String} text String to write out inside the doctype. + */ + doctype: function (text) { + html.push('', indent ? '\n' : ''); + }, - function destroy() { - selectedElm = selectedElmGhost = null; + /** + * Resets the internal buffer if one wants to reuse the writer. + * + * @method reset + */ + reset: function () { + html.length = 0; + }, - if (isIE) { - detachResizeStartListener(); - detachEvent(rootElement, 'controlselect', nativeControlSelect); + /** + * Returns the contents that got serialized. + * + * @method getContent + * @return {String} HTML contents that got written down. + */ + getContent: function () { + return html.join('').replace(/\n$/, ''); } - } - - return { - isResizable: isResizable, - showResizeRect: showResizeRect, - hideResizeRect: hideResizeRect, - updateResizeRect: updateResizeRect, - controlSelect: controlSelect, - destroy: destroy }; }; } ); - /** - * ScrollIntoView.js + * Serializer.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -29269,77 +27051,161 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * This class is used to serialize down the DOM tree into a string using a Writer instance. + * + * + * @example + * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('

    text

    ')); + * @class tinymce.html.Serializer + * @version 3.4 + */ define( - 'tinymce.core.dom.ScrollIntoView', + 'tinymce.core.html.Serializer', [ - 'tinymce.core.dom.NodeType' + "tinymce.core.html.Writer", + "tinymce.core.html.Schema" ], - function (NodeType) { - var getPos = function (elm) { - var x = 0, y = 0; + 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); - var offsetParent = elm; - while (offsetParent && offsetParent.nodeType) { - x += offsetParent.offsetLeft || 0; - y += offsetParent.offsetTop || 0; - offsetParent = offsetParent.offsetParent; - } + settings = settings || {}; + settings.validate = "validate" in settings ? settings.validate : true; - return { x: x, y: y }; - }; + self.schema = schema = schema || new Schema(); + self.writer = writer; - var fireScrollIntoViewEvent = function (editor, elm, alignToTop) { - var scrollEvent = { elm: elm, alignToTop: alignToTop }; - editor.fire('scrollIntoView', scrollEvent); - return scrollEvent.isDefaultPrevented(); - }; + /** + * 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; - var scrollIntoView = function (editor, elm, alignToTop) { - var y, viewPort, dom = editor.dom, root = dom.getRoot(), viewPortY, viewPortH, offsetY = 0; + validate = settings.validate; - if (fireScrollIntoViewEvent(editor, elm, alignToTop)) { - return; - } + handlers = { + // #text + 3: function (node) { + writer.text(node.value, node.raw); + }, - if (!NodeType.isElement(elm)) { - return; - } + // #comment + 8: function (node) { + writer.comment(node.value); + }, - if (alignToTop === false) { - offsetY = elm.offsetHeight; - } + // Processing instruction + 7: function (node) { + writer.pi(node.name, node.value); + }, - if (root.nodeName !== 'BODY') { - var scrollContainer = editor.selection.getScrollContainer(); - if (scrollContainer) { - y = getPos(elm).y - getPos(scrollContainer).y + offsetY; - viewPortH = scrollContainer.clientHeight; - viewPortY = scrollContainer.scrollTop; - if (y < viewPortY || y + 25 > viewPortY + viewPortH) { - scrollContainer.scrollTop = y < viewPortY ? y : y - viewPortH + 25; + // 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)); + } } + }; - return; + 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); + } } - } - viewPort = dom.getViewPort(editor.getWin()); - y = dom.getPos(elm).y + offsetY; - viewPortY = viewPort.y; - viewPortH = viewPort.h; - if (y < viewPort.y || y + 25 > viewPortY + viewPortH) { - editor.getWin().scrollTo(0, y < viewPortY ? y : y - viewPortH + 25); - } - }; + // Serialize element and treat all non elements as fragments + if (node.type == 1 && !settings.inner) { + walk(node); + } else { + handlers[11](node); + } - return { - scrollIntoView: scrollIntoView + return writer.getContent(); + }; }; } ); /** - * TridentSelection.js + * Serializer.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -29349,765 +27215,696 @@ define( */ /** - * Selection class for old explorer versions. This one fakes the - * native selection object available on modern browsers. + * 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. * - * @private - * @class tinymce.dom.TridentSelection + * @class tinymce.dom.Serializer */ define( - 'tinymce.core.dom.TridentSelection', + 'tinymce.core.dom.Serializer', [ + 'global!document', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.Env', + 'tinymce.core.html.DomParser', + 'tinymce.core.html.Entities', + 'tinymce.core.html.Node', + 'tinymce.core.html.SaxParser', + 'tinymce.core.html.Schema', + 'tinymce.core.html.Serializer', + 'tinymce.core.text.Zwsp', + 'tinymce.core.util.Tools' ], - function () { - function Selection(selection) { - var self = this, dom = selection.dom, FALSE = false; - - function getPosition(rng, start) { - var checkRng, startIndex = 0, endIndex, inside, - children, child, offset, index, position = -1, parent; + function (document, DOMUtils, Env, DomParser, Entities, Node, SaxParser, Schema, Serializer, Zwsp, Tools) { + var each = Tools.each, trim = Tools.trim; + var DOM = DOMUtils.DOM; - // Setup test range, collapse it and get the parent - checkRng = rng.duplicate(); - checkRng.collapse(start); - parent = checkRng.parentElement(); + /** + * 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

    + */ + function trimTrailingBr(rootNode) { + var brNode1, brNode2; - // Check if the selection is within the right document - if (parent.ownerDocument !== selection.dom.doc) { - return; - } + function isBr(node) { + return node && node.name === 'br'; + } - // IE will report non editable elements as it's parent so look for an editable one - while (parent.contentEditable === "false") { - parent = parent.parentNode; - } + brNode1 = rootNode.lastChild; + if (isBr(brNode1)) { + brNode2 = brNode1.prev; - // If parent doesn't have any children then return that we are inside the element - if (!parent.hasChildNodes()) { - return { node: parent, inside: 1 }; + if (isBr(brNode2)) { + brNode1.remove(); + brNode2.remove(); } + } + } - // Setup node list and endIndex - children = parent.children; - endIndex = children.length - 1; + /** + * Constructs a new DOM serializer class. + * + * @constructor + * @method Serializer + * @param {Object} settings Serializer settings object. + * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. + */ + return function (settings, editor) { + var dom, schema, htmlParser, tempAttrs = ["data-mce-selected"]; - // Perform a binary search for the position - while (startIndex <= endIndex) { - index = Math.floor((startIndex + endIndex) / 2); + if (editor) { + dom = editor.dom; + schema = editor.schema; + } - // Move selection to node and compare the ranges - child = children[index]; - checkRng.moveToElementText(child); - position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng); + function trimHtml(html) { + var trimContentRegExp = new RegExp([ + ']+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers + '\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected + ].join('|'), 'gi'); - // Before/after or an exact match - if (position > 0) { - endIndex = index - 1; - } else if (position < 0) { - startIndex = index + 1; - } else { - return { node: child }; - } - } + html = Zwsp.trim(html.replace(trimContentRegExp, '')); - // Check if child position is before or we didn't find a position - if (position < 0) { - // No element child was found use the parent element and the offset inside that - if (!child) { - checkRng.moveToElementText(parent); - checkRng.collapse(true); - child = parent; - inside = true; - } else { - checkRng.collapse(false); - } + return html; + } - // Walk character by character in text node until we hit the selected range endpoint, - // hit the end of document or parent isn't the right one - // We need to walk char by char since rng.text or rng.htmlText will trim line endings - offset = 0; - while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { - if (checkRng.move('character', 1) === 0 || parent != checkRng.parentElement()) { - break; - } + function trimContent(html) { + var content = html; + var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g; + var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema; - offset++; - } - } else { - // Child position is after the selection endpoint - checkRng.collapse(true); + content = trimHtml(content); + shortEndedElements = schema.getShortEndedElements(); - // Walk character by character in text node until we hit the selected range endpoint, hit - // the end of document or parent isn't the right one - offset = 0; - while (checkRng.compareEndPoints(start ? 'StartToStart' : 'StartToEnd', rng) !== 0) { - if (checkRng.move('character', -1) === 0 || parent != checkRng.parentElement()) { - break; - } + // Remove all bogus elements marked with "all" + while ((matches = bogusAllRegExp.exec(content))) { + index = bogusAllRegExp.lastIndex; + matchLength = matches[0].length; - offset++; + if (shortEndedElements[matches[1]]) { + endTagIndex = index; + } else { + endTagIndex = SaxParser.findEndTag(schema, content, index); } + + content = content.substring(0, index - matchLength) + content.substring(endTagIndex); + bogusAllRegExp.lastIndex = index - matchLength; } - return { node: child, position: position, offset: offset, inside: inside }; + return content; } - // Returns a W3C DOM compatible range object by using the IE Range API - function getRange() { - var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed, tmpRange, element2, bookmark; - - // If selection is outside the current document just return an empty range - element = ieRange.item ? ieRange.item(0) : ieRange.parentElement(); - if (element.ownerDocument != dom.doc) { - return domRange; - } + /** + * Returns a trimmed version of the editor contents to be used for the undo level. This + * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also + * remove the data-mce-selected attributes used for selection of objects and caret containers. + * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will + * be removed by the serialization logic when you save. + * + * @private + * @return {String} HTML contents of the editor excluding some internal bogus elements. + */ + function getTrimmedContent() { + return trimContent(editor.getBody().innerHTML); + } - collapsed = selection.isCollapsed(); + function addTempAttr(name) { + if (Tools.inArray(tempAttrs, name) === -1) { + htmlParser.addAttributeFilter(name, function (nodes, name) { + var i = nodes.length; - // Handle control selection - if (ieRange.item) { - domRange.setStart(element.parentNode, dom.nodeIndex(element)); - domRange.setEnd(domRange.startContainer, domRange.startOffset + 1); + while (i--) { + nodes[i].attr(name, null); + } + }); - return domRange; + tempAttrs.push(name); } + } - function findEndPoint(start) { - var endPoint = getPosition(ieRange, start), container, offset, textNodeOffset = 0, sibling, undef, nodeValue; - - container = endPoint.node; - offset = endPoint.offset; - - if (endPoint.inside && !container.hasChildNodes()) { - domRange[start ? 'setStart' : 'setEnd'](container, 0); - return; - } + // Default DOM and Schema if they are undefined + dom = dom || DOM; + schema = schema || new Schema(settings); + settings.entity_encoding = settings.entity_encoding || 'named'; + settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; - if (offset === undef) { - domRange[start ? 'setStartBefore' : 'setEndAfter'](container); - return; - } + htmlParser = new DomParser(settings, schema); - if (endPoint.position < 0) { - sibling = endPoint.inside ? container.firstChild : container.nextSibling; + // Convert tabindex back to elements when serializing contents + htmlParser.addAttributeFilter('data-mce-tabindex', function (nodes, name) { + var i = nodes.length, node; - if (!sibling) { - domRange[start ? 'setStartAfter' : 'setEndAfter'](container); - return; - } - - if (!offset) { - if (sibling.nodeType == 3) { - domRange[start ? 'setStart' : 'setEnd'](sibling, 0); - } else { - domRange[start ? 'setStartBefore' : 'setEndBefore'](sibling); - } + while (i--) { + node = nodes[i]; + node.attr('tabindex', node.attributes.map['data-mce-tabindex']); + node.attr(name, null); + } + }); - return; - } + // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed + htmlParser.addAttributeFilter('src,href,style', function (nodes, name) { + var i = nodes.length, node, value, internalName = 'data-mce-' + name; + var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; - // Find the text node and offset - while (sibling) { - if (sibling.nodeType == 3) { - nodeValue = sibling.nodeValue; - textNodeOffset += nodeValue.length; - - // We are at or passed the position we where looking for - if (textNodeOffset >= offset) { - container = sibling; - textNodeOffset -= offset; - textNodeOffset = nodeValue.length - textNodeOffset; - break; - } - } + while (i--) { + node = nodes[i]; - sibling = sibling.nextSibling; - } + value = node.attributes.map[internalName]; + if (value !== undef) { + // Set external name to internal value and remove internal + node.attr(name, value.length > 0 ? value : null); + node.attr(internalName, null); } else { - // Find the text node and offset - sibling = container.previousSibling; + // No internal attribute found then convert the value we have in the DOM + value = node.attributes.map[name]; - if (!sibling) { - return domRange[start ? 'setStartBefore' : 'setEndBefore'](container); + if (name === "style") { + value = dom.serializeStyle(dom.parseStyle(value), node.name); + } else if (urlConverter) { + value = urlConverter.call(urlConverterScope, value, name, node.name); } - // If there isn't any text to loop then use the first position - if (!offset) { - if (container.nodeType == 3) { - domRange[start ? 'setStart' : 'setEnd'](sibling, container.nodeValue.length); - } else { - domRange[start ? 'setStartAfter' : 'setEndAfter'](sibling); - } - - return; - } + node.attr(name, value.length > 0 ? value : null); + } + } + }); - while (sibling) { - if (sibling.nodeType == 3) { - textNodeOffset += sibling.nodeValue.length; + // Remove internal classes mceItem<..> or mceSelected + htmlParser.addAttributeFilter('class', function (nodes) { + var i = nodes.length, node, value; - // We are at or passed the position we where looking for - if (textNodeOffset >= offset) { - container = sibling; - textNodeOffset -= offset; - break; - } - } + while (i--) { + node = nodes[i]; + value = node.attr('class'); - sibling = sibling.previousSibling; - } + if (value) { + value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); + node.attr('class', value.length > 0 ? value : null); } - - domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset); } + }); - try { - // Find start point - findEndPoint(true); + // Remove bookmark elements + htmlParser.addAttributeFilter('data-mce-type', function (nodes, name, args) { + var i = nodes.length, node; + + while (i--) { + node = nodes[i]; - // Find end point if needed - if (!collapsed) { - findEndPoint(); + if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { + node.remove(); } - } catch (ex) { - // IE has a nasty bug where text nodes might throw "invalid argument" when you - // access the nodeValue or other properties of text nodes. This seems to happen when - // text nodes are split into two nodes by a delete/backspace call. - // So let us detect and try to fix it. - if (ex.number == -2147024809) { - // Get the current selection - bookmark = self.getBookmark(2); + } + }); + + htmlParser.addNodeFilter('noscript', function (nodes) { + var i = nodes.length, node; - // Get start element - tmpRange = ieRange.duplicate(); - tmpRange.collapse(true); - element = tmpRange.parentElement(); + while (i--) { + node = nodes[i].firstChild; - // Get end element - if (!collapsed) { - tmpRange = ieRange.duplicate(); - tmpRange.collapse(false); - element2 = tmpRange.parentElement(); - element2.innerHTML = element2.innerHTML; - } + if (node) { + node.value = Entities.decode(node.value); + } + } + }); - // Remove the broken elements - element.innerHTML = element.innerHTML; + // Force script into CDATA sections and remove the mce- prefix also add comments around styles + htmlParser.addNodeFilter('script,style', function (nodes, name) { + var i = nodes.length, node, value, type; - // Restore the selection - self.moveToBookmark(bookmark); + function trim(value) { + /*jshint maxlen:255 */ + /*eslint max-len:0 */ + return value.replace(/()/g, '\n') + .replace(/^[\r\n]*|[\r\n]*$/g, '') + .replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); + } - // Since the range has moved we need to re-get it - ieRange = selection.getRng(); + while (i--) { + node = nodes[i]; + value = node.firstChild ? node.firstChild.value : ''; - // Find start point - findEndPoint(true); + if (name === "script") { + // Remove mce- prefix from script elements and remove default type since the user specified + // a script element without type attribute + type = node.attr('type'); + if (type) { + node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, '')); + } - // Find end point if needed - if (!collapsed) { - findEndPoint(); + if (value.length > 0) { + node.firstChild.value = '// '; } } else { - throw ex; // Throw other errors + if (value.length > 0) { + node.firstChild.value = ''; + } } } + }); - return domRange; - } - - this.getBookmark = function (type) { - var rng = selection.getRng(), bookmark = {}; - - function getIndexes(node) { - var parent, root, children, i, indexes = []; - - parent = node.parentNode; - root = dom.getRoot().parentNode; - - while (parent != root && parent.nodeType !== 9) { - children = parent.children; + // Convert comments to cdata and handle protected comments + htmlParser.addNodeFilter('#comment', function (nodes) { + var i = nodes.length, node; - i = children.length; - while (i--) { - if (node === children[i]) { - indexes.push(i); - break; - } - } + while (i--) { + node = nodes[i]; - node = parent; - parent = parent.parentNode; + if (node.value.indexOf('[CDATA[') === 0) { + node.name = '#cdata'; + node.type = 4; + node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); + } else if (node.value.indexOf('mce:protected ') === 0) { + node.name = "#text"; + node.type = 3; + node.raw = true; + node.value = unescape(node.value).substr(14); } - - return indexes; } + }); - function getBookmarkEndPoint(start) { - var position; + htmlParser.addNodeFilter('xml:namespace,input', function (nodes, name) { + var i = nodes.length, node; - position = getPosition(rng, start); - if (position) { - return { - position: position.position, - offset: position.offset, - indexes: getIndexes(position.node), - inside: position.inside - }; + while (i--) { + node = nodes[i]; + if (node.type === 7) { + node.remove(); + } else if (node.type === 1) { + if (name === "input" && !("type" in node.attributes.map)) { + node.attr('type', 'text'); + } } } + }); + + // Remove internal data attributes + htmlParser.addAttributeFilter( + 'data-mce-src,data-mce-href,data-mce-style,' + + 'data-mce-selected,data-mce-expando,' + + 'data-mce-type,data-mce-resize', - // Non ubstructive bookmark - if (type === 2) { - // Handle text selection - if (!rng.item) { - bookmark.start = getBookmarkEndPoint(true); + function (nodes, name) { + var i = nodes.length; - if (!selection.isCollapsed()) { - bookmark.end = getBookmarkEndPoint(); - } - } else { - bookmark.start = { ctrl: true, indexes: getIndexes(rng.item(0)) }; + while (i--) { + nodes[i].attr(name, null); } } + ); - return bookmark; - }; + // Return public methods + return { + /** + * Schema instance that was used to when the Serializer was constructed. + * + * @field {tinymce.html.Schema} schema + */ + schema: schema, - this.moveToBookmark = function (bookmark) { - var rng, body = dom.doc.body; + /** + * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name + * and then execute the callback ones it has finished parsing the document. + * + * @example + * parser.addNodeFilter('p,h1', function(nodes, name) { + * for (var i = 0; i < nodes.length; i++) { + * console.log(nodes[i].name); + * } + * }); + * @method addNodeFilter + * @method {String} name Comma separated list of nodes to collect. + * @param {function} callback Callback function to execute once it has collected nodes. + */ + addNodeFilter: htmlParser.addNodeFilter, - function resolveIndexes(indexes) { - var node, i, idx, children; + /** + * Adds a attribute filter function to the parser used by the serializer, the parser will + * collect nodes that has the specified attributes + * and then execute the callback ones it has finished parsing the document. + * + * @example + * parser.addAttributeFilter('src,href', function(nodes, name) { + * for (var i = 0; i < nodes.length; i++) { + * console.log(nodes[i].name); + * } + * }); + * @method addAttributeFilter + * @method {String} name Comma separated list of nodes to collect. + * @param {function} callback Callback function to execute once it has collected nodes. + */ + addAttributeFilter: htmlParser.addAttributeFilter, - node = dom.getRoot(); - for (i = indexes.length - 1; i >= 0; i--) { - children = node.children; - idx = indexes[i]; + /** + * Serializes the specified browser DOM node into a HTML string. + * + * @method serialize + * @param {DOMNode} node DOM node to serialize. + * @param {Object} args Arguments option that gets passed to event handlers. + */ + serialize: function (node, args) { + var self = this, impl, doc, oldDoc, htmlSerializer, content, rootNode; - if (idx <= children.length - 1) { - node = children[idx]; - } + // Explorer won't clone contents of script and style and the + // selected index of select elements are cleared on a clone operation. + if (Env.ie && dom.select('script,style,select,map').length > 0) { + content = node.innerHTML; + node = node.cloneNode(false); + dom.setHTML(node, content); + } else { + node = node.cloneNode(true); } - return node; - } - - function setBookmarkEndPoint(start) { - var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset; - - if (endPoint) { - moveLeft = endPoint.position > 0; + // Nodes needs to be attached to something in WebKit/Opera + // This fix will make DOM ranges and make Sizzle happy! + impl = document.implementation; + if (impl.createHTMLDocument) { + // Create an empty HTML document + doc = impl.createHTMLDocument(""); - moveRng = body.createTextRange(); - moveRng.moveToElementText(resolveIndexes(endPoint.indexes)); + // Add the element or it's children if it's a body element to the new document + each(node.nodeName == 'BODY' ? node.childNodes : [node], function (node) { + doc.body.appendChild(doc.importNode(node, true)); + }); - offset = endPoint.offset; - if (offset !== undef) { - moveRng.collapse(endPoint.inside || moveLeft); - moveRng.moveStart('character', moveLeft ? -offset : offset); + // Grab first child or body element for serialization + if (node.nodeName != 'BODY') { + node = doc.body.firstChild; } else { - moveRng.collapse(start); - } - - rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng); - - if (start) { - rng.collapse(true); + node = doc.body; } - } - } - if (bookmark.start) { - if (bookmark.start.ctrl) { - rng = body.createControlRange(); - rng.addElement(resolveIndexes(bookmark.start.indexes)); - rng.select(); - } else { - rng = body.createTextRange(); - setBookmarkEndPoint(true); - setBookmarkEndPoint(); - rng.select(); + // set the new document in DOMUtils so createElement etc works + oldDoc = dom.doc; + dom.doc = doc; } - } - }; - this.addRange = function (rng) { - var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, - doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; - - function setEndPoint(start) { - var container, offset, marker, tmpRng, nodes; + args = args || {}; + args.format = args.format || 'html'; - marker = dom.create('a'); - container = start ? startContainer : endContainer; - offset = start ? startOffset : endOffset; - tmpRng = ieRng.duplicate(); + // Don't wrap content if we want selected html + if (args.selection) { + args.forced_root_block = ''; + } - if (container == doc || container == doc.documentElement) { - container = body; - offset = 0; + // Pre process + if (!args.no_events) { + args.node = node; + self.onPreProcess(args); } - if (container.nodeType == 3) { - container.parentNode.insertBefore(marker, container); - tmpRng.moveToElementText(marker); - tmpRng.moveStart('character', offset); - dom.remove(marker); - ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); - } else { - nodes = container.childNodes; + // Parse HTML + content = Zwsp.trim(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node))); + rootNode = htmlParser.parse(content, args); + trimTrailingBr(rootNode); - if (nodes.length) { - if (offset >= nodes.length) { - dom.insertAfter(marker, nodes[nodes.length - 1]); - } else { - container.insertBefore(marker, nodes[offset]); - } + // Serialize HTML + htmlSerializer = new Serializer(settings, schema); + args.content = htmlSerializer.serialize(rootNode); - tmpRng.moveToElementText(marker); - } else if (container.canHaveHTML) { - // Empty node selection for example
    |
    - // Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open - container.innerHTML = ''; - marker = container.firstChild; - tmpRng.moveToElementText(marker); - tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason - } + // Post process + if (!args.no_events) { + self.onPostProcess(args); + } - ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng); - dom.remove(marker); + // Restore the old document if it was changed + if (oldDoc) { + dom.doc = oldDoc; } - } - // Setup some shorter versions - startContainer = rng.startContainer; - startOffset = rng.startOffset; - endContainer = rng.endContainer; - endOffset = rng.endOffset; - ieRng = body.createTextRange(); - - // If single element selection then try making a control selection out of it - if (startContainer == endContainer && startContainer.nodeType == 1) { - // Trick to place the caret inside an empty block element like

    - if (startOffset == endOffset && !startContainer.hasChildNodes()) { - if (startContainer.canHaveHTML) { - // Check if previous sibling is an empty block if it is then we need to render it - // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 - // Example this:

    |

    would become this:

    |

    - sibling = startContainer.previousSibling; - if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { - sibling.innerHTML = ''; - } else { - sibling = null; - } + args.node = null; - startContainer.innerHTML = ''; - ieRng.moveToElementText(startContainer.lastChild); - ieRng.select(); - dom.doc.selection.clear(); - startContainer.innerHTML = ''; + return args.content; + }, - if (sibling) { - sibling.innerHTML = ''; - } - return; - } + /** + * Adds valid elements rules to the serializers schema instance this enables you to specify things + * like what elements should be outputted and what attributes specific elements might have. + * Consult the Wiki for more details on this format. + * + * @method addRules + * @param {String} rules Valid elements rules string to add to schema. + */ + addRules: function (rules) { + schema.addValidElements(rules); + }, + + /** + * Sets the valid elements rules to the serializers schema instance this enables you to specify things + * like what elements should be outputted and what attributes specific elements might have. + * Consult the Wiki for more details on this format. + * + * @method setRules + * @param {String} rules Valid elements rules string. + */ + setRules: function (rules) { + schema.setValidElements(rules); + }, - startOffset = dom.nodeIndex(startContainer); - startContainer = startContainer.parentNode; + onPreProcess: function (args) { + if (editor) { + editor.fire('PreProcess', args); } + }, - 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 - } + onPostProcess: function (args) { + if (editor) { + editor.fire('PostProcess', args); } - } + }, - // Set start/end point of selection - setEndPoint(true); - setEndPoint(); + /** + * Adds a temporary internal attribute these attributes will get removed on undo and + * when getting contents out of the editor. + * + * @method addTempAttr + * @param {String} name string + */ + addTempAttr: addTempAttr, - // Select the new range and scroll it into view - ieRng.select(); + // Internal + trimHtml: trimHtml, + getTrimmedContent: getTrimmedContent, + trimContent: trimContent }; - - // Expose range method - this.getRangeAt = getRange; - } - - return Selection; + }; } ); -define( - 'ephox.sugar.api.dom.Replication', +/** + * 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', [ - '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' + "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; + }; - function (Attr, Element, Insert, InsertAll, Remove, Traverse) { - var clone = function (original, deep) { - return Element.fromDom(original.dom().cloneNode(deep)); + var isPaddingNode = function (node) { + return node.name === 'br' || node.value === '\u00a0'; }; - /** Shallow clone - just the tag, no children */ - var shallow = function (original) { - return clone(original, false); + var isPaddedEmptyBlock = function (schema, node) { + var blockElements = schema.getBlockElements(); + return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild); }; - /** Deep clone - everything copied including children */ - var deep = function (original) { - return clone(original, true); + var isEmptyFragmentElement = function (schema, node) { + var nonEmptyElements = schema.getNonEmptyElements(); + return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node)); }; - /** Shallow clone, with a new tag */ - var shallowAs = function (original, tag) { - var nu = Element.fromTag(tag); + var isListFragment = function (schema, fragment) { + var firstChild = fragment.firstChild; + var lastChild = fragment.lastChild; - var attributes = Attr.clone(original); - Attr.setAll(nu, attributes); + // Skip meta since it's likely
      ..
    + if (firstChild && firstChild.name === 'meta') { + firstChild = firstChild.next; + } - return nu; + // Skip mce_marker since it's likely
      ..
    + if (lastChild && lastChild.attr('id') === 'mce_marker') { + lastChild = lastChild.prev; + } + + // Skip last child if it's an empty block + if (isEmptyFragmentElement(schema, lastChild)) { + lastChild = lastChild.prev; + } + + if (!firstChild || firstChild !== lastChild) { + return false; + } + + return firstChild.name === 'ul' || firstChild.name === 'ol'; }; - /** Deep clone, with a new tag */ - var copy = function (original, tag) { - var nu = shallowAs(original, tag); + var cleanupDomFragment = function (domFragment) { + var firstChild = domFragment.firstChild; + var lastChild = domFragment.lastChild; - // 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. + // TODO: remove the meta tag from paste logic + if (firstChild && firstChild.nodeName === 'META') { + firstChild.parentNode.removeChild(firstChild); + } - var cloneChildren = Traverse.children(deep(original)); - InsertAll.append(nu, cloneChildren); + if (lastChild && lastChild.id === 'mce_marker') { + lastChild.parentNode.removeChild(lastChild); + } - return nu; + return domFragment; }; - /** Change the tag name, but keep all children */ - var mutate = function (original, tag) { - var nu = shallowAs(original, tag); + var toDomFragment = function (dom, serializer, fragment) { + var html = serializer.serialize(fragment); + var domFragment = dom.createFragment(html); - Insert.before(original, nu); - var children = Traverse.children(original); - InsertAll.append(nu, children); - Remove.remove(original); - return nu; + return cleanupDomFragment(domFragment); }; - return { - shallow: shallow, - shallowAs: shallowAs, - deep: deep, - copy: copy, - mutate: mutate + var listItems = function (elm) { + return Tools.grep(elm.childNodes, function (child) { + return child.nodeName === 'LI'; + }); }; - } -); -define( - 'ephox.sugar.api.node.Fragment', + var isEmpty = function (elm) { + return !elm.firstChild; + }; - [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.node.Element', - 'global!document' - ], + var trimListItems = function (elms) { + return elms.length > 0 && isEmpty(elms[elms.length - 1]) ? elms.slice(0, -1) : elms; + }; - 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); + var getParentLi = function (dom, node) { + var parentBlock = dom.getParent(node, dom.isBlock); + return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null; }; - return { - fromElements: fromElements + var isParentBlockLi = function (dom, node) { + return !!getParentLi(dom, node); }; - } -); -define( - 'ephox.sugar.impl.ClosestOrAncestor', + var getSplit = function (parentNode, rng) { + var beforeRng = rng.cloneRange(); + var afterRng = rng.cloneRange(); - [ - 'ephox.katamari.api.Type', - 'ephox.katamari.api.Option' - ], + beforeRng.setStartBefore(parentNode); + afterRng.setEndAfter(parentNode); - 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); + return [ + beforeRng.cloneContents(), + afterRng.cloneContents() + ]; }; - } -); -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' - ], + var findFirstIn = function (node, rootNode) { + var caretPos = CaretPosition.before(node); + var caretWalker = new CaretWalker(rootNode); + var newCaretPos = caretWalker.next(caretPos); - function (Type, Arr, Fun, Option, Body, Compare, Element, ClosestOrAncestor) { - var first = function (predicate) { - return descendant(Body.body(), predicate); + return newCaretPos ? newCaretPos.toRange() : null; }; - 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 findLastOf = function (node, rootNode) { + var caretPos = CaretPosition.after(node); + var caretWalker = new CaretWalker(rootNode); + var newCaretPos = caretWalker.prev(caretPos); - 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); + return newCaretPos ? newCaretPos.toRange() : null; }; - var sibling = function (scope, predicate) { - var element = scope.dom(); - if (!element.parentNode) return Option.none(); + var insertMiddle = function (target, elms, rootNode, rng) { + var parts = getSplit(target, rng); + var parentElm = target.parentNode; - return child(Element.fromDom(element.parentNode), function (x) { - return !Compare.eq(scope, x) && predicate(x); + parentElm.insertBefore(parts[0], target); + Tools.each(elms, function (li) { + parentElm.insertBefore(li, target); }); - }; + parentElm.insertBefore(parts[1], target); + parentElm.removeChild(target); - var child = function (scope, predicate) { - var result = Arr.find(scope.dom().childNodes, - Fun.compose(predicate, Element.fromDom)); - return result.map(Element.fromDom); + return findLastOf(elms[elms.length - 1], rootNode); }; - 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(); - }; + var insertBefore = function (target, elms, rootNode) { + var parentElm = target.parentNode; - return descend(scope.dom()); - }; + Tools.each(elms, function (elm) { + parentElm.insertBefore(elm, target); + }); - return { - first: first, - ancestor: ancestor, - closest: closest, - sibling: sibling, - child: child, - descendant: descendant + return findFirstIn(target, rootNode); }; - } -); -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 insertAfter = function (target, elms, rootNode, dom) { + dom.insertAfter(elms.reverse(), target); + return findLastOf(elms[0], rootNode); }; - var ancestor = function (scope, selector, isRoot) { - return PredicateFind.ancestor(scope, function (e) { - return Selectors.is(e, selector); - }, isRoot); - }; + var insertAtCaret = function (serializer, dom, rng, fragment) { + var domFragment = toDomFragment(dom, serializer, fragment); + var liTarget = getParentLi(dom, rng.startContainer); + var liElms = trimListItems(listItems(domFragment.firstChild)); + var BEGINNING = 1, END = 2; + var rootNode = dom.getRoot(); - var sibling = function (scope, selector) { - return PredicateFind.sibling(scope, function (e) { - return Selectors.is(e, selector); - }); - }; + var isAt = function (location) { + var caretPos = CaretPosition.fromRangeStart(rng); + var caretWalker = new CaretWalker(dom.getRoot()); + var newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos); - var child = function (scope, selector) { - return PredicateFind.child(scope, function (e) { - return Selectors.is(e, selector); - }); - }; + return newPos ? getParentLi(dom, newPos.getNode()) !== liTarget : true; + }; - var descendant = function (scope, selector) { - return Selectors.one(selector, scope); - }; + if (isAt(BEGINNING)) { + return insertBefore(liTarget, liElms, rootNode); + } else if (isAt(END)) { + return insertAfter(liTarget, liElms, rootNode, dom); + } - var closest = function (scope, selector, isRoot) { - return ClosestOrAncestor(Selectors.is, ancestor, scope, selector, isRoot); + return insertMiddle(liTarget, liElms, rootNode, rng); }; return { - first: first, - ancestor: ancestor, - sibling: sibling, - child: child, - descendant: descendant, - closest: closest + isListFragment: isListFragment, + insertAtCaret: insertAtCaret, + isParentBlockLi: isParentBlockLi, + trimListItems: trimListItems, + listItems: listItems }; } ); - /** - * Parents.js + * InsertContent.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -30116,348 +27913,407 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Handles inserts of contents into the editor instance. + * + * @class tinymce.InsertContent + * @private + */ define( - 'tinymce.core.dom.Parents', + 'tinymce.core.InsertContent', [ - 'ephox.katamari.api.Fun', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.search.Traverse' + 'ephox.katamari.api.Option', + 'ephox.sugar.api.node.Element', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.caret.CaretWalker', + 'tinymce.core.dom.ElementUtils', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.PaddingBr', + 'tinymce.core.dom.RangeNormalizer', + 'tinymce.core.Env', + 'tinymce.core.html.Serializer', + 'tinymce.core.InsertList', + 'tinymce.core.util.Tools' ], - function (Fun, Compare, Traverse) { - var dropLast = function (xs) { - return xs.slice(0, -1); - }; + function (Option, Element, CaretPosition, CaretWalker, ElementUtils, NodeType, PaddingBr, RangeNormalizer, Env, Serializer, InsertList, Tools) { + var isTableCell = NodeType.matchNodeNames('td th'); - 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); - })); + var validInsertion = function (editor, value, parentNode) { + // Should never insert content into bogus elements, since these can + // be resize handles or similar + if (parentNode.getAttribute('data-mce-bogus') === 'all') { + parentNode.parentNode.insertBefore(editor.dom.createFragment(value), parentNode); } else { - return []; + // Check if parent is empty or only has one BR element then set the innerHTML of that parent + var node = parentNode.firstChild; + var node2 = parentNode.lastChild; + if (!node || (node === node2 && node.nodeName === 'BR')) {/// + editor.dom.setHTML(parentNode, value); + } else { + editor.selection.setContent(value); + } } }; - var parents = function (startNode, rootElm) { - return parentsUntil(startNode, rootElm, Fun.constant(false)); + var trimBrsFromTableCell = function (dom, elm) { + Option.from(dom.getParent(elm, 'td,th')).map(Element.fromDom).each(PaddingBr.trimBlockTrailingBr); }; - var parentsAndSelf = function (startNode, rootElm) { - return [startNode].concat(parents(startNode, rootElm)); - }; + var insertHtmlAtCaret = function (editor, value, details) { + var parser, serializer, parentNode, rootNode, fragment, args; + var marker, rng, node, node2, bookmarkHtml, merge; + var textInlineElements = editor.schema.getTextInlineElements(); + var selection = editor.selection, dom = editor.dom; - return { - parentsUntil: parentsUntil, - parents: parents, - parentsAndSelf: parentsAndSelf - }; - } -); + function trimOrPaddLeftRight(html) { + var rng, container, offset; -define( - 'ephox.katamari.api.Options', + rng = selection.getRng(true); + container = rng.startContainer; + offset = rng.startOffset; - [ - 'ephox.katamari.api.Option' - ], + function hasSiblingText(siblingName) { + return container[siblingName] && container[siblingName].nodeType == 3; + } - 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; - }; + if (container.nodeType == 3) { + if (offset > 0) { + html = html.replace(/^ /, ' '); + } else if (!hasSiblingText('previousSibling')) { + html = html.replace(/^ /, ' '); + } - /** 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; + if (offset < container.length) { + html = html.replace(/ (
    |)$/, ' '); + } else if (!hasSiblingText('nextSibling')) { + html = html.replace(/( | )(
    |)$/, ' '); + } } - } - 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 html; } - return Option.some(f.apply(null, r)); - }; - return { - cat: cat, - findMap: findMap, - liftN: liftN - }; - } -); + // Removes   from a [b] c -> a  c -> a c + function trimNbspAfterDeleteAndPaddValue() { + var rng, container, offset; -/** - * 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 - */ + rng = selection.getRng(true); + container = rng.startContainer; + offset = rng.startOffset; -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); - } - }; + if (container.nodeType == 3 && rng.collapsed) { + if (container.data[offset] === '\u00a0') { + container.deleteData(offset, 1); - 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); + if (!/[\u00a0| ]$/.test(value)) { + value += ' '; + } + } else if (container.data[offset - 1] === '\u00a0') { + container.deleteData(offset - 1, 1); + + if (!/[\u00a0| ]$/.test(value)) { + value = ' ' + value; + } + } + } } - }; - var getFirstChildren = function (node) { - return Traverse.firstChild(node).fold( - Fun.constant([node]), - function (child) { - return [node].concat(getFirstChildren(child)); + function reduceInlineTextElements() { + if (merge) { + var root = editor.getBody(), elementUtils = new ElementUtils(dom); + + Tools.each(dom.select('*[data-mce-fragment]'), function (node) { + for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) { + if (textInlineElements[node.nodeName.toLowerCase()] && elementUtils.compare(testNode, node)) { + dom.remove(node, true); + } + } + }); } - ); - }; + } - 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)); + function markFragmentElements(fragment) { + var node = fragment; + + while ((node = node.walk())) { + if (node.type === 1) { + node.attr('data-mce-fragment', '1'); } } - ); - }; + } - 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); - }; + function umarkFragmentElements(elm) { + Tools.each(elm.getElementsByTagName('*'), function (elm) { + elm.removeAttribute('data-mce-fragment'); + }); + } - return { - hasAllContentsSelected: hasAllContentsSelected - }; - } -); + function isPartOfFragment(node) { + return !!node.getAttribute('data-mce-fragment'); + } -/** - * 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 - */ + function canHaveChildren(node) { + return node && !editor.schema.getShortEndedElements()[node.nodeName]; + } -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'); + function moveSelectionToMarker(marker) { + var parentEditableFalseElm, parentBlock, nextRng; - var getSpan = function (td, key) { - var value = parseInt(Attr.get(td, key), 10); - return isNaN(value) ? 1 : value; - }; + function getContentEditableFalseParent(node) { + var root = editor.getBody(); - var fillout = function (table, x, y, tr, td) { - var rowspan = getSpan(td, 'rowspan'); - var colspan = getSpan(td, 'colspan'); - var rows = table.rows(); + for (; node && node !== root; node = node.parentNode) { + if (editor.dom.getContentEditable(node) === 'false') { + return node; + } + } - for (var y2 = y; y2 < y + rowspan; y2++) { - if (!rows[y2]) { - rows[y2] = tableRow(Replication.deep(tr), []); + return null; } - 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); + if (!marker) { + return; } - } - }; - var cellExists = function (table, x, y) { - var rows = table.rows(); - var cells = rows[y] ? rows[y].cells() : []; - return !!cells[x]; - }; + selection.scrollIntoView(marker); - var skipCellsX = function (table, x, y) { - while (cellExists(table, x, y)) { - x++; - } + // If marker is in cE=false then move selection to that element instead + parentEditableFalseElm = getContentEditableFalseParent(marker); + if (parentEditableFalseElm) { + dom.remove(marker); + selection.select(parentEditableFalseElm); + return; + } - return x; - }; + // Move selection before marker and remove it + rng = dom.createRng(); - var getWidth = function (rows) { - return Arr.foldl(rows, function (acc, row) { - return row.cells().length > acc ? row.cells().length : acc; - }, 0); - }; + // If previous sibling is a text node set the selection to the end of that node + node = marker.previousSibling; + if (node && node.nodeType == 3) { + rng.setStart(node, node.nodeValue.length); - 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)); + // TODO: Why can't we normalize on IE + if (!Env.ie) { + node2 = marker.nextSibling; + if (node2 && node2.nodeType == 3) { + node.appendData(node2.data); + node2.parentNode.removeChild(node2); + } + } + } else { + // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node + rng.setStartBefore(marker); + rng.setEndBefore(marker); + } + + function findNextCaretRng(rng) { + var caretPos = CaretPosition.fromRangeStart(rng); + var caretWalker = new CaretWalker(editor.getBody()); + + caretPos = caretWalker.next(caretPos); + if (caretPos) { + return caretPos.toRange(); + } + } + + // Remove the marker node and set the new range + parentBlock = dom.getParent(marker, dom.isBlock); + dom.remove(marker); + + if (parentBlock && dom.isEmpty(parentBlock)) { + editor.$(parentBlock).empty(); + + rng.setStart(parentBlock, 0); + rng.setEnd(parentBlock, 0); + + if (!isTableCell(parentBlock) && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) { + rng = nextRng; + dom.remove(parentBlock); + } else { + dom.add(parentBlock, dom.create('br', { 'data-mce-bogus': '1' })); } } + + selection.setRng(rng); } - return Option.none(); - }; + // Check for whitespace before/after value + if (/^ | $/.test(value)) { + value = trimOrPaddLeftRight(value); + } - var extractRows = function (table, sx, sy, ex, ey) { - var newRows = []; - var rows = table.rows(); + // Setup parser and serializer + parser = editor.parser; + merge = details.merge; - 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)); + 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, paste: details.paste }; + args = editor.fire('BeforeSetContent', args); + if (args.isDefaultPrevented()) { + editor.fire('SetContent', { content: args.content, format: 'html', selection: true, paste: details.paste }); + return; } - return newRows; - }; + value = args.content; - 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); + // Add caret at end of contents if it's missing + if (value.indexOf('{$caret}') == -1) { + value += '{$caret}'; + } - return tableModel(table.element(), getWidth(newRows), newRows); - }; + // Replace the caret marker with a span bookmark element + value = value.replace(/\{\$caret\}/, bookmarkHtml); - var createDomTable = function (table, rows) { - var tableElement = Replication.shallow(table.element()); - var tableBody = Element.fromTag('tbody'); + // 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); + } + } - InsertAll.append(tableBody, rows); - Insert.append(tableElement, tableBody); + // 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(); + } - return tableElement; - }; + parentNode = selection.getNode(); - 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; - }); + // Parse the fragment within the context of the parent node + var parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data }; + fragment = parser.parse(value, parserArgs); - var tr = Replication.shallow(row.element()); - InsertAll.append(tr, cells); - return tr; - }); - }; + // 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; + } - var fromDom = function (tableElm) { - var table = tableModel(Replication.shallow(tableElm), 0, []); + markFragmentElements(fragment); - 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); - }); - }); + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') == 'mce_marker') { + marker = node; - return tableModel(table.element(), getWidth(table.rows()), table.rows()); + 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 toDom = function (table) { - return createDomTable(table, modelRowsToDomRows(table)); + 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 subsection = function (table, startElement, endElement) { - return findElementPos(table, startElement).bind(function (startPos) { - return findElementPos(table, endElement).map(function (endPos) { - return subTable(table, startPos, endPos); - }); - }); + var insertAtCaret = function (editor, value) { + var result = processValue(value); + insertHtmlAtCaret(editor, result.content, result.details); }; return { - fromDom: fromDom, - toDom: toDom, - subsection: subsection + insertAtCaret: insertAtCaret }; } ); - /** - * MultiRange.js + * DeleteUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -30467,92 +28323,92 @@ define( */ define( - 'tinymce.core.selection.MultiRange', + 'tinymce.core.delete.DeleteUtils', [ - 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', 'ephox.sugar.api.node.Element', - 'tinymce.core.dom.RangeUtils' + 'ephox.sugar.api.search.PredicateFind', + 'tinymce.core.dom.ElementType' ], - function (Arr, Element, RangeUtils) { - var getRanges = function (selection) { - var ranges = []; - - for (var i = 0; i < selection.rangeCount; i++) { - ranges.push(selection.getRangeAt(i)); - } + function (Option, Compare, Element, PredicateFind, ElementType) { + var isBeforeRoot = function (rootNode) { + return function (elm) { + return Compare.eq(rootNode, Element.fromDom(elm.dom().parentNode)); + }; + }; - return ranges; + var getParentBlock = function (rootNode, elm) { + return Compare.contains(rootNode, elm) ? PredicateFind.closest(elm, function (element) { + return ElementType.isTextBlock(element) || ElementType.isListItem(element); + }, isBeforeRoot(rootNode)) : Option.none(); }; - var getSelectedNodes = function (ranges) { - return Arr.bind(ranges, function (range) { - var node = RangeUtils.getSelectedNode(range); - return node ? [ Element.fromDom(node) ] : []; - }); + var placeCaretInEmptyBody = function (editor) { + var body = editor.getBody(); + var node = body.firstChild && editor.dom.isBlock(body.firstChild) ? body.firstChild : body; + editor.selection.setCursorLocation(node, 0); }; - var hasMultipleRanges = function (selection) { - return getRanges(selection).length > 1; + var paddEmptyBody = function (editor) { + if (editor.dom.isEmpty(editor.getBody())) { + editor.setContent(''); + placeCaretInEmptyBody(editor); + } }; return { - getRanges: getRanges, - getSelectedNodes: getSelectedNodes, - hasMultipleRanges: hasMultipleRanges + getParentBlock: getParentBlock, + paddEmptyBody: paddEmptyBody }; } ); -/** - * 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.sugar.api.search.SelectorExists', + [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.SelectorFilter', - 'tinymce.core.dom.ElementType', - 'tinymce.core.selection.MultiRange' + 'ephox.sugar.api.search.SelectorFind' ], - function (Arr, Element, SelectorFilter, ElementType, MultiRange) { - var getCellsFromRanges = function (ranges) { - return Arr.filter(MultiRange.getSelectedNodes(ranges), ElementType.isTableCell); + + function (SelectorFind) { + var any = function (selector) { + return SelectorFind.first(selector).isSome(); }; - var getCellsFromElement = function (elm) { - var selectedCells = SelectorFilter.descendants(elm, 'td[data-mce-selected],th[data-mce-selected]'); - return selectedCells; + var ancestor = function (scope, selector, isRoot) { + return SelectorFind.ancestor(scope, selector, isRoot).isSome(); }; - var getCellsFromElementOrRanges = function (ranges, element) { - var selectedCells = getCellsFromElement(element); - var rangeCells = getCellsFromRanges(ranges); - return selectedCells.length > 0 ? selectedCells : rangeCells; + var sibling = function (scope, selector) { + return SelectorFind.sibling(scope, selector).isSome(); }; - var getCellsFromEditor = function (editor) { - return getCellsFromElementOrRanges(MultiRange.getRanges(editor.selection.getSel()), Element.fromDom(editor.getBody())); + 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 { - getCellsFromRanges: getCellsFromRanges, - getCellsFromElement: getCellsFromElement, - getCellsFromElementOrRanges: getCellsFromElementOrRanges, - getCellsFromEditor: getCellsFromEditor + any: any, + ancestor: ancestor, + sibling: sibling, + child: child, + descendant: descendant, + closest: closest }; } ); /** - * FragmentReader.js + * Empty.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -30562,120 +28418,91 @@ define( */ define( - 'tinymce.core.selection.FragmentReader', + 'tinymce.core.dom.Empty', [ - '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' + 'ephox.sugar.api.search.SelectorExists', + 'tinymce.core.caret.CaretCandidate', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.TreeWalker' ], - 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'; - }); + 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 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 isWhitespace = function (rootNode, node) { + return NodeType.isText(node) && /^[ \t\r\n]*$/.test(node.data) && hasWhitespacePreserveParent(rootNode, node) === false; }; - 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 isNamedAnchor = function (node) { + return NodeType.isElement(node) && node.nodeName === 'A' && node.hasAttribute('name'); }; - 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 isContent = function (rootNode, node) { + return (CaretCandidate.isCaretCandidate(node) && isWhitespace(rootNode, node) === false) || isNamedAnchor(node) || isBookmark(node); }; - 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 isBookmark = NodeType.hasAttribute('data-mce-bookmark'); + var isBogus = NodeType.hasAttribute('data-mce-bogus'); + var isBogusAll = NodeType.hasAttributeValue('data-mce-bogus', 'all'); - var emptyFragment = function () { - return Fragment.fromElements([]); - }; + var isEmptyNode = function (targetNode) { + var walker, node, brCount = 0; - var getFragmentFromRange = function (rootNode, rng) { - return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng)); - }; + if (isContent(targetNode, targetNode)) { + return false; + } else { + node = targetNode.firstChild; + if (!node) { + return true; + } - var getParentTable = function (rootElm, cell) { - return SelectorFind.ancestor(cell, 'table', Fun.curry(Compare.eq, rootElm)); - }; + walker = new TreeWalker(node, targetNode); + do { + if (isBogusAll(node)) { + node = walker.next(true); + continue; + } - 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); + if (isBogus(node)) { + node = walker.next(); + continue; + } - return SimpleTableModel.subsection(fullTableModel, firstCell, lastCell).map(function (sectionedTableModel) { - return Fragment.fromElements([SimpleTableModel.toDom(sectionedTableModel)]); - }); - }).getOrThunk(emptyFragment); - }; + if (NodeType.isBr(node)) { + brCount++; + node = walker.next(); + continue; + } - var getSelectionFragment = function (rootNode, ranges) { - return ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0]); + if (isContent(targetNode, node)) { + return false; + } + + node = walker.next(); + } while (node); + + return brCount <= 1; + } }; - var read = function (rootNode, ranges) { - var selectedCells = TableCellSelection.getCellsFromElementOrRanges(ranges, rootNode); - return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges); + var isEmpty = function (elm) { + return isEmptyNode(elm.dom()); }; return { - read: read + isEmpty: isEmpty }; } ); /** - * Selection.js + * BlockBoundary.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -30684,1025 +28511,1143 @@ define( * 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', + '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.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' + 'tinymce.core.delete.DeleteUtils', + 'tinymce.core.dom.Empty', + 'tinymce.core.dom.NodeType' ], - 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; + 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 isAttachedToDom = function (node) { - return !!(node && node.ownerDocument) && Compare.contains(Element.fromDom(node.ownerDocument), Element.fromDom(node)); + var getBlockPosition = function (rootNode, pos) { + var rootElm = Element.fromDom(rootNode); + var containerElm = Element.fromDom(pos.container()); + return DeleteUtils.getParentBlock(rootElm, containerElm).map(function (block) { + return BlockPosition(block, pos); + }); }; - var isValidRange = function (rng) { - if (!rng) { - return false; - } else if (rng.select) { // Native IE range still produced by placeCaretAt - return true; + 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 isAttachedToDom(rng.startContainer) && isAttachedToDom(rng.endContainer); + return blockPosition; } }; - 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; + 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); + }); + }); }); - }; - /** - * 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; + return Options.liftN([fromBlockPos, toBlockPos], BlockBoundary).filter(function (blockBoundary) { + return isDifferentBlocks(blockBoundary) && hasSameParent(blockBoundary) && isEditable(blockBoundary); + }); + }; - self.dom = dom; - self.win = win; - self.serializer = serializer; - self.editor = editor; - self.bookmarkManager = new BookmarkManager(self); - self.controlSelection = new ControlSelection(self, editor); + var read = function (rootNode, forward, rng) { + return rng.collapsed ? readFromRange(rootNode, forward, rng) : Option.none(); + }; - // No W3C Range support - if (!self.win.getSelection) { - self.tridentSel = new TridentSelection(self); - } - } + return { + read: read + }; + } +); - 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(); +/** + * 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 + */ - 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); +define( + 'tinymce.core.delete.MergeBlocks', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Option', + 'ephox.sugar.api.dom.Compare', + '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.ElementType', + 'tinymce.core.dom.Empty', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.Parents' + ], + function (Arr, Option, Compare, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, ElementType, Empty, NodeType, Parents) { + var getChildrenUntilBlockBoundary = function (block) { + var children = Traverse.children(block); + return Arr.findIndex(children, ElementType.isBlock).fold( + function () { + return children; + }, + function (index) { + return children.slice(0, index); } - }, - - /** - * 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); + var extractChildren = function (block) { + var children = getChildrenUntilBlockBoundary(block); - if (args.format === 'text') { - return self.isCollapsed() ? '' : Zwsp.trim(rng.text || (se.toString ? se.toString() : '')); - } + Arr.each(children, function (node) { + Remove.remove(node); + }); - 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 = '
    ' + (rng.item ? rng.item(0).outerHTML : rng.htmlText); - tmpElm.removeChild(tmpElm.firstChild); - } else { - tmpElm.innerHTML = rng.toString(); - } + return children; + }; - // Keep whitespace before and after - if (/^\s/.test(tmpElm.innerHTML)) { - whiteSpaceBefore = ' '; + var trimBr = function (first, block) { + CaretFinder.positionIn(first, block.dom()).each(function (position) { + var node = position.getNode(); + if (NodeType.isBr(node)) { + Remove.remove(Element.fromDom(node)); } + }); + }; - if (/\s+$/.test(tmpElm.innerHTML)) { - whiteSpaceAfter = ' '; - } + var removeEmptyRoot = function (rootNode, block) { + var parents = Parents.parentsAndSelf(block, rootNode); + return Arr.find(parents.reverse(), Empty.isEmpty).each(Remove.remove); + }; - args.getInner = true; + var findParentInsertPoint = function (toBlock, block) { + var parents = Traverse.parents(block, function (elm) { + return Compare.eq(elm, toBlock); + }); - args.content = self.isCollapsed() ? '' : whiteSpaceBefore + self.serializer.serialize(tmpElm, args) + whiteSpaceAfter; - self.editor.fire('GetContent', args); + return Option.from(parents[parents.length - 2]); + }; - return args.content; - }, + var getInsertionPoint = function (fromBlock, toBlock) { + if (Compare.contains(toBlock, fromBlock)) { + return Traverse.parent(fromBlock).bind(function (parent) { + return Compare.eq(parent, toBlock) ? Option.some(fromBlock) : findParentInsertPoint(toBlock, fromBlock); + }); + } else { + return Option.none(); + } + }; - /** - * Sets the current selection to the specified content. If any contents is selected it will be replaced - * with the contents passed in to this function. If there is no selection the contents will be inserted - * where the caret is placed in the editor/page. - * - * @method setContent - * @param {String} content HTML contents to set could also be other formats depending on settings. - * @param {Object} args Optional settings object with for example data format. - * @example - * // Inserts some HTML contents at the current selection - * tinymce.activeEditor.selection.setContent('Some contents'); - */ - setContent: function (content, args) { - var self = this, rng = self.getRng(), caretNode, doc = self.win.document, frag, temp; + var mergeBlockInto = function (rootNode, fromBlock, toBlock) { + if (Empty.isEmpty(toBlock)) { + Remove.remove(toBlock); + return CaretFinder.firstPositionIn(fromBlock.dom()); + } else { + trimBr(true, fromBlock); + trimBr(false, toBlock); - args = args || { format: 'html' }; - args.set = true; - args.selection = true; - args.content = content; + var children = extractChildren(fromBlock); - // Dispatch before set content event - if (!args.no_events) { - self.editor.fire('BeforeSetContent', args); - } + return getInsertionPoint(fromBlock, toBlock).fold( + function () { + removeEmptyRoot(rootNode, fromBlock); - content = args.content; + var position = CaretFinder.lastPositionIn(toBlock.dom()); - if (rng.insertNode) { - // Make caret marker since insertNode places the caret in the beginning of text after insert - content += '_'; + Arr.each(children, function (node) { + Insert.append(toBlock, node); + }); - // Delete and insert new node - if (rng.startContainer == doc && rng.endContainer == doc) { - // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents - doc.body.innerHTML = content; - } else { - rng.deleteContents(); + return position; + }, + function (target) { + var position = CaretFinder.prevPosition(toBlock.dom(), CaretPosition.before(target.dom())); - if (doc.body.childNodes.length === 0) { - doc.body.innerHTML = content; - } else { - // createContextualFragment doesn't exists in IE 9 DOMRanges - if (rng.createContextualFragment) { - rng.insertNode(rng.createContextualFragment(content)); - } else { - // Fake createContextualFragment call in IE 9 - frag = doc.createDocumentFragment(); - temp = doc.createElement('div'); + Arr.each(children, function (node) { + Insert.before(target, node); + }); - frag.appendChild(temp); - temp.outerHTML = content; + removeEmptyRoot(rootNode, fromBlock); - rng.insertNode(frag); - } - } + return position; } + ); + } + }; - // Move to caret marker - caretNode = self.dom.get('__caret'); + var mergeBlocks = function (rootNode, forward, block1, block2) { + return forward ? mergeBlockInto(rootNode, block2, block1) : mergeBlockInto(rootNode, block1, block2); + }; - // Make sure we wrap it compleatly, Opera fails with a simple select call - rng = doc.createRange(); - rng.setStartBefore(caretNode); - rng.setEndBefore(caretNode); - self.setRng(rng); + return { + mergeBlocks: mergeBlocks + }; + } +); - // Remove the caret position - self.dom.remove('__caret'); +/** + * 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 + */ - try { - self.setRng(rng); - } catch (ex) { - // Might fail on Opera for some odd reason - } - } else { - if (rng.item) { - // Delete content and get caret text selection - doc.execCommand('Delete', false, null); - rng = self.getRng(); - } +define( + 'tinymce.core.delete.BlockBoundaryDelete', + [ + 'ephox.sugar.api.node.Element', + 'tinymce.core.delete.BlockBoundary', + 'tinymce.core.delete.MergeBlocks' + ], + function (Element, BlockBoundary, MergeBlocks) { + var backspaceDelete = function (editor, forward) { + var position, rootNode = Element.fromDom(editor.getBody()); - // Explorer removes spaces from the beginning of pasted contents - if (/^\s+/.test(content)) { - rng.pasteHTML('_' + content); - self.dom.remove('__mce_tmp'); - } else { - rng.pasteHTML(content); - } - } + position = BlockBoundary.read(rootNode.dom(), forward, editor.selection.getRng()).bind(function (blockBoundary) { + return MergeBlocks.mergeBlocks(rootNode, forward, blockBoundary.from().block(), blockBoundary.to().block()); + }); - // Dispatch set content event - if (!args.no_events) { - self.editor.fire('SetContent', args); - } - }, + position.each(function (pos) { + editor.selection.setRng(pos.toRange()); + }); - /** - * Returns the start element of a selection range. If the start is in a text - * node the parent element will be returned. - * - * @method getStart - * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. - * @return {Element} Start element of selection range. - */ - getStart: function (real) { - var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; + return position.isSome(); + }; - if (rng.duplicate || rng.item) { - // Control selection, return first item - if (rng.item) { - return rng.item(0); - } + return { + backspaceDelete: backspaceDelete + }; + } +); - // Get start element - checkRng = rng.duplicate(); - checkRng.collapse(1); - startElement = checkRng.parentElement(); - if (startElement.ownerDocument !== self.dom.doc) { - startElement = self.dom.getRoot(); - } +/** + * 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 + */ - // Check if range parent is inside the start element, then return the inner parent element - // This will fix issues when a single element is selected, IE would otherwise return the wrong start element - parentElement = node = rng.parentElement(); - while ((node = node.parentNode)) { - if (node == startElement) { - startElement = parentElement; - break; - } - } +define( + 'tinymce.core.delete.BlockRangeDelete', + [ + 'ephox.katamari.api.Options', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.delete.DeleteUtils', + 'tinymce.core.delete.MergeBlocks' + ], + function (Options, Compare, Element, CaretFinder, CaretPosition, DeleteUtils, MergeBlocks) { + var deleteRangeMergeBlocks = function (rootNode, selection) { + var rng = selection.getRng(); - return startElement; - } + return Options.liftN([ + DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.startContainer)), + DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.endContainer)) + ], function (block1, block2) { + if (Compare.eq(block1, block2) === false) { + rng.deleteContents(); - startElement = rng.startContainer; + MergeBlocks.mergeBlocks(rootNode, true, block1, block2).each(function (pos) { + selection.setRng(pos.toRange()); + }); - if (startElement.nodeType == 1 && startElement.hasChildNodes()) { - if (!real || !rng.collapsed) { - startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)]; - } + return true; + } else { + return false; } + }).getOr(false); + }; - if (startElement && startElement.nodeType == 3) { - return startElement.parentNode; - } + var isEverythingSelected = function (rootNode, rng) { + var noPrevious = CaretFinder.prevPosition(rootNode.dom(), CaretPosition.fromRangeStart(rng)).isNone(); + var noNext = CaretFinder.nextPosition(rootNode.dom(), CaretPosition.fromRangeEnd(rng)).isNone(); + return noPrevious && noNext; + }; - return startElement; - }, + var emptyEditor = function (editor) { + editor.setContent(''); + editor.selection.setCursorLocation(); + return true; + }; - /** - * Returns the end element of a selection range. If the end is in a text - * node the parent element will be returned. - * - * @method getEnd - * @param {Boolean} real Optional state to get the real parent when the selection is collapsed not the closest element. - * @return {Element} End element of selection range. - */ - getEnd: function (real) { - var self = this, rng = self.getRng(), endElement, endOffset; + var deleteRange = function (editor) { + var rootNode = Element.fromDom(editor.getBody()); + var rng = editor.selection.getRng(); + return isEverythingSelected(rootNode, rng) ? emptyEditor(editor) : deleteRangeMergeBlocks(rootNode, editor.selection); + }; - if (rng.duplicate || rng.item) { - if (rng.item) { - return rng.item(0); - } + var backspaceDelete = function (editor, forward) { + return editor.selection.isCollapsed() ? false : deleteRange(editor, editor.selection.getRng()); + }; - rng = rng.duplicate(); - rng.collapse(0); - endElement = rng.parentElement(); - if (endElement.ownerDocument !== self.dom.doc) { - endElement = self.dom.getRoot(); - } + return { + backspaceDelete: backspaceDelete + }; + } +); - if (endElement && endElement.nodeName == 'BODY') { - return endElement.lastChild || endElement; - } +/** + * 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 + */ - return endElement; - } +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' ] } + ]); - endElement = rng.endContainer; - endOffset = rng.endOffset; + 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; + }; - if (endElement.nodeType == 1 && endElement.hasChildNodes()) { - if (!real || !rng.collapsed) { - endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; - } - } + var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) { + var toCefElm = to.getNode(forward === false); + return DeleteUtils.getParentBlock(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)); + }); + }; - if (endElement && endElement.nodeType == 3) { - return endElement.parentNode; + 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(); } + }); + }; - return endElement; - }, - - /** - * Returns a bookmark location for the current selection. This bookmark object - * can then be used to restore the selection after some content modification to the document. - * - * @method getBookmark - * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex. - * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization. - * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection. - * @example - * // Stores a bookmark of the current selection - * var bm = tinymce.activeEditor.selection.getBookmark(); - * - * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); - * - * // Restore the selection bookmark - * tinymce.activeEditor.selection.moveToBookmark(bm); - */ - getBookmark: function (type, normalized) { - return this.bookmarkManager.getBookmark(type, normalized); - }, - - /** - * Restores the selection to the specified bookmark. - * - * @method moveToBookmark - * @param {Object} bookmark Bookmark to restore selection from. - * @return {Boolean} true/false if it was successful or not. - * @example - * // Stores a bookmark of the current selection - * var bm = tinymce.activeEditor.selection.getBookmark(); - * - * tinymce.activeEditor.setContent(tinymce.activeEditor.getContent() + 'Some new content'); - * - * // Restore the selection bookmark - * tinymce.activeEditor.selection.moveToBookmark(bm); - */ - moveToBookmark: function (bookmark) { - return this.bookmarkManager.moveToBookmark(bookmark); - }, + 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(); + } + }; - /** - * Selects the specified element. This will place the start and end of the selection range around the element. - * - * @method select - * @param {Element} node HTML DOM element to select. - * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser. - * @return {Element} Selected element the same element as the one that got passed in. - * @example - * // Select the first paragraph in the active editor - * tinymce.activeEditor.selection.select(tinymce.activeEditor.dom.select('p')[0]); - */ - select: function (node, content) { - var self = this, dom = self.dom, rng = dom.createRng(), idx; + 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); + } + }; - // Clear stored range set by FocusManager - self.lastFocusBookmark = null; + var read = function (rootNode, forward, rng) { + var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng); + var from = CaretPosition.fromRangeStart(normalizedRange); - if (node) { - if (!content && self.controlSelection.controlSelect(node)) { - return; - } + 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); + } + }; - idx = dom.nodeIndex(node); - rng.setStart(node.parentNode, idx); - rng.setEnd(node.parentNode, idx + 1); + return { + read: read + }; + } +); - // Find first/last text node or BR element - if (content) { - self._moveEndPoint(rng, node, true); - self._moveEndPoint(rng, node); - } +/** + * 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 + */ - self.setRng(rng); - } +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.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.Empty', + 'tinymce.core.dom.NodeType' + ], + function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretFinder, CaretPosition, Empty, NodeType) { + 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(); + }; - return node; - }, + var reposition = function (elm, pos) { + return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos; + }; - /** - * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection. - * - * @method isCollapsed - * @return {Boolean} true/false state if the selection range is collapsed or not. - * Collapsed means if it's a caret or a larger selection. - */ - isCollapsed: function () { - var self = this, rng = self.getRng(), sel = self.getSel(); + var beforeOrStartOf = function (node) { + return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node); + }; - if (!rng || rng.item) { - return false; - } + var afterOrEndOf = function (node) { + return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node); + }; - if (rng.compareEndPoints) { - return rng.compareEndPoints('StartToEnd', rng) === 0; - } + var getPreviousSiblingCaretPosition = function (elm) { + if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { + return Option.some(afterOrEndOf(elm.previousSibling)); + } else { + return elm.previousSibling ? CaretFinder.lastPositionIn(elm.previousSibling) : Option.none(); + } + }; - return !sel || rng.collapsed; - }, + var getNextSiblingCaretPosition = function (elm) { + if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { + return Option.some(beforeOrStartOf(elm.nextSibling)); + } else { + return elm.nextSibling ? CaretFinder.firstPositionIn(elm.nextSibling) : Option.none(); + } + }; - /** - * Collapse the selection to start or end of range. - * - * @method collapse - * @param {Boolean} toStart Optional boolean state if to collapse to end or not. Defaults to false. - */ - collapse: function (toStart) { - var self = this, rng = self.getRng(), node; + var findCaretPositionBackwardsFromElm = function (rootElement, elm) { + var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode); + return CaretFinder.prevPosition(rootElement, startPosition).fold( + function () { + return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)); + }, + Option.some + ); + }; - // Control range on IE - if (rng.item) { - node = rng.item(0); - rng = self.win.document.body.createTextRange(); - rng.moveToElementText(node); - } + var findCaretPositionForwardsFromElm = function (rootElement, elm) { + return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)).fold( + function () { + return CaretFinder.prevPosition(rootElement, CaretPosition.before(elm)); + }, + Option.some + ); + }; - rng.collapse(!!toStart); - self.setRng(rng); - }, + var findCaretPositionBackwards = function (rootElement, elm) { + return getPreviousSiblingCaretPosition(elm).orThunk(function () { + return getNextSiblingCaretPosition(elm); + }).orThunk(function () { + return findCaretPositionBackwardsFromElm(rootElement, elm); + }); + }; - /** - * Returns the browsers internal selection object. - * - * @method getSel - * @return {Selection} Internal browser selection object. - */ - getSel: function () { - var win = this.win; + var findCaretPositionForward = function (rootElement, elm) { + return getNextSiblingCaretPosition(elm).orThunk(function () { + return getPreviousSiblingCaretPosition(elm); + }).orThunk(function () { + return findCaretPositionForwardsFromElm(rootElement, elm); + }); + }; - return win.getSelection ? win.getSelection() : win.document.selection; - }, + var findCaretPosition = function (forward, rootElement, elm) { + return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); + }; - /** - * Returns the browsers internal range object. - * - * @method getRng - * @param {Boolean} w3c Forces a compatible W3C range on IE. - * @return {Range} Internal browser range object. - * @see http://www.quirksmode.org/dom/range_intro.html - * @see http://www.dotvoid.com/2001/03/using-the-range-object-in-mozilla/ - */ - getRng: function (w3c) { - var self = this, selection, rng, elm, doc, ieRng; + var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) { + return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); + }; - function tryCompareBoundaryPoints(how, sourceRange, destinationRange) { - try { - return sourceRange.compareBoundaryPoints(how, destinationRange); - } catch (ex) { - // Gecko throws wrong document exception if the range points - // to nodes that where removed from the dom #6690 - // Browsers should mutate existing DOMRange instances so that they always point - // to something in the document this is not the case in Gecko works fine in IE/WebKit/Blink - // For performance reasons just return -1 - return -1; - } + var setSelection = function (editor, forward, pos) { + pos.fold( + function () { + editor.focus(); + }, + function (pos) { + editor.selection.setRng(pos.toRange(), forward); } + ); + }; - if (!self.win) { - return null; - } + var eqRawNode = function (rawNode) { + return function (elm) { + return elm.dom() === rawNode; + }; + }; - doc = self.win.document; + var isBlock = function (editor, elm) { + return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm)); + }; - if (typeof doc === 'undefined' || doc === null) { - return null; - } + var paddEmptyBlock = function (elm) { + if (Empty.isEmpty(elm)) { + var br = Element.fromHtml('
    '); + Remove.empty(elm); + Insert.append(elm, br); + return Option.some(CaretPosition.before(br.dom())); + } else { + return Option.none(); + } + }; - // Use last rng passed from FocusManager if it's available this enables - // calls to editor.selection.getStart() to work when caret focus is lost on IE - if (!w3c && self.lastFocusBookmark) { - var bookmark = self.lastFocusBookmark; + // When deleting an element between two text nodes IE 11 doesn't automatically merge the adjacent text nodes + var deleteNormalized = function (elm, afterDeletePosOpt) { + return Options.liftN([Traverse.prevSibling(elm), Traverse.nextSibling(elm), afterDeletePosOpt], function (prev, next, afterDeletePos) { + var offset, prevNode = prev.dom(), nextNode = next.dom(); - // Convert bookmark to range IE 11 fix - if (bookmark.startContainer) { - rng = doc.createRange(); - rng.setStart(bookmark.startContainer, bookmark.startOffset); - rng.setEnd(bookmark.endContainer, bookmark.endOffset); + if (NodeType.isText(prevNode) && NodeType.isText(nextNode)) { + offset = prevNode.data.length; + prevNode.appendData(nextNode.data); + Remove.remove(next); + Remove.remove(elm); + if (afterDeletePos.container() === nextNode) { + return new CaretPosition(prevNode, offset); } else { - rng = bookmark; + return afterDeletePos; } - - return rng; + } else { + Remove.remove(elm); + return afterDeletePos; } + }).orThunk(function () { + Remove.remove(elm); + return afterDeletePosOpt; + }); + }; - // Found tridentSel object then we need to use that one - if (w3c && self.tridentSel) { - return self.tridentSel.getRangeAt(0); - } + var deleteElement = function (editor, forward, elm) { + var afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom()); + var parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody())); + var normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos); - try { - if ((selection = self.getSel())) { - if (selection.rangeCount > 0) { - rng = selection.getRangeAt(0); - } else { - rng = selection.createRange ? selection.createRange() : doc.createRange(); - } - } - } catch (ex) { - // IE throws unspecified error here if TinyMCE is placed in a frame/iframe + parentBlock.bind(paddEmptyBlock).fold( + function () { + setSelection(editor, forward, normalizedAfterDeletePos); + }, + function (paddPos) { + setSelection(editor, forward, Option.some(paddPos)); } + ); + }; - rng = eventProcessRanges(self.editor, [ rng ])[0]; + return { + deleteElement: deleteElement + }; + } +); +/** + * CefDelete.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 + */ - // We have W3C ranges and it's IE then fake control selection since IE9 doesn't handle that correctly yet - // IE 11 doesn't support the selection object so we check for that as well - if (isIE && rng && rng.setStart && doc.selection) { - try { - // IE will sometimes throw an exception here - ieRng = doc.selection.createRange(); - } catch (ex) { - // Ignore - } +define( + 'tinymce.core.delete.CefDelete', + [ + 'ephox.katamari.api.Arr', + 'ephox.sugar.api.dom.Remove', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.SelectorFilter', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.delete.CefDeleteAction', + 'tinymce.core.delete.DeleteElement', + 'tinymce.core.delete.DeleteUtils', + 'tinymce.core.dom.NodeType' + ], + function (Arr, Remove, Element, SelectorFilter, CaretPosition, CefDeleteAction, DeleteElement, DeleteUtils, NodeType) { + var deleteElement = function (editor, forward) { + return function (element) { + DeleteElement.deleteElement(editor, forward, Element.fromDom(element)); + return true; + }; + }; - if (ieRng && ieRng.item) { - elm = ieRng.item(0); - rng = doc.createRange(); - rng.setStartBefore(elm); - rng.setEndAfter(elm); - } - } + var moveToElement = function (editor, forward) { + return function (element) { + var pos = forward ? CaretPosition.before(element) : CaretPosition.after(element); + editor.selection.setRng(pos.toRange()); + return true; + }; + }; - // No range found then create an empty one - // This can occur when the editor is placed in a hidden container element on Gecko - // Or on IE when there was an exception - if (!rng) { - rng = doc.createRange ? doc.createRange() : doc.body.createTextRange(); - } + var moveToPosition = function (editor) { + return function (pos) { + editor.selection.setRng(pos.toRange()); + return true; + }; + }; - // If range is at start of document then move it to start of body - if (rng.setStart && rng.startContainer.nodeType === 9 && rng.collapsed) { - elm = self.dom.getRoot(); - rng.setStart(elm, 0); - rng.setEnd(elm, 0); - } + var backspaceDeleteCaret = function (editor, forward) { + var result = CefDeleteAction.read(editor.getBody(), forward, editor.selection.getRng()).map(function (deleteAction) { + return deleteAction.fold( + deleteElement(editor, forward), + moveToElement(editor, forward), + moveToPosition(editor) + ); + }); - if (self.selectedRange && self.explicitRange) { - if (tryCompareBoundaryPoints(rng.START_TO_START, rng, self.selectedRange) === 0 && - tryCompareBoundaryPoints(rng.END_TO_END, rng, self.selectedRange) === 0) { - // Safari, Opera and Chrome only ever select text which causes the range to change. - // This lets us use the originally set range if the selection hasn't been changed by the user. - rng = self.explicitRange; - } else { - self.selectedRange = null; - self.explicitRange = null; - } - } + return result.getOr(false); + }; - return rng; - }, + var deleteOffscreenSelection = function (rootElement) { + Arr.each(SelectorFilter.descendants(rootElement, '.mce-offscreen-selection'), Remove.remove); + }; - /** - * Changes the selection to the specified DOM range. - * - * @method setRng - * @param {Range} rng Range to select. - * @param {Boolean} forward Optional boolean if the selection is forwards or backwards. - */ - setRng: function (rng, forward) { - var self = this, sel, node, evt; + var backspaceDeleteRange = function (editor, forward) { + var selectedElement = editor.selection.getNode(); + if (NodeType.isContentEditableFalse(selectedElement)) { + deleteOffscreenSelection(Element.fromDom(editor.getBody())); + DeleteElement.deleteElement(editor, forward, Element.fromDom(editor.selection.getNode())); + DeleteUtils.paddEmptyBody(editor); + return true; + } else { + return false; + } + }; - if (!isValidRange(rng)) { - return; + var getContentEditableRoot = function (root, node) { + while (node && node !== root) { + if (NodeType.isContentEditableTrue(node) || NodeType.isContentEditableFalse(node)) { + return node; } - // Is IE specific range - if (rng.select) { - self.explicitRange = null; - - try { - rng.select(); - } catch (ex) { - // Needed for some odd IE bug #1843306 - } + node = node.parentNode; + } - return; - } + return null; + }; - if (!self.tridentSel) { - sel = self.getSel(); + var paddEmptyElement = function (editor) { + var br, ceRoot = getContentEditableRoot(editor.getBody(), editor.selection.getNode()); - evt = self.editor.fire('SetSelectionRange', { range: rng, forward: forward }); - rng = evt.range; + if (NodeType.isContentEditableTrue(ceRoot) && editor.dom.isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) { + br = editor.dom.create('br', { "data-mce-bogus": "1" }); + editor.dom.setHTML(ceRoot, ''); + ceRoot.appendChild(br); + editor.selection.setRng(CaretPosition.before(br).toRange()); + } - if (sel) { - self.explicitRange = rng; + return true; + }; - try { - sel.removeAllRanges(); - sel.addRange(rng); - } catch (ex) { - // IE might throw errors here if the editor is within a hidden container and selection is changed - } - - // Forward is set to false and we have an extend function - if (forward === false && sel.extend) { - sel.collapse(rng.endContainer, rng.endOffset); - sel.extend(rng.startContainer, rng.startOffset); - } - - // adding range isn't always successful so we need to check range count otherwise an exception can occur - self.selectedRange = sel.rangeCount > 0 ? sel.getRangeAt(0) : null; - } - - // WebKit egde case selecting images works better using setBaseAndExtent when the image is floated - if (!rng.collapsed && rng.startContainer === rng.endContainer && sel.setBaseAndExtent && !Env.ie) { - if (rng.endOffset - rng.startOffset < 2) { - if (rng.startContainer.hasChildNodes()) { - node = rng.startContainer.childNodes[rng.startOffset]; - if (node && node.tagName === 'IMG') { - sel.setBaseAndExtent( - rng.startContainer, - rng.startOffset, - rng.endContainer, - rng.endOffset - ); - - // Since the setBaseAndExtent is fixed in more recent Blink versions we - // need to detect if it's doing the wrong thing and falling back to the - // crazy incorrect behavior api call since that seems to be the only way - // to get it to work on Safari WebKit as of 2017-02-23 - if (sel.anchorNode !== rng.startContainer || sel.focusNode !== rng.endContainer) { - sel.setBaseAndExtent(node, 0, node, 1); - } - } - } - } - } + var backspaceDelete = function (editor, forward) { + if (editor.selection.isCollapsed()) { + return backspaceDeleteCaret(editor, forward); + } else { + return backspaceDeleteRange(editor, forward); + } + }; - self.editor.fire('AfterSetSelectionRange', { range: rng, forward: forward }); - } else { - // Is W3C Range fake range on IE - if (rng.cloneRange) { - try { - self.tridentSel.addRange(rng); - } catch (ex) { - //IE9 throws an error here if called before selection is placed in the editor - } - } - } - }, + return { + backspaceDelete: backspaceDelete, + paddEmptyElement: paddEmptyElement + }; + } +); - /** - * Sets the current selection to the specified DOM element. - * - * @method setNode - * @param {Element} elm Element to set as the contents of the selection. - * @return {Element} Returns the element that got passed in. - * @example - * // Inserts a DOM node at current selection/caret location - * tinymce.activeEditor.selection.setNode(tinymce.activeEditor.dom.create('img', {src: 'some.gif', title: 'some title'})); - */ - setNode: function (elm) { - var self = this; - - self.setContent(self.dom.getOuterHTML(elm)); +/** + * CaretContainerInline.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 + */ - return elm; - }, +define( + 'tinymce.core.caret.CaretContainerInline', + [ + 'ephox.katamari.api.Fun', + 'tinymce.core.dom.NodeType', + 'tinymce.core.text.Zwsp' + ], + function (Fun, NodeType, Zwsp) { + var isText = NodeType.isText; - /** - * Returns the currently selected element or the common ancestor element for both start and end of the selection. - * - * @method getNode - * @return {Element} Currently selected element or common ancestor element. - * @example - * // Alerts the currently selected elements node name - * alert(tinymce.activeEditor.selection.getNode().nodeName); - */ - getNode: function () { - var self = this, rng = self.getRng(), elm; - var startContainer, endContainer, startOffset, endOffset, root = self.dom.getRoot(); + var startsWithCaretContainer = function (node) { + return isText(node) && node.data[0] === Zwsp.ZWSP; + }; - function skipEmptyTextNodes(node, forwards) { - var orig = node; + var endsWithCaretContainer = function (node) { + return isText(node) && node.data[node.data.length - 1] === Zwsp.ZWSP; + }; - while (node && node.nodeType === 3 && node.length === 0) { - node = forwards ? node.nextSibling : node.previousSibling; - } + var createZwsp = function (node) { + return node.ownerDocument.createTextNode(Zwsp.ZWSP); + }; - return node || orig; + var insertBefore = function (node) { + if (isText(node.previousSibling)) { + if (endsWithCaretContainer(node.previousSibling)) { + return node.previousSibling; + } else { + node.previousSibling.appendData(Zwsp.ZWSP); + return node.previousSibling; } - - // Range maybe lost after the editor is made visible again - if (!rng) { - return root; + } else if (isText(node)) { + if (startsWithCaretContainer(node)) { + return node; + } else { + node.insertData(0, Zwsp.ZWSP); + return node; } + } else { + var newNode = createZwsp(node); + node.parentNode.insertBefore(newNode, node); + return newNode; + } + }; - startContainer = rng.startContainer; - endContainer = rng.endContainer; - startOffset = rng.startOffset; - endOffset = rng.endOffset; - - if (rng.setStart) { - elm = rng.commonAncestorContainer; - - // Handle selection a image or other control like element such as anchors - if (!rng.collapsed) { - if (startContainer == endContainer) { - if (endOffset - startOffset < 2) { - if (startContainer.hasChildNodes()) { - elm = startContainer.childNodes[startOffset]; - } - } - } + var insertAfter = function (node) { + if (isText(node.nextSibling)) { + if (startsWithCaretContainer(node.nextSibling)) { + return node.nextSibling; + } else { + node.nextSibling.insertData(0, Zwsp.ZWSP); + return node.nextSibling; + } + } else if (isText(node)) { + if (endsWithCaretContainer(node)) { + return node; + } else { + node.appendData(Zwsp.ZWSP); + return node; + } + } else { + var newNode = createZwsp(node); + if (node.nextSibling) { + node.parentNode.insertBefore(newNode, node.nextSibling); + } else { + node.parentNode.appendChild(newNode); + } + return newNode; + } + }; - // If the anchor node is a element instead of a text node then return this element - //if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) - // return sel.anchorNode.childNodes[sel.anchorOffset]; + var insertInline = function (before, node) { + return before ? insertBefore(node) : insertAfter(node); + }; - // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. - // This happens when you double click an underlined word in FireFox. - if (startContainer.nodeType === 3 && endContainer.nodeType === 3) { - if (startContainer.length === startOffset) { - startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); - } else { - startContainer = startContainer.parentNode; - } + return { + insertInline: insertInline, + insertInlineBefore: Fun.curry(insertInline, true), + insertInlineAfter: Fun.curry(insertInline, false) + }; + } +); +/** + * CaretContainerRemove.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 + */ - if (endOffset === 0) { - endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); - } else { - endContainer = endContainer.parentNode; - } +define( + 'tinymce.core.caret.CaretContainerRemove', + [ + 'ephox.katamari.api.Arr', + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.NodeType', + 'tinymce.core.text.Zwsp' + ], + function (Arr, CaretContainer, CaretPosition, NodeType, Zwsp) { + var isElement = NodeType.isElement; + var isText = NodeType.isText; - if (startContainer && startContainer === endContainer) { - return startContainer; - } - } - } + var removeNode = function (node) { + var parentNode = node.parentNode; + if (parentNode) { + parentNode.removeChild(node); + } + }; - if (elm && elm.nodeType == 3) { - return elm.parentNode; - } + var getNodeValue = function (node) { + try { + return node.nodeValue; + } catch (ex) { + // IE sometimes produces "Invalid argument" on nodes + return ""; + } + }; - return elm; - } + var setNodeValue = function (node, text) { + if (text.length === 0) { + removeNode(node); + } else { + node.nodeValue = text; + } + }; - elm = rng.item ? rng.item(0) : rng.parentElement(); + var trimCount = function (text) { + var trimmedText = Zwsp.trim(text); + return { count: text.length - trimmedText.length, text: trimmedText }; + }; - // IE 7 might return elements outside the iframe - if (elm.ownerDocument !== self.win.document) { - elm = root; - } + var removeUnchanged = function (caretContainer, pos) { + remove(caretContainer); + return pos; + }; - return elm; - }, + var removeTextAndReposition = function (caretContainer, pos) { + var before = trimCount(caretContainer.data.substr(0, pos.offset())); + var after = trimCount(caretContainer.data.substr(pos.offset())); + var text = before.text + after.text; - getSelectedBlocks: function (startElm, endElm) { - var self = this, dom = self.dom, node, root, selectedBlocks = []; + if (text.length > 0) { + setNodeValue(caretContainer, text); + return new CaretPosition(caretContainer, pos.offset() - before.count); + } else { + return pos; + } + }; - root = dom.getRoot(); - startElm = dom.getParent(startElm || self.getStart(), dom.isBlock); - endElm = dom.getParent(endElm || self.getEnd(), dom.isBlock); + var removeElementAndReposition = function (caretContainer, pos) { + var parentNode = pos.container(); + var newPosition = Arr.indexOf(parentNode.childNodes, caretContainer).map(function (index) { + return index < pos.offset() ? new CaretPosition(parentNode, pos.offset() - 1) : pos; + }).getOr(pos); + remove(caretContainer); + return newPosition; + }; - if (startElm && startElm != root) { - selectedBlocks.push(startElm); - } + var removeTextCaretContainer = function (caretContainer, pos) { + return pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); + }; - if (startElm && endElm && startElm != endElm) { - node = startElm; + var removeElementCaretContainer = function (caretContainer, pos) { + return pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); + }; - var walker = new TreeWalker(startElm, root); - while ((node = walker.next()) && node != endElm) { - if (dom.isBlock(node)) { - selectedBlocks.push(node); - } - } - } + var removeAndReposition = function (container, pos) { + return CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos); + }; - if (endElm && startElm != endElm && endElm != root) { - selectedBlocks.push(endElm); + var remove = function (caretContainerNode) { + if (isElement(caretContainerNode) && CaretContainer.isCaretContainer(caretContainerNode)) { + if (CaretContainer.hasContent(caretContainerNode)) { + caretContainerNode.removeAttribute('data-mce-caret'); + } else { + removeNode(caretContainerNode); } + } - return selectedBlocks; - }, - - isForward: function () { - var dom = this.dom, sel = this.getSel(), anchorRange, focusRange; + if (isText(caretContainerNode)) { + var text = Zwsp.trim(getNodeValue(caretContainerNode)); + setNodeValue(caretContainerNode, text); + } + }; - // No support for selection direction then always return true - if (!sel || !sel.anchorNode || !sel.focusNode) { - return true; - } + return { + removeAndReposition: removeAndReposition, + remove: remove + }; + } +); +/** + * DefaultSettings.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 + */ - anchorRange = dom.createRng(); - anchorRange.setStart(sel.anchorNode, sel.anchorOffset); - anchorRange.collapse(true); +define( + 'tinymce.core.EditorSettings', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Obj', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Strings', + 'ephox.katamari.api.Struct', + 'ephox.katamari.api.Type', + 'ephox.sand.api.PlatformDetection', + 'tinymce.core.util.Tools' + ], + function (Arr, Fun, Obj, Option, Strings, Struct, Type, PlatformDetection, Tools) { + var sectionResult = Struct.immutable('sections', 'settings'); + var detection = PlatformDetection.detect(); + var isTouch = detection.deviceType.isTouch(); + var mobilePlugins = [ 'lists', 'autolink', 'autosave' ]; + var defaultMobileSettings = { theme: 'mobile' }; - focusRange = dom.createRng(); - focusRange.setStart(sel.focusNode, sel.focusOffset); - focusRange.collapse(true); + var normalizePlugins = function (plugins) { + var pluginNames = Type.isArray(plugins) ? plugins.join(' ') : plugins; + var trimmedPlugins = Arr.map(Type.isString(pluginNames) ? pluginNames.split(' ') : [ ], Strings.trim); + return Arr.filter(trimmedPlugins, function (item) { + return item.length > 0; + }); + }; - return anchorRange.compareBoundaryPoints(anchorRange.START_TO_START, focusRange) <= 0; - }, + var filterMobilePlugins = function (plugins) { + return Arr.filter(plugins, Fun.curry(Arr.contains, mobilePlugins)); + }; - normalize: function () { - var self = this, rng = self.getRng(); + var extractSections = function (keys, settings) { + var result = Obj.bifilter(settings, function (value, key) { + return Arr.contains(keys, key); + }); - if (new RangeUtils(self.dom).normalize(rng) && !MultiRange.hasMultipleRanges(self.getSel())) { - self.setRng(rng, self.isForward()); - } + return sectionResult(result.t, result.f); + }; - return rng; - }, + var getSection = function (sectionResult, name, defaults) { + var sections = sectionResult.sections(); + var sectionSettings = sections.hasOwnProperty(name) ? sections[name] : { }; + return Tools.extend({}, defaults, sectionSettings); + }; - /** - * Executes callback when the current selection starts/stops matching the specified selector. The current - * state will be passed to the callback as it's first argument. - * - * @method selectorChanged - * @param {String} selector CSS selector to check for. - * @param {function} callback Callback with state and args when the selector is matches or not. - */ - selectorChanged: function (selector, callback) { - var self = this, currentSelectors; + var hasSection = function (sectionResult, name) { + return sectionResult.sections().hasOwnProperty(name); + }; - if (!self.selectorChangedData) { - self.selectorChangedData = {}; - currentSelectors = {}; + var getDefaultSettings = function (id, documentBaseUrl, editor) { + return { + 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', - self.editor.on('NodeChange', function (e) { - var node = e.element, dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; + // 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', + entity_encoding: 'named', + url_converter: editor.convertURL, + url_converter_scope: editor, + ie7_compat: true + }; + }; - // Check for new matching selectors - each(self.selectorChangedData, function (callbacks, selector) { - each(parents, function (node) { - if (dom.is(node, selector)) { - if (!currentSelectors[selector]) { - // Execute callbacks - each(callbacks, function (callback) { - callback(true, { node: node, selector: selector, parents: parents }); - }); + var getExternalPlugins = function (overrideSettings, settings) { + var userDefinedExternalPlugins = settings.external_plugins ? settings.external_plugins : { }; - currentSelectors[selector] = callbacks; - } + if (overrideSettings && overrideSettings.external_plugins) { + return Tools.extend({}, overrideSettings.external_plugins, userDefinedExternalPlugins); + } else { + return userDefinedExternalPlugins; + } + }; - matchedSelectors[selector] = callbacks; - return false; - } - }); - }); + var combinePlugins = function (forcedPlugins, plugins) { + return [].concat(normalizePlugins(forcedPlugins)).concat(normalizePlugins(plugins)); + }; - // Check if current selectors still match - each(currentSelectors, function (callbacks, selector) { - if (!matchedSelectors[selector]) { - delete currentSelectors[selector]; + var processPlugins = function (isTouchDevice, sectionResult, defaultOverrideSettings, settings) { + var forcedPlugins = normalizePlugins(defaultOverrideSettings.forced_plugins); + var plugins = normalizePlugins(settings.plugins); + var platformPlugins = isTouchDevice && hasSection(sectionResult, 'mobile') ? filterMobilePlugins(plugins) : plugins; + var combinedPlugins = combinePlugins(forcedPlugins, platformPlugins); - each(callbacks, function (callback) { - callback(false, { node: node, selector: selector, parents: parents }); - }); - } - }); - }); - } + return Tools.extend(settings, { + plugins: combinedPlugins.join(' ') + }); + }; - // Add selector listeners - if (!self.selectorChangedData[selector]) { - self.selectorChangedData[selector] = []; - } + var isOnMobile = function (isTouchDevice, sectionResult) { + var isInline = sectionResult.settings().inline; // We don't support mobile inline yet + return isTouchDevice && hasSection(sectionResult, 'mobile') && !isInline; + }; - self.selectorChangedData[selector].push(callback); + var combineSettings = function (isTouchDevice, defaultSettings, defaultOverrideSettings, settings) { + var sectionResult = extractSections(['mobile'], settings); + var extendedSettings = Tools.extend( + // Default settings + defaultSettings, - return self; - }, + // tinymce.overrideDefaults settings + defaultOverrideSettings, - getScrollContainer: function () { - var scrollContainer, node = this.dom.getRoot(); + // User settings + sectionResult.settings(), - while (node && node.nodeName != 'BODY') { - if (node.scrollHeight > node.clientHeight) { - scrollContainer = node; - break; - } + // Sections + isOnMobile(isTouchDevice, sectionResult) ? getSection(sectionResult, 'mobile', defaultMobileSettings) : { }, - node = node.parentNode; + // Forced settings + { + validate: true, + content_editable: sectionResult.settings().inline, + external_plugins: getExternalPlugins(defaultOverrideSettings, sectionResult.settings()) } + ); - return scrollContainer; - }, + return processPlugins(isTouchDevice, sectionResult, defaultOverrideSettings, extendedSettings); + }; - scrollIntoView: function (elm, alignToTop) { - ScrollIntoView.scrollIntoView(this.editor, elm, alignToTop); - }, + var getEditorSettings = function (editor, id, documentBaseUrl, defaultOverrideSettings, settings) { + var defaultSettings = getDefaultSettings(id, documentBaseUrl, editor); + return combineSettings(isTouch, defaultSettings, defaultOverrideSettings, settings); + }; - placeCaretAt: function (clientX, clientY) { - this.setRng(RangeUtils.getCaretRangeFromPoint(clientX, clientY, this.editor.getDoc())); - }, + var get = function (editor, name) { + return Option.from(editor.settings[name]); + }; - _moveEndPoint: function (rng, node, start) { - var root = node, walker = new TreeWalker(node, root); - var nonEmptyElementsMap = this.dom.schema.getNonEmptyElements(); + var getFiltered = function (predicate, editor, name) { + return Option.from(editor.settings[name]).filter(predicate); + }; - do { - // Text node - if (node.nodeType == 3 && trim(node.nodeValue).length !== 0) { - if (start) { - rng.setStart(node, 0); - } else { - rng.setEnd(node, node.nodeValue.length); - } + return { + getEditorSettings: getEditorSettings, + get: get, + getString: Fun.curry(getFiltered, Type.isString), + combineSettings: combineSettings + }; + } +); - return; - } +/** + * 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 + */ - // BR/IMG/INPUT elements but not table cells - if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) { - if (start) { - rng.setStartBefore(node); - } else { - if (node.nodeName == 'BR') { - rng.setEndBefore(node); - } else { - rng.setEndAfter(node); - } - } +define( + 'tinymce.core.text.Bidi', + [ + ], + function () { + var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; - return; - } + var hasStrongRtl = function (text) { + return strongRtl.test(text); + }; - // Found empty text block old IE can place the selection inside those - if (Env.ie && Env.ie < 11 && this.dom.isBlock(node) && this.dom.isEmpty(node)) { - if (start) { - rng.setStart(node, 0); - } else { - rng.setEnd(node, 0); - } - - return; - } - } while ((node = (start ? walker.next() : walker.prev()))); - - // Failed to find any text node or other suitable location then move to the root of body - if (root.nodeName == 'BODY') { - if (start) { - rng.setStart(root, 0); - } else { - rng.setEnd(root, root.childNodes.length); - } - } - }, - - getBoundingClientRect: function () { - var rng = this.getRng(); - return rng.collapsed ? CaretPosition.fromRangeStart(rng).getClientRects()[0] : rng.getBoundingClientRect(); - }, - - destroy: function () { - this.win = null; - this.controlSelection.destroy(); - } + return { + hasStrongRtl: hasStrongRtl }; - - return Selection; } ); - /** - * Node.js + * InlineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -31711,514 +29656,549 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class is a minimalistic implementation of a DOM like node used by the DomParser class. - * - * @example - * var node = new tinymce.html.Node('strong', 1); - * someRoot.append(node); - * - * @class tinymce.html.Node - * @version 3.4 - */ define( - 'tinymce.core.html.Node', + 'tinymce.core.keyboard.InlineUtils', [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.katamari.api.Type', + 'ephox.sugar.api.node.Element', + 'ephox.sugar.api.search.Selectors', + '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.EditorSettings', + 'tinymce.core.text.Bidi' ], - function () { - var whiteSpaceRegExp = /^[ \t\r\n]*$/; - var typeLookup = { - '#text': 3, - '#comment': 8, - '#cdata': 4, - '#pi': 7, - '#doctype': 10, - '#document-fragment': 11 + function ( + Arr, Fun, Option, Options, Type, Element, Selectors, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, NodeType, EditorSettings, + Bidi + ) { + var isInlineTarget = function (editor, elm) { + var selector = EditorSettings.getString(editor, 'inline_boundaries_selector').getOr('a[href],code'); + return Selectors.is(Element.fromDom(elm), selector); }; - // Walks the tree left/right - function walk(node, rootNode, prev) { - var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next'; + var isRtl = function (element) { + return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || Bidi.hasStrongRtl(element.textContent); + }; - // Walk into nodes if it has a start - if (node[startName]) { - return node[startName]; - } + var findInlineParents = function (isInlineTarget, rootNode, pos) { + return Arr.filter(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget); + }; - // Return the sibling if it has one - if (node !== rootNode) { - sibling = node[siblingName]; + var findRootInline = function (isInlineTarget, rootNode, pos) { + var parents = findInlineParents(isInlineTarget, rootNode, pos); + return Option.from(parents[parents.length - 1]); + }; - if (sibling) { - return sibling; - } + var hasSameParentBlock = function (rootNode, node1, node2) { + var block1 = CaretUtils.getParentBlock(node1, rootNode); + var block2 = CaretUtils.getParentBlock(node2, rootNode); + return block1 && block1 === block2; + }; - // Walk up the parents to look for siblings - for (parent = node.parent; parent && parent !== rootNode; parent = parent.parent) { - sibling = parent[siblingName]; + var isAtZwsp = function (pos) { + return CaretContainer.isBeforeInline(pos) || CaretContainer.isAfterInline(pos); + }; - if (sibling) { - return sibling; + 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; } } - } + }; - /** - * Constructs a new Node instance. - * - * @constructor - * @method Node - * @param {String} name Name of the node type. - * @param {Number} type Numeric type representing the node. - */ - function Node(name, type) { - this.name = name; - this.type = type; + var normalizeForwards = Fun.curry(normalizePosition, true); + var normalizeBackwards = Fun.curry(normalizePosition, false); - if (type === 1) { - this.attributes = []; - this.attributes.map = {}; + return { + isInlineTarget: isInlineTarget, + findRootInline: findRootInline, + isRtl: isRtl, + isAtZwsp: isAtZwsp, + normalizePosition: normalizePosition, + normalizeForwards: normalizeForwards, + normalizeBackwards: normalizeBackwards, + hasSameParentBlock: hasSameParentBlock + }; + } +); +/** + * BoundaryCaret.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.BoundaryCaret', + [ + 'ephox.katamari.api.Option', + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.caret.CaretContainerInline', + 'tinymce.core.caret.CaretContainerRemove', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.NodeType', + 'tinymce.core.keyboard.InlineUtils' + ], + function (Option, CaretContainer, CaretContainerInline, CaretContainerRemove, CaretFinder, CaretPosition, NodeType, InlineUtils) { + var insertInlinePos = function (pos, before) { + if (NodeType.isText(pos.container())) { + return CaretContainerInline.insertInline(before, pos.container()); + } else { + return CaretContainerInline.insertInline(before, pos.getNode()); } - } + }; - Node.prototype = { - /** - * Replaces the current node with the specified one. - * - * @example - * someNode.replace(someNewNode); - * - * @method replace - * @param {tinymce.html.Node} node Node to replace the current node with. - * @return {tinymce.html.Node} The old node that got replaced. - */ - replace: function (node) { - var self = this; + var isPosCaretContainer = function (pos, caret) { + var caretNode = caret.get(); + return caretNode && pos.container() === caretNode && CaretContainer.isCaretContainerInline(caretNode); + }; - if (node.parent) { - node.remove(); + var renderCaret = function (caret, location) { + return location.fold( + function (element) { // Before + CaretContainerRemove.remove(caret.get()); + var text = CaretContainerInline.insertInlineBefore(element); + caret.set(text); + return Option.some(new CaretPosition(text, text.length - 1)); + }, + function (element) { // Start + return CaretFinder.firstPositionIn(element).map(function (pos) { + if (!isPosCaretContainer(pos, caret)) { + CaretContainerRemove.remove(caret.get()); + var text = insertInlinePos(pos, true); + caret.set(text); + return new CaretPosition(text, 1); + } else { + return new CaretPosition(caret.get(), 1); + } + }); + }, + function (element) { // End + return CaretFinder.lastPositionIn(element).map(function (pos) { + if (!isPosCaretContainer(pos, caret)) { + CaretContainerRemove.remove(caret.get()); + var text = insertInlinePos(pos, false); + caret.set(text); + return new CaretPosition(text, text.length - 1); + } else { + return new CaretPosition(caret.get(), caret.get().length - 1); + } + }); + }, + function (element) { // After + CaretContainerRemove.remove(caret.get()); + var text = CaretContainerInline.insertInlineAfter(element); + caret.set(text); + return Option.some(new CaretPosition(text, 1)); } + ); + }; - self.insert(node, self); - self.remove(); - - return self; - }, - - /** - * Gets/sets or removes an attribute by name. - * - * @example - * someNode.attr("name", "value"); // Sets an attribute - * console.log(someNode.attr("name")); // Gets an attribute - * someNode.attr("name", null); // Removes an attribute - * - * @method attr - * @param {String} name Attribute name to set or get. - * @param {String} value Optional value to set. - * @return {String/tinymce.html.Node} String or undefined on a get operation or the current node on a set operation. - */ - attr: function (name, value) { - var self = this, attrs, i, undef; - - if (typeof name !== "string") { - for (i in name) { - self.attr(i, name[i]); - } + return { + renderCaret: renderCaret + }; + } +); +/** + * LazyEvaluator.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 + */ - return self; +define( + 'tinymce.core.util.LazyEvaluator', + [ + 'ephox.katamari.api.Option' + ], + function (Option) { + var evaluateUntil = function (fns, args) { + for (var i = 0; i < fns.length; i++) { + var result = fns[i].apply(null, args); + if (result.isSome()) { + return result; } + } - if ((attrs = self.attributes)) { - if (value !== undef) { - // Remove attribute - if (value === null) { - if (name in attrs.map) { - delete attrs.map[name]; + return Option.none(); + }; - i = attrs.length; - while (i--) { - if (attrs[i].name === name) { - attrs = attrs.splice(i, 1); - return self; - } - } - } + return { + evaluateUntil: evaluateUntil + }; + } +); +/** + * BoundaryLocation.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 + */ - return self; - } +define( + 'tinymce.core.keyboard.BoundaryLocation', + [ + 'ephox.katamari.api.Adt', + '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.dom.NodeType', + 'tinymce.core.keyboard.InlineUtils', + 'tinymce.core.util.LazyEvaluator' + ], + function (Adt, Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, NodeType, InlineUtils, LazyEvaluator) { + var Location = Adt.generate([ + { before: [ 'element' ] }, + { start: [ 'element' ] }, + { end: [ 'element' ] }, + { after: [ 'element' ] } + ]); - // Set attribute - if (name in attrs.map) { - // Set attribute - i = attrs.length; - while (i--) { - if (attrs[i].name === name) { - attrs[i].value = value; - break; - } - } - } else { - attrs.push({ name: name, value: value }); - } + var rescope = function (rootNode, node) { + var parentBlock = CaretUtils.getParentBlock(node, rootNode); + return parentBlock ? parentBlock : rootNode; + }; - attrs.map[name] = value; + var before = function (isInlineTarget, rootNode, pos) { + var nPos = InlineUtils.normalizeForwards(pos); + var scope = rescope(rootNode, nPos.container()); + return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold( + function () { + return CaretFinder.nextPosition(scope, nPos) + .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope)) + .map(function (inline) { + return Location.before(inline); + }); + }, + Option.none + ); + }; - return self; - } + var start = function (isInlineTarget, rootNode, pos) { + var nPos = InlineUtils.normalizeBackwards(pos); + return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) { + var prevPos = CaretFinder.prevPosition(inline, nPos); + return prevPos.isNone() ? Option.some(Location.start(inline)) : Option.none(); + }); + }; - return attrs.map[name]; - } - }, + var end = function (isInlineTarget, rootNode, pos) { + var nPos = InlineUtils.normalizeForwards(pos); + return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) { + var nextPos = CaretFinder.nextPosition(inline, nPos); + return nextPos.isNone() ? Option.some(Location.end(inline)) : Option.none(); + }); + }; - /** - * Does a shallow clones the node into a new node. It will also exclude id attributes since - * there should only be one id per document. - * - * @example - * var clonedNode = node.clone(); - * - * @method clone - * @return {tinymce.html.Node} New copy of the original node. - */ - clone: function () { - var self = this, clone = new Node(self.name, self.type), i, l, selfAttrs, selfAttr, cloneAttrs; + var after = function (isInlineTarget, rootNode, pos) { + var nPos = InlineUtils.normalizeBackwards(pos); + var scope = rescope(rootNode, nPos.container()); + return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold( + function () { + return CaretFinder.prevPosition(scope, nPos) + .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope)) + .map(function (inline) { + return Location.after(inline); + }); + }, + Option.none + ); + }; - // Clone element attributes - if ((selfAttrs = self.attributes)) { - cloneAttrs = []; - cloneAttrs.map = {}; + var isValidLocation = function (location) { + return InlineUtils.isRtl(getElement(location)) === false; + }; - for (i = 0, l = selfAttrs.length; i < l; i++) { - selfAttr = selfAttrs[i]; + var readLocation = function (isInlineTarget, rootNode, pos) { + var location = LazyEvaluator.evaluateUntil([ + before, + start, + end, + after + ], [isInlineTarget, rootNode, pos]); - // Clone everything except id - if (selfAttr.name !== 'id') { - cloneAttrs[cloneAttrs.length] = { name: selfAttr.name, value: selfAttr.value }; - cloneAttrs.map[selfAttr.name] = selfAttr.value; - } - } + return location.filter(isValidLocation); + }; - clone.attributes = cloneAttrs; - } + var getElement = function (location) { + return location.fold( + Fun.identity, // Before + Fun.identity, // Start + Fun.identity, // End + Fun.identity // After + ); + }; - clone.value = self.value; - clone.shortEnded = self.shortEnded; + var getName = function (location) { + return location.fold( + Fun.constant('before'), // Before + Fun.constant('start'), // Start + Fun.constant('end'), // End + Fun.constant('after') // After + ); + }; - return clone; - }, + var outside = function (location) { + return location.fold( + Location.before, // Before + Location.before, // Start + Location.after, // End + Location.after // After + ); + }; - /** - * Wraps the node in in another node. - * - * @example - * node.wrap(wrapperNode); - * - * @method wrap - */ - wrap: function (wrapper) { - var self = this; + var inside = function (location) { + return location.fold( + Location.start, // Before + Location.start, // Start + Location.end, // End + Location.end // After + ); + }; - self.parent.insert(wrapper, self); - wrapper.append(self); - - return self; - }, - - /** - * Unwraps the node in other words it removes the node but keeps the children. - * - * @example - * node.unwrap(); - * - * @method unwrap - */ - unwrap: function () { - var self = this, node, next; - - for (node = self.firstChild; node;) { - next = node.next; - self.insert(node, self, true); - node = next; - } - - self.remove(); - }, - - /** - * Removes the node from it's parent. - * - * @example - * node.remove(); - * - * @method remove - * @return {tinymce.html.Node} Current node that got removed. - */ - remove: function () { - var self = this, parent = self.parent, next = self.next, prev = self.prev; - - if (parent) { - if (parent.firstChild === self) { - parent.firstChild = next; - - if (next) { - next.prev = null; - } - } else { - prev.next = next; - } - - if (parent.lastChild === self) { - parent.lastChild = prev; - - if (prev) { - prev.next = null; - } - } else { - next.prev = prev; - } - - self.parent = self.next = self.prev = null; - } - - return self; - }, - - /** - * Appends a new node as a child of the current node. - * - * @example - * node.append(someNode); - * - * @method append - * @param {tinymce.html.Node} node Node to append as a child of the current one. - * @return {tinymce.html.Node} The node that got appended. - */ - append: function (node) { - var self = this, last; - - if (node.parent) { - node.remove(); - } + var isEq = function (location1, location2) { + return getName(location1) === getName(location2) && getElement(location1) === getElement(location2); + }; - last = self.lastChild; - if (last) { - last.next = node; - node.prev = last; - self.lastChild = node; + var betweenInlines = function (forward, isInlineTarget, rootNode, from, to, location) { + return Options.liftN([ + InlineUtils.findRootInline(isInlineTarget, rootNode, from), + InlineUtils.findRootInline(isInlineTarget, rootNode, to) + ], function (fromInline, toInline) { + if (fromInline !== toInline && InlineUtils.hasSameParentBlock(rootNode, fromInline, toInline)) { + // Force after since some browsers normalize and lean left into the closest inline + return Location.after(forward ? fromInline : toInline); } else { - self.lastChild = self.firstChild = node; - } - - node.parent = self; - - return node; - }, - - /** - * Inserts a node at a specific position as a child of the current node. - * - * @example - * parentNode.insert(newChildNode, oldChildNode); - * - * @method insert - * @param {tinymce.html.Node} node Node to insert as a child of the current node. - * @param {tinymce.html.Node} refNode Reference node to set node before/after. - * @param {Boolean} before Optional state to insert the node before the reference node. - * @return {tinymce.html.Node} The node that got inserted. - */ - insert: function (node, refNode, before) { - var parent; - - if (node.parent) { - node.remove(); + return location; } + }).getOr(location); + }; - parent = refNode.parent || this; - - if (before) { - if (refNode === parent.firstChild) { - parent.firstChild = node; - } else { - refNode.prev.next = node; - } - - node.prev = refNode.prev; - node.next = refNode; - refNode.prev = node; - } else { - if (refNode === parent.lastChild) { - parent.lastChild = node; - } else { - refNode.next.prev = node; - } - - node.next = refNode.next; - node.prev = refNode; - refNode.next = node; + var skipNoMovement = function (fromLocation, toLocation) { + return fromLocation.fold( + Fun.constant(true), + function (fromLocation) { + return !isEq(fromLocation, toLocation); } + ); + }; - node.parent = parent; - - return node; - }, - - /** - * Get all children by name. - * - * @method getAll - * @param {String} name Name of the child nodes to collect. - * @return {Array} Array with child nodes matchin the specified name. - */ - getAll: function (name) { - var self = this, node, collection = []; + var findLocationTraverse = function (forward, isInlineTarget, rootNode, fromLocation, pos) { + var from = InlineUtils.normalizePosition(forward, pos); + var to = CaretFinder.fromPosition(forward, rootNode, from).map(Fun.curry(InlineUtils.normalizePosition, forward)); - for (node = self.firstChild; node; node = walk(node, self)) { - if (node.name === name) { - collection.push(node); - } + var location = to.fold( + function () { + return fromLocation.map(outside); + }, + function (to) { + return readLocation(isInlineTarget, rootNode, to) + .map(Fun.curry(betweenInlines, forward, isInlineTarget, rootNode, from, to)) + .filter(Fun.curry(skipNoMovement, fromLocation)); } + ); - return collection; - }, - - /** - * Removes all children of the current node. - * - * @method empty - * @return {tinymce.html.Node} The current node that got cleared. - */ - empty: function () { - var self = this, nodes, i, node; - - // Remove all children - if (self.firstChild) { - nodes = []; - - // Collect the children - for (node = self.firstChild; node; node = walk(node, self)) { - nodes.push(node); - } - - // Remove the children - i = nodes.length; - while (i--) { - node = nodes[i]; - node.parent = node.firstChild = node.lastChild = node.next = node.prev = null; - } - } + return location.filter(isValidLocation); + }; - self.firstChild = self.lastChild = null; + var findLocationSimple = function (forward, location) { + if (forward) { + return location.fold( + Fun.compose(Option.some, Location.start), // Before -> Start + Option.none, + Fun.compose(Option.some, Location.after), // End -> After + Option.none + ); + } else { + return location.fold( + Option.none, + Fun.compose(Option.some, Location.before), // Before <- Start + Option.none, + Fun.compose(Option.some, Location.end) // End <- After + ); + } + }; - return self; - }, + var findLocation = function (forward, isInlineTarget, rootNode, pos) { + var from = InlineUtils.normalizePosition(forward, pos); + var fromLocation = readLocation(isInlineTarget, rootNode, from); - /** - * Returns true/false if the node is to be considered empty or not. - * - * @example - * node.isEmpty({img: true}); - * @method isEmpty - * @param {Object} elements Name/value object with elements that are automatically treated as non empty elements. - * @param {Object} whitespace Name/value object with elements that are automatically treated whitespace preservables. - * @param {function} predicate Optional predicate that gets called after the other rules determine that the node is empty. Should return true if the node is a content node. - * @return {Boolean} true/false if the node is empty or not. - */ - isEmpty: function (elements, whitespace, predicate) { - var self = this, node = self.firstChild, i, name; + return readLocation(isInlineTarget, rootNode, from).bind(Fun.curry(findLocationSimple, forward)).orThunk(function () { + return findLocationTraverse(forward, isInlineTarget, rootNode, fromLocation, pos); + }); + }; - whitespace = whitespace || {}; + return { + readLocation: readLocation, + findLocation: findLocation, + prevLocation: Fun.curry(findLocation, false), + nextLocation: Fun.curry(findLocation, true), + getElement: getElement, + outside: outside, + inside: inside + }; + } +); +/** + * BoundarySelection.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 + */ - if (node) { - do { - if (node.type === 1) { - // Ignore bogus elements - if (node.attributes.map['data-mce-bogus']) { - continue; - } +define( + 'tinymce.core.keyboard.BoundarySelection', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Cell', + 'ephox.katamari.api.Fun', + 'tinymce.core.caret.CaretContainerRemove', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.keyboard.BoundaryCaret', + 'tinymce.core.keyboard.BoundaryLocation', + 'tinymce.core.keyboard.InlineUtils' + ], + function (Arr, Cell, Fun, CaretContainerRemove, CaretPosition, BoundaryCaret, BoundaryLocation, InlineUtils) { + var setCaretPosition = function (editor, pos) { + var rng = editor.dom.createRng(); + rng.setStart(pos.container(), pos.offset()); + rng.setEnd(pos.container(), pos.offset()); + editor.selection.setRng(rng); + }; - // Keep empty elements like - if (elements[node.name]) { - return false; - } + var isFeatureEnabled = function (editor) { + return editor.settings.inline_boundaries !== 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; - } - } - } + var setSelected = function (state, elm) { + if (state) { + elm.setAttribute('data-mce-selected', '1'); + } else { + elm.removeAttribute('data-mce-selected', '1'); + } + }; - // Keep comments - if (node.type === 8) { - return false; - } + var renderCaretLocation = function (editor, caret, location) { + return BoundaryCaret.renderCaret(caret, location).map(function (pos) { + setCaretPosition(editor, pos); + return location; + }); + }; - // Keep non whitespace text nodes - if (node.type === 3 && !whiteSpaceRegExp.test(node.value)) { - return false; - } + var findLocation = function (editor, caret, forward) { + var rootNode = editor.getBody(); + var from = CaretPosition.fromRangeStart(editor.selection.getRng()); + var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); + var location = BoundaryLocation.findLocation(forward, isInlineTarget, rootNode, from); + return location.bind(function (location) { + return renderCaretLocation(editor, caret, location); + }); + }; - // Keep whitespace preserve elements - if (node.type === 3 && node.parent && whitespace[node.parent.name] && whiteSpaceRegExp.test(node.value)) { - return false; - } + var toggleInlines = function (isInlineTarget, dom, elms) { + var selectedInlines = Arr.filter(dom.select('*[data-mce-selected]'), isInlineTarget); + var targetInlines = Arr.filter(elms, isInlineTarget); + Arr.each(Arr.difference(selectedInlines, targetInlines), Fun.curry(setSelected, false)); + Arr.each(Arr.difference(targetInlines, selectedInlines), Fun.curry(setSelected, true)); + }; - // Predicate tells that the node is contents - if (predicate && predicate(node)) { - return false; - } - } while ((node = walk(node, self))); + var safeRemoveCaretContainer = function (editor, caret) { + if (editor.selection.isCollapsed() && editor.composing !== true && caret.get()) { + var pos = CaretPosition.fromRangeStart(editor.selection.getRng()); + if (CaretPosition.isTextPosition(pos) && InlineUtils.isAtZwsp(pos) === false) { + setCaretPosition(editor, CaretContainerRemove.removeAndReposition(caret.get(), pos)); + caret.set(null); } + } + }; - 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); + var renderInsideInlineCaret = function (isInlineTarget, editor, caret, elms) { + if (editor.selection.isCollapsed()) { + var inlines = Arr.filter(elms, isInlineTarget); + Arr.each(inlines, function (inline) { + var pos = CaretPosition.fromRangeStart(editor.selection.getRng()); + BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), pos).bind(function (location) { + return renderCaretLocation(editor, caret, location); + }); + }); } }; - /** - * 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; + var move = function (editor, caret, forward) { + return function () { + return isFeatureEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false; + }; + }; - // Create node - node = new Node(name, typeLookup[name] || 1); + var setupSelectedState = function (editor) { + var caret = new Cell(null); + var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); - // Add attributes if needed - if (attrs) { - for (attrName in attrs) { - node.attr(attrName, attrs[attrName]); + editor.on('NodeChange', function (e) { + if (isFeatureEnabled(editor)) { + toggleInlines(isInlineTarget, editor.dom, e.parents); + safeRemoveCaretContainer(editor, caret); + renderInsideInlineCaret(isInlineTarget, editor, caret, e.parents); } - } + }); - return node; + return caret; }; - return Node; + return { + move: move, + setupSelectedState: setupSelectedState, + setCaretPosition: setCaretPosition + }; } ); /** - * SaxParser.js + * InlineBoundaryDelete.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -32227,498 +30207,416 @@ define( * 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.delete.InlineBoundaryDelete', [ - "tinymce.core.html.Schema", - "tinymce.core.html.Entities", - "tinymce.core.util.Tools" + 'ephox.katamari.api.Fun', + 'ephox.katamari.api.Option', + 'ephox.katamari.api.Options', + 'ephox.sugar.api.node.Element', + 'global!document', + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.caret.CaretUtils', + 'tinymce.core.delete.DeleteElement', + 'tinymce.core.keyboard.BoundaryCaret', + 'tinymce.core.keyboard.BoundaryLocation', + 'tinymce.core.keyboard.BoundarySelection', + 'tinymce.core.keyboard.InlineUtils' ], - function (Schema, Entities, Tools) { - var each = Tools.each; - - var isValidPrefixAttrName = function (name) { - return name.indexOf('data-') === 0 || name.indexOf('aria-') === 0; + function ( + Fun, Option, Options, Element, document, CaretContainer, CaretFinder, CaretPosition, CaretUtils, DeleteElement, BoundaryCaret, BoundaryLocation, BoundarySelection, + InlineUtils + ) { + var isFeatureEnabled = function (editor) { + return editor.settings.inline_boundaries !== false; }; - var trimComments = function (text) { - return text.replace(//g, ''); + var rangeFromPositions = function (from, to) { + var range = document.createRange(); + + range.setStart(from.container(), from.offset()); + range.setEnd(to.container(), to.offset()); + + return range; }; - /** - * 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; + // Checks for delete at |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); - if (matches[1] === '/') { // End element - count--; - } else if (!matches[1]) { // Start element - if (matches[2] in shortEndedElements) { - continue; - } + return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) { + return pos.isEqual(normalizedLastPos); + }).getOr(true); + }).getOr(true); + }; - count++; - } + var setCaretLocation = function (editor, caret) { + return function (location) { + return BoundaryCaret.renderCaret(caret, location).map(function (pos) { + BoundarySelection.setCaretPosition(editor, pos); + return true; + }).getOr(false); + }; + }; - if (count === 0) { - break; - } - } + var deleteFromTo = function (editor, caret, from, to) { + var rootNode = editor.getBody(); + var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); - return index; - } + editor.undoManager.ignore(function () { + editor.selection.setRng(rangeFromPositions(from, to)); + editor.execCommand('Delete'); - /** - * 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; + BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng())) + .map(BoundaryLocation.inside) + .map(setCaretLocation(editor, caret)); + }); - function noop() { } + editor.nodeChanged(); + }; - settings = settings || {}; - self.schema = schema = schema || new Schema(); + var rescope = function (rootNode, node) { + var parentBlock = CaretUtils.getParentBlock(node, rootNode); + return parentBlock ? parentBlock : rootNode; + }; - if (settings.fix_self_closing !== false) { - settings.fix_self_closing = true; - } + 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); - // 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; + 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 + ); } - }); - - /** - * 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; + }) + .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); + }); - // Find position of parent of the same type - pos = stack.length; - while (pos--) { - if (stack[pos].name === name) { - break; + 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; } - } - - // 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); + }).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); } - } - // Remove the open elements from the stack - stack.length = pos; - } + return true; + }); + }).getOr(false); } + }); + }; - 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 + 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); + } - // Validate name and value pass through all data- attributes - if (validate && !isInternalElement && isValidPrefixAttrName(name) === false) { - attrRule = validAttributesMap[name]; + return false; + }; - // Find rule by pattern matching - if (!attrRule && validAttributePatterns) { - i = validAttributePatterns.length; - while (i--) { - attrRule = validAttributePatterns[i]; - if (attrRule.pattern.test(name)) { - break; - } - } + return { + backspaceDelete: backspaceDelete + }; + } +); +define( + 'tinymce.core.delete.TableDeleteAction', - // No rule matched - if (i === -1) { - attrRule = null; - } - } + [ + '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' + ], - // No attribute rule found - if (!attrRule) { - return; - } + 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' ] } + ]); - // Validate value - if (attrRule.validValues && !(value in attrRule.validValues)) { - return; - } - } + var getClosestCell = function (container, isRoot) { + return SelectorFind.closest(Element.fromDom(container), 'td,th', isRoot); + }; - // Block any javascript: urls or non image data uris - if (filteredUrlAttrs[name] && !settings.allow_script_urls) { - var uri = value.replace(trimRegExp, ''); + var getClosestTable = function (cell, isRoot) { + return SelectorFind.ancestor(cell, 'table', isRoot); + }; - try { - // Might throw malformed URI sequence - uri = decodeURIComponent(uri); - } catch (ex) { - // Fallback to non UTF-8 decoder - uri = unescape(uri); - } + var isExpandedCellRng = function (cellRng) { + return Compare.eq(cellRng.start(), cellRng.end()) === false; + }; - if (scriptUriRegExp.test(uri)) { - return; - } + 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(); + }); + }); + }; - if (!settings.allow_html_data_urls && dataUriRegExp.test(uri) && !/^data:image\//i.test(uri)) { - return; - } - } + var getCellRng = function (rng, isRoot) { + return Options.liftN([ // get start and end cell + getClosestCell(rng.startContainer, isRoot), + getClosestCell(rng.endContainer, isRoot) + ], tableCellRng) + .filter(isExpandedCellRng); + }; - // Block data or event attributes on elements marked as internal - if (isInternalElement && (name in filteredUrlAttrs || name.indexOf('on') === 0)) { - return; - } + var getTableSelectionFromCellRng = function (cellRng, isRoot) { + return getTableFromCellRng(cellRng, isRoot) + .bind(function (table) { + var cells = SelectorFilter.descendants(table, 'td,th'); - // Add attribute to list and map - attrList.map[name] = value; - attrList.push({ - name: name, - value: value - }); - } + return tableSelection(cellRng, table, cells); + }); + }; - // 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'); + var getTableSelectionFromRng = function (rootNode, rng) { + var isRoot = Fun.curry(Compare.eq, rootNode); - attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; + return getCellRng(rng, isRoot) + .map(function (cellRng) { + return getTableSelectionFromCellRng(cellRng, isRoot); + }); + }; - // 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 + '>'; + var getCellIndex = function (cellArray, cell) { + return Arr.findIndex(cellArray, function (x) { + return Compare.eq(x, cell); + }); + }; - 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))); - } + 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); + }); + }; - if ((value = matches[6])) { // End element - value = value.toLowerCase(); + var getAction = function (tableSelection) { + return getSelectedCells(tableSelection) + .bind(function (selected) { + var cells = tableSelection.cells(); - // 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); - } + return selected.length === cells.length ? deleteAction.removeTable(tableSelection.table()) : deleteAction.emptyCells(selected); + }); + }; - 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; - } + var getActionFromCells = function (cells) { + return deleteAction.emptyCells(cells); + }; - value = value.toLowerCase(); + var getActionFromRange = function (rootNode, rng) { + return getTableSelectionFromRng(rootNode, rng) + .map(getAction); + }; - // 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); - } + return { + getActionFromRange: getActionFromRange, + getActionFromCells: getActionFromCells + }; + } +); - isShortEnded = value in shortEndedElements; +/** + * 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 + */ - // Is self closing tag for example an
  • after an open
  • - if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) { - processEndTag(value); - } +define( + 'tinymce.core.delete.TableDelete', + [ + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Fun', + 'ephox.sugar.api.dom.Compare', + 'ephox.sugar.api.node.Element', + 'tinymce.core.caret.CaretFinder', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.delete.DeleteElement', + 'tinymce.core.delete.TableDeleteAction', + 'tinymce.core.dom.ElementType', + 'tinymce.core.dom.PaddingBr', + 'tinymce.core.dom.Parents', + 'tinymce.core.selection.TableCellSelection' + ], + function (Arr, Fun, Compare, Element, CaretFinder, CaretPosition, DeleteElement, TableDeleteAction, ElementType, PaddingBr, Parents, TableCellSelection) { + var emptyCells = function (editor, cells) { + Arr.each(cells, PaddingBr.fillWithPaddingBr); + editor.selection.setCursorLocation(cells[0].dom(), 0); - // Validate element - if (!validate || (elementRule = schema.getElementRule(value))) { - isValidElement = true; + return true; + }; - // Grab attributes map and patters when validation is enabled - if (validate) { - validAttributesMap = elementRule.attributes; - validAttributePatterns = elementRule.attributePatterns; - } + var deleteTableElement = function (editor, table) { + DeleteElement.deleteElement(editor, false, table); - // Parse attributes - if ((attribsValue = matches[8])) { - isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element + return true; + }; - // If the element has internal attributes then remove it if we are told to do so - if (isInternalElement && removeInternalElements) { - isValidElement = false; - } + 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); + }; - attrList = []; - attrList.map = {}; + var deleteRange = function (editor) { + var rootNode = Element.fromDom(editor.getBody()); + var rng = editor.selection.getRng(); + var selectedCells = TableCellSelection.getCellsFromEditor(editor); - attribsValue.replace(attrRegExp, parseAttribute); - } else { - attrList = []; - attrList.map = {}; - } + return selectedCells.length !== 0 ? emptyCells(editor, selectedCells) : handleCellRange(editor, rootNode, rng); + }; - // Process attributes if validation is enabled - if (validate && !isInternalElement) { - attributesRequired = elementRule.attributesRequired; - attributesDefault = elementRule.attributesDefault; - attributesForced = elementRule.attributesForced; - anyAttributesRequired = elementRule.removeEmptyAttrs; + var getParentCell = function (rootElm, elm) { + return Arr.find(Parents.parentsAndSelf(elm, rootElm), ElementType.isTableCell); + }; - // Check if any attribute exists - if (anyAttributesRequired && !attrList.length) { - isValidElement = false; - } + var deleteCaret = function (editor, forward) { + var rootElm = Element.fromDom(editor.getBody()); + var from = CaretPosition.fromRangeStart(editor.selection.getRng()); + return getParentCell(rootElm, Element.fromDom(editor.selection.getStart(true))).bind(function (fromCell) { + return CaretFinder.navigate(forward, editor.getBody(), from).bind(function (to) { + return getParentCell(rootElm, Element.fromDom(to.getNode())).map(function (toCell) { + return Compare.eq(toCell, fromCell) === false; + }); + }); + }).getOr(false); + }; - // Handle forced attributes - if (attributesForced) { - i = attributesForced.length; - while (i--) { - attr = attributesForced[i]; - name = attr.name; - attrValue = attr.value; + var backspaceDelete = function (editor, forward) { + return editor.selection.isCollapsed() ? deleteCaret(editor, forward) : deleteRange(editor); + }; - if (attrValue === '{$uid}') { - attrValue = 'mce_' + idCount++; - } + return { + backspaceDelete: backspaceDelete + }; + } +); - attrList.map[name] = attrValue; - attrList.push({ name: name, value: attrValue }); - } - } +/** + * 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 + */ - // Handle default attributes - if (attributesDefault) { - i = attributesDefault.length; - while (i--) { - attr = attributesDefault[i]; - name = attr.name; - - if (!(name in attrList.map)) { - attrValue = attr.value; - - if (attrValue === '{$uid}') { - attrValue = 'mce_' + idCount++; - } - - attrList.map[name] = attrValue; - attrList.push({ name: name, value: attrValue }); - } - } - } - - // Handle required attributes - if (attributesRequired) { - i = attributesRequired.length; - while (i--) { - if (attributesRequired[i] in attrList.map) { - break; - } - } - - // None of the required attributes where found - if (i === -1) { - isValidElement = false; - } - } - - // Invalidate element if it's marked as bogus - if ((attr = attrList.map['data-mce-bogus'])) { - if (attr === 'all') { - index = findEndTag(schema, html, tokenRegExp.lastIndex); - tokenRegExp.lastIndex = index; - continue; - } - - isValidElement = false; - } - } - - if (isValidElement) { - self.start(value, attrList, isShortEnded); - } - } else { - isValidElement = false; - } - - // Treat script, noscript and style a bit different since they may include code that looks like elements - if ((endRegExp = specialElements[value])) { - endRegExp.lastIndex = index = matches.index + matches[0].length; - - if ((matches = endRegExp.exec(html))) { - if (isValidElement) { - text = html.substr(index, matches.index - index); - } - - index = matches.index + matches[0].length; - } else { - text = html.substr(index); - index = html.length; - } - - if (isValidElement) { - if (text.length > 0) { - self.text(text, true); - } - - self.end(value); - } - - tokenRegExp.lastIndex = index; - continue; - } - - // Push value on to stack - if (!isShortEnded) { - if (!attribsValue || attribsValue.indexOf('/') != attribsValue.length - 1) { - stack.push({ name: value, valid: isValidElement }); - } else if (isValidElement) { - self.end(value); - } - } - } else if ((value = matches[1])) { // Comment - // Padd comment value to avoid browsers from parsing invalid comments as HTML - if (value.charAt(0) === '>') { - value = ' ' + value; - } - - if (!settings.allow_conditional_comments && value.substr(0, 3).toLowerCase() === '[if') { - value = ' ' + value; - } - - self.comment(value); - } else if ((value = matches[2])) { // CDATA - self.cdata(trimComments(value)); - } else if ((value = matches[3])) { // DOCTYPE - self.doctype(value); - } else if ((value = matches[4])) { // PI - self.pi(value, matches[5]); - } - - index = matches.index + matches[0].length; - } - - // Text - if (index < html.length) { - self.text(decode(html.substr(index))); - } - - // Close any open elements - for (i = stack.length - 1; i >= 0; i--) { - value = stack[i]; +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); + }; - if (value.valid) { - self.end(value.name); - } - } - }; - } + 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); + } + }; - SaxParser.findEndTag = findEndTag; + 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 SaxParser; + return { + deleteCommand: deleteCommand, + forwardDeleteCommand: forwardDeleteCommand + }; } ); /** - * DomParser.js + * EditorCommands.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -32728,899 +30626,741 @@ define( */ /** - * This class parses HTML code into a DOM like structure of nodes it will remove redundant whitespace and make - * sure that the node tree is valid according to the specified schema. - * So for example:

    a

    b

    c

    will become

    a

    b

    c

    - * - * @example - * var parser = new tinymce.html.DomParser({validate: true}, schema); - * var rootNode = parser.parse('

    content

    '); + * 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.html.DomParser - * @version 3.4 + * @class tinymce.EditorCommands */ define( - 'tinymce.core.html.DomParser', + 'tinymce.core.EditorCommands', [ - "tinymce.core.html.Node", - "tinymce.core.html.Schema", - "tinymce.core.html.SaxParser", - "tinymce.core.util.Tools" + 'tinymce.core.Env', + 'tinymce.core.InsertContent', + 'tinymce.core.delete.DeleteCommands', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.selection.SelectionBookmark', + 'tinymce.core.util.Tools' ], - function (Node, Schema, SaxParser, Tools) { - var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend; - - var paddEmptyNode = function (settings, node) { - if (settings.padd_empty_with_br) { - node.empty().append(new Node('br', '1')).shortEnded = true; - } else { - node.empty().append(new Node('#text', '3')).value = '\u00a0'; - } - }; - - var hasOnlyChild = function (node, name) { - return node && node.firstChild === node.lastChild && node.firstChild.name === name; - }; + function (Env, InsertContent, DeleteCommands, NodeType, RangeUtils, TreeWalker, SelectionBookmark, 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; - var isPadded = function (schema, node) { - var rule = schema.getElementRule(node.name); - return rule && rule.paddEmpty; - }; + return function (editor) { + var dom, selection, formatter, + commands = { state: {}, exec: {}, value: {} }, + settings = editor.settings, + bookmark; - var isEmpty = function (schema, nonEmptyElements, whitespaceElements, node) { - return node.isEmpty(nonEmptyElements, whitespaceElements, function (node) { - return isPadded(schema, node); + editor.on('PreInit', function () { + dom = editor.dom; + selection = editor.selection; + settings = editor.settings; + formatter = editor.formatter; }); - }; - - /** - * Constructs a new DomParser instance. - * - * @constructor - * @method DomParser - * @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. - */ - return function (settings, schema) { - var self = this, nodeFilters = {}, attributeFilters = [], matchedNodes = {}, matchedAttributes = {}; - - settings = settings || {}; - settings.validate = "validate" in settings ? settings.validate : true; - settings.root_name = settings.root_name || 'body'; - self.schema = schema = schema || new Schema(); - - function fixInvalidChildren(nodes) { - var ni, node, parent, parents, newParent, currentNode, tempNode, childNode, i; - var nonEmptyElements, whitespaceElements, nonSplitableElements, textBlockElements, specialElements, sibling, nextNode; - - nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table'); - nonEmptyElements = schema.getNonEmptyElements(); - whitespaceElements = schema.getWhiteSpaceElements(); - textBlockElements = schema.getTextBlockElements(); - specialElements = schema.getSpecialElements(); - for (ni = 0; ni < nodes.length; ni++) { - node = nodes[ni]; + /** + * 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; - // Already removed or fixed - if (!node.parent || node.fixed) { - continue; - } + if (editor.removed) { + return; + } - // If the invalid element is a text block and the text block is within a parent LI element - // Then unwrap the first text block and convert other sibling text blocks to LI elements similar to Word/Open Office - if (textBlockElements[node.name] && node.parent.name == 'li') { - // Move sibling text blocks after LI element - sibling = node.next; - while (sibling) { - if (textBlockElements[sibling.name]) { - sibling.name = 'li'; - sibling.fixed = true; - node.parent.insert(sibling, node.parent); - } else { - break; - } + if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { + editor.focus(); + } else { + SelectionBookmark.restore(editor); + } - sibling = sibling.next; - } + args = editor.fire('BeforeExecCommand', { command: command, ui: ui, value: value }); + if (args.isDefaultPrevented()) { + return false; + } - // Unwrap current text block - node.unwrap(node); - continue; - } + customCommand = command.toLowerCase(); + if ((func = commands.exec[customCommand])) { + func(customCommand, ui, value); + editor.fire('ExecCommand', { command: command, ui: ui, value: value }); + return true; + } - // Get list of all parent nodes until we find a valid parent to stick the child into - parents = [node]; - for (parent = node.parent; parent && !schema.isValidChild(parent.name, node.name) && - !nonSplitableElements[parent.name]; parent = parent.parent) { - parents.push(parent); + // 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; } + }); - // Found a suitable parent - if (parent && parents.length > 1) { - // Reverse the array since it makes looping easier - parents.reverse(); - - // Clone the related parent and insert that after the moved node - newParent = currentNode = self.filterNode(parents[0].clone()); + if (state) { + return state; + } - // Start cloning and moving children on the left side of the target node - for (i = 0; i < parents.length - 1; i++) { - if (schema.isValidChild(currentNode.name, parents[i].name)) { - tempNode = self.filterNode(parents[i].clone()); - currentNode.append(tempNode); - } else { - tempNode = currentNode; - } + // 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; + } - for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1];) { - nextNode = childNode.next; - tempNode.append(childNode); - childNode = nextNode; - } + // Browser commands + try { + state = editor.getDoc().execCommand(command, ui, value); + } catch (ex) { + // Ignore old IE errors + } - currentNode = tempNode; - } + if (state) { + editor.fire('ExecCommand', { command: command, ui: ui, value: value }); + return true; + } - if (!isEmpty(schema, nonEmptyElements, whitespaceElements, newParent)) { - parent.insert(newParent, parents[0], true); - parent.insert(node, newParent); - } else { - parent.insert(node, parents[0], true); - } + return false; + } - // Check if the element is empty by looking through it's contents and special treatment for


    - parent = parents[0]; - if (isEmpty(schema, nonEmptyElements, whitespaceElements, parent) || hasOnlyChild(parent, 'br')) { - parent.empty().remove(); - } - } else if (node.parent) { - // If it's an LI try to find a UL/OL for it or wrap it - if (node.name === 'li') { - sibling = node.prev; - if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { - sibling.append(node); - continue; - } + /** + * 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; - sibling = node.next; - if (sibling && (sibling.name === 'ul' || sibling.name === 'ul')) { - sibling.insert(node, sibling.firstChild, true); - continue; - } + if (editor.quirks.isHidden() || editor.removed) { + return; + } - node.wrap(self.filterNode(new Node('ul', 1))); - continue; - } + command = command.toLowerCase(); + if ((func = commands.state[command])) { + return func(command); + } - // Try wrapping the element in a DIV - if (schema.isValidChild(node.parent.name, 'div') && schema.isValidChild('div', node.name)) { - node.wrap(self.filterNode(new Node('div', 1))); - } else { - // We failed wrapping it, then remove or unwrap it - if (specialElements[node.name]) { - node.empty().remove(); - } else { - node.unwrap(); - } - } - } + // Browser commands + try { + return editor.getDoc().queryCommandState(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 } + + return false; } /** - * Runs the specified node though the element and attributes filters. + * Queries the command value for example the current fontsize. * - * @method filterNode - * @param {tinymce.html.Node} Node the node to run filters on. - * @return {tinymce.html.Node} The passed in node. + * @method queryCommandValue + * @param {String} command Command to check the value of. + * @return {Object} Command value of false if it's not found. */ - self.filterNode = function (node) { - var i, name, list; - - // Run element filters - if (name in nodeFilters) { - list = matchedNodes[name]; + function queryCommandValue(command) { + var func; - if (list) { - list.push(node); - } else { - matchedNodes[name] = [node]; - } + if (editor.quirks.isHidden() || editor.removed) { + return; } - // Run attribute filters - i = attributeFilters.length; - while (i--) { - name = attributeFilters[i].name; - - if (name in node.attributes.map) { - list = matchedAttributes[name]; - - if (list) { - list.push(node); - } else { - matchedAttributes[name] = [node]; - } - } + command = command.toLowerCase(); + if ((func = commands.value[command])) { + return func(command); } - return node; - }; + // Browser commands + try { + return editor.getDoc().queryCommandValue(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } + } /** - * Adds a node filter function to the parser, the parser will collect the specified nodes by name - * and then execute the callback ones it has finished parsing the document. + * Adds commands to the command collection. * - * @example - * parser.addNodeFilter('p,h1', function(nodes, name) { - * for (var i = 0; i < nodes.length; i++) { - * console.log(nodes[i].name); - * } - * }); - * @method addNodeFilter - * @method {String} name Comma separated list of nodes to collect. - * @param {function} callback Callback function to execute once it has collected nodes. + * @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. */ - self.addNodeFilter = function (name, callback) { - each(explode(name), function (name) { - var list = nodeFilters[name]; - - if (!list) { - nodeFilters[name] = list = []; - } + function addCommands(commandList, type) { + type = type || 'exec'; - list.push(callback); + each(commandList, function (callback, command) { + each(command.toLowerCase().split(','), function (command) { + commands[type][command] = callback; + }); }); - }; + } - /** - * Adds a attribute filter function to the parser, the parser will collect nodes that has the specified attributes - * and then execute the callback ones it has finished parsing the document. - * - * @example - * parser.addAttributeFilter('src,href', function(nodes, name) { - * for (var i = 0; i < nodes.length; i++) { - * console.log(nodes[i].name); - * } - * }); - * @method addAttributeFilter - * @method {String} name Comma separated list of nodes to collect. - * @param {function} callback Callback function to execute once it has collected nodes. - */ - self.addAttributeFilter = function (name, callback) { - each(explode(name), function (name) { - var i; - - for (i = 0; i < attributeFilters.length; i++) { - if (attributeFilters[i].name === name) { - attributeFilters[i].callbacks.push(callback); - return; - } - } - - attributeFilters.push({ name: name, callbacks: [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); + }; + } /** - * Parses the specified HTML string into a DOM like node tree and returns the result. + * Returns true/false if the command is supported or not. * - * @example - * var rootNode = new DomParser({...}).parse('text'); - * @method parse - * @param {String} html Html string to sax parse. - * @param {Object} args Optional args object that gets passed to all filter functions. - * @return {tinymce.html.Node} Root node containing the tree. + * @method queryCommandSupported + * @param {String} command Command that we check support for. + * @return {Boolean} true/false if the command is supported or not. */ - self.parse = function (html, args) { - var parser, rootNode, node, nodes, i, l, fi, fl, list, name, validate; - var blockElements, startWhiteSpaceRegExp, invalidChildren = [], isInWhiteSpacePreservedElement; - var endWhiteSpaceRegExp, allWhiteSpaceRegExp, isAllWhiteSpaceRegExp, whiteSpaceElements; - var children, nonEmptyElements, rootBlockName; - - args = args || {}; - matchedNodes = {}; - matchedAttributes = {}; - blockElements = extend(makeMap('script,style,head,html,body,title,meta,param'), schema.getBlockElements()); - nonEmptyElements = schema.getNonEmptyElements(); - children = schema.children; - validate = settings.validate; - rootBlockName = "forced_root_block" in args ? args.forced_root_block : settings.forced_root_block; + function queryCommandSupported(command) { + command = command.toLowerCase(); - whiteSpaceElements = schema.getWhiteSpaceElements(); - startWhiteSpaceRegExp = /^[ \t\r\n]+/; - endWhiteSpaceRegExp = /[ \t\r\n]+$/; - allWhiteSpaceRegExp = /[ \t\r\n]+/g; - isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/; + if (commands.exec[command]) { + return true; + } - function addRootBlocks() { - var node = rootNode.firstChild, next, rootBlockNode; + // Browser commands + try { + return editor.getDoc().queryCommandSupported(command); + } catch (ex) { + // Fails sometimes see bug: 1896577 + } - // Removes whitespace at beginning and end of block so: - //

    x

    ->

    x

    - function trim(rootBlockNode) { - if (rootBlockNode) { - node = rootBlockNode.firstChild; - if (node && node.type == 3) { - node.value = node.value.replace(startWhiteSpaceRegExp, ''); - } + return false; + } - node = rootBlockNode.lastChild; - if (node && node.type == 3) { - node.value = node.value.replace(endWhiteSpaceRegExp, ''); - } - } - } + function addQueryStateHandler(command, callback, scope) { + command = command.toLowerCase(); + commands.state[command] = function () { + return callback.call(scope || editor); + }; + } - // 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; - } + function addQueryValueHandler(command, callback, scope) { + command = command.toLowerCase(); + commands.value[command] = function () { + return callback.call(scope || editor); + }; + } - while (node) { - next = node.next; + function hasCustomCommand(command) { + command = command.toLowerCase(); + return !!commands.exec[command]; + } - 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; - } + // Expose public methods + extend(this, { + execCommand: execCommand, + queryCommandState: queryCommandState, + queryCommandValue: queryCommandValue, + queryCommandSupported: queryCommandSupported, + addCommands: addCommands, + addCommand: addCommand, + addQueryStateHandler: addQueryStateHandler, + addQueryValueHandler: addQueryValueHandler, + hasCustomCommand: hasCustomCommand + }); - node = next; - } + // Private methods - trim(rootBlockNode); + function execNativeCommand(command, ui, value) { + if (ui === undefined) { + ui = FALSE; } - function createNode(name, type) { - var node = new Node(name, type), list; + if (value === undefined) { + value = null; + } - if (name in nodeFilters) { - list = matchedNodes[name]; + return editor.getDoc().execCommand(command, ui, value); + } - if (list) { - list.push(node); - } else { - matchedNodes[name] = [node]; - } - } + function isFormatMatch(name) { + return formatter.match(name); + } - return node; - } + function toggleFormat(name, value) { + formatter.toggle(name, value ? { value: value } : undefined); + editor.nodeChanged(); + } - function removeWhitespaceBefore(node) { - var textNode, textNodeNext, textVal, sibling, blockElements = schema.getBlockElements(); + function storeSelection(type) { + bookmark = selection.getBookmark(type); + } - for (textNode = node.prev; textNode && textNode.type === 3;) { - textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); + function restoreSelection() { + selection.moveToBookmark(bookmark); + } - // Found a text node with non whitespace then trim that and break - if (textVal.length > 0) { - textNode.value = textVal; - return; - } + // Add execCommand overrides + addCommands({ + // Ignore these, added for compatibility + 'mceResetDesignMode,mceBeginUndoLevel': function () { }, - textNodeNext = textNode.next; + // Add undo manager logic + 'mceEndUndoLevel,mceAddUndoLevel': function () { + editor.undoManager.add(); + }, - // 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; - } + 'Cut,Copy,Paste': function (command) { + var doc = editor.getDoc(), failed; - if (!blockElements[textNodeNext.name] && textNodeNext.name != 'script' && textNodeNext.name != 'style') { - textNode = textNode.prev; - continue; - } - } + // Try executing the native command + try { + execNativeCommand(command); + } catch (ex) { + // Command failed + failed = TRUE; + } - sibling = textNode.prev; - textNode.remove(); - textNode = sibling; + // 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; } - } - function cloneAndExcludeBlocks(input) { - var name, output = {}; + // 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." + ); - for (name in input) { - if (name !== 'li' && name != 'p') { - output[name] = input[name]; + if (Env.mac) { + msg = msg.replace(/Ctrl\+/g, '\u2318+'); } - } - return output; - } + editor.notificationManager.open({ text: msg, type: 'error' }); + } + }, - parser = new SaxParser({ - validate: validate, - allow_script_urls: settings.allow_script_urls, - allow_conditional_comments: settings.allow_conditional_comments, + // Override unlink command + unlink: function () { + if (selection.isCollapsed()) { + var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); + if (elm) { + editor.dom.remove(elm, true); + } - // Exclude P and LI from DOM parsing since it's treated better by the DOM parser - self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), + return; + } - cdata: function (text) { - node.append(createNode('#cdata', 4)).value = text; - }, + formatter.remove("link"); + }, - text: function (text, raw) { - var textNode; + // Override justify commands to use the text formatter engine + 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone': function (command) { + var align = command.substring(7); - // Trim all redundant whitespace on non white space elements - if (!isInWhiteSpacePreservedElement) { - text = text.replace(allWhiteSpaceRegExp, ' '); + if (align == 'full') { + align = 'justify'; + } - if (node.lastChild && blockElements[node.lastChild.name]) { - text = text.replace(startWhiteSpaceRegExp, ''); - } + // Remove all other alignments first + each('left,center,right,justify'.split(','), function (name) { + if (align != name) { + formatter.remove('align' + name); } + }); - // Do we need to create the node - if (text.length !== 0) { - textNode = createNode('#text', 3); - textNode.raw = !!raw; - node.append(textNode).value = text; - } - }, + if (align != 'none') { + toggleFormat('align' + align); + } + }, - comment: function (text) { - node.append(createNode('#comment', 8)).value = text; - }, + // Override list commands to fix WebKit bug + 'InsertUnorderedList,InsertOrderedList': function (command) { + var listElm, listParent; - pi: function (name, text) { - node.append(createNode(name, 7)).value = text; - removeWhitespaceBefore(node); - }, + execNativeCommand(command); - doctype: function (text) { - var newNode; + // 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; - newNode = node.append(createNode('#doctype', 10)); - newNode.value = text; - removeWhitespaceBefore(node); - }, + // 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(); + } + } + }, - start: function (name, attrs, empty) { - var newNode, attrFiltersLen, elementRule, attrName, parent; + // Override commands to use the text formatter engine + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { + toggleFormat(command); + }, - elementRule = validate ? schema.getElementRule(name) : {}; - if (elementRule) { - newNode = createNode(elementRule.outputName || name, 1); - newNode.attributes = attrs; - newNode.shortEnded = empty; + // Override commands to use the text formatter engine + 'ForeColor,HiliteColor,FontName': function (command, ui, value) { + toggleFormat(command, value); + }, - node.append(newNode); + FontSize: function (command, ui, value) { + var fontClasses, fontSizes; - // 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); - } + // 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); - attrFiltersLen = attributeFilters.length; - while (attrFiltersLen--) { - attrName = attributeFilters[attrFiltersLen].name; + if (fontClasses) { + value = fontClasses[value - 1] || value; + } else { + value = fontSizes[value - 1] || value; + } + } - if (attrName in attrs.map) { - list = matchedAttributes[attrName]; + toggleFormat(command, value); + }, - if (list) { - list.push(newNode); - } else { - matchedAttributes[attrName] = [newNode]; - } - } - } + RemoveFormat: function (command) { + formatter.remove(command); + }, - // Trim whitespace before block - if (blockElements[name]) { - removeWhitespaceBefore(newNode); - } + mceBlockQuote: function () { + toggleFormat('blockquote'); + }, - // Change current node if the element wasn't empty i.e not
    or - if (!empty) { - node = newNode; - } + FormatBlock: function (command, ui, value) { + return toggleFormat(value || 'p'); + }, - // Check if we are inside a whitespace preserved element - if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { - isInWhiteSpacePreservedElement = true; - } - } - }, + mceCleanup: function () { + var bookmark = selection.getBookmark(); - end: function (name) { - var textNode, elementRule, text, sibling, tempNode; + editor.setContent(editor.getContent({ cleanup: TRUE }), { cleanup: TRUE }); - elementRule = validate ? schema.getElementRule(name) : {}; - if (elementRule) { - if (blockElements[name]) { - if (!isInWhiteSpacePreservedElement) { - // Trim whitespace of the first node in a block - textNode = node.firstChild; - if (textNode && textNode.type === 3) { - text = textNode.value.replace(startWhiteSpaceRegExp, ''); + selection.moveToBookmark(bookmark); + }, - // Any characters left after trim or should we remove it - if (text.length > 0) { - textNode.value = text; - textNode = textNode.next; - } else { - sibling = textNode.next; - textNode.remove(); - textNode = sibling; + mceRemoveNode: function (command, ui, value) { + var node = value || selection.getNode(); - // Remove any pure whitespace siblings - while (textNode && textNode.type === 3) { - text = textNode.value; - sibling = textNode.next; + // Make sure that the body node isn't removed + if (node != editor.getBody()) { + storeSelection(); + editor.dom.remove(node, TRUE); + restoreSelection(); + } + }, - if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { - textNode.remove(); - textNode = sibling; - } + mceSelectNodeDepth: function (command, ui, value) { + var counter = 0; - textNode = sibling; - } - } - } + dom.getParent(selection.getNode(), function (node) { + if (node.nodeType == 1 && counter++ == value) { + selection.select(node); + return FALSE; + } + }, editor.getBody()); + }, - // Trim whitespace of the last node in a block - textNode = node.lastChild; - if (textNode && textNode.type === 3) { - text = textNode.value.replace(endWhiteSpaceRegExp, ''); + mceSelectNode: function (command, ui, value) { + selection.select(value); + }, - // Any characters left after trim or should we remove it - if (text.length > 0) { - textNode.value = text; - textNode = textNode.prev; - } else { - sibling = textNode.prev; - textNode.remove(); - textNode = sibling; + mceInsertContent: function (command, ui, value) { + InsertContent.insertAtCaret(editor, value); + }, - // Remove any pure whitespace siblings - while (textNode && textNode.type === 3) { - text = textNode.value; - sibling = textNode.prev; + mceInsertRawHTML: function (command, ui, value) { + selection.setContent('tiny_mce_marker'); + editor.setContent( + editor.getContent().replace(/tiny_mce_marker/g, function () { + return value; + }) + ); + }, - if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) { - textNode.remove(); - textNode = sibling; - } + mceToggleFormat: function (command, ui, value) { + toggleFormat(value); + }, - textNode = sibling; - } - } - } - } + mceSetContent: function (command, ui, value) { + editor.setContent(value); + }, - // Trim start white space - // Removed due to: #5424 - /*textNode = node.prev; - if (textNode && textNode.type === 3) { - text = textNode.value.replace(startWhiteSpaceRegExp, ''); + 'Indent,Outdent': function (command) { + var intentValue, indentUnit, value; - if (text.length > 0) - textNode.value = text; - else - textNode.remove(); - }*/ - } + // Setup indent level + intentValue = settings.indentation; + indentUnit = /[a-z%]+$/i.exec(intentValue); + intentValue = parseInt(intentValue, 10); - // Check if we exited a whitespace preserved element - if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) { - isInWhiteSpacePreservedElement = false; - } + 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'); + } - // Handle empty nodes - if (elementRule.removeEmpty || elementRule.paddEmpty) { - if (isEmpty(schema, nonEmptyElements, whiteSpaceElements, node)) { - if (elementRule.paddEmpty) { - paddEmptyNode(settings, node); - } else { - // Leave nodes that have a name like - if (!node.attributes.map.name && !node.attributes.map.id) { - tempNode = node.parent; + each(selection.getSelectedBlocks(), function (element) { + if (dom.getContentEditable(element) === "false") { + return; + } - if (blockElements[node.name]) { - node.empty().remove(); - } else { - node.unwrap(); - } + 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'; - node = tempNode; - return; - } - } + 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); } } - - node = node.parent; - } + }); + } else { + execNativeCommand(command); } - }, schema); - - rootNode = node = new Node(args.context || settings.root_name, 11); + }, - parser.parse(html); + mceRepaint: function () { + }, - // Fix invalid children or report invalid children in a contextual parsing - if (validate && invalidChildren.length) { - if (!args.context) { - fixInvalidChildren(invalidChildren); - } else { - args.invalid = true; - } - } + InsertHorizontalRule: function () { + editor.execCommand('mceInsertContent', false, '
    '); + }, - // Wrap nodes in the root into block elements if the root is body - if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) { - addRootBlocks(); - } + mceToggleVisualAid: function () { + editor.hasVisual = !editor.hasVisual; + editor.addVisual(); + }, - // Run filters only when the contents is valid - if (!args.invalid) { - // Run node filters - for (name in matchedNodes) { - list = nodeFilters[name]; - nodes = matchedNodes[name]; + mceReplaceContent: function (command, ui, value) { + editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({ format: 'text' }))); + }, - // Remove already removed children - fi = nodes.length; - while (fi--) { - if (!nodes[fi].parent) { - nodes.splice(fi, 1); - } - } + mceInsertLink: function (command, ui, value) { + var anchor; - for (i = 0, l = list.length; i < l; i++) { - list[i](nodes, name, args); - } + if (typeof value == 'string') { + value = { href: value }; } - // Run attribute filters - for (i = 0, l = attributeFilters.length; i < l; i++) { - list = attributeFilters[i]; - - if (list.name in matchedAttributes) { - nodes = matchedAttributes[list.name]; + anchor = dom.getParent(selection.getNode(), 'a'); - // Remove already removed children - fi = nodes.length; - while (fi--) { - if (!nodes[fi].parent) { - nodes.splice(fi, 1); - } - } + // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. + value.href = value.href.replace(' ', '%20'); - for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) { - list.callbacks[fi](nodes, list.name, args); - } - } + // Remove existing links if there could be child links or that the href isn't specified + if (!anchor || !value.href) { + formatter.remove('link'); } - } - return rootNode; - }; + // Apply new link to selection + if (value.href) { + formatter.apply('link', value, anchor); + } + }, - // Remove
    at end of block elements Gecko and WebKit injects BR elements to - // make it possible to place the caret inside empty blocks. This logic tries to remove - // these elements and keep br elements that where intended to be there intact - if (settings.remove_trailing_brs) { - self.addNodeFilter('br', function (nodes) { - var i, l = nodes.length, node, blockElements = extend({}, schema.getBlockElements()); - var nonEmptyElements = schema.getNonEmptyElements(), parent, lastParent, prev, prevName; - var whiteSpaceElements = schema.getNonEmptyElements(); - var elementRule, textNode; + selectAll: function () { + var editingHost = dom.getParent(selection.getStart(), NodeType.isContentEditableTrue); + if (editingHost) { + var rng = dom.createRng(); + rng.selectNodeContents(editingHost); + selection.setRng(rng); + } + }, - // Remove brs from body element as well - blockElements.body = 1; + "delete": function () { + DeleteCommands.deleteCommand(editor); + }, - // Must loop forwards since it will otherwise remove all brs in

    a


    - for (i = 0; i < l; i++) { - node = nodes[i]; - parent = node.parent; + "forwardDelete": function () { + DeleteCommands.forwardDeleteCommand(editor); + }, - if (blockElements[node.parent.name] && node === parent.lastChild) { - // Loop all nodes to the left of the current node and check for other BR elements - // excluding bookmarks since they are invisible - prev = node.prev; - while (prev) { - prevName = prev.name; + mceNewDocument: function () { + editor.setContent(''); + }, - // Ignore bookmarks - if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') { - // Found a non BR element - if (prevName !== "br") { - break; - } + 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); - // Found another br it's a

    structure then don't remove anything - if (prevName === 'br') { - node = null; - break; - } - } + var offset = rng.startOffset; + var container = rng.startContainer; - prev = prev.prev; - } + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + var isAfterLastNodeInContainer = offset > container.childNodes.length - 1; - if (node) { - node.remove(); + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } + } - // Is the parent to be considered empty after we removed the BR - if (isEmpty(schema, nonEmptyElements, whiteSpaceElements, parent)) { - elementRule = schema.getElementRule(parent.name); + 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 - // Remove or padd the element depending on schema rule - if (elementRule) { - if (elementRule.removeEmpty) { - parent.remove(); - } else if (elementRule.paddEmpty) { - paddEmptyNode(settings, parent); - } - } - } - } - } else { - // Replaces BR elements inside inline elements like


    - // so they become

     

    - lastParent = node; - while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { - lastParent = parent; + // 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; + } - if (blockElements[parent.name]) { - break; - } + // 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(); - parent = parent.parent; + while ((node = walker.next())) { + if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { + return true; } + } + } - if (lastParent === parent && settings.padd_empty_with_br !== true) { - textNode = new Node('#text', 3); - textNode.value = '\u00a0'; - node.replace(textNode); - } + 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); - self.addAttributeFilter('href', function (nodes) { - var i = nodes.length, node; + // 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); + } - var appendRel = function (rel) { - var parts = rel.split(' ').filter(function (p) { - return p.length > 0; - }); - return parts.concat(['noopener']).sort().join(' '); - }; + // Insert temp marker and scroll to that + marker = dom.create('span', {}, ' '); + brElm.parentNode.insertBefore(marker, brElm); + selection.scrollIntoView(marker); + dom.remove(marker); - var addNoOpener = function (rel) { - var newRel = rel ? Tools.trim(rel) : ''; - if (!/\b(noopener)\b/g.test(newRel)) { - return appendRel(newRel); + if (!extraBr) { + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); } else { - return newRel; + rng.setStartBefore(brElm); + rng.setEndBefore(brElm); } - }; - 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'))); - } - } + selection.setRng(rng); + editor.undoManager.add(); + + return TRUE; } }); - // 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; + // 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; + }, - while (i--) { - node = nodes[i]; - if (node.name === 'a' && node.firstChild && !node.attr('href')) { - parent = node.parent; + 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function (command) { + return isFormatMatch(command); + }, - // Move children after current node - sibling = node.lastChild; - do { - prevSibling = sibling.prev; - parent.insert(sibling, node); - sibling = prevSibling; - } while (sibling); - } - } - }); - } + mceBlockQuote: function () { + return isFormatMatch('blockquote'); + }, - if (settings.fix_list_elements) { - self.addNodeFilter('ul,ol', function (nodes) { - var i = nodes.length, node, parentNode; + Outdent: function () { + var node; - while (i--) { - node = nodes[i]; - parentNode = node.parent; + if (settings.inline_styles) { + if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { + return TRUE; + } - 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 ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) { + return TRUE; } } - }); - } - 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; + return ( + queryCommandState('InsertUnorderedList') || + queryCommandState('InsertOrderedList') || + (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE')) + ); + }, - validClassesMap = validClasses['*']; - if (validClassesMap && validClassesMap[className]) { - valid = true; - } + 'InsertUnorderedList,InsertOrderedList': function (command) { + var list = dom.getParent(selection.getNode(), 'ul,ol'); - validClassesMap = validClasses[node.name]; - if (!valid && validClassesMap && validClassesMap[className]) { - valid = true; - } + return list && + ( + command === 'insertunorderedlist' && list.tagName === 'UL' || + command === 'insertorderedlist' && list.tagName === 'OL' + ); + } + }, 'state'); - if (valid) { - if (classValue) { - classValue += ' '; - } + // Add queryCommandValue overrides + addCommands({ + 'FontSize,FontName': function (command) { + var value = 0, parent; - classValue += className; - } + if ((parent = dom.getParent(selection.getNode(), 'span'))) { + if (command == 'fontsize') { + value = parent.style.fontSize; + } else { + value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase(); } + } - if (!classValue.length) { - classValue = null; - } + return value; + } + }, 'value'); - node.attr('class', classValue); - } - }); - } + // Add undo manager logic + addCommands({ + Undo: function () { + editor.undoManager.undo(); + }, + + Redo: function () { + editor.undoManager.redo(); + } + }); }; } ); + /** - * Writer.js + * EventDispatcher.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -33630,363 +31370,296 @@ define( */ /** - * This class is used to write HTML tags out it can be used with the Serializer or the SaxParser. + * This class lets you add/remove and fire events by name on the specified scope. This makes + * it easy to add event listener logic to any class. * - * @class tinymce.html.Writer + * @class tinymce.util.EventDispatcher * @example - * var writer = new tinymce.html.Writer({indent: true}); - * var parser = new tinymce.html.SaxParser(writer).parse('


    '); - * console.log(writer.getContent()); + * var eventDispatcher = new EventDispatcher(); * - * @class tinymce.html.Writer - * @version 3.4 + * eventDispatcher.on('click', function() {console.log('data');}); + * eventDispatcher.fire('click', {data: 123}); */ define( - 'tinymce.core.html.Writer', + 'tinymce.core.util.EventDispatcher', [ - "tinymce.core.html.Entities", "tinymce.core.util.Tools" ], - function (Entities, Tools) { - var makeMap = Tools.makeMap; + function (Tools) { + var nativeEvents = Tools.makeMap( + "focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange " + + "mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover " + + "draggesture dragdrop drop drag submit " + + "compositionstart compositionend compositionupdate touchstart touchmove touchend", + ' ' + ); - /** - * Constructs a new Writer instance. - * - * @constructor - * @method Writer - * @param {Object} settings Name/value settings object. - */ - return function (settings) { - var html = [], indent, indentBefore, indentAfter, encode, htmlOutput; + function Dispatcher(settings) { + var self = this, scope, bindings = {}, toggleEvent; - settings = settings || {}; - indent = settings.indent; - indentBefore = makeMap(settings.indent_before || ''); - indentAfter = makeMap(settings.indent_after || ''); - encode = Entities.getEncodeFunc(settings.entity_encoding || 'raw', settings.entities); - htmlOutput = settings.element_format == "html"; + function returnFalse() { + return false; + } - return { - /** - * Writes the a start element such as

    . - * - * @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; + function returnTrue() { + return true; + } - if (indent && indentBefore[name] && html.length > 0) { - value = html[html.length - 1]; + settings = settings || {}; + scope = settings.scope || self; + toggleEvent = settings.toggleEvent || returnFalse; - if (value.length > 0 && value !== '\n') { - html.push('\n'); - } - } + /** + * Fires the specified event by name. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object?} args Event arguments. + * @return {Object} Event args instance passed in. + * @example + * instance.fire('event', {...}); + */ + function fire(name, args) { + var handlers, i, l, callback; - html.push('<', name); + name = name.toLowerCase(); + args = args || {}; + args.type = 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]; + // Setup target is there isn't one + if (!args.target) { + args.target = scope; + } - if (value.length > 0 && value !== '\n') { - html.push('\n'); - } - } - }, + // Add event delegation methods if they are missing + if (!args.preventDefault) { + // Add preventDefault method + args.preventDefault = function () { + args.isDefaultPrevented = returnTrue; + }; - /** - * Writes the a end element such as

    . - * - * @method end - * @param {String} name Name of the element. - */ - end: function (name) { - var value; + // Add stopPropagation + args.stopPropagation = function () { + args.isPropagationStopped = returnTrue; + }; - /*if (indent && indentBefore[name] && html.length > 0) { - value = html[html.length - 1]; + // Add stopImmediatePropagation + args.stopImmediatePropagation = function () { + args.isImmediatePropagationStopped = returnTrue; + }; - if (value.length > 0 && value !== '\n') - html.push('\n'); - }*/ + // Add event delegation states + args.isDefaultPrevented = returnFalse; + args.isPropagationStopped = returnFalse; + args.isImmediatePropagationStopped = returnFalse; + } - html.push(''); + if (settings.beforeFire) { + settings.beforeFire(args); + } - if (indent && indentAfter[name] && html.length > 0) { - value = html[html.length - 1]; + handlers = bindings[name]; + if (handlers) { + for (i = 0, l = handlers.length; i < l; i++) { + callback = handlers[i]; - if (value.length > 0 && value !== '\n') { - html.push('\n'); + // Unbind handlers marked with "once" + if (callback.once) { + off(name, callback.func); } - } - }, - - /** - * Writes a text node. - * - * @method text - * @param {String} text String to write out. - * @param {Boolean} raw Optional raw state if true the contents wont get encoded. - */ - text: function (text, raw) { - if (text.length > 0) { - html[html.length] = raw ? text : encode(text); - } - }, - - /** - * Writes a cdata node such as . - * - * @method cdata - * @param {String} text String to write out inside the cdata. - */ - cdata: function (text) { - html.push(''); - }, - /** - * Writes a comment node such as . - * - * @method cdata - * @param {String} text String to write out inside the comment. - */ - comment: function (text) { - html.push(''); - }, - - /** - * Writes a PI node such as . - * - * @method pi - * @param {String} name Name of the pi. - * @param {String} text String to write out inside the pi. - */ - pi: function (name, text) { - if (text) { - html.push(''); - } else { - html.push(''); - } + // Stop immediate propagation if needed + if (args.isImmediatePropagationStopped()) { + args.stopPropagation(); + return args; + } - if (indent) { - html.push('\n'); + // If callback returns false then prevent default and stop all propagation + if (callback.func.call(scope, args) === false) { + args.preventDefault(); + return args; + } } - }, - - /** - * Writes a doctype node such as . - * - * @method doctype - * @param {String} text String to write out inside the doctype. - */ - doctype: function (text) { - html.push('', indent ? '\n' : ''); - }, - - /** - * Resets the internal buffer if one wants to reuse the writer. - * - * @method reset - */ - reset: function () { - html.length = 0; - }, - - /** - * Returns the contents that got serialized. - * - * @method getContent - * @return {String} HTML contents that got written down. - */ - getContent: function () { - return html.join('').replace(/\n$/, ''); } - }; - }; - } -); -/** - * 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 down the DOM tree into a string using a Writer instance. - * - * - * @example - * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('

    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; + return args; + } /** - * Serializes the specified node into a string. + * Binds an event listener to a specific event by name. * + * @method on + * @param {String} name Event name or space separated list of events to bind. + * @param {callback} callback Callback to be executed when the event occurs. + * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. + * @return {Object} Current class instance. * @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. + * instance.on('event', function(e) { + * // Callback logic + * }); */ - self.serialize = function (node) { - var handlers, validate; - - validate = settings.validate; - - handlers = { - // #text - 3: function (node) { - writer.text(node.value, node.raw); - }, + function on(name, callback, prepend, extra) { + var handlers, names, i; - // #comment - 8: function (node) { - writer.comment(node.value); - }, + if (callback === false) { + callback = returnFalse; + } - // Processing instruction - 7: function (node) { - writer.pi(node.name, node.value); - }, + if (callback) { + callback = { + func: callback + }; - // Doctype - 10: function (node) { - writer.doctype(node.value); - }, + if (extra) { + Tools.extend(callback, extra); + } - // CDATA - 4: function (node) { - writer.cdata(node.value); - }, + names = name.toLowerCase().split(' '); + i = names.length; + while (i--) { + name = names[i]; + handlers = bindings[name]; + if (!handlers) { + handlers = bindings[name] = []; + toggleEvent(name, true); + } - // Document fragment - 11: function (node) { - if ((node = node.firstChild)) { - do { - walk(node); - } while ((node = node.next)); + if (prepend) { + handlers.unshift(callback); + } else { + handlers.push(callback); } } - }; - - 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; + return self; + } - // Sort attributes - if (validate && attrs && attrs.length > 1) { - sortedAttrs = []; - sortedAttrs.map = {}; + /** + * Unbinds an event listener to a specific event by name. + * + * @method off + * @param {String?} name Name of the event to unbind. + * @param {callback?} callback Callback to unbind. + * @return {Object} Current class instance. + * @example + * // Unbind specific callback + * instance.off('event', handler); + * + * // Unbind all listeners by name + * instance.off('event'); + * + * // Unbind all events + * instance.off(); + */ + function off(name, callback) { + var i, handlers, bindingName, names, hi; - elementRule = schema.getElementRule(node.name); - if (elementRule) { - for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { - attrName = elementRule.attributesOrder[i]; + if (name) { + names = name.toLowerCase().split(' '); + i = names.length; + while (i--) { + name = names[i]; + handlers = bindings[name]; - if (attrName in attrs.map) { - attrValue = attrs.map[attrName]; - sortedAttrs.map[attrName] = attrValue; - sortedAttrs.push({ name: attrName, value: attrValue }); - } - } + // Unbind all handlers + if (!name) { + for (bindingName in bindings) { + toggleEvent(bindingName, false); + delete bindings[bindingName]; + } - for (i = 0, l = attrs.length; i < l; i++) { - attrName = attrs[i].name; + return self; + } - if (!(attrName in sortedAttrs.map)) { - attrValue = attrs.map[attrName]; - sortedAttrs.map[attrName] = attrValue; - sortedAttrs.push({ name: attrName, value: attrValue }); + if (handlers) { + // Unbind all by name + if (!callback) { + handlers.length = 0; + } else { + // Unbind specific ones + hi = handlers.length; + while (hi--) { + if (handlers[hi].func === callback) { + handlers = handlers.slice(0, hi).concat(handlers.slice(hi + 1)); + bindings[name] = handlers; } } - - attrs = sortedAttrs; } - } - - writer.start(node.name, attrs, isEmpty); - if (!isEmpty) { - if ((node = node.firstChild)) { - do { - walk(node); - } while ((node = node.next)); + if (!handlers.length) { + toggleEvent(name, false); + delete bindings[name]; } - - 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); + for (name in bindings) { + toggleEvent(name, false); + } + + bindings = {}; } - return writer.getContent(); - }; + return self; + } + + /** + * Binds an event listener to a specific event by name + * and automatically unbind the event once the callback fires. + * + * @method once + * @param {String} name Event name or space separated list of events to bind. + * @param {callback} callback Callback to be executed when the event occurs. + * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. + * @return {Object} Current class instance. + * @example + * instance.once('event', function(e) { + * // Callback logic + * }); + */ + function once(name, callback, prepend) { + return on(name, callback, prepend, { once: true }); + } + + /** + * Returns true/false if the dispatcher has a event of the specified name. + * + * @method has + * @param {String} name Name of the event to check for. + * @return {Boolean} true/false if the event exists or not. + */ + function has(name) { + name = name.toLowerCase(); + return !(!bindings[name] || bindings[name].length === 0); + } + + // Expose + self.fire = fire; + self.on = on; + self.off = off; + self.once = once; + self.has = has; + } + + /** + * Returns true/false if the specified event name is a native browser event or not. + * + * @method isNative + * @param {String} name Name to check if it's native. + * @return {Boolean} true/false if the event is native or not. + * @static + */ + Dispatcher.isNative = function (name) { + return !!nativeEvents[name.toLowerCase()]; }; + + return Dispatcher; } ); /** - * Serializer.js + * Observable.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -33996,490 +31669,432 @@ define( */ /** - * 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. + * This mixin will add event binding logic to classes. * - * @class tinymce.dom.Serializer + * @mixin tinymce.util.Observable */ define( - 'tinymce.core.dom.Serializer', + 'tinymce.core.util.Observable', [ - "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" + "tinymce.core.util.EventDispatcher" ], - 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

    - */ - function trimTrailingBr(rootNode) { - var brNode1, brNode2; - - function isBr(node) { - return node && node.name === 'br'; + function (EventDispatcher) { + function getEventDispatcher(obj) { + if (!obj._eventDispatcher) { + obj._eventDispatcher = new EventDispatcher({ + scope: obj, + toggleEvent: function (name, state) { + if (EventDispatcher.isNative(name) && obj.toggleNativeEvent) { + obj.toggleNativeEvent(name, state); + } + } + }); } - brNode1 = rootNode.lastChild; - if (isBr(brNode1)) { - brNode2 = brNode1.prev; + return obj._eventDispatcher; + } - if (isBr(brNode2)) { - brNode1.remove(); - brNode2.remove(); + return { + /** + * Fires the specified event by name. Consult the + *
    event reference for more details on each event. + * + * @method fire + * @param {String} name Name of the event to fire. + * @param {Object?} args Event arguments. + * @param {Boolean?} bubble True/false if the event is to be bubbled. + * @return {Object} Event args instance passed in. + * @example + * instance.fire('event', {...}); + */ + fire: function (name, args, bubble) { + var self = this; + + // Prevent all events except the remove event after the instance has been removed + if (self.removed && name !== "remove") { + return args; } - } - } - /** - * Constructs a new DOM serializer class. - * - * @constructor - * @method Serializer - * @param {Object} settings Serializer settings object. - * @param {tinymce.Editor} editor Optional editor to bind events to and get schema/dom from. - */ - return function (settings, editor) { - var dom, schema, htmlParser, tempAttrs = ["data-mce-selected"]; + args = getEventDispatcher(self).fire(name, args, bubble); - if (editor) { - dom = editor.dom; - schema = editor.schema; - } + // Bubble event up to parents + if (bubble !== false && self.parent) { + var parent = self.parent(); + while (parent && !args.isPropagationStopped()) { + parent.fire(name, args, false); + parent = parent.parent(); + } + } - function trimHtml(html) { - var trimContentRegExp = new RegExp([ - ']+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers - '\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected - ].join('|'), 'gi'); + return args; + }, - html = Zwsp.trim(html.replace(trimContentRegExp, '')); + /** + * Binds an event listener to a specific event by name. Consult the + * event reference for more details on each event. + * + * @method on + * @param {String} name Event name or space separated list of events to bind. + * @param {callback} callback Callback to be executed when the event occurs. + * @param {Boolean} first Optional flag if the event should be prepended. Use this with care. + * @return {Object} Current class instance. + * @example + * instance.on('event', function(e) { + * // Callback logic + * }); + */ + on: function (name, callback, prepend) { + return getEventDispatcher(this).on(name, callback, prepend); + }, - return html; - } + /** + * Unbinds an event listener to a specific event by name. Consult the + * event reference for more details on each event. + * + * @method off + * @param {String?} name Name of the event to unbind. + * @param {callback?} callback Callback to unbind. + * @return {Object} Current class instance. + * @example + * // Unbind specific callback + * instance.off('event', handler); + * + * // Unbind all listeners by name + * instance.off('event'); + * + * // Unbind all events + * instance.off(); + */ + off: function (name, callback) { + return getEventDispatcher(this).off(name, callback); + }, - function trimContent(html) { - var content = html; - var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g; - var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema; + /** + * Bind the event callback and once it fires the callback is removed. Consult the + * event reference for more details on each event. + * + * @method once + * @param {String} name Name of the event to bind. + * @param {callback} callback Callback to bind only once. + * @return {Object} Current class instance. + */ + once: function (name, callback) { + return getEventDispatcher(this).once(name, callback); + }, - content = trimHtml(content); - shortEndedElements = schema.getShortEndedElements(); + /** + * Returns true/false if the object has a event of the specified name. + * + * @method hasEventListeners + * @param {String} name Name of the event to check for. + * @return {Boolean} true/false if the event exists or not. + */ + hasEventListeners: function (name) { + return getEventDispatcher(this).has(name); + } + }; + } +); +/** + * EditorObservable.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 + */ - // Remove all bogus elements marked with "all" - while ((matches = bogusAllRegExp.exec(content))) { - index = bogusAllRegExp.lastIndex; - matchLength = matches[0].length; +/** + * This mixin contains the event logic for the tinymce.Editor class. + * + * @mixin tinymce.EditorObservable + * @extends tinymce.util.Observable + */ +define( + 'tinymce.core.EditorObservable', + [ + "tinymce.core.util.Observable", + "tinymce.core.dom.DOMUtils", + "tinymce.core.util.Tools" + ], + function (Observable, DOMUtils, Tools) { + var DOM = DOMUtils.DOM, customEventRootDelegates; - if (shortEndedElements[matches[1]]) { - endTagIndex = index; - } else { - endTagIndex = SaxParser.findEndTag(schema, content, index); - } + /** + * Returns the event target so for the specified event. Some events fire + * only on document, some fire on documentElement etc. This also handles the + * custom event root setting where it returns that element instead of the body. + * + * @private + * @param {tinymce.Editor} editor Editor instance to get event target from. + * @param {String} eventName Name of the event for example "click". + * @return {Element/Document} HTML Element or document target to bind on. + */ + function getEventTarget(editor, eventName) { + if (eventName == 'selectionchange') { + return editor.getDoc(); + } - content = content.substring(0, index - matchLength) + content.substring(endTagIndex); - bogusAllRegExp.lastIndex = index - matchLength; + // Need to bind mousedown/mouseup etc to document not body in iframe mode + // Since the user might click on the HTML element not the BODY + if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) { + return editor.getDoc().documentElement; + } + + // Bind to event root instead of body if it's defined + if (editor.settings.event_root) { + if (!editor.eventRoot) { + editor.eventRoot = DOM.select(editor.settings.event_root)[0]; } - return content; + return editor.eventRoot; } - /** - * Returns a trimmed version of the editor contents to be used for the undo level. This - * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also - * remove the data-mce-selected attributes used for selection of objects and caret containers. - * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will - * be removed by the serialization logic when you save. - * - * @private - * @return {String} HTML contents of the editor excluding some internal bogus elements. - */ - function getTrimmedContent() { - return trimContent(editor.getBody().innerHTML); - } + return editor.getBody(); + } - function addTempAttr(name) { - if (Tools.inArray(tempAttrs, name) === -1) { - htmlParser.addAttributeFilter(name, function (nodes, name) { - var i = nodes.length; + /** + * Binds a event delegate for the specified name this delegate will fire + * the event to the editor dispatcher. + * + * @private + * @param {tinymce.Editor} editor Editor instance to get event target from. + * @param {String} eventName Name of the event for example "click". + */ + function bindEventDelegate(editor, eventName) { + var eventRootElm, delegate; - while (i--) { - nodes[i].attr(name, null); - } - }); + function isListening(editor) { + return !editor.hidden && !editor.readonly; + } - tempAttrs.push(name); - } + if (!editor.delegates) { + editor.delegates = {}; } - // Default DOM and Schema if they are undefined - dom = dom || DOM; - schema = schema || new Schema(settings); - settings.entity_encoding = settings.entity_encoding || 'named'; - settings.remove_trailing_brs = "remove_trailing_brs" in settings ? settings.remove_trailing_brs : true; + if (editor.delegates[eventName] || editor.removed) { + return; + } - htmlParser = new DomParser(settings, schema); + eventRootElm = getEventTarget(editor, eventName); - // Convert tabindex back to elements when serializing contents - htmlParser.addAttributeFilter('data-mce-tabindex', function (nodes, name) { - var i = nodes.length, node; + if (editor.settings.event_root) { + if (!customEventRootDelegates) { + customEventRootDelegates = {}; + editor.editorManager.on('removeEditor', function () { + var name; - while (i--) { - node = nodes[i]; - node.attr('tabindex', node.attributes.map['data-mce-tabindex']); - node.attr(name, null); + if (!editor.editorManager.activeEditor) { + if (customEventRootDelegates) { + for (name in customEventRootDelegates) { + editor.dom.unbind(getEventTarget(editor, name)); + } + + customEventRootDelegates = null; + } + } + }); } - }); - // Convert move data-mce-src, data-mce-href and data-mce-style into nodes or process them if needed - htmlParser.addAttributeFilter('src,href,style', function (nodes, name) { - var i = nodes.length, node, value, internalName = 'data-mce-' + name; - var urlConverter = settings.url_converter, urlConverterScope = settings.url_converter_scope, undef; + if (customEventRootDelegates[eventName]) { + return; + } - while (i--) { - node = nodes[i]; + delegate = function (e) { + var target = e.target, editors = editor.editorManager.get(), i = editors.length; - value = node.attributes.map[internalName]; - if (value !== undef) { - // Set external name to internal value and remove internal - node.attr(name, value.length > 0 ? value : null); - node.attr(internalName, null); - } else { - // No internal attribute found then convert the value we have in the DOM - value = node.attributes.map[name]; + while (i--) { + var body = editors[i].getBody(); - if (name === "style") { - value = dom.serializeStyle(dom.parseStyle(value), node.name); - } else if (urlConverter) { - value = urlConverter.call(urlConverterScope, value, name, node.name); + if (body === target || DOM.isChildOf(target, body)) { + if (isListening(editors[i])) { + editors[i].fire(eventName, e); + } } + } + }; - node.attr(name, value.length > 0 ? value : null); + customEventRootDelegates[eventName] = delegate; + DOM.bind(eventRootElm, eventName, delegate); + } else { + delegate = function (e) { + if (isListening(editor)) { + editor.fire(eventName, e); } - } - }); + }; - // Remove internal classes mceItem<..> or mceSelected - htmlParser.addAttributeFilter('class', function (nodes) { - var i = nodes.length, node, value; + DOM.bind(eventRootElm, eventName, delegate); + editor.delegates[eventName] = delegate; + } + } - while (i--) { - node = nodes[i]; - value = node.attr('class'); + var EditorObservable = { + /** + * Bind any pending event delegates. This gets executed after the target body/document is created. + * + * @private + */ + bindPendingEventDelegates: function () { + var self = this; - if (value) { - value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, ''); - node.attr('class', value.length > 0 ? value : null); - } - } - }); + Tools.each(self._pendingNativeEvents, function (name) { + bindEventDelegate(self, name); + }); + }, - // Remove bookmark elements - htmlParser.addAttributeFilter('data-mce-type', function (nodes, name, args) { - var i = nodes.length, node; + /** + * Toggles a native event on/off this is called by the EventDispatcher when + * the first native event handler is added and when the last native event handler is removed. + * + * @private + */ + toggleNativeEvent: function (name, state) { + var self = this; - while (i--) { - node = nodes[i]; + // Never bind focus/blur since the FocusManager fakes those + if (name == "focus" || name == "blur") { + return; + } - if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) { - node.remove(); + if (state) { + if (self.initialized) { + bindEventDelegate(self, name); + } else { + if (!self._pendingNativeEvents) { + self._pendingNativeEvents = [name]; + } else { + self._pendingNativeEvents.push(name); + } } + } else if (self.initialized) { + self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); + delete self.delegates[name]; } - }); - - htmlParser.addNodeFilter('noscript', function (nodes) { - var i = nodes.length, node; + }, - while (i--) { - node = nodes[i].firstChild; + /** + * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc. + * + * @private + */ + unbindAllNativeEvents: function () { + var self = this, name; - if (node) { - node.value = Entities.decode(node.value); + if (self.delegates) { + for (name in self.delegates) { + self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); } - } - }); - // Force script into CDATA sections and remove the mce- prefix also add comments around styles - htmlParser.addNodeFilter('script,style', function (nodes, name) { - var i = nodes.length, node, value, type; + delete self.delegates; + } - function trim(value) { - /*jshint maxlen:255 */ - /*eslint max-len:0 */ - return value.replace(/()/g, '\n') - .replace(/^[\r\n]*|[\r\n]*$/g, '') - .replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, ''); + if (!self.inline) { + self.getBody().onload = null; + self.dom.unbind(self.getWin()); + self.dom.unbind(self.getDoc()); } - while (i--) { - node = nodes[i]; - value = node.firstChild ? node.firstChild.value : ''; + self.dom.unbind(self.getBody()); + self.dom.unbind(self.getContainer()); + } + }; - if (name === "script") { - // Remove mce- prefix from script elements and remove default type since the user specified - // a script element without type attribute - type = node.attr('type'); - if (type) { - node.attr('type', type == 'mce-no/type' ? null : type.replace(/^mce\-/, '')); - } + EditorObservable = Tools.extend({}, Observable, EditorObservable); - if (value.length > 0) { - node.firstChild.value = '// '; - } - } else { - if (value.length > 0) { - node.firstChild.value = ''; - } - } - } - }); + return EditorObservable; + } +); - // Convert comments to cdata and handle protected comments - htmlParser.addNodeFilter('#comment', function (nodes) { - var i = nodes.length, node; +/** + * ErrorReporter.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 + */ - while (i--) { - node = nodes[i]; +/** + * Various error reporting helper functions. + * + * @class tinymce.ErrorReporter + * @private + */ +define( + 'tinymce.core.ErrorReporter', + [ + 'global!window', + 'tinymce.core.AddOnManager' + ], + function (window, AddOnManager) { + var PluginManager = AddOnManager.PluginManager; - if (node.value.indexOf('[CDATA[') === 0) { - node.name = '#cdata'; - node.type = 4; - node.value = node.value.replace(/^\[CDATA\[|\]\]$/g, ''); - } else if (node.value.indexOf('mce:protected ') === 0) { - node.name = "#text"; - node.type = 3; - node.raw = true; - node.value = unescape(node.value).substr(14); - } + var resolvePluginName = function (targetUrl, suffix) { + for (var name in PluginManager.urls) { + var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js'; + if (matchUrl === targetUrl) { + return name; } - }); + } - htmlParser.addNodeFilter('xml:namespace,input', function (nodes, name) { - var i = nodes.length, node; + return null; + }; - while (i--) { - node = nodes[i]; - if (node.type === 7) { - node.remove(); - } else if (node.type === 1) { - if (name === "input" && !("type" in node.attributes.map)) { - node.attr('type', 'text'); - } - } - } + var pluginUrlToMessage = function (editor, url) { + var plugin = resolvePluginName(url, editor.suffix); + return plugin ? + 'Failed to load plugin: ' + plugin + ' from url ' + url : + 'Failed to load plugin url: ' + url; + }; + + var displayNotification = function (editor, message) { + editor.notificationManager.open({ + type: 'error', + text: message }); + }; - // Remove internal data attributes - htmlParser.addAttributeFilter( - 'data-mce-src,data-mce-href,data-mce-style,' + - 'data-mce-selected,data-mce-expando,' + - 'data-mce-type,data-mce-resize', + var displayError = function (editor, message) { + if (editor._skinLoaded) { + displayNotification(editor, message); + } else { + editor.on('SkinLoaded', function () { + displayNotification(editor, message); + }); + } + }; - function (nodes, name) { - var i = nodes.length; + var uploadError = function (editor, message) { + displayError(editor, 'Failed to upload image: ' + message); + }; - while (i--) { - nodes[i].attr(name, null); - } + var pluginLoadError = function (editor, url) { + displayError(editor, pluginUrlToMessage(editor, url)); + }; + + var initError = function (message) { + var console = window.console; + if (console && !window.test) { // Skip test env + if (console.error) { + console.error.apply(console, arguments); + } else { + console.log.apply(console, arguments); } - ); + } + }; - // Return public methods - return { - /** - * Schema instance that was used to when the Serializer was constructed. - * - * @field {tinymce.html.Schema} schema - */ - schema: schema, - - /** - * Adds a node filter function to the parser used by the serializer, the parser will collect the specified nodes by name - * and then execute the callback ones it has finished parsing the document. - * - * @example - * parser.addNodeFilter('p,h1', function(nodes, name) { - * for (var i = 0; i < nodes.length; i++) { - * console.log(nodes[i].name); - * } - * }); - * @method addNodeFilter - * @method {String} name Comma separated list of nodes to collect. - * @param {function} callback Callback function to execute once it has collected nodes. - */ - addNodeFilter: htmlParser.addNodeFilter, - - /** - * Adds a attribute filter function to the parser used by the serializer, the parser will - * collect nodes that has the specified attributes - * and then execute the callback ones it has finished parsing the document. - * - * @example - * parser.addAttributeFilter('src,href', function(nodes, name) { - * for (var i = 0; i < nodes.length; i++) { - * console.log(nodes[i].name); - * } - * }); - * @method addAttributeFilter - * @method {String} name Comma separated list of nodes to collect. - * @param {function} callback Callback function to execute once it has collected nodes. - */ - addAttributeFilter: htmlParser.addAttributeFilter, - - /** - * Serializes the specified browser DOM node into a HTML string. - * - * @method serialize - * @param {DOMNode} node DOM node to serialize. - * @param {Object} args Arguments option that gets passed to event handlers. - */ - serialize: function (node, args) { - var self = this, impl, doc, oldDoc, htmlSerializer, content, rootNode; - - // Explorer won't clone contents of script and style and the - // selected index of select elements are cleared on a clone operation. - if (Env.ie && dom.select('script,style,select,map').length > 0) { - content = node.innerHTML; - node = node.cloneNode(false); - dom.setHTML(node, content); - } else { - node = node.cloneNode(true); - } - - // Nodes needs to be attached to something in WebKit/Opera - // This fix will make DOM ranges and make Sizzle happy! - impl = document.implementation; - if (impl.createHTMLDocument) { - // Create an empty HTML document - doc = impl.createHTMLDocument(""); - - // Add the element or it's children if it's a body element to the new document - each(node.nodeName == 'BODY' ? node.childNodes : [node], function (node) { - doc.body.appendChild(doc.importNode(node, true)); - }); - - // Grab first child or body element for serialization - if (node.nodeName != 'BODY') { - node = doc.body.firstChild; - } else { - node = doc.body; - } - - // set the new document in DOMUtils so createElement etc works - oldDoc = dom.doc; - dom.doc = doc; - } - - args = args || {}; - args.format = args.format || 'html'; - - // Don't wrap content if we want selected html - if (args.selection) { - args.forced_root_block = ''; - } - - // Pre process - if (!args.no_events) { - args.node = node; - self.onPreProcess(args); - } - - // Parse HTML - content = Zwsp.trim(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node))); - rootNode = htmlParser.parse(content, args); - trimTrailingBr(rootNode); - - // Serialize HTML - htmlSerializer = new Serializer(settings, schema); - args.content = htmlSerializer.serialize(rootNode); - - // Post process - if (!args.no_events) { - self.onPostProcess(args); - } - - // Restore the old document if it was changed - if (oldDoc) { - dom.doc = oldDoc; - } - - args.node = null; - - return args.content; - }, - - /** - * Adds valid elements rules to the serializers schema instance this enables you to specify things - * like what elements should be outputted and what attributes specific elements might have. - * Consult the Wiki for more details on this format. - * - * @method addRules - * @param {String} rules Valid elements rules string to add to schema. - */ - addRules: function (rules) { - schema.addValidElements(rules); - }, - - /** - * Sets the valid elements rules to the serializers schema instance this enables you to specify things - * like what elements should be outputted and what attributes specific elements might have. - * Consult the Wiki for more details on this format. - * - * @method setRules - * @param {String} rules Valid elements rules string. - */ - setRules: function (rules) { - schema.setValidElements(rules); - }, - - onPreProcess: function (args) { - if (editor) { - editor.fire('PreProcess', args); - } - }, - - onPostProcess: function (args) { - if (editor) { - editor.fire('PostProcess', args); - } - }, - - /** - * Adds a temporary internal attribute these attributes will get removed on undo and - * when getting contents out of the editor. - * - * @method addTempAttr - * @param {String} name string - */ - addTempAttr: addTempAttr, - - // Internal - trimHtml: trimHtml, - getTrimmedContent: getTrimmedContent, - trimContent: trimContent - }; + return { + pluginLoadError: pluginLoadError, + uploadError: uploadError, + displayError: displayError, + initError: initError }; } ); - /** - * DeleteUtils.js + * CaretContainerInput.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -34488,93 +32103,84 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * This module shows the invisble block that the caret is currently in when contents is added to that block. + */ define( - 'tinymce.core.delete.DeleteUtils', + 'tinymce.core.caret.CaretContainerInput', [ - 'ephox.katamari.api.Option', - 'ephox.sugar.api.dom.Compare', + 'ephox.katamari.api.Fun', 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.PredicateFind', - 'tinymce.core.dom.ElementType' + 'ephox.sugar.api.search.SelectorFind', + 'tinymce.core.caret.CaretContainer' ], - function (Option, Compare, Element, PredicateFind, ElementType) { - var isBeforeRoot = function (rootNode) { - return function (elm) { - return Compare.eq(rootNode, Element.fromDom(elm.dom().parentNode)); - }; + function (Fun, Element, SelectorFind, CaretContainer) { + var findBlockCaretContainer = function (editor) { + return SelectorFind.descendant(Element.fromDom(editor.getBody()), '*[data-mce-caret]').fold(Fun.constant(null), function (elm) { + return elm.dom(); + }); }; - var getParentBlock = function (rootNode, elm) { - return Compare.contains(rootNode, elm) ? PredicateFind.closest(elm, function (element) { - return ElementType.isTextBlock(element) || ElementType.isListItem(element); - }, isBeforeRoot(rootNode)) : Option.none(); + var removeIeControlRect = function (editor) { + editor.selection.setRng(editor.selection.getRng()); }; - var placeCaretInEmptyBody = function (editor) { - var body = editor.getBody(); - var node = body.firstChild && editor.dom.isBlock(body.firstChild) ? body.firstChild : body; - editor.selection.setCursorLocation(node, 0); + var showBlockCaretContainer = function (editor, blockCaretContainer) { + if (blockCaretContainer.hasAttribute('data-mce-caret')) { + CaretContainer.showCaretContainerBlock(blockCaretContainer); + removeIeControlRect(editor); + editor.selection.scrollIntoView(blockCaretContainer); + } }; - var paddEmptyBody = function (editor) { - if (editor.dom.isEmpty(editor.getBody())) { - editor.setContent(''); - placeCaretInEmptyBody(editor); + var handleBlockContainer = function (editor, e) { + var blockCaretContainer = findBlockCaretContainer(editor); + + if (!blockCaretContainer) { + return; } + + if (e.type === 'compositionstart') { + e.preventDefault(); + e.stopPropagation(); + showBlockCaretContainer(blockCaretContainer); + return; + } + + if (CaretContainer.hasContent(blockCaretContainer)) { + showBlockCaretContainer(editor, blockCaretContainer); + } + }; + + var setup = function (editor) { + editor.on('keyup compositionstart', Fun.curry(handleBlockContainer, editor)); }; return { - getParentBlock: getParentBlock, - paddEmptyBody: paddEmptyBody + setup: setup }; } ); - define( - 'ephox.sugar.api.search.SelectorExists', + 'ephox.sand.api.XMLHttpRequest', [ - 'ephox.sugar.api.search.SelectorFind' + 'ephox.sand.util.Global' ], - 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 + function (Global) { + /* + * IE8 and above per + * https://developer.mozilla.org/en/docs/XMLHttpRequest + */ + return function () { + var f = Global.getOrDie('XMLHttpRequest'); + return new f(); }; } ); - /** - * Empty.js + * Uploader.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -34583,357 +32189,303 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Upload blobs or blob infos to the specified URL or handler. + * + * @private + * @class tinymce.file.Uploader + * @example + * var uploader = new Uploader({ + * url: '/upload.php', + * basePath: '/base/path', + * credentials: true, + * handler: function(data, success, failure) { + * ... + * } + * }); + * + * uploader.upload(blobInfos).then(function(result) { + * ... + * }); + */ define( - 'tinymce.core.dom.Empty', + 'tinymce.core.file.Uploader', [ - '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' + 'ephox.sand.api.XMLHttpRequest', + 'global!window', + 'tinymce.core.util.Fun', + 'tinymce.core.util.Promise', + 'tinymce.core.util.Tools' ], - 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)); - }; + function (XMLHttpRequest, window, Fun, Promise, Tools) { + return function (uploadStatus, settings) { + var pendingPromises = {}; - var isWhitespace = function (rootNode, node) { - return NodeType.isText(node) && /^[ \t\r\n]*$/.test(node.data) && hasWhitespacePreserveParent(rootNode, node) === false; - }; + function pathJoin(path1, path2) { + if (path1) { + return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); + } - var isNamedAnchor = function (node) { - return NodeType.isElement(node) && node.nodeName === 'A' && node.hasAttribute('name'); - }; + return path2; + } - var isContent = function (rootNode, node) { - return (CaretCandidate.isCaretCandidate(node) && isWhitespace(rootNode, node) === false) || isNamedAnchor(node) || isBookmark(node); - }; + function defaultHandler(blobInfo, success, failure, progress) { + var xhr, formData; - var isBookmark = NodeType.hasAttribute('data-mce-bookmark'); - var isBogus = NodeType.hasAttribute('data-mce-bogus'); - var isBogusAll = NodeType.hasAttributeValue('data-mce-bogus', 'all'); + xhr = new XMLHttpRequest(); + xhr.open('POST', settings.url); + xhr.withCredentials = settings.credentials; - var isEmptyNode = function (targetNode) { - var walker, node, brCount = 0; + xhr.upload.onprogress = function (e) { + progress(e.loaded / e.total * 100); + }; - if (isContent(targetNode, targetNode)) { - return false; - } else { - node = targetNode.firstChild; - if (!node) { - return true; - } + xhr.onerror = function () { + failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status); + }; - walker = new TreeWalker(node, targetNode); - do { - if (isBogusAll(node)) { - node = walker.next(true); - continue; - } + xhr.onload = function () { + var json; - if (isBogus(node)) { - node = walker.next(); - continue; + if (xhr.status < 200 || xhr.status >= 300) { + failure("HTTP Error: " + xhr.status); + return; } - if (NodeType.isBr(node)) { - brCount++; - node = walker.next(); - continue; - } + json = JSON.parse(xhr.responseText); - if (isContent(targetNode, node)) { - return false; + if (!json || typeof json.location != "string") { + failure("Invalid JSON: " + xhr.responseText); + return; } - node = walker.next(); - } while (node); + success(pathJoin(settings.basePath, json.location)); + }; - return brCount <= 1; + formData = new window.FormData(); // TODO: Stick this in sand + formData.append('file', blobInfo.blob(), blobInfo.filename()); + + xhr.send(formData); } - }; - var isEmpty = function (elm) { - return isEmptyNode(elm.dom()); - }; + function noUpload() { + return new Promise(function (resolve) { + resolve([]); + }); + } - return { - isEmpty: isEmpty - }; - } -); + function handlerSuccess(blobInfo, url) { + return { + url: url, + blobInfo: blobInfo, + status: true + }; + } -/** - * 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 - */ + function handlerFailure(blobInfo, error) { + return { + url: '', + blobInfo: blobInfo, + status: false, + error: error + }; + } -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'); + function resolvePending(blobUri, result) { + Tools.each(pendingPromises[blobUri], function (resolve) { + resolve(result); + }); - var getBlockPosition = function (rootNode, pos) { - var rootElm = Element.fromDom(rootNode); - var containerElm = Element.fromDom(pos.container()); - return DeleteUtils.getParentBlock(rootElm, containerElm).map(function (block) { - return BlockPosition(block, pos); - }); - }; + delete pendingPromises[blobUri]; + } - var isDifferentBlocks = function (blockBoundary) { - return Compare.eq(blockBoundary.from().block(), blockBoundary.to().block()) === false; - }; + function uploadBlobInfo(blobInfo, handler, openNotification) { + uploadStatus.markPending(blobInfo.blobUri()); - 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(); - }; + return new Promise(function (resolve) { + var notification, progress; - var isEditable = function (blockBoundary) { - return NodeType.isContentEditableFalse(blockBoundary.from().block()) === false && NodeType.isContentEditableFalse(blockBoundary.to().block()) === false; - }; + var noop = function () { + }; - 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; - } - }; + try { + var closeNotification = function () { + if (notification) { + notification.close(); + progress = noop; // Once it's closed it's closed + } + }; - 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.Compare', - '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.ElementType', - 'tinymce.core.dom.Empty', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.Parents' - ], - function (Arr, Option, Compare, Insert, Remove, Element, Traverse, CaretFinder, CaretPosition, ElementType, Empty, NodeType, Parents) { - var getChildrenUntilBlockBoundary = function (block) { - var children = Traverse.children(block); - return Arr.findIndex(children, ElementType.isBlock).fold( - function () { - return children; - }, - function (index) { - return children.slice(0, index); - } - ); - }; + var success = function (url) { + closeNotification(); + uploadStatus.markUploaded(blobInfo.blobUri(), url); + resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url)); + resolve(handlerSuccess(blobInfo, url)); + }; - var extractChildren = function (block) { - var children = getChildrenUntilBlockBoundary(block); + var failure = function (error) { + closeNotification(); + uploadStatus.removeFailed(blobInfo.blobUri()); + resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error)); + resolve(handlerFailure(blobInfo, error)); + }; - Arr.each(children, function (node) { - Remove.remove(node); - }); + progress = function (percent) { + if (percent < 0 || percent > 100) { + return; + } - return children; - }; + if (!notification) { + notification = openNotification(); + } - var trimBr = function (first, block) { - CaretFinder.positionIn(first, block.dom()).each(function (position) { - var node = position.getNode(); - if (NodeType.isBr(node)) { - Remove.remove(Element.fromDom(node)); - } - }); - }; + notification.progressBar.value(percent); + }; - var removeEmptyRoot = function (rootNode, block) { - var parents = Parents.parentsAndSelf(block, rootNode); - return Arr.find(parents.reverse(), Empty.isEmpty).each(Remove.remove); - }; + handler(blobInfo, success, failure, progress); + } catch (ex) { + resolve(handlerFailure(blobInfo, ex.message)); + } + }); + } - var findParentInsertPoint = function (toBlock, block) { - var parents = Traverse.parents(block, function (elm) { - return Compare.eq(elm, toBlock); - }); + function isDefaultHandler(handler) { + return handler === defaultHandler; + } - return Option.from(parents[parents.length - 2]); - }; + function pendingUploadBlobInfo(blobInfo) { + var blobUri = blobInfo.blobUri(); - var getInsertionPoint = function (fromBlock, toBlock) { - if (Compare.contains(toBlock, fromBlock)) { - return Traverse.parent(fromBlock).bind(function (parent) { - return Compare.eq(parent, toBlock) ? Option.some(fromBlock) : findParentInsertPoint(toBlock, fromBlock); + return new Promise(function (resolve) { + pendingPromises[blobUri] = pendingPromises[blobUri] || []; + pendingPromises[blobUri].push(resolve); }); - } else { - return Option.none(); } - }; - - var mergeBlockInto = function (rootNode, fromBlock, toBlock) { - if (Empty.isEmpty(toBlock)) { - Remove.remove(toBlock); - return CaretFinder.firstPositionIn(fromBlock.dom()); - } else { - trimBr(true, fromBlock); - trimBr(false, toBlock); - - var children = extractChildren(fromBlock); - return getInsertionPoint(fromBlock, toBlock).fold( - function () { - removeEmptyRoot(rootNode, fromBlock); + function uploadBlobs(blobInfos, openNotification) { + blobInfos = Tools.grep(blobInfos, function (blobInfo) { + return !uploadStatus.isUploaded(blobInfo.blobUri()); + }); - var position = CaretFinder.lastPositionIn(toBlock.dom()); + return Promise.all(Tools.map(blobInfos, function (blobInfo) { + return uploadStatus.isPending(blobInfo.blobUri()) ? + pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification); + })); + } - Arr.each(children, function (node) { - Insert.append(toBlock, node); - }); + function upload(blobInfos, openNotification) { + return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification); + } - return position; - }, - function (target) { - var position = CaretFinder.prevPosition(toBlock.dom(), CaretPosition.before(target.dom())); + settings = Tools.extend({ + credentials: false, + // We are adding a notify argument to this (at the moment, until it doesn't work) + handler: defaultHandler + }, settings); - Arr.each(children, function (node) { - Insert.before(target, node); - }); + return { + upload: upload + }; + }; + } +); +define( + 'ephox.sand.api.Blob', - removeEmptyRoot(rootNode, fromBlock); + [ + 'ephox.sand.util.Global' + ], - return position; - } - ); - } + function (Global) { + /* + * IE10 and above per + * https://developer.mozilla.org/en-US/docs/Web/API/Blob + */ + return function (parts, properties) { + var f = Global.getOrDie('Blob'); + return new f(parts, properties); }; + } +); +define( + 'ephox.sand.api.FileReader', - var mergeBlocks = function (rootNode, forward, block1, block2) { - return forward ? mergeBlockInto(rootNode, block2, block1) : mergeBlockInto(rootNode, block1, block2); - }; + [ + 'ephox.sand.util.Global' + ], - return { - mergeBlocks: mergeBlocks + function (Global) { + /* + * IE10 and above per + * https://developer.mozilla.org/en-US/docs/Web/API/FileReader + */ + return function () { + var f = Global.getOrDie('FileReader'); + return new f(); }; } ); +define( + 'ephox.sand.api.Uint8Array', -/** - * 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 - */ + [ + 'ephox.sand.util.Global' + ], + function (Global) { + /* + * https://developer.mozilla.org/en-US/docs/Web/API/Uint8Array + * + * IE10 and above per + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays + */ + return function (arr) { + var f = Global.getOrDie('Uint8Array'); + return new f(arr); + }; + } +); define( - 'tinymce.core.delete.BlockBoundaryDelete', + 'ephox.sand.api.Window', + [ - 'ephox.sugar.api.node.Element', - 'tinymce.core.delete.BlockBoundary', - 'tinymce.core.delete.MergeBlocks' + 'ephox.sand.util.Global' ], - function (Element, BlockBoundary, MergeBlocks) { - var backspaceDelete = function (editor, forward) { - var position, rootNode = Element.fromDom(editor.getBody()); - position = BlockBoundary.read(rootNode.dom(), forward, editor.selection.getRng()).bind(function (blockBoundary) { - return MergeBlocks.mergeBlocks(rootNode, forward, blockBoundary.from().block(), blockBoundary.to().block()); - }); + 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). + ****************************************************************************************** + */ - position.each(function (pos) { - editor.selection.setRng(pos.toRange()); - }); + /* + * 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); + }; - return position.isSome(); + /* + * 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 { - backspaceDelete: backspaceDelete + atob: atob, + requestAnimationFrame: requestAnimationFrame }; } ); - /** - * BlockRangeDelete.js + * Conversions.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -34942,185 +32494,129 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Converts blob/uris back and forth. + * + * @private + * @class tinymce.file.Conversions + */ define( - 'tinymce.core.delete.BlockRangeDelete', + 'tinymce.core.file.Conversions', [ - 'ephox.katamari.api.Options', - 'ephox.sugar.api.dom.Compare', - 'ephox.sugar.api.node.Element', - 'tinymce.core.caret.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.delete.DeleteUtils', - 'tinymce.core.delete.MergeBlocks' + 'ephox.sand.api.Blob', + 'ephox.sand.api.FileReader', + 'ephox.sand.api.Uint8Array', + 'ephox.sand.api.Window', + 'ephox.sand.api.XMLHttpRequest', + 'tinymce.core.util.Promise' ], - function (Options, Compare, Element, CaretFinder, CaretPosition, DeleteUtils, MergeBlocks) { - var deleteRangeMergeBlocks = function (rootNode, selection) { - var rng = selection.getRng(); - - return Options.liftN([ - DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.startContainer)), - DeleteUtils.getParentBlock(rootNode, Element.fromDom(rng.endContainer)) - ], function (block1, block2) { - if (Compare.eq(block1, block2) === false) { - rng.deleteContents(); - - MergeBlocks.mergeBlocks(rootNode, true, block1, block2).each(function (pos) { - selection.setRng(pos.toRange()); - }); + function (Blob, FileReader, Uint8Array, Window, XMLHttpRequest, Promise) { + function blobUriToBlob(url) { + return new Promise(function (resolve, reject) { - return true; - } else { - return false; - } - }).getOr(false); - }; + var rejectWithError = function () { + reject("Cannot convert " + url + " to Blob. Resource might not exist or is inaccessible."); + }; - var isEverythingSelected = function (rootNode, rng) { - var noPrevious = CaretFinder.prevPosition(rootNode.dom(), CaretPosition.fromRangeStart(rng)).isNone(); - var noNext = CaretFinder.nextPosition(rootNode.dom(), CaretPosition.fromRangeEnd(rng)).isNone(); - return noPrevious && noNext; - }; + try { + var xhr = new XMLHttpRequest(); - var emptyEditor = function (editor) { - editor.setContent(''); - editor.selection.setCursorLocation(); - return true; - }; + xhr.open('GET', url, true); + xhr.responseType = 'blob'; - var deleteRange = function (editor) { - var rootNode = Element.fromDom(editor.getBody()); - var rng = editor.selection.getRng(); - return isEverythingSelected(rootNode, rng) ? emptyEditor(editor) : deleteRangeMergeBlocks(rootNode, editor.selection); - }; + xhr.onload = function () { + if (this.status == 200) { + resolve(this.response); + } else { + // IE11 makes it into onload but responds with status 500 + rejectWithError(); + } + }; - var backspaceDelete = function (editor, forward) { - return editor.selection.isCollapsed() ? false : deleteRange(editor, editor.selection.getRng()); - }; + // Chrome fires an error event instead of the exception + // Also there seems to be no way to intercept the message that is logged to the console + xhr.onerror = rejectWithError; - return { - backspaceDelete: backspaceDelete - }; - } -); + xhr.send(); + } catch (ex) { + rejectWithError(); + } + }); + } -define( - 'ephox.katamari.api.Adt', + function parseDataUri(uri) { + var type, matches; - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Obj', - 'ephox.katamari.api.Type', - 'global!Array', - 'global!Error', - 'global!console' - ], + uri = decodeURIComponent(uri).split(','); - 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'); - } - if (cases.length === 0) { - throw new Error('there must be at least one case'); + matches = /data:([^;]+)/.exec(uri[0]); + if (matches) { + type = matches[1]; } - var constructors = [ ]; - - // adt is mutated to add the individual cases - var adt = {}; - Arr.each(cases, function (acase, count) { - var keys = Obj.keys(acase); + return { + type: type, + data: uri[1] + }; + } - // validation - if (keys.length !== 1) { - throw new Error('one and only one name per case'); - } + function dataUriToBlob(uri) { + return new Promise(function (resolve) { + var str, arr, i; - var key = keys[0]; - var value = acase[key]; + uri = parseDataUri(uri); - // 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'); + // Might throw error if data isn't proper base64 + try { + str = Window.atob(uri.data); + } catch (e) { + resolve(new Blob([])); + return; } - 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]; + arr = new Uint8Array(str.length); + for (i = 0; i < arr.length; i++) { + arr[i] = str.charCodeAt(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(',')); - } + resolve(new Blob([arr], { type: uri.type })); + }); + } - var allReqd = Arr.forall(constructors, function (reqKey) { - return Arr.contains(branchKeys, reqKey); - }); + function uriToBlob(url) { + if (url.indexOf('blob:') === 0) { + return blobUriToBlob(url); + } - if (!allReqd) throw new Error('Not all branches were specified when using match. Specified: ' + branchKeys.join(', ') + '\nRequired: ' + constructors.join(', ')); + if (url.indexOf('data:') === 0) { + return dataUriToBlob(url); + } - return branches[key].apply(null, args); - }; + return null; + } - // - // 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, + function blobToDataUri(blob) { + return new Promise(function (resolve) { + var reader = new FileReader(); - // NOTE: Only for debugging. - log: function (label) { - console.log(label, { - constructors: constructors, - constructor: key, - params: args - }); - } - }; + reader.onloadend = function () { + resolve(reader.result); }; + + reader.readAsDataURL(blob); }); + } - return adt; - }; return { - generate: generate + uriToBlob: uriToBlob, + blobToDataUri: blobToDataUri, + parseDataUri: parseDataUri }; } ); /** - * CefDeleteAction.js + * ImageScanner.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -35129,285 +32625,173 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris. + * + * @private + * @class tinymce.file.ImageScanner + */ define( - 'tinymce.core.delete.CefDeleteAction', + 'tinymce.core.file.ImageScanner', [ - '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' + "tinymce.core.util.Promise", + "tinymce.core.util.Arr", + "tinymce.core.util.Fun", + "tinymce.core.file.Conversions", + "tinymce.core.Env" ], - function (Adt, Option, Element, CaretFinder, CaretPosition, CaretUtils, DeleteUtils, Empty, NodeType) { - var DeleteAction = Adt.generate([ - { remove: [ 'element' ] }, - { moveToElement: [ 'element' ] }, - { moveToPosition: [ 'position' ] } - ]); + function (Promise, Arr, Fun, Conversions, Env) { + var count = 0; - 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 uniqueId = function (prefix) { + return (prefix || 'blobid') + (count++); }; - var deleteEmptyBlockOrMoveToCef = function (rootNode, forward, from, to) { - var toCefElm = to.getNode(forward === false); - return DeleteUtils.getParentBlock(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 imageToBlobInfo = function (blobCache, img, resolve, reject) { + var base64, blobInfo; - 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)); + if (img.src.indexOf('blob:') === 0) { + blobInfo = blobCache.getByUri(img.src); + + if (blobInfo) { + resolve({ + image: img, + blobInfo: blobInfo + }); } else { - return Option.none(); - } - }); - }; + Conversions.uriToBlob(img.src).then(function (blob) { + Conversions.blobToDataUri(blob).then(function (dataUri) { + base64 = Conversions.parseDataUri(dataUri).data; + blobInfo = blobCache.create(uniqueId(), blob, base64); + blobCache.add(blobInfo); - 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(); - } - }; + resolve({ + image: img, + blobInfo: blobInfo + }); + }); + }, function (err) { + reject(err); + }); + } - 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); + return; } - }; - var read = function (rootNode, forward, rng) { - var normalizedRange = CaretUtils.normalizeRange(forward ? 1 : -1, rootNode, rng); - var from = CaretPosition.fromRangeStart(normalizedRange); + base64 = Conversions.parseDataUri(img.src).data; + blobInfo = blobCache.findFirst(function (cachedBlobInfo) { + return cachedBlobInfo.base64() === base64; + }); - 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())); + if (blobInfo) { + resolve({ + image: img, + blobInfo: blobInfo + }); } else { - return getContentEditableAction(rootNode, forward, from); + Conversions.uriToBlob(img.src).then(function (blob) { + blobInfo = blobCache.create(uniqueId(), blob, base64); + blobCache.add(blobInfo); + + resolve({ + image: img, + blobInfo: blobInfo + }); + }, function (err) { + reject(err); + }); } }; - return { - read: read + var getAllImages = function (elm) { + return elm ? elm.getElementsByTagName('img') : []; }; - } -); -/** - * 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 - */ + return function (uploadStatus, blobCache) { + var cachedPromises = {}; -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.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.Empty', - 'tinymce.core.dom.NodeType' - ], - function (Fun, Option, Options, Insert, Remove, Element, Node, PredicateFind, Traverse, CaretCandidate, CaretFinder, CaretPosition, Empty, NodeType) { - 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(); - }; + function findAll(elm, predicate) { + var images, promises; - var reposition = function (elm, pos) { - return needsReposition(pos, elm) ? new CaretPosition(pos.container(), pos.offset() - 1) : pos; - }; + if (!predicate) { + predicate = Fun.constant(true); + } - var beforeOrStartOf = function (node) { - return NodeType.isText(node) ? new CaretPosition(node, 0) : CaretPosition.before(node); - }; + images = Arr.filter(getAllImages(elm), function (img) { + var src = img.src; - var afterOrEndOf = function (node) { - return NodeType.isText(node) ? new CaretPosition(node, node.data.length) : CaretPosition.after(node); - }; + if (!Env.fileApi) { + return false; + } - var getPreviousSiblingCaretPosition = function (elm) { - if (CaretCandidate.isCaretCandidate(elm.previousSibling)) { - return Option.some(afterOrEndOf(elm.previousSibling)); - } else { - return elm.previousSibling ? CaretFinder.lastPositionIn(elm.previousSibling) : Option.none(); - } - }; + if (img.hasAttribute('data-mce-bogus')) { + return false; + } - var getNextSiblingCaretPosition = function (elm) { - if (CaretCandidate.isCaretCandidate(elm.nextSibling)) { - return Option.some(beforeOrStartOf(elm.nextSibling)); - } else { - return elm.nextSibling ? CaretFinder.firstPositionIn(elm.nextSibling) : Option.none(); - } - }; + if (img.hasAttribute('data-mce-placeholder')) { + return false; + } - var findCaretPositionBackwardsFromElm = function (rootElement, elm) { - var startPosition = CaretPosition.before(elm.previousSibling ? elm.previousSibling : elm.parentNode); - return CaretFinder.prevPosition(rootElement, startPosition).fold( - function () { - return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)); - }, - Option.some - ); - }; + if (!src || src == Env.transparentSrc) { + return false; + } - var findCaretPositionForwardsFromElm = function (rootElement, elm) { - return CaretFinder.nextPosition(rootElement, CaretPosition.after(elm)).fold( - function () { - return CaretFinder.prevPosition(rootElement, CaretPosition.before(elm)); - }, - Option.some - ); - }; + if (src.indexOf('blob:') === 0) { + return !uploadStatus.isUploaded(src); + } - var findCaretPositionBackwards = function (rootElement, elm) { - return getPreviousSiblingCaretPosition(elm).orThunk(function () { - return getNextSiblingCaretPosition(elm); - }).orThunk(function () { - return findCaretPositionBackwardsFromElm(rootElement, elm); - }); - }; + if (src.indexOf('data:') === 0) { + return predicate(img); + } - var findCaretPositionForward = function (rootElement, elm) { - return getNextSiblingCaretPosition(elm).orThunk(function () { - return getPreviousSiblingCaretPosition(elm); - }).orThunk(function () { - return findCaretPositionForwardsFromElm(rootElement, elm); - }); - }; + return false; + }); - var findCaretPosition = function (forward, rootElement, elm) { - return forward ? findCaretPositionForward(rootElement, elm) : findCaretPositionBackwards(rootElement, elm); - }; + promises = Arr.map(images, function (img) { + var newPromise; - var findCaretPosOutsideElmAfterDelete = function (forward, rootElement, elm) { - return findCaretPosition(forward, rootElement, elm).map(Fun.curry(reposition, elm)); - }; + if (cachedPromises[img.src]) { + // Since the cached promise will return the cached image + // We need to wrap it and resolve with the actual image + return new Promise(function (resolve) { + cachedPromises[img.src].then(function (imageInfo) { + if (typeof imageInfo === 'string') { // error apparently + return imageInfo; + } + resolve({ + image: img, + blobInfo: imageInfo.blobInfo + }); + }); + }); + } - var setSelection = function (editor, forward, pos) { - pos.fold( - function () { - editor.focus(); - }, - function (pos) { - editor.selection.setRng(pos.toRange(), forward); - } - ); - }; + newPromise = new Promise(function (resolve, reject) { + imageToBlobInfo(blobCache, img, resolve, reject); + }).then(function (result) { + delete cachedPromises[result.image.src]; + return result; + })['catch'](function (error) { + delete cachedPromises[img.src]; + return error; + }); - var eqRawNode = function (rawNode) { - return function (elm) { - return elm.dom() === rawNode; - }; - }; + cachedPromises[img.src] = newPromise; - var isBlock = function (editor, elm) { - return elm && editor.schema.getBlockElements().hasOwnProperty(Node.name(elm)); - }; + return newPromise; + }); - var paddEmptyBlock = function (elm) { - if (Empty.isEmpty(elm)) { - var br = Element.fromHtml('
    '); - Remove.empty(elm); - Insert.append(elm, br); - return Option.some(CaretPosition.before(br.dom())); - } else { - return Option.none(); + return Promise.all(promises); } - }; - - // When deleting an element between two text nodes IE 11 doesn't automatically merge the adjacent text nodes - var deleteNormalized = function (elm, afterDeletePosOpt) { - return Options.liftN([Traverse.prevSibling(elm), Traverse.nextSibling(elm), afterDeletePosOpt], function (prev, next, afterDeletePos) { - var offset, prevNode = prev.dom(), nextNode = next.dom(); - - if (NodeType.isText(prevNode) && NodeType.isText(nextNode)) { - offset = prevNode.data.length; - prevNode.appendData(nextNode.data); - Remove.remove(next); - Remove.remove(elm); - if (afterDeletePos.container() === nextNode) { - return new CaretPosition(prevNode, offset); - } else { - return afterDeletePos; - } - } else { - Remove.remove(elm); - return afterDeletePos; - } - }).orThunk(function () { - Remove.remove(elm); - return afterDeletePosOpt; - }); - }; - - var deleteElement = function (editor, forward, elm) { - var afterDeletePos = findCaretPosOutsideElmAfterDelete(forward, editor.getBody(), elm.dom()); - var parentBlock = PredicateFind.ancestor(elm, Fun.curry(isBlock, editor), eqRawNode(editor.getBody())); - var normalizedAfterDeletePos = deleteNormalized(elm, afterDeletePos); - - parentBlock.bind(paddEmptyBlock).fold( - function () { - setSelection(editor, forward, normalizedAfterDeletePos); - }, - function (paddPos) { - setSelection(editor, forward, Option.some(paddPos)); - } - ); - }; - return { - deleteElement: deleteElement + return { + findAll: findAll + }; }; } ); /** - * CefDelete.js + * Uuid.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -35416,112 +32800,40 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Generates unique ids. + * + * @class tinymce.util.Uuid + * @private + */ define( - 'tinymce.core.delete.CefDelete', + 'tinymce.core.util.Uuid', [ - 'ephox.katamari.api.Arr', - 'ephox.sugar.api.dom.Remove', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.SelectorFilter', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.delete.CefDeleteAction', - 'tinymce.core.delete.DeleteElement', - 'tinymce.core.delete.DeleteUtils', - 'tinymce.core.dom.NodeType' ], - function (Arr, Remove, Element, SelectorFilter, CaretPosition, CefDeleteAction, DeleteElement, DeleteUtils, NodeType) { - var deleteElement = function (editor, forward) { - return function (element) { - DeleteElement.deleteElement(editor, forward, Element.fromDom(element)); - return true; - }; - }; - - var moveToElement = function (editor, forward) { - return function (element) { - var pos = forward ? CaretPosition.before(element) : CaretPosition.after(element); - editor.selection.setRng(pos.toRange()); - return true; - }; - }; + function () { + var count = 0; - var moveToPosition = function (editor) { - return function (pos) { - editor.selection.setRng(pos.toRange()); - return true; + var seed = function () { + var rnd = function () { + return Math.round(Math.random() * 0xFFFFFFFF).toString(36); }; - }; - - var backspaceDeleteCaret = function (editor, forward) { - var result = CefDeleteAction.read(editor.getBody(), forward, editor.selection.getRng()).map(function (deleteAction) { - return deleteAction.fold( - deleteElement(editor, forward), - moveToElement(editor, forward), - moveToPosition(editor) - ); - }); - - return result.getOr(false); - }; - - var deleteOffscreenSelection = function (rootElement) { - Arr.each(SelectorFilter.descendants(rootElement, '.mce-offscreen-selection'), Remove.remove); - }; - - var backspaceDeleteRange = function (editor, forward) { - var selectedElement = editor.selection.getNode(); - if (NodeType.isContentEditableFalse(selectedElement)) { - deleteOffscreenSelection(Element.fromDom(editor.getBody())); - DeleteElement.deleteElement(editor, forward, Element.fromDom(editor.selection.getNode())); - DeleteUtils.paddEmptyBody(editor); - return true; - } else { - return false; - } - }; - - var getContentEditableRoot = function (root, node) { - while (node && node !== root) { - if (NodeType.isContentEditableTrue(node) || NodeType.isContentEditableFalse(node)) { - return node; - } - - node = node.parentNode; - } - - return null; - }; - - var paddEmptyElement = function (editor) { - var br, ceRoot = getContentEditableRoot(editor.getBody(), editor.selection.getNode()); - - if (NodeType.isContentEditableTrue(ceRoot) && editor.dom.isBlock(ceRoot) && editor.dom.isEmpty(ceRoot)) { - br = editor.dom.create('br', { "data-mce-bogus": "1" }); - editor.dom.setHTML(ceRoot, ''); - ceRoot.appendChild(br); - editor.selection.setRng(CaretPosition.before(br).toRange()); - } - return true; + var now = new Date().getTime(); + return 's' + now.toString(36) + rnd() + rnd() + rnd(); }; - var backspaceDelete = function (editor, forward) { - if (editor.selection.isCollapsed()) { - return backspaceDeleteCaret(editor, forward); - } else { - return backspaceDeleteRange(editor, forward); - } + var uuid = function (prefix) { + return prefix + (count++) + seed(); }; return { - backspaceDelete: backspaceDelete, - paddEmptyElement: paddEmptyElement + uuid: uuid }; } ); /** - * CaretContainerInline.js + * BlobCache.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -35530,89 +32842,120 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Hold blob info objects where a blob has extra internal information. + * + * @private + * @class tinymce.file.BlobCache + */ define( - 'tinymce.core.caret.CaretContainerInline', + 'tinymce.core.file.BlobCache', [ - 'ephox.katamari.api.Fun', - 'tinymce.core.dom.NodeType', - 'tinymce.core.text.Zwsp' + 'ephox.sand.api.URL', + 'tinymce.core.util.Arr', + 'tinymce.core.util.Fun', + 'tinymce.core.util.Uuid' ], - function (Fun, NodeType, Zwsp) { - var isText = NodeType.isText; + function (URL, Arr, Fun, Uuid) { + return function () { + var cache = [], constant = Fun.constant; - var startsWithCaretContainer = function (node) { - return isText(node) && node.data[0] === Zwsp.ZWSP; - }; + function mimeToExt(mime) { + var mimes = { + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/gif': 'gif', + 'image/png': 'png' + }; - var endsWithCaretContainer = function (node) { - return isText(node) && node.data[node.data.length - 1] === Zwsp.ZWSP; - }; + return mimes[mime.toLowerCase()] || 'dat'; + } - var createZwsp = function (node) { - return node.ownerDocument.createTextNode(Zwsp.ZWSP); - }; + function create(o, blob, base64, filename) { + return typeof o === 'object' ? toBlobInfo(o) : toBlobInfo({ + id: o, + name: filename, + blob: blob, + base64: base64 + }); + } - var insertBefore = function (node) { - if (isText(node.previousSibling)) { - if (endsWithCaretContainer(node.previousSibling)) { - return node.previousSibling; - } else { - node.previousSibling.appendData(Zwsp.ZWSP); - return node.previousSibling; - } - } else if (isText(node)) { - if (startsWithCaretContainer(node)) { - return node; - } else { - node.insertData(0, Zwsp.ZWSP); - return node; + function toBlobInfo(o) { + var id, name; + + if (!o.blob || !o.base64) { + throw "blob and base64 representations of the image are required for BlobInfo to be created"; } - } else { - var newNode = createZwsp(node); - node.parentNode.insertBefore(newNode, node); - return newNode; + + id = o.id || Uuid.uuid('blobid'); + name = o.name || id; + + return { + id: constant(id), + name: constant(name), + filename: constant(name + '.' + mimeToExt(o.blob.type)), + blob: constant(o.blob), + base64: constant(o.base64), + blobUri: constant(o.blobUri || URL.createObjectURL(o.blob)), + uri: constant(o.uri) + }; } - }; - var insertAfter = function (node) { - if (isText(node.nextSibling)) { - if (startsWithCaretContainer(node.nextSibling)) { - return node.nextSibling; - } else { - node.nextSibling.insertData(0, Zwsp.ZWSP); - return node.nextSibling; - } - } else if (isText(node)) { - if (endsWithCaretContainer(node)) { - return node; - } else { - node.appendData(Zwsp.ZWSP); - return node; - } - } else { - var newNode = createZwsp(node); - if (node.nextSibling) { - node.parentNode.insertBefore(newNode, node.nextSibling); - } else { - node.parentNode.appendChild(newNode); + function add(blobInfo) { + if (!get(blobInfo.id())) { + cache.push(blobInfo); } - return newNode; } - }; - var insertInline = function (before, node) { - return before ? insertBefore(node) : insertAfter(node); - }; + function get(id) { + return findFirst(function (cachedBlobInfo) { + return cachedBlobInfo.id() === id; + }); + } - return { - insertInline: insertInline, - insertInlineBefore: Fun.curry(insertInline, true), - insertInlineAfter: Fun.curry(insertInline, false) + function findFirst(predicate) { + return Arr.filter(cache, predicate)[0]; + } + + function getByUri(blobUri) { + return findFirst(function (blobInfo) { + return blobInfo.blobUri() == blobUri; + }); + } + + function removeByUri(blobUri) { + cache = Arr.filter(cache, function (blobInfo) { + if (blobInfo.blobUri() === blobUri) { + URL.revokeObjectURL(blobInfo.blobUri()); + return false; + } + + return true; + }); + } + + function destroy() { + Arr.each(cache, function (cachedBlobInfo) { + URL.revokeObjectURL(cachedBlobInfo.blobUri()); + }); + + cache = []; + } + + return { + create: create, + add: add, + get: get, + getByUri: getByUri, + findFirst: findFirst, + removeByUri: removeByUri, + destroy: destroy + }; }; } ); /** - * CaretContainerRemove.js + * UploadStatus.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -35621,111 +32964,77 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was. + * + * @private + * @class tinymce.file.UploadStatus + */ define( - 'tinymce.core.caret.CaretContainerRemove', + 'tinymce.core.file.UploadStatus', [ - 'ephox.katamari.api.Arr', - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.NodeType', - 'tinymce.core.text.Zwsp', - 'tinymce.core.util.Tools' ], - function (Arr, CaretContainer, CaretPosition, NodeType, Zwsp, Tools) { - var isElement = NodeType.isElement; - var isText = NodeType.isText; - - var removeNode = function (node) { - var parentNode = node.parentNode; - if (parentNode) { - parentNode.removeChild(node); - } - }; + function () { + return function () { + var PENDING = 1, UPLOADED = 2; + var blobUriStatuses = {}; - var getNodeValue = function (node) { - try { - return node.nodeValue; - } catch (ex) { - // IE sometimes produces "Invalid argument" on nodes - return ""; + function createStatus(status, resultUri) { + return { + status: status, + resultUri: resultUri + }; } - }; - var setNodeValue = function (node, text) { - if (text.length === 0) { - removeNode(node); - } else { - node.nodeValue = text; + function hasBlobUri(blobUri) { + return blobUri in blobUriStatuses; } - }; - - var trimCount = function (text) { - var trimmedText = Zwsp.trim(text); - return { count: text.length - trimmedText.length, text: trimmedText }; - }; - - var removeUnchanged = function (caretContainer, pos) { - remove(caretContainer); - return pos; - }; - var removeTextAndReposition = function (caretContainer, pos) { - var before = trimCount(caretContainer.data.substr(0, pos.offset())); - var after = trimCount(caretContainer.data.substr(pos.offset())); - var text = before.text + after.text; + function getResultUri(blobUri) { + var result = blobUriStatuses[blobUri]; - if (text.length > 0) { - setNodeValue(caretContainer, text); - return new CaretPosition(caretContainer, pos.offset() - before.count); - } else { - return pos; + return result ? result.resultUri : null; } - }; - var removeElementAndReposition = function (caretContainer, pos) { - var parentNode = pos.container(); - var newPosition = Arr.indexOf(parentNode.childNodes, caretContainer).map(function (index) { - return index < pos.offset() ? new CaretPosition(parentNode, pos.offset() - 1) : pos; - }).getOr(pos); - remove(caretContainer); - return newPosition; - }; + function isPending(blobUri) { + return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false; + } - var removeTextCaretContainer = function (caretContainer, pos) { - return pos.container() === caretContainer ? removeTextAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); - }; + function isUploaded(blobUri) { + return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false; + } - var removeElementCaretContainer = function (caretContainer, pos) { - return pos.container() === caretContainer.parentNode ? removeElementAndReposition(caretContainer, pos) : removeUnchanged(caretContainer, pos); - }; + function markPending(blobUri) { + blobUriStatuses[blobUri] = createStatus(PENDING, null); + } - var removeAndReposition = function (container, pos) { - return CaretPosition.isTextPosition(pos) ? removeTextCaretContainer(container, pos) : removeElementCaretContainer(container, pos); - }; + function markUploaded(blobUri, resultUri) { + blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri); + } - var remove = function (caretContainerNode) { - if (isElement(caretContainerNode) && CaretContainer.isCaretContainer(caretContainerNode)) { - if (CaretContainer.hasContent(caretContainerNode)) { - caretContainerNode.removeAttribute('data-mce-caret'); - } else { - removeNode(caretContainerNode); - } + function removeFailed(blobUri) { + delete blobUriStatuses[blobUri]; } - if (isText(caretContainerNode)) { - var text = Zwsp.trim(getNodeValue(caretContainerNode)); - setNodeValue(caretContainerNode, text); + function destroy() { + blobUriStatuses = {}; } - }; - return { - removeAndReposition: removeAndReposition, - remove: remove + return { + hasBlobUri: hasBlobUri, + getResultUri: getResultUri, + isPending: isPending, + isUploaded: isUploaded, + markPending: markPending, + markUploaded: markUploaded, + removeFailed: removeFailed, + destroy: destroy + }; }; } ); /** - * DefaultSettings.js + * EditorUpload.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -35734,288 +33043,252 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Handles image uploads, updates undo stack and patches over various internal functions. + * + * @private + * @class tinymce.EditorUpload + */ define( - 'tinymce.core.EditorSettings', + 'tinymce.core.EditorUpload', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Obj', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Strings', - 'ephox.katamari.api.Struct', - 'ephox.katamari.api.Type', - 'ephox.sand.api.PlatformDetection', - 'tinymce.core.util.Tools' + "tinymce.core.util.Arr", + "tinymce.core.file.Uploader", + "tinymce.core.file.ImageScanner", + "tinymce.core.file.BlobCache", + "tinymce.core.file.UploadStatus", + "tinymce.core.ErrorReporter" ], - function (Arr, Fun, Obj, Option, Strings, Struct, Type, PlatformDetection, Tools) { - var sectionResult = Struct.immutable('sections', 'settings'); - var detection = PlatformDetection.detect(); - var isTouch = detection.deviceType.isTouch(); - var mobilePlugins = [ 'lists', 'autolink', 'autosave' ]; - - var normalizePlugins = function (plugins) { - return Type.isArray(plugins) ? plugins.join(' ') : plugins; - }; + function (Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) { + return function (editor) { + var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings; + var uploadStatus = new UploadStatus(); - var filterMobilePlugins = function (plugins) { - var trimmedPlugins = Arr.map(normalizePlugins(plugins).split(' '), Strings.trim); - return Arr.filter(trimmedPlugins, Fun.curry(Arr.contains, mobilePlugins)).join(' '); - }; + function aliveGuard(callback) { + return function (result) { + if (editor.selection) { + return callback(result); + } - var extractSections = function (keys, settings) { - var result = Obj.bifilter(settings, function (value, key) { - return Arr.contains(keys, key); - }); + return []; + }; + } - return sectionResult(result.t, result.f); - }; + function cacheInvalidator() { + return '?' + (new Date()).getTime(); + } - var getSection = function (sectionResult, name) { - var sections = sectionResult.sections(); - return sections.hasOwnProperty(name) ? sections[name] : { }; - }; + // Replaces strings without regexps to avoid FF regexp to big issue + function replaceString(content, search, replace) { + var index = 0; - var hasSection = function (sectionResult, name) { - return sectionResult.sections().hasOwnProperty(name); - }; + do { + index = content.indexOf(search, index); - var getDefaultSettings = function (id, documentBaseUrl, editor) { - return { - 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', + if (index !== -1) { + content = content.substring(0, index) + replace + content.substr(index + search.length); + index += replace.length - search.length + 1; + } + } while (index !== -1); - // 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', - entity_encoding: 'named', - url_converter: editor.convertURL, - url_converter_scope: editor, - ie7_compat: true - }; - }; + return content; + } - var getExternalPlugins = function (overrideSettings, settings) { - var userDefinedExternalPlugins = settings.external_plugins ? settings.external_plugins : { }; + function replaceImageUrl(content, targetUrl, replacementUrl) { + content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"'); + content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"'); - if (overrideSettings && overrideSettings.external_plugins) { - return Tools.extend({}, overrideSettings.external_plugins, userDefinedExternalPlugins); - } else { - return userDefinedExternalPlugins; + return content; } - }; - var combineSettings = function (defaultSettings, defaultOverrideSettings, settings) { - var sectionResult = extractSections(['mobile'], settings); - var plugins = sectionResult.settings().plugins; + function replaceUrlInUndoStack(targetUrl, replacementUrl) { + Arr.each(editor.undoManager.data, function (level) { + if (level.type === 'fragmented') { + level.fragments = Arr.map(level.fragments, function (fragment) { + return replaceImageUrl(fragment, targetUrl, replacementUrl); + }); + } else { + level.content = replaceImageUrl(level.content, targetUrl, replacementUrl); + } + }); + } - var extendedSettings = Tools.extend( - // Default settings - defaultSettings, + function openNotification() { + return editor.notificationManager.open({ + text: editor.translate('Image uploading...'), + type: 'info', + timeout: -1, + progressBar: true + }); + } - // tinymce.overrideDefaults settings - defaultOverrideSettings, + function replaceImageUri(image, resultUri) { + blobCache.removeByUri(image.src); + replaceUrlInUndoStack(image.src, resultUri); - // User settings - sectionResult.settings(), + editor.$(image).attr({ + src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri, + 'data-mce-src': editor.convertURL(resultUri, 'src') + }); + } - // Sections - isTouch ? getSection(sectionResult, 'mobile') : { }, + function uploadImages(callback) { + if (!uploader) { + uploader = new Uploader(uploadStatus, { + url: settings.images_upload_url, + basePath: settings.images_upload_base_path, + credentials: settings.images_upload_credentials, + handler: settings.images_upload_handler + }); + } - // Forced settings - { - validate: true, - content_editable: sectionResult.settings().inline, - external_plugins: getExternalPlugins(defaultOverrideSettings, sectionResult.settings()) - }, + return scanForImages().then(aliveGuard(function (imageInfos) { + var blobInfos; - // TODO: Remove this once we fix each plugin with a mobile version - isTouch && plugins && hasSection(sectionResult, 'mobile') ? { plugins: filterMobilePlugins(plugins) } : { } - ); + blobInfos = Arr.map(imageInfos, function (imageInfo) { + return imageInfo.blobInfo; + }); - return extendedSettings; - }; + return uploader.upload(blobInfos, openNotification).then(aliveGuard(function (result) { + var filteredResult = Arr.map(result, function (uploadInfo, index) { + var image = imageInfos[index].image; - var getEditorSettings = function (editor, id, documentBaseUrl, defaultOverrideSettings, settings) { - var defaultSettings = getDefaultSettings(id, documentBaseUrl, editor); - return combineSettings(defaultSettings, defaultOverrideSettings, settings); - }; + if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) { + replaceImageUri(image, uploadInfo.url); + } else if (uploadInfo.error) { + ErrorReporter.uploadError(editor, uploadInfo.error); + } - var get = function (editor, name) { - return Option.from(editor.settings[name]); - }; + return { + element: image, + status: uploadInfo.status + }; + }); - var getFiltered = function (predicate, editor, name) { - return Option.from(editor.settings[name]).filter(predicate); - }; + if (callback) { + callback(filteredResult); + } - return { - getEditorSettings: getEditorSettings, - get: get, - getString: Fun.curry(getFiltered, Type.isString), + return filteredResult; + })); + })); + } - // TODO: Remove this once we have proper mobile plugins - filterMobilePlugins: filterMobilePlugins - }; - } -); + function uploadImagesAuto(callback) { + if (settings.automatic_uploads !== false) { + return uploadImages(callback); + } + } -/** - * 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 - */ + function isValidDataUriImage(imgElm) { + return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true; + } -define( - 'tinymce.core.text.Bidi', - [ - ], - function () { - var strongRtl = /[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/; + function scanForImages() { + if (!imageScanner) { + imageScanner = new ImageScanner(uploadStatus, blobCache); + } - var hasStrongRtl = function (text) { - return strongRtl.test(text); - }; + return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function (result) { + result = Arr.filter(result, function (resultItem) { + // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible. + // In such case resultItem will contain appropriate text error message, instead of image data. + if (typeof resultItem === 'string') { + ErrorReporter.displayError(editor, resultItem); + return false; + } + return true; + }); - return { - hasStrongRtl: hasStrongRtl - }; - } -); -/** - * InlineUtils.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 - */ + Arr.each(result, function (resultItem) { + replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri()); + resultItem.image.src = resultItem.blobInfo.blobUri(); + resultItem.image.removeAttribute('data-mce-src'); + }); -define( - 'tinymce.core.keyboard.InlineUtils', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Options', - 'ephox.katamari.api.Type', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.Selectors', - '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.EditorSettings', - 'tinymce.core.text.Bidi' - ], - function ( - Arr, Fun, Option, Options, Type, Element, Selectors, CaretContainer, CaretFinder, CaretPosition, CaretUtils, CaretWalker, DOMUtils, NodeType, EditorSettings, - Bidi - ) { - var isInlineTarget = function (editor, elm) { - var selector = EditorSettings.getString(editor, 'inline_boundaries_selector').getOr('a[href],code'); - return Selectors.is(Element.fromDom(elm), selector); - }; - - var isRtl = function (element) { - return DOMUtils.DOM.getStyle(element, 'direction', true) === 'rtl' || Bidi.hasStrongRtl(element.textContent); - }; + return result; + })); + } - var findInlineParents = function (isInlineTarget, rootNode, pos) { - return Arr.filter(DOMUtils.DOM.getParents(pos.container(), '*', rootNode), isInlineTarget); - }; + function destroy() { + blobCache.destroy(); + uploadStatus.destroy(); + imageScanner = uploader = null; + } - var findRootInline = function (isInlineTarget, rootNode, pos) { - var parents = findInlineParents(isInlineTarget, rootNode, pos); - return Option.from(parents[parents.length - 1]); - }; + function replaceBlobUris(content) { + return content.replace(/src="(blob:[^"]+)"/g, function (match, blobUri) { + var resultUri = uploadStatus.getResultUri(blobUri); - var hasSameParentBlock = function (rootNode, node1, node2) { - var block1 = CaretUtils.getParentBlock(node1, rootNode); - var block2 = CaretUtils.getParentBlock(node2, rootNode); - return block1 && block1 === block2; - }; + if (resultUri) { + return 'src="' + resultUri + '"'; + } - var isAtZwsp = function (pos) { - return CaretContainer.isBeforeInline(pos) || CaretContainer.isAfterInline(pos); - }; + var blobInfo = blobCache.getByUri(blobUri); - var normalizePosition = function (forward, pos) { - var container = pos.container(), offset = pos.offset(); + if (!blobInfo) { + blobInfo = Arr.reduce(editor.editorManager.get(), function (result, editor) { + return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri); + }, null); + } - if (forward) { - if (CaretContainer.isCaretContainerInline(container)) { - if (NodeType.isText(container.nextSibling)) { - return new CaretPosition(container.nextSibling, 0); - } else { - return CaretPosition.after(container); + if (blobInfo) { + return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"'; } + + return match; + }); + } + + editor.on('setContent', function () { + if (editor.settings.automatic_uploads !== false) { + uploadImagesAuto(); } else { - return CaretContainer.isBeforeInline(pos) ? new CaretPosition(container, offset + 1) : pos; + scanForImages(); } - } 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; + }); + + editor.on('RawSaveContent', function (e) { + e.content = replaceBlobUris(e.content); + }); + + editor.on('getContent', function (e) { + if (e.source_view || e.format == 'raw') { + return; } - } - }; - var normalizeForwards = Fun.curry(normalizePosition, true); - var normalizeBackwards = Fun.curry(normalizePosition, false); + e.content = replaceBlobUris(e.content); + }); - return { - isInlineTarget: isInlineTarget, - findRootInline: findRootInline, - isRtl: isRtl, - isAtZwsp: isAtZwsp, - normalizePosition: normalizePosition, - normalizeForwards: normalizeForwards, - normalizeBackwards: normalizeBackwards, - hasSameParentBlock: hasSameParentBlock + editor.on('PostRender', function () { + editor.parser.addNodeFilter('img', function (images) { + Arr.each(images, function (img) { + var src = img.attr('src'); + + if (blobCache.getByUri(src)) { + return; + } + + var resultUri = uploadStatus.getResultUri(src); + if (resultUri) { + img.attr('src', resultUri); + } + }); + }); + }); + + return { + blobCache: blobCache, + uploadImages: uploadImages, + uploadImagesAuto: uploadImagesAuto, + scanForImages: scanForImages, + destroy: destroy + }; }; } ); /** - * BoundaryCaret.js + * ForceBlocks.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36024,80 +33297,105 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Makes sure that everything gets wrapped in paragraphs. + * + * @private + * @class tinymce.ForceBlocks + */ define( - 'tinymce.core.keyboard.BoundaryCaret', + 'tinymce.core.ForceBlocks', [ - 'ephox.katamari.api.Option', - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretContainerInline', - 'tinymce.core.caret.CaretContainerRemove', - 'tinymce.core.caret.CaretFinder', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.NodeType', - 'tinymce.core.keyboard.InlineUtils' + 'ephox.katamari.api.Fun' ], - function (Option, CaretContainer, CaretContainerInline, CaretContainerRemove, CaretFinder, CaretPosition, NodeType, InlineUtils) { - var insertInlinePos = function (pos, before) { - if (NodeType.isText(pos.container())) { - return CaretContainerInline.insertInline(before, pos.container()); - } else { - return CaretContainerInline.insertInline(before, pos.getNode()); + function (Fun) { + var addRootBlocks = function (editor) { + var settings = editor.settings, dom = editor.dom, selection = editor.selection; + var schema = editor.schema, blockElements = schema.getBlockElements(); + var node = selection.getStart(), rootNode = editor.getBody(), rng; + var startContainer, startOffset, endContainer, endOffset, rootBlockNode; + var tempNode, wrapped, restoreSelection; + var rootNodeName, forcedRootBlock; + + forcedRootBlock = settings.forced_root_block; + + if (!node || node.nodeType !== 1 || !forcedRootBlock) { + return; } - }; - var isPosCaretContainer = function (pos, caret) { - var caretNode = caret.get(); - return caretNode && pos.container() === caretNode && CaretContainer.isCaretContainerInline(caretNode); - }; + // Check if node is wrapped in block + while (node && node !== rootNode) { + if (blockElements[node.nodeName]) { + return; + } - var renderCaret = function (caret, location) { - return location.fold( - function (element) { // Before - CaretContainerRemove.remove(caret.get()); - var text = CaretContainerInline.insertInlineBefore(element); - caret.set(text); - return Option.some(new CaretPosition(text, text.length - 1)); - }, - function (element) { // Start - return CaretFinder.firstPositionIn(element).map(function (pos) { - if (!isPosCaretContainer(pos, caret)) { - CaretContainerRemove.remove(caret.get()); - var text = insertInlinePos(pos, true); - caret.set(text); - return new CaretPosition(text, 1); - } else { - return new CaretPosition(caret.get(), 1); - } - }); - }, - function (element) { // End - return CaretFinder.lastPositionIn(element).map(function (pos) { - if (!isPosCaretContainer(pos, caret)) { - CaretContainerRemove.remove(caret.get()); - var text = insertInlinePos(pos, false); - caret.set(text); - return new CaretPosition(text, text.length - 1); - } else { - return new CaretPosition(caret.get(), caret.get().length - 1); - } - }); - }, - function (element) { // After - CaretContainerRemove.remove(caret.get()); - var text = CaretContainerInline.insertInlineAfter(element); - caret.set(text); - return Option.some(new CaretPosition(text, 1)); + node = node.parentNode; + } + + // Get current selection + rng = selection.getRng(); + startContainer = rng.startContainer; + startOffset = rng.startOffset; + endContainer = rng.endContainer; + endOffset = rng.endOffset; + + try { + restoreSelection = editor.getDoc().activeElement === rootNode; + } catch (ex) { + // IE throws unspecified error here sometimes + } + + // Wrap non block elements and text nodes + node = rootNode.firstChild; + rootNodeName = rootNode.nodeName.toLowerCase(); + while (node) { + // TODO: Break this up, too complex + if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) && + schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) { + // Remove empty text nodes + if (node.nodeType === 3 && node.nodeValue.length === 0) { + tempNode = node; + node = node.nextSibling; + dom.remove(tempNode); + continue; + } + + if (!rootBlockNode) { + rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs); + node.parentNode.insertBefore(rootBlockNode, node); + wrapped = true; + } + + tempNode = node; + node = node.nextSibling; + rootBlockNode.appendChild(tempNode); + } else { + rootBlockNode = null; + node = node.nextSibling; } - ); + } + + if (wrapped && restoreSelection) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + editor.nodeChanged(); + } + }; + + var setup = function (editor) { + if (editor.settings.forced_root_block) { + editor.on('NodeChange', Fun.curry(addRootBlocks, editor)); + } }; return { - renderCaret: renderCaret + setup: setup }; } ); /** - * LazyEvaluator.js + * Dimensions.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36106,30 +33404,66 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * This module measures nodes and returns client rects. The client rects has an + * extra node property. + * + * @private + * @class tinymce.dom.Dimensions + */ define( - 'tinymce.core.util.LazyEvaluator', + 'tinymce.core.dom.Dimensions', [ - 'ephox.katamari.api.Option' + "tinymce.core.util.Arr", + "tinymce.core.dom.NodeType", + "tinymce.core.geom.ClientRect" ], - function (Option) { - var evaluateUntil = function (fns, args) { - for (var i = 0; i < fns.length; i++) { - var result = fns[i].apply(null, args); - if (result.isSome()) { - return result; - } + function (Arr, NodeType, ClientRect) { + + function getClientRects(node) { + function toArrayWithNode(clientRects) { + return Arr.map(clientRects, function (clientRect) { + clientRect = ClientRect.clone(clientRect); + clientRect.node = node; + + return clientRect; + }); } - return Option.none(); - }; + if (Arr.isArray(node)) { + return Arr.reduce(node, function (result, node) { + return result.concat(getClientRects(node)); + }, []); + } + + if (NodeType.isElement(node)) { + return toArrayWithNode(node.getClientRects()); + } + + if (NodeType.isText(node)) { + var rng = node.ownerDocument.createRange(); + + rng.setStart(node, 0); + rng.setEnd(node, node.data.length); + + return toArrayWithNode(rng.getClientRects()); + } + } return { - evaluateUntil: evaluateUntil + /** + * Returns the client rects for a specific node. + * + * @method getClientRects + * @param {Array/DOMNode} node Node or array of nodes to get client rects on. + * @param {Array} Array of client rects with a extra node property. + */ + getClientRects: getClientRects }; } ); /** - * BoundaryLocation.js + * LineUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36138,216 +33472,137 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * Utility functions for working with lines. + * + * @private + * @class tinymce.caret.LineUtils + */ define( - 'tinymce.core.keyboard.BoundaryLocation', + 'tinymce.core.caret.LineUtils', [ - 'ephox.katamari.api.Adt', - '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.dom.NodeType', - 'tinymce.core.keyboard.InlineUtils', - 'tinymce.core.util.LazyEvaluator' + "tinymce.core.util.Fun", + "tinymce.core.util.Arr", + "tinymce.core.dom.NodeType", + "tinymce.core.dom.Dimensions", + "tinymce.core.geom.ClientRect", + "tinymce.core.caret.CaretUtils", + "tinymce.core.caret.CaretCandidate" ], - function (Adt, Fun, Option, Options, CaretContainer, CaretFinder, CaretPosition, CaretUtils, NodeType, InlineUtils, LazyEvaluator) { - var Location = Adt.generate([ - { before: [ 'element' ] }, - { start: [ 'element' ] }, - { end: [ 'element' ] }, - { after: [ 'element' ] } - ]); + function (Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) { + var isContentEditableFalse = NodeType.isContentEditableFalse, + findNode = CaretUtils.findNode, + curry = Fun.curry; - var rescope = function (rootNode, node) { - var parentBlock = CaretUtils.getParentBlock(node, rootNode); - return parentBlock ? parentBlock : rootNode; - }; + function distanceToRectLeft(clientRect, clientX) { + return Math.abs(clientRect.left - clientX); + } - var before = function (isInlineTarget, rootNode, pos) { - var nPos = InlineUtils.normalizeForwards(pos); - var scope = rescope(rootNode, nPos.container()); - return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold( - function () { - return CaretFinder.nextPosition(scope, nPos) - .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope)) - .map(function (inline) { - return Location.before(inline); - }); - }, - Option.none - ); - }; + function distanceToRectRight(clientRect, clientX) { + return Math.abs(clientRect.right - clientX); + } - var start = function (isInlineTarget, rootNode, pos) { - var nPos = InlineUtils.normalizeBackwards(pos); - return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) { - var prevPos = CaretFinder.prevPosition(inline, nPos); - return prevPos.isNone() ? Option.some(Location.start(inline)) : Option.none(); - }); - }; + function findClosestClientRect(clientRects, clientX) { + function isInside(clientX, clientRect) { + return clientX >= clientRect.left && clientX <= clientRect.right; + } - var end = function (isInlineTarget, rootNode, pos) { - var nPos = InlineUtils.normalizeForwards(pos); - return InlineUtils.findRootInline(isInlineTarget, rootNode, nPos).bind(function (inline) { - var nextPos = CaretFinder.nextPosition(inline, nPos); - return nextPos.isNone() ? Option.some(Location.end(inline)) : Option.none(); - }); - }; + return Arr.reduce(clientRects, function (oldClientRect, clientRect) { + var oldDistance, newDistance; - var after = function (isInlineTarget, rootNode, pos) { - var nPos = InlineUtils.normalizeBackwards(pos); - var scope = rescope(rootNode, nPos.container()); - return InlineUtils.findRootInline(isInlineTarget, scope, nPos).fold( - function () { - return CaretFinder.prevPosition(scope, nPos) - .bind(Fun.curry(InlineUtils.findRootInline, isInlineTarget, scope)) - .map(function (inline) { - return Location.after(inline); - }); - }, - Option.none - ); - }; + oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX)); + newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX)); - var isValidLocation = function (location) { - return InlineUtils.isRtl(getElement(location)) === false; - }; + if (isInside(clientX, clientRect)) { + return clientRect; + } - var readLocation = function (isInlineTarget, rootNode, pos) { - var location = LazyEvaluator.evaluateUntil([ - before, - start, - end, - after - ], [isInlineTarget, rootNode, pos]); + if (isInside(clientX, oldClientRect)) { + return oldClientRect; + } - return location.filter(isValidLocation); - }; + // cE=false has higher priority + if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) { + return clientRect; + } - var getElement = function (location) { - return location.fold( - Fun.identity, // Before - Fun.identity, // Start - Fun.identity, // End - Fun.identity // After - ); - }; + if (newDistance < oldDistance) { + return clientRect; + } - var getName = function (location) { - return location.fold( - Fun.constant('before'), // Before - Fun.constant('start'), // Start - Fun.constant('end'), // End - Fun.constant('after') // After - ); - }; + return oldClientRect; + }); + } - var outside = function (location) { - return location.fold( - Location.before, // Before - Location.before, // Start - Location.after, // End - Location.after // After - ); - }; + function walkUntil(direction, rootNode, predicateFn, node) { + while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) { + if (predicateFn(node)) { + return; + } + } + } - var inside = function (location) { - return location.fold( - Location.start, // Before - Location.start, // Start - Location.end, // End - Location.end // After - ); - }; + function findLineNodeRects(rootNode, targetNodeRect) { + var clientRects = []; - var isEq = function (location1, location2) { - return getName(location1) === getName(location2) && getElement(location1) === getElement(location2); - }; + function collect(checkPosFn, node) { + var lineRects; - var betweenInlines = function (forward, isInlineTarget, rootNode, from, to, location) { - return Options.liftN([ - InlineUtils.findRootInline(isInlineTarget, rootNode, from), - InlineUtils.findRootInline(isInlineTarget, rootNode, to) - ], function (fromInline, toInline) { - if (fromInline !== toInline && InlineUtils.hasSameParentBlock(rootNode, fromInline, toInline)) { - // Force after since some browsers normalize and lean left into the closest inline - return Location.after(forward ? fromInline : toInline); - } else { - return location; - } - }).getOr(location); - }; + lineRects = Arr.filter(Dimensions.getClientRects(node), function (clientRect) { + return !checkPosFn(clientRect, targetNodeRect); + }); - var skipNoMovement = function (fromLocation, toLocation) { - return fromLocation.fold( - Fun.constant(true), - function (fromLocation) { - return !isEq(fromLocation, toLocation); - } - ); - }; + clientRects = clientRects.concat(lineRects); - var findLocationTraverse = function (forward, isInlineTarget, rootNode, fromLocation, pos) { - var from = InlineUtils.normalizePosition(forward, pos); - var to = CaretFinder.fromPosition(forward, rootNode, from).map(Fun.curry(InlineUtils.normalizePosition, forward)); + return lineRects.length === 0; + } - var location = to.fold( - function () { - return fromLocation.map(outside); - }, - function (to) { - return readLocation(isInlineTarget, rootNode, to) - .map(Fun.curry(betweenInlines, forward, isInlineTarget, rootNode, from, to)) - .filter(Fun.curry(skipNoMovement, fromLocation)); - } - ); + clientRects.push(targetNodeRect); + walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node); + walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node); - return location.filter(isValidLocation); - }; + return clientRects; + } - var findLocationSimple = function (forward, location) { - if (forward) { - return location.fold( - Fun.compose(Option.some, Location.start), // Before -> Start - Option.none, - Fun.compose(Option.some, Location.after), // End -> After - Option.none - ); - } else { - return location.fold( - Option.none, - Fun.compose(Option.some, Location.before), // Before <- Start - Option.none, - Fun.compose(Option.some, Location.end) // End <- After - ); - } - }; + function getContentEditableFalseChildren(rootNode) { + return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse); + } - var findLocation = function (forward, isInlineTarget, rootNode, pos) { - var from = InlineUtils.normalizePosition(forward, pos); - var fromLocation = readLocation(isInlineTarget, rootNode, from); + function caretInfo(clientRect, clientX) { + return { + node: clientRect.node, + before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX) + }; + } - return readLocation(isInlineTarget, rootNode, from).bind(Fun.curry(findLocationSimple, forward)).orThunk(function () { - return findLocationTraverse(forward, isInlineTarget, rootNode, fromLocation, pos); + function closestCaret(rootNode, clientX, clientY) { + var contentEditableFalseNodeRects, closestNodeRect; + + contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode)); + contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function (clientRect) { + return clientY >= clientRect.top && clientY <= clientRect.bottom; }); - }; + + closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX); + if (closestNodeRect) { + closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX); + if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) { + return caretInfo(closestNodeRect, clientX); + } + } + + return null; + } return { - readLocation: readLocation, - findLocation: findLocation, - prevLocation: Fun.curry(findLocation, false), - nextLocation: Fun.curry(findLocation, true), - getElement: getElement, - outside: outside, - inside: inside + findClosestClientRect: findClosestClientRect, + findLineNodeRects: findLineNodeRects, + closestCaret: closestCaret }; } ); /** - * BoundarySelection.js + * LineWalker.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36356,114 +33611,168 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/** + * This module lets you walk the document line by line + * returing nodes and client rects for each line. + * + * @private + * @class tinymce.caret.LineWalker + */ define( - 'tinymce.core.keyboard.BoundarySelection', + 'tinymce.core.caret.LineWalker', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Cell', - 'ephox.katamari.api.Fun', - 'tinymce.core.caret.CaretContainerRemove', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.keyboard.BoundaryCaret', - 'tinymce.core.keyboard.BoundaryLocation', - 'tinymce.core.keyboard.InlineUtils' + "tinymce.core.util.Fun", + "tinymce.core.util.Arr", + "tinymce.core.dom.Dimensions", + "tinymce.core.caret.CaretCandidate", + "tinymce.core.caret.CaretUtils", + "tinymce.core.caret.CaretWalker", + "tinymce.core.caret.CaretPosition", + "tinymce.core.geom.ClientRect" ], - function (Arr, Cell, Fun, CaretContainerRemove, CaretPosition, BoundaryCaret, BoundaryLocation, InlineUtils) { - var setCaretPosition = function (editor, pos) { - var rng = editor.dom.createRng(); - rng.setStart(pos.container(), pos.offset()); - rng.setEnd(pos.container(), pos.offset()); - editor.selection.setRng(rng); - }; + function (Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) { + var curry = Fun.curry; - var isFeatureEnabled = function (editor) { - return editor.settings.inline_boundaries !== false; - }; + function findUntil(direction, rootNode, predicateFn, node) { + while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) { + if (predicateFn(node)) { + return; + } + } + } - var setSelected = function (state, elm) { - if (state) { - elm.setAttribute('data-mce-selected', '1'); - } else { - elm.removeAttribute('data-mce-selected', '1'); + function walkUntil(direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) { + var line = 0, node, result = [], targetClientRect; + + function add(node) { + var i, clientRect, clientRects; + + clientRects = Dimensions.getClientRects(node); + if (direction == -1) { + clientRects = clientRects.reverse(); + } + + for (i = 0; i < clientRects.length; i++) { + clientRect = clientRects[i]; + if (isBeflowFn(clientRect, targetClientRect)) { + continue; + } + + if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) { + line++; + } + + clientRect.line = line; + + if (predicateFn(clientRect)) { + return true; + } + + result.push(clientRect); + } } - }; - var renderCaretLocation = function (editor, caret, location) { - return BoundaryCaret.renderCaret(caret, location).map(function (pos) { - setCaretPosition(editor, pos); - return location; - }); - }; + targetClientRect = Arr.last(caretPosition.getClientRects()); + if (!targetClientRect) { + return result; + } - var findLocation = function (editor, caret, forward) { - var rootNode = editor.getBody(); - var from = CaretPosition.fromRangeStart(editor.selection.getRng()); - var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); - var location = BoundaryLocation.findLocation(forward, isInlineTarget, rootNode, from); - return location.bind(function (location) { - return renderCaretLocation(editor, caret, location); - }); - }; + node = caretPosition.getNode(); + add(node); + findUntil(direction, rootNode, add, node); - var toggleInlines = function (isInlineTarget, dom, elms) { - var selectedInlines = Arr.filter(dom.select('*[data-mce-selected]'), isInlineTarget); - var targetInlines = Arr.filter(elms, isInlineTarget); - Arr.each(Arr.difference(selectedInlines, targetInlines), Fun.curry(setSelected, false)); - Arr.each(Arr.difference(targetInlines, selectedInlines), Fun.curry(setSelected, true)); - }; + return result; + } - var safeRemoveCaretContainer = function (editor, caret) { - if (editor.selection.isCollapsed() && editor.composing !== true && caret.get()) { - var pos = CaretPosition.fromRangeStart(editor.selection.getRng()); - if (CaretPosition.isTextPosition(pos) && InlineUtils.isAtZwsp(pos) === false) { - setCaretPosition(editor, CaretContainerRemove.removeAndReposition(caret.get(), pos)); - caret.set(null); + function aboveLineNumber(lineNumber, clientRect) { + return clientRect.line > lineNumber; + } + + function isLine(lineNumber, clientRect) { + return clientRect.line === lineNumber; + } + + var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow); + var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove); + + function positionsUntil(direction, rootNode, predicateFn, node) { + var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn, + caretPosition, result = [], line = 0, clientRect, targetClientRect; + + function getClientRect(caretPosition) { + if (direction == 1) { + return Arr.last(caretPosition.getClientRects()); } + + return Arr.last(caretPosition.getClientRects()); } - }; - var renderInsideInlineCaret = function (isInlineTarget, editor, caret, elms) { - if (editor.selection.isCollapsed()) { - var inlines = Arr.filter(elms, isInlineTarget); - Arr.each(inlines, function (inline) { - var pos = CaretPosition.fromRangeStart(editor.selection.getRng()); - BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), pos).bind(function (location) { - return renderCaretLocation(editor, caret, location); - }); - }); + if (direction == 1) { + walkFn = caretWalker.next; + isBelowFn = ClientRect.isBelow; + isAboveFn = ClientRect.isAbove; + caretPosition = CaretPosition.after(node); + } else { + walkFn = caretWalker.prev; + isBelowFn = ClientRect.isAbove; + isAboveFn = ClientRect.isBelow; + caretPosition = CaretPosition.before(node); } - }; - var move = function (editor, caret, forward) { - return function () { - return isFeatureEnabled(editor) ? findLocation(editor, caret, forward).isSome() : false; - }; - }; + targetClientRect = getClientRect(caretPosition); - var setupSelectedState = function (editor) { - var caret = new Cell(null); - var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); + do { + if (!caretPosition.isVisible()) { + continue; + } - editor.on('NodeChange', function (e) { - if (isFeatureEnabled(editor)) { - toggleInlines(isInlineTarget, editor.dom, e.parents); - safeRemoveCaretContainer(editor, caret); - renderInsideInlineCaret(isInlineTarget, editor, caret, e.parents); + clientRect = getClientRect(caretPosition); + + if (isAboveFn(clientRect, targetClientRect)) { + continue; } - }); - return caret; - }; + if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) { + line++; + } + + clientRect = ClientRect.clone(clientRect); + clientRect.position = caretPosition; + clientRect.line = line; + + if (predicateFn(clientRect)) { + return result; + } + + result.push(clientRect); + } while ((caretPosition = walkFn(caretPosition))); + + return result; + } return { - move: move, - setupSelectedState: setupSelectedState, - setCaretPosition: setCaretPosition + upUntil: upUntil, + downUntil: downUntil, + + /** + * Find client rects with line and caret position until the predicate returns true. + * + * @method positionsUntil + * @param {Number} direction Direction forward/backward 1/-1. + * @param {DOMNode} rootNode Root node to walk within. + * @param {function} predicateFn Gets the client rect as it's input. + * @param {DOMNode} node Node to start walking from. + * @return {Array} Array of client rects with line and position properties. + */ + positionsUntil: positionsUntil, + + isAboveLine: curry(aboveLineNumber), + isLine: curry(isLine) }; } ); /** - * InlineBoundaryDelete.js + * CefUtils.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36473,266 +33782,414 @@ define( */ define( - 'tinymce.core.delete.InlineBoundaryDelete', + 'tinymce.core.keyboard.CefUtils', [ - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.katamari.api.Options', - 'ephox.sugar.api.node.Element', - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretFinder', 'tinymce.core.caret.CaretPosition', 'tinymce.core.caret.CaretUtils', - 'tinymce.core.delete.DeleteElement', - 'tinymce.core.keyboard.BoundaryCaret', - 'tinymce.core.keyboard.BoundaryLocation', - 'tinymce.core.keyboard.BoundarySelection', - 'tinymce.core.keyboard.InlineUtils' + 'tinymce.core.dom.NodeType', + 'tinymce.core.util.Fun' ], - function ( - Fun, Option, Options, Element, CaretContainer, CaretFinder, CaretPosition, CaretUtils, DeleteElement, BoundaryCaret, BoundaryLocation, BoundarySelection, - InlineUtils - ) { - var isFeatureEnabled = function (editor) { - return editor.settings.inline_boundaries !== false; - }; - - var rangeFromPositions = function (from, to) { - var range = document.createRange(); + function (CaretPosition, CaretUtils, NodeType, Fun) { + var isContentEditableTrue = NodeType.isContentEditableTrue; + var isContentEditableFalse = NodeType.isContentEditableFalse; - range.setStart(from.container(), from.offset()); - range.setEnd(to.container(), to.offset()); + var showCaret = function (direction, editor, node, before) { + // TODO: Figure out a better way to handle this dependency + return editor._selectionOverrides.showCaret(direction, node, before); + }; - return range; + var getNodeRange = function (node) { + var rng = node.ownerDocument.createRange(); + rng.selectNode(node); + return rng; }; - // Checks for delete at |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); + var selectNode = function (editor, node) { + var e; - return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) { - return pos.isEqual(normalizedLastPos); - }).getOr(true); - }).getOr(true); - }; + e = editor.fire('BeforeObjectSelected', { target: node }); + if (e.isDefaultPrevented()) { + return null; + } - var setCaretLocation = function (editor, caret) { - return function (location) { - return BoundaryCaret.renderCaret(caret, location).map(function (pos) { - BoundarySelection.setCaretPosition(editor, pos); - return true; - }).getOr(false); - }; + return getNodeRange(node); }; - 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'); + var renderCaretAtRange = function (editor, range) { + var caretPosition, ceRoot; - BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng())) - .map(BoundaryLocation.inside) - .map(setCaretLocation(editor, caret)); - }); + range = CaretUtils.normalizeRange(1, editor.getBody(), range); + caretPosition = CaretPosition.fromRangeStart(range); - editor.nodeChanged(); - }; + if (isContentEditableFalse(caretPosition.getNode())) { + return showCaret(1, editor, caretPosition.getNode(), !caretPosition.isAtEnd()); + } - var rescope = function (rootNode, node) { - var parentBlock = CaretUtils.getParentBlock(node, rootNode); - return parentBlock ? parentBlock : rootNode; - }; + if (isContentEditableFalse(caretPosition.getNode(true))) { + return showCaret(1, editor, caretPosition.getNode(true), false); + } - 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); + // TODO: Should render caret before/after depending on where you click on the page forces after now + ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue)); + if (isContentEditableFalse(ceRoot)) { + return showCaret(1, editor, ceRoot, false); + } - 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); - }); + return null; + }; - 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); - } + var renderRangeCaret = function (editor, range) { + var caretRange; - return true; - }); - }).getOr(false); - } - }); - }; + if (!range || !range.collapsed) { + return range; + } - 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); + caretRange = renderCaretAtRange(editor, range); + if (caretRange) { + return caretRange; } - return false; + return range; }; return { - backspaceDelete: backspaceDelete + showCaret: showCaret, + selectNode: selectNode, + renderCaretAtRange: renderCaretAtRange, + renderRangeCaret: renderRangeCaret }; } ); -define( - 'tinymce.core.delete.TableDeleteAction', +/** + * CefNavigation.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.CefNavigation', [ - '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' + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.caret.CaretUtils', + 'tinymce.core.caret.CaretWalker', + 'tinymce.core.caret.LineUtils', + 'tinymce.core.caret.LineWalker', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.Env', + 'tinymce.core.keyboard.CefUtils', + 'tinymce.core.util.Arr', + 'tinymce.core.util.Fun' ], + function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, LineUtils, LineWalker, NodeType, RangeUtils, Env, CefUtils, Arr, Fun) { + var isContentEditableFalse = NodeType.isContentEditableFalse; + var getSelectedNode = RangeUtils.getSelectedNode; + var isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse; + var isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; - 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 getVisualCaretPosition = function (walkFn, caretPosition) { + while ((caretPosition = walkFn(caretPosition))) { + if (caretPosition.isVisible()) { + return caretPosition; + } + } - var getClosestCell = function (container, isRoot) { - return SelectorFind.closest(Element.fromDom(container), 'td,th', isRoot); + return caretPosition; }; - var getClosestTable = function (cell, isRoot) { - return SelectorFind.ancestor(cell, 'table', isRoot); + var isMoveInsideSameBlock = function (fromCaretPosition, toCaretPosition) { + var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition); + + // Handle bogus BR

    abc|

    + if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) { + return true; + } + + return inSameBlock; }; - var isExpandedCellRng = function (cellRng) { - return Compare.eq(cellRng.start(), cellRng.end()) === false; + var isRangeInCaretContainerBlock = function (range) { + return CaretContainer.isCaretContainerBlock(range.startContainer); }; - 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 getNormalizedRangeEndPoint = function (direction, rootNode, range) { + range = CaretUtils.normalizeRange(direction, rootNode, range); + + if (direction === -1) { + return CaretPosition.fromRangeStart(range); + } + + return CaretPosition.fromRangeEnd(range); }; - 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 moveToCeFalseHorizontally = function (direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range) { + var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock; + + if (!range.collapsed) { + node = getSelectedNode(range); + if (isContentEditableFalse(node)) { + return CefUtils.showCaret(direction, editor, node, direction === -1); + } + } + + rangeIsInContainerBlock = isRangeInCaretContainerBlock(range); + caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); + + if (isBeforeContentEditableFalseFn(caretPosition)) { + return CefUtils.selectNode(editor, caretPosition.getNode(direction === -1)); + } + + caretPosition = getNextPosFn(caretPosition); + if (!caretPosition) { + if (rangeIsInContainerBlock) { + return range; + } + + return null; + } + + if (isBeforeContentEditableFalseFn(caretPosition)) { + return CefUtils.showCaret(direction, editor, caretPosition.getNode(direction === -1), direction === 1); + } + + // Peek ahead for handling of ab|c -> abc| + peekCaretPosition = getNextPosFn(caretPosition); + if (isBeforeContentEditableFalseFn(peekCaretPosition)) { + if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) { + return CefUtils.showCaret(direction, editor, peekCaretPosition.getNode(direction === -1), direction === 1); + } + } + + if (rangeIsInContainerBlock) { + return CefUtils.renderRangeCaret(editor, caretPosition.toRange()); + } + + return null; }; - var getTableSelectionFromCellRng = function (cellRng, isRoot) { - return getTableFromCellRng(cellRng, isRoot) - .bind(function (table) { - var cells = SelectorFilter.descendants(table, 'td,th'); + var moveToCeFalseVertically = function (direction, editor, walkerFn, range) { + var caretPosition, linePositions, nextLinePositions, + closestNextLineRect, caretClientRect, clientX, + dist1, dist2, contentEditableFalseNode; - return tableSelection(cellRng, table, cells); - }); + contentEditableFalseNode = getSelectedNode(range); + caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); + linePositions = walkerFn(editor.getBody(), LineWalker.isAboveLine(1), caretPosition); + nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1)); + caretClientRect = Arr.last(caretPosition.getClientRects()); + + if (isBeforeContentEditableFalse(caretPosition)) { + contentEditableFalseNode = caretPosition.getNode(); + } + + if (isAfterContentEditableFalse(caretPosition)) { + contentEditableFalseNode = caretPosition.getNode(true); + } + + if (!caretClientRect) { + return null; + } + + clientX = caretClientRect.left; + + closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX); + if (closestNextLineRect) { + if (isContentEditableFalse(closestNextLineRect.node)) { + dist1 = Math.abs(clientX - closestNextLineRect.left); + dist2 = Math.abs(clientX - closestNextLineRect.right); + + return CefUtils.showCaret(direction, editor, closestNextLineRect.node, dist1 < dist2); + } + } + + if (contentEditableFalseNode) { + var caretPositions = LineWalker.positionsUntil(direction, editor.getBody(), LineWalker.isAboveLine(1), contentEditableFalseNode); + + closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX); + if (closestNextLineRect) { + return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); + } + + closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0))); + if (closestNextLineRect) { + return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); + } + } }; - var getTableSelectionFromRng = function (rootNode, rng) { - var isRoot = Fun.curry(Compare.eq, rootNode); + var createTextBlock = function (editor) { + var textBlock = editor.dom.create(editor.settings.forced_root_block); - return getCellRng(rng, isRoot) - .map(function (cellRng) { - return getTableSelectionFromCellRng(cellRng, isRoot); - }); + if (!Env.ie || Env.ie >= 11) { + textBlock.innerHTML = '
    '; + } + + return textBlock; }; - var getCellIndex = function (cellArray, cell) { - return Arr.findIndex(cellArray, function (x) { - return Compare.eq(x, cell); - }); + var exitPreBlock = function (editor, direction, range) { + var pre, caretPos, newBlock; + var caretWalker = new CaretWalker(editor.getBody()); + var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); + var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); + + if (range.collapsed && editor.settings.forced_root_block) { + pre = editor.dom.getParent(range.startContainer, 'PRE'); + if (!pre) { + return; + } + + if (direction === 1) { + caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range)); + } else { + caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range)); + } + + if (!caretPos) { + newBlock = createTextBlock(editor); + + if (direction === 1) { + editor.$(pre).after(newBlock); + } else { + editor.$(pre).before(newBlock); + } + + editor.selection.select(newBlock, true); + editor.selection.collapse(); + } + } }; - 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 getHorizontalRange = function (editor, forward) { + var caretWalker = new CaretWalker(editor.getBody()); + var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); + var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); + var newRange, direction = forward ? 1 : -1; + var getNextPosFn = forward ? getNextVisualCaretPosition : getPrevVisualCaretPosition; + var isBeforeContentEditableFalseFn = forward ? isBeforeContentEditableFalse : isAfterContentEditableFalse; + var range = editor.selection.getRng(); + + newRange = moveToCeFalseHorizontally(direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range); + if (newRange) { + return newRange; + } + + newRange = exitPreBlock(editor, direction, range); + if (newRange) { + return newRange; + } + + return null; }; - var getAction = function (tableSelection) { - return getSelectedCells(tableSelection) - .bind(function (selected) { - var cells = tableSelection.cells(); + var getVerticalRange = function (editor, down) { + var newRange, direction = down ? 1 : -1; + var walkerFn = down ? LineWalker.downUntil : LineWalker.upUntil; + var range = editor.selection.getRng(); - return selected.length === cells.length ? deleteAction.removeTable(tableSelection.table()) : deleteAction.emptyCells(selected); - }); + newRange = moveToCeFalseVertically(direction, editor, walkerFn, range); + if (newRange) { + return newRange; + } + + newRange = exitPreBlock(editor, direction, range); + if (newRange) { + return newRange; + } + + return null; }; - var getActionFromCells = function (cells) { - return deleteAction.emptyCells(cells); + var moveH = function (editor, forward) { + return function () { + var newRng = getHorizontalRange(editor, forward); + + if (newRng) { + editor.selection.setRng(newRng); + return true; + } else { + return false; + } + }; }; - var getActionFromRange = function (rootNode, rng) { - return getTableSelectionFromRng(rootNode, rng) - .map(getAction); + var moveV = function (editor, down) { + return function () { + var newRng = getVerticalRange(editor, down); + + if (newRng) { + editor.selection.setRng(newRng); + return true; + } else { + return false; + } + }; }; return { - getActionFromRange: getActionFromRange, - getActionFromCells: getActionFromCells + moveH: moveH, + moveV: moveV }; } ); +define( + 'ephox.katamari.api.Merger', + + [ + 'ephox.katamari.api.Type', + 'global!Array', + 'global!Error' + ], + + function (Type, Array, Error) { + + var shallow = function (old, nu) { + return nu; + }; + + var deep = function (old, nu) { + var bothObjects = Type.isObject(old) && Type.isObject(nu); + return bothObjects ? deepMerge(old, nu) : nu; + }; + + var baseMerge = function (merger) { + return function() { + // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome + var objects = new Array(arguments.length); + for (var i = 0; i < objects.length; i++) objects[i] = arguments[i]; + + if (objects.length === 0) throw new Error('Can\'t merge zero objects'); + + var ret = {}; + for (var j = 0; j < objects.length; j++) { + var curObject = objects[j]; + for (var key in curObject) if (curObject.hasOwnProperty(key)) { + ret[key] = merger(ret[key], curObject[key]); + } + } + return ret; + }; + }; + + var deepMerge = baseMerge(deep); + var merge = baseMerge(shallow); + + return { + deepMerge: deepMerge, + merge: merge + }; + } +); /** - * TableDelete.js + * MatchKeys.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36742,60 +34199,64 @@ define( */ define( - 'tinymce.core.delete.TableDelete', + 'tinymce.core.keyboard.MatchKeys', [ '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' + 'ephox.katamari.api.Merger' ], - 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; + function (Arr, Fun, Merger) { + var defaultPatterns = function (patterns) { + return Arr.map(patterns, function (pattern) { + return Merger.merge({ + shiftKey: false, + altKey: false, + ctrlKey: false, + metaKey: false, + keyCode: 0, + action: Fun.noop + }, pattern); + }); }; - var deleteTableElement = function (editor, table) { - DeleteElement.deleteElement(editor, false, table); - - return true; + var matchesEvent = function (pattern, evt) { + return ( + evt.keyCode === pattern.keyCode && + evt.shiftKey === pattern.shiftKey && + evt.altKey === pattern.altKey && + evt.ctrlKey === pattern.ctrlKey && + evt.metaKey === pattern.metaKey + ); }; - 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 match = function (patterns, evt) { + return Arr.bind(defaultPatterns(patterns), function (pattern) { + return matchesEvent(pattern, evt) ? [pattern] : [ ]; + }); }; - 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 action = function (f) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return f.apply(null, args); + }; }; - var backspaceDelete = function (editor) { - return editor.selection.isCollapsed() ? false : deleteRange(editor); + var execute = function (patterns, evt) { + return Arr.find(match(patterns, evt), function (pattern) { + return pattern.action(); + }); }; return { - backspaceDelete: backspaceDelete + match: match, + action: action, + execute: execute }; } ); - /** - * Commands.js + * ArrowKeys.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36805,61 +34266,43 @@ define( */ define( - 'tinymce.core.delete.DeleteCommands', + 'tinymce.core.keyboard.ArrowKeys', [ - 'tinymce.core.delete.BlockBoundaryDelete', - 'tinymce.core.delete.BlockRangeDelete', - 'tinymce.core.delete.CefDelete', - 'tinymce.core.delete.DeleteUtils', - 'tinymce.core.delete.InlineBoundaryDelete', - 'tinymce.core.delete.TableDelete' + 'tinymce.core.keyboard.BoundarySelection', + 'tinymce.core.keyboard.CefNavigation', + 'tinymce.core.keyboard.MatchKeys', + 'tinymce.core.util.VK' ], - 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); - } + function (BoundarySelection, CefNavigation, MatchKeys, VK) { + var executeKeydownOverride = function (editor, caret, evt) { + 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).each(function (_) { + evt.preventDefault(); + }); }; - 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'); - } + var setup = function (editor, caret) { + editor.on('keydown', function (evt) { + if (evt.isDefaultPrevented() === false) { + executeKeydownOverride(editor, caret, evt); + } + }); }; return { - deleteCommand: deleteCommand, - forwardDeleteCommand: forwardDeleteCommand + setup: setup }; } ); + /** - * InsertList.js + * DeleteBackspaceKeys.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -36868,607 +34311,727 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Handles inserts of lists into the editor instance. - * - * @class tinymce.InsertList - * @private - */ define( - 'tinymce.core.InsertList', + 'tinymce.core.keyboard.DeleteBackspaceKeys', [ - "tinymce.core.util.Tools", - "tinymce.core.caret.CaretWalker", - "tinymce.core.caret.CaretPosition" + 'tinymce.core.delete.BlockBoundaryDelete', + 'tinymce.core.delete.BlockRangeDelete', + 'tinymce.core.delete.CefDelete', + 'tinymce.core.delete.InlineBoundaryDelete', + 'tinymce.core.delete.TableDelete', + 'tinymce.core.keyboard.MatchKeys', + 'tinymce.core.util.VK' ], - function (Tools, CaretWalker, CaretPosition) { - var hasOnlyOneChild = function (node) { - return node.firstChild && node.firstChild === node.lastChild; + function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, InlineBoundaryDelete, TableDelete, MatchKeys, VK) { + var executeKeydownOverride = function (editor, caret, evt) { + 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) }, + { keyCode: VK.DELETE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, true) }, + { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, false) }, + { 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) }, + { keyCode: VK.BACKSPACE, action: MatchKeys.action(TableDelete.backspaceDelete, editor, false) }, + { keyCode: VK.DELETE, action: MatchKeys.action(TableDelete.backspaceDelete, editor, true) } + ], evt).each(function (_) { + evt.preventDefault(); + }); }; - var isPaddingNode = function (node) { - return node.name === 'br' || node.value === '\u00a0'; + var executeKeyupOverride = function (editor, evt) { + MatchKeys.execute([ + { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) }, + { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) } + ], evt); }; - var isPaddedEmptyBlock = function (schema, node) { - var blockElements = schema.getBlockElements(); - return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild); - }; + var setup = function (editor, caret) { + editor.on('keydown', function (evt) { + if (evt.isDefaultPrevented() === false) { + executeKeydownOverride(editor, caret, evt); + } + }); - var isEmptyFragmentElement = function (schema, node) { - var nonEmptyElements = schema.getNonEmptyElements(); - return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node)); + editor.on('keyup', function (evt) { + if (evt.isDefaultPrevented() === false) { + executeKeyupOverride(editor, evt); + } + }); }; - var isListFragment = function (schema, fragment) { - var firstChild = fragment.firstChild; - var lastChild = fragment.lastChild; - - // Skip meta since it's likely
      ..
    - if (firstChild && firstChild.name === 'meta') { - firstChild = firstChild.next; - } - - // Skip mce_marker since it's likely
      ..
    - if (lastChild && lastChild.attr('id') === 'mce_marker') { - lastChild = lastChild.prev; - } - - // Skip last child if it's an empty block - if (isEmptyFragmentElement(schema, lastChild)) { - lastChild = lastChild.prev; - } - - if (!firstChild || firstChild !== lastChild) { - return false; - } - - return firstChild.name === 'ul' || firstChild.name === 'ol'; + return { + setup: setup }; + } +); - var cleanupDomFragment = function (domFragment) { - var firstChild = domFragment.firstChild; - var lastChild = domFragment.lastChild; - - // TODO: remove the meta tag from paste logic - if (firstChild && firstChild.nodeName === 'META') { - firstChild.parentNode.removeChild(firstChild); - } +/** + * InsertNewLine.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 + */ - if (lastChild && lastChild.id === 'mce_marker') { - lastChild.parentNode.removeChild(lastChild); - } +define( + 'tinymce.core.keyboard.InsertNewLine', + [ + 'tinymce.core.caret.CaretContainer', + 'tinymce.core.dom.NodeType', + 'tinymce.core.dom.RangeUtils', + 'tinymce.core.dom.TreeWalker', + 'tinymce.core.text.Zwsp', + 'tinymce.core.util.Tools' + ], + 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; + }; - return domFragment; + var isTableCell = function (node) { + return node && /^(TD|TH|CAPTION)$/.test(node.nodeName); }; - var toDomFragment = function (dom, serializer, fragment) { - var html = serializer.serialize(fragment); - var domFragment = dom.createFragment(html); + var hasFirstChild = function (elm, name) { + return elm.firstChild && elm.firstChild.nodeName == name; + }; - return cleanupDomFragment(domFragment); + var hasParent = function (elm, parentName) { + return elm && elm.parentNode && elm.parentNode.nodeName === parentName; }; - var listItems = function (elm) { - return Tools.grep(elm.childNodes, function (child) { - return child.nodeName === 'LI'; - }); + var emptyBlock = function (elm) { + elm.innerHTML = '
    '; }; - var isEmpty = function (elm) { - return !elm.firstChild; + var containerAndSiblingName = function (container, nodeName) { + return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName); }; - var trimListItems = function (elms) { - return elms.length > 0 && isEmpty(elms[elms.length - 1]) ? elms.slice(0, -1) : elms; + var isListBlock = function (elm) { + return elm && /^(OL|UL|LI)$/.test(elm.nodeName); }; - var getParentLi = function (dom, node) { - var parentBlock = dom.getParent(node, dom.isBlock); - return parentBlock && parentBlock.nodeName === 'LI' ? parentBlock : null; + var isNestedList = function (elm) { + return isListBlock(elm) && isListBlock(elm.parentNode); }; - var isParentBlockLi = function (dom, node) { - return !!getParentLi(dom, node); + // Returns true if the block can be split into two blocks or not + var canSplitBlock = function (dom, node) { + return node && + dom.isBlock(node) && + !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && + !/^(fixed|absolute)/i.test(node.style.position) && + dom.getContentEditable(node) !== "true"; }; - var getSplit = function (parentNode, rng) { - var beforeRng = rng.cloneRange(); - var afterRng = rng.cloneRange(); + // Remove the first empty inline element of the block so this:

    x

    becomes this:

    x

    + var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) { + var node = block, firstChilds = [], i; - beforeRng.setStartBefore(parentNode); - afterRng.setEndAfter(parentNode); + if (!node) { + return; + } - return [ - beforeRng.cloneContents(), - afterRng.cloneContents() - ]; - }; + // Find inner most first child ex:

    *

    + while ((node = node.firstChild)) { + if (dom.isBlock(node)) { + return; + } - var findFirstIn = function (node, rootNode) { - var caretPos = CaretPosition.before(node); - var caretWalker = new CaretWalker(rootNode); - var newCaretPos = caretWalker.next(caretPos); + if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + firstChilds.push(node); + } + } - return newCaretPos ? newCaretPos.toRange() : null; + i = firstChilds.length; + while (i--) { + node = firstChilds[i]; + if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { + dom.remove(node); + } else { + if (isEmptyAnchor(node)) { + dom.remove(node); + } + } + } }; - var findLastOf = function (node, rootNode) { - var caretPos = CaretPosition.after(node); - var caretWalker = new CaretWalker(rootNode); - var newCaretPos = caretWalker.prev(caretPos); - - return newCaretPos ? newCaretPos.toRange() : null; + var normalizeZwspOffset = function (start, container, offset) { + if (NodeType.isText(container) === false) { + return offset; + } if (start) { + return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset; + } else { + return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset; + } }; - var insertMiddle = function (target, elms, rootNode, rng) { - var parts = getSplit(target, rng); - var parentElm = target.parentNode; - - parentElm.insertBefore(parts[0], target); - Tools.each(elms, function (li) { - parentElm.insertBefore(li, target); - }); - parentElm.insertBefore(parts[1], target); - parentElm.removeChild(target); - - return findLastOf(elms[elms.length - 1], rootNode); + var includeZwspInRange = function (rng) { + var newRng = rng.cloneRange(); + newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); + newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); + return newRng; }; - var insertBefore = function (target, elms, rootNode) { - var parentElm = target.parentNode; - - Tools.each(elms, function (elm) { - parentElm.insertBefore(elm, target); - }); + var firstNonWhiteSpaceNodeSibling = function (node) { + while (node) { + if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) { + return node; + } - return findFirstIn(target, rootNode); + node = node.nextSibling; + } }; - var insertAfter = function (target, elms, rootNode, dom) { - dom.insertAfter(elms.reverse(), target); - return findLastOf(elms[0], rootNode); + // 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); }; - var insertAtCaret = function (serializer, dom, rng, fragment) { - var domFragment = toDomFragment(dom, serializer, fragment); - var liTarget = getParentLi(dom, rng.startContainer); - var liElms = trimListItems(listItems(domFragment.firstChild)); - var BEGINNING = 1, END = 2; - var rootNode = dom.getRoot(); + // 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]+/, ''); + } - var isAt = function (location) { - var caretPos = CaretPosition.fromRangeStart(rng); - var caretWalker = new CaretWalker(dom.getRoot()); - var newPos = location === BEGINNING ? caretWalker.prev(caretPos) : caretWalker.next(caretPos); + node = node.firstChild; + } while (node); + }; - return newPos ? getParentLi(dom, newPos.getNode()) !== liTarget : true; - }; + var getEditableRoot = function (dom, node) { + var root = dom.getRoot(), parent, editableRoot; - if (isAt(BEGINNING)) { - return insertBefore(liTarget, liElms, rootNode); - } else if (isAt(END)) { - return insertAfter(liTarget, liElms, rootNode, dom); - } + // 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; + } - return insertMiddle(liTarget, liElms, rootNode, rng); - }; + parent = parent.parentNode; + } - return { - isListFragment: isListFragment, - insertAtCaret: insertAtCaret, - isParentBlockLi: isParentBlockLi, - trimListItems: trimListItems, - listItems: listItems + return parent !== root ? editableRoot : root; }; - } -); -/** - * InsertContent.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 contents into the editor instance. - * - * @class tinymce.InsertContent - * @private - */ -define( - 'tinymce.core.InsertContent', - [ - 'ephox.katamari.api.Option', - 'ephox.sugar.api.node.Element', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.caret.CaretWalker', - 'tinymce.core.dom.ElementUtils', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.PaddingBr', - 'tinymce.core.dom.RangeNormalizer', - 'tinymce.core.Env', - 'tinymce.core.html.Serializer', - 'tinymce.core.InsertList', - 'tinymce.core.util.Tools' - ], - function (Option, Element, CaretPosition, CaretWalker, ElementUtils, NodeType, PaddingBr, RangeNormalizer, Env, Serializer, InsertList, Tools) { - var isTableCell = NodeType.matchNodeNames('td th'); + var setForcedBlockAttrs = function (editor, node) { + var forcedRootBlockName = editor.settings.forced_root_block; - var validInsertion = function (editor, value, parentNode) { - // Should never insert content into bogus elements, since these can - // be resize handles or similar - if (parentNode.getAttribute('data-mce-bogus') === 'all') { - parentNode.parentNode.insertBefore(editor.dom.createFragment(value), parentNode); - } else { - // Check if parent is empty or only has one BR element then set the innerHTML of that parent - var node = parentNode.firstChild; - var node2 = parentNode.lastChild; - if (!node || (node === node2 && node.nodeName === 'BR')) {/// - editor.dom.setHTML(parentNode, value); - } else { - editor.selection.setContent(value); - } + if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { + editor.dom.setAttribs(node, editor.settings.forced_root_block_attrs); } }; - var trimBrsFromTableCell = function (dom, elm) { - Option.from(dom.getParent(elm, 'td,th')).map(Element.fromDom).each(PaddingBr.trimBlockTrailingBr); - }; + // 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); - var insertHtmlAtCaret = function (editor, value, details) { - var parser, serializer, parentNode, rootNode, fragment, args; - var marker, rng, node, node2, bookmarkHtml, merge; - var textInlineElements = editor.schema.getTextInlineElements(); - var selection = editor.selection, dom = editor.dom; + // 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; - function trimOrPaddLeftRight(html) { - var rng, container, offset; + if (parentBlock == editor.getBody() || isTableCell(parentBlock)) { + rootBlockName = parentBlock.nodeName.toLowerCase(); + } else { + rootBlockName = parentBlock.parentNode.nodeName.toLowerCase(); + } - rng = selection.getRng(true); - container = rng.startContainer; - offset = rng.startOffset; + if (!parentBlock.hasChildNodes()) { + newBlock = dom.create(blockName); + setForcedBlockAttrs(editor, newBlock); + parentBlock.appendChild(newBlock); + rng.setStart(newBlock, 0); + rng.setEnd(newBlock, 0); + return newBlock; + } - function hasSiblingText(siblingName) { - return container[siblingName] && container[siblingName].nodeType == 3; + // Find parent that is the first child of parentBlock + node = container; + while (node.parentNode != parentBlock) { + node = node.parentNode; } - if (container.nodeType == 3) { - if (offset > 0) { - html = html.replace(/^ /, ' '); - } else if (!hasSiblingText('previousSibling')) { - html = html.replace(/^ /, ' '); - } + // Loop left to find start node start wrapping at + while (node && !dom.isBlock(node)) { + startNode = node; + node = node.previousSibling; + } - if (offset < container.length) { - html = html.replace(/ (
    |)$/, ' '); - } else if (!hasSiblingText('nextSibling')) { - html = html.replace(/( | )(
    |)$/, ' '); + 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; } - } - return html; + // Restore range to it's past location + rng.setStart(container, offset); + rng.setEnd(container, offset); + } } - // Removes   from a [b] c -> a  c -> a c - function trimNbspAfterDeleteAndPaddValue() { - var rng, container, offset; - - rng = selection.getRng(true); - container = rng.startContainer; - offset = rng.startOffset; + return container; + }; - if (container.nodeType == 3 && rng.collapsed) { - if (container.data[offset] === '\u00a0') { - container.deleteData(offset, 1); + // 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; - if (!/[\u00a0| ]$/.test(value)) { - value += ' '; - } - } else if (container.data[offset - 1] === '\u00a0') { - container.deleteData(offset - 1, 1); + // IE will render the blocks correctly other browsers needs a BR + block.normalize(); // Remove empty text nodes that got left behind by the extract - if (!/[\u00a0| ]$/.test(value)) { - value = ' ' + value; - } - } - } + // 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'); } + }; - function reduceInlineTextElements() { - if (merge) { - var root = editor.getBody(), elementUtils = new ElementUtils(dom); + var getContainerBlock = function (containerBlock) { + var containerBlockParent = containerBlock.parentNode; - Tools.each(dom.select('*[data-mce-fragment]'), function (node) { - for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) { - if (textInlineElements[node.nodeName.toLowerCase()] && elementUtils.compare(testNode, node)) { - dom.remove(node, true); - } - } - }); - } + if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { + return containerBlockParent; } - function markFragmentElements(fragment) { - var node = fragment; - - while ((node = node.walk())) { - if (node.type === 1) { - node.attr('data-mce-fragment', '1'); - } - } - } + return containerBlock; + }; - function umarkFragmentElements(elm) { - Tools.each(elm.getElementsByTagName('*'), function (elm) { - elm.removeAttribute('data-mce-fragment'); - }); - } + var isFirstOrLastLi = function (containerBlock, parentBlock, first) { + var node = containerBlock[first ? 'firstChild' : 'lastChild']; - function isPartOfFragment(node) { - return !!node.getAttribute('data-mce-fragment'); - } + // Find first/last element since there might be whitespace there + while (node) { + if (node.nodeType == 1) { + break; + } - function canHaveChildren(node) { - return node && !editor.schema.getShortEndedElements()[node.nodeName]; + node = node[first ? 'nextSibling' : 'previousSibling']; } - function moveSelectionToMarker(marker) { - var parentEditableFalseElm, parentBlock, nextRng; - - function getContentEditableFalseParent(node) { - var root = editor.getBody(); + return node === parentBlock; + }; - for (; node && node !== root; node = node.parentNode) { - if (editor.dom.getContentEditable(node) === 'false') { - return node; - } - } + 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 schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); + var rng = editor.selection.getRng(); - return null; - } + // 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(); - if (!marker) { + if (!root) { return; } - selection.scrollIntoView(marker); + if (/^(LI|DT|DD)$/.test(root.nodeName)) { + var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); - // If marker is in cE=false then move selection to that element instead - parentEditableFalseElm = getContentEditableFalseParent(marker); - if (parentEditableFalseElm) { - dom.remove(marker); - selection.select(parentEditableFalseElm); - return; + if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) { + root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); + } } - // Move selection before marker and remove it rng = dom.createRng(); + root.normalize(); - // If previous sibling is a text node set the selection to the end of that node - node = marker.previousSibling; - if (node && node.nodeType == 3) { - rng.setStart(node, node.nodeValue.length); + if (root.hasChildNodes()) { + walker = new TreeWalker(root, root); - // TODO: Why can't we normalize on IE - if (!Env.ie) { - node2 = marker.nextSibling; - if (node2 && node2.nodeType == 3) { - node.appendData(node2.data); - node2.parentNode.removeChild(node2); + while ((node = walker.current())) { + if (node.nodeType == 3) { + rng.setStart(node, 0); + rng.setEnd(node, 0); + break; + } + + if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) { + rng.setStartBefore(node); + rng.setEndBefore(node); + break; } + + lastNode = node; + node = walker.next(); + } + + if (!node) { + rng.setStart(lastNode, 0); + rng.setEnd(lastNode, 0); } } else { - // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node - rng.setStartBefore(marker); - rng.setEndBefore(marker); + if (root.nodeName == 'BR') { + if (root.nextSibling && dom.isBlock(root.nextSibling)) { + rng.setStartBefore(root); + rng.setEndBefore(root); + } else { + rng.setStartAfter(root); + rng.setEndAfter(root); + } + } else { + rng.setStart(root, 0); + rng.setEnd(root, 0); + } } - function findNextCaretRng(rng) { - var caretPos = CaretPosition.fromRangeStart(rng); - var caretWalker = new CaretWalker(editor.getBody()); + selection.setRng(rng); - caretPos = caretWalker.next(caretPos); - if (caretPos) { - return caretPos.toRange(); - } + // Remove tempElm created for old IE:s + dom.remove(tempElm); + selection.scrollIntoView(root); + } + + // Creates a new block element by cloning the current one or creating a new one if the name is specified + // This function will also copy any text formatting from the parent block and add it to the new one + function createNewBlock(name) { + var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements(); + + if (name || parentBlockName == "TABLE" || parentBlockName == "HR") { + block = dom.create(name || newBlockName); + setForcedBlockAttrs(editor, block); + } else { + block = parentBlock.cloneNode(false); } - // Remove the marker node and set the new range - parentBlock = dom.getParent(marker, dom.isBlock); - dom.remove(marker); + caretNode = block; - if (parentBlock && dom.isEmpty(parentBlock)) { - editor.$(parentBlock).empty(); + if (settings.keep_styles === false) { + dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block + dom.setAttrib(block, 'class', null); + } else { + // Clone any parent styles + do { + if (textInlineElements[node.nodeName]) { + // Never clone a caret containers + if (node.id == '_mce_caret') { + continue; + } - rng.setStart(parentBlock, 0); - rng.setEnd(parentBlock, 0); + clonedNode = node.cloneNode(false); + dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique - if (!isTableCell(parentBlock) && !isPartOfFragment(parentBlock) && (nextRng = findNextCaretRng(rng))) { - rng = nextRng; - dom.remove(parentBlock); + if (block.hasChildNodes()) { + clonedNode.appendChild(block.firstChild); + block.appendChild(clonedNode); + } else { + caretNode = clonedNode; + block.appendChild(clonedNode); + } + } + } while ((node = node.parentNode) && node != editableRoot); + } + + emptyBlock(caretNode); + + return block; + } + + // Returns true/false if the caret is at the start/end of the parent block element + function isCaretAtStartOrEndOfBlock(start) { + var walker, node, name, normalizedOffset; + + normalizedOffset = normalizeZwspOffset(start, container, offset); + + // Caret is in the middle of a text node like "a|b" + if (container.nodeType == 3 && (start ? normalizedOffset > 0 : normalizedOffset < container.nodeValue.length)) { + return false; + } + + // If after the last element in block node edge case for #5091 + if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { + return true; + } + + // If the caret if before the first element in parentBlock + if (start && container.nodeType == 1 && container == parentBlock.firstChild) { + return true; + } + + // Caret can be before/after a table or a hr + if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) { + return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); + } + + // Walk the DOM and look for text nodes or non empty elements + walker = new TreeWalker(container, parentBlock); + + // If caret is in beginning or end of a text block then jump to the next/previous node + if (container.nodeType == 3) { + if (start && normalizedOffset === 0) { + walker.prev(); + } else if (!start && normalizedOffset == container.nodeValue.length) { + walker.next(); + } + } + + while ((node = walker.current())) { + if (node.nodeType === 1) { + // Ignore bogus elements + if (!node.getAttribute('data-mce-bogus')) { + // Keep empty elements like but not trailing br:s like

    text|

    + name = node.nodeName.toLowerCase(); + if (nonEmptyElementsMap[name] && name !== 'br') { + return false; + } + } + } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { + return false; + } + + if (start) { + walker.prev(); } else { - dom.add(parentBlock, dom.create('br', { 'data-mce-bogus': '1' })); + walker.next(); } } - selection.setRng(rng); + return true; } - // Check for whitespace before/after value - if (/^ | $/.test(value)) { - value = trimOrPaddLeftRight(value); - } + // Inserts a block or br before/after or in the middle of a split list of the LI is empty + function handleEmptyListItem() { + if (containerBlock == editor.getBody()) { + return; + } - // Setup parser and serializer - parser = editor.parser; - merge = details.merge; + if (isNestedList(containerBlock)) { + newBlockName = 'LI'; + } - serializer = new Serializer({ - validate: editor.settings.validate - }, editor.schema); - bookmarkHtml = '​'; + newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); - // Run beforeSetContent handlers on the HTML to be inserted - args = { content: value, format: 'html', selection: true }; - editor.fire('BeforeSetContent', args); - value = args.content; + if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) { + if (hasParent(containerBlock, 'LI')) { + // Nested list is inside a LI + dom.insertAfter(newBlock, getContainerBlock(containerBlock)); + } else { + // Is first and last list item then replace the OL/UL with a text block + dom.replace(newBlock, containerBlock); + } + } else if (isFirstOrLastLi(containerBlock, parentBlock, true)) { + if (hasParent(containerBlock, 'LI')) { + // List nested in an LI then move the list to a new sibling LI + dom.insertAfter(newBlock, getContainerBlock(containerBlock)); + newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed + newBlock.appendChild(containerBlock); + } else { + // First LI in list then remove LI and add text block before list + containerBlock.parentNode.insertBefore(newBlock, containerBlock); + } + } else if (isFirstOrLastLi(containerBlock, parentBlock, false)) { + // Last LI in list then remove LI and add text block after list + dom.insertAfter(newBlock, getContainerBlock(containerBlock)); + } else { + // Middle LI in list the split the list and insert a text block in the middle + // Extract after fragment and insert it after the current block + containerBlock = getContainerBlock(containerBlock); + tmpRng = rng.cloneRange(); + tmpRng.setStartAfter(parentBlock); + tmpRng.setEndAfter(containerBlock); + fragment = tmpRng.extractContents(); - // Add caret at end of contents if it's missing - if (value.indexOf('{$caret}') == -1) { - value += '{$caret}'; + if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { + newBlock = fragment.firstChild; + dom.insertAfter(fragment, containerBlock); + } else { + dom.insertAfter(fragment, containerBlock); + dom.insertAfter(newBlock, containerBlock); + } + } + + dom.remove(parentBlock); + moveToCaretPosition(newBlock); } - // Replace the caret marker with a span bookmark element - value = value.replace(/\{\$caret\}/, bookmarkHtml); + 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(); + } - // 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); + // 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); } - } - // 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(); + moveToCaretPosition(newBlock); } - parentNode = selection.getNode(); + // 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; - // Parse the fragment within the context of the parent node - var parserArgs = { context: parentNode.nodeName.toLowerCase(), data: details.data }; - fragment = parser.parse(value, parserArgs); + // Resolve node index + if (container.nodeType == 1 && container.hasChildNodes()) { + isAfterLastNodeInContainer = offset > container.childNodes.length - 1; - // 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; + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } } - markFragmentElements(fragment); + // Get editable root node, normally the body element but sometimes a div or span + editableRoot = getEditableRoot(dom, container); - // Move the caret to a more suitable location - node = fragment.lastChild; - if (node.attr('id') == 'mce_marker') { - marker = node; + // If there is no editable root then enter is done inside a contentEditable false element + if (!editableRoot) { + return; + } - 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; - } + // 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; } - editor._selectionOverrides.showBlockCaretContainer(parentNode); + // Wrap the current node and it's sibling in a default block if it's needed. + // for example this text|text2 will become this

    text|text2

    + // This won't happen if root blocks are disabled or the shiftKey is pressed + if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { + container = wrapSelfAndSiblingsInDefaultBlock(editor, newBlockName, rng, container, offset); + } - // 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 + // Find parent block and setup empty block paddings + parentBlock = dom.getParent(container, dom.isBlock); + containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; - // Insert bookmark node and get the parent - selection.setContent(bookmarkHtml); - parentNode = selection.getNode(); - rootNode = editor.getBody(); + // Setup block names + parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - // Opera will return the document node when selection is in root - if (parentNode.nodeType == 9) { - parentNode = node = rootNode; - } else { - node = parentNode; - } + // Enter inside block contained within a LI then split or insert before/after LI + if (containerBlockName == 'LI' && !evt.ctrlKey) { + parentBlock = containerBlock; + containerBlock = containerBlock.parentNode; + parentBlockName = containerBlockName; + } - // Find the ancestor just before the root element - while (node !== rootNode) { - parentNode = node; - node = node.parentNode; + // Handle enter in list item + if (/^(LI|DT|DD)$/.test(parentBlockName)) { + if (!newBlockName && shiftKey) { + insertBr(editor, evt); + return; } - // 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); - }) - ) - ); + // Handle enter inside an empty list item + if (dom.isEmpty(parentBlock)) { + handleEmptyListItem(); + return; + } + } - // 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); + // Don't split PRE tags but insert a BR instead easier when writing code samples etc + if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { + if (!shiftKey) { + insertBr(editor, evt); + return; + } + } else { + // If no root block is configured then insert a BR by default or if the shiftKey is pressed + if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { + insertBr(editor, evt); + return; } } - reduceInlineTextElements(); - moveSelectionToMarker(dom.get('mce_marker')); - umarkFragmentElements(editor.getBody()); - trimBrsFromTableCell(editor.dom, editor.selection.getStart()); + // If parent block is root then never insert new blocks + if (newBlockName && parentBlock === editor.getBody()) { + return; + } - editor.fire('SetContent', args); - editor.addVisual(); - }; + // Default block name if it's not configured + newBlockName = newBlockName || 'P'; - var processValue = function (value) { - var details; + // Insert new block before/after the parent block depending on caret location + if (CaretContainer.isCaretContainerBlock(parentBlock)) { + newBlock = CaretContainer.showCaretContainerBlock(parentBlock); + if (dom.isEmpty(parentBlock)) { + emptyBlock(parentBlock); + } + moveToCaretPosition(newBlock); + } else if (isCaretAtStartOrEndOfBlock()) { + insertNewBlockAfter(); + } else if (isCaretAtStartOrEndOfBlock(true)) { + // Insert new block before + newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); - if (typeof value !== 'string') { - details = Tools.extend({ - paste: value.paste, - data: { - paste: value.paste - } - }, value); + // Adjust caret position if HR + containerAndSiblingName(parentBlock, 'HR') ? moveToCaretPosition(newBlock) : moveToCaretPosition(parentBlock); + } else { + // Extract after fragment and insert it after the current block + tmpRng = includeZwspInRange(rng).cloneRange(); + tmpRng.setEndAfter(parentBlock); + fragment = tmpRng.extractContents(); + trimLeadingLineBreaks(fragment); + newBlock = fragment.firstChild; + dom.insertAfter(fragment, parentBlock); + trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock); + addBrToBlockIfNeeded(dom, parentBlock); - return { - content: value.content, - details: details - }; + if (dom.isEmpty(parentBlock)) { + emptyBlock(parentBlock); + } + + newBlock.normalize(); + + // New block might become empty if it's

    a |

    + if (dom.isEmpty(newBlock)) { + dom.remove(newBlock); + insertNewBlockAfter(); + } else { + moveToCaretPosition(newBlock); + } } - return { - content: value, - details: {} - }; - }; + dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique - var insertAtCaret = function (editor, value) { - var result = processValue(value); - insertHtmlAtCaret(editor, result.content, result.details); + // Allow custom handling of new blocks + editor.fire('NewBlock', { newBlock: newBlock }); }; return { - insertAtCaret: insertAtCaret + insert: insert }; } ); + /** - * EditorCommands.js + * EnterKey.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -37477,10458 +35040,933 @@ define( * 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.keyboard.EnterKey', [ - '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' + 'tinymce.core.keyboard.InsertNewLine', + 'tinymce.core.util.VK' ], - 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; + function (InsertNewLine, VK) { + var endTypingLevel = function (undoManager) { + if (undoManager.typing) { + undoManager.typing = false; + undoManager.add(); + } + }; - 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; + var handleEnterKeyEvent = function (editor, event) { + if (event.isDefaultPrevented()) { + return; + } - if (editor.removed) { - return; - } + event.preventDefault(); - if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { - editor.focus(); + endTypingLevel(editor.undoManager); + editor.undoManager.transact(function () { + if (editor.selection.isCollapsed() === false) { + editor.execCommand('Delete'); } - args = editor.fire('BeforeExecCommand', { command: command, ui: ui, value: value }); - if (args.isDefaultPrevented()) { - return false; - } + InsertNewLine.insert(editor, event); + }); + }; - customCommand = command.toLowerCase(); - if ((func = commands.exec[customCommand])) { - func(customCommand, ui, value); - editor.fire('ExecCommand', { command: command, ui: ui, value: value }); - return true; + var setup = function (editor) { + editor.on('keydown', function (event) { + if (event.keyCode === VK.ENTER) { + handleEnterKeyEvent(editor, event); } + }); + }; - // 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; - } - }); + return { + setup: setup + }; + } +); - if (state) { - return state; - } +/** + * InsertSpace.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 + */ - // 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; - } +define( + 'tinymce.core.keyboard.InsertSpace', + [ + 'ephox.katamari.api.Fun', + 'tinymce.core.caret.CaretPosition', + 'tinymce.core.dom.NodeType', + 'tinymce.core.keyboard.BoundaryLocation', + 'tinymce.core.keyboard.InlineUtils' + ], + function (Fun, CaretPosition, NodeType, BoundaryLocation, InlineUtils) { + var isValidInsertPoint = function (location, caretPosition) { + return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container()); + }; - // Browser commands - try { - state = editor.getDoc().execCommand(command, ui, value); - } catch (ex) { - // Ignore old IE errors - } + var insertNbspAtPosition = function (editor, caretPosition) { + var container = caretPosition.container(); + var offset = caretPosition.offset(); - if (state) { - editor.fire('ExecCommand', { command: command, ui: ui, value: value }); - return true; - } + container.insertData(offset, '\u00a0'); + editor.selection.setCursorLocation(container, offset + 1); + }; + var insertAtLocation = function (editor, caretPosition, location) { + if (isValidInsertPoint(location, caretPosition)) { + insertNbspAtPosition(editor, caretPosition); + return true; + } else { 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; + var insertAtCaret = function (editor) { + var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); + var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); + var boundaryLocation = BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), caretPosition); + return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false); + }; - if (editor.quirks.isHidden() || editor.removed) { - return; - } + var isAtStartOrEnd = function (location) { + return location.fold( + Fun.constant(false), // Before + Fun.constant(true), // Start + Fun.constant(true), // End + Fun.constant(false) // After + ); + }; - command = command.toLowerCase(); - if ((func = commands.state[command])) { - return func(command); - } + var insertAtSelection = function (editor) { + return editor.selection.isCollapsed() ? insertAtCaret(editor) : false; + }; - // Browser commands - try { - return editor.getDoc().queryCommandState(command); - } catch (ex) { - // Fails sometimes see bug: 1896577 - } + return { + insertAtSelection: insertAtSelection + }; + } +); - return false; - } +/** + * SpaceKey.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 + */ - /** - * 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; +define( + 'tinymce.core.keyboard.SpaceKey', + [ + 'tinymce.core.keyboard.InsertSpace', + 'tinymce.core.keyboard.MatchKeys', + 'tinymce.core.util.VK' + ], + function (InsertSpace, MatchKeys, VK) { + var executeKeydownOverride = function (editor, evt) { + MatchKeys.execute([ + { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) } + ], evt).each(function (_) { + evt.preventDefault(); + }); + }; - if (editor.quirks.isHidden() || editor.removed) { - return; + var setup = function (editor) { + editor.on('keydown', function (evt) { + if (evt.isDefaultPrevented() === false) { + executeKeydownOverride(editor, evt); } + }); + }; - command = command.toLowerCase(); - if ((func = commands.value[command])) { - return func(command); - } + return { + setup: setup + }; + } +); - // Browser commands - try { - return editor.getDoc().queryCommandValue(command); - } catch (ex) { - // Fails sometimes see bug: 1896577 - } - } +/** + * KeyboardOverrides.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 + */ - /** - * 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'; +define( + 'tinymce.core.keyboard.KeyboardOverrides', + [ + 'tinymce.core.keyboard.ArrowKeys', + 'tinymce.core.keyboard.BoundarySelection', + 'tinymce.core.keyboard.DeleteBackspaceKeys', + 'tinymce.core.keyboard.EnterKey', + 'tinymce.core.keyboard.SpaceKey' + ], + function (ArrowKeys, BoundarySelection, DeleteBackspaceKeys, EnterKey, SpaceKey) { + var setup = function (editor) { + var caret = BoundarySelection.setupSelectedState(editor); - each(commandList, function (callback, command) { - each(command.toLowerCase().split(','), function (command) { - commands[type][command] = callback; - }); - }); - } + ArrowKeys.setup(editor, caret); + DeleteBackspaceKeys.setup(editor, caret); + EnterKey.setup(editor); + SpaceKey.setup(editor); + }; - function addCommand(command, callback, scope) { - command = command.toLowerCase(); - commands.exec[command] = function (command, ui, value, args) { - return callback.call(scope || editor, ui, value, args); - }; - } + return { + setup: setup + }; + } +); +/** + * NodeChange.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 nodechange event dispatching both manual and through selection change events. + * + * @class tinymce.NodeChange + * @private + */ +define( + 'tinymce.core.NodeChange', + [ + "tinymce.core.dom.RangeUtils", + "tinymce.core.Env", + "tinymce.core.util.Delay" + ], + function (RangeUtils, Env, Delay) { + return function (editor) { + var lastRng, lastPath = []; /** - * Returns true/false if the command is supported or not. + * Returns true/false if the current element path has been changed or not. * - * @method queryCommandSupported - * @param {String} command Command that we check support for. - * @return {Boolean} true/false if the command is supported or not. + * @private + * @return {Boolean} True if the element path is the same false if it's not. */ - function queryCommandSupported(command) { - command = command.toLowerCase(); + function isSameElementPath(startElm) { + var i, currentPath; - if (commands.exec[command]) { - return true; - } + currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm); + if (currentPath.length === lastPath.length) { + for (i = currentPath.length; i >= 0; i--) { + if (currentPath[i] !== lastPath[i]) { + break; + } + } - // Browser commands - try { - return editor.getDoc().queryCommandSupported(command); - } catch (ex) { - // Fails sometimes see bug: 1896577 + if (i === -1) { + lastPath = currentPath; + return true; + } } + lastPath = currentPath; + return false; } - function addQueryStateHandler(command, callback, scope) { - command = command.toLowerCase(); - commands.state[command] = function () { - return callback.call(scope || editor); - }; - } + // Gecko doesn't support the "selectionchange" event + if (!('onselectionchange' in editor.getDoc())) { + editor.on('NodeChange Click MouseUp KeyUp Focus', function (e) { + var nativeRng, fakeRng; - function addQueryValueHandler(command, callback, scope) { - command = command.toLowerCase(); - commands.value[command] = function () { - return callback.call(scope || editor); - }; - } + // Since DOM Ranges mutate on modification + // of the DOM we need to clone it's contents + nativeRng = editor.selection.getRng(); + fakeRng = { + startContainer: nativeRng.startContainer, + startOffset: nativeRng.startOffset, + endContainer: nativeRng.endContainer, + endOffset: nativeRng.endOffset + }; - function hasCustomCommand(command) { - command = command.toLowerCase(); - return !!commands.exec[command]; + // Always treat nodechange as a selectionchange since applying + // formatting to the current range wouldn't update the range but it's parent + if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) { + editor.fire('SelectionChange'); + } + + lastRng = fakeRng; + }); } - // Expose public methods - extend(this, { - execCommand: execCommand, - queryCommandState: queryCommandState, - queryCommandValue: queryCommandValue, - queryCommandSupported: queryCommandSupported, - addCommands: addCommands, - addCommand: addCommand, - addQueryStateHandler: addQueryStateHandler, - addQueryValueHandler: addQueryValueHandler, - hasCustomCommand: hasCustomCommand + // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body + // When the contextmenu event fires the selection is located at the right location + editor.on('contextmenu', function () { + editor.fire('SelectionChange'); }); - // Private methods + // Selection change is delayed ~200ms on IE when you click inside the current range + editor.on('SelectionChange', function () { + var startElm = editor.selection.getStart(true); - function execNativeCommand(command, ui, value) { - if (ui === undefined) { - ui = FALSE; + // 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 (!startElm || (!Env.range && editor.selection.isCollapsed())) { + return; } - if (value === undefined) { - value = null; + if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) { + editor.nodeChanged({ selectionChange: true }); } + }); - 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; + // Fire an extra nodeChange on mouseup for compatibility reasons + editor.on('MouseUp', function (e) { + if (!e.isDefaultPrevented()) { + // Delay nodeChanged call for WebKit edge case issue where the range + // isn't updated until after you click outside a selected image + if (editor.selection.getNode().nodeName == 'IMG') { + Delay.setEditorTimeout(editor, function () { + editor.nodeChanged(); + }); + } else { + editor.nodeChanged(); } + } + }); - // 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." - ); + /** + * Dispatches out a onNodeChange event to all observers. This method should be called when you + * need to update the UI states or element path etc. + * + * @method nodeChanged + * @param {Object} args Optional args to pass to NodeChange event handlers. + */ + this.nodeChanged = function (args) { + var selection = editor.selection, node, parents, root; - if (Env.mac) { - msg = msg.replace(/Ctrl\+/g, '\u2318+'); - } + // Fix for bug #1896577 it seems that this can not be fired while the editor is loading + if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) { + // Get start node + root = editor.getBody(); + node = selection.getStart(true) || root; - editor.notificationManager.open({ text: msg, type: 'error' }); + // Make sure the node is within the editor root or is the editor root + if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) { + node = root; } - }, - // Override unlink command - unlink: function () { - if (selection.isCollapsed()) { - var elm = editor.dom.getParent(editor.selection.getStart(), 'a'); - if (elm) { - editor.dom.remove(elm, true); + // Get parents and add them to object + parents = []; + editor.dom.getParent(node, function (node) { + if (node === root) { + return 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'; - } + parents.push(node); + }); - // 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, '
    '); - }, - - mceToggleVisualAid: function () { - editor.hasVisual = !editor.hasVisual; - editor.addVisual(); - }, - - mceReplaceContent: function (command, ui, value) { - editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({ format: 'text' }))); - }, - - mceInsertLink: function (command, ui, value) { - var anchor; - - if (typeof value == 'string') { - value = { href: value }; - } - - anchor = dom.getParent(selection.getNode(), 'a'); - - // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here. - value.href = value.href.replace(' ', '%20'); - - // Remove existing links if there could be child links or that the href isn't specified - if (!anchor || !value.href) { - formatter.remove('link'); - } - - // Apply new link to selection - if (value.href) { - formatter.apply('link', value, anchor); - } - }, - - selectAll: function () { - var root = dom.getRoot(), rng; - - if (selection.getRng().setStart) { - var editingHost = dom.getParent(selection.getStart(), NodeType.isContentEditableTrue); - if (editingHost) { - rng = dom.createRng(); - rng.selectNodeContents(editingHost); - selection.setRng(rng); - } - } else { - // IE will render it's own root level block elements and sometimes - // even put font elements in them when the user starts typing. So we need to - // move the selection to a more suitable element from this: - // |

    to this:

    |

    - 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(); - } - }); - }; - } -); - -/** - * 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); - } - - editor.getWin().focus(); - } - - // 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); - } - - // 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(); - } - - 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 - }; - } -); - -/** - * EditorObservable.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 mixin contains the event logic for the tinymce.Editor class. - * - * @mixin tinymce.EditorObservable - * @extends tinymce.util.Observable - */ -define( - 'tinymce.core.EditorObservable', - [ - "tinymce.core.util.Observable", - "tinymce.core.dom.DOMUtils", - "tinymce.core.util.Tools" - ], - function (Observable, DOMUtils, Tools) { - var DOM = DOMUtils.DOM, customEventRootDelegates; - - /** - * Returns the event target so for the specified event. Some events fire - * only on document, some fire on documentElement etc. This also handles the - * custom event root setting where it returns that element instead of the body. - * - * @private - * @param {tinymce.Editor} editor Editor instance to get event target from. - * @param {String} eventName Name of the event for example "click". - * @return {Element/Document} HTML Element or document target to bind on. - */ - function getEventTarget(editor, eventName) { - if (eventName == 'selectionchange') { - return editor.getDoc(); - } - - // Need to bind mousedown/mouseup etc to document not body in iframe mode - // Since the user might click on the HTML element not the BODY - if (!editor.inline && /^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(eventName)) { - return editor.getDoc().documentElement; - } - - // Bind to event root instead of body if it's defined - if (editor.settings.event_root) { - if (!editor.eventRoot) { - editor.eventRoot = DOM.select(editor.settings.event_root)[0]; - } - - return editor.eventRoot; - } - - return editor.getBody(); - } - - /** - * Binds a event delegate for the specified name this delegate will fire - * the event to the editor dispatcher. - * - * @private - * @param {tinymce.Editor} editor Editor instance to get event target from. - * @param {String} eventName Name of the event for example "click". - */ - function bindEventDelegate(editor, eventName) { - var eventRootElm, delegate; - - function isListening(editor) { - return !editor.hidden && !editor.readonly; - } - - if (!editor.delegates) { - editor.delegates = {}; - } - - if (editor.delegates[eventName] || editor.removed) { - return; - } - - eventRootElm = getEventTarget(editor, eventName); - - if (editor.settings.event_root) { - if (!customEventRootDelegates) { - customEventRootDelegates = {}; - editor.editorManager.on('removeEditor', function () { - var name; - - if (!editor.editorManager.activeEditor) { - if (customEventRootDelegates) { - for (name in customEventRootDelegates) { - editor.dom.unbind(getEventTarget(editor, name)); - } - - customEventRootDelegates = null; - } - } - }); - } - - if (customEventRootDelegates[eventName]) { - return; - } - - delegate = function (e) { - var target = e.target, editors = editor.editorManager.get(), i = editors.length; - - while (i--) { - var body = editors[i].getBody(); - - if (body === target || DOM.isChildOf(target, body)) { - if (isListening(editors[i])) { - editors[i].fire(eventName, e); - } - } - } - }; - - customEventRootDelegates[eventName] = delegate; - DOM.bind(eventRootElm, eventName, delegate); - } else { - delegate = function (e) { - if (isListening(editor)) { - editor.fire(eventName, e); - } - }; - - DOM.bind(eventRootElm, eventName, delegate); - editor.delegates[eventName] = delegate; - } - } - - var EditorObservable = { - /** - * Bind any pending event delegates. This gets executed after the target body/document is created. - * - * @private - */ - bindPendingEventDelegates: function () { - var self = this; - - Tools.each(self._pendingNativeEvents, function (name) { - bindEventDelegate(self, name); - }); - }, - - /** - * Toggles a native event on/off this is called by the EventDispatcher when - * the first native event handler is added and when the last native event handler is removed. - * - * @private - */ - toggleNativeEvent: function (name, state) { - var self = this; - - // Never bind focus/blur since the FocusManager fakes those - if (name == "focus" || name == "blur") { - return; - } - - if (state) { - if (self.initialized) { - bindEventDelegate(self, name); - } else { - if (!self._pendingNativeEvents) { - self._pendingNativeEvents = [name]; - } else { - self._pendingNativeEvents.push(name); - } - } - } else if (self.initialized) { - self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); - delete self.delegates[name]; - } - }, - - /** - * Unbinds all native event handlers that means delegates, custom events bound using the Events API etc. - * - * @private - */ - unbindAllNativeEvents: function () { - var self = this, name; - - if (self.delegates) { - for (name in self.delegates) { - self.dom.unbind(getEventTarget(self, name), name, self.delegates[name]); - } - - delete self.delegates; - } - - if (!self.inline) { - self.getBody().onload = null; - self.dom.unbind(self.getWin()); - self.dom.unbind(self.getDoc()); - } - - self.dom.unbind(self.getBody()); - self.dom.unbind(self.getContainer()); - } - }; - - EditorObservable = Tools.extend({}, Observable, EditorObservable); - - return EditorObservable; - } -); - -/** - * ErrorReporter.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 - */ - -/** - * Various error reporting helper functions. - * - * @class tinymce.ErrorReporter - * @private - */ -define( - 'tinymce.core.ErrorReporter', - [ - 'tinymce.core.AddOnManager' - ], - function (AddOnManager) { - var PluginManager = AddOnManager.PluginManager; - - var resolvePluginName = function (targetUrl, suffix) { - for (var name in PluginManager.urls) { - var matchUrl = PluginManager.urls[name] + '/plugin' + suffix + '.js'; - if (matchUrl === targetUrl) { - return name; - } - } - - return null; - }; - - var pluginUrlToMessage = function (editor, url) { - var plugin = resolvePluginName(url, editor.suffix); - return plugin ? - 'Failed to load plugin: ' + plugin + ' from url ' + url : - 'Failed to load plugin url: ' + url; - }; - - var displayNotification = function (editor, message) { - editor.notificationManager.open({ - type: 'error', - text: message - }); - }; - - var displayError = function (editor, message) { - if (editor._skinLoaded) { - displayNotification(editor, message); - } else { - editor.on('SkinLoaded', function () { - displayNotification(editor, message); - }); - } - }; - - var uploadError = function (editor, message) { - displayError(editor, 'Failed to upload image: ' + message); - }; - - var pluginLoadError = function (editor, url) { - displayError(editor, pluginUrlToMessage(editor, url)); - }; - - var initError = function (message) { - var console = window.console; - if (console && !window.test) { // Skip test env - if (console.error) { - console.error.apply(console, arguments); - } else { - console.log.apply(console, arguments); - } - } - }; - - return { - pluginLoadError: pluginLoadError, - uploadError: uploadError, - displayError: displayError, - initError: initError - }; - } -); -/** - * CaretContainerInput.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 shows the invisble block that the caret is currently in when contents is added to that block. - */ -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, Element, SelectorFind, CaretContainer) { - var findBlockCaretContainer = function (editor) { - return SelectorFind.descendant(Element.fromDom(editor.getBody()), '*[data-mce-caret]').fold(Fun.constant(null), function (elm) { - return elm.dom(); - }); - }; - - var removeIeControlRect = function (editor) { - editor.selection.setRng(editor.selection.getRng()); - }; - - var showBlockCaretContainer = function (editor, blockCaretContainer) { - if (blockCaretContainer.hasAttribute('data-mce-caret')) { - CaretContainer.showCaretContainerBlock(blockCaretContainer); - removeIeControlRect(editor); - editor.selection.scrollIntoView(blockCaretContainer); - } - }; - - var handleBlockContainer = function (editor, e) { - var blockCaretContainer = findBlockCaretContainer(editor); - - if (!blockCaretContainer) { - return; - } - - if (e.type === 'compositionstart') { - e.preventDefault(); - e.stopPropagation(); - showBlockCaretContainer(blockCaretContainer); - return; - } - - if (CaretContainer.hasContent(blockCaretContainer)) { - showBlockCaretContainer(editor, blockCaretContainer); - } - }; - - var setup = function (editor) { - editor.on('keyup compositionstart', Fun.curry(handleBlockContainer, editor)); - }; - - return { - setup: setup - }; - } -); -/** - * Uploader.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 - */ - -/** - * Upload blobs or blob infos to the specified URL or handler. - * - * @private - * @class tinymce.file.Uploader - * @example - * var uploader = new Uploader({ - * url: '/upload.php', - * basePath: '/base/path', - * credentials: true, - * handler: function(data, success, failure) { - * ... - * } - * }); - * - * uploader.upload(blobInfos).then(function(result) { - * ... - * }); - */ -define( - 'tinymce.core.file.Uploader', - [ - "tinymce.core.util.Promise", - "tinymce.core.util.Tools", - "tinymce.core.util.Fun" - ], - function (Promise, Tools, Fun) { - return function (uploadStatus, settings) { - var pendingPromises = {}; - - function pathJoin(path1, path2) { - if (path1) { - return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, ''); - } - - return path2; - } - - function defaultHandler(blobInfo, success, failure, progress) { - var xhr, formData; - - xhr = new XMLHttpRequest(); - xhr.open('POST', settings.url); - xhr.withCredentials = settings.credentials; - - xhr.upload.onprogress = function (e) { - progress(e.loaded / e.total * 100); - }; - - xhr.onerror = function () { - failure("Image upload failed due to a XHR Transport error. Code: " + xhr.status); - }; - - xhr.onload = function () { - var json; - - if (xhr.status < 200 || xhr.status >= 300) { - failure("HTTP Error: " + xhr.status); - return; - } - - json = JSON.parse(xhr.responseText); - - if (!json || typeof json.location != "string") { - failure("Invalid JSON: " + xhr.responseText); - return; - } - - success(pathJoin(settings.basePath, json.location)); - }; - - formData = new FormData(); - formData.append('file', blobInfo.blob(), blobInfo.filename()); - - xhr.send(formData); - } - - function noUpload() { - return new Promise(function (resolve) { - resolve([]); - }); - } - - function handlerSuccess(blobInfo, url) { - return { - url: url, - blobInfo: blobInfo, - status: true - }; - } - - function handlerFailure(blobInfo, error) { - return { - url: '', - blobInfo: blobInfo, - status: false, - error: error - }; - } - - function resolvePending(blobUri, result) { - Tools.each(pendingPromises[blobUri], function (resolve) { - resolve(result); - }); - - delete pendingPromises[blobUri]; - } - - function uploadBlobInfo(blobInfo, handler, openNotification) { - uploadStatus.markPending(blobInfo.blobUri()); - - return new Promise(function (resolve) { - var notification, progress; - - var noop = function () { - }; - - try { - var closeNotification = function () { - if (notification) { - notification.close(); - progress = noop; // Once it's closed it's closed - } - }; - - var success = function (url) { - closeNotification(); - uploadStatus.markUploaded(blobInfo.blobUri(), url); - resolvePending(blobInfo.blobUri(), handlerSuccess(blobInfo, url)); - resolve(handlerSuccess(blobInfo, url)); - }; - - var failure = function (error) { - closeNotification(); - uploadStatus.removeFailed(blobInfo.blobUri()); - resolvePending(blobInfo.blobUri(), handlerFailure(blobInfo, error)); - resolve(handlerFailure(blobInfo, error)); - }; - - progress = function (percent) { - if (percent < 0 || percent > 100) { - return; - } - - if (!notification) { - notification = openNotification(); - } - - notification.progressBar.value(percent); - }; - - handler(blobInfo, success, failure, progress); - } catch (ex) { - resolve(handlerFailure(blobInfo, ex.message)); - } - }); - } - - function isDefaultHandler(handler) { - return handler === defaultHandler; - } - - function pendingUploadBlobInfo(blobInfo) { - var blobUri = blobInfo.blobUri(); - - return new Promise(function (resolve) { - pendingPromises[blobUri] = pendingPromises[blobUri] || []; - pendingPromises[blobUri].push(resolve); - }); - } - - function uploadBlobs(blobInfos, openNotification) { - blobInfos = Tools.grep(blobInfos, function (blobInfo) { - return !uploadStatus.isUploaded(blobInfo.blobUri()); - }); - - return Promise.all(Tools.map(blobInfos, function (blobInfo) { - return uploadStatus.isPending(blobInfo.blobUri()) ? - pendingUploadBlobInfo(blobInfo) : uploadBlobInfo(blobInfo, settings.handler, openNotification); - })); - } - - function upload(blobInfos, openNotification) { - return (!settings.url && isDefaultHandler(settings.handler)) ? noUpload() : uploadBlobs(blobInfos, openNotification); - } - - settings = Tools.extend({ - credentials: false, - // We are adding a notify argument to this (at the moment, until it doesn't work) - handler: defaultHandler - }, settings); - - return { - upload: upload - }; - }; - } -); -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 - * - * 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 - */ - -/** - * Converts blob/uris back and forth. - * - * @private - * @class tinymce.file.Conversions - */ -define( - 'tinymce.core.file.Conversions', - [ - 'ephox.sand.api.Window', - 'tinymce.core.util.Promise' - ], - function (Window, Promise) { - function blobUriToBlob(url) { - return new Promise(function (resolve, reject) { - - var rejectWithError = function () { - reject("Cannot convert " + url + " to Blob. Resource might not exist or is inaccessible."); - }; - - try { - var xhr = new XMLHttpRequest(); - - xhr.open('GET', url, true); - xhr.responseType = 'blob'; - - xhr.onload = function () { - if (this.status == 200) { - resolve(this.response); - } else { - // IE11 makes it into onload but responds with status 500 - rejectWithError(); - } - }; - - // Chrome fires an error event instead of the exception - // Also there seems to be no way to intercept the message that is logged to the console - xhr.onerror = rejectWithError; - - xhr.send(); - } catch (ex) { - rejectWithError(); - } - }); - } - - function parseDataUri(uri) { - var type, matches; - - uri = decodeURIComponent(uri).split(','); - - matches = /data:([^;]+)/.exec(uri[0]); - if (matches) { - type = matches[1]; - } - - return { - type: type, - data: uri[1] - }; - } - - function dataUriToBlob(uri) { - return new Promise(function (resolve) { - var str, arr, i; - - uri = parseDataUri(uri); - - // Might throw error if data isn't proper base64 - try { - str = Window.atob(uri.data); - } catch (e) { - resolve(new Blob([])); - return; - } - - arr = new Uint8Array(str.length); - - for (i = 0; i < arr.length; i++) { - arr[i] = str.charCodeAt(i); - } - - resolve(new Blob([arr], { type: uri.type })); - }); - } - - function uriToBlob(url) { - if (url.indexOf('blob:') === 0) { - return blobUriToBlob(url); - } - - if (url.indexOf('data:') === 0) { - return dataUriToBlob(url); - } - - return null; - } - - function blobToDataUri(blob) { - return new Promise(function (resolve) { - var reader = new FileReader(); - - reader.onloadend = function () { - resolve(reader.result); - }; - - reader.readAsDataURL(blob); - }); - } - - return { - uriToBlob: uriToBlob, - blobToDataUri: blobToDataUri, - parseDataUri: parseDataUri - }; - } -); -/** - * ImageScanner.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 - */ - -/** - * Finds images with data uris or blob uris. If data uris are found it will convert them into blob uris. - * - * @private - * @class tinymce.file.ImageScanner - */ -define( - 'tinymce.core.file.ImageScanner', - [ - "tinymce.core.util.Promise", - "tinymce.core.util.Arr", - "tinymce.core.util.Fun", - "tinymce.core.file.Conversions", - "tinymce.core.Env" - ], - function (Promise, Arr, Fun, Conversions, Env) { - var count = 0; - - var uniqueId = function (prefix) { - return (prefix || 'blobid') + (count++); - }; - - var imageToBlobInfo = function (blobCache, img, resolve, reject) { - var base64, blobInfo; - - if (img.src.indexOf('blob:') === 0) { - blobInfo = blobCache.getByUri(img.src); - - if (blobInfo) { - resolve({ - image: img, - blobInfo: blobInfo - }); - } else { - Conversions.uriToBlob(img.src).then(function (blob) { - Conversions.blobToDataUri(blob).then(function (dataUri) { - base64 = Conversions.parseDataUri(dataUri).data; - blobInfo = blobCache.create(uniqueId(), blob, base64); - blobCache.add(blobInfo); - - resolve({ - image: img, - blobInfo: blobInfo - }); - }); - }, function (err) { - reject(err); - }); - } - - return; - } - - base64 = Conversions.parseDataUri(img.src).data; - blobInfo = blobCache.findFirst(function (cachedBlobInfo) { - return cachedBlobInfo.base64() === base64; - }); - - if (blobInfo) { - resolve({ - image: img, - blobInfo: blobInfo - }); - } else { - Conversions.uriToBlob(img.src).then(function (blob) { - blobInfo = blobCache.create(uniqueId(), blob, base64); - blobCache.add(blobInfo); - - resolve({ - image: img, - blobInfo: blobInfo - }); - }, function (err) { - reject(err); - }); - } - }; - - var getAllImages = function (elm) { - return elm ? elm.getElementsByTagName('img') : []; - }; - - return function (uploadStatus, blobCache) { - var cachedPromises = {}; - - function findAll(elm, predicate) { - var images, promises; - - if (!predicate) { - predicate = Fun.constant(true); - } - - images = Arr.filter(getAllImages(elm), function (img) { - var src = img.src; - - if (!Env.fileApi) { - return false; - } - - if (img.hasAttribute('data-mce-bogus')) { - return false; - } - - if (img.hasAttribute('data-mce-placeholder')) { - return false; - } - - if (!src || src == Env.transparentSrc) { - return false; - } - - if (src.indexOf('blob:') === 0) { - return !uploadStatus.isUploaded(src); - } - - if (src.indexOf('data:') === 0) { - return predicate(img); - } - - return false; - }); - - promises = Arr.map(images, function (img) { - var newPromise; - - if (cachedPromises[img.src]) { - // Since the cached promise will return the cached image - // We need to wrap it and resolve with the actual image - return new Promise(function (resolve) { - cachedPromises[img.src].then(function (imageInfo) { - if (typeof imageInfo === 'string') { // error apparently - return imageInfo; - } - resolve({ - image: img, - blobInfo: imageInfo.blobInfo - }); - }); - }); - } - - newPromise = new Promise(function (resolve, reject) { - imageToBlobInfo(blobCache, img, resolve, reject); - }).then(function (result) { - delete cachedPromises[result.image.src]; - return result; - })['catch'](function (error) { - delete cachedPromises[img.src]; - return error; - }); - - cachedPromises[img.src] = newPromise; - - return newPromise; - }); - - return Promise.all(promises); - } - - return { - findAll: findAll - }; - }; - } -); -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 - * - * 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 - */ - -/** - * Generates unique ids. - * - * @class tinymce.util.Uuid - * @private - */ -define( - 'tinymce.core.util.Uuid', - [ - ], - function () { - var count = 0; - - var seed = function () { - var rnd = function () { - return Math.round(Math.random() * 0xFFFFFFFF).toString(36); - }; - - var now = new Date().getTime(); - return 's' + now.toString(36) + rnd() + rnd() + rnd(); - }; - - var uuid = function (prefix) { - return prefix + (count++) + seed(); - }; - - return { - uuid: uuid - }; - } -); - -/** - * BlobCache.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 - */ - -/** - * Hold blob info objects where a blob has extra internal information. - * - * @private - * @class tinymce.file.BlobCache - */ -define( - 'tinymce.core.file.BlobCache', - [ - 'ephox.sand.api.URL', - 'tinymce.core.util.Arr', - 'tinymce.core.util.Fun', - 'tinymce.core.util.Uuid' - ], - function (URL, Arr, Fun, Uuid) { - return function () { - var cache = [], constant = Fun.constant; - - function mimeToExt(mime) { - var mimes = { - 'image/jpeg': 'jpg', - 'image/jpg': 'jpg', - 'image/gif': 'gif', - 'image/png': 'png' - }; - - return mimes[mime.toLowerCase()] || 'dat'; - } - - function create(o, blob, base64, filename) { - return typeof o === 'object' ? toBlobInfo(o) : toBlobInfo({ - id: o, - name: filename, - blob: blob, - base64: base64 - }); - } - - function toBlobInfo(o) { - var id, name; - - if (!o.blob || !o.base64) { - throw "blob and base64 representations of the image are required for BlobInfo to be created"; - } - - id = o.id || Uuid.uuid('blobid'); - name = o.name || id; - - return { - id: constant(id), - name: constant(name), - filename: constant(name + '.' + mimeToExt(o.blob.type)), - blob: constant(o.blob), - base64: constant(o.base64), - blobUri: constant(o.blobUri || URL.createObjectURL(o.blob)), - uri: constant(o.uri) - }; - } - - function add(blobInfo) { - if (!get(blobInfo.id())) { - cache.push(blobInfo); - } - } - - function get(id) { - return findFirst(function (cachedBlobInfo) { - return cachedBlobInfo.id() === id; - }); - } - - function findFirst(predicate) { - return Arr.filter(cache, predicate)[0]; - } - - function getByUri(blobUri) { - return findFirst(function (blobInfo) { - return blobInfo.blobUri() == blobUri; - }); - } - - function removeByUri(blobUri) { - cache = Arr.filter(cache, function (blobInfo) { - if (blobInfo.blobUri() === blobUri) { - URL.revokeObjectURL(blobInfo.blobUri()); - return false; - } - - return true; - }); - } - - function destroy() { - Arr.each(cache, function (cachedBlobInfo) { - URL.revokeObjectURL(cachedBlobInfo.blobUri()); - }); - - cache = []; - } - - return { - create: create, - add: add, - get: get, - getByUri: getByUri, - findFirst: findFirst, - removeByUri: removeByUri, - destroy: destroy - }; - }; - } -); -/** - * UploadStatus.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 - */ - -/** - * Holds the current status of a blob uri, if it's pending or uploaded and what the result urls was. - * - * @private - * @class tinymce.file.UploadStatus - */ -define( - 'tinymce.core.file.UploadStatus', - [ - ], - function () { - return function () { - var PENDING = 1, UPLOADED = 2; - var blobUriStatuses = {}; - - function createStatus(status, resultUri) { - return { - status: status, - resultUri: resultUri - }; - } - - function hasBlobUri(blobUri) { - return blobUri in blobUriStatuses; - } - - function getResultUri(blobUri) { - var result = blobUriStatuses[blobUri]; - - return result ? result.resultUri : null; - } - - function isPending(blobUri) { - return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === PENDING : false; - } - - function isUploaded(blobUri) { - return hasBlobUri(blobUri) ? blobUriStatuses[blobUri].status === UPLOADED : false; - } - - function markPending(blobUri) { - blobUriStatuses[blobUri] = createStatus(PENDING, null); - } - - function markUploaded(blobUri, resultUri) { - blobUriStatuses[blobUri] = createStatus(UPLOADED, resultUri); - } - - function removeFailed(blobUri) { - delete blobUriStatuses[blobUri]; - } - - function destroy() { - blobUriStatuses = {}; - } - - return { - hasBlobUri: hasBlobUri, - getResultUri: getResultUri, - isPending: isPending, - isUploaded: isUploaded, - markPending: markPending, - markUploaded: markUploaded, - removeFailed: removeFailed, - destroy: destroy - }; - }; - } -); -/** - * EditorUpload.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 image uploads, updates undo stack and patches over various internal functions. - * - * @private - * @class tinymce.EditorUpload - */ -define( - 'tinymce.core.EditorUpload', - [ - "tinymce.core.util.Arr", - "tinymce.core.file.Uploader", - "tinymce.core.file.ImageScanner", - "tinymce.core.file.BlobCache", - "tinymce.core.file.UploadStatus", - "tinymce.core.ErrorReporter" - ], - function (Arr, Uploader, ImageScanner, BlobCache, UploadStatus, ErrorReporter) { - return function (editor) { - var blobCache = new BlobCache(), uploader, imageScanner, settings = editor.settings; - var uploadStatus = new UploadStatus(); - - function aliveGuard(callback) { - return function (result) { - if (editor.selection) { - return callback(result); - } - - return []; - }; - } - - function cacheInvalidator() { - return '?' + (new Date()).getTime(); - } - - // Replaces strings without regexps to avoid FF regexp to big issue - function replaceString(content, search, replace) { - var index = 0; - - do { - index = content.indexOf(search, index); - - if (index !== -1) { - content = content.substring(0, index) + replace + content.substr(index + search.length); - index += replace.length - search.length + 1; - } - } while (index !== -1); - - return content; - } - - function replaceImageUrl(content, targetUrl, replacementUrl) { - content = replaceString(content, 'src="' + targetUrl + '"', 'src="' + replacementUrl + '"'); - content = replaceString(content, 'data-mce-src="' + targetUrl + '"', 'data-mce-src="' + replacementUrl + '"'); - - return content; - } - - function replaceUrlInUndoStack(targetUrl, replacementUrl) { - Arr.each(editor.undoManager.data, function (level) { - if (level.type === 'fragmented') { - level.fragments = Arr.map(level.fragments, function (fragment) { - return replaceImageUrl(fragment, targetUrl, replacementUrl); - }); - } else { - level.content = replaceImageUrl(level.content, targetUrl, replacementUrl); - } - }); - } - - function openNotification() { - return editor.notificationManager.open({ - text: editor.translate('Image uploading...'), - type: 'info', - timeout: -1, - progressBar: true - }); - } - - function replaceImageUri(image, resultUri) { - blobCache.removeByUri(image.src); - replaceUrlInUndoStack(image.src, resultUri); - - editor.$(image).attr({ - src: settings.images_reuse_filename ? resultUri + cacheInvalidator() : resultUri, - 'data-mce-src': editor.convertURL(resultUri, 'src') - }); - } - - function uploadImages(callback) { - if (!uploader) { - uploader = new Uploader(uploadStatus, { - url: settings.images_upload_url, - basePath: settings.images_upload_base_path, - credentials: settings.images_upload_credentials, - handler: settings.images_upload_handler - }); - } - - return scanForImages().then(aliveGuard(function (imageInfos) { - var blobInfos; - - blobInfos = Arr.map(imageInfos, function (imageInfo) { - return imageInfo.blobInfo; - }); - - return uploader.upload(blobInfos, openNotification).then(aliveGuard(function (result) { - var filteredResult = Arr.map(result, function (uploadInfo, index) { - var image = imageInfos[index].image; - - if (uploadInfo.status && editor.settings.images_replace_blob_uris !== false) { - replaceImageUri(image, uploadInfo.url); - } else if (uploadInfo.error) { - ErrorReporter.uploadError(editor, uploadInfo.error); - } - - return { - element: image, - status: uploadInfo.status - }; - }); - - if (callback) { - callback(filteredResult); - } - - return filteredResult; - })); - })); - } - - function uploadImagesAuto(callback) { - if (settings.automatic_uploads !== false) { - return uploadImages(callback); - } - } - - function isValidDataUriImage(imgElm) { - return settings.images_dataimg_filter ? settings.images_dataimg_filter(imgElm) : true; - } - - function scanForImages() { - if (!imageScanner) { - imageScanner = new ImageScanner(uploadStatus, blobCache); - } - - return imageScanner.findAll(editor.getBody(), isValidDataUriImage).then(aliveGuard(function (result) { - result = Arr.filter(result, function (resultItem) { - // ImageScanner internally converts images that it finds, but it may fail to do so if image source is inaccessible. - // In such case resultItem will contain appropriate text error message, instead of image data. - if (typeof resultItem === 'string') { - ErrorReporter.displayError(editor, resultItem); - return false; - } - return true; - }); - - Arr.each(result, function (resultItem) { - replaceUrlInUndoStack(resultItem.image.src, resultItem.blobInfo.blobUri()); - resultItem.image.src = resultItem.blobInfo.blobUri(); - resultItem.image.removeAttribute('data-mce-src'); - }); - - return result; - })); - } - - function destroy() { - blobCache.destroy(); - uploadStatus.destroy(); - imageScanner = uploader = null; - } - - function replaceBlobUris(content) { - return content.replace(/src="(blob:[^"]+)"/g, function (match, blobUri) { - var resultUri = uploadStatus.getResultUri(blobUri); - - if (resultUri) { - return 'src="' + resultUri + '"'; - } - - var blobInfo = blobCache.getByUri(blobUri); - - if (!blobInfo) { - blobInfo = Arr.reduce(editor.editorManager.get(), function (result, editor) { - return result || editor.editorUpload && editor.editorUpload.blobCache.getByUri(blobUri); - }, null); - } - - if (blobInfo) { - return 'src="data:' + blobInfo.blob().type + ';base64,' + blobInfo.base64() + '"'; - } - - return match; - }); - } - - editor.on('setContent', function () { - if (editor.settings.automatic_uploads !== false) { - uploadImagesAuto(); - } else { - scanForImages(); - } - }); - - editor.on('RawSaveContent', function (e) { - e.content = replaceBlobUris(e.content); - }); - - editor.on('getContent', function (e) { - if (e.source_view || e.format == 'raw') { - return; - } - - e.content = replaceBlobUris(e.content); - }); - - editor.on('PostRender', function () { - editor.parser.addNodeFilter('img', function (images) { - Arr.each(images, function (img) { - var src = img.attr('src'); - - if (blobCache.getByUri(src)) { - return; - } - - var resultUri = uploadStatus.getResultUri(src); - if (resultUri) { - img.attr('src', resultUri); - } - }); - }); - }); - - return { - blobCache: blobCache, - uploadImages: uploadImages, - uploadImagesAuto: uploadImagesAuto, - scanForImages: scanForImages, - destroy: destroy - }; - }; - } -); -/** - * ForceBlocks.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 - */ - -/** - * Makes sure that everything gets wrapped in paragraphs. - * - * @private - * @class tinymce.ForceBlocks - */ -define( - 'tinymce.core.ForceBlocks', - [ - 'ephox.katamari.api.Fun' - ], - function (Fun) { - var addRootBlocks = function (editor) { - var settings = editor.settings, dom = editor.dom, selection = editor.selection; - var schema = editor.schema, blockElements = schema.getBlockElements(); - var node = selection.getStart(), rootNode = editor.getBody(), rng; - var startContainer, startOffset, endContainer, endOffset, rootBlockNode; - var tempNode, offset = -0xFFFFFF, wrapped, restoreSelection; - var tmpRng, rootNodeName, forcedRootBlock; - - forcedRootBlock = settings.forced_root_block; - - if (!node || node.nodeType !== 1 || !forcedRootBlock) { - return; - } - - // Check if node is wrapped in block - while (node && node !== rootNode) { - if (blockElements[node.nodeName]) { - return; - } - - node = node.parentNode; - } - - // Get current selection - rng = selection.getRng(); - if (rng.setStart) { - startContainer = rng.startContainer; - startOffset = rng.startOffset; - endContainer = rng.endContainer; - endOffset = rng.endOffset; - - try { - restoreSelection = editor.getDoc().activeElement === rootNode; - } catch (ex) { - // IE throws unspecified error here sometimes - } - } else { - // Force control range into text range - if (rng.item) { - node = rng.item(0); - rng = editor.getDoc().body.createTextRange(); - rng.moveToElementText(node); - } - - restoreSelection = rng.parentElement().ownerDocument === editor.getDoc(); - tmpRng = rng.duplicate(); - tmpRng.collapse(true); - startOffset = tmpRng.move('character', offset) * -1; - - if (!tmpRng.collapsed) { - tmpRng = rng.duplicate(); - tmpRng.collapse(false); - endOffset = (tmpRng.move('character', offset) * -1) - startOffset; - } - } - - // Wrap non block elements and text nodes - node = rootNode.firstChild; - rootNodeName = rootNode.nodeName.toLowerCase(); - while (node) { - // TODO: Break this up, too complex - if (((node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName]))) && - schema.isValidChild(rootNodeName, forcedRootBlock.toLowerCase())) { - // Remove empty text nodes - if (node.nodeType === 3 && node.nodeValue.length === 0) { - tempNode = node; - node = node.nextSibling; - dom.remove(tempNode); - continue; - } - - if (!rootBlockNode) { - rootBlockNode = dom.create(forcedRootBlock, editor.settings.forced_root_block_attrs); - node.parentNode.insertBefore(rootBlockNode, node); - wrapped = true; - } - - tempNode = node; - node = node.nextSibling; - rootBlockNode.appendChild(tempNode); - } else { - rootBlockNode = null; - node = node.nextSibling; - } - } - - if (wrapped && restoreSelection) { - if (rng.setStart) { - rng.setStart(startContainer, startOffset); - rng.setEnd(endContainer, endOffset); - selection.setRng(rng); - } else { - // Only select if the previous selection was inside the document to prevent auto focus in quirks mode - try { - rng = editor.getDoc().body.createTextRange(); - rng.moveToElementText(rootNode); - rng.collapse(true); - rng.moveStart('character', startOffset); - - if (endOffset > 0) { - rng.moveEnd('character', endOffset); - } - - rng.select(); - } catch (ex) { - // Ignore - } - } - - editor.nodeChanged(); - } - }; - - var setup = function (editor) { - if (editor.settings.forced_root_block) { - editor.on('NodeChange', Fun.curry(addRootBlocks, editor)); - } - }; - - return { - setup: setup - }; - } -); -/** - * Dimensions.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 measures nodes and returns client rects. The client rects has an - * extra node property. - * - * @private - * @class tinymce.dom.Dimensions - */ -define( - 'tinymce.core.dom.Dimensions', - [ - "tinymce.core.util.Arr", - "tinymce.core.dom.NodeType", - "tinymce.core.geom.ClientRect" - ], - function (Arr, NodeType, ClientRect) { - - function getClientRects(node) { - function toArrayWithNode(clientRects) { - return Arr.map(clientRects, function (clientRect) { - clientRect = ClientRect.clone(clientRect); - clientRect.node = node; - - return clientRect; - }); - } - - if (Arr.isArray(node)) { - return Arr.reduce(node, function (result, node) { - return result.concat(getClientRects(node)); - }, []); - } - - if (NodeType.isElement(node)) { - return toArrayWithNode(node.getClientRects()); - } - - if (NodeType.isText(node)) { - var rng = node.ownerDocument.createRange(); - - rng.setStart(node, 0); - rng.setEnd(node, node.data.length); - - return toArrayWithNode(rng.getClientRects()); - } - } - - return { - /** - * Returns the client rects for a specific node. - * - * @method getClientRects - * @param {Array/DOMNode} node Node or array of nodes to get client rects on. - * @param {Array} Array of client rects with a extra node property. - */ - getClientRects: getClientRects - }; - } -); -/** - * LineUtils.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 for working with lines. - * - * @private - * @class tinymce.caret.LineUtils - */ -define( - 'tinymce.core.caret.LineUtils', - [ - "tinymce.core.util.Fun", - "tinymce.core.util.Arr", - "tinymce.core.dom.NodeType", - "tinymce.core.dom.Dimensions", - "tinymce.core.geom.ClientRect", - "tinymce.core.caret.CaretUtils", - "tinymce.core.caret.CaretCandidate" - ], - function (Fun, Arr, NodeType, Dimensions, ClientRect, CaretUtils, CaretCandidate) { - var isContentEditableFalse = NodeType.isContentEditableFalse, - findNode = CaretUtils.findNode, - curry = Fun.curry; - - function distanceToRectLeft(clientRect, clientX) { - return Math.abs(clientRect.left - clientX); - } - - function distanceToRectRight(clientRect, clientX) { - return Math.abs(clientRect.right - clientX); - } - - function findClosestClientRect(clientRects, clientX) { - function isInside(clientX, clientRect) { - return clientX >= clientRect.left && clientX <= clientRect.right; - } - - return Arr.reduce(clientRects, function (oldClientRect, clientRect) { - var oldDistance, newDistance; - - oldDistance = Math.min(distanceToRectLeft(oldClientRect, clientX), distanceToRectRight(oldClientRect, clientX)); - newDistance = Math.min(distanceToRectLeft(clientRect, clientX), distanceToRectRight(clientRect, clientX)); - - if (isInside(clientX, clientRect)) { - return clientRect; - } - - if (isInside(clientX, oldClientRect)) { - return oldClientRect; - } - - // cE=false has higher priority - if (newDistance == oldDistance && isContentEditableFalse(clientRect.node)) { - return clientRect; - } - - if (newDistance < oldDistance) { - return clientRect; - } - - return oldClientRect; - }); - } - - function walkUntil(direction, rootNode, predicateFn, node) { - while ((node = findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) { - if (predicateFn(node)) { - return; - } - } - } - - function findLineNodeRects(rootNode, targetNodeRect) { - var clientRects = []; - - function collect(checkPosFn, node) { - var lineRects; - - lineRects = Arr.filter(Dimensions.getClientRects(node), function (clientRect) { - return !checkPosFn(clientRect, targetNodeRect); - }); - - clientRects = clientRects.concat(lineRects); - - return lineRects.length === 0; - } - - clientRects.push(targetNodeRect); - walkUntil(-1, rootNode, curry(collect, ClientRect.isAbove), targetNodeRect.node); - walkUntil(1, rootNode, curry(collect, ClientRect.isBelow), targetNodeRect.node); - - return clientRects; - } - - function getContentEditableFalseChildren(rootNode) { - return Arr.filter(Arr.toArray(rootNode.getElementsByTagName('*')), isContentEditableFalse); - } - - function caretInfo(clientRect, clientX) { - return { - node: clientRect.node, - before: distanceToRectLeft(clientRect, clientX) < distanceToRectRight(clientRect, clientX) - }; - } - - function closestCaret(rootNode, clientX, clientY) { - var contentEditableFalseNodeRects, closestNodeRect; - - contentEditableFalseNodeRects = Dimensions.getClientRects(getContentEditableFalseChildren(rootNode)); - contentEditableFalseNodeRects = Arr.filter(contentEditableFalseNodeRects, function (clientRect) { - return clientY >= clientRect.top && clientY <= clientRect.bottom; - }); - - closestNodeRect = findClosestClientRect(contentEditableFalseNodeRects, clientX); - if (closestNodeRect) { - closestNodeRect = findClosestClientRect(findLineNodeRects(rootNode, closestNodeRect), clientX); - if (closestNodeRect && isContentEditableFalse(closestNodeRect.node)) { - return caretInfo(closestNodeRect, clientX); - } - } - - return null; - } - - return { - findClosestClientRect: findClosestClientRect, - findLineNodeRects: findLineNodeRects, - closestCaret: closestCaret - }; - } -); -/** - * LineWalker.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 lets you walk the document line by line - * returing nodes and client rects for each line. - * - * @private - * @class tinymce.caret.LineWalker - */ -define( - 'tinymce.core.caret.LineWalker', - [ - "tinymce.core.util.Fun", - "tinymce.core.util.Arr", - "tinymce.core.dom.Dimensions", - "tinymce.core.caret.CaretCandidate", - "tinymce.core.caret.CaretUtils", - "tinymce.core.caret.CaretWalker", - "tinymce.core.caret.CaretPosition", - "tinymce.core.geom.ClientRect" - ], - function (Fun, Arr, Dimensions, CaretCandidate, CaretUtils, CaretWalker, CaretPosition, ClientRect) { - var curry = Fun.curry; - - function findUntil(direction, rootNode, predicateFn, node) { - while ((node = CaretUtils.findNode(node, direction, CaretCandidate.isEditableCaretCandidate, rootNode))) { - if (predicateFn(node)) { - return; - } - } - } - - function walkUntil(direction, isAboveFn, isBeflowFn, rootNode, predicateFn, caretPosition) { - var line = 0, node, result = [], targetClientRect; - - function add(node) { - var i, clientRect, clientRects; - - clientRects = Dimensions.getClientRects(node); - if (direction == -1) { - clientRects = clientRects.reverse(); - } - - for (i = 0; i < clientRects.length; i++) { - clientRect = clientRects[i]; - if (isBeflowFn(clientRect, targetClientRect)) { - continue; - } - - if (result.length > 0 && isAboveFn(clientRect, Arr.last(result))) { - line++; - } - - clientRect.line = line; - - if (predicateFn(clientRect)) { - return true; - } - - result.push(clientRect); - } - } - - targetClientRect = Arr.last(caretPosition.getClientRects()); - if (!targetClientRect) { - return result; - } - - node = caretPosition.getNode(); - add(node); - findUntil(direction, rootNode, add, node); - - return result; - } - - function aboveLineNumber(lineNumber, clientRect) { - return clientRect.line > lineNumber; - } - - function isLine(lineNumber, clientRect) { - return clientRect.line === lineNumber; - } - - var upUntil = curry(walkUntil, -1, ClientRect.isAbove, ClientRect.isBelow); - var downUntil = curry(walkUntil, 1, ClientRect.isBelow, ClientRect.isAbove); - - function positionsUntil(direction, rootNode, predicateFn, node) { - var caretWalker = new CaretWalker(rootNode), walkFn, isBelowFn, isAboveFn, - caretPosition, result = [], line = 0, clientRect, targetClientRect; - - function getClientRect(caretPosition) { - if (direction == 1) { - return Arr.last(caretPosition.getClientRects()); - } - - return Arr.last(caretPosition.getClientRects()); - } - - if (direction == 1) { - walkFn = caretWalker.next; - isBelowFn = ClientRect.isBelow; - isAboveFn = ClientRect.isAbove; - caretPosition = CaretPosition.after(node); - } else { - walkFn = caretWalker.prev; - isBelowFn = ClientRect.isAbove; - isAboveFn = ClientRect.isBelow; - caretPosition = CaretPosition.before(node); - } - - targetClientRect = getClientRect(caretPosition); - - do { - if (!caretPosition.isVisible()) { - continue; - } - - clientRect = getClientRect(caretPosition); - - if (isAboveFn(clientRect, targetClientRect)) { - continue; - } - - if (result.length > 0 && isBelowFn(clientRect, Arr.last(result))) { - line++; - } - - clientRect = ClientRect.clone(clientRect); - clientRect.position = caretPosition; - clientRect.line = line; - - if (predicateFn(clientRect)) { - return result; - } - - result.push(clientRect); - } while ((caretPosition = walkFn(caretPosition))); - - return result; - } - - return { - upUntil: upUntil, - downUntil: downUntil, - - /** - * Find client rects with line and caret position until the predicate returns true. - * - * @method positionsUntil - * @param {Number} direction Direction forward/backward 1/-1. - * @param {DOMNode} rootNode Root node to walk within. - * @param {function} predicateFn Gets the client rect as it's input. - * @param {DOMNode} node Node to start walking from. - * @return {Array} Array of client rects with line and position properties. - */ - positionsUntil: positionsUntil, - - isAboveLine: curry(aboveLineNumber), - isLine: curry(isLine) - }; - } -); -/** - * CefUtils.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.CefUtils', - [ - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.caret.CaretUtils', - 'tinymce.core.dom.NodeType', - 'tinymce.core.util.Fun' - ], - function (CaretPosition, CaretUtils, NodeType, Fun) { - var isContentEditableTrue = NodeType.isContentEditableTrue; - var isContentEditableFalse = NodeType.isContentEditableFalse; - - var showCaret = function (direction, editor, node, before) { - // TODO: Figure out a better way to handle this dependency - return editor._selectionOverrides.showCaret(direction, node, before); - }; - - var getNodeRange = function (node) { - var rng = node.ownerDocument.createRange(); - rng.selectNode(node); - return rng; - }; - - var selectNode = function (editor, node) { - var e; - - e = editor.fire('BeforeObjectSelected', { target: node }); - if (e.isDefaultPrevented()) { - return null; - } - - return getNodeRange(node); - }; - - var renderCaretAtRange = function (editor, range) { - var caretPosition, ceRoot; - - range = CaretUtils.normalizeRange(1, editor.getBody(), range); - caretPosition = CaretPosition.fromRangeStart(range); - - if (isContentEditableFalse(caretPosition.getNode())) { - return showCaret(1, editor, caretPosition.getNode(), !caretPosition.isAtEnd()); - } - - if (isContentEditableFalse(caretPosition.getNode(true))) { - return showCaret(1, editor, caretPosition.getNode(true), false); - } - - // TODO: Should render caret before/after depending on where you click on the page forces after now - ceRoot = editor.dom.getParent(caretPosition.getNode(), Fun.or(isContentEditableFalse, isContentEditableTrue)); - if (isContentEditableFalse(ceRoot)) { - return showCaret(1, editor, ceRoot, false); - } - - return null; - }; - - var renderRangeCaret = function (editor, range) { - var caretRange; - - if (!range || !range.collapsed) { - return range; - } - - caretRange = renderCaretAtRange(editor, range); - if (caretRange) { - return caretRange; - } - - return range; - }; - - return { - showCaret: showCaret, - selectNode: selectNode, - renderCaretAtRange: renderCaretAtRange, - renderRangeCaret: renderRangeCaret - }; - } -); - -/** - * CefNavigation.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.CefNavigation', - [ - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.caret.CaretUtils', - 'tinymce.core.caret.CaretWalker', - 'tinymce.core.caret.LineUtils', - 'tinymce.core.caret.LineWalker', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangeUtils', - 'tinymce.core.Env', - 'tinymce.core.keyboard.CefUtils', - 'tinymce.core.util.Arr', - 'tinymce.core.util.Fun' - ], - function (CaretContainer, CaretPosition, CaretUtils, CaretWalker, LineUtils, LineWalker, NodeType, RangeUtils, Env, CefUtils, Arr, Fun) { - var isContentEditableFalse = NodeType.isContentEditableFalse; - var getSelectedNode = RangeUtils.getSelectedNode; - var isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse; - var isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; - - var getVisualCaretPosition = function (walkFn, caretPosition) { - while ((caretPosition = walkFn(caretPosition))) { - if (caretPosition.isVisible()) { - return caretPosition; - } - } - - return caretPosition; - }; - - var isMoveInsideSameBlock = function (fromCaretPosition, toCaretPosition) { - var inSameBlock = CaretUtils.isInSameBlock(fromCaretPosition, toCaretPosition); - - // Handle bogus BR

    abc|

    - if (!inSameBlock && NodeType.isBr(fromCaretPosition.getNode())) { - return true; - } - - return inSameBlock; - }; - - var isRangeInCaretContainerBlock = function (range) { - return CaretContainer.isCaretContainerBlock(range.startContainer); - }; - - var getNormalizedRangeEndPoint = function (direction, rootNode, range) { - range = CaretUtils.normalizeRange(direction, rootNode, range); - - if (direction === -1) { - return CaretPosition.fromRangeStart(range); - } - - return CaretPosition.fromRangeEnd(range); - }; - - var moveToCeFalseHorizontally = function (direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range) { - var node, caretPosition, peekCaretPosition, rangeIsInContainerBlock; - - if (!range.collapsed) { - node = getSelectedNode(range); - if (isContentEditableFalse(node)) { - return CefUtils.showCaret(direction, editor, node, direction === -1); - } - } - - rangeIsInContainerBlock = isRangeInCaretContainerBlock(range); - caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); - - if (isBeforeContentEditableFalseFn(caretPosition)) { - return CefUtils.selectNode(editor, caretPosition.getNode(direction === -1)); - } - - caretPosition = getNextPosFn(caretPosition); - if (!caretPosition) { - if (rangeIsInContainerBlock) { - return range; - } - - return null; - } - - if (isBeforeContentEditableFalseFn(caretPosition)) { - return CefUtils.showCaret(direction, editor, caretPosition.getNode(direction === -1), direction === 1); - } - - // Peek ahead for handling of ab|c -> abc| - peekCaretPosition = getNextPosFn(caretPosition); - if (isBeforeContentEditableFalseFn(peekCaretPosition)) { - if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) { - return CefUtils.showCaret(direction, editor, peekCaretPosition.getNode(direction === -1), direction === 1); - } - } - - if (rangeIsInContainerBlock) { - return CefUtils.renderRangeCaret(editor, caretPosition.toRange()); - } - - return null; - }; - - var moveToCeFalseVertically = function (direction, editor, walkerFn, range) { - var caretPosition, linePositions, nextLinePositions, - closestNextLineRect, caretClientRect, clientX, - dist1, dist2, contentEditableFalseNode; - - contentEditableFalseNode = getSelectedNode(range); - caretPosition = getNormalizedRangeEndPoint(direction, editor.getBody(), range); - linePositions = walkerFn(editor.getBody(), LineWalker.isAboveLine(1), caretPosition); - nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1)); - caretClientRect = Arr.last(caretPosition.getClientRects()); - - if (isBeforeContentEditableFalse(caretPosition)) { - contentEditableFalseNode = caretPosition.getNode(); - } - - if (isAfterContentEditableFalse(caretPosition)) { - contentEditableFalseNode = caretPosition.getNode(true); - } - - if (!caretClientRect) { - return null; - } - - clientX = caretClientRect.left; - - closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX); - if (closestNextLineRect) { - if (isContentEditableFalse(closestNextLineRect.node)) { - dist1 = Math.abs(clientX - closestNextLineRect.left); - dist2 = Math.abs(clientX - closestNextLineRect.right); - - return CefUtils.showCaret(direction, editor, closestNextLineRect.node, dist1 < dist2); - } - } - - if (contentEditableFalseNode) { - var caretPositions = LineWalker.positionsUntil(direction, editor.getBody(), LineWalker.isAboveLine(1), contentEditableFalseNode); - - closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX); - if (closestNextLineRect) { - return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); - } - - closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0))); - if (closestNextLineRect) { - return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange()); - } - } - }; - - var createTextBlock = function (editor) { - var textBlock = editor.dom.create(editor.settings.forced_root_block); - - if (!Env.ie || Env.ie >= 11) { - textBlock.innerHTML = '
    '; - } - - return textBlock; - }; - - var exitPreBlock = function (editor, direction, range) { - var pre, caretPos, newBlock; - var caretWalker = new CaretWalker(editor.getBody()); - var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); - var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); - - if (range.collapsed && editor.settings.forced_root_block) { - pre = editor.dom.getParent(range.startContainer, 'PRE'); - if (!pre) { - return; - } - - if (direction === 1) { - caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range)); - } else { - caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range)); - } - - if (!caretPos) { - newBlock = createTextBlock(editor); - - if (direction === 1) { - editor.$(pre).after(newBlock); - } else { - editor.$(pre).before(newBlock); - } - - editor.selection.select(newBlock, true); - editor.selection.collapse(); - } - } - }; - - var getHorizontalRange = function (editor, forward) { - var caretWalker = new CaretWalker(editor.getBody()); - var getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next); - var getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev); - var newRange, direction = forward ? 1 : -1; - var getNextPosFn = forward ? getNextVisualCaretPosition : getPrevVisualCaretPosition; - var isBeforeContentEditableFalseFn = forward ? isBeforeContentEditableFalse : isAfterContentEditableFalse; - var range = editor.selection.getRng(); - - newRange = moveToCeFalseHorizontally(direction, editor, getNextPosFn, isBeforeContentEditableFalseFn, range); - if (newRange) { - return newRange; - } - - newRange = exitPreBlock(editor, direction, range); - if (newRange) { - return newRange; - } - - return null; - }; - - var getVerticalRange = function (editor, down) { - var newRange, direction = down ? 1 : -1; - var walkerFn = down ? LineWalker.downUntil : LineWalker.upUntil; - var range = editor.selection.getRng(); - - newRange = moveToCeFalseVertically(direction, editor, walkerFn, range); - if (newRange) { - return newRange; - } - - newRange = exitPreBlock(editor, direction, range); - if (newRange) { - return newRange; - } - - return null; - }; - - var moveH = function (editor, forward) { - return function () { - var newRng = getHorizontalRange(editor, forward); - - if (newRng) { - editor.selection.setRng(newRng); - return true; - } else { - return false; - } - }; - }; - - var moveV = function (editor, down) { - return function () { - var newRng = getVerticalRange(editor, down); - - if (newRng) { - editor.selection.setRng(newRng); - return true; - } else { - return false; - } - }; - }; - - return { - moveH: moveH, - moveV: moveV - }; - } -); - -define( - 'ephox.katamari.api.Merger', - - [ - 'ephox.katamari.api.Type', - 'global!Array', - 'global!Error' - ], - - function (Type, Array, Error) { - - var shallow = function (old, nu) { - return nu; - }; - - var deep = function (old, nu) { - var bothObjects = Type.isObject(old) && Type.isObject(nu); - return bothObjects ? deepMerge(old, nu) : nu; - }; - - var baseMerge = function (merger) { - return function() { - // Don't use array slice(arguments), makes the whole function unoptimisable on Chrome - var objects = new Array(arguments.length); - for (var i = 0; i < objects.length; i++) objects[i] = arguments[i]; - - if (objects.length === 0) throw new Error('Can\'t merge zero objects'); - - var ret = {}; - for (var j = 0; j < objects.length; j++) { - var curObject = objects[j]; - for (var key in curObject) if (curObject.hasOwnProperty(key)) { - ret[key] = merger(ret[key], curObject[key]); - } - } - return ret; - }; - }; - - var deepMerge = baseMerge(deep); - var merge = baseMerge(shallow); - - return { - deepMerge: deepMerge, - merge: merge - }; - } -); -/** - * MatchKeys.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.MatchKeys', - [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Merger' - ], - function (Arr, Fun, Merger) { - var defaultPatterns = function (patterns) { - return Arr.map(patterns, function (pattern) { - return Merger.merge({ - shiftKey: false, - altKey: false, - ctrlKey: false, - metaKey: false, - keyCode: 0, - action: Fun.noop - }, pattern); - }); - }; - - var matchesEvent = function (pattern, evt) { - return ( - evt.keyCode === pattern.keyCode && - evt.shiftKey === pattern.shiftKey && - evt.altKey === pattern.altKey && - evt.ctrlKey === pattern.ctrlKey && - evt.metaKey === pattern.metaKey - ); - }; - - var match = function (patterns, evt) { - return Arr.bind(defaultPatterns(patterns), function (pattern) { - return matchesEvent(pattern, evt) ? [pattern] : [ ]; - }); - }; - - var action = function (f) { - var args = Array.prototype.slice.call(arguments, 1); - return function () { - return f.apply(null, args); - }; - }; - - var execute = function (patterns, evt) { - return Arr.find(match(patterns, evt), function (pattern) { - return pattern.action(); - }); - }; - - return { - match: match, - action: action, - execute: execute - }; - } -); -/** - * ArrowKeys.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.ArrowKeys', - [ - 'tinymce.core.keyboard.BoundarySelection', - 'tinymce.core.keyboard.CefNavigation', - 'tinymce.core.keyboard.MatchKeys', - 'tinymce.core.util.VK' - ], - function (BoundarySelection, CefNavigation, MatchKeys, VK) { - var executeKeydownOverride = function (editor, caret, evt) { - 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).each(function (_) { - evt.preventDefault(); - }); - }; - - var setup = function (editor, caret) { - editor.on('keydown', function (evt) { - if (evt.isDefaultPrevented() === false) { - executeKeydownOverride(editor, caret, evt); - } - }); - }; - - return { - setup: setup - }; - } -); - -/** - * DeleteBackspaceKeys.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.DeleteBackspaceKeys', - [ - 'tinymce.core.delete.BlockBoundaryDelete', - 'tinymce.core.delete.BlockRangeDelete', - 'tinymce.core.delete.CefDelete', - 'tinymce.core.delete.InlineBoundaryDelete', - 'tinymce.core.keyboard.MatchKeys', - 'tinymce.core.util.VK' - ], - function (BlockBoundaryDelete, BlockRangeDelete, CefDelete, InlineBoundaryDelete, MatchKeys, VK) { - var executeKeydownOverride = function (editor, caret, evt) { - 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) }, - { keyCode: VK.DELETE, action: MatchKeys.action(InlineBoundaryDelete.backspaceDelete, editor, caret, true) }, - { keyCode: VK.BACKSPACE, action: MatchKeys.action(BlockRangeDelete.backspaceDelete, editor, false) }, - { 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).each(function (_) { - evt.preventDefault(); - }); - }; - - var executeKeyupOverride = function (editor, evt) { - MatchKeys.execute([ - { keyCode: VK.BACKSPACE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) }, - { keyCode: VK.DELETE, action: MatchKeys.action(CefDelete.paddEmptyElement, editor) } - ], evt); - }; - - var setup = function (editor, caret) { - editor.on('keydown', function (evt) { - if (evt.isDefaultPrevented() === false) { - executeKeydownOverride(editor, caret, evt); - } - }); - - editor.on('keyup', function (evt) { - if (evt.isDefaultPrevented() === false) { - executeKeyupOverride(editor, evt); - } - }); - }; - - return { - setup: setup - }; - } -); - -/** - * InsertNewLine.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.InsertNewLine', - [ - 'tinymce.core.caret.CaretContainer', - 'tinymce.core.dom.NodeType', - 'tinymce.core.dom.RangeUtils', - 'tinymce.core.dom.TreeWalker', - 'tinymce.core.text.Zwsp', - 'tinymce.core.util.Tools' - ], - 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; - }; - - var isTableCell = function (node) { - 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) { - elm.innerHTML = '
    '; - }; - - var containerAndSiblingName = function (container, nodeName) { - return container.nodeName === nodeName || (container.previousSibling && container.previousSibling.nodeName === nodeName); - }; - - var isListBlock = function (elm) { - return elm && /^(OL|UL|LI)$/.test(elm.nodeName); - }; - - var isNestedList = function (elm) { - return isListBlock(elm) && isListBlock(elm.parentNode); - }; - - // Returns true if the block can be split into two blocks or not - var canSplitBlock = function (dom, node) { - return node && - dom.isBlock(node) && - !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && - !/^(fixed|absolute)/i.test(node.style.position) && - dom.getContentEditable(node) !== "true"; - }; - - // Remove the first empty inline element of the block so this:

    x

    becomes this:

    x

    - var trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) { - var node = block, firstChilds = [], i; - - if (!node) { - return; - } - - // Find inner most first child ex:

    *

    - while ((node = node.firstChild)) { - if (dom.isBlock(node)) { - return; - } - - if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { - firstChilds.push(node); - } - } - - i = firstChilds.length; - while (i--) { - node = firstChilds[i]; - if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { - dom.remove(node); - } else { - if (isEmptyAnchor(node)) { - dom.remove(node); - } - } - } - }; - - var normalizeZwspOffset = function (start, container, offset) { - if (NodeType.isText(container) === false) { - return offset; - } if (start) { - return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset; - } else { - return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset; - } - }; - - var includeZwspInRange = function (rng) { - var newRng = rng.cloneRange(); - newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); - newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); - return newRng; - }; - - var firstNonWhiteSpaceNodeSibling = function (node) { - while (node) { - if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) { - return node; - } - - node = node.nextSibling; - } - }; - - // 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 schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); - var rng = editor.selection.getRng(); - - // 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(); - - if (!root) { - return; - } - - if (/^(LI|DT|DD)$/.test(root.nodeName)) { - var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); - - if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) { - root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); - } - } - - rng = dom.createRng(); - root.normalize(); - - if (root.hasChildNodes()) { - walker = new TreeWalker(root, root); - - while ((node = walker.current())) { - if (node.nodeType == 3) { - rng.setStart(node, 0); - rng.setEnd(node, 0); - break; - } - - if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) { - rng.setStartBefore(node); - rng.setEndBefore(node); - break; - } - - lastNode = node; - node = walker.next(); - } - - if (!node) { - rng.setStart(lastNode, 0); - rng.setEnd(lastNode, 0); - } - } else { - if (root.nodeName == 'BR') { - if (root.nextSibling && dom.isBlock(root.nextSibling)) { - rng.setStartBefore(root); - rng.setEndBefore(root); - } else { - rng.setStartAfter(root); - rng.setEndAfter(root); - } - } else { - rng.setStart(root, 0); - rng.setEnd(root, 0); - } - } - - selection.setRng(rng); - - // Remove tempElm created for old IE:s - dom.remove(tempElm); - selection.scrollIntoView(root); - } - - // Creates a new block element by cloning the current one or creating a new one if the name is specified - // This function will also copy any text formatting from the parent block and add it to the new one - function createNewBlock(name) { - var node = container, block, clonedNode, caretNode, textInlineElements = schema.getTextInlineElements(); - - if (name || parentBlockName == "TABLE" || parentBlockName == "HR") { - block = dom.create(name || newBlockName); - setForcedBlockAttrs(editor, block); - } else { - block = parentBlock.cloneNode(false); - } - - caretNode = block; - - if (settings.keep_styles === false) { - dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block - dom.setAttrib(block, 'class', null); - } else { - // Clone any parent styles - do { - if (textInlineElements[node.nodeName]) { - // Never clone a caret containers - if (node.id == '_mce_caret') { - continue; - } - - clonedNode = node.cloneNode(false); - dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique - - if (block.hasChildNodes()) { - clonedNode.appendChild(block.firstChild); - block.appendChild(clonedNode); - } else { - caretNode = clonedNode; - block.appendChild(clonedNode); - } - } - } while ((node = node.parentNode) && node != editableRoot); - } - - emptyBlock(caretNode); - - return block; - } - - // Returns true/false if the caret is at the start/end of the parent block element - function isCaretAtStartOrEndOfBlock(start) { - var walker, node, name, normalizedOffset; - - normalizedOffset = normalizeZwspOffset(start, container, offset); - - // Caret is in the middle of a text node like "a|b" - if (container.nodeType == 3 && (start ? normalizedOffset > 0 : normalizedOffset < container.nodeValue.length)) { - return false; - } - - // If after the last element in block node edge case for #5091 - if (container.parentNode == parentBlock && isAfterLastNodeInContainer && !start) { - return true; - } - - // If the caret if before the first element in parentBlock - if (start && container.nodeType == 1 && container == parentBlock.firstChild) { - return true; - } - - // Caret can be before/after a table or a hr - if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) { - return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); - } - - // Walk the DOM and look for text nodes or non empty elements - walker = new TreeWalker(container, parentBlock); - - // If caret is in beginning or end of a text block then jump to the next/previous node - if (container.nodeType == 3) { - if (start && normalizedOffset === 0) { - walker.prev(); - } else if (!start && normalizedOffset == container.nodeValue.length) { - walker.next(); - } - } - - while ((node = walker.current())) { - if (node.nodeType === 1) { - // Ignore bogus elements - if (!node.getAttribute('data-mce-bogus')) { - // Keep empty elements like but not trailing br:s like

    text|

    - name = node.nodeName.toLowerCase(); - if (nonEmptyElementsMap[name] && name !== 'br') { - return false; - } - } - } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { - return false; - } - - if (start) { - walker.prev(); - } else { - walker.next(); - } - } - - return true; - } - - // Inserts a block or br before/after or in the middle of a split list of the LI is empty - function handleEmptyListItem() { - if (containerBlock == editor.getBody()) { - return; - } - - if (isNestedList(containerBlock)) { - newBlockName = 'LI'; - } - - newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); - - if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) { - if (hasParent(containerBlock, 'LI')) { - // Nested list is inside a LI - dom.insertAfter(newBlock, getContainerBlock(containerBlock)); - } else { - // Is first and last list item then replace the OL/UL with a text block - dom.replace(newBlock, containerBlock); - } - } else if (isFirstOrLastLi(containerBlock, parentBlock, true)) { - if (hasParent(containerBlock, 'LI')) { - // List nested in an LI then move the list to a new sibling LI - dom.insertAfter(newBlock, getContainerBlock(containerBlock)); - newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed - newBlock.appendChild(containerBlock); - } else { - // First LI in list then remove LI and add text block before list - containerBlock.parentNode.insertBefore(newBlock, containerBlock); - } - } else if (isFirstOrLastLi(containerBlock, parentBlock, false)) { - // Last LI in list then remove LI and add text block after list - dom.insertAfter(newBlock, getContainerBlock(containerBlock)); - } else { - // Middle LI in list the split the list and insert a text block in the middle - // Extract after fragment and insert it after the current block - containerBlock = getContainerBlock(containerBlock); - tmpRng = rng.cloneRange(); - tmpRng.setStartAfter(parentBlock); - tmpRng.setEndAfter(containerBlock); - fragment = tmpRng.extractContents(); - - if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { - newBlock = fragment.firstChild; - dom.insertAfter(fragment, containerBlock); - } else { - dom.insertAfter(fragment, containerBlock); - dom.insertAfter(newBlock, containerBlock); - } - } - - dom.remove(parentBlock); - moveToCaretPosition(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 this text|text2 will become this

    text|text2

    - // This won't happen if root blocks are disabled or the shiftKey is pressed - if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { - container = wrapSelfAndSiblingsInDefaultBlock(editor, newBlockName, rng, container, offset); - } - - // Find parent block and setup empty block paddings - parentBlock = dom.getParent(container, dom.isBlock); - containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; - - // Setup block names - parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - - // Enter inside block contained within a LI then split or insert before/after LI - if (containerBlockName == 'LI' && !evt.ctrlKey) { - parentBlock = containerBlock; - containerBlock = containerBlock.parentNode; - parentBlockName = containerBlockName; - } - - // Handle enter in list item - if (/^(LI|DT|DD)$/.test(parentBlockName)) { - if (!newBlockName && shiftKey) { - insertBr(editor, evt); - return; - } - - // Handle enter inside an empty list item - if (dom.isEmpty(parentBlock)) { - handleEmptyListItem(); - return; - } - } - - // Don't split PRE tags but insert a BR instead easier when writing code samples etc - if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { - if (!shiftKey) { - insertBr(editor, evt); - return; - } - } else { - // If no root block is configured then insert a BR by default or if the shiftKey is pressed - if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { - insertBr(editor, evt); - return; - } - } - - // If parent block is root then never insert new blocks - if (newBlockName && parentBlock === editor.getBody()) { - return; - } - - // Default block name if it's not configured - newBlockName = newBlockName || 'P'; - - // Insert new block before/after the parent block depending on caret location - if (CaretContainer.isCaretContainerBlock(parentBlock)) { - newBlock = CaretContainer.showCaretContainerBlock(parentBlock); - if (dom.isEmpty(parentBlock)) { - emptyBlock(parentBlock); - } - moveToCaretPosition(newBlock); - } else if (isCaretAtStartOrEndOfBlock()) { - insertNewBlockAfter(); - } else if (isCaretAtStartOrEndOfBlock(true)) { - // Insert new block before - newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); - - // Adjust caret position if HR - containerAndSiblingName(parentBlock, 'HR') ? moveToCaretPosition(newBlock) : moveToCaretPosition(parentBlock); - } else { - // Extract after fragment and insert it after the current block - tmpRng = includeZwspInRange(rng).cloneRange(); - tmpRng.setEndAfter(parentBlock); - fragment = tmpRng.extractContents(); - trimLeadingLineBreaks(fragment); - newBlock = fragment.firstChild; - dom.insertAfter(fragment, parentBlock); - trimInlineElementsOnLeftSideOfBlock(dom, nonEmptyElementsMap, newBlock); - addBrToBlockIfNeeded(dom, parentBlock); - - if (dom.isEmpty(parentBlock)) { - emptyBlock(parentBlock); - } - - newBlock.normalize(); - - // New block might become empty if it's

    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(); - } - }; - - 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); - } - }); - }; - - return { - setup: setup - }; - } -); - -/** - * InsertSpace.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.InsertSpace', - [ - 'ephox.katamari.api.Fun', - 'tinymce.core.caret.CaretPosition', - 'tinymce.core.dom.NodeType', - 'tinymce.core.keyboard.BoundaryLocation', - 'tinymce.core.keyboard.InlineUtils' - ], - function (Fun, CaretPosition, NodeType, BoundaryLocation, InlineUtils) { - var isValidInsertPoint = function (location, caretPosition) { - return isAtStartOrEnd(location) && NodeType.isText(caretPosition.container()); - }; - - var insertNbspAtPosition = function (editor, caretPosition) { - var container = caretPosition.container(); - var offset = caretPosition.offset(); - - container.insertData(offset, '\u00a0'); - editor.selection.setCursorLocation(container, offset + 1); - }; - - var insertAtLocation = function (editor, caretPosition, location) { - if (isValidInsertPoint(location, caretPosition)) { - insertNbspAtPosition(editor, caretPosition); - return true; - } else { - return false; - } - }; - - var insertAtCaret = function (editor) { - var isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); - var caretPosition = CaretPosition.fromRangeStart(editor.selection.getRng()); - var boundaryLocation = BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), caretPosition); - return boundaryLocation.map(Fun.curry(insertAtLocation, editor, caretPosition)).getOr(false); - }; - - var isAtStartOrEnd = function (location) { - return location.fold( - Fun.constant(false), // Before - Fun.constant(true), // Start - Fun.constant(true), // End - Fun.constant(false) // After - ); - }; - - var insertAtSelection = function (editor) { - return editor.selection.isCollapsed() ? insertAtCaret(editor) : false; - }; - - return { - insertAtSelection: insertAtSelection - }; - } -); - -/** - * SpaceKey.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.SpaceKey', - [ - 'tinymce.core.keyboard.InsertSpace', - 'tinymce.core.keyboard.MatchKeys', - 'tinymce.core.util.VK' - ], - function (InsertSpace, MatchKeys, VK) { - var executeKeydownOverride = function (editor, evt) { - MatchKeys.execute([ - { keyCode: VK.SPACEBAR, action: MatchKeys.action(InsertSpace.insertAtSelection, editor) } - ], evt).each(function (_) { - evt.preventDefault(); - }); - }; - - var setup = function (editor) { - editor.on('keydown', function (evt) { - if (evt.isDefaultPrevented() === false) { - executeKeydownOverride(editor, evt); - } - }); - }; - - return { - setup: setup - }; - } -); - -/** - * KeyboardOverrides.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.KeyboardOverrides', - [ - 'tinymce.core.keyboard.ArrowKeys', - 'tinymce.core.keyboard.BoundarySelection', - 'tinymce.core.keyboard.DeleteBackspaceKeys', - 'tinymce.core.keyboard.EnterKey', - 'tinymce.core.keyboard.SpaceKey' - ], - function (ArrowKeys, BoundarySelection, DeleteBackspaceKeys, EnterKey, SpaceKey) { - var setup = function (editor) { - var caret = BoundarySelection.setupSelectedState(editor); - - ArrowKeys.setup(editor, caret); - DeleteBackspaceKeys.setup(editor, caret); - EnterKey.setup(editor); - SpaceKey.setup(editor); - }; - - return { - setup: setup - }; - } -); -/** - * NodeChange.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 nodechange event dispatching both manual and through selection change events. - * - * @class tinymce.NodeChange - * @private - */ -define( - 'tinymce.core.NodeChange', - [ - "tinymce.core.dom.RangeUtils", - "tinymce.core.Env", - "tinymce.core.util.Delay" - ], - function (RangeUtils, Env, Delay) { - return function (editor) { - var lastRng, lastPath = []; - - /** - * Returns true/false if the current element path has been changed or not. - * - * @private - * @return {Boolean} True if the element path is the same false if it's not. - */ - function isSameElementPath(startElm) { - var i, currentPath; - - currentPath = editor.$(startElm).parentsUntil(editor.getBody()).add(startElm); - if (currentPath.length === lastPath.length) { - for (i = currentPath.length; i >= 0; i--) { - if (currentPath[i] !== lastPath[i]) { - break; - } - } - - if (i === -1) { - lastPath = currentPath; - return true; - } - } - - lastPath = currentPath; - - return false; - } - - // Gecko doesn't support the "selectionchange" event - if (!('onselectionchange' in editor.getDoc())) { - editor.on('NodeChange Click MouseUp KeyUp Focus', function (e) { - var nativeRng, fakeRng; - - // Since DOM Ranges mutate on modification - // of the DOM we need to clone it's contents - nativeRng = editor.selection.getRng(); - fakeRng = { - startContainer: nativeRng.startContainer, - startOffset: nativeRng.startOffset, - endContainer: nativeRng.endContainer, - endOffset: nativeRng.endOffset - }; - - // Always treat nodechange as a selectionchange since applying - // formatting to the current range wouldn't update the range but it's parent - if (e.type == 'nodechange' || !RangeUtils.compareRanges(fakeRng, lastRng)) { - editor.fire('SelectionChange'); - } - - lastRng = fakeRng; - }); - } - - // IE has a bug where it fires a selectionchange on right click that has a range at the start of the body - // When the contextmenu event fires the selection is located at the right location - editor.on('contextmenu', function () { - editor.fire('SelectionChange'); - }); - - // Selection change is delayed ~200ms on IE when you click inside the current range - 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 (!startElm || (!Env.range && editor.selection.isCollapsed())) { - return; - } - - if (!isSameElementPath(startElm) && editor.dom.isChildOf(startElm, editor.getBody())) { - editor.nodeChanged({ selectionChange: true }); - } - }); - - // Fire an extra nodeChange on mouseup for compatibility reasons - editor.on('MouseUp', function (e) { - if (!e.isDefaultPrevented()) { - // Delay nodeChanged call for WebKit edge case issue where the range - // isn't updated until after you click outside a selected image - if (editor.selection.getNode().nodeName == 'IMG') { - Delay.setEditorTimeout(editor, function () { - editor.nodeChanged(); - }); - } else { - editor.nodeChanged(); - } - } - }); - - /** - * Dispatches out a onNodeChange event to all observers. This method should be called when you - * need to update the UI states or element path etc. - * - * @method nodeChanged - * @param {Object} args Optional args to pass to NodeChange event handlers. - */ - this.nodeChanged = function (args) { - var selection = editor.selection, node, parents, root; - - // Fix for bug #1896577 it seems that this can not be fired while the editor is loading - if (editor.initialized && selection && !editor.settings.disable_nodechange && !editor.readonly) { - // Get start node - root = editor.getBody(); - node = selection.getStart(true) || root; - - // Make sure the node is within the editor root or is the editor root - if (node.ownerDocument != editor.getDoc() || !editor.dom.isChildOf(node, root)) { - node = root; - } - - // Get parents and add them to object - parents = []; - editor.dom.getParent(node, function (node) { - if (node === root) { - return true; - } - - parents.push(node); - }); - - args = args || {}; - args.element = node; - args.parents = parents; - - editor.fire('NodeChange', args); - } - }; - }; - } -); - -/** - * MousePosition.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 calculates an absolute coordinate inside the editor body for both local and global mouse events. - * - * @private - * @class tinymce.dom.MousePosition - */ -define( - 'tinymce.core.dom.MousePosition', - [ - ], - function () { - var getAbsolutePosition = function (elm) { - var doc, docElem, win, clientRect; - - clientRect = elm.getBoundingClientRect(); - doc = elm.ownerDocument; - docElem = doc.documentElement; - win = doc.defaultView; - - return { - top: clientRect.top + win.pageYOffset - docElem.clientTop, - left: clientRect.left + win.pageXOffset - docElem.clientLeft - }; - }; - - var getBodyPosition = function (editor) { - return editor.inline ? getAbsolutePosition(editor.getBody()) : { left: 0, top: 0 }; - }; - - var getScrollPosition = function (editor) { - var body = editor.getBody(); - return editor.inline ? { left: body.scrollLeft, top: body.scrollTop } : { left: 0, top: 0 }; - }; - - var getBodyScroll = function (editor) { - var body = editor.getBody(), docElm = editor.getDoc().documentElement; - var inlineScroll = { left: body.scrollLeft, top: body.scrollTop }; - var iframeScroll = { left: body.scrollLeft || docElm.scrollLeft, top: body.scrollTop || docElm.scrollTop }; - - return editor.inline ? inlineScroll : iframeScroll; - }; - - var getMousePosition = function (editor, event) { - if (event.target.ownerDocument !== editor.getDoc()) { - var iframePosition = getAbsolutePosition(editor.getContentAreaContainer()); - var scrollPosition = getBodyScroll(editor); - - return { - left: event.pageX - iframePosition.left + scrollPosition.left, - top: event.pageY - iframePosition.top + scrollPosition.top - }; - } - - return { - left: event.pageX, - top: event.pageY - }; - }; - - var calculatePosition = function (bodyPosition, scrollPosition, mousePosition) { - return { - pageX: (mousePosition.left - bodyPosition.left) + scrollPosition.left, - pageY: (mousePosition.top - bodyPosition.top) + scrollPosition.top - }; - }; - - var calc = function (editor, event) { - return calculatePosition(getBodyPosition(editor), getScrollPosition(editor), getMousePosition(editor, event)); - }; - - return { - calc: calc - }; - } -); - -/** - * DragDropOverrides.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 overriding the drag/drop logic of the editor. - * - * @private - * @class tinymce.DragDropOverrides - */ -define( - 'tinymce.core.DragDropOverrides', - [ - "tinymce.core.dom.NodeType", - "tinymce.core.util.Arr", - "tinymce.core.util.Fun", - "tinymce.core.util.Delay", - "tinymce.core.dom.DOMUtils", - "tinymce.core.dom.MousePosition" - ], - function ( - NodeType, Arr, Fun, Delay, DOMUtils, MousePosition - ) { - var isContentEditableFalse = NodeType.isContentEditableFalse, - isContentEditableTrue = NodeType.isContentEditableTrue; - - var isDraggable = function (rootElm, elm) { - return isContentEditableFalse(elm) && elm !== rootElm; - }; - - var isValidDropTarget = function (editor, targetElement, dragElement) { - if (targetElement === dragElement || editor.dom.isChildOf(targetElement, dragElement)) { - return false; - } - - if (isContentEditableFalse(targetElement)) { - return false; - } - - return true; - }; - - var cloneElement = function (elm) { - var cloneElm = elm.cloneNode(true); - cloneElm.removeAttribute('data-mce-selected'); - return cloneElm; - }; - - var createGhost = function (editor, elm, width, height) { - var clonedElm = elm.cloneNode(true); - - editor.dom.setStyles(clonedElm, { width: width, height: height }); - editor.dom.setAttrib(clonedElm, 'data-mce-selected', null); - - var ghostElm = editor.dom.create('div', { - 'class': 'mce-drag-container', - 'data-mce-bogus': 'all', - unselectable: 'on', - contenteditable: 'false' - }); - - editor.dom.setStyles(ghostElm, { - position: 'absolute', - opacity: 0.5, - overflow: 'hidden', - border: 0, - padding: 0, - margin: 0, - width: width, - height: height - }); - - editor.dom.setStyles(clonedElm, { - margin: 0, - boxSizing: 'border-box' - }); - - ghostElm.appendChild(clonedElm); - - return ghostElm; - }; - - var appendGhostToBody = function (ghostElm, bodyElm) { - if (ghostElm.parentNode !== bodyElm) { - bodyElm.appendChild(ghostElm); - } - }; - - var moveGhost = function (ghostElm, position, width, height, maxX, maxY) { - var overflowX = 0, overflowY = 0; - - ghostElm.style.left = position.pageX + 'px'; - ghostElm.style.top = position.pageY + 'px'; - - if (position.pageX + width > maxX) { - overflowX = (position.pageX + width) - maxX; - } - - if (position.pageY + height > maxY) { - overflowY = (position.pageY + height) - maxY; - } - - ghostElm.style.width = (width - overflowX) + 'px'; - ghostElm.style.height = (height - overflowY) + 'px'; - }; - - var removeElement = function (elm) { - if (elm && elm.parentNode) { - elm.parentNode.removeChild(elm); - } - }; - - var isLeftMouseButtonPressed = function (e) { - return e.button === 0; - }; - - var hasDraggableElement = function (state) { - return state.element; - }; - - var applyRelPos = function (state, position) { - return { - pageX: position.pageX - state.relX, - pageY: position.pageY + 5 - }; - }; - - var start = function (state, editor) { - return function (e) { - if (isLeftMouseButtonPressed(e)) { - var ceElm = Arr.find(editor.dom.getParents(e.target), Fun.or(isContentEditableFalse, isContentEditableTrue)); - - if (isDraggable(editor.getBody(), ceElm)) { - var elmPos = editor.dom.getPos(ceElm); - var bodyElm = editor.getBody(); - var docElm = editor.getDoc().documentElement; - - state.element = ceElm; - state.screenX = e.screenX; - state.screenY = e.screenY; - state.maxX = (editor.inline ? bodyElm.scrollWidth : docElm.offsetWidth) - 2; - state.maxY = (editor.inline ? bodyElm.scrollHeight : docElm.offsetHeight) - 2; - state.relX = e.pageX - elmPos.x; - state.relY = e.pageY - elmPos.y; - state.width = ceElm.offsetWidth; - state.height = ceElm.offsetHeight; - state.ghost = createGhost(editor, ceElm, state.width, state.height); - } - } - }; - }; - - var move = function (state, editor) { - // Reduces laggy drag behavior on Gecko - var throttledPlaceCaretAt = Delay.throttle(function (clientX, clientY) { - editor._selectionOverrides.hideFakeCaret(); - editor.selection.placeCaretAt(clientX, clientY); - }, 0); - - return function (e) { - var movement = Math.max(Math.abs(e.screenX - state.screenX), Math.abs(e.screenY - state.screenY)); - - if (hasDraggableElement(state) && !state.dragging && movement > 10) { - var args = editor.fire('dragstart', { target: state.element }); - if (args.isDefaultPrevented()) { - return; - } - - state.dragging = true; - editor.focus(); - } - - if (state.dragging) { - var targetPos = applyRelPos(state, MousePosition.calc(editor, e)); - - appendGhostToBody(state.ghost, editor.getBody()); - moveGhost(state.ghost, targetPos, state.width, state.height, state.maxX, state.maxY); - - throttledPlaceCaretAt(e.clientX, e.clientY); - } - }; - }; - - // Returns the raw element instead of the fake cE=false element - var getRawTarget = function (selection) { - var rng = selection.getSel().getRangeAt(0); - var startContainer = rng.startContainer; - return startContainer.nodeType === 3 ? startContainer.parentNode : startContainer; - }; - - var drop = function (state, editor) { - return function (e) { - if (state.dragging) { - if (isValidDropTarget(editor, getRawTarget(editor.selection), state.element)) { - var targetClone = cloneElement(state.element); - - var args = editor.fire('drop', { - targetClone: targetClone, - clientX: e.clientX, - clientY: e.clientY - }); - - if (!args.isDefaultPrevented()) { - targetClone = args.targetClone; - - editor.undoManager.transact(function () { - removeElement(state.element); - editor.insertContent(editor.dom.getOuterHTML(targetClone)); - editor._selectionOverrides.hideFakeCaret(); - }); - } - } - } - - removeDragState(state); - }; - }; - - var stop = function (state, editor) { - return function () { - removeDragState(state); - if (state.dragging) { - editor.fire('dragend'); - } - }; - }; - - var removeDragState = function (state) { - state.dragging = false; - state.element = null; - removeElement(state.ghost); - }; - - var bindFakeDragEvents = function (editor) { - var state = {}, pageDom, dragStartHandler, dragHandler, dropHandler, dragEndHandler, rootDocument; - - pageDom = DOMUtils.DOM; - rootDocument = document; - dragStartHandler = start(state, editor); - dragHandler = move(state, editor); - dropHandler = drop(state, editor); - dragEndHandler = stop(state, editor); - - editor.on('mousedown', dragStartHandler); - editor.on('mousemove', dragHandler); - editor.on('mouseup', dropHandler); - - pageDom.bind(rootDocument, 'mousemove', dragHandler); - pageDom.bind(rootDocument, 'mouseup', dragEndHandler); - - editor.on('remove', function () { - pageDom.unbind(rootDocument, 'mousemove', dragHandler); - pageDom.unbind(rootDocument, 'mouseup', dragEndHandler); - }); - }; - - var blockIeDrop = function (editor) { - editor.on('drop', function (e) { - // FF doesn't pass out clientX/clientY for drop since this is for IE we just use null instead - var realTarget = typeof e.clientX !== 'undefined' ? editor.getDoc().elementFromPoint(e.clientX, e.clientY) : null; - - if (isContentEditableFalse(realTarget) || isContentEditableFalse(editor.dom.getContentEditableParent(realTarget))) { - e.preventDefault(); - } - }); - }; - - var init = function (editor) { - bindFakeDragEvents(editor); - blockIeDrop(editor); - }; - - return { - init: init - }; - } -); - -/** - * 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 - * - * 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 overriding the selection with keyboard/mouse - * around contentEditable=false regions. - * - * @example - * // Disable the default cE=false selection - * tinymce.activeEditor.on('ShowCaret BeforeObjectSelected', function(e) { - * e.preventDefault(); - * }); - * - * @private - * @class tinymce.SelectionOverrides - */ -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.dom.RangePoint', - 'tinymce.core.keyboard.CefUtils', - 'tinymce.core.util.Delay', - 'tinymce.core.util.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, - isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse; - - function SelectionOverrides(editor) { - var rootNode = editor.getBody(); - var fakeCaret = new FakeCaret(editor.getBody(), isBlock), - realSelectionId = 'sel-' + editor.dom.uniqueId(), - selectedContentEditableNode; - - function isFakeSelectionElement(elm) { - return editor.dom.hasClass(elm, 'mce-offscreen-selection'); - } - - function getRealSelectionElement() { - var container = editor.dom.get(realSelectionId); - return container ? container.getElementsByTagName('*')[0] : container; - } - - function isBlock(node) { - return editor.dom.isBlock(node); - } - - function setRange(range) { - //console.log('setRange', range); - if (range) { - editor.selection.setRng(range); - } - } - - function getRange() { - return editor.selection.getRng(); - } - - function scrollIntoView(node, alignToTop) { - editor.selection.scrollIntoView(node, alignToTop); - } - - function showCaret(direction, node, before) { - var e; - - e = editor.fire('ShowCaret', { - target: node, - direction: direction, - before: before - }); - - if (e.isDefaultPrevented()) { - return null; - } - - scrollIntoView(node, direction === -1); - - return fakeCaret.show(before, node); - } - - function getNormalizedRangeEndPoint(direction, range) { - range = CaretUtils.normalizeRange(direction, rootNode, range); - - if (direction == -1) { - return CaretPosition.fromRangeStart(range); - } - - return CaretPosition.fromRangeEnd(range); - } - - function showBlockCaretContainer(blockCaretContainer) { - if (blockCaretContainer.hasAttribute('data-mce-caret')) { - CaretContainer.showCaretContainerBlock(blockCaretContainer); - setRange(getRange()); // Removes control rect on IE - scrollIntoView(blockCaretContainer[0]); - } - } - - function registerEvents() { - function getContentEditableRoot(node) { - var root = editor.getBody(); - - while (node && node != root) { - if (isContentEditableTrue(node) || isContentEditableFalse(node)) { - return node; - } - - node = node.parentNode; - } - - return null; - } - - // 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 (e) { - var range = getRange(); - - if (range.collapsed && EditorView.isXYInContentArea(editor, e.clientX, e.clientY)) { - setRange(CefUtils.renderCaretAtRange(editor, range)); - } - }); - - editor.on('click', function (e) { - var contentEditableRoot; - - contentEditableRoot = getContentEditableRoot(e.target); - if (contentEditableRoot) { - // Prevent clicks on links in a cE=false element - if (isContentEditableFalse(contentEditableRoot)) { - e.preventDefault(); - editor.focus(); - } - - // Removes fake selection if a cE=true is clicked within a cE=false like the toc title - if (isContentEditableTrue(contentEditableRoot)) { - if (editor.dom.isChildOf(contentEditableRoot, editor.selection.getNode())) { - removeContentEditableSelection(); - } - } - } - }); - - editor.on('blur NewBlock', function () { - removeContentEditableSelection(); - hideFakeCaret(); - }); - - function handleTouchSelect(editor) { - var moved = false; - - editor.on('touchstart', function () { - moved = false; - }); - - editor.on('touchmove', function () { - moved = true; - }); - - editor.on('touchend', function (e) { - var contentEditableRoot = getContentEditableRoot(e.target); - - if (isContentEditableFalse(contentEditableRoot)) { - if (!moved) { - e.preventDefault(); - setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot)); - } - } - }); - } - - var hasNormalCaretPosition = function (elm) { - var caretWalker = new CaretWalker(elm); - - if (!elm.firstChild) { - return false; - } - - var startPos = CaretPosition.before(elm.firstChild); - var newPos = caretWalker.next(startPos); - - return newPos && !isBeforeContentEditableFalse(newPos) && !isAfterContentEditableFalse(newPos); - }; - - var isInSameBlock = function (node1, node2) { - var block1 = editor.dom.getParent(node1, editor.dom.isBlock); - var block2 = editor.dom.getParent(node2, editor.dom.isBlock); - return block1 === block2; - }; - - // Checks if the target node is in a block and if that block has a caret position better than the - // suggested caretNode this is to prevent the caret from being sucked in towards a cE=false block if - // they are adjacent on the vertical axis - var hasBetterMouseTarget = function (targetNode, caretNode) { - var targetBlock = editor.dom.getParent(targetNode, editor.dom.isBlock); - var caretBlock = editor.dom.getParent(caretNode, editor.dom.isBlock); - - return targetBlock && !isInSameBlock(targetBlock, caretBlock) && hasNormalCaretPosition(targetBlock); - }; - - handleTouchSelect(editor); - - 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)) { - e.preventDefault(); - setContentEditableSelection(CefUtils.selectNode(editor, contentEditableRoot)); - } else { - removeContentEditableSelection(); - - // Check that we're not attempting a shift + click select within a contenteditable='true' element - 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 { - // Remove needs to be called here since the mousedown might alter the selection without calling selection.setRng - // and therefore not fire the AfterSetSelectionRange event. - removeContentEditableSelection(); - hideFakeCaret(); - - var caretInfo = LineUtils.closestCaret(rootNode, e.clientX, e.clientY); - if (caretInfo) { - if (!hasBetterMouseTarget(e.target, caretInfo.node)) { - e.preventDefault(); - editor.getBody().focus(); - setRange(showCaret(1, caretInfo.node, caretInfo.before)); - } - } - } - }); - - editor.on('keypress', function (e) { - if (VK.modifierPressed(e)) { - return; - } - - switch (e.keyCode) { - default: - if (isContentEditableFalse(editor.selection.getNode())) { - e.preventDefault(); - } - break; - } - }); - - editor.on('getSelectionRange', function (e) { - var rng = e.range; - - if (selectedContentEditableNode) { - if (!selectedContentEditableNode.parentNode) { - selectedContentEditableNode = null; - return; - } - - rng = rng.cloneRange(); - rng.selectNode(selectedContentEditableNode); - e.range = rng; - } - }); - - editor.on('setSelectionRange', function (e) { - var rng; - - rng = setContentEditableSelection(e.range, e.forward); - if (rng) { - e.range = rng; - } - }); - - editor.on('AfterSetSelectionRange', function (e) { - var rng = e.range; - - if (!isRangeInCaretContainer(rng)) { - hideFakeCaret(); - } - - if (!isFakeSelectionElement(rng.startContainer.parentNode)) { - removeContentEditableSelection(); - } - }); - - editor.on('focus', function () { - // Make sure we have a proper fake caret on focus - Delay.setEditorTimeout(editor, function () { - editor.selection.setRng(CefUtils.renderRangeCaret(editor, editor.selection.getRng())); - }, 0); - }); - - editor.on('copy', function (e) { - var clipboardData = e.clipboardData; - - // Make sure we get proper html/text for the fake cE=false selection - // Doesn't work at all on Edge since it doesn't have proper clipboardData support - if (!e.isDefaultPrevented() && e.clipboardData && !Env.ie) { - var realSelectionElement = getRealSelectionElement(); - if (realSelectionElement) { - e.preventDefault(); - clipboardData.clearData(); - clipboardData.setData('text/html', realSelectionElement.outerHTML); - clipboardData.setData('text/plain', realSelectionElement.outerText); - } - } - }); - - DragDropOverrides.init(editor); - } - - function addCss() { - var styles = editor.contentStyles, rootClass = '.mce-content-body'; - - styles.push(fakeCaret.getCss()); - styles.push( - rootClass + ' .mce-offscreen-selection {' + - 'position: absolute;' + - 'left: -9999999999px;' + - 'max-width: 1000000px;' + - '}' + - rootClass + ' *[contentEditable=false] {' + - 'cursor: default;' + - '}' + - rootClass + ' *[contentEditable=true] {' + - 'cursor: text;' + - '}' - ); - } - - function isWithinCaretContainer(node) { - return ( - CaretContainer.isCaretContainer(node) || - CaretContainer.startsWithCaretContainer(node) || - CaretContainer.endsWithCaretContainer(node) - ); - } - - function isRangeInCaretContainer(rng) { - return isWithinCaretContainer(rng.startContainer) || isWithinCaretContainer(rng.endContainer); - } - - function setContentEditableSelection(range, forward) { - var node, $ = editor.$, dom = editor.dom, $realSelectionContainer, sel, - startContainer, startOffset, endOffset, e, caretPosition, targetClone, origTargetClone; - - if (!range) { - return null; - } - - if (range.collapsed) { - if (!isRangeInCaretContainer(range)) { - if (forward === false) { - caretPosition = getNormalizedRangeEndPoint(-1, range); - - if (isContentEditableFalse(caretPosition.getNode(true))) { - return showCaret(-1, caretPosition.getNode(true), false); - } - - if (isContentEditableFalse(caretPosition.getNode())) { - return showCaret(-1, caretPosition.getNode(), !caretPosition.isAtEnd()); - } - } else { - caretPosition = getNormalizedRangeEndPoint(1, range); - - if (isContentEditableFalse(caretPosition.getNode())) { - return showCaret(1, caretPosition.getNode(), !caretPosition.isAtEnd()); - } - - if (isContentEditableFalse(caretPosition.getNode(true))) { - return showCaret(1, caretPosition.getNode(true), false); - } - } - } - - return null; - } - - startContainer = range.startContainer; - startOffset = range.startOffset; - endOffset = range.endOffset; - - // Normalizes [] to [] - if (startContainer.nodeType == 3 && startOffset == 0 && isContentEditableFalse(startContainer.parentNode)) { - startContainer = startContainer.parentNode; - startOffset = dom.nodeIndex(startContainer); - startContainer = startContainer.parentNode; - } - - if (startContainer.nodeType != 1) { - return null; - } - - if (endOffset == startOffset + 1) { - node = startContainer.childNodes[startOffset]; - } - - if (!isContentEditableFalse(node)) { - return null; - } - - targetClone = origTargetClone = node.cloneNode(true); - e = editor.fire('ObjectSelected', { target: node, targetClone: targetClone }); - if (e.isDefaultPrevented()) { - return null; - } - - $realSelectionContainer = SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).fold( - function () { - return $([]); - }, - function (elm) { - return $([elm.dom()]); - } - ); - - targetClone = e.targetClone; - if ($realSelectionContainer.length === 0) { - $realSelectionContainer = $( - '
    ' - ).attr('id', realSelectionId); - - $realSelectionContainer.appendTo(editor.getBody()); - } - - range = editor.dom.createRng(); - - // WHY is IE making things so hard! Copy on x produces: x - // This is a ridiculous hack where we place the selection from a block over the inline element - // so that just the inline element is copied as is and not converted. - if (targetClone === origTargetClone && Env.ie) { - $realSelectionContainer.empty().append('

    \u00a0

    ').append(targetClone); - range.setStartAfter($realSelectionContainer[0].firstChild.firstChild); - range.setEndAfter(targetClone); - } else { - $realSelectionContainer.empty().append('\u00a0').append(targetClone).append('\u00a0'); - range.setStart($realSelectionContainer[0].firstChild, 1); - range.setEnd($realSelectionContainer[0].lastChild, 0); - } - - $realSelectionContainer.css({ - top: dom.getPos(node, editor.getBody()).y - }); - - $realSelectionContainer[0].focus(); - sel = editor.selection.getSel(); - sel.removeAllRanges(); - sel.addRange(range); - - 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(); - - return range; - } - - function removeContentEditableSelection() { - if (selectedContentEditableNode) { - selectedContentEditableNode.removeAttribute('data-mce-selected'); - SelectorFind.descendant(Element.fromDom(editor.getBody()), '#' + realSelectionId).each(Remove.remove); - selectedContentEditableNode = null; - } - } - - function destroy() { - fakeCaret.destroy(); - selectedContentEditableNode = null; - } - - function hideFakeCaret() { - fakeCaret.hide(); - } - - if (Env.ceFalse) { - registerEvents(); - addCss(); - } - - return { - showCaret: showCaret, - showBlockCaretContainer: showBlockCaretContainer, - hideFakeCaret: hideFakeCaret, - destroy: destroy - }; - } - - return SelectionOverrides; - } -); - -/** - * 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 - * - * 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 paths of nodes within an element. - * - * @private - * @class tinymce.dom.NodePath - */ -define( - 'tinymce.core.dom.NodePath', - [ - "tinymce.core.dom.DOMUtils" - ], - function (DOMUtils) { - function create(rootNode, targetNode, normalized) { - var path = []; - - for (; targetNode && targetNode != rootNode; targetNode = targetNode.parentNode) { - path.push(DOMUtils.nodeIndex(targetNode, normalized)); - } - - return path; - } - - function resolve(rootNode, path) { - var i, node, children; - - for (node = rootNode, i = path.length - 1; i >= 0; i--) { - children = node.childNodes; - - if (path[i] > children.length - 1) { - return null; - } - - node = children[path[i]]; - } - - return node; - } - - return { - create: create, - resolve: resolve - }; - } -); -/** - * Quirks.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 - * - * @ignore-file - */ - -/** - * This file includes fixes for various browser quirks it's made to make it easy to add/remove browser specific fixes. - * - * @private - * @class tinymce.util.Quirks - */ -define( - 'tinymce.core.util.Quirks', - [ - "tinymce.core.util.VK", - "tinymce.core.dom.RangeUtils", - "tinymce.core.dom.TreeWalker", - "tinymce.core.dom.NodePath", - "tinymce.core.html.Node", - "tinymce.core.html.Entities", - "tinymce.core.Env", - "tinymce.core.util.Tools", - "tinymce.core.util.Delay", - "tinymce.core.caret.CaretContainer", - "tinymce.core.caret.CaretPosition", - "tinymce.core.caret.CaretWalker" - ], - function (VK, RangeUtils, TreeWalker, NodePath, Node, Entities, Env, Tools, Delay, CaretContainer, CaretPosition, CaretWalker) { - return function (editor) { - var each = Tools.each; - var BACKSPACE = VK.BACKSPACE, DELETE = VK.DELETE, dom = editor.dom, selection = editor.selection, - settings = editor.settings, parser = editor.parser, serializer = editor.serializer; - var isGecko = Env.gecko, isIE = Env.ie, isWebKit = Env.webkit; - var mceInternalUrlPrefix = 'data:text/mce-internal,'; - var mceInternalDataType = isIE ? 'Text' : 'URL'; - - /** - * Executes a command with a specific state this can be to enable/disable browser editing features. - */ - function setEditorCommandState(cmd, state) { - try { - editor.getDoc().execCommand(cmd, false, state); - } catch (ex) { - // Ignore - } - } - - /** - * Returns current IE document mode. - */ - function getDocumentMode() { - var documentMode = editor.getDoc().documentMode; - - return documentMode ? documentMode : 6; - } - - /** - * Returns true/false if the event is prevented or not. - * - * @private - * @param {Event} e Event object. - * @return {Boolean} true/false if the event is prevented or not. - */ - function isDefaultPrevented(e) { - return e.isDefaultPrevented(); - } - - /** - * Sets Text/URL data on the event's dataTransfer object to a special data:text/mce-internal url. - * This is to workaround the inability to set custom contentType on IE and Safari. - * The editor's selected content is encoded into this url so drag and drop between editors will work. - * - * @private - * @param {DragEvent} e Event object - */ - function setMceInternalContent(e) { - var selectionHtml, internalContent; - - if (e.dataTransfer) { - if (editor.selection.isCollapsed() && e.target.tagName == 'IMG') { - selection.select(e.target); - } - - selectionHtml = editor.selection.getContent(); - - // Safari/IE doesn't support custom dataTransfer items so we can only use URL and Text - if (selectionHtml.length > 0) { - internalContent = mceInternalUrlPrefix + escape(editor.id) + ',' + escape(selectionHtml); - e.dataTransfer.setData(mceInternalDataType, internalContent); - } - } - } - - /** - * Gets content of special data:text/mce-internal url on the event's dataTransfer object. - * This is to workaround the inability to set custom contentType on IE and Safari. - * The editor's selected content is encoded into this url so drag and drop between editors will work. - * - * @private - * @param {DragEvent} e Event object - * @returns {String} mce-internal content - */ - function getMceInternalContent(e) { - var internalContent; - - if (e.dataTransfer) { - internalContent = e.dataTransfer.getData(mceInternalDataType); - - if (internalContent && internalContent.indexOf(mceInternalUrlPrefix) >= 0) { - internalContent = internalContent.substr(mceInternalUrlPrefix.length).split(','); - - return { - id: unescape(internalContent[0]), - html: unescape(internalContent[1]) - }; - } - } - - return null; - } - - /** - * Inserts contents using the paste clipboard command if it's available if it isn't it will fallback - * to the core command. - * - * @private - * @param {String} content Content to insert at selection. - * @param {Boolean} internal State if the paste is to be considered internal or external. - */ - function insertClipboardContents(content, internal) { - if (editor.queryCommandSupported('mceInsertClipboardContent')) { - editor.execCommand('mceInsertClipboardContent', false, { content: content, internal: internal }); - } else { - editor.execCommand('mceInsertContent', false, content); - } - } - - /** - * Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors. - * - * For example: - *

    |

    - * - * Or: - *

    |

    - * - * Or: - * [

    ] - */ - function emptyEditorWhenDeleting() { - function serializeRng(rng) { - var body = dom.create("body"); - var contents = rng.cloneContents(); - body.appendChild(contents); - return selection.serializer.serialize(body, { format: 'html' }); - } - - function allContentsSelected(rng) { - if (!rng.setStart) { - if (rng.item) { - return false; - } - - var bodyRng = rng.duplicate(); - bodyRng.moveToElementText(editor.getBody()); - return RangeUtils.compareRanges(rng, bodyRng); - } - - var selection = serializeRng(rng); - - var allRng = dom.createRng(); - allRng.selectNode(editor.getBody()); - - var allSelection = serializeRng(allRng); - return selection === allSelection; - } - - editor.on('keydown', function (e) { - var keyCode = e.keyCode, isCollapsed, body; - - // Empty the editor if it's needed for example backspace at

    |

    - if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) { - isCollapsed = editor.selection.isCollapsed(); - body = editor.getBody(); - - // Selection is collapsed but the editor isn't empty - if (isCollapsed && !dom.isEmpty(body)) { - return; - } - - // Selection isn't collapsed but not all the contents is selected - if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { - return; - } - - // Manually empty the editor - e.preventDefault(); - editor.setContent(''); - - if (body.firstChild && dom.isBlock(body.firstChild)) { - editor.selection.setCursorLocation(body.firstChild, 0); - } else { - editor.selection.setCursorLocation(body, 0); - } - - editor.nodeChanged(); - } - }); - } - - /** - * WebKit doesn't select all the nodes in the body when you press Ctrl+A. - * IE selects more than the contents [

    a

    ] instead of

    [a] see bug #6438 - * This selects the whole body so that backspace/delete logic will delete everything - */ - function selectAll() { - editor.shortcuts.add('meta+a', null, 'SelectAll'); - } - - /** - * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. - * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. - * - * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until - * you enter a character into the editor. - * - * It also happens when the first focus in made to the body. - * - * See: https://bugs.webkit.org/show_bug.cgi?id=83566 - */ - function inputMethodFocus() { - if (!editor.settings.content_editable) { - // Case 1 IME doesn't initialize if you focus the document - // Disabled since it was interferring with the cE=false logic - // Also coultn't reproduce the issue on Safari 9 - /*dom.bind(editor.getDoc(), 'focusin', function() { - selection.setRng(selection.getRng()); - });*/ - - // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event - // Needs to be both down/up due to weird rendering bug on Chrome Windows - dom.bind(editor.getDoc(), 'mousedown mouseup', function (e) { - var rng; - - if (e.target == editor.getDoc().documentElement) { - rng = selection.getRng(); - editor.getBody().focus(); - - if (e.type == 'mousedown') { - if (CaretContainer.isCaretContainer(rng.startContainer)) { - return; - } - - // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret - selection.placeCaretAt(e.clientX, e.clientY); - } else { - selection.setRng(rng); - } - } - }); - } - } - - /** - * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the - * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is - * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js - * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other - * browsers. - * - * It also fixes a bug on Firefox where it's impossible to delete HR elements. - */ - function removeHrOnBackspace() { - editor.on('keydown', function (e) { - if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { - // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow - if (!editor.getBody().getElementsByTagName('hr').length) { - return; - } - - if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { - var node = selection.getNode(); - var previousSibling = node.previousSibling; - - if (node.nodeName == 'HR') { - dom.remove(node); - e.preventDefault(); - return; - } - - if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "hr") { - dom.remove(previousSibling); - e.preventDefault(); - } - } - } - }); - } - - /** - * Firefox 3.x has an issue where the body element won't get proper focus if you click out - * side it's rectangle. - */ - function focusBody() { - // Fix for a focus bug in FF 3.x where the body element - // wouldn't get proper focus if the user clicked on the HTML element - if (!window.Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 - editor.on('mousedown', function (e) { - if (!isDefaultPrevented(e) && e.target.nodeName === "HTML") { - var body = editor.getBody(); - - // Blur the body it's focused but not correctly focused - body.blur(); - - // Refocus the body after a little while - Delay.setEditorTimeout(editor, function () { - body.focus(); - }); - } - }); - } - } - - /** - * WebKit has a bug where it isn't possible to select image, hr or anchor elements - * by clicking on them so we need to fake that. - */ - function selectControlElements() { - editor.on('click', function (e) { - var target = e.target; - - // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 - // WebKit can't even do simple things like selecting an image - // Needs to be the setBaseAndExtend or it will fail to select floated images - if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== "false") { - e.preventDefault(); - editor.selection.select(target); - editor.nodeChanged(); - } - - if (target.nodeName == 'A' && dom.hasClass(target, 'mce-item-anchor')) { - e.preventDefault(); - selection.select(target); - } - }); - } - - /** - * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. - * - * Fixes do backspace/delete on this: - *

    bla[ck

    r]ed

    - * - * Would become: - *

    bla|ed

    - * - * Instead of: - *

    bla|ed

    - */ - function removeStylesWhenDeletingAcrossBlockElements() { - function getAttributeApplyFunction() { - var template = dom.getAttribs(selection.getStart().cloneNode(false)); - - return function () { - var target = selection.getStart(); - - if (target !== editor.getBody()) { - dom.setAttrib(target, "style", null); - - each(template, function (attr) { - target.setAttributeNode(attr.cloneNode(true)); - }); - } - }; - } - - function isSelectionAcrossElements() { - return !selection.isCollapsed() && - dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); - } - - editor.on('keypress', function (e) { - var applyAttributes; - - if (!isDefaultPrevented(e) && (e.keyCode == 8 || e.keyCode == 46) && isSelectionAcrossElements()) { - applyAttributes = getAttributeApplyFunction(); - editor.getDoc().execCommand('delete', false, null); - applyAttributes(); - e.preventDefault(); - return false; - } - }); - - dom.bind(editor.getDoc(), 'cut', function (e) { - var applyAttributes; - - if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { - applyAttributes = getAttributeApplyFunction(); - - Delay.setEditorTimeout(editor, function () { - applyAttributes(); - }); - } - }); - } - - /** - * Screen readers on IE needs to have the role application set on the body. - */ - function ensureBodyHasRoleApplication() { - document.body.setAttribute("role", "application"); - } - - /** - * Backspacing into a table behaves differently depending upon browser type. - * Therefore, disable Backspace when cursor immediately follows a table. - */ - function disableBackspaceIntoATable() { - editor.on('keydown', function (e) { - if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { - if (selection.isCollapsed() && selection.getRng(true).startOffset === 0) { - var previousSibling = selection.getNode().previousSibling; - if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === "table") { - e.preventDefault(); - return false; - } - } - } - }); - } - - /** - * Old IE versions can't properly render BR elements in PRE tags white in contentEditable mode. So this - * logic adds a \n before the BR so that it will get rendered. - */ - function addNewLinesBeforeBrInPre() { - // IE8+ rendering mode does the right thing with BR in PRE - if (getDocumentMode() > 7) { - return; - } - - // Enable display: none in area and add a specific class that hides all BR elements in PRE to - // avoid the caret from getting stuck at the BR elements while pressing the right arrow key - setEditorCommandState('RespectVisibilityInDesign', true); - editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); - dom.addClass(editor.getBody(), 'mceHideBrInPre'); - - // Adds a \n before all BR elements in PRE to get them visual - parser.addNodeFilter('pre', function (nodes) { - var i = nodes.length, brNodes, j, brElm, sibling; - - while (i--) { - brNodes = nodes[i].getAll('br'); - j = brNodes.length; - while (j--) { - brElm = brNodes[j]; - - // Add \n before BR in PRE elements on older IE:s so the new lines get rendered - sibling = brElm.prev; - if (sibling && sibling.type === 3 && sibling.value.charAt(sibling.value - 1) != '\n') { - sibling.value += '\n'; - } else { - brElm.parent.insert(new Node('#text', 3), brElm, true).value = '\n'; - } - } - } - }); - - // Removes any \n before BR elements in PRE since other browsers and in contentEditable=false mode they will be visible - serializer.addNodeFilter('pre', function (nodes) { - var i = nodes.length, brNodes, j, brElm, sibling; - - while (i--) { - brNodes = nodes[i].getAll('br'); - j = brNodes.length; - while (j--) { - brElm = brNodes[j]; - sibling = brElm.prev; - if (sibling && sibling.type == 3) { - sibling.value = sibling.value.replace(/\r?\n$/, ''); - } - } - } - }); - } - - /** - * Moves style width/height to attribute width/height when the user resizes an image on IE. - */ - function removePreSerializedStylesWhenSelectingControls() { - dom.bind(editor.getBody(), 'mouseup', function () { - var value, node = selection.getNode(); - - // Moved styles to attributes on IMG eements - if (node.nodeName == 'IMG') { - // Convert style width to width attribute - if ((value = dom.getStyle(node, 'width'))) { - dom.setAttrib(node, 'width', value.replace(/[^0-9%]+/g, '')); - dom.setStyle(node, 'width', ''); - } - - // Convert style height to height attribute - if ((value = dom.getStyle(node, 'height'))) { - dom.setAttrib(node, 'height', value.replace(/[^0-9%]+/g, '')); - dom.setStyle(node, 'height', ''); - } - } - }); - } - - /** - * Removes a blockquote when backspace is pressed at the beginning of it. - * - * For example: - *

    |x

    - * - * Becomes: - *

    |x

    - */ - function removeBlockQuoteOnBackSpace() { - // Add block quote deletion handler - editor.on('keydown', function (e) { - var rng, container, offset, root, parent; - - if (isDefaultPrevented(e) || e.keyCode != VK.BACKSPACE) { - return; - } - - rng = selection.getRng(); - container = rng.startContainer; - offset = rng.startOffset; - root = dom.getRoot(); - parent = container; - - if (!rng.collapsed || offset !== 0) { - return; - } - - while (parent && parent.parentNode && parent.parentNode.firstChild == parent && parent.parentNode != root) { - parent = parent.parentNode; - } - - // Is the cursor at the beginning of a blockquote? - if (parent.tagName === 'BLOCKQUOTE') { - // Remove the blockquote - editor.formatter.toggle('blockquote', null, parent); - - // Move the caret to the beginning of container - rng = dom.createRng(); - rng.setStart(container, 0); - rng.setEnd(container, 0); - selection.setRng(rng); - } - }); - } - - /** - * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. - */ - function setGeckoEditingOptions() { - function setOpts() { - refreshContentEditable(); - - setEditorCommandState("StyleWithCSS", false); - setEditorCommandState("enableInlineTableEditing", false); - - if (!settings.object_resizing) { - setEditorCommandState("enableObjectResizing", false); - } - } - - if (!settings.readonly) { - editor.on('BeforeExecCommand MouseDown', setOpts); - } - } - - /** - * Fixes a gecko link bug, when a link is placed at the end of block elements there is - * no way to move the caret behind the link. This fix adds a bogus br element after the link. - * - * For example this: - *

    x

    - * - * Becomes this: - *

    x

    - */ - function addBrAfterLastLinks() { - function fixLinks() { - each(dom.select('a'), function (node) { - var parentNode = node.parentNode, root = dom.getRoot(); - - if (parentNode.lastChild === node) { - while (parentNode && !dom.isBlock(parentNode)) { - if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { - return; - } - - parentNode = parentNode.parentNode; - } - - dom.add(parentNode, 'br', { 'data-mce-bogus': 1 }); - } - }); - } - - editor.on('SetContent ExecCommand', function (e) { - if (e.type == "setcontent" || e.command === 'mceInsertLink') { - fixLinks(); - } - }); - } - - /** - * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by - * default we want to change that behavior. - */ - function setDefaultBlockType() { - if (settings.forced_root_block) { - editor.on('init', function () { - setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); - }); - } - } - - /** - * Deletes the selected image on IE instead of navigating to previous page. - */ - function deleteControlItemOnBackSpace() { - editor.on('keydown', function (e) { - var rng; - - if (!isDefaultPrevented(e) && e.keyCode == BACKSPACE) { - rng = editor.getDoc().selection.createRange(); - if (rng && rng.item) { - e.preventDefault(); - editor.undoManager.beforeChange(); - dom.remove(rng.item(0)); - editor.undoManager.add(); - } - } - }); - } - - /** - * IE10 doesn't properly render block elements with the right height until you add contents to them. - * This fixes that by adding a padding-right to all empty text block elements. - * See: https://connect.microsoft.com/IE/feedback/details/743881 - */ - function renderEmptyBlocksFix() { - var emptyBlocksCSS; - - // IE10+ - if (getDocumentMode() >= 10) { - emptyBlocksCSS = ''; - each('p div h1 h2 h3 h4 h5 h6'.split(' '), function (name, i) { - emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; - }); - - editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); - } - } - - /** - * Old IE versions can't retain contents within noscript elements so this logic will store the contents - * as a attribute and the insert that value as it's raw text when the DOM is serialized. - */ - function keepNoScriptContents() { - if (getDocumentMode() < 9) { - parser.addNodeFilter('noscript', function (nodes) { - var i = nodes.length, node, textNode; - - while (i--) { - node = nodes[i]; - textNode = node.firstChild; - - if (textNode) { - node.attr('data-mce-innertext', textNode.value); - } - } - }); - - serializer.addNodeFilter('noscript', function (nodes) { - var i = nodes.length, node, textNode, value; - - while (i--) { - node = nodes[i]; - textNode = nodes[i].firstChild; - - if (textNode) { - textNode.value = Entities.decode(textNode.value); - } else { - // Old IE can't retain noscript value so an attribute is used to store it - value = node.attributes.map['data-mce-innertext']; - if (value) { - node.attr('data-mce-innertext', null); - textNode = new Node('#text', 3); - textNode.value = value; - textNode.raw = true; - node.append(textNode); - } - } - } - }); - } - } - - /** - * IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode. - */ - function fixCaretSelectionOfDocumentElementOnIe() { - var doc = dom.doc, body = doc.body, started, startRng, htmlElm; - - // Return range from point or null if it failed - function rngFromPoint(x, y) { - var rng = body.createTextRange(); - - try { - rng.moveToPoint(x, y); - } catch (ex) { - // IE sometimes throws and exception, so lets just ignore it - rng = null; - } - - return rng; - } - - // Fires while the selection is changing - function selectionChange(e) { - var pointRng; - - // Check if the button is down or not - if (e.button) { - // Create range from mouse position - pointRng = rngFromPoint(e.x, e.y); - - if (pointRng) { - // Check if pointRange is before/after selection then change the endPoint - if (pointRng.compareEndPoints('StartToStart', startRng) > 0) { - pointRng.setEndPoint('StartToStart', startRng); - } else { - pointRng.setEndPoint('EndToEnd', startRng); - } - - pointRng.select(); - } - } else { - endSelection(); - } - } - - // Removes listeners - function endSelection() { - var rng = doc.selection.createRange(); - - // If the range is collapsed then use the last start range - if (startRng && !rng.item && rng.compareEndPoints('StartToEnd', rng) === 0) { - startRng.select(); - } - - dom.unbind(doc, 'mouseup', endSelection); - dom.unbind(doc, 'mousemove', selectionChange); - startRng = started = 0; - } - - // Make HTML element unselectable since we are going to handle selection by hand - doc.documentElement.unselectable = true; - - // Detect when user selects outside BODY - dom.bind(doc, 'mousedown contextmenu', function (e) { - if (e.target.nodeName === 'HTML') { - if (started) { - endSelection(); - } - - // Detect vertical scrollbar, since IE will fire a mousedown on the scrollbar and have target set as HTML - htmlElm = doc.documentElement; - if (htmlElm.scrollHeight > htmlElm.clientHeight) { - return; - } - - started = 1; - // Setup start position - startRng = rngFromPoint(e.x, e.y); - if (startRng) { - // Listen for selection change events - dom.bind(doc, 'mouseup', endSelection); - dom.bind(doc, 'mousemove', selectionChange); - - dom.getRoot().focus(); - startRng.select(); - } - } - }); - } - - /** - * Fixes selection issues where the caret can be placed between two inline elements like a|b - * this fix will lean the caret right into the closest inline element. - */ - function normalizeSelection() { - // 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)) { - // 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); - } - - /** - * Forces Gecko to render a broken image icon if it fails to load an image. - */ - function showBrokenImageIcon() { - editor.contentStyles.push( - 'img:-moz-broken {' + - '-moz-force-broken-image-icon:1;' + - 'min-width:24px;' + - 'min-height:24px' + - '}' - ); - } - - /** - * iOS has a bug where it's impossible to type if the document has a touchstart event - * bound and the user touches the document while having the on screen keyboard visible. - * - * The touch event moves the focus to the parent document while having the caret inside the iframe - * this fix moves the focus back into the iframe document. - */ - function restoreFocusOnKeyDown() { - if (!editor.inline) { - editor.on('keydown', function () { - if (document.activeElement == document.body) { - editor.getWin().focus(); - } - }); - } - } - - /** - * IE 11 has an annoying issue where you can't move focus into the editor - * by clicking on the white area HTML element. We used to be able to to fix this with - * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection - * object it's not possible anymore. So we need to hack in a ungly CSS to force the - * body to be at least 150px. If the user clicks the HTML element out side this 150px region - * we simply move the focus into the first paragraph. Not ideal since you loose the - * positioning of the caret but goot enough for most cases. - */ - function bodyHeight() { - if (!editor.inline) { - editor.contentStyles.push('body {min-height: 150px}'); - editor.on('click', function (e) { - var rng; - - if (e.target.nodeName == 'HTML') { - // Edge seems to only need focus if we set the range - // the caret will become invisible and moved out of the iframe!! - if (Env.ie > 11) { - editor.getBody().focus(); - return; - } - - // Need to store away non collapsed ranges since the focus call will mess that up see #7382 - rng = editor.selection.getRng(); - editor.getBody().focus(); - editor.selection.setRng(rng); - editor.selection.normalize(); - editor.nodeChanged(); - } - }); - } - } - - /** - * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. - * You might then loose all your work so we need to block that behavior and replace it with our own. - */ - function blockCmdArrowNavigation() { - if (Env.mac) { - editor.on('keydown', function (e) { - if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode == 37 || e.keyCode == 39)) { - e.preventDefault(); - editor.selection.getSel().modify('move', e.keyCode == 37 ? 'backward' : 'forward', 'lineboundary'); - } - }); - } - } - - /** - * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin. - */ - function disableAutoUrlDetect() { - setEditorCommandState("AutoUrlDetect", false); - } - - /** - * iOS 7.1 introduced two new bugs: - * 1) It's possible to open links within a contentEditable area by clicking on them. - * 2) If you hold down the finger it will display the link/image touch callout menu. - */ - function tapLinksAndImages() { - editor.on('click', function (e) { - var elm = e.target; - - do { - if (elm.tagName === 'A') { - e.preventDefault(); - return; - } - } while ((elm = elm.parentNode)); - }); - - editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); - } - - /** - * iOS Safari and possible other browsers have a bug where it won't fire - * a click event when a contentEditable is focused. This function fakes click events - * by using touchstart/touchend and measuring the time and distance travelled. - */ - /* - function touchClickEvent() { - editor.on('touchstart', function(e) { - var elm, time, startTouch, changedTouches; - - elm = e.target; - time = new Date().getTime(); - changedTouches = e.changedTouches; - - if (!changedTouches || changedTouches.length > 1) { - return; - } - - startTouch = changedTouches[0]; - - editor.once('touchend', function(e) { - var endTouch = e.changedTouches[0], args; - - if (new Date().getTime() - time > 500) { - return; - } - - if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) { - return; - } - - if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) { - return; - } - - args = { - target: elm - }; - - each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) { - args[key] = endTouch[key]; - }); - - args = editor.fire('click', args); - - if (!args.isDefaultPrevented()) { - // iOS WebKit can't place the caret properly once - // you bind touch events so we need to do this manually - // TODO: Expand to the closest word? Touble tap still works. - editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY); - editor.nodeChanged(); - } - }); - }); - } - */ - - /** - * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. - * For example this:
    ' + - '' - ); - }, - - bindStates: function () { - var self = this, $ = self.$, textCls = self.classPrefix + 'txt'; + startTouch = changedTouches[0]; - function setButtonText(text) { - var $span = $('span.' + textCls, self.getEl()); + editor.once('touchend', function(e) { + var endTouch = e.changedTouches[0], args; - if (text) { - if (!$span[0]) { - $('button:first', self.getEl()).append(''); - $span = $('span.' + textCls, self.getEl()); + if (new Date().getTime() - time > 500) { + return; } - $span.html(self.encode(text)); - } else { - $span.remove(); - } - - self.classes.toggle('btn-has-text', !!text); - } + if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) { + return; + } - self.state.on('change:text', function (e) { - setButtonText(e.value); - }); + if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) { + return; + } - self.state.on('change:icon', function (e) { - var icon = e.value, prefix = self.classPrefix; + args = { + target: elm + }; - self.settings.icon = icon; - icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; + each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) { + args[key] = endTouch[key]; + }); - var btnElm = self.getEl().firstChild, iconElm = btnElm.getElementsByTagName('i')[0]; + args = editor.fire('click', args); - if (icon) { - if (!iconElm || iconElm != btnElm.firstChild) { - iconElm = document.createElement('i'); - btnElm.insertBefore(iconElm, btnElm.firstChild); + if (!args.isDefaultPrevented()) { + // iOS WebKit can't place the caret properly once + // you bind touch events so we need to do this manually + // TODO: Expand to the closest word? Touble tap still works. + editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY); + editor.nodeChanged(); } - - iconElm.className = icon; - } else if (iconElm) { - btnElm.removeChild(iconElm); - } - - setButtonText(self.state.get('text')); + }); }); - - return self._super(); } - }); - } -); - -/** - * ButtonGroup.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 control enables you to put multiple buttons into a group. This is - * useful when you want to combine similar toolbar buttons into a group. - * - * @example - * // Create and render a buttongroup with two buttons to the body element - * tinymce.ui.Factory.create({ - * type: 'buttongroup', - * items: [ - * {text: 'Button A'}, - * {text: 'Button B'} - * ] - * }).renderTo(document.body); - * - * @-x-less ButtonGroup.less - * @class tinymce.ui.ButtonGroup - * @extends tinymce.ui.Container - */ -define( - 'tinymce.core.ui.ButtonGroup', - [ - "tinymce.core.ui.Container" - ], - function (Container) { - "use strict"; - - return Container.extend({ - Defaults: { - defaultType: 'button', - role: 'group' - }, + */ /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. + * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. + * For example this:
    ' + - '' - ); + DOM.addClass(targetElm, 'mce-content-body'); + editor.contentDocument = doc = settings.content_document || document; + editor.contentWindow = settings.content_window || window; + editor.bodyElement = targetElm; - self.classes.add('has-open'); - } + // Prevent leak in IE + settings.content_document = settings.content_window = null; - return ( - '
    ' + - '' + - statusHtml + - openBtnHtml + - '
    ' - ); - }, + // TODO: Fix this + settings.root_name = targetElm.nodeName.toLowerCase(); + } - value: function (value) { - if (arguments.length) { - this.state.set('value', value); - return this; - } + // It will not steal focus while setting contentEditable + body = editor.getBody(); + body.disabled = true; + editor.readonly = settings.readonly; - // Make sure the real state is in sync - if (this.state.get('rendered')) { - this.state.set('value', this.getEl('inp').value); + if (!editor.readonly) { + if (editor.inline && DOM.getStyle(body, 'position', true) === 'static') { + body.style.position = 'relative'; } - return this.state.get('value'); - }, + body.contentEditable = editor.getParam('content_editable_state', true); + } - showAutoComplete: function (items, term) { - var self = this; + body.disabled = false; - if (items.length === 0) { - self.hideMenu(); - return; + editor.editorUpload = new EditorUpload(editor); + editor.schema = new Schema(settings); + editor.dom = new DOMUtils(doc, { + keep_values: true, + url_converter: editor.convertURL, + url_converter_scope: editor, + hex_colors: settings.force_hex_style_colors, + class_filter: settings.class_filter, + update_styles: true, + root_element: editor.inline ? editor.getBody() : null, + collect: settings.content_editable, + schema: editor.schema, + onSetAttrib: function (e) { + editor.fire('SetAttrib', e); } + }); - var insert = function (value, title) { - return function () { - self.fire('selectitem', { - title: title, - value: value - }); - }; - }; - - if (self.menu) { - self.menu.items().remove(); - } else { - self.menu = Factory.create({ - type: 'menu', - classes: 'combobox-menu', - layout: 'flow' - }).parent(self).renderTo(); - } - - Tools.each(items, function (item) { - self.menu.add({ - text: item.title, - url: item.previewUrl, - match: term, - classes: 'menu-item-ellipsis', - onclick: insert(item.value, item.title) - }); - }); - - self.menu.renderNew(); - self.hideMenu(); - - self.menu.on('cancel', function (e) { - if (e.control.parent() === self.menu) { - e.stopPropagation(); - self.focus(); - self.hideMenu(); - } - }); - - self.menu.on('select', function () { - self.focus(); - }); - - var maxW = self.layoutRect().w; - self.menu.layoutRect({ w: maxW, minW: 0, maxW: maxW }); - self.menu.reflow(); - self.menu.show(); - self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); - }, - - hideMenu: function () { - if (this.menu) { - this.menu.hide(); - } - }, + editor.parser = createParser(editor); + editor.serializer = new Serializer(settings, editor); + editor.selection = new Selection(editor.dom, editor.getWin(), editor.serializer, editor); + editor.formatter = new Formatter(editor); + editor.undoManager = new UndoManager(editor); + editor._nodeChangeDispatcher = new NodeChange(editor); + editor._selectionOverrides = new SelectionOverrides(editor); - bindStates: function () { - var self = this; + CaretContainerInput.setup(editor); + KeyboardOverrides.setup(editor); + ForceBlocks.setup(editor); - self.state.on('change:value', function (e) { - if (self.getEl('inp').value != e.value) { - self.getEl('inp').value = e.value; - } - }); + editor.fire('PreInit'); - self.state.on('change:disabled', function (e) { - self.getEl('inp').disabled = e.value; - }); + if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { + doc.body.spellcheck = false; // Gecko + DOM.setAttrib(body, "spellcheck", "false"); + } - self.state.on('change:statusLevel', function (e) { - var statusIconElm = self.getEl('status'); - var prefix = self.classPrefix, value = e.value; + editor.quirks = new Quirks(editor); + editor.fire('PostRender'); - DomUtils.css(statusIconElm, 'display', value === 'none' ? 'none' : ''); - DomUtils.toggleClass(statusIconElm, prefix + 'i-checkmark', value === 'ok'); - DomUtils.toggleClass(statusIconElm, prefix + 'i-warning', value === 'warn'); - DomUtils.toggleClass(statusIconElm, prefix + 'i-error', value === 'error'); - self.classes.toggle('has-status', value !== 'none'); - self.repaint(); - }); + if (settings.directionality) { + body.dir = settings.directionality; + } - DomUtils.on(self.getEl('status'), 'mouseleave', function () { - self.tooltip().hide(); - }); + if (settings.nowrap) { + body.style.whiteSpace = "nowrap"; + } - self.on('cancel', function (e) { - if (self.menu && self.menu.visible()) { - e.stopPropagation(); - self.hideMenu(); - } + if (settings.protect) { + editor.on('BeforeSetContent', function (e) { + Tools.each(settings.protect, function (pattern) { + e.content = e.content.replace(pattern, function (str) { + return ''; + }); + }); }); + } - var focusIdx = function (idx, menu) { - if (menu && menu.items().length > 0) { - menu.items().eq(idx)[0].focus(); - } - }; - - self.on('keydown', function (e) { - var keyCode = e.keyCode; + editor.on('SetContent', function () { + editor.addVisual(editor.getBody()); + }); - if (e.target.nodeName === 'INPUT') { - if (keyCode === VK.DOWN) { - e.preventDefault(); - self.fire('autocomplete'); - focusIdx(0, self.menu); - } else if (keyCode === VK.UP) { - e.preventDefault(); - focusIdx(-1, self.menu); - } - } + // Remove empty contents + if (settings.padd_empty_editor) { + editor.on('PostProcess', function (e) { + e.content = e.content.replace(/^(]*>( | |\s|\u00a0|
    |)<\/p>[\r\n]*|
    [\r\n]*)$/, ''); }); - - return self._super(); - }, - - remove: function () { - $(this.getEl('inp')).off(); - - if (this.menu) { - this.menu.remove(); - } - - this._super(); } - }); - } -); -/** - * ColorBox.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 widget lets you enter colors and browse for colors by pressing the color button. It also displays - * a preview of the current color. - * - * @-x-less ColorBox.less - * @class tinymce.ui.ColorBox - * @extends tinymce.ui.ComboBox - */ -define( - 'tinymce.core.ui.ColorBox', - [ - "tinymce.core.ui.ComboBox" - ], - function (ComboBox) { - "use strict"; - - return ComboBox.extend({ - /** - * Constructs a new control instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - */ - init: function (settings) { - var self = this; - settings.spellcheck = false; + editor.load({ initial: true, format: 'html' }); + editor.startContent = editor.getContent({ format: 'raw' }); - if (settings.onaction) { - settings.icon = 'none'; - } + editor.on('compositionstart compositionend', function (e) { + editor.composing = e.type === 'compositionstart'; + }); - self._super(settings); + // Add editor specific CSS styles + if (editor.contentStyles.length > 0) { + contentCssText = ''; - self.classes.add('colorbox'); - self.on('change keyup postrender', function () { - self.repaintColor(self.value()); + Tools.each(editor.contentStyles, function (style) { + contentCssText += style + "\r\n"; }); - }, - repaintColor: function (value) { - var openElm = this.getEl('open'); - var elm = openElm ? openElm.getElementsByTagName('i')[0] : null; + editor.dom.addStyle(contentCssText); + } - if (elm) { - try { - elm.style.background = value; - } catch (ex) { - // Ignore - } + getStyleSheetLoader(editor).loadAll( + editor.contentCSS, + function (_) { + initEditor(editor); + }, + function (urls) { + initEditor(editor); } - }, - - bindStates: function () { - var self = this; - - self.state.on('change:value', function (e) { - if (self.state.get('rendered')) { - self.repaintColor(e.value); - } - }); + ); - return self._super(); + // Append specified content CSS last + if (settings.content_style) { + appendStyle(editor, settings.content_style); } - }); + }; + + return { + initContentBody: initContentBody + }; } ); + /** - * PanelButton.js + * PluginManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -50624,121 +38442,18 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Creates a new panel button. - * - * @class tinymce.ui.PanelButton - * @extends tinymce.ui.Button - */ define( - 'tinymce.core.ui.PanelButton', + 'tinymce.core.PluginManager', [ - "tinymce.core.ui.Button", - "tinymce.core.ui.FloatPanel" + 'tinymce.core.AddOnManager' ], - function (Button, FloatPanel) { - "use strict"; - - return Button.extend({ - /** - * Shows the panel for the button. - * - * @method showPanel - */ - showPanel: function () { - var self = this, settings = self.settings; - - self.active(true); - - if (!self.panel) { - var panelSettings = settings.panel; - - // Wrap panel in grid layout if type if specified - // This makes it possible to add forms or other containers directly in the panel option - if (panelSettings.type) { - panelSettings = { - layout: 'grid', - items: panelSettings - }; - } - - panelSettings.role = panelSettings.role || 'dialog'; - panelSettings.popover = true; - panelSettings.autohide = true; - panelSettings.ariaRoot = true; - - self.panel = new FloatPanel(panelSettings).on('hide', function () { - self.active(false); - }).on('cancel', function (e) { - e.stopPropagation(); - self.focus(); - self.hidePanel(); - }).parent(self).renderTo(self.getContainerElm()); - - self.panel.fire('show'); - self.panel.reflow(); - } else { - self.panel.show(); - } - - var rel = self.panel.testMoveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tc', 'bc-tl', 'bc-tr'] : ['bc-tc', 'bc-tr', 'bc-tl'])); - - self.panel.classes.toggle('start', rel === 'bc-tl'); - self.panel.classes.toggle('end', rel === 'bc-tr'); - - self.panel.moveRel(self.getEl(), rel); - }, - - /** - * Hides the panel for the button. - * - * @method hidePanel - */ - hidePanel: function () { - var self = this; - - if (self.panel) { - self.panel.hide(); - } - }, - - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this; - - self.aria('haspopup', true); - - self.on('click', function (e) { - if (e.control === self) { - if (self.panel && self.panel.visible()) { - self.hidePanel(); - } else { - self.showPanel(); - self.panel.focus(!!e.aria); - } - } - }); - - return self._super(); - }, - - remove: function () { - if (this.panel) { - this.panel.remove(); - this.panel = null; - } - - return this._super(); - } - }); + function (AddOnManager) { + return AddOnManager.PluginManager; } ); + /** - * ColorButton.js + * ThemeManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -50747,127 +38462,18 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class creates a color button control. This is a split button in which the main - * button has a visual representation of the currently selected color. When clicked - * the caret button displays a color picker, allowing the user to select a new color. - * - * @-x-less ColorButton.less - * @class tinymce.ui.ColorButton - * @extends tinymce.ui.PanelButton - */ define( - 'tinymce.core.ui.ColorButton', + 'tinymce.core.ThemeManager', [ - "tinymce.core.ui.PanelButton", - "tinymce.core.dom.DOMUtils" + 'tinymce.core.AddOnManager' ], - function (PanelButton, DomUtils) { - "use strict"; - - var DOM = DomUtils.DOM; - - return PanelButton.extend({ - /** - * Constructs a new ColorButton instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - */ - init: function (settings) { - this._super(settings); - this.classes.add('colorbutton'); - }, - - /** - * Getter/setter for the current color. - * - * @method color - * @param {String} [color] Color to set. - * @return {String|tinymce.ui.ColorButton} Current color or current instance. - */ - color: function (color) { - if (color) { - this._color = color; - this.getEl('preview').style.backgroundColor = color; - return this; - } - - return this._color; - }, - - /** - * Resets the current color. - * - * @method resetColor - * @return {tinymce.ui.ColorButton} Current instance. - */ - resetColor: function () { - this._color = null; - this.getEl('preview').style.backgroundColor = null; - return this; - }, - - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, id = self._id, prefix = self.classPrefix, text = self.state.get('text'); - var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : ''; - var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '', - textHtml = ''; - - if (text) { - self.classes.add('btn-has-text'); - textHtml = '' + self.encode(text) + ''; - } - - return ( - '
    ' + - '' + - '' + - '
    ' - ); - }, - - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this, onClickHandler = self.settings.onclick; - - self.on('click', function (e) { - if (e.aria && e.aria.key == 'down') { - return; - } - - if (e.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) { - e.stopImmediatePropagation(); - onClickHandler.call(self, e); - } - }); - - delete self.settings.onclick; - - return self._super(); - } - }); + function (AddOnManager) { + return AddOnManager.ThemeManager; } ); /** - * Color.js + * Init.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -50876,449 +38482,305 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * This class lets you parse/serialize colors and convert rgb/hsb. - * - * @class tinymce.util.Color - * @example - * var white = new tinymce.util.Color({r: 255, g: 255, b: 255}); - * var red = new tinymce.util.Color('#FF0000'); - * - * console.log(white.toHex(), red.toHsv()); - */ -define( - 'tinymce.core.util.Color', - [ - ], - function () { - var min = Math.min, max = Math.max, round = Math.round; - - /** - * Constructs a new color instance. - * - * @constructor - * @method Color - * @param {String} value Optional initial value to parse. - */ - function Color(value) { - var self = this, r = 0, g = 0, b = 0; - - function rgb2hsv(r, g, b) { - var h, s, v, d, minRGB, maxRGB; - - h = 0; - s = 0; - v = 0; - r = r / 255; - g = g / 255; - b = b / 255; - - minRGB = min(r, min(g, b)); - maxRGB = max(r, max(g, b)); - - if (minRGB == maxRGB) { - v = minRGB; - - return { - h: 0, - s: 0, - v: v * 100 - }; - } - - /*eslint no-nested-ternary:0 */ - d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r); - h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5); - h = 60 * (h - d / (maxRGB - minRGB)); - s = (maxRGB - minRGB) / maxRGB; - v = maxRGB; - - return { - h: round(h), - s: round(s * 100), - v: round(v * 100) - }; - } +define( + 'tinymce.core.init.Init', + [ + 'ephox.katamari.api.Type', + 'global!document', + 'global!window', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.Env', + 'tinymce.core.init.InitContentBody', + 'tinymce.core.PluginManager', + 'tinymce.core.ThemeManager', + 'tinymce.core.util.Tools', + 'tinymce.core.util.Uuid' + ], + function (Type, document, window, DOMUtils, Env, InitContentBody, PluginManager, ThemeManager, Tools, Uuid) { + var DOM = DOMUtils.DOM; - function hsvToRgb(hue, saturation, brightness) { - var side, chroma, x, match; + var initPlugin = function (editor, initializedPlugins, plugin) { + var Plugin = PluginManager.get(plugin), pluginUrl, pluginInstance; - hue = (parseInt(hue, 10) || 0) % 360; - saturation = parseInt(saturation, 10) / 100; - brightness = parseInt(brightness, 10) / 100; - saturation = max(0, min(saturation, 1)); - brightness = max(0, min(brightness, 1)); + pluginUrl = PluginManager.urls[plugin] || editor.documentBaseUrl.replace(/\/$/, ''); + plugin = Tools.trim(plugin); + if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) { + Tools.each(PluginManager.dependencies(plugin), function (dep) { + initPlugin(editor, initializedPlugins, dep); + }); - if (saturation === 0) { - r = g = b = round(255 * brightness); + if (editor.plugins[plugin]) { return; } - side = hue / 60; - chroma = brightness * saturation; - x = chroma * (1 - Math.abs(side % 2 - 1)); - match = brightness - chroma; + pluginInstance = new Plugin(editor, pluginUrl, editor.$); - switch (Math.floor(side)) { - case 0: - r = chroma; - g = x; - b = 0; - break; + editor.plugins[plugin] = pluginInstance; - case 1: - r = x; - g = chroma; - b = 0; - break; + if (pluginInstance.init) { + pluginInstance.init(editor, pluginUrl); + initializedPlugins.push(plugin); + } + } + }; - case 2: - r = 0; - g = chroma; - b = x; - break; + var trimLegacyPrefix = function (name) { + // Themes and plugins can be prefixed with - to prevent them from being lazy loaded + return name.replace(/^\-/, ''); + }; - case 3: - r = 0; - g = x; - b = chroma; - break; + var initPlugins = function (editor) { + var initializedPlugins = []; - case 4: - r = x; - g = 0; - b = chroma; - break; + Tools.each(editor.settings.plugins.split(/[ ,]/), function (name) { + initPlugin(editor, initializedPlugins, trimLegacyPrefix(name)); + }); + }; - case 5: - r = chroma; - g = 0; - b = x; - break; + var initTheme = function (editor) { + var Theme, theme = editor.settings.theme; - default: - r = g = b = 0; - } + if (Type.isString(theme)) { + editor.settings.theme = trimLegacyPrefix(theme); - r = round(255 * (r + match)); - g = round(255 * (g + match)); - b = round(255 * (b + match)); + Theme = ThemeManager.get(theme); + editor.theme = new Theme(editor, ThemeManager.urls[theme]); + + if (editor.theme.init) { + editor.theme.init(editor, ThemeManager.urls[theme] || editor.documentBaseUrl.replace(/\/$/, ''), editor.$); + } + } else { + // Theme set to false or null doesn't produce a theme api + editor.theme = {}; } + }; - /** - * Returns the hex string of the current color. For example: #ff00ff - * - * @method toHex - * @return {String} Hex string of current color. - */ - function toHex() { - function hex(val) { - val = parseInt(val, 10).toString(16); + var renderFromLoadedTheme = function (editor) { + var w, h, minHeight, re, info, settings = editor.settings, elm = editor.getElement(); - return val.length > 1 ? val : '0' + val; - } + w = settings.width || DOM.getStyle(elm, 'width') || '100%'; + h = settings.height || DOM.getStyle(elm, 'height') || elm.offsetHeight; + minHeight = settings.min_height || 100; + re = /^[0-9\.]+(|px)$/i; - return '#' + hex(r) + hex(g) + hex(b); + if (re.test('' + w)) { + w = Math.max(parseInt(w, 10), 100); } - /** - * Returns the r, g, b values of the color. Each channel has a range from 0-255. - * - * @method toRgb - * @return {Object} Object with r, g, b fields. - */ - function toRgb() { - return { - r: r, - g: g, - b: b - }; + if (re.test('' + h)) { + h = Math.max(parseInt(h, 10), minHeight); } - /** - * Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100. - * - * @method toHsv - * @return {Object} Object with h, s, v fields. - */ - function toHsv() { - return rgb2hsv(r, g, b); + // Render UI + info = editor.theme.renderUI({ + targetNode: elm, + width: w, + height: h, + deltaWidth: settings.delta_width, + deltaHeight: settings.delta_height + }); + + // Resize editor + if (!settings.content_editable) { + h = (info.iframeHeight || h) + (typeof h === 'number' ? (info.deltaHeight || 0) : ''); + if (h < minHeight) { + h = minHeight; + } } - /** - * Parses the specified value and populates the color instance. - * - * Supported format examples: - * * rbg(255,0,0) - * * #ff0000 - * * #fff - * * {r: 255, g: 0, b: 0} - * * {h: 360, s: 100, v: 100} - * - * @method parse - * @param {Object/String} value Color value to parse. - * @return {tinymce.util.Color} Current color instance. - */ - function parse(value) { - var matches; + editor.editorContainer = info.editorContainer; + info.height = h; - if (typeof value == 'object') { - if ("r" in value) { - r = value.r; - g = value.g; - b = value.b; - } else if ("v" in value) { - hsvToRgb(value.h, value.s, value.v); - } - } else { - if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) { - r = parseInt(matches[1], 10); - g = parseInt(matches[2], 10); - b = parseInt(matches[3], 10); - } else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) { - r = parseInt(matches[1], 16); - g = parseInt(matches[2], 16); - b = parseInt(matches[3], 16); - } else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) { - r = parseInt(matches[1] + matches[1], 16); - g = parseInt(matches[2] + matches[2], 16); - b = parseInt(matches[3] + matches[3], 16); - } - } + return info; + }; - r = r < 0 ? 0 : (r > 255 ? 255 : r); - g = g < 0 ? 0 : (g > 255 ? 255 : g); - b = b < 0 ? 0 : (b > 255 ? 255 : b); + var renderFromThemeFunc = function (editor) { + var info, elm = editor.getElement(); - return self; + info = editor.settings.theme(editor, elm); + + if (info.editorContainer.nodeType) { + info.editorContainer.id = info.editorContainer.id || editor.id + "_parent"; } - if (value) { - parse(value); + if (info.iframeContainer.nodeType) { + info.iframeContainer.id = info.iframeContainer.id || editor.id + "_iframecontainer"; } - self.toRgb = toRgb; - self.toHsv = toHsv; - self.toHex = toHex; - self.parse = parse; - } + info.height = info.iframeHeight ? info.iframeHeight : elm.offsetHeight; + editor.editorContainer = info.editorContainer; - return Color; - } -); + return info; + }; -/** - * ColorPicker.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 - */ + var renderThemeFalse = function (editor) { + var iframeContainer = DOM.create('div'); -/** - * Color picker widget lets you select colors. - * - * @-x-less ColorPicker.less - * @class tinymce.ui.ColorPicker - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.ColorPicker', - [ - "tinymce.core.ui.Widget", - "tinymce.core.ui.DragHelper", - "tinymce.core.ui.DomUtils", - "tinymce.core.util.Color" - ], - function (Widget, DragHelper, DomUtils, Color) { - "use strict"; + DOM.insertAfter(iframeContainer, editor.getElement()); - return Widget.extend({ - Defaults: { - classes: "widget colorpicker" - }, + editor.editorContainer = iframeContainer; - /** - * Constructs a new colorpicker instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {String} color Initial color value. - */ - init: function (settings) { - this._super(settings); - }, + return { + editorContainer: iframeContainer, + iframeContainer: iframeContainer + }; + }; - postRender: function () { - var self = this, color = self.color(), hsv, hueRootElm, huePointElm, svRootElm, svPointElm; + var renderThemeUi = function (editor) { + var settings = editor.settings, elm = editor.getElement(); - hueRootElm = self.getEl('h'); - huePointElm = self.getEl('hp'); - svRootElm = self.getEl('sv'); - svPointElm = self.getEl('svp'); + editor.orgDisplay = elm.style.display; - function getPos(elm, event) { - var pos = DomUtils.getPos(elm), x, y; + if (Type.isString(settings.theme)) { + return renderFromLoadedTheme(editor); + } else if (Type.isFunction(settings.theme)) { + return renderFromThemeFunc(editor); + } else { + return renderThemeFalse(editor); + } + }; - x = event.pageX - pos.x; - y = event.pageY - pos.y; + var relaxDomain = function (editor, ifr) { + // Domain relaxing is required since the user has messed around with document.domain + // This only applies to IE 11 other browsers including Edge seems to handle document.domain + if (document.domain !== window.location.hostname && Env.ie && Env.ie < 12) { + var bodyUuid = Uuid.uuid('mce'); - x = Math.max(0, Math.min(x / elm.clientWidth, 1)); - y = Math.max(0, Math.min(y / elm.clientHeight, 1)); + editor[bodyUuid] = function () { + InitContentBody.initContentBody(editor); + }; - return { - x: x, - y: y - }; - } + /*eslint no-script-url:0 */ + var domainRelaxUrl = 'javascript:(function(){' + + 'document.open();document.domain="' + document.domain + '";' + + 'var ed = window.parent.tinymce.get("' + editor.id + '");document.write(ed.iframeHTML);' + + 'document.close();ed.' + bodyUuid + '(true);})()'; - function updateColor(hsv, hueUpdate) { - var hue = (360 - hsv.h) / 360; + DOM.setAttrib(ifr, 'src', domainRelaxUrl); + return true; + } - DomUtils.css(huePointElm, { - top: (hue * 100) + '%' - }); + return false; + }; - if (!hueUpdate) { - DomUtils.css(svPointElm, { - left: hsv.s + '%', - top: (100 - hsv.v) + '%' - }); - } + var createIframe = function (editor, o) { + var settings = editor.settings, bodyId, bodyClass; - svRootElm.style.background = new Color({ s: 100, v: 100, h: hsv.h }).toHex(); - self.color().parse({ s: hsv.s, v: hsv.v, h: hsv.h }); - } + editor.iframeHTML = settings.doctype + ''; - function updateSaturationAndValue(e) { - var pos; + // We only need to override paths if we have to + // IE has a bug where it remove site absolute urls to relative ones if this is specified + if (settings.document_base_url != editor.documentBaseUrl) { + editor.iframeHTML += ''; + } - pos = getPos(svRootElm, e); - hsv.s = pos.x * 100; - hsv.v = (1 - pos.y) * 100; + // IE8 doesn't support carets behind images setting ie7_compat would force IE8+ to run in IE7 compat mode. + if (!Env.caretAfter && settings.ie7_compat) { + editor.iframeHTML += ''; + } - updateColor(hsv); - self.fire('change'); - } + editor.iframeHTML += ''; - function updateHue(e) { - var pos; + bodyId = settings.body_id || 'tinymce'; + if (bodyId.indexOf('=') != -1) { + bodyId = editor.getParam('body_id', '', 'hash'); + bodyId = bodyId[editor.id] || bodyId; + } - pos = getPos(hueRootElm, e); - hsv = color.toHsv(); - hsv.h = (1 - pos.y) * 360; - updateColor(hsv, true); - self.fire('change'); - } + bodyClass = settings.body_class || ''; + if (bodyClass.indexOf('=') != -1) { + bodyClass = editor.getParam('body_class', '', 'hash'); + bodyClass = bodyClass[editor.id] || ''; + } - self._repaint = function () { - hsv = color.toHsv(); - updateColor(hsv); - }; + if (settings.content_security_policy) { + editor.iframeHTML += ''; + } - self._super(); + editor.iframeHTML += '
    '; - self._svdraghelper = new DragHelper(self._id + '-sv', { - start: updateSaturationAndValue, - drag: updateSaturationAndValue - }); + // Create iframe + // TODO: ACC add the appropriate description on this. + var ifr = DOM.create('iframe', { + id: editor.id + "_ifr", + frameBorder: '0', + allowTransparency: "true", + title: editor.editorManager.translate( + "Rich Text Area. Press ALT-F9 for menu. " + + "Press ALT-F10 for toolbar. Press ALT-0 for help" + ), + style: { + width: '100%', + height: o.height, + display: 'block' // Important for Gecko to render the iframe correctly + } + }); - self._hdraghelper = new DragHelper(self._id + '-h', { - start: updateHue, - drag: updateHue - }); + ifr.onload = function () { + ifr.onload = null; + editor.fire("load"); + }; - self._repaint(); - }, + var isDomainRelaxed = relaxDomain(editor, ifr); - rgb: function () { - return this.color().toRgb(); - }, + editor.contentAreaContainer = o.iframeContainer; + editor.iframeElement = ifr; - value: function (value) { - var self = this; + DOM.add(o.iframeContainer, ifr); - if (arguments.length) { - self.color().parse(value); + return isDomainRelaxed; + }; - if (self._rendered) { - self._repaint(); - } - } else { - return self.color().toHex(); - } - }, + var init = function (editor) { + var settings = editor.settings, elm = editor.getElement(), boxInfo; - color: function () { - if (!this._color) { - this._color = new Color(); - } + editor.rtl = settings.rtl_ui || editor.editorManager.i18n.rtl; + editor.editorManager.i18n.setCode(settings.language); + settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', editor.getLang('aria.rich_text_area')); - return this._color; - }, + editor.fire('ScriptsLoaded'); - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, id = self._id, prefix = self.classPrefix, hueHtml; - var stops = '#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000'; + initTheme(editor); + initPlugins(editor); + boxInfo = renderThemeUi(editor); - function getOldIeFallbackHtml() { - var i, l, html = '', gradientPrefix, stopsList; + // Load specified content CSS last + if (settings.content_css) { + Tools.each(Tools.explode(settings.content_css), function (u) { + editor.contentCSS.push(editor.documentBaseURI.toAbsolute(u)); + }); + } - gradientPrefix = 'filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='; - stopsList = stops.split(','); - for (i = 0, l = stopsList.length - 1; i < l; i++) { - html += ( - '
    ' - ); - } + // Content editable mode ends here + if (settings.content_editable) { + return InitContentBody.initContentBody(editor); + } - return html; - } + var isDomainRelaxed = createIframe(editor, boxInfo); - var gradientCssText = ( - 'background: -ms-linear-gradient(top,' + stops + ');' + - 'background: linear-gradient(to bottom,' + stops + ');' - ); + if (boxInfo.editorContainer) { + DOM.get(boxInfo.editorContainer).style.display = editor.orgDisplay; + editor.hidden = DOM.isHidden(boxInfo.editorContainer); + } - hueHtml = ( - '
    ' + - getOldIeFallbackHtml() + - '
    ' + - '
    ' - ); + editor.getElement().style.display = 'none'; + DOM.setAttrib(editor.id, 'aria-hidden', true); - return ( - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - hueHtml + - '
    ' - ); + if (!isDomainRelaxed) { + InitContentBody.initContentBody(editor); } - }); + }; + + return { + init: init + }; } ); + /** - * Path.js + * Render.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -51327,130 +38789,232 @@ define( * Contributing: http://www.tinymce.com/contributing */ -/** - * Creates a new path control. - * - * @-x-less Path.less - * @class tinymce.ui.Path - * @extends tinymce.ui.Widget - */ define( - 'tinymce.core.ui.Path', + 'tinymce.core.init.Render', [ - "tinymce.core.ui.Widget" + 'ephox.katamari.api.Type', + '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.PluginManager', + 'tinymce.core.ThemeManager', + 'tinymce.core.util.Tools' ], - function (Widget) { - "use strict"; + function (Type, window, NotificationManager, WindowManager, DOMUtils, EventUtils, ScriptLoader, Env, ErrorReporter, Init, PluginManager, ThemeManager, Tools) { + var DOM = DOMUtils.DOM; - return Widget.extend({ - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {String} delimiter Delimiter to display between row in path. - */ - init: function (settings) { - var self = this; + var hasSkipLoadPrefix = function (name) { + return name.charAt(0) === '-'; + }; + + var loadLanguage = function (scriptLoader, editor) { + var settings = editor.settings; + + if (settings.language && settings.language !== 'en' && !settings.language_url) { + settings.language_url = editor.editorManager.baseURL + '/langs/' + settings.language + '.js'; + } + + if (settings.language_url) { + scriptLoader.add(settings.language_url); + } + }; + + var loadTheme = function (scriptLoader, editor, suffix, callback) { + var settings = editor.settings, theme = settings.theme; + + if (Type.isString(theme)) { + if (!hasSkipLoadPrefix(theme) && !ThemeManager.urls.hasOwnProperty(theme)) { + var themeUrl = settings.theme_url; + + if (themeUrl) { + ThemeManager.load(theme, editor.documentBaseURI.toAbsolute(themeUrl)); + } else { + ThemeManager.load(theme, 'themes/' + theme + '/theme' + suffix + '.js'); + } + } + + scriptLoader.loadQueue(function () { + ThemeManager.waitFor(theme, callback); + }); + } else { + callback(); + } + }; + + var loadPlugins = function (settings, suffix) { + if (Tools.isArray(settings.plugins)) { + settings.plugins = settings.plugins.join(' '); + } + + Tools.each(settings.external_plugins, function (url, name) { + PluginManager.load(name, url); + settings.plugins += ' ' + name; + }); + + Tools.each(settings.plugins.split(/[ ,]/), function (plugin) { + plugin = Tools.trim(plugin); + + if (plugin && !PluginManager.urls[plugin]) { + if (hasSkipLoadPrefix(plugin)) { + plugin = plugin.substr(1, plugin.length); + + var dependencies = PluginManager.dependencies(plugin); + + Tools.each(dependencies, function (dep) { + var defaultSettings = { + prefix: 'plugins/', + resource: dep, + suffix: '/plugin' + suffix + '.js' + }; - if (!settings.delimiter) { - settings.delimiter = '\u00BB'; + dep = PluginManager.createUrl(defaultSettings, dep); + PluginManager.load(dep.resource, dep); + }); + } else { + PluginManager.load(plugin, { + prefix: 'plugins/', + resource: plugin, + suffix: '/plugin' + suffix + '.js' + }); + } } + }); + }; + + var loadScripts = function (editor, suffix) { + var scriptLoader = ScriptLoader.ScriptLoader; - self._super(settings); - self.classes.add('path'); - self.canFocus = true; + loadTheme(scriptLoader, editor, suffix, function () { + loadLanguage(scriptLoader, editor); + loadPlugins(editor.settings, suffix); - self.on('click', function (e) { - var index, target = e.target; + scriptLoader.loadQueue(function () { + if (!editor.removed) { + Init.init(editor); + } + }, editor, function (urls) { + ErrorReporter.pluginLoadError(editor, urls[0]); - if ((index = target.getAttribute('data-index'))) { - self.fire('select', { value: self.row()[index], index: index }); + if (!editor.removed) { + Init.init(editor); } }); + }); + }; - self.row(self.settings.row); - }, + var render = function (editor) { + var settings = editor.settings, id = editor.id; - /** - * Focuses the current control. - * - * @method focus - * @return {tinymce.ui.Control} Current control instance. - */ - focus: function () { - var self = this; + function readyHandler() { + DOM.unbind(window, 'ready', readyHandler); + editor.render(); + } - self.getEl().firstChild.focus(); + // Page is not loaded yet, wait for it + if (!EventUtils.Event.domLoaded) { + DOM.bind(window, 'ready', readyHandler); + return; + } - return self; - }, + // Element not found, then skip initialization + if (!editor.getElement()) { + return; + } - /** - * Sets/gets the data to be used for the path. - * - * @method row - * @param {Array} row Array with row name is rendered to path. - */ - row: function (row) { - if (!arguments.length) { - return this.state.get('row'); + // No editable support old iOS versions etc + if (!Env.contentEditable) { + return; + } + + // Hide target element early to prevent content flashing + if (!settings.inline) { + editor.orgVisibility = editor.getElement().style.visibility; + editor.getElement().style.visibility = 'hidden'; + } else { + editor.inline = true; + } + + var form = editor.getElement().form || DOM.getParent(id, 'form'); + if (form) { + editor.formElement = form; + + // Add hidden input for non input elements inside form elements + if (settings.hidden_input && !/TEXTAREA|INPUT/i.test(editor.getElement().nodeName)) { + DOM.insertAfter(DOM.create('input', { type: 'hidden', name: id }), id); + editor.hasHiddenInput = true; } - this.state.set('row', row); + // Pass submit/reset from form to editor instance + editor.formEventDelegate = function (e) { + editor.fire(e.type, e); + }; + + DOM.bind(form, 'submit reset', editor.formEventDelegate); - return this; - }, + // Reset contents in editor when the form is reset + editor.on('reset', function () { + editor.setContent(editor.startContent, { format: 'raw' }); + }); - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this; + // Check page uses id="submit" or name="submit" for it's submit button + if (settings.submit_patch && !form.submit.nodeType && !form.submit.length && !form._mceOldSubmit) { + form._mceOldSubmit = form.submit; + form.submit = function () { + editor.editorManager.triggerSave(); + editor.setDirty(false); - return ( - '
    ' + - self._getDataPathHtml(self.state.get('row')) + - '
    ' - ); - }, + return form._mceOldSubmit(form); + }; + } + } - bindStates: function () { - var self = this; + editor.windowManager = new WindowManager(editor); + editor.notificationManager = new NotificationManager(editor); - self.state.on('change:row', function (e) { - self.innerHtml(self._getDataPathHtml(e.value)); + if (settings.encoding === 'xml') { + editor.on('GetContent', function (e) { + if (e.save) { + e.content = DOM.encode(e.content); + } }); + } - return self._super(); - }, + if (settings.add_form_submit_trigger) { + editor.on('submit', function () { + if (editor.initialized) { + editor.save(); + } + }); + } - _getDataPathHtml: function (data) { - var self = this, parts = data || [], i, l, html = '', prefix = self.classPrefix; + if (settings.add_unload_trigger) { + editor._beforeUnload = function () { + if (editor.initialized && !editor.destroyed && !editor.isHidden()) { + editor.save({ format: 'raw', no_events: true, set_dirty: false }); + } + }; - for (i = 0, l = parts.length; i < l; i++) { - html += ( - (i > 0 ? '' : '') + - '
    ' + parts[i].name + '
    ' - ); - } + editor.editorManager.on('BeforeUnload', editor._beforeUnload); + } - if (!html) { - html = '
    \u00a0
    '; - } + editor.editorManager.add(editor); + loadScripts(editor, editor.suffix); + }; - return html; - } - }); + return { + render: render + }; } ); /** - * ElementPath.js + * Mode.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -51460,79 +39024,92 @@ define( */ /** - * This control creates an path for the current selections parent elements in TinyMCE. + * Mode switcher logic. * - * @class tinymce.ui.ElementPath - * @extends tinymce.ui.Path + * @private + * @class tinymce.Mode */ define( - 'tinymce.core.ui.ElementPath', + 'tinymce.core.Mode', [ - "tinymce.core.ui.Path" ], - function (Path) { - return Path.extend({ - /** - * Post render method. Called after the control has been rendered to the target. - * - * @method postRender - * @return {tinymce.ui.ElementPath} Current combobox instance. - */ - postRender: function () { - var self = this, editor = self.settings.editor; + function () { + function setEditorCommandState(editor, cmd, state) { + try { + editor.getDoc().execCommand(cmd, false, state); + } catch (ex) { + // Ignore + } + } - function isHidden(elm) { - if (elm.nodeType === 1) { - if (elm.nodeName == "BR" || !!elm.getAttribute('data-mce-bogus')) { - return true; - } + function clickBlocker(editor) { + var target, handler; - if (elm.getAttribute('data-mce-type') === 'bookmark') { - return true; - } - } + target = editor.getBody(); - return false; + handler = function (e) { + if (editor.dom.getParents(e.target, 'a').length > 0) { + e.preventDefault(); } + }; - if (editor.settings.elementpath !== false) { - self.on('select', function (e) { - editor.focus(); - editor.selection.select(this.row()[e.index].element); - editor.nodeChanged(); - }); + editor.dom.bind(target, 'click', handler); - editor.on('nodeChange', function (e) { - var outParents = [], parents = e.parents, i = parents.length; + return { + unbind: function () { + editor.dom.unbind(target, 'click', handler); + } + }; + } - while (i--) { - if (parents[i].nodeType == 1 && !isHidden(parents[i])) { - var args = editor.fire('ResolveName', { - name: parents[i].nodeName.toLowerCase(), - target: parents[i] - }); + function toggleReadOnly(editor, state) { + if (editor._clickBlocker) { + editor._clickBlocker.unbind(); + editor._clickBlocker = null; + } - if (!args.isDefaultPrevented()) { - outParents.push({ name: args.name, element: parents[i] }); - } + if (state) { + editor._clickBlocker = clickBlocker(editor); + editor.selection.controlSelection.hideResizeRect(); + editor.readonly = true; + editor.getBody().contentEditable = false; + } else { + editor.readonly = false; + editor.getBody().contentEditable = true; + setEditorCommandState(editor, "StyleWithCSS", false); + setEditorCommandState(editor, "enableInlineTableEditing", false); + setEditorCommandState(editor, "enableObjectResizing", false); + editor.focus(); + editor.nodeChanged(); + } + } - if (args.isPropagationStopped()) { - break; - } - } - } + function setMode(editor, mode) { + var currentMode = editor.readonly ? 'readonly' : 'design'; - self.row(outParents); - }); - } + if (mode == currentMode) { + return; + } - return self._super(); + if (editor.initialized) { + toggleReadOnly(editor, mode == 'readonly'); + } else { + editor.on('init', function () { + toggleReadOnly(editor, mode == 'readonly'); + }); } - }); + + // Event is NOT preventable + editor.fire('SwitchMode', { mode: mode }); + } + + return { + setMode: setMode + }; } ); /** - * FormItem.js + * Shortcuts.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -51542,281 +39119,218 @@ define( */ /** - * This class is a container created by the form element with - * a label and control item. + * Contains logic for handling keyboard shortcuts. * - * @class tinymce.ui.FormItem - * @extends tinymce.ui.Container - * @setting {String} label Label to display for the form item. + * @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.ui.FormItem', + 'tinymce.core.Shortcuts', [ - "tinymce.core.ui.Container" + 'tinymce.core.util.Tools', + 'tinymce.core.Env' ], - function (Container) { - "use strict"; + function (Tools, Env) { + var each = Tools.each, explode = Tools.explode; - return Container.extend({ - Defaults: { - layout: 'flex', - align: 'center', - defaults: { - flex: 1 - } - }, + var keyCodeLookup = { + "f9": 120, + "f10": 121, + "f11": 122 + }; - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, layout = self._layout, prefix = self.classPrefix; + var modifierNames = Tools.makeMap('alt,ctrl,shift,meta,access'); - self.classes.add('formitem'); - layout.preRender(self); + return function (editor) { + var self = this, shortcuts = {}, pendingPatterns = []; - return ( - '
    ' + - (self.settings.title ? ('
    ' + - self.settings.title + '
    ') : '') + - '
    ' + - (self.settings.html || '') + layout.renderHtml(self) + - '
    ' + - '
    ' - ); - } - }); - } -); -/** - * Form.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 - */ + function parseShortcut(pattern) { + var id, key, shortcut = {}; -/** - * This class creates a form container. A form container has the ability - * to automatically wrap items in tinymce.ui.FormItem instances. - * - * Each FormItem instance is a container for the label and the item. - * - * @example - * tinymce.ui.Factory.create({ - * type: 'form', - * items: [ - * {type: 'textbox', label: 'My text box'} - * ] - * }).renderTo(document.body); - * - * @class tinymce.ui.Form - * @extends tinymce.ui.Container - */ -define( - 'tinymce.core.ui.Form', - [ - "tinymce.core.ui.Container", - "tinymce.core.ui.FormItem", - "tinymce.core.util.Tools" - ], - function (Container, FormItem, Tools) { - "use strict"; + // 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); + } + } + }); - return Container.extend({ - Defaults: { - containerCls: 'form', - layout: 'flex', - direction: 'column', - align: 'stretch', - flex: 1, - padding: 20, - labelGap: 30, - spacing: 10, - callbacks: { - submit: function () { - this.submit(); + // 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(','); - /** - * This method gets invoked before the control is rendered. - * - * @method preRender - */ - preRender: function () { - var self = this, items = self.items(); + // Handle special access modifier differently depending on Mac/Win + if (shortcut.access) { + shortcut.alt = true; - if (!self.settings.formItemDefaults) { - self.settings.formItemDefaults = { - layout: 'flex', - autoResize: "overflow", - defaults: { flex: 1 } - }; + if (Env.mac) { + shortcut.ctrl = true; + } else { + shortcut.shift = true; + } } - // Wrap any labeled items in FormItems - items.each(function (ctrl) { - var formItem, label = ctrl.settings.label; + // 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; + } + } - if (label) { - formItem = new FormItem(Tools.extend({ - items: { - type: 'label', - id: ctrl._id + '-l', - text: label, - flex: 0, - forId: ctrl._id, - disabled: ctrl.disabled() - } - }, self.settings.formItemDefaults)); + return shortcut; + } - formItem.type = 'formitem'; - ctrl.aria('labelledby', ctrl._id + '-l'); + function createShortcut(pattern, desc, cmdFunc, scope) { + var shortcuts; - if (typeof ctrl.settings.flex == "undefined") { - ctrl.settings.flex = 1; - } + shortcuts = Tools.map(explode(pattern, '>'), parseShortcut); + shortcuts[shortcuts.length - 1] = Tools.extend(shortcuts[shortcuts.length - 1], { + func: cmdFunc, + scope: scope || editor + }); - self.replace(ctrl, formItem); - formItem.add(ctrl); - } + return Tools.extend(shortcuts[0], { + desc: editor.translate(desc), + subpatterns: shortcuts.slice(1) }); - }, + } - /** - * Fires a submit event with the serialized form. - * - * @method submit - * @return {Object} Event arguments object. - */ - submit: function () { - return this.fire('submit', { data: this.toJSON() }); - }, + function hasModifier(e) { + return e.altKey || e.ctrlKey || e.metaKey; + } - /** - * Post render method. Called after the control has been rendered to the target. - * - * @method postRender - * @return {tinymce.ui.ComboBox} Current combobox instance. - */ - postRender: function () { - var self = this; + function isFunctionKey(e) { + return e.type === "keydown" && e.keyCode >= 112 && e.keyCode <= 123; + } - self._super(); - self.fromJSON(self.settings.data); - }, + function matchShortcut(e, shortcut) { + if (!shortcut) { + return false; + } - bindStates: function () { - var self = this; + if (shortcut.ctrl != e.ctrlKey || shortcut.meta != e.metaKey) { + return false; + } + + if (shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) { + return false; + } - self._super(); + if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) { + e.preventDefault(); + return true; + } - function recalcLabels() { - var maxLabelWidth = 0, labels = [], i, labelGap, items; + return false; + } - if (self.settings.labelGapCalc === false) { - return; - } + function executeShortcutAction(shortcut) { + return shortcut.func ? shortcut.func.call(shortcut.scope) : null; + } - if (self.settings.labelGapCalc == "children") { - items = self.find('formitem'); - } else { - items = self.items(); - } + 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); - items.filter('formitem').each(function (item) { - var labelCtrl = item.items()[0], labelWidth = labelCtrl.getEl().clientWidth; + if (e.type == "keydown") { + executeShortcutAction(shortcut); + } - maxLabelWidth = labelWidth > maxLabelWidth ? labelWidth : maxLabelWidth; - labels.push(labelCtrl); + return true; + } }); - labelGap = self.settings.labelGap || 0; + if (matchShortcut(e, pendingPatterns[0])) { + if (pendingPatterns.length === 1) { + if (e.type == "keydown") { + executeShortcutAction(pendingPatterns[0]); + } + } - i = labels.length; - while (i--) { - labels[i].settings.minWidth = maxLabelWidth + labelGap; + pendingPatterns.shift(); } } + }); - self.on('show', recalcLabels); - recalcLabels(); - } - }); - } -); -/** - * FieldSet.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 - */ + /** + * 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; -/** - * This class creates fieldset containers. - * - * @-x-less FieldSet.less - * @class tinymce.ui.FieldSet - * @extends tinymce.ui.Form - */ -define( - 'tinymce.core.ui.FieldSet', - [ - "tinymce.core.ui.Form" - ], - function (Form) { - "use strict"; + cmd = cmdFunc; - return Form.extend({ - Defaults: { - containerCls: 'fieldset', - layout: 'flex', - direction: 'column', - align: 'stretch', - flex: 1, - padding: "25 15 5 15", - labelGap: 30, - spacing: 10, - border: 1 - }, + 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; + }; /** - * Renders the control as a HTML string. + * Remove a keyboard shortcut by pattern. * - * @method renderHtml - * @return {String} HTML representing the control. + * @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. */ - renderHtml: function () { - var self = this, layout = self._layout, prefix = self.classPrefix; + self.remove = function (pattern) { + var shortcut = createShortcut(pattern); - self.preRender(); - layout.preRender(self); + if (shortcuts[shortcut.id]) { + delete shortcuts[shortcut.id]; + return true; + } - return ( - '
    ' + - (self.settings.title ? ('' + - self.settings.title + '') : '') + - '
    ' + - (self.settings.html || '') + layout.renderHtml(self) + - '
    ' + - '
    ' - ); - } - }); + return false; + }; + }; } ); + /** - * LinkTargets.js + * Sidebar.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -51826,132 +39340,31 @@ define( */ /** - * This module is enables you to get anything that you can link to in a element. + * This module handle sidebar instances for the editor. * + * @class tinymce.ui.Sidebar * @private - * @class tinymce.content.LinkTargets */ define( - 'tinymce.core.content.LinkTargets', + 'tinymce.core.ui.Sidebar', [ - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.SelectorFilter', - 'tinymce.core.dom.DOMUtils', - 'tinymce.core.dom.NodeType', - 'tinymce.core.util.Arr', - 'tinymce.core.util.Fun', - 'tinymce.core.util.Tools', - 'tinymce.core.util.Uuid' ], - function (Element, SelectorFilter, DOMUtils, NodeType, Arr, Fun, Tools, Uuid) { - var trim = Tools.trim; - - var create = function (type, title, url, level, attach) { - return { - type: type, - title: title, - url: url, - level: level, - attach: attach - }; - }; - - var isChildOfContentEditableTrue = function (node) { - while ((node = node.parentNode)) { - var value = node.contentEditable; - if (value && value !== 'inherit') { - return NodeType.isContentEditableTrue(node); - } - } - - return false; - }; - - var select = function (selector, root) { - return Arr.map(SelectorFilter.descendants(Element.fromDom(root), selector), function (element) { - return element.dom(); - }); - }; - - var getElementText = function (elm) { - return elm.innerText || elm.textContent; - }; - - var getOrGenerateId = function (elm) { - return elm.id ? elm.id : Uuid.uuid('h'); - }; - - var isAnchor = function (elm) { - return elm && elm.nodeName === 'A' && (elm.id || elm.name); - }; - - var isValidAnchor = function (elm) { - return isAnchor(elm) && isEditable(elm); - }; - - var isHeader = function (elm) { - return elm && /^(H[1-6])$/.test(elm.nodeName); - }; - - var isEditable = function (elm) { - return isChildOfContentEditableTrue(elm) && !NodeType.isContentEditableFalse(elm); - }; - - var isValidHeader = function (elm) { - return isHeader(elm) && isEditable(elm); - }; - - var getLevel = function (elm) { - return isHeader(elm) ? parseInt(elm.nodeName.substr(1), 10) : 0; - }; - - var headerTarget = function (elm) { - var headerId = getOrGenerateId(elm); - - var attach = function () { - elm.id = headerId; - }; - - return create('header', getElementText(elm), '#' + headerId, getLevel(elm), attach); - }; - - var anchorTarget = function (elm) { - var anchorId = elm.id || elm.name; - var anchorText = getElementText(elm); - - return create('anchor', anchorText ? anchorText : '#' + anchorId, '#' + anchorId, 0, Fun.noop); - }; - - var getHeaderTargets = function (elms) { - return Arr.map(Arr.filter(elms, isValidHeader), headerTarget); - }; - - var getAnchorTargets = function (elms) { - return Arr.map(Arr.filter(elms, isValidAnchor), anchorTarget); - }; - - var getTargetElements = function (elm) { - var elms = select('h1,h2,h3,h4,h5,h6,a:not([href])', elm); - return elms; - }; - - var hasTitle = function (target) { - return trim(target.title).length > 0; - }; - - var find = function (elm) { - var elms = getTargetElements(elm); - return Arr.filter(getHeaderTargets(elms).concat(getAnchorTargets(elms)), hasTitle); + function ( + ) { + var add = function (editor, name, settings) { + var sidebars = editor.sidebars ? editor.sidebars : []; + sidebars.push({ name: name, settings: settings }); + editor.sidebars = sidebars; }; return { - find: find + add: add }; } ); /** - * FilePicker.js + * URI.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -51961,740 +39374,435 @@ define( */ /** - * This class creates a file picker control. - * - * @class tinymce.ui.FilePicker - * @extends tinymce.ui.ComboBox + * This class handles parsing, modification and serialization of URI/URL strings. + * @class tinymce.util.URI */ define( - 'tinymce.core.ui.FilePicker', + 'tinymce.core.util.URI', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'global!window', - 'tinymce.core.content.LinkTargets', - 'tinymce.core.EditorManager', - 'tinymce.core.ui.ComboBox', + 'global!document', 'tinymce.core.util.Tools' ], - function (Arr, Fun, window, LinkTargets, EditorManager, ComboBox, Tools) { - "use strict"; - - var getActiveEditor = function () { - return window.tinymce ? window.tinymce.activeEditor : EditorManager.activeEditor; - }; - - var history = {}; - var HISTORY_LENGTH = 5; - - var toMenuItem = function (target) { - return { - title: target.title, - value: { - title: { raw: target.title }, - url: target.url, - attach: target.attach - } - }; + 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 }; - var toMenuItems = function (targets) { - return Tools.map(targets, toMenuItem); - }; + /** + * 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; - var staticMenuItem = function (title, url) { - return { - title: title, - value: { - title: title, - url: url, - attach: Fun.noop - } - }; - }; + url = trim(url); + settings = self.settings = settings || {}; + baseUri = settings.base_uri; - var isUniqueUrl = function (url, targets) { - var foundTarget = Arr.exists(targets, function (target) { - return target.url === url; - }); + // 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; + } - return !foundTarget; - }; + var isProtocolRelative = url.indexOf('//') === 0; - var getSetting = function (editorSettings, name, defaultValue) { - var value = name in editorSettings ? editorSettings[name] : defaultValue; - return value === false ? null : value; - }; + // Absolute path with no host, fake host and protocol + if (url.indexOf('/') === 0 && !isProtocolRelative) { + url = (baseUri ? baseUri.protocol || 'http' : 'http') + '://mce_host' + url; + } - var createMenuItems = function (term, targets, fileType, editorSettings) { - var separator = { title: '-' }; + // 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]; + } + } - var fromHistoryMenuItems = function (history) { - var historyItems = history.hasOwnProperty(fileType) ? history[fileType] : [ ]; - var uniqueHistory = Arr.filter(historyItems, function (url) { - return isUniqueUrl(url, targets); - }); + // Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri) + url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something - return Tools.map(uniqueHistory, function (url) { - return { - title: url, - value: { - title: url, - url: url, - attach: Fun.noop - } - }; - }); - }; + /*jshint maxlen: 255 */ + /*eslint max-len: 0 */ + url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url); - var fromMenuItems = function (type) { - var filteredTargets = Arr.filter(targets, function (target) { - return target.type === type; - }); + each(queryParts, function (v, i) { + var part = url[i]; - return toMenuItems(filteredTargets); - }; + // Zope 3 workaround, they use @@something + if (part) { + part = part.replace(/\(mce_at\)/g, '@@'); + } - var anchorMenuItems = function () { - var anchorMenuItems = fromMenuItems('anchor'); - var topAnchor = getSetting(editorSettings, 'anchor_top', '#top'); - var bottomAchor = getSetting(editorSettings, 'anchor_bottom', '#bottom'); + self[v] = part; + }); - if (topAnchor !== null) { - anchorMenuItems.unshift(staticMenuItem('', topAnchor)); + if (baseUri) { + if (!self.protocol) { + self.protocol = baseUri.protocol; } - if (bottomAchor !== null) { - anchorMenuItems.push(staticMenuItem('', bottomAchor)); + if (!self.userInfo) { + self.userInfo = baseUri.userInfo; } - return anchorMenuItems; - }; + if (!self.port && self.host === 'mce_host') { + self.port = baseUri.port; + } - var join = function (items) { - return Arr.foldl(items, function (a, b) { - var bothEmpty = a.length === 0 || b.length === 0; - return bothEmpty ? a.concat(b) : a.concat(separator, b); - }, []); - }; + if (!self.host || self.host === 'mce_host') { + self.host = baseUri.host; + } - if (editorSettings.typeahead_urls === false) { - return []; + self.source = ''; } - return fileType === 'file' ? join([ - filterByQuery(term, fromHistoryMenuItems(history)), - filterByQuery(term, fromMenuItems('header')), - filterByQuery(term, anchorMenuItems()) - ]) : filterByQuery(term, fromHistoryMenuItems(history)); - }; + if (isProtocolRelative) { + self.protocol = ''; + } - var addToHistory = function (url, fileType) { - var items = history[fileType]; + //t.path = t.path || '/'; + } - if (!/^https?/.test(url)) { - return; - } + 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; - if (items) { - if (Arr.indexOf(items, url) === -1) { - history[fileType] = items.slice(0, HISTORY_LENGTH).concat(url); - } - } else { - history[fileType] = [url]; - } - }; + path = /^(.*?)\/?(\w+)?$/.exec(path); - var filterByQuery = function (term, menuItems) { - var lowerCaseTerm = term.toLowerCase(); - var result = Tools.grep(menuItems, function (item) { - return item.title.toLowerCase().indexOf(lowerCaseTerm) !== -1; - }); + // Update path parts + self.path = path[0]; + self.directory = path[1]; + self.file = path[2]; - return result.length === 1 && result[0].title === term ? [] : result; - }; + // Rebuild source + self.source = ''; + self.getURI(); + }, - var getTitle = function (linkDetails) { - var title = linkDetails.title; - return title.raw ? title.raw : title; - }; + /** + * 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; - var setupAutoCompleteHandler = function (ctrl, editorSettings, bodyElm, fileType) { - var autocomplete = function (term) { - var linkTargets = LinkTargets.find(bodyElm); - var menuItems = createMenuItems(term, linkTargets, fileType, editorSettings); - ctrl.showAutoComplete(menuItems, term); - }; + if (uri === "./") { + return uri; + } - ctrl.on('autocomplete', function () { - autocomplete(ctrl.value()); - }); + uri = new URI(uri, { base_uri: self }); - ctrl.on('selectitem', function (e) { - var linkDetails = e.value; + // 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(); + } - ctrl.value(linkDetails.url); - var title = getTitle(linkDetails); + var tu = self.getURI(), uu = uri.getURI(); - if (fileType === 'image') { - ctrl.fire('change', { meta: { alt: title, attach: linkDetails.attach } }); - } else { - ctrl.fire('change', { meta: { text: title, attach: linkDetails.attach } }); + // 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; } - ctrl.focus(); - }); + output = self.toRelPath(self.path, uri.path); - ctrl.on('click', function (e) { - if (ctrl.value().length === 0 && e.target.nodeName === 'INPUT') { - autocomplete(''); + // Add query + if (uri.query) { + output += '?' + uri.query; } - }); - ctrl.on('PostRender', function () { - ctrl.getRoot().on('submit', function (e) { - if (!e.isDefaultPrevented()) { - addToHistory(ctrl.value(), fileType); - } - }); - }); - }; + // Add anchor + if (uri.anchor) { + output += '#' + uri.anchor; + } - var statusToUiState = function (result) { - var status = result.status, message = result.message; + return output; + }, - if (status === 'valid') { - return { status: 'ok', message: message }; - } else if (status === 'unknown') { - return { status: 'warn', message: message }; - } else if (status === 'invalid') { - return { status: 'warn', message: message }; - } else { - return { status: 'none', message: '' }; - } - }; + /** + * 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 }); - var setupLinkValidatorHandler = function (ctrl, editorSettings, fileType) { - var validatorHandler = editorSettings.filepicker_validator_handler; - if (validatorHandler) { - var validateUrl = function (url) { - if (url.length === 0) { - ctrl.statusLevel('none'); - return; - } + return uri.getURI(noHost && this.isSameOrigin(uri)); + }, - validatorHandler({ - url: url, - type: fileType - }, function (result) { - var uiState = statusToUiState(result); + /** + * 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; + } - ctrl.statusMessage(uiState.message); - ctrl.statusLevel(uiState.status); - }); - }; + var defaultPort = DEFAULT_PORTS[this.protocol]; + if (defaultPort && ((this.port || defaultPort) == (uri.port || defaultPort))) { + return true; + } + } - ctrl.state.on('change:value', function (e) { - validateUrl(e.value); - }); - } - }; + return false; + }, - return ComboBox.extend({ /** - * Constructs a new control instance with the specified settings. + * Converts a absolute path into a relative path. * - * @constructor - * @param {Object} settings Name/value object with settings. + * @method toRelPath + * @param {String} base Base point to convert the path from. + * @param {String} path Absolute path to convert into a relative path. */ - init: function (settings) { - var self = this, editor = getActiveEditor(), editorSettings = editor.settings; - var actionCallback, fileBrowserCallback, fileBrowserCallbackTypes; - var fileType = settings.filetype; + toRelPath: function (base, path) { + var items, breakPoint = 0, out = '', i, l; - settings.spellcheck = false; + // Split the paths + base = base.substring(0, base.lastIndexOf('/')); + base = base.split('/'); + items = path.split('/'); - fileBrowserCallbackTypes = editorSettings.file_picker_types || editorSettings.file_browser_callback_types; - if (fileBrowserCallbackTypes) { - fileBrowserCallbackTypes = Tools.makeMap(fileBrowserCallbackTypes, /[, ]/); + 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 (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType]) { - fileBrowserCallback = editorSettings.file_picker_callback; - if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) { - actionCallback = function () { - var meta = self.fire('beforecall').meta; - - meta = Tools.extend({ filetype: fileType }, meta); - - // file_picker_callback(callback, currentValue, metaData) - fileBrowserCallback.call( - editor, - function (value, meta) { - self.value(value).fire('change', { meta: meta }); - }, - self.value(), - meta - ); - }; - } else { - // Legacy callback: file_picker_callback(id, currentValue, filetype, window) - fileBrowserCallback = editorSettings.file_browser_callback; - if (fileBrowserCallback && (!fileBrowserCallbackTypes || fileBrowserCallbackTypes[fileType])) { - actionCallback = function () { - fileBrowserCallback( - self.getEl('inp').id, - self.value(), - fileType, - window - ); - }; + 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 (actionCallback) { - settings.icon = 'browse'; - settings.onaction = actionCallback; + if (breakPoint === 1) { + return path; } - self._super(settings); + for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) { + out += "../"; + } - setupAutoCompleteHandler(self, editorSettings, editor.getBody(), fileType); - setupLinkValidatorHandler(self, editorSettings, fileType); - } - }); - } -); -/** - * FitLayout.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 - */ + for (i = breakPoint - 1, l = items.length; i < l; i++) { + if (i != breakPoint - 1) { + out += "/" + items[i]; + } else { + out += items[i]; + } + } -/** - * This layout manager will resize the control to be the size of it's parent container. - * In other words width: 100% and height: 100%. - * - * @-x-less FitLayout.less - * @class tinymce.ui.FitLayout - * @extends tinymce.ui.AbsoluteLayout - */ -define( - 'tinymce.core.ui.FitLayout', - [ - "tinymce.core.ui.AbsoluteLayout" - ], - function (AbsoluteLayout) { - "use strict"; + return out; + }, - return AbsoluteLayout.extend({ /** - * Recalculates the positions of the controls in the specified container. + * Converts a relative path into a absolute path. * - * @method recalc - * @param {tinymce.ui.Container} container Container instance to recalc. + * @method toAbsPath + * @param {String} base Base point to convert the path from. + * @param {String} path Relative path to convert into an absolute path. */ - recalc: function (container) { - var contLayoutRect = container.layoutRect(), paddingBox = container.paddingBox; + toAbsPath: function (base, path) { + var i, nb = 0, o = [], tr, outPath; - container.items().filter(':visible').each(function (ctrl) { - ctrl.layoutRect({ - x: paddingBox.left, - y: paddingBox.top, - w: contLayoutRect.innerW - paddingBox.right - paddingBox.left, - h: contLayoutRect.innerH - paddingBox.top - paddingBox.bottom - }); + // Split paths + tr = /\/$/.test(path) ? '/' : ''; + base = base.split('/'); + path = path.split('/'); - if (ctrl.recalc) { - ctrl.recalc(); + // Remove empty chunks + each(base, function (k) { + if (k) { + o.push(k); } }); - } - }); - } -); -/** - * FlexLayout.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 layout manager works similar to the CSS flex box. - * - * @setting {String} direction row|row-reverse|column|column-reverse - * @setting {Number} flex A positive-number to flex by. - * @setting {String} align start|end|center|stretch - * @setting {String} pack start|end|justify - * - * @class tinymce.ui.FlexLayout - * @extends tinymce.ui.AbsoluteLayout - */ -define( - 'tinymce.core.ui.FlexLayout', - [ - "tinymce.core.ui.AbsoluteLayout" - ], - function (AbsoluteLayout) { - "use strict"; - - return AbsoluteLayout.extend({ - /** - * Recalculates the positions of the controls in the specified container. - * - * @method recalc - * @param {tinymce.ui.Container} container Container instance to recalc. - */ - recalc: function (container) { - // A ton of variables, needs to be in the same scope for performance - var i, l, items, contLayoutRect, contPaddingBox, contSettings, align, pack, spacing, totalFlex, availableSpace, direction; - var ctrl, ctrlLayoutRect, ctrlSettings, flex, maxSizeItems = [], size, maxSize, ratio, rect, pos, maxAlignEndPos; - var sizeName, minSizeName, posName, maxSizeName, beforeName, innerSizeName, deltaSizeName, contentSizeName; - var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignBeforeName, alignAfterName; - var alignDeltaSizeName, alignContentSizeName; - var max = Math.max, min = Math.min; - - // Get container items, properties and settings - items = container.items().filter(':visible'); - contLayoutRect = container.layoutRect(); - contPaddingBox = container.paddingBox; - contSettings = container.settings; - direction = container.isRtl() ? (contSettings.direction || 'row-reversed') : contSettings.direction; - align = contSettings.align; - pack = container.isRtl() ? (contSettings.pack || 'end') : contSettings.pack; - spacing = contSettings.spacing || 0; - - if (direction == "row-reversed" || direction == "column-reverse") { - items = items.set(items.toArray().reverse()); - direction = direction.split('-')[0]; - } - - // Setup axis variable name for row/column direction since the calculations is the same - if (direction == "column") { - posName = "y"; - sizeName = "h"; - minSizeName = "minH"; - maxSizeName = "maxH"; - innerSizeName = "innerH"; - beforeName = 'top'; - deltaSizeName = "deltaH"; - contentSizeName = "contentH"; - - alignBeforeName = "left"; - alignSizeName = "w"; - alignAxisName = "x"; - alignInnerSizeName = "innerW"; - alignMinSizeName = "minW"; - alignAfterName = "right"; - alignDeltaSizeName = "deltaW"; - alignContentSizeName = "contentW"; - } else { - posName = "x"; - sizeName = "w"; - minSizeName = "minW"; - maxSizeName = "maxW"; - innerSizeName = "innerW"; - beforeName = 'left'; - deltaSizeName = "deltaW"; - contentSizeName = "contentW"; - - alignBeforeName = "top"; - alignSizeName = "h"; - alignAxisName = "y"; - alignInnerSizeName = "innerH"; - alignMinSizeName = "minH"; - alignAfterName = "bottom"; - alignDeltaSizeName = "deltaH"; - alignContentSizeName = "contentH"; - } - // Figure out total flex, availableSpace and collect any max size elements - availableSpace = contLayoutRect[innerSizeName] - contPaddingBox[beforeName] - contPaddingBox[beforeName]; - maxAlignEndPos = totalFlex = 0; - for (i = 0, l = items.length; i < l; i++) { - ctrl = items[i]; - ctrlLayoutRect = ctrl.layoutRect(); - ctrlSettings = ctrl.settings; - flex = ctrlSettings.flex; - availableSpace -= (i < l - 1 ? spacing : 0); - - if (flex > 0) { - totalFlex += flex; - - // Flexed item has a max size then we need to check if we will hit that size - if (ctrlLayoutRect[maxSizeName]) { - maxSizeItems.push(ctrl); - } + base = o; - ctrlLayoutRect.flex = flex; + // Merge relURLParts chunks + for (i = path.length - 1, o = []; i >= 0; i--) { + // Ignore empty or . + if (path[i].length === 0 || path[i] === ".") { + continue; } - availableSpace -= ctrlLayoutRect[minSizeName]; + // Is parent + if (path[i] === '..') { + nb++; + continue; + } - // Calculate the align end position to be used to check for overflow/underflow - size = contPaddingBox[alignBeforeName] + ctrlLayoutRect[alignMinSizeName] + contPaddingBox[alignAfterName]; - if (size > maxAlignEndPos) { - maxAlignEndPos = size; + // Move up + if (nb > 0) { + nb--; + continue; } - } - // Calculate minW/minH - rect = {}; - if (availableSpace < 0) { - rect[minSizeName] = contLayoutRect[minSizeName] - availableSpace + contLayoutRect[deltaSizeName]; - } else { - rect[minSizeName] = contLayoutRect[innerSizeName] - availableSpace + contLayoutRect[deltaSizeName]; + o.push(path[i]); } - rect[alignMinSizeName] = maxAlignEndPos + contLayoutRect[alignDeltaSizeName]; - - rect[contentSizeName] = contLayoutRect[innerSizeName] - availableSpace; - rect[alignContentSizeName] = maxAlignEndPos; - rect.minW = min(rect.minW, contLayoutRect.maxW); - rect.minH = min(rect.minH, contLayoutRect.maxH); - rect.minW = max(rect.minW, contLayoutRect.startMinWidth); - rect.minH = max(rect.minH, contLayoutRect.startMinHeight); - - // Resize container container if minSize was changed - if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { - rect.w = rect.minW; - rect.h = rect.minH; - - container.layoutRect(rect); - this.recalc(container); - - // Forced recalc for example if items are hidden/shown - if (container._lastRect === null) { - var parentCtrl = container.parent(); - if (parentCtrl) { - parentCtrl._lastRect = null; - parentCtrl.recalc(); - } - } + i = base.length - nb; - return; + // If /a/b/c or / + if (i <= 0) { + outPath = o.reverse().join('/'); + } else { + outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/'); } - // Handle max size elements, check if they will become to wide with current options - ratio = availableSpace / totalFlex; - for (i = 0, l = maxSizeItems.length; i < l; i++) { - ctrl = maxSizeItems[i]; - ctrlLayoutRect = ctrl.layoutRect(); - maxSize = ctrlLayoutRect[maxSizeName]; - size = ctrlLayoutRect[minSizeName] + ctrlLayoutRect.flex * ratio; + // Add front / if it's needed + if (outPath.indexOf('/') !== 0) { + outPath = '/' + outPath; + } - if (size > maxSize) { - availableSpace -= (ctrlLayoutRect[maxSizeName] - ctrlLayoutRect[minSizeName]); - totalFlex -= ctrlLayoutRect.flex; - ctrlLayoutRect.flex = 0; - ctrlLayoutRect.maxFlexSize = maxSize; - } else { - ctrlLayoutRect.maxFlexSize = 0; - } + // Add traling / if it's needed + if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) { + outPath += tr; } - // Setup new ratio, target layout rect, start position - ratio = availableSpace / totalFlex; - pos = contPaddingBox[beforeName]; - rect = {}; + 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; - // Handle pack setting moves the start position to end, center - if (totalFlex === 0) { - if (pack == "end") { - pos = availableSpace + contPaddingBox[beforeName]; - } else if (pack == "center") { - pos = Math.round( - (contLayoutRect[innerSizeName] / 2) - ((contLayoutRect[innerSizeName] - availableSpace) / 2) - ) + contPaddingBox[beforeName]; + // Rebuild source + if (!self.source || noProtoHost) { + s = ''; - if (pos < 0) { - pos = contPaddingBox[beforeName]; + if (!noProtoHost) { + if (self.protocol) { + s += self.protocol + '://'; + } else { + s += '//'; } - } else if (pack == "justify") { - pos = contPaddingBox[beforeName]; - spacing = Math.floor(availableSpace / (items.length - 1)); - } - } - // Default aligning (start) the other ones needs to be calculated while doing the layout - rect[alignAxisName] = contPaddingBox[alignBeforeName]; + if (self.userInfo) { + s += self.userInfo + '@'; + } - // Start laying out controls - for (i = 0, l = items.length; i < l; i++) { - ctrl = items[i]; - ctrlLayoutRect = ctrl.layoutRect(); - size = ctrlLayoutRect.maxFlexSize || ctrlLayoutRect[minSizeName]; + if (self.host) { + s += self.host; + } - // Align the control on the other axis - if (align === "center") { - rect[alignAxisName] = Math.round((contLayoutRect[alignInnerSizeName] / 2) - (ctrlLayoutRect[alignSizeName] / 2)); - } else if (align === "stretch") { - rect[alignSizeName] = max( - ctrlLayoutRect[alignMinSizeName] || 0, - contLayoutRect[alignInnerSizeName] - contPaddingBox[alignBeforeName] - contPaddingBox[alignAfterName] - ); - rect[alignAxisName] = contPaddingBox[alignBeforeName]; - } else if (align === "end") { - rect[alignAxisName] = contLayoutRect[alignInnerSizeName] - ctrlLayoutRect[alignSizeName] - contPaddingBox.top; + if (self.port) { + s += ':' + self.port; + } } - // Calculate new size based on flex - if (ctrlLayoutRect.flex > 0) { - size += ctrlLayoutRect.flex * ratio; + if (self.path) { + s += self.path; } - rect[sizeName] = size; - rect[posName] = pos; - ctrl.layoutRect(rect); + if (self.query) { + s += '?' + self.query; + } - // Recalculate containers - if (ctrl.recalc) { - ctrl.recalc(); + if (self.anchor) { + s += '#' + self.anchor; } - // Move x/y position - pos += size + spacing; + self.source = s; } + + return self.source; } - }); - } -); -/** - * FlowLayout.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 layout manager will place the controls by using the browsers native layout. - * - * @-x-less FlowLayout.less - * @class tinymce.ui.FlowLayout - * @extends tinymce.ui.Layout - */ -define( - 'tinymce.core.ui.FlowLayout', - [ - "tinymce.core.ui.Layout" - ], - function (Layout) { - return Layout.extend({ - Defaults: { - containerClass: 'flow-layout', - controlClass: 'flow-layout-item', - endClass: 'break' - }, + URI.parseDataUri = function (uri) { + var type, matches; - /** - * Recalculates the positions of the controls in the specified container. - * - * @method recalc - * @param {tinymce.ui.Container} container Container instance to recalc. - */ - recalc: function (container) { - container.items().filter(':visible').each(function (ctrl) { - if (ctrl.recalc) { - ctrl.recalc(); - } - }); - }, + uri = decodeURIComponent(uri).split(','); - isNative: function () { - return true; + matches = /data:([^;]+)/.exec(uri[0]); + if (matches) { + type = matches[1]; } - }); - } -); -/** - * FontInfo.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 computing font size for elements. - * - * @private - * @class tinymce.fmt.FontInfo - */ -define( - 'tinymce.core.fmt.FontInfo', - [ - 'ephox.katamari.api.Fun', - 'ephox.katamari.api.Option', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.node.Node', - 'tinymce.core.dom.DOMUtils' - ], - function (Fun, Option, Element, Node, DOMUtils) { - var getSpecifiedFontProp = function (propName, rootElm, elm) { - while (elm !== rootElm) { - if (elm.style[propName]) { - var foundStyle = elm.style[propName]; - return foundStyle !== '' ? Option.some(foundStyle) : Option.none(); - } - elm = elm.parentNode; - } - return Option.none(); + return { + type: type, + data: uri[1] + }; }; - var toPt = function (fontSize) { - if (/[0-9.]+px$/.test(fontSize)) { - return Math.round(parseInt(fontSize, 10) * 72 / 96) + 'pt'; - } + URI.getDocumentBaseUrl = function (loc) { + var baseUrl; - return fontSize; - }; + // 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; + } - var normalizeFontFamily = function (fontFamily) { - // 'Font name', Font -> Font name,Font - return fontFamily.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); - }; + if (/^[^:]+:\/\/\/?[^\/]+\//.test(baseUrl)) { + baseUrl = baseUrl.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); - var getComputedFontProp = function (propName, elm) { - return Option.from(DOMUtils.DOM.getStyle(elm, propName, true)); - }; + if (!/[\/\\]$/.test(baseUrl)) { + baseUrl += '/'; + } + } - var getFontProp = function (propName) { - return function (rootElm, elm) { - return Option.from(elm) - .map(Element.fromDom) - .filter(Node.isElement) - .bind(function (element) { - return getSpecifiedFontProp(propName, rootElm, element.dom()) - .or(getComputedFontProp(propName, element.dom())); - }) - .getOr(''); - }; + return baseUrl; }; - return { - getFontSize: getFontProp('fontSize'), - getFontFamily: Fun.compose(normalizeFontFamily, getFontProp('fontFamily')), - toPt: toPt - }; + return URI; } ); /** - * FormatControls.js + * Editor.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -52703,1433 +39811,1322 @@ define( * Contributing: http://www.tinymce.com/contributing */ +/*jshint scripturl:true */ + +/** + * Include the base event class documentation. + * + * @include ../../../../../tools/docs/tinymce.Event.js + */ + /** - * Internal class containing all TinyMCE specific control types such as - * format listboxes, fontlist boxes, toolbar buttons etc. + * This class contains the core logic for a TinyMCE editor. + * + * @class tinymce.Editor + * @mixes tinymce.util.Observable + * @example + * // Add a class to all paragraphs in the editor. + * tinymce.activeEditor.dom.addClass(tinymce.activeEditor.dom.select('p'), 'someclass'); * - * @class tinymce.ui.FormatControls + * // Gets the current editors selection as text + * tinymce.activeEditor.selection.getContent({format: 'text'}); + * + * // Creates a new editor instance + * var ed = new tinymce.Editor('textareaid', { + * some_setting: 1 + * }, tinymce.EditorManager); + * + * ed.render(); */ define( - 'tinymce.core.ui.FormatControls', + 'tinymce.core.Editor', [ - 'ephox.katamari.api.Arr', - 'ephox.katamari.api.Fun', - 'ephox.sugar.api.node.Element', - 'ephox.sugar.api.search.SelectorFind', + 'tinymce.core.AddOnManager', + 'tinymce.core.dom.DomQuery', 'tinymce.core.dom.DOMUtils', - 'tinymce.core.EditorManager', + 'tinymce.core.EditorCommands', + 'tinymce.core.EditorFocus', + 'tinymce.core.EditorObservable', + 'tinymce.core.EditorSettings', 'tinymce.core.Env', - 'tinymce.core.fmt.FontInfo', - 'tinymce.core.ui.Control', - 'tinymce.core.ui.FloatPanel', - 'tinymce.core.ui.Widget', - 'tinymce.core.util.Tools' + 'tinymce.core.html.Serializer', + 'tinymce.core.init.Render', + 'tinymce.core.Mode', + 'tinymce.core.Shortcuts', + 'tinymce.core.ui.Sidebar', + 'tinymce.core.util.Tools', + 'tinymce.core.util.URI', + 'tinymce.core.util.Uuid' ], - function (Arr, Fun, Element, SelectorFind, DOMUtils, EditorManager, Env, FontInfo, Control, FloatPanel, Widget, Tools) { - var each = Tools.each; + function ( + 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 ie = Env.ie; - var flatten = function (ar) { - return Arr.foldl(ar, function (result, item) { - return result.concat(item); - }, []); - }; + /** + * Include Editor API docs. + * + * @include ../../../../../tools/docs/tinymce.Editor.js + */ + + /** + * Constructs a editor instance by id. + * + * @constructor + * @method Editor + * @param {String} id Unique id for the editor. + * @param {Object} settings Settings for the editor. + * @param {tinymce.EditorManager} editorManager EditorManager instance. + */ + function Editor(id, settings, editorManager) { + var self = this, documentBaseUrl, baseUri; + + documentBaseUrl = self.documentBaseUrl = editorManager.documentBaseURL; + baseUri = editorManager.baseURI; - EditorManager.on('AddEditor', function (e) { - var editor = e.editor; + /** + * Name/value collection with editor settings. + * + * @property settings + * @type Object + * @example + * // Get the value of the theme setting + * tinymce.activeEditor.windowManager.alert("You are using the " + tinymce.activeEditor.settings.theme + " theme"); + */ + settings = EditorSettings.getEditorSettings(self, id, documentBaseUrl, editorManager.defaultSettings, settings); + self.settings = settings; - setupRtlMode(editor); - registerControls(editor); - setupContainer(editor); - }); + AddOnManager.language = settings.language || 'en'; + AddOnManager.languageLoad = settings.language_load; + AddOnManager.baseURL = editorManager.baseURL; - Control.translate = function (text) { - return EditorManager.translate(text); - }; + /** + * Editor instance id, normally the same as the div/textarea that was replaced. + * + * @property id + * @type String + */ + self.id = id; - Widget.tooltips = !Env.iOS; + /** + * State to force the editor to return false on a isDirty call. + * + * @property isNotDirty + * @type Boolean + * @deprecated Use editor.setDirty instead. + */ + self.setDirty(false); - function setupContainer(editor) { - if (editor.settings.ui_container) { - Env.container = SelectorFind.descendant(Element.fromDom(document.body), editor.settings.ui_container).fold(Fun.constant(null), function (elm) { - return elm.dom(); - }); - } - } + /** + * Name/Value object containing plugin instances. + * + * @property plugins + * @type Object + * @example + * // Execute a method inside a plugin directly + * tinymce.activeEditor.plugins.someplugin.someMethod(); + */ + self.plugins = {}; - function setupRtlMode(editor) { - editor.on('ScriptsLoaded', function () { - if (editor.rtl) { - Control.rtl = true; - } + /** + * URI object to document configured for the TinyMCE instance. + * + * @property documentBaseURI + * @type tinymce.util.URI + * @example + * // Get relative URL from the location of document_base_url + * tinymce.activeEditor.documentBaseURI.toRelative('/somedir/somefile.htm'); + * + * // Get absolute URL from the location of document_base_url + * tinymce.activeEditor.documentBaseURI.toAbsolute('somefile.htm'); + */ + self.documentBaseURI = new URI(settings.document_base_url, { + base_uri: baseUri }); - } - - function registerControls(editor) { - var formatMenu; - - function createListBoxChangeHandler(items, formatName) { - return function () { - var self = this; - editor.on('nodeChange', function (e) { - var formatter = editor.formatter; - var value = null; + /** + * URI object to current document that holds the TinyMCE editor instance. + * + * @property baseURI + * @type tinymce.util.URI + * @example + * // Get relative URL from the location of the API + * tinymce.activeEditor.baseURI.toRelative('/somedir/somefile.htm'); + * + * // Get absolute URL from the location of the API + * tinymce.activeEditor.baseURI.toAbsolute('somefile.htm'); + */ + self.baseURI = baseUri; - each(e.parents, function (node) { - each(items, function (item) { - if (formatName) { - if (formatter.matchNode(node, formatName, { value: item.value })) { - value = item.value; - } - } else { - if (formatter.matchNode(node, item.value)) { - value = item.value; - } - } + /** + * Array with CSS files to load into the iframe. + * + * @property contentCSS + * @type Array + */ + self.contentCSS = []; - if (value) { - return false; - } - }); + /** + * Array of CSS styles to add to head of document when the editor loads. + * + * @property contentStyles + * @type Array + */ + self.contentStyles = []; - if (value) { - return false; - } - }); + self.shortcuts = new Shortcuts(self); + self.loadedCSS = {}; + self.editorCommands = new EditorCommands(self); + self.suffix = editorManager.suffix; + self.editorManager = editorManager; + self.inline = settings.inline; + self.buttons = {}; + self.menuItems = {}; - self.value(value); - }); - }; + if (settings.cache_suffix) { + Env.cacheSuffix = settings.cache_suffix.replace(/^[\?\&]+/, ''); } - function createFontNameListBoxChangeHandler(items) { - return function () { - var self = this; - - var getFirstFont = function (fontFamily) { - return fontFamily ? fontFamily.split(',')[0] : ''; - }; - - editor.on('init nodeChange', function (e) { - var fontFamily, value = null; + if (settings.override_viewport === false) { + Env.overrideViewPort = false; + } - fontFamily = FontInfo.getFontFamily(editor.getBody(), e.element); + // Call setup + editorManager.fire('SetupEditor', self); + self.execCallback('setup', self); - each(items, function (item) { - if (item.value.toLowerCase() === fontFamily.toLowerCase()) { - value = item.value; - } - }); + /** + * Dom query instance with default scope to the editor document and default element is the body of the editor. + * + * @property $ + * @type tinymce.dom.DomQuery + * @example + * tinymce.activeEditor.$('p').css('color', 'red'); + * tinymce.activeEditor.$().append('

    new

    '); + */ + self.$ = DomQuery.overrideDefaults(function () { + return { + context: self.inline ? self.getBody() : self.getDoc(), + element: self.getBody() + }; + }); + } - each(items, function (item) { - if (!value && getFirstFont(item.value).toLowerCase() === getFirstFont(fontFamily).toLowerCase()) { - value = item.value; - } - }); + Editor.prototype = { + /** + * Renders the editor/adds it to the page. + * + * @method render + */ + render: function () { + Render.render(this); + }, - self.value(value); + /** + * Focuses/activates the editor. This will set this editor as the activeEditor in the tinymce collection + * it will also place DOM focus inside the editor. + * + * @method focus + * @param {Boolean} skipFocus Skip DOM focus. Just set is as the active editor. + */ + focus: function (skipFocus) { + EditorFocus.focus(this, skipFocus); + }, - if (!value && fontFamily) { - self.text(getFirstFont(fontFamily)); - } - }); - }; - } + /** + * Executes a legacy callback. This method is useful to call old 2.x option callbacks. + * There new event model is a better way to add callback so this method might be removed in the future. + * + * @method execCallback + * @param {String} name Name of the callback to execute. + * @return {Object} Return value passed from callback function. + */ + execCallback: function (name) { + var self = this, callback = self.settings[name], scope; - function createFontSizeListBoxChangeHandler(items) { - return function () { - var self = this; + if (!callback) { + return; + } - editor.on('init nodeChange', function (e) { - var px, pt, value = null; + // Look through lookup + if (self.callbackLookup && (scope = self.callbackLookup[name])) { + callback = scope.func; + scope = scope.scope; + } - px = FontInfo.getFontSize(editor.getBody(), e.element); - pt = FontInfo.toPt(px); + if (typeof callback === 'string') { + scope = callback.replace(/\.\w+$/, ''); + scope = scope ? resolve(scope) : 0; + callback = resolve(callback); + self.callbackLookup = self.callbackLookup || {}; + self.callbackLookup[name] = { func: callback, scope: scope }; + } - each(items, function (item) { - if (item.value === px) { - value = px; - } else if (item.value === pt) { - value = pt; - } - }); + return callback.apply(scope || self, Array.prototype.slice.call(arguments, 1)); + }, - self.value(value); + /** + * Translates the specified string by replacing variables with language pack items it will also check if there is + * a key matching the input. + * + * @method translate + * @param {String} text String to translate by the language pack data. + * @return {String} Translated string. + */ + translate: function (text) { + if (text && Tools.is(text, 'string')) { + var lang = this.settings.language || 'en', i18n = this.editorManager.i18n; - if (!value) { - self.text(pt); - } + text = i18n.data[lang + '.' + text] || text.replace(/\{\#([^\}]+)\}/g, function (a, b) { + return i18n.data[lang + '.' + b] || '{#' + b + '}'; }); - }; - } - - function createFormats(formats) { - formats = formats.replace(/;$/, '').split(';'); - - var i = formats.length; - while (i--) { - formats[i] = formats[i].split('='); } - return formats; - } + return this.editorManager.translate(text); + }, - function createFormatMenu() { - var count = 0, newFormats = []; + /** + * Returns a language pack item by name/key. + * + * @method getLang + * @param {String} name Name/key to get from the language pack. + * @param {String} defaultVal Optional default value to retrieve. + */ + getLang: function (name, defaultVal) { + return ( + this.editorManager.i18n.data[(this.settings.language || 'en') + '.' + name] || + (defaultVal !== undefined ? defaultVal : '{#' + name + '}') + ); + }, - var defaultStyleFormats = [ - { - title: 'Headings', items: [ - { title: 'Heading 1', format: 'h1' }, - { title: 'Heading 2', format: 'h2' }, - { title: 'Heading 3', format: 'h3' }, - { title: 'Heading 4', format: 'h4' }, - { title: 'Heading 5', format: 'h5' }, - { title: 'Heading 6', format: 'h6' } - ] - }, + /** + * Returns a configuration parameter by name. + * + * @method getParam + * @param {String} name Configruation parameter to retrieve. + * @param {String} defaultVal Optional default value to return. + * @param {String} type Optional type parameter. + * @return {String} Configuration parameter value or default value. + * @example + * // Returns a specific config value from the currently active editor + * var someval = tinymce.activeEditor.getParam('myvalue'); + * + * // Returns a specific config value from a specific editor instance by id + * var someval2 = tinymce.get('my_editor').getParam('myvalue'); + */ + getParam: function (name, defaultVal, type) { + var value = name in this.settings ? this.settings[name] : defaultVal, output; - { - title: 'Inline', items: [ - { title: 'Bold', icon: 'bold', format: 'bold' }, - { title: 'Italic', icon: 'italic', format: 'italic' }, - { title: 'Underline', icon: 'underline', format: 'underline' }, - { title: 'Strikethrough', icon: 'strikethrough', format: 'strikethrough' }, - { title: 'Superscript', icon: 'superscript', format: 'superscript' }, - { title: 'Subscript', icon: 'subscript', format: 'subscript' }, - { title: 'Code', icon: 'code', format: 'code' } - ] - }, + if (type === 'hash') { + output = {}; - { - title: 'Blocks', items: [ - { title: 'Paragraph', format: 'p' }, - { title: 'Blockquote', format: 'blockquote' }, - { title: 'Div', format: 'div' }, - { title: 'Pre', format: 'pre' } - ] - }, + if (typeof value === 'string') { + each(value.indexOf('=') > 0 ? value.split(/[;,](?![^=;,]*(?:[;,]|$))/) : value.split(','), function (value) { + value = value.split('='); - { - title: 'Alignment', items: [ - { title: 'Left', icon: 'alignleft', format: 'alignleft' }, - { title: 'Center', icon: 'aligncenter', format: 'aligncenter' }, - { title: 'Right', icon: 'alignright', format: 'alignright' }, - { title: 'Justify', icon: 'alignjustify', format: 'alignjustify' } - ] + if (value.length > 1) { + output[trim(value[0])] = trim(value[1]); + } else { + output[trim(value[0])] = trim(value); + } + }); + } else { + output = value; } - ]; - function createMenu(formats) { - var menu = []; + return output; + } - if (!formats) { - return; - } + return value; + }, - each(formats, function (format) { - var menuItem = { - text: format.title, - icon: format.icon - }; + /** + * Dispatches out a onNodeChange event to all observers. This method should be called when you + * need to update the UI states or element path etc. + * + * @method nodeChanged + * @param {Object} args Optional args to pass to NodeChange event handlers. + */ + nodeChanged: function (args) { + this._nodeChangeDispatcher.nodeChanged(args); + }, - if (format.items) { - menuItem.menu = createMenu(format.items); - } else { - var formatName = format.format || "custom" + count++; + /** + * Adds a button that later gets created by the theme in the editors toolbars. + * + * @method addButton + * @param {String} name Button name to add. + * @param {Object} settings Settings object with title, cmd etc. + * @example + * // Adds a custom button to the editor that inserts contents when clicked + * tinymce.init({ + * ... + * + * toolbar: 'example' + * + * setup: function(ed) { + * ed.addButton('example', { + * title: 'My title', + * image: '../js/tinymce/plugins/example/img/example.gif', + * onclick: function() { + * ed.insertContent('Hello world!!'); + * } + * }); + * } + * }); + */ + addButton: function (name, settings) { + var self = this; - if (!format.format) { - format.name = formatName; - newFormats.push(format); - } + if (settings.cmd) { + settings.onclick = function () { + self.execCommand(settings.cmd); + }; + } - menuItem.format = formatName; - menuItem.cmd = format.cmd; - } + if (!settings.text && !settings.icon) { + settings.icon = name; + } - menu.push(menuItem); - }); + self.buttons = self.buttons; + settings.tooltip = settings.tooltip || settings.title; + self.buttons[name] = settings; + }, + + /** + * Adds a sidebar for the editor instance. + * + * @method addSidebar + * @param {String} name Sidebar name to add. + * @param {Object} settings Settings object with icon, onshow etc. + * @example + * // Adds a custom sidebar that when clicked logs the panel element + * tinymce.init({ + * ... + * setup: function(ed) { + * ed.addSidebar('example', { + * tooltip: 'My sidebar', + * icon: 'my-side-bar', + * onshow: function(api) { + * console.log(api.element()); + * } + * }); + * } + * }); + */ + addSidebar: function (name, settings) { + return Sidebar.add(this, name, settings); + }, + + /** + * Adds a menu item to be used in the menus of the theme. There might be multiple instances + * of this menu item for example it might be used in the main menus of the theme but also in + * the context menu so make sure that it's self contained and supports multiple instances. + * + * @method addMenuItem + * @param {String} name Menu item name to add. + * @param {Object} settings Settings object with title, cmd etc. + * @example + * // Adds a custom menu item to the editor that inserts contents when clicked + * // The context option allows you to add the menu item to an existing default menu + * tinymce.init({ + * ... + * + * setup: function(ed) { + * ed.addMenuItem('example', { + * text: 'My menu item', + * context: 'tools', + * onclick: function() { + * ed.insertContent('Hello world!!'); + * } + * }); + * } + * }); + */ + addMenuItem: function (name, settings) { + var self = this; - return menu; + if (settings.cmd) { + settings.onclick = function () { + self.execCommand(settings.cmd); + }; } - function createStylesMenu() { - var menu; + self.menuItems = self.menuItems; + self.menuItems[name] = settings; + }, + + /** + * Adds a contextual toolbar to be rendered when the selector matches. + * + * @method addContextToolbar + * @param {function/string} predicate Predicate that needs to return true if provided strings get converted into CSS predicates. + * @param {String/Array} items String or array with items to add to the context toolbar. + */ + addContextToolbar: function (predicate, items) { + var self = this, selector; - if (editor.settings.style_formats_merge) { - if (editor.settings.style_formats) { - menu = createMenu(defaultStyleFormats.concat(editor.settings.style_formats)); - } else { - menu = createMenu(defaultStyleFormats); - } - } else { - menu = createMenu(editor.settings.style_formats || defaultStyleFormats); - } + self.contextToolbars = self.contextToolbars || []; - return menu; + // Convert selector to predicate + if (typeof predicate == "string") { + selector = predicate; + predicate = function (elm) { + return self.dom.is(elm, selector); + }; } - editor.on('init', function () { - each(newFormats, function (format) { - editor.formatter.register(format.name, format); - }); + self.contextToolbars.push({ + id: Uuid.uuid('mcet'), + predicate: predicate, + items: items }); + }, - return { - type: 'menu', - items: createStylesMenu(), - onPostRender: function (e) { - editor.fire('renderFormatsMenu', { control: e.control }); - }, - itemDefaults: { - preview: true, + /** + * Adds a custom command to the editor, you can also override existing commands with this method. + * The command that you add can be executed with execCommand. + * + * @method addCommand + * @param {String} name Command name to add/override. + * @param {addCommandCallback} callback Function to execute when the command occurs. + * @param {Object} scope Optional scope to execute the function in. + * @example + * // Adds a custom command that later can be executed using execCommand + * tinymce.init({ + * ... + * + * setup: function(ed) { + * // Register example command + * ed.addCommand('mycommand', function(ui, v) { + * ed.windowManager.alert('Hello world!! Selection: ' + ed.selection.getContent({format: 'text'})); + * }); + * } + * }); + */ + addCommand: function (name, callback, scope) { + /** + * Callback function that gets called when a command is executed. + * + * @callback addCommandCallback + * @param {Boolean} ui Display UI state true/false. + * @param {Object} value Optional value for command. + * @return {Boolean} True/false state if the command was handled or not. + */ + this.editorCommands.addCommand(name, callback, scope); + }, - textStyle: function () { - if (this.settings.format) { - return editor.formatter.getCssText(this.settings.format); - } - }, + /** + * Adds a custom query state command to the editor, you can also override existing commands with this method. + * The command that you add can be executed with queryCommandState function. + * + * @method addQueryStateHandler + * @param {String} name Command name to add/override. + * @param {addQueryStateHandlerCallback} callback Function to execute when the command state retrieval occurs. + * @param {Object} scope Optional scope to execute the function in. + */ + addQueryStateHandler: function (name, callback, scope) { + /** + * Callback function that gets called when a queryCommandState is executed. + * + * @callback addQueryStateHandlerCallback + * @return {Boolean} True/false state if the command is enabled or not like is it bold. + */ + this.editorCommands.addQueryStateHandler(name, callback, scope); + }, - onPostRender: function () { - var self = this; + /** + * Adds a custom query value command to the editor, you can also override existing commands with this method. + * The command that you add can be executed with queryCommandValue function. + * + * @method addQueryValueHandler + * @param {String} name Command name to add/override. + * @param {addQueryValueHandlerCallback} callback Function to execute when the command value retrieval occurs. + * @param {Object} scope Optional scope to execute the function in. + */ + addQueryValueHandler: function (name, callback, scope) { + /** + * Callback function that gets called when a queryCommandValue is executed. + * + * @callback addQueryValueHandlerCallback + * @return {Object} Value of the command or undefined. + */ + this.editorCommands.addQueryValueHandler(name, callback, scope); + }, - self.parent().on('show', function () { - var formatName, command; + /** + * Adds a keyboard shortcut for some command or function. + * + * @method addShortcut + * @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} sc Optional scope to execute the function in. + * @return {Boolean} true/false state if the shortcut was added or not. + */ + addShortcut: function (pattern, desc, cmdFunc, scope) { + this.shortcuts.add(pattern, desc, cmdFunc, scope); + }, - formatName = self.settings.format; - if (formatName) { - self.disabled(!editor.formatter.canApply(formatName)); - self.active(editor.formatter.match(formatName)); - } + /** + * Executes a command on the current instance. These commands can be TinyMCE internal commands prefixed with "mce" or + * they can be build in browser commands such as "Bold". A compleate list of browser commands is available on MSDN or Mozilla.org. + * This function will dispatch the execCommand function on each plugin, theme or the execcommand_callback option if none of these + * return true it will handle the command as a internal browser command. + * + * @method execCommand + * @param {String} cmd Command name to execute, for example mceLink or Bold. + * @param {Boolean} ui True/false state if a UI (dialog) should be presented or not. + * @param {mixed} value Optional command value, this can be anything. + * @param {Object} args Optional arguments object. + */ + execCommand: function (cmd, ui, value, args) { + return this.editorCommands.execCommand(cmd, ui, value, args); + }, - command = self.settings.cmd; - if (command) { - self.active(editor.queryCommandState(command)); - } - }); - }, + /** + * Returns a command specific state, for example if bold is enabled or not. + * + * @method queryCommandState + * @param {string} cmd Command to query state from. + * @return {Boolean} Command specific state, for example if bold is enabled or not. + */ + queryCommandState: function (cmd) { + return this.editorCommands.queryCommandState(cmd); + }, - onclick: function () { - if (this.settings.format) { - toggleFormat(this.settings.format); - } + /** + * Returns a command specific value, for example the current font size. + * + * @method queryCommandValue + * @param {string} cmd Command to query value from. + * @return {Object} Command specific value, for example the current font size. + */ + queryCommandValue: function (cmd) { + return this.editorCommands.queryCommandValue(cmd); + }, - if (this.settings.cmd) { - editor.execCommand(this.settings.cmd); - } - } - } - }; - } + /** + * Returns true/false if the command is supported or not. + * + * @method queryCommandSupported + * @param {String} cmd Command that we check support for. + * @return {Boolean} true/false if the command is supported or not. + */ + queryCommandSupported: function (cmd) { + return this.editorCommands.queryCommandSupported(cmd); + }, - formatMenu = createFormatMenu(); + /** + * Shows the editor and hides any textarea/div that the editor is supposed to replace. + * + * @method show + */ + show: function () { + var self = this; - function initOnPostRender(name) { - return function () { - var self = this; + if (self.hidden) { + self.hidden = false; - // TODO: Fix this - if (editor.formatter) { - editor.formatter.formatChanged(name, function (state) { - self.active(state); - }); + if (self.inline) { + self.getBody().contentEditable = true; } else { - editor.on('init', function () { - editor.formatter.formatChanged(name, function (state) { - self.active(state); - }); - }); - } - }; - } - - // Simple format controls : - each({ - bold: 'Bold', - italic: 'Italic', - underline: 'Underline', - strikethrough: 'Strikethrough', - subscript: 'Subscript', - superscript: 'Superscript' - }, function (text, name) { - editor.addButton(name, { - tooltip: text, - onPostRender: initOnPostRender(name), - onclick: function () { - toggleFormat(name); + DOM.show(self.getContainer()); + DOM.hide(self.id); } - }); - }); - - // Simple command controls :[,] - each({ - outdent: ['Decrease indent', 'Outdent'], - indent: ['Increase indent', 'Indent'], - cut: ['Cut', 'Cut'], - copy: ['Copy', 'Copy'], - paste: ['Paste', 'Paste'], - help: ['Help', 'mceHelp'], - selectall: ['Select all', 'SelectAll'], - removeformat: ['Clear formatting', 'RemoveFormat'], - visualaid: ['Visual aids', 'mceToggleVisualAid'], - newdocument: ['New document', 'mceNewDocument'] - }, function (item, name) { - editor.addButton(name, { - tooltip: item[0], - cmd: item[1] - }); - }); - // Simple command controls with format state - each({ - blockquote: ['Blockquote', 'mceBlockQuote'], - subscript: ['Subscript', 'Subscript'], - superscript: ['Superscript', 'Superscript'], - alignleft: ['Align left', 'JustifyLeft'], - aligncenter: ['Align center', 'JustifyCenter'], - alignright: ['Align right', 'JustifyRight'], - alignjustify: ['Justify', 'JustifyFull'], - alignnone: ['No alignment', 'JustifyNone'] - }, function (item, name) { - editor.addButton(name, { - tooltip: item[0], - cmd: item[1], - onPostRender: initOnPostRender(name) - }); - }); + self.load(); + self.fire('show'); + } + }, - function toggleUndoRedoState(type) { - return function () { - var self = this; + /** + * Hides the editor and shows any textarea/div that the editor is supposed to replace. + * + * @method hide + */ + hide: function () { + var self = this, doc = self.getDoc(); - function checkState() { - var typeFn = type == 'redo' ? 'hasRedo' : 'hasUndo'; - return editor.undoManager ? editor.undoManager[typeFn]() : false; + if (!self.hidden) { + // Fixed bug where IE has a blinking cursor left from the editor + if (ie && doc && !self.inline) { + doc.execCommand('SelectAll'); } - self.disabled(!checkState()); - editor.on('Undo Redo AddUndo TypingUndo ClearUndos SwitchMode', function () { - self.disabled(editor.readonly || !checkState()); - }); - }; - } - - function toggleVisualAidState() { - var self = this; - - editor.on('VisualAid', function (e) { - self.active(e.hasVisual); - }); + // We must save before we hide so Safari doesn't crash + self.save(); - self.active(editor.hasVisual); - } + if (self.inline) { + self.getBody().contentEditable = false; - var trimMenuItems = function (menuItems) { - var outputMenuItems = menuItems; + // Make sure the editor gets blurred + if (self == self.editorManager.focusedEditor) { + self.editorManager.focusedEditor = null; + } + } else { + DOM.hide(self.getContainer()); + DOM.setStyle(self.id, 'display', self.orgDisplay); + } - if (outputMenuItems.length > 0 && outputMenuItems[0].text === '-') { - outputMenuItems = outputMenuItems.slice(1); + self.hidden = true; + self.fire('hide'); } + }, - if (outputMenuItems.length > 0 && outputMenuItems[outputMenuItems.length - 1].text === '-') { - outputMenuItems = outputMenuItems.slice(0, outputMenuItems.length - 1); - } + /** + * Returns true/false if the editor is hidden or not. + * + * @method isHidden + * @return {Boolean} True/false if the editor is hidden or not. + */ + isHidden: function () { + return !!this.hidden; + }, - return outputMenuItems; - }; + /** + * Sets the progress state, this will display a throbber/progess for the editor. + * This is ideal for asynchronous operations like an AJAX save call. + * + * @method setProgressState + * @param {Boolean} state Boolean state if the progress should be shown or hidden. + * @param {Number} time Optional time to wait before the progress gets shown. + * @return {Boolean} Same as the input state. + * @example + * // Show progress for the active editor + * tinymce.activeEditor.setProgressState(true); + * + * // Hide progress for the active editor + * tinymce.activeEditor.setProgressState(false); + * + * // Show progress after 3 seconds + * tinymce.activeEditor.setProgressState(true, 3000); + */ + setProgressState: function (state, time) { + this.fire('ProgressState', { state: state, time: time }); + }, - var createCustomMenuItems = function (names) { - var items, nameList; + /** + * Loads contents from the textarea or div element that got converted into an editor instance. + * This method will move the contents from that textarea or div into the editor by using setContent + * so all events etc that method has will get dispatched as well. + * + * @method load + * @param {Object} args Optional content object, this gets passed around through the whole load process. + * @return {String} HTML string that got set into the editor. + */ + load: function (args) { + var self = this, elm = self.getElement(), html; - if (typeof names === 'string') { - nameList = names.split(' '); - } else if (Tools.isArray(names)) { - return flatten(Tools.map(names, createCustomMenuItems)); + if (self.removed) { + return ''; } - items = Tools.grep(nameList, function (name) { - return name === '|' || name in editor.menuItems; - }); - - return Tools.map(items, function (name) { - return name === '|' ? { text: '-' } : editor.menuItems[name]; - }); - }; - - var createContextMenuItems = function (context) { - var outputMenuItems = [{ text: '-' }]; - var menuItems = Tools.grep(editor.menuItems, function (menuItem) { - return menuItem.context === context; - }); - - Tools.each(menuItems, function (menuItem) { - if (menuItem.separator == 'before') { - outputMenuItems.push({ text: '|' }); - } + if (elm) { + args = args || {}; + args.load = true; - if (menuItem.prependToContext) { - outputMenuItems.unshift(menuItem); - } else { - outputMenuItems.push(menuItem); - } + html = self.setContent(elm.value !== undefined ? elm.value : elm.innerHTML, args); + args.element = elm; - if (menuItem.separator == 'after') { - outputMenuItems.push({ text: '|' }); + if (!args.no_events) { + self.fire('LoadContent', args); } - }); - return outputMenuItems; - }; + args.element = elm = null; - var createInsertMenu = function (editorSettings) { - if (editorSettings.insert_button_items) { - return trimMenuItems(createCustomMenuItems(editorSettings.insert_button_items)); - } else { - return trimMenuItems(createContextMenuItems('insert')); + return html; } - }; - - editor.addButton('undo', { - tooltip: 'Undo', - onPostRender: toggleUndoRedoState('undo'), - cmd: 'undo' - }); - - editor.addButton('redo', { - tooltip: 'Redo', - onPostRender: toggleUndoRedoState('redo'), - cmd: 'redo' - }); - - editor.addMenuItem('newdocument', { - text: 'New document', - icon: 'newdocument', - cmd: 'mceNewDocument' - }); - - editor.addMenuItem('undo', { - text: 'Undo', - icon: 'undo', - shortcut: 'Meta+Z', - onPostRender: toggleUndoRedoState('undo'), - cmd: 'undo' - }); - - editor.addMenuItem('redo', { - text: 'Redo', - icon: 'redo', - shortcut: 'Meta+Y', - onPostRender: toggleUndoRedoState('redo'), - cmd: 'redo' - }); - - editor.addMenuItem('visualaid', { - text: 'Visual aids', - selectable: true, - onPostRender: toggleVisualAidState, - cmd: 'mceToggleVisualAid' - }); + }, - editor.addButton('remove', { - tooltip: 'Remove', - icon: 'remove', - cmd: 'Delete' - }); + /** + * Saves the contents from a editor out to the textarea or div element that got converted into an editor instance. + * This method will move the HTML contents from the editor into that textarea or div by getContent + * so all events etc that method has will get dispatched as well. + * + * @method save + * @param {Object} args Optional content object, this gets passed around through the whole save process. + * @return {String} HTML string that got set into the textarea/div. + */ + save: function (args) { + var self = this, elm = self.getElement(), html, form; - editor.addButton('insert', { - type: 'menubutton', - icon: 'insert', - menu: [], - oncreatemenu: function () { - this.menu.add(createInsertMenu(editor.settings)); - this.menu.renderNew(); + if (!elm || !self.initialized || self.removed) { + return; } - }); - each({ - cut: ['Cut', 'Cut', 'Meta+X'], - copy: ['Copy', 'Copy', 'Meta+C'], - paste: ['Paste', 'Paste', 'Meta+V'], - selectall: ['Select all', 'SelectAll', 'Meta+A'], - bold: ['Bold', 'Bold', 'Meta+B'], - italic: ['Italic', 'Italic', 'Meta+I'], - underline: ['Underline', 'Underline', 'Meta+U'], - strikethrough: ['Strikethrough', 'Strikethrough'], - subscript: ['Subscript', 'Subscript'], - superscript: ['Superscript', 'Superscript'], - removeformat: ['Clear formatting', 'RemoveFormat'] - }, function (item, name) { - editor.addMenuItem(name, { - text: item[0], - icon: name, - shortcut: item[2], - cmd: item[1] - }); - }); + args = args || {}; + args.save = true; - editor.on('mousedown', function () { - FloatPanel.hideAll(); - }); + args.element = elm; + html = args.content = self.getContent(args); - function toggleFormat(fmt) { - if (fmt.control) { - fmt = fmt.control.value(); + if (!args.no_events) { + self.fire('SaveContent', args); } - if (fmt) { - editor.execCommand('mceToggleFormat', false, fmt); + // Always run this internal event + if (args.format == 'raw') { + self.fire('RawSaveContent', args); } - } - - function hideMenuObjects(menu) { - var count = menu.length; - Tools.each(menu, function (item) { - if (item.menu) { - item.hidden = hideMenuObjects(item.menu) === 0; - } + html = args.content; - var formatName = item.format; - if (formatName) { - item.hidden = !editor.formatter.canApply(formatName); + if (!/TEXTAREA|INPUT/i.test(elm.nodeName)) { + // Update DIV element when not in inline mode + if (!self.inline) { + elm.innerHTML = html; } - if (item.hidden) { - count--; + // Update hidden form element + if ((form = DOM.getParent(self.id, 'form'))) { + each(form.elements, function (elm) { + if (elm.name == self.id) { + elm.value = html; + return false; + } + }); } - }); - - return count; - } - - function hideFormatMenuItems(menu) { - var count = menu.items().length; + } else { + elm.value = html; + } - menu.items().each(function (item) { - if (item.menu) { - item.visible(hideFormatMenuItems(item.menu) > 0); - } + args.element = elm = null; - if (!item.menu && item.settings.menu) { - item.visible(hideMenuObjects(item.settings.menu) > 0); - } + if (args.set_dirty !== false) { + self.setDirty(false); + } - var formatName = item.settings.format; - if (formatName) { - item.visible(editor.formatter.canApply(formatName)); - } + return html; + }, - if (!item.visible()) { - count--; - } - }); + /** + * Sets the specified content to the editor instance, this will cleanup the content before it gets set using + * the different cleanup rules options. + * + * @method setContent + * @param {String} content Content to set to editor, normally HTML contents but can be other formats as well. + * @param {Object} args Optional content object, this gets passed around through the whole set process. + * @return {String} HTML string that got set into the editor. + * @example + * // Sets the HTML contents of the activeEditor editor + * tinymce.activeEditor.setContent('some html'); + * + * // Sets the raw contents of the activeEditor editor + * tinymce.activeEditor.setContent('some html', {format: 'raw'}); + * + * // Sets the content of a specific editor (my_editor in this example) + * tinymce.get('my_editor').setContent(data); + * + * // Sets the bbcode contents of the activeEditor editor if the bbcode plugin was added + * tinymce.activeEditor.setContent('[b]some[/b] html', {format: 'bbcode'}); + */ + setContent: function (content, args) { + var self = this, body = self.getBody(), forcedRootBlockName, padd; - return count; - } + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.set = true; + args.content = content; - editor.addButton('styleselect', { - type: 'menubutton', - text: 'Formats', - menu: formatMenu, - onShowMenu: function () { - if (editor.settings.style_formats_autohide) { - hideFormatMenuItems(this.menu); - } + // Do preprocessing + if (!args.no_events) { + self.fire('BeforeSetContent', args); } - }); - editor.addButton('formatselect', function () { - var items = [], blocks = createFormats(editor.settings.block_formats || - 'Paragraph=p;' + - 'Heading 1=h1;' + - 'Heading 2=h2;' + - 'Heading 3=h3;' + - 'Heading 4=h4;' + - 'Heading 5=h5;' + - 'Heading 6=h6;' + - 'Preformatted=pre' - ); - - each(blocks, function (block) { - items.push({ - text: block[0], - value: block[1], - textStyle: function () { - return editor.formatter.getCssText(block[1]); - } - }); - }); - - return { - type: 'listbox', - text: blocks[0][0], - values: items, - fixedWidth: true, - onselect: toggleFormat, - onPostRender: createListBoxChangeHandler(items) - }; - }); + content = args.content; - editor.addButton('fontselect', function () { - var defaultFontsFormats = - 'Andale Mono=andale mono,monospace;' + - 'Arial=arial,helvetica,sans-serif;' + - 'Arial Black=arial black,sans-serif;' + - 'Book Antiqua=book antiqua,palatino,serif;' + - 'Comic Sans MS=comic sans ms,sans-serif;' + - 'Courier New=courier new,courier,monospace;' + - 'Georgia=georgia,palatino,serif;' + - 'Helvetica=helvetica,arial,sans-serif;' + - 'Impact=impact,sans-serif;' + - 'Symbol=symbol;' + - 'Tahoma=tahoma,arial,helvetica,sans-serif;' + - 'Terminal=terminal,monaco,monospace;' + - 'Times New Roman=times new roman,times,serif;' + - 'Trebuchet MS=trebuchet ms,geneva,sans-serif;' + - 'Verdana=verdana,geneva,sans-serif;' + - 'Webdings=webdings;' + - 'Wingdings=wingdings,zapf dingbats'; - - var items = [], fonts = createFormats(editor.settings.font_formats || defaultFontsFormats); - - each(fonts, function (font) { - items.push({ - text: { raw: font[0] }, - value: font[1], - textStyle: font[1].indexOf('dings') == -1 ? 'font-family:' + font[1] : '' - }); - }); + // Padd empty content in Gecko and Safari. Commands will otherwise fail on the content + // It will also be impossible to place the caret in the editor unless there is a BR element present + if (content.length === 0 || /^\s+$/.test(content)) { + padd = ie && ie < 11 ? '' : '
    '; - return { - type: 'listbox', - text: 'Font Family', - tooltip: 'Font Family', - values: items, - fixedWidth: true, - onPostRender: createFontNameListBoxChangeHandler(items), - onselect: function (e) { - if (e.control.settings.value) { - editor.execCommand('FontName', false, e.control.settings.value); - } + // Todo: There is a lot more root elements that need special padding + // so separate this and add all of them at some point. + if (body.nodeName == 'TABLE') { + content = '' + padd + ''; + } else if (/^(UL|OL)$/.test(body.nodeName)) { + content = '
  • ' + padd + '
  • '; } - }; - }); - editor.addButton('fontsizeselect', function () { - var items = [], defaultFontsizeFormats = '8pt 10pt 12pt 14pt 18pt 24pt 36pt'; - var fontsizeFormats = editor.settings.fontsize_formats || defaultFontsizeFormats; + forcedRootBlockName = self.settings.forced_root_block; - each(fontsizeFormats.split(' '), function (item) { - var text = item, value = item; - // Allow text=value font sizes. - var values = item.split('='); - if (values.length > 1) { - text = values[0]; - value = values[1]; + // Check if forcedRootBlock is configured and that the block is a valid child of the body + if (forcedRootBlockName && self.schema.isValidChild(body.nodeName.toLowerCase(), forcedRootBlockName.toLowerCase())) { + // Padd with bogus BR elements on modern browsers and IE 7 and 8 since they don't render empty P tags properly + content = padd; + content = self.dom.createHTML(forcedRootBlockName, self.settings.forced_root_block_attrs, content); + } else if (!ie && !content) { + // We need to add a BR when forced_root_block is disabled on non IE browsers to place the caret + content = '
    '; } - items.push({ text: text, value: value }); - }); - return { - type: 'listbox', - text: 'Font Sizes', - tooltip: 'Font Sizes', - values: items, - fixedWidth: true, - onPostRender: createFontSizeListBoxChangeHandler(items), - onclick: function (e) { - if (e.control.settings.value) { - editor.execCommand('FontSize', false, e.control.settings.value); - } + self.dom.setHTML(body, content); + + self.fire('SetContent', args); + } else { + // Parse and serialize the html + if (args.format !== 'raw') { + content = new Serializer({ + validate: self.validate + }, self.schema).serialize( + self.parser.parse(content, { isRootContent: true }) + ); } - }; - }); - editor.addMenuItem('formats', { - text: 'Formats', - menu: formatMenu - }); - } + // Set the new cleaned contents to the editor + args.content = trim(content); + self.dom.setHTML(body, args.content); - return {}; - } -); + // Do post processing + if (!args.no_events) { + self.fire('SetContent', args); + } -/** - * GridLayout.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 - */ + // Don't normalize selection if the focused element isn't the body in + // content editable mode since it will steal focus otherwise + /*if (!self.settings.content_editable || document.activeElement === self.getBody()) { + self.selection.normalize(); + }*/ + } -/** - * This layout manager places controls in a grid. - * - * @setting {Number} spacing Spacing between controls. - * @setting {Number} spacingH Horizontal spacing between controls. - * @setting {Number} spacingV Vertical spacing between controls. - * @setting {Number} columns Number of columns to use. - * @setting {String/Array} alignH start|end|center|stretch or array of values for each column. - * @setting {String/Array} alignV start|end|center|stretch or array of values for each column. - * @setting {String} pack start|end - * - * @class tinymce.ui.GridLayout - * @extends tinymce.ui.AbsoluteLayout - */ -define( - 'tinymce.core.ui.GridLayout', - [ - "tinymce.core.ui.AbsoluteLayout" - ], - function (AbsoluteLayout) { - "use strict"; + return args.content; + }, - return AbsoluteLayout.extend({ /** - * Recalculates the positions of the controls in the specified container. + * Gets the content from the editor instance, this will cleanup the content before it gets returned using + * the different cleanup rules options. + * + * @method getContent + * @param {Object} args Optional content object, this gets passed around through the whole get process. + * @return {String} Cleaned content string, normally HTML contents. + * @example + * // Get the HTML contents of the currently active editor + * console.debug(tinymce.activeEditor.getContent()); + * + * // Get the raw contents of the currently active editor + * tinymce.activeEditor.getContent({format: 'raw'}); * - * @method recalc - * @param {tinymce.ui.Container} container Container instance to recalc. + * // Get content of a specific editor: + * tinymce.get('content id').getContent() */ - recalc: function (container) { - var settings, rows, cols, items, contLayoutRect, width, height, rect, - ctrlLayoutRect, ctrl, x, y, posX, posY, ctrlSettings, contPaddingBox, align, spacingH, spacingV, alignH, alignV, maxX, maxY, - colWidths = [], rowHeights = [], ctrlMinWidth, ctrlMinHeight, availableWidth, availableHeight, reverseRows, idx; - - // Get layout settings - settings = container.settings; - items = container.items().filter(':visible'); - contLayoutRect = container.layoutRect(); - cols = settings.columns || Math.ceil(Math.sqrt(items.length)); - rows = Math.ceil(items.length / cols); - spacingH = settings.spacingH || settings.spacing || 0; - spacingV = settings.spacingV || settings.spacing || 0; - alignH = settings.alignH || settings.align; - alignV = settings.alignV || settings.align; - contPaddingBox = container.paddingBox; - reverseRows = 'reverseRows' in settings ? settings.reverseRows : container.isRtl(); - - if (alignH && typeof alignH == "string") { - alignH = [alignH]; - } - - if (alignV && typeof alignV == "string") { - alignV = [alignV]; - } - - // Zero padd columnWidths - for (x = 0; x < cols; x++) { - colWidths.push(0); - } - - // Zero padd rowHeights - for (y = 0; y < rows; y++) { - rowHeights.push(0); - } - - // Calculate columnWidths and rowHeights - for (y = 0; y < rows; y++) { - for (x = 0; x < cols; x++) { - ctrl = items[y * cols + x]; - - // Out of bounds - if (!ctrl) { - break; - } - - ctrlLayoutRect = ctrl.layoutRect(); - ctrlMinWidth = ctrlLayoutRect.minW; - ctrlMinHeight = ctrlLayoutRect.minH; - - colWidths[x] = ctrlMinWidth > colWidths[x] ? ctrlMinWidth : colWidths[x]; - rowHeights[y] = ctrlMinHeight > rowHeights[y] ? ctrlMinHeight : rowHeights[y]; - } - } - - // Calculate maxX - availableWidth = contLayoutRect.innerW - contPaddingBox.left - contPaddingBox.right; - for (maxX = 0, x = 0; x < cols; x++) { - maxX += colWidths[x] + (x > 0 ? spacingH : 0); - availableWidth -= (x > 0 ? spacingH : 0) + colWidths[x]; - } + getContent: function (args) { + var self = this, content, body = self.getBody(); - // Calculate maxY - availableHeight = contLayoutRect.innerH - contPaddingBox.top - contPaddingBox.bottom; - for (maxY = 0, y = 0; y < rows; y++) { - maxY += rowHeights[y] + (y > 0 ? spacingV : 0); - availableHeight -= (y > 0 ? spacingV : 0) + rowHeights[y]; + if (self.removed) { + return ''; } - maxX += contPaddingBox.left + contPaddingBox.right; - maxY += contPaddingBox.top + contPaddingBox.bottom; - - // Calculate minW/minH - rect = {}; - rect.minW = maxX + (contLayoutRect.w - contLayoutRect.innerW); - rect.minH = maxY + (contLayoutRect.h - contLayoutRect.innerH); - - rect.contentW = rect.minW - contLayoutRect.deltaW; - rect.contentH = rect.minH - contLayoutRect.deltaH; - rect.minW = Math.min(rect.minW, contLayoutRect.maxW); - rect.minH = Math.min(rect.minH, contLayoutRect.maxH); - rect.minW = Math.max(rect.minW, contLayoutRect.startMinWidth); - rect.minH = Math.max(rect.minH, contLayoutRect.startMinHeight); - - // Resize container container if minSize was changed - if (contLayoutRect.autoResize && (rect.minW != contLayoutRect.minW || rect.minH != contLayoutRect.minH)) { - rect.w = rect.minW; - rect.h = rect.minH; - - container.layoutRect(rect); - this.recalc(container); - - // Forced recalc for example if items are hidden/shown - if (container._lastRect === null) { - var parentCtrl = container.parent(); - if (parentCtrl) { - parentCtrl._lastRect = null; - parentCtrl.recalc(); - } - } - - return; - } + // Setup args object + args = args || {}; + args.format = args.format || 'html'; + args.get = true; + args.getInner = true; - // Update contentW/contentH so absEnd moves correctly - if (contLayoutRect.autoResize) { - rect = container.layoutRect(rect); - rect.contentW = rect.minW - contLayoutRect.deltaW; - rect.contentH = rect.minH - contLayoutRect.deltaH; + // Do preprocessing + if (!args.no_events) { + self.fire('BeforeGetContent', args); } - var flexV; - - if (settings.packV == 'start') { - flexV = 0; + // Get raw contents or by default the cleaned contents + if (args.format == 'raw') { + content = Tools.trim(self.serializer.getTrimmedContent()); + } else if (args.format == 'text') { + content = body.innerText || body.textContent; } else { - flexV = availableHeight > 0 ? Math.floor(availableHeight / rows) : 0; + content = self.serializer.serialize(body, args); } - // Calculate totalFlex - var totalFlex = 0; - var flexWidths = settings.flexWidths; - if (flexWidths) { - for (x = 0; x < flexWidths.length; x++) { - totalFlex += flexWidths[x]; - } + // Trim whitespace in beginning/end of HTML + if (args.format != 'text') { + args.content = trim(content); } else { - totalFlex = cols; + args.content = content; } - // Calculate new column widths based on flex values - var ratio = availableWidth / totalFlex; - for (x = 0; x < cols; x++) { - colWidths[x] += flexWidths ? flexWidths[x] * ratio : ratio; + // Do post processing + if (!args.no_events) { + self.fire('GetContent', args); } - // Move/resize controls - posY = contPaddingBox.top; - for (y = 0; y < rows; y++) { - posX = contPaddingBox.left; - height = rowHeights[y] + flexV; - - for (x = 0; x < cols; x++) { - if (reverseRows) { - idx = y * cols + cols - 1 - x; - } else { - idx = y * cols + x; - } - - ctrl = items[idx]; - - // No more controls to render then break - if (!ctrl) { - break; - } - - // Get control settings and calculate x, y - ctrlSettings = ctrl.settings; - ctrlLayoutRect = ctrl.layoutRect(); - width = Math.max(colWidths[x], ctrlLayoutRect.startMinWidth); - ctrlLayoutRect.x = posX; - ctrlLayoutRect.y = posY; - - // Align control horizontal - align = ctrlSettings.alignH || (alignH ? (alignH[x] || alignH[0]) : null); - if (align == "center") { - ctrlLayoutRect.x = posX + (width / 2) - (ctrlLayoutRect.w / 2); - } else if (align == "right") { - ctrlLayoutRect.x = posX + width - ctrlLayoutRect.w; - } else if (align == "stretch") { - ctrlLayoutRect.w = width; - } - - // Align control vertical - align = ctrlSettings.alignV || (alignV ? (alignV[x] || alignV[0]) : null); - if (align == "center") { - ctrlLayoutRect.y = posY + (height / 2) - (ctrlLayoutRect.h / 2); - } else if (align == "bottom") { - ctrlLayoutRect.y = posY + height - ctrlLayoutRect.h; - } else if (align == "stretch") { - ctrlLayoutRect.h = height; - } - - ctrl.layoutRect(ctrlLayoutRect); - - posX += width + spacingH; - - if (ctrl.recalc) { - ctrl.recalc(); - } - } + return args.content; + }, - posY += height + spacingV; + /** + * Inserts content at caret position. + * + * @method insertContent + * @param {String} content Content to insert. + * @param {Object} args Optional args to pass to insert call. + */ + insertContent: function (content, args) { + if (args) { + content = extend({ content: content }, args); } - } - }); - } -); - -/** - * Iframe.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 - */ -/*jshint scripturl:true */ + this.execCommand('mceInsertContent', false, content); + }, -/** - * This class creates an iframe. - * - * @setting {String} url Url to open in the iframe. - * - * @-x-less Iframe.less - * @class tinymce.ui.Iframe - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.Iframe', - [ - "tinymce.core.ui.Widget", - "tinymce.core.util.Delay" - ], - function (Widget, Delay) { - "use strict"; + /** + * Returns true/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. + * + * The dirty state is automatically set to true if you do modifications to the content in other + * words when new undo levels is created or if you undo/redo to update the contents of the editor. It will also be set + * to false if you call editor.save(). + * + * @method isDirty + * @return {Boolean} True/false if the editor is dirty or not. It will get dirty if the user has made modifications to the contents. + * @example + * if (tinymce.activeEditor.isDirty()) + * alert("You must save your contents."); + */ + isDirty: function () { + return !this.isNotDirty; + }, - return Widget.extend({ /** - * Renders the control as a HTML string. + * Explicitly sets the dirty state. This will fire the dirty event if the editor dirty state is changed from false to true + * by invoking this method. + * + * @method setDirty + * @param {Boolean} state True/false if the editor is considered dirty. + * @example + * function ajaxSave() { + * var editor = tinymce.get('elm1'); * - * @method renderHtml - * @return {String} HTML representing the control. + * // Save contents using some XHR call + * alert(editor.getContent()); + * + * editor.setDirty(false); // Force not dirty state + * } */ - renderHtml: function () { - var self = this; + setDirty: function (state) { + var oldState = !this.isNotDirty; - self.classes.add('iframe'); - self.canFocus = false; + this.isNotDirty = !state; - /*eslint no-script-url:0 */ - return ( - '' - ); + if (state && state != oldState) { + this.fire('dirty'); + } }, /** - * Setter for the iframe source. + * Sets the editor mode. Mode can be for example "design", "code" or "readonly". * - * @method src - * @param {String} src Source URL for iframe. + * @method setMode + * @param {String} mode Mode to set the editor in. */ - src: function (src) { - this.getEl().src = src; + setMode: function (mode) { + Mode.setMode(this, mode); }, /** - * Inner HTML for the iframe. + * Returns the editors container element. The container element wrappes in + * all the elements added to the page for the editor. Such as UI, iframe etc. * - * @method html - * @param {String} html HTML string to set as HTML inside the iframe. - * @param {function} callback Optional callback to execute when the iframe body is filled with contents. - * @return {tinymce.ui.Iframe} Current iframe control. + * @method getContainer + * @return {Element} HTML DOM element for the editor container. */ - html: function (html, callback) { - var self = this, body = this.getEl().contentWindow.document.body; - - // Wait for iframe to initialize IE 10 takes time - if (!body) { - Delay.setTimeout(function () { - self.html(html); - }); - } else { - body.innerHTML = html; + getContainer: function () { + var self = this; - if (callback) { - callback(); - } + if (!self.container) { + self.container = DOM.get(self.editorContainer || self.id + '_parent'); } - return this; - } - }); - } -); - -/** - * InfoBox.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 - */ - -/** - * .... - * - * @-x-less InfoBox.less - * @class tinymce.ui.InfoBox - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.InfoBox', - [ - "tinymce.core.ui.Widget" - ], - function (Widget) { - "use strict"; + return self.container; + }, - return Widget.extend({ /** - * Constructs a instance with the specified settings. + * Returns the editors content area container element. The this element is the one who + * holds the iframe or the editable element. * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} multiline Multiline label. + * @method getContentAreaContainer + * @return {Element} HTML DOM element for the editor area container. */ - init: function (settings) { - var self = this; - - self._super(settings); - self.classes.add('widget').add('infobox'); - self.canFocus = false; + getContentAreaContainer: function () { + return this.contentAreaContainer; }, - severity: function (level) { - this.classes.remove('error'); - this.classes.remove('warning'); - this.classes.remove('success'); - this.classes.add(level); - }, + /** + * Returns the target element/textarea that got replaced with a TinyMCE editor instance. + * + * @method getElement + * @return {Element} HTML DOM element for the replaced element. + */ + getElement: function () { + if (!this.targetElm) { + this.targetElm = DOM.get(this.id); + } - help: function (state) { - this.state.set('help', state); + return this.targetElm; }, /** - * Renders the control as a HTML string. + * Returns the iframes window object. * - * @method renderHtml - * @return {String} HTML representing the control. + * @method getWin + * @return {Window} Iframe DOM window object. */ - renderHtml: function () { - var self = this, prefix = self.classPrefix; + getWin: function () { + var self = this, elm; - return ( - '
    ' + - '
    ' + - self.encode(self.state.get('text')) + - '' + - '
    ' + - '
    ' - ); - }, + if (!self.contentWindow) { + elm = self.iframeElement; - bindStates: function () { - var self = this; + if (elm) { + self.contentWindow = elm.contentWindow; + } + } - self.state.on('change:text', function (e) { - self.getEl('body').firstChild.data = self.encode(e.value); + return self.contentWindow; + }, - if (self.state.get('rendered')) { - self.updateLayoutRect(); - } - }); + /** + * Returns the iframes document object. + * + * @method getDoc + * @return {Document} Iframe DOM document object. + */ + getDoc: function () { + var self = this, win; - self.state.on('change:help', function (e) { - self.classes.toggle('has-help', e.value); + if (!self.contentDocument) { + win = self.getWin(); - if (self.state.get('rendered')) { - self.updateLayoutRect(); + if (win) { + self.contentDocument = win.document; } - }); - - return self._super(); - } - }); - } -); + } -/** - * Label.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 - */ + return self.contentDocument; + }, -/** - * This class creates a label element. A label is a simple text control - * that can be bound to other controls. - * - * @-x-less Label.less - * @class tinymce.ui.Label - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.Label', - [ - "tinymce.core.ui.Widget", - "tinymce.core.ui.DomUtils" - ], - function (Widget, DomUtils) { - "use strict"; + /** + * Returns the root element of the editable area. + * For a non-inline iframe-based editor, returns the iframe's body element. + * + * @method getBody + * @return {Element} The root element of the editable area. + */ + getBody: function () { + var doc = this.getDoc(); + return this.bodyElement || (doc ? doc.body : null); + }, - return Widget.extend({ /** - * Constructs a instance with the specified settings. + * URL converter function this gets executed each time a user adds an img, a or + * any other element that has a URL in it. This will be called both by the DOM and HTML + * manipulation functions. * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} multiline Multiline label. + * @method convertURL + * @param {string} url URL to convert. + * @param {string} name Attribute name src, href etc. + * @param {string/HTMLElement} elm Tag name or HTML DOM element depending on HTML or DOM insert. + * @return {string} Converted URL string. */ - init: function (settings) { - var self = this; + convertURL: function (url, name, elm) { + var self = this, settings = self.settings; - self._super(settings); - self.classes.add('widget').add('label'); - self.canFocus = false; + // Use callback instead + if (settings.urlconverter_callback) { + return self.execCallback('urlconverter_callback', url, elm, true, name); + } - if (settings.multiline) { - self.classes.add('autoscroll'); + // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs + if (!settings.convert_urls || (elm && elm.nodeName == 'LINK') || url.indexOf('file:') === 0 || url.length === 0) { + return url; } - if (settings.strong) { - self.classes.add('strong'); + // Convert to relative + if (settings.relative_urls) { + return self.documentBaseURI.toRelative(url); } + + // Convert to absolute + url = self.documentBaseURI.toAbsolute(url, settings.remove_script_host); + + return url; }, /** - * Initializes the current controls layout rect. - * This will be executed by the layout managers to determine the - * default minWidth/minHeight etc. + * Adds visual aid for tables, anchors etc so they can be more easily edited inside the editor. * - * @method initLayoutRect - * @return {Object} Layout rect instance. + * @method addVisual + * @param {Element} elm Optional root element to loop though to find tables etc that needs the visual aid. */ - initLayoutRect: function () { - var self = this, layoutRect = self._super(); - - if (self.settings.multiline) { - var size = DomUtils.getSize(self.getEl()); + addVisual: function (elm) { + var self = this, settings = self.settings, dom = self.dom, cls; - // Check if the text fits within maxW if not then try word wrapping it - if (size.width > layoutRect.maxW) { - layoutRect.minW = layoutRect.maxW; - self.classes.add('multiline'); - } + elm = elm || self.getBody(); - self.getEl().style.width = layoutRect.minW + 'px'; - layoutRect.startMinH = layoutRect.h = layoutRect.minH = Math.min(layoutRect.maxH, DomUtils.getSize(self.getEl()).height); + if (self.hasVisual === undefined) { + self.hasVisual = settings.visual; } - return layoutRect; + each(dom.select('table,a', elm), function (elm) { + var value; + + switch (elm.nodeName) { + case 'TABLE': + cls = settings.visual_table_class || 'mce-item-table'; + value = dom.getAttrib(elm, 'border'); + + if ((!value || value == '0') && self.hasVisual) { + dom.addClass(elm, cls); + } else { + dom.removeClass(elm, cls); + } + + return; + + case 'A': + if (!dom.getAttrib(elm, 'href', false)) { + value = dom.getAttrib(elm, 'name') || elm.id; + cls = settings.visual_anchor_class || 'mce-item-anchor'; + + if (value && self.hasVisual) { + dom.addClass(elm, cls); + } else { + dom.removeClass(elm, cls); + } + } + + return; + } + }); + + self.fire('VisualAid', { element: elm, hasVisual: self.hasVisual }); }, /** - * Repaints the control after a layout operation. + * Removes the editor from the dom and tinymce collection. * - * @method repaint + * @method remove */ - repaint: function () { + remove: function () { var self = this; - if (!self.settings.multiline) { - self.getEl().style.lineHeight = self.layoutRect().h + 'px'; - } + if (!self.removed) { + self.save(); + self.removed = 1; + self.unbindAllNativeEvents(); - return self._super(); - }, + // Remove any hidden input + if (self.hasHiddenInput) { + DOM.remove(self.getElement().nextSibling); + } + + if (!self.inline) { + // IE 9 has a bug where the selection stops working if you place the + // caret inside the editor then remove the iframe + if (ie && ie < 10) { + self.getDoc().execCommand('SelectAll', false, null); + } + + DOM.setStyle(self.id, 'display', self.orgDisplay); + self.getBody().onload = null; // Prevent #6816 + } + + self.fire('remove'); - severity: function (level) { - this.classes.remove('error'); - this.classes.remove('warning'); - this.classes.remove('success'); - this.classes.add(level); + self.editorManager.remove(self); + DOM.remove(self.getContainer()); + self._selectionOverrides.destroy(); + self.editorUpload.destroy(); + self.destroy(); + } }, /** - * Renders the control as a HTML string. + * Destroys the editor instance by removing all events, element references or other resources + * that could leak memory. This method will be called automatically when the page is unloaded + * but you can also call it directly if you know what you are doing. * - * @method renderHtml - * @return {String} HTML representing the control. + * @method destroy + * @param {Boolean} automatic Optional state if the destroy is an automatic destroy or user called one. */ - renderHtml: function () { - var self = this, targetCtrl, forName, forId = self.settings.forId; - var text = self.settings.html ? self.settings.html : self.encode(self.state.get('text')); - - if (!forId && (forName = self.settings.forName)) { - targetCtrl = self.getRoot().find('#' + forName)[0]; + destroy: function (automatic) { + var self = this, form; - if (targetCtrl) { - forId = targetCtrl._id; - } + // One time is enough + if (self.destroyed) { + return; } - if (forId) { - return ( - '' - ); + // If user manually calls destroy and not remove + // Users seems to have logic that calls destroy instead of remove + if (!automatic && !self.removed) { + self.remove(); + return; } - return ( - '' + - text + - '' - ); - }, + if (!automatic) { + self.editorManager.off('beforeunload', self._beforeUnload); - bindStates: function () { - var self = this; + // Manual destroy + if (self.theme && self.theme.destroy) { + self.theme.destroy(); + } - self.state.on('change:text', function (e) { - self.innerHtml(self.encode(e.value)); + // Destroy controls, selection and dom + self.selection.destroy(); + self.dom.destroy(); + } - if (self.state.get('rendered')) { - self.updateLayoutRect(); + form = self.formElement; + if (form) { + if (form._mceOldSubmit) { + form.submit = form._mceOldSubmit; + form._mceOldSubmit = null; } - }); - return self._super(); - } - }); - } -); + DOM.unbind(form, 'submit reset', self.formEventDelegate); + } -/** - * Toolbar.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 - */ + self.contentAreaContainer = self.formElement = self.container = self.editorContainer = null; + self.bodyElement = self.contentDocument = self.contentWindow = null; + self.iframeElement = self.targetElm = null; -/** - * Creates a new toolbar. - * - * @class tinymce.ui.Toolbar - * @extends tinymce.ui.Container - */ -define( - 'tinymce.core.ui.Toolbar', - [ - "tinymce.core.ui.Container" - ], - function (Container) { - "use strict"; + if (self.selection) { + self.selection = self.selection.win = self.selection.dom = self.selection.dom.doc = null; + } - return Container.extend({ - Defaults: { - role: 'toolbar', - layout: 'flow' + self.destroyed = 1; }, /** - * Constructs a instance with the specified settings. + * Uploads all data uri/blob uri images in the editor contents to server. * - * @constructor - * @param {Object} settings Name/value object with settings. + * @method uploadImages + * @param {function} callback Optional callback with images and status for each image. + * @return {tinymce.util.Promise} Promise instance. */ - init: function (settings) { - var self = this; - - self._super(settings); - self.classes.add('toolbar'); + uploadImages: function (callback) { + return this.editorUpload.uploadImages(callback); }, - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this; - - self.items().each(function (ctrl) { - ctrl.classes.add('toolbar-item'); - }); + // Internal functions - return self._super(); + _scanForImages: function () { + return this.editorUpload.scanForImages(); } - }); - } -); -/** - * MenuBar.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 new menubar. - * - * @-x-less MenuBar.less - * @class tinymce.ui.MenuBar - * @extends tinymce.ui.Container - */ -define( - 'tinymce.core.ui.MenuBar', - [ - "tinymce.core.ui.Toolbar" - ], - function (Toolbar) { - "use strict"; + extend(Editor.prototype, EditorObservable); - return Toolbar.extend({ - Defaults: { - role: 'menubar', - containerCls: 'menubar', - ariaRoot: true, - defaults: { - type: 'menubutton' - } - } - }); + return Editor; } ); + /** - * MenuButton.js + * FocusManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -54139,272 +41136,242 @@ define( */ /** - * Creates a new menu button. + * This class manages the focus/blur state of the editor. This class is needed since some + * browsers fire false focus/blur states when the selection is moved to a UI dialog or similar. + * + * This class will fire two events focus and blur on the editor instances that got affected. + * It will also handle the restore of selection when the focus is lost and returned. * - * @-x-less MenuButton.less - * @class tinymce.ui.MenuButton - * @extends tinymce.ui.Button + * @class tinymce.FocusManager */ define( - 'tinymce.core.ui.MenuButton', + 'tinymce.core.FocusManager', [ - "tinymce.core.ui.Button", - "tinymce.core.ui.Factory", - "tinymce.core.ui.MenuBar" + 'ephox.sugar.api.node.Element', + 'global!document', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.selection.SelectionBookmark', + 'tinymce.core.util.Delay' ], - function (Button, Factory, MenuBar) { - "use strict"; - - // TODO: Maybe add as some global function - function isChildOf(node, parent) { - while (node) { - if (parent === node) { - return true; - } - - node = node.parentNode; - } - - return false; - } - - var MenuButton = Button.extend({ - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - */ - init: function (settings) { - var self = this; - - self._renderOpen = true; - - self._super(settings); - settings = self.settings; - - self.classes.add('menubtn'); - - if (settings.fixedWidth) { - self.classes.add('fixed-width'); - } - - self.aria('haspopup', true); - - self.state.set('menu', settings.menu || self.render()); - }, + function (Element, document, DOMUtils, SelectionBookmark, Delay) { + var selectionChangeHandler, documentFocusInHandler, documentMouseUpHandler, DOM = DOMUtils.DOM; - /** - * Shows the menu for the button. - * - * @method showMenu - */ - showMenu: function (toggle) { - var self = this, menu; + var isUIElement = function (editor, elm) { + var customSelector = editor ? editor.settings.custom_ui_selector : ''; + var parent = DOM.getParent(elm, function (elm) { + return ( + FocusManager.isEditorUIElement(elm) || + (customSelector ? editor.dom.is(elm, customSelector) : false) + ); + }); + return parent !== null; + }; - if (self.menu && self.menu.visible() && toggle !== false) { - return self.hideMenu(); + /** + * Constructs a new focus manager instance. + * + * @constructor FocusManager + * @param {tinymce.EditorManager} editorManager Editor manager instance to handle focus for. + */ + function FocusManager(editorManager) { + function getActiveElement() { + try { + return document.activeElement; + } catch (ex) { + // IE sometimes fails to get the activeElement when resizing table + // TODO: Investigate this + return document.body; } + } - if (!self.menu) { - menu = self.state.get('menu') || []; - - // Is menu array then auto constuct menu control - if (menu.length) { - menu = { - type: 'menu', - items: menu - }; - } else { - menu.type = menu.type || 'menu'; - } - - if (!menu.renderTo) { - self.menu = Factory.create(menu).parent(self).renderTo(); - } else { - self.menu = menu.parent(self).show().renderTo(); - } + function registerEvents(e) { + var editor = e.editor; - self.fire('createmenu'); - self.menu.reflow(); - self.menu.on('cancel', function (e) { - if (e.control.parent() === self.menu) { - e.stopPropagation(); - self.focus(); - self.hideMenu(); + editor.on('init', function () { + editor.on('keyup mouseup nodechange', function (e) { + if (e.type === 'nodechange' && e.selectionChange) { + return; } + SelectionBookmark.store(editor); }); + }); - // Move focus to button when a menu item is selected/clicked - self.menu.on('select', function () { - self.focus(); - }); + editor.on('focusin', function () { + var focusedEditor = editorManager.focusedEditor; - self.menu.on('show hide', function (e) { - if (e.control == self.menu) { - self.activeMenu(e.type == 'show'); + if (focusedEditor != editor) { + if (focusedEditor) { + focusedEditor.fire('blur', { focusedEditor: editor }); } - self.aria('expanded', e.type == 'show'); - }).fire('show'); - } - - self.menu.show(); - self.menu.layoutRect({ w: self.layoutRect().w }); - self.menu.moveRel(self.getEl(), self.isRtl() ? ['br-tr', 'tr-br'] : ['bl-tl', 'tl-bl']); - self.fire('showmenu'); - }, + editorManager.setActive(editor); + editorManager.focusedEditor = editor; + editor.fire('focus', { blurredEditor: focusedEditor }); + editor.focus(true); + } + }); - /** - * Hides the menu for the button. - * - * @method hideMenu - */ - hideMenu: function () { - var self = this; + editor.on('focusout', function () { + Delay.setEditorTimeout(editor, function () { + var focusedEditor = editorManager.focusedEditor; - if (self.menu) { - self.menu.items().each(function (item) { - if (item.hideMenu) { - item.hideMenu(); + // Still the same editor the blur was outside any editor UI + if (!isUIElement(editor, getActiveElement()) && focusedEditor == editor) { + editor.fire('blur', { focusedEditor: null }); + editorManager.focusedEditor = null; } }); + }); - self.menu.hide(); - } - }, - - /** - * Sets the active menu state. - * - * @private - */ - activeMenu: function (state) { - this.classes.toggle('active', state); - }, + // Check if focus is moved to an element outside the active editor by checking if the target node + // isn't within the body of the activeEditor nor a UI element such as a dialog child control + if (!documentFocusInHandler) { + documentFocusInHandler = function (e) { + var activeEditor = editorManager.activeEditor, target; - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, id = self._id, prefix = self.classPrefix; - var icon = self.settings.icon, image, text = self.state.get('text'), - textHtml = ''; + target = e.target; - image = self.settings.image; - if (image) { - icon = 'none'; + if (activeEditor && target.ownerDocument === document) { + // Fire a blur event if the element isn't a UI element + if (target !== document.body && !isUIElement(activeEditor, target) && editorManager.focusedEditor === activeEditor) { + activeEditor.fire('blur', { focusedEditor: null }); + editorManager.focusedEditor = null; + } + } + }; - // Support for [high dpi, low dpi] image sources - if (typeof image != "string") { - image = window.getSelection ? image[0] : image[1]; - } + DOM.bind(document, 'focusin', documentFocusInHandler); + } + } - image = ' style="background-image: url(\'' + image + '\')"'; - } else { - image = ''; + function unregisterDocumentEvents(e) { + if (editorManager.focusedEditor == e.editor) { + editorManager.focusedEditor = null; } - if (text) { - self.classes.add('btn-has-text'); - textHtml = '' + self.encode(text) + ''; + if (!editorManager.activeEditor) { + DOM.unbind(document, 'selectionchange', selectionChangeHandler); + DOM.unbind(document, 'focusin', documentFocusInHandler); + DOM.unbind(document, 'mouseup', documentMouseUpHandler); + selectionChangeHandler = documentFocusInHandler = documentMouseUpHandler = null; } + } - icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; + editorManager.on('AddEditor', registerEvents); + editorManager.on('RemoveEditor', unregisterDocumentEvents); + } - self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button'); + /** + * Returns true if the specified element is part of the UI for example an button or text input. + * + * @method isEditorUIElement + * @param {Element} elm Element to check if it's part of the UI or not. + * @return {Boolean} True/false state if the element is part of the UI or not. + */ + FocusManager.isEditorUIElement = function (elm) { + // Needs to be converted to string since svg can have focus: #6776 + return elm.className.toString().indexOf('mce-') !== -1; + }; - return ( - '
    ' + - '' + - '
    ' - ); - }, + FocusManager._isUIElement = isUIElement; - /** - * Gets invoked after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this; + return FocusManager; + } +); - self.on('click', function (e) { - if (e.control === self && isChildOf(e.target, self.getEl())) { - self.focus(); - self.showMenu(!e.aria); +/** + * LegacyInput.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 + */ - if (e.aria) { - self.menu.items().filter(':visible')[0].focus(); - } - } - }); +/** + * Converts legacy input to modern HTML. + * + * @class tinymce.LegacyInput + * @private + */ +define( + 'tinymce.core.LegacyInput', + [ + "tinymce.core.util.Tools" + ], + function (Tools) { + var each = Tools.each, explode = Tools.explode; - self.on('mouseenter', function (e) { - var overCtrl = e.control, parent = self.parent(), hasVisibleSiblingMenu; + var register = function (EditorManager) { + EditorManager.on('AddEditor', function (e) { + var editor = e.editor; - if (overCtrl && parent && overCtrl instanceof MenuButton && overCtrl.parent() == parent) { - parent.items().filter('MenuButton').each(function (ctrl) { - if (ctrl.hideMenu && ctrl != overCtrl) { - if (ctrl.menu && ctrl.menu.visible()) { - hasVisibleSiblingMenu = true; - } + editor.on('preInit', function () { + var filters, fontSizes, dom, settings = editor.settings; - ctrl.hideMenu(); + function replaceWithSpan(node, styles) { + each(styles, function (value, name) { + if (value) { + dom.setStyle(node, name, value); } }); - if (hasVisibleSiblingMenu) { - overCtrl.focus(); // Fix for: #5887 - overCtrl.showMenu(); - } + dom.rename(node, 'span'); } - }); - - return self._super(); - }, - bindStates: function () { - var self = this; + function convert(e) { + dom = editor.dom; - self.state.on('change:menu', function () { - if (self.menu) { - self.menu.remove(); + if (settings.convert_fonts_to_spans) { + each(dom.select('font,u,strike', e.node), function (node) { + filters[node.nodeName.toLowerCase()](dom, node); + }); + } } - self.menu = null; - }); + if (settings.inline_styles) { + fontSizes = explode(settings.font_size_legacy_values); - return self._super(); - }, + filters = { + font: function (dom, node) { + replaceWithSpan(node, { + backgroundColor: node.style.backgroundColor, + color: node.color, + fontFamily: node.face, + fontSize: fontSizes[parseInt(node.size, 10) - 1] + }); + }, - /** - * Removes the control and it's menus. - * - * @method remove - */ - remove: function () { - this._super(); + u: function (dom, node) { + // HTML5 allows U element + if (editor.settings.schema === "html4") { + replaceWithSpan(node, { + textDecoration: 'underline' + }); + } + }, - if (this.menu) { - this.menu.remove(); - } - } - }); + strike: function (dom, node) { + replaceWithSpan(node, { + textDecoration: 'line-through' + }); + } + }; + + editor.on('PreProcess SetContent', convert); + } + }); + }); + }; - return MenuButton; + return { + register: register + }; } ); - /** - * MenuItem.js + * I18n.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -54414,364 +41381,141 @@ define( */ /** - * Creates a new menu item. + * I18n class that handles translation of TinyMCE UI. + * Uses po style with csharp style parameters. * - * @-x-less MenuItem.less - * @class tinymce.ui.MenuItem - * @extends tinymce.ui.Control + * @class tinymce.util.I18n */ define( - 'tinymce.core.ui.MenuItem', + 'tinymce.core.util.I18n', [ - "tinymce.core.ui.Widget", - "tinymce.core.ui.Factory", - "tinymce.core.Env", - "tinymce.core.util.Delay" + "tinymce.core.util.Tools" ], - function (Widget, Factory, Env, Delay) { + function (Tools) { "use strict"; - return Widget.extend({ - Defaults: { - border: 0, - role: 'menuitem' - }, + var data = {}, code = "en"; + return { /** - * Constructs a instance with the specified settings. + * Sets the current language code. * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} selectable Selectable menu. - * @setting {Array} menu Submenu array with items. - * @setting {String} shortcut Shortcut to display for menu item. Example: Ctrl+X + * @method setCode + * @param {String} newCode Current language code. */ - init: function (settings) { - var self = this, text; - - self._super(settings); - - settings = self.settings; - - self.classes.add('menu-item'); - - if (settings.menu) { - self.classes.add('menu-item-expand'); - } - - if (settings.preview) { - self.classes.add('menu-item-preview'); - } - - text = self.state.get('text'); - if (text === '-' || text === '|') { - self.classes.add('menu-item-sep'); - self.aria('role', 'separator'); - self.state.set('text', '-'); - } - - if (settings.selectable) { - self.aria('role', 'menuitemcheckbox'); - self.classes.add('menu-item-checkbox'); - settings.icon = 'selected'; - } - - if (!settings.preview && !settings.selectable) { - self.classes.add('menu-item-normal'); - } - - self.on('mousedown', function (e) { - e.preventDefault(); - }); - - if (settings.menu && !settings.ariaHideMenu) { - self.aria('haspopup', true); + setCode: function (newCode) { + if (newCode) { + code = newCode; + this.rtl = this.data[newCode] ? this.data[newCode]._dir === 'rtl' : false; } }, /** - * Returns true/false if the menuitem has sub menu. + * Returns the current language code. * - * @method hasMenus - * @return {Boolean} True/false state if it has submenu. + * @method getCode + * @return {String} Current language code. */ - hasMenus: function () { - return !!this.settings.menu; + getCode: function () { + return code; }, /** - * Shows the menu for the menu item. + * Property gets set to true if a RTL language pack was loaded. * - * @method showMenu + * @property rtl + * @type Boolean */ - showMenu: function () { - var self = this, settings = self.settings, menu, parent = self.parent(); - - parent.items().each(function (ctrl) { - if (ctrl !== self) { - ctrl.hideMenu(); - } - }); - - if (settings.menu) { - menu = self.menu; - - if (!menu) { - menu = settings.menu; - - // Is menu array then auto constuct menu control - if (menu.length) { - menu = { - type: 'menu', - items: menu - }; - } else { - menu.type = menu.type || 'menu'; - } - - if (parent.settings.itemDefaults) { - menu.itemDefaults = parent.settings.itemDefaults; - } - - menu = self.menu = Factory.create(menu).parent(self).renderTo(); - menu.reflow(); - menu.on('cancel', function (e) { - e.stopPropagation(); - self.focus(); - menu.hide(); - }); - menu.on('show hide', function (e) { - if (e.control.items) { - e.control.items().each(function (ctrl) { - ctrl.active(ctrl.settings.selected); - }); - } - }).fire('show'); - - menu.on('hide', function (e) { - if (e.control === menu) { - self.classes.remove('selected'); - } - }); - - menu.submenu = true; - } else { - menu.show(); - } - - menu._parentMenu = parent; - - menu.classes.add('menu-sub'); - - var rel = menu.testMoveRel( - self.getEl(), - self.isRtl() ? ['tl-tr', 'bl-br', 'tr-tl', 'br-bl'] : ['tr-tl', 'br-bl', 'tl-tr', 'bl-br'] - ); - - menu.moveRel(self.getEl(), rel); - menu.rel = rel; - - rel = 'menu-sub-' + rel; - menu.classes.remove(menu._lastRel).add(rel); - menu._lastRel = rel; - - self.classes.add('selected'); - self.aria('expanded', true); - } - }, + rtl: false, /** - * Hides the menu for the menu item. + * Adds translations for a specific language code. * - * @method hideMenu + * @method add + * @param {String} code Language code like sv_SE. + * @param {Array} items Name/value array with English en_US to sv_SE. */ - hideMenu: function () { - var self = this; + add: function (code, items) { + var langData = data[code]; - if (self.menu) { - self.menu.items().each(function (item) { - if (item.hideMenu) { - item.hideMenu(); - } - }); + if (!langData) { + data[code] = langData = {}; + } - self.menu.hide(); - self.aria('expanded', false); + for (var name in items) { + langData[name] = items[name]; } - return self; + this.setCode(code); }, /** - * Renders the control as a HTML string. + * 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 renderHtml - * @return {String} HTML representing the control. + * @method translate + * @param {String/Object/Array} text Text to translate. + * @return {String} String that got translated. */ - renderHtml: function () { - var self = this, id = self._id, settings = self.settings, prefix = self.classPrefix, text = self.state.get('text'); - var icon = self.settings.icon, image = '', shortcut = settings.shortcut; - var url = self.encode(settings.url), iconHtml = ''; - - // Converts shortcut format to Mac/PC variants - function convertShortcut(shortcut) { - var i, value, replace = {}; - - if (Env.mac) { - replace = { - alt: '⌥', - ctrl: '⌘', - shift: '⇧', - meta: '⌘' - }; - } else { - replace = { - meta: 'Ctrl' - }; - } - - shortcut = shortcut.split('+'); - - for (i = 0; i < shortcut.length; i++) { - value = replace[shortcut[i].toLowerCase()]; + translate: function (text) { + var langData = data[code] || {}; - if (value) { - shortcut[i] = value; - } + /** + * 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 shortcut.join('+'); - } - - function escapeRegExp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - } - - function markMatches(text) { - var match = settings.match || ''; - - return match ? text.replace(new RegExp(escapeRegExp(match), 'gi'), function (match) { - return '!mce~match[' + match + ']mce~match!'; - }) : text; - } - - function boldMatches(text) { - return text. - replace(new RegExp(escapeRegExp('!mce~match['), 'g'), ''). - replace(new RegExp(escapeRegExp(']mce~match!'), 'g'), ''); - } - - if (icon) { - self.parent().classes.add('menu-has-icons'); + return !isEmpty(obj) ? '' + obj : ''; } - if (settings.image) { - image = ' style="background-image: url(\'' + settings.image + '\')"'; + function isEmpty(text) { + return text === '' || text === null || Tools.is(text, 'undefined'); } - if (shortcut) { - shortcut = convertShortcut(shortcut); + 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; } - icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none'); - iconHtml = (text !== '-' ? '\u00a0' : ''); - - text = boldMatches(self.encode(markMatches(text))); - url = boldMatches(self.encode(markMatches(url))); - - return ( - '
    ' + - iconHtml + - (text !== '-' ? '' + text + '' : '') + - (shortcut ? '
    ' + shortcut + '
    ' : '') + - (settings.menu ? '
    ' : '') + - (url ? '' : '') + - '
    ' - ); - }, - - /** - * Gets invoked after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this, settings = self.settings; - var textStyle = settings.textStyle; - if (typeof textStyle == "function") { - textStyle = textStyle.call(this); + if (isEmpty(text)) { + return ''; } - if (textStyle) { - var textElm = self.getEl('text'); - if (textElm) { - textElm.setAttribute('style', textStyle); - } + if (Tools.is(text, 'object') && Tools.hasOwn(text, 'raw')) { + return toString(text.raw); } - self.on('mouseenter click', function (e) { - if (e.control === self) { - if (!settings.menu && e.type === 'click') { - self.fire('select'); - - // Edge will crash if you stress it see #2660 - Delay.requestAnimationFrame(function () { - self.parent().hideAll(); - }); - } else { - self.showMenu(); - - if (e.aria) { - self.menu.focus(true); - } - } - } - }); - - self._super(); - - return self; - }, - - hover: function () { - var self = this; - - self.parent().items().each(function (ctrl) { - ctrl.classes.remove('selected'); - }); - - self.classes.toggle('selected', true); - - return self; - }, - - active: function (state) { - if (typeof state != "undefined") { - this.aria('checked', state); + 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 this._super(state); + return getLangData(text).replace(/{context:\w+}$/, ''); }, - /** - * Removes the control and it's menus. - * - * @method remove - */ - remove: function () { - this._super(); - - if (this.menu) { - this.menu.remove(); - } - } - }); + data: data + }; } ); - /** - * Throbber.js + * EditorManager.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -54781,960 +41525,788 @@ define( */ /** - * This class enables you to display a Throbber for any element. + * This class used as a factory for manager for tinymce.Editor instances. + * + * @example + * tinymce.EditorManager.init({}); * - * @-x-less Throbber.less - * @class tinymce.ui.Throbber + * @class tinymce.EditorManager + * @mixes tinymce.util.Observable + * @static */ define( - 'tinymce.core.ui.Throbber', + 'tinymce.core.EditorManager', [ - "tinymce.core.dom.DomQuery", - "tinymce.core.ui.Control", - "tinymce.core.util.Delay" + 'ephox.katamari.api.Arr', + 'ephox.katamari.api.Type', + 'global!document', + 'global!window', + 'tinymce.core.AddOnManager', + 'tinymce.core.dom.DomQuery', + 'tinymce.core.dom.DOMUtils', + 'tinymce.core.Editor', + 'tinymce.core.Env', + 'tinymce.core.ErrorReporter', + 'tinymce.core.FocusManager', + 'tinymce.core.LegacyInput', + 'tinymce.core.util.I18n', + 'tinymce.core.util.Observable', + 'tinymce.core.util.Promise', + 'tinymce.core.util.Tools', + 'tinymce.core.util.URI' ], - function ($, Control, Delay) { - "use strict"; - - /** - * Constructs a new throbber. - * - * @constructor - * @param {Element} elm DOM Html element to display throbber in. - * @param {Boolean} inline Optional true/false state if the throbber should be appended to end of element for infinite scroll. - */ - return function (elm, inline) { - var self = this, state, classPrefix = Control.classPrefix, timer; + function (Arr, Type, document, window, 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 = []; - /** - * Shows the throbber. - * - * @method show - * @param {Number} [time] Time to wait before showing. - * @param {function} [callback] Optional callback to execute when the throbber is shown. - * @return {tinymce.ui.Throbber} Current throbber instance. - */ - self.show = function (time, callback) { - function render() { - if (state) { - $(elm).append( - '
    ' - ); + 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'; + }; - if (callback) { - callback(); - } - } + function globalEventDelegate(e) { + each(EditorManager.get(), function (editor) { + if (e.type === 'scroll') { + editor.fire('ScrollWindow', e); + } else { + editor.fire('ResizeWindow', e); } + }); + } - self.hide(); - - state = true; - - if (time) { - timer = Delay.setTimeout(render, time); + function toggleGlobalEvents(state) { + if (state !== boundGlobalEvents) { + if (state) { + DomQuery(window).on('resize scroll', globalEventDelegate); } else { - render(); + DomQuery(window).off('resize scroll', globalEventDelegate); } - return self; - }; - - /** - * Hides the throbber. - * - * @method hide - * @return {tinymce.ui.Throbber} Current throbber instance. - */ - self.hide = function () { - var child = elm.lastChild; + boundGlobalEvents = state; + } + } - Delay.clearTimeout(timer); + function removeEditorFromList(targetEditor) { + var oldEditors = editors; - if (child && child.className.indexOf('throbber') != -1) { - child.parentNode.removeChild(child); + delete legacyEditors[targetEditor.id]; + for (var i = 0; i < legacyEditors.length; i++) { + if (legacyEditors[i] === targetEditor) { + legacyEditors.splice(i, 1); + break; } + } - state = false; + editors = Arr.filter(editors, function (editor) { + return targetEditor !== editor; + }); - return self; - }; - }; - } -); + // Select another editor since the active one was removed + if (EditorManager.activeEditor === targetEditor) { + EditorManager.activeEditor = editors.length > 0 ? editors[0] : null; + } -/** - * Menu.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 - */ + // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor + if (EditorManager.focusedEditor === targetEditor) { + EditorManager.focusedEditor = null; + } -/** - * Creates a new menu. - * - * @-x-less Menu.less - * @class tinymce.ui.Menu - * @extends tinymce.ui.FloatPanel - */ -define( - 'tinymce.core.ui.Menu', - [ - "tinymce.core.ui.FloatPanel", - "tinymce.core.ui.MenuItem", - "tinymce.core.ui.Throbber", - "tinymce.core.util.Tools" - ], - function (FloatPanel, MenuItem, Throbber, Tools) { - "use strict"; + return oldEditors.length !== editors.length; + } - return FloatPanel.extend({ - Defaults: { - defaultType: 'menuitem', - border: 1, - layout: 'stack', - role: 'application', - bodyRole: 'menu', - ariaRoot: true - }, + function purgeDestroyedEditor(editor) { + // User has manually destroyed the editor lets clean up the mess + if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) { + removeEditorFromList(editor); + editor.unbindAllNativeEvents(); + editor.destroy(true); + editor.removed = true; + editor = null; + } + + return editor; + } + + EditorManager = { + defaultSettings: {}, /** - * Constructs a instance with the specified settings. + * Dom query instance. * - * @constructor - * @param {Object} settings Name/value object with settings. + * @property $ + * @type tinymce.dom.DomQuery */ - init: function (settings) { - var self = this; - - settings.autohide = true; - settings.constrainToViewport = true; - - if (typeof settings.items === 'function') { - settings.itemsFactory = settings.items; - settings.items = []; - } - - if (settings.itemDefaults) { - var items = settings.items, i = items.length; - - while (i--) { - items[i] = Tools.extend({}, settings.itemDefaults, items[i]); - } - } - - self._super(settings); - self.classes.add('menu'); - }, + $: DomQuery, /** - * Repaints the control after a layout operation. + * Major version of TinyMCE build. * - * @method repaint + * @property majorVersion + * @type String */ - repaint: function () { - this.classes.toggle('menu-align', true); - - this._super(); + majorVersion: '4', - this.getEl().style.height = ''; - this.getEl('body').style.height = ''; + /** + * Minor version of TinyMCE build. + * + * @property minorVersion + * @type String + */ + minorVersion: '7.0', - return this; - }, + /** + * Release date of TinyMCE build. + * + * @property releaseDate + * @type String + */ + releaseDate: '2017-10-03', /** - * Hides/closes the menu. + * Collection of editor instances. Deprecated use tinymce.get() instead. * - * @method cancel + * @property editors + * @type Object */ - cancel: function () { - var self = this; + editors: legacyEditors, - self.hideAll(); - self.fire('select'); - }, + /** + * Collection of language pack data. + * + * @property i18n + * @type Object + */ + i18n: I18n, /** - * Loads new items from the factory items function. + * Currently active editor instance. * - * @method load + * @property activeEditor + * @type tinymce.Editor + * @example + * tinyMCE.activeEditor.selection.getContent(); + * tinymce.EditorManager.activeEditor.selection.getContent(); */ - load: function () { - var self = this, time, factory; + activeEditor: null, + + settings: {}, + + setup: function () { + var self = this, baseURL, documentBaseURL, suffix = "", preInit, src; + + // Get base URL for the current document + documentBaseURL = URI.getDocumentBaseUrl(document.location); + + // Check if the URL is a document based format like: http://site/dir/file and file:/// + // leave other formats like applewebdata://... intact + if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) { + documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, ''); - function hideThrobber() { - if (self.throbber) { - self.throbber.hide(); - self.throbber = null; + if (!/[\/\\]$/.test(documentBaseURL)) { + documentBaseURL += '/'; } } - factory = self.settings.itemsFactory; - if (!factory) { - return; - } + // If tinymce is defined and has a base use that or use the old tinyMCEPreInit + preInit = window.tinymce || window.tinyMCEPreInit; + if (preInit) { + baseURL = preInit.base || preInit.baseURL; + suffix = preInit.suffix; + } else { + // Get base where the tinymce script is located + var scripts = document.getElementsByTagName('script'); + for (var i = 0; i < scripts.length; i++) { + src = scripts[i].src; - if (!self.throbber) { - self.throbber = new Throbber(self.getEl('body'), true); + // Script types supported: + // tinymce.js tinymce.min.js tinymce.dev.js + // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js + // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js + var srcScript = src.substring(src.lastIndexOf('/')); + if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) { + if (srcScript.indexOf('.min') != -1) { + suffix = '.min'; + } - if (self.items().length === 0) { - self.throbber.show(); - self.fire('loading'); - } else { - self.throbber.show(100, function () { - self.items().remove(); - self.fire('loading'); - }); + baseURL = src.substring(0, src.lastIndexOf('/')); + break; + } } - self.on('hide close', hideThrobber); - } - - self.requestTime = time = new Date().getTime(); + // We didn't find any baseURL by looking at the script elements + // Try to use the document.currentScript as a fallback + if (!baseURL && document.currentScript) { + src = document.currentScript.src; - self.settings.itemsFactory(function (items) { - if (items.length === 0) { - self.hide(); - return; - } + if (src.indexOf('.min') != -1) { + suffix = '.min'; + } - if (self.requestTime !== time) { - return; + baseURL = src.substring(0, src.lastIndexOf('/')); } + } - self.getEl().style.width = ''; - self.getEl('body').style.width = ''; - - hideThrobber(); - self.items().remove(); - self.getEl('body').innerHTML = ''; + /** + * Base URL where the root directory if TinyMCE is located. + * + * @property baseURL + * @type String + */ + self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL); - self.add(items); - self.renderNew(); - self.fire('loaded'); - }); - }, + /** + * Document base URL where the current document is located. + * + * @property documentBaseURL + * @type String + */ + self.documentBaseURL = documentBaseURL; - /** - * Hide menu and all sub menus. - * - * @method hideAll - */ - hideAll: function () { - var self = this; + /** + * Absolute baseURI for the installation path of TinyMCE. + * + * @property baseURI + * @type tinymce.util.URI + */ + self.baseURI = new URI(self.baseURL); - this.find('menuitem').exec('hideMenu'); + /** + * Current suffix to add to each plugin/theme that gets loaded for example ".min". + * + * @property suffix + * @type String + */ + self.suffix = suffix; - return self._super(); + self.focusManager = new FocusManager(self); }, /** - * Invoked before the menu is rendered. + * Overrides the default settings for editor instances. * - * @method preRender + * @method overrideDefaults + * @param {Object} defaultSettings Defaults settings object. */ - preRender: function () { - var self = this; - - self.items().each(function (ctrl) { - var settings = ctrl.settings; - - if (settings.icon || settings.image || settings.selectable) { - self._hasIcons = true; - return false; - } - }); + overrideDefaults: function (defaultSettings) { + var baseUrl, suffix; - if (self.settings.itemsFactory) { - self.on('postrender', function () { - if (self.settings.itemsFactory) { - self.load(); - } - }); + baseUrl = defaultSettings.base_url; + if (baseUrl) { + this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, '')); + this.baseURI = new URI(this.baseURL); } - return self._super(); - } - }); - } -); + suffix = defaultSettings.suffix; + if (defaultSettings.suffix) { + this.suffix = suffix; + } -/** - * ListBox.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.defaultSettings = defaultSettings; -/** - * Creates a new list box control. - * - * @-x-less ListBox.less - * @class tinymce.ui.ListBox - * @extends tinymce.ui.MenuButton - */ -define( - 'tinymce.core.ui.ListBox', - [ - "tinymce.core.ui.MenuButton", - "tinymce.core.ui.Menu" - ], - function (MenuButton, Menu) { - "use strict"; + var pluginBaseUrls = defaultSettings.plugin_base_urls; + for (var name in pluginBaseUrls) { + AddOnManager.PluginManager.urls[name] = pluginBaseUrls[name]; + } + }, - return MenuButton.extend({ /** - * Constructs a instance with the specified settings. + * Initializes a set of editors. This method will create editors based on various settings. * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Array} values Array with values to add to list box. + * @method init + * @param {Object} settings Settings object to be passed to each editor instance. + * @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized. + * @example + * // Initializes a editor using the longer method + * tinymce.EditorManager.init({ + * some_settings : 'some value' + * }); + * + * // Initializes a editor instance using the shorter version and with a promise + * tinymce.init({ + * some_settings : 'some value' + * }).then(function(editors) { + * ... + * }); */ init: function (settings) { - var self = this, values, selected, selectedText, lastItemCtrl; - - function setSelected(menuValues) { - // Try to find a selected value - for (var i = 0; i < menuValues.length; i++) { - selected = menuValues[i].selected || settings.value === menuValues[i].value; - - if (selected) { - selectedText = selectedText || menuValues[i].text; - self.state.set('value', menuValues[i].value); - return true; - } - - // If the value has a submenu, try to find the selected values in that menu - if (menuValues[i].menu) { - if (setSelected(menuValues[i].menu)) { - return true; - } - } - } - } - - self._super(settings); - settings = self.settings; - - self._values = values = settings.values; - if (values) { - if (typeof settings.value != "undefined") { - setSelected(values); - } + var self = this, result, invalidInlineTargets; - // Default with first item - if (!selected && values.length > 0) { - selectedText = values[0].text; - self.state.set('value', values[0].value); - } + invalidInlineTargets = Tools.makeMap( + 'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' + + 'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu', + ' ' + ); - self.state.set('menu', values); + function isInvalidInlineTarget(settings, elm) { + return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets; } - self.state.set('text', settings.text || selectedText); - - self.classes.add('listbox'); + function createId(elm) { + var id = elm.id; - self.on('select', function (e) { - var ctrl = e.control; + // Use element id, or unique name or generate a unique id + if (!id) { + id = elm.name; - if (lastItemCtrl) { - e.lastControl = lastItemCtrl; - } + if (id && !DOM.get(id)) { + id = elm.name; + } else { + // Generate unique name + id = DOM.uniqueId(); + } - if (settings.multiple) { - ctrl.active(!ctrl.active()); - } else { - self.value(e.control.value()); + elm.setAttribute('id', id); } - lastItemCtrl = ctrl; - }); - }, - - /** - * Getter/setter function for the control value. - * - * @method value - * @param {String} [value] Value to be set. - * @return {Boolean/tinymce.ui.ListBox} Value or self if it's a set operation. - */ - bindStates: function () { - var self = this; - - function activateMenuItemsByValue(menu, value) { - if (menu instanceof Menu) { - menu.items().each(function (ctrl) { - if (!ctrl.hasMenus()) { - ctrl.active(ctrl.value() === value); - } - }); - } + return id; } - function getSelectedItem(menuValues, value) { - var selectedItem; + function execCallback(name) { + var callback = settings[name]; - if (!menuValues) { + if (!callback) { return; } - for (var i = 0; i < menuValues.length; i++) { - if (menuValues[i].value === value) { - return menuValues[i]; - } - - if (menuValues[i].menu) { - selectedItem = getSelectedItem(menuValues[i].menu, value); - if (selectedItem) { - return selectedItem; - } - } - } + return callback.apply(self, Array.prototype.slice.call(arguments, 2)); } - self.on('show', function (e) { - activateMenuItemsByValue(e.control, self.value()); - }); + function hasClass(elm, className) { + return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className); + } - self.state.on('change:value', function (e) { - var selectedItem = getSelectedItem(self.state.get('menu'), e.value); + function findTargets(settings) { + var l, targets = []; - if (selectedItem) { - self.text(selectedItem.text); - } else { - self.text(self.settings.text); + if (Env.ie && Env.ie < 11) { + ErrorReporter.initError( + 'TinyMCE does not support the browser you are using. For a list of supported' + + ' browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/' + ); + return []; } - }); - return self._super(); - } - }); - } -); + if (settings.types) { + each(settings.types, function (type) { + targets = targets.concat(DOM.select(type.selector)); + }); -/** - * Radio.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 - */ + return targets; + } else if (settings.selector) { + return DOM.select(settings.selector); + } else if (settings.target) { + return [settings.target]; + } -/** - * Creates a new radio button. - * - * @-x-less Radio.less - * @class tinymce.ui.Radio - * @extends tinymce.ui.Checkbox - */ -define( - 'tinymce.core.ui.Radio', - [ - "tinymce.core.ui.Checkbox" - ], - function (Checkbox) { - "use strict"; + // Fallback to old setting + switch (settings.mode) { + case "exact": + l = settings.elements || ''; - return Checkbox.extend({ - Defaults: { - classes: "radio", - role: "radio" - } - }); - } -); -/** - * ResizeHandle.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 - */ + if (l.length > 0) { + each(explode(l), function (id) { + var elm; -/** - * Renders a resize handle that fires ResizeStart, Resize and ResizeEnd events. - * - * @-x-less ResizeHandle.less - * @class tinymce.ui.ResizeHandle - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.ResizeHandle', - [ - "tinymce.core.ui.Widget", - "tinymce.core.ui.DragHelper" - ], - function (Widget, DragHelper) { - "use strict"; + if ((elm = DOM.get(id))) { + targets.push(elm); + } else { + each(document.forms, function (f) { + each(f.elements, function (e) { + if (e.name === id) { + id = 'mce_editor_' + instanceCounter++; + DOM.setAttrib(e, 'id', id); + targets.push(e); + } + }); + }); + } + }); + } + break; - return Widget.extend({ - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, prefix = self.classPrefix; + case "textareas": + case "specific_textareas": + each(DOM.select('textarea'), function (elm) { + if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) { + return; + } - self.classes.add('resizehandle'); + if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) { + targets.push(elm); + } + }); + break; + } - if (self.settings.direction == "both") { - self.classes.add('resizehandle-both'); + return targets; } - self.canFocus = false; - - return ( - '
    ' + - '' + - '
    ' - ); - }, - - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this; + var provideResults = function (editors) { + result = editors; + }; - self._super(); + function initEditors() { + var initCount = 0, editors = [], targets; - self.resizeDragHelper = new DragHelper(this._id, { - start: function () { - self.fire('ResizeStart'); - }, + function createEditor(id, settings, targetElm) { + var editor = new Editor(id, settings, self); - drag: function (e) { - if (self.settings.direction != "both") { - e.deltaX = 0; - } + editors.push(editor); - self.fire('Resize', e); - }, + editor.on('init', function () { + if (++initCount === targets.length) { + provideResults(editors); + } + }); - stop: function () { - self.fire('ResizeEnd'); + editor.targetElm = editor.targetElm || targetElm; + editor.render(); } - }); - }, - - remove: function () { - if (this.resizeDragHelper) { - this.resizeDragHelper.destroy(); - } - - return this._super(); - } - }); - } -); - -/** - * SelectBox.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 new select box control. - * - * @-x-less SelectBox.less - * @class tinymce.ui.SelectBox - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.SelectBox', - [ - "tinymce.core.ui.Widget" - ], - function (Widget) { - "use strict"; - - function createOptions(options) { - var strOptions = ''; - if (options) { - for (var i = 0; i < options.length; i++) { - strOptions += ''; - } - } - return strOptions; - } + DOM.unbind(window, 'ready', initEditors); + execCallback('onpageload'); - return Widget.extend({ - Defaults: { - classes: "selectbox", - role: "selectbox", - options: [] - }, - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Array} options Array with options to add to the select box. - */ - init: function (settings) { - var self = this; + targets = DomQuery.unique(findTargets(settings)); - self._super(settings); + // TODO: Deprecate this one + if (settings.types) { + each(settings.types, function (type) { + Tools.each(targets, function (elm) { + if (DOM.is(elm, type.selector)) { + createEditor(createId(elm), extend({}, settings, type), elm); + return false; + } - if (self.settings.size) { - self.size = self.settings.size; - } + return true; + }); + }); - if (self.settings.options) { - self._options = self.settings.options; - } + return; + } - self.on('keydown', function (e) { - var rootControl; + Tools.each(targets, function (elm) { + purgeDestroyedEditor(self.get(elm.id)); + }); - if (e.keyCode == 13) { - e.preventDefault(); + targets = Tools.grep(targets, function (elm) { + return !self.get(elm.id); + }); - // Find root control that we can do toJSON on - self.parents().reverse().each(function (ctrl) { - if (ctrl.toJSON) { - rootControl = ctrl; - return false; + if (targets.length === 0) { + provideResults([]); + } else { + each(targets, function (elm) { + if (isInvalidInlineTarget(settings, elm)) { + ErrorReporter.initError('Could not initialize inline editor on invalid inline target element', elm); + } else { + createEditor(createId(elm), settings, elm); } }); + } + } + + self.settings = settings; + DOM.bind(window, 'ready', initEditors); - // Fire event on current text box with the serialized data of the whole form - self.fire('submit', { data: rootControl.toJSON() }); + return new Promise(function (resolve) { + if (result) { + resolve(result); + } else { + provideResults = function (editors) { + resolve(editors); + }; } }); }, /** - * Getter/setter function for the options state. + * Returns a editor instance by id. + * + * @method get + * @param {String/Number} id Editor instance id or index 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 + * 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!'); + * }); * - * @method options - * @param {Array} [state] State to be set. - * @return {Array|tinymce.ui.SelectBox} Array of string options. + * // Adds an onclick event to an editor by id (longer version) + * tinymce.EditorManager.get('mytextbox').on('click', function(e) { + * ed.windowManager.alert('Hello world!'); + * }); */ - options: function (state) { - if (!arguments.length) { - return this.state.get('options'); + get: function (id) { + 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; } - - this.state.set('options', state); - - return this; }, - renderHtml: function () { - var self = this, options, size = ''; - - options = createOptions(self._options); + /** + * Adds an editor instance to the editor collection. This will also set it as the active editor. + * + * @method add + * @param {tinymce.Editor} editor Editor instance to add to the collection. + * @return {tinymce.Editor} The same instance that got passed in. + */ + add: function (editor) { + var self = this, existingEditor; - if (self.size) { - size = ' size = "' + self.size + '"'; + // 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; } - return ( - '' - ); - }, - - bindStates: function () { - var self = this; - - self.state.on('change:options', function (e) { - self.getEl().innerHTML = createOptions(e.value); - }); - - return self._super(); - } - }); - } -); - -/** - * Slider.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 - */ - -/** - * Slider control. - * - * @-x-less Slider.less - * @class tinymce.ui.Slider - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.Slider', - [ - "tinymce.core.ui.Widget", - "tinymce.core.ui.DragHelper", - "tinymce.core.ui.DomUtils" - ], - function (Widget, DragHelper, DomUtils) { - "use strict"; - - function constrain(value, minVal, maxVal) { - if (value < minVal) { - value = minVal; - } - - if (value > maxVal) { - value = maxVal; - } - - return value; - } - - function setAriaProp(el, name, value) { - el.setAttribute('aria-' + name, value); - } - - function updateSliderHandle(ctrl, value) { - var maxHandlePos, shortSizeName, sizeName, stylePosName, styleValue, handleEl; - - if (ctrl.settings.orientation == "v") { - stylePosName = "top"; - sizeName = "height"; - shortSizeName = "h"; - } else { - stylePosName = "left"; - sizeName = "width"; - shortSizeName = "w"; - } + 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; + } - handleEl = ctrl.getEl('handle'); - maxHandlePos = (ctrl.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName]; + legacyEditors.push(editor); - styleValue = (maxHandlePos * ((value - ctrl._minValue) / (ctrl._maxValue - ctrl._minValue))) + 'px'; - handleEl.style[stylePosName] = styleValue; - handleEl.style.height = ctrl.layoutRect().h + 'px'; + editors.push(editor); + } - setAriaProp(handleEl, 'valuenow', value); - setAriaProp(handleEl, 'valuetext', '' + ctrl.settings.previewFilter(value)); - setAriaProp(handleEl, 'valuemin', ctrl._minValue); - setAriaProp(handleEl, 'valuemax', ctrl._maxValue); - } + toggleGlobalEvents(true); - return Widget.extend({ - init: function (settings) { - var self = this; + // Doesn't call setActive method since we don't want + // to fire a bunch of activate/deactivate calls while initializing + self.activeEditor = editor; - if (!settings.previewFilter) { - settings.previewFilter = function (value) { - return Math.round(value * 100) / 100.0; - }; - } + self.fire('AddEditor', { editor: editor }); - self._super(settings); - self.classes.add('slider'); + if (!beforeUnloadDelegate) { + beforeUnloadDelegate = function () { + self.fire('BeforeUnload'); + }; - if (settings.orientation == "v") { - self.classes.add('vertical'); + DOM.bind(window, 'beforeunload', beforeUnloadDelegate); } - self._minValue = settings.minValue || 0; - self._maxValue = settings.maxValue || 100; - self._initValue = self.state.get('value'); + return editor; }, - renderHtml: function () { - var self = this, id = self._id, prefix = self.classPrefix; - - return ( - '
    ' + - '
    ' + - '
    ' - ); + /** + * Creates an editor instance and adds it to the EditorManager collection. + * + * @method createEditor + * @param {String} id Instance id to use for editor. + * @param {Object} settings Editor instance settings. + * @return {tinymce.Editor} Editor instance that got created. + */ + createEditor: function (id, settings) { + return this.add(new Editor(id, settings, this)); }, - reset: function () { - this.value(this._initValue).repaint(); - }, + /** + * Removes a editor or editors form page. + * + * @example + * // Remove all editors bound to divs + * tinymce.remove('div'); + * + * // Remove all editors bound to textareas + * tinymce.remove('textarea'); + * + * // Remove all editors + * tinymce.remove(); + * + * // Remove specific instance by id + * tinymce.remove('#id'); + * + * @method remove + * @param {tinymce.Editor/String/Object} [selector] CSS selector or editor instance to remove. + * @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, editor; - postRender: function () { - var self = this, minValue, maxValue, screenCordName, - stylePosName, sizeName, shortSizeName; + // Remove all editors + if (!selector) { + for (i = editors.length - 1; i >= 0; i--) { + self.remove(editors[i]); + } - function toFraction(min, max, val) { - return (val + min) / (max - min); + return; } - function fromFraction(min, max, val) { - return (val * (max - min)) - min; - } + // Remove editors by selector + if (Type.isString(selector)) { + selector = selector.selector || selector; - function handleKeyboard(minValue, maxValue) { - function alter(delta) { - var value; + each(DOM.select(selector), function (elm) { + editor = self.get(elm.id); - value = self.value(); - value = fromFraction(minValue, maxValue, toFraction(minValue, maxValue, value) + (delta * 0.05)); - value = constrain(value, minValue, maxValue); + if (editor) { + self.remove(editor); + } + }); - self.value(value); + return; + } - self.fire('dragstart', { value: value }); - self.fire('drag', { value: value }); - self.fire('dragend', { value: value }); - } + // Remove specific editor + editor = selector; - self.on('keydown', function (e) { - switch (e.keyCode) { - case 37: - case 38: - alter(-1); - break; + // Not in the collection + if (Type.isNull(self.get(editor.id))) { + return null; + } - case 39: - case 40: - alter(1); - break; - } - }); + if (removeEditorFromList(editor)) { + self.fire('RemoveEditor', { editor: editor }); + } + + if (editors.length === 0) { + DOM.unbind(window, 'beforeunload', beforeUnloadDelegate); } - function handleDrag(minValue, maxValue, handleEl) { - var startPos, startHandlePos, maxHandlePos, handlePos, value; + editor.remove(); - self._dragHelper = new DragHelper(self._id, { - handle: self._id + "-handle", + toggleGlobalEvents(editors.length > 0); - start: function (e) { - startPos = e[screenCordName]; - startHandlePos = parseInt(self.getEl('handle').style[stylePosName], 10); - maxHandlePos = (self.layoutRect()[shortSizeName] || 100) - DomUtils.getSize(handleEl)[sizeName]; - self.fire('dragstart', { value: value }); - }, + return editor; + }, - drag: function (e) { - var delta = e[screenCordName] - startPos; + /** + * Executes a specific command on the currently active editor. + * + * @method execCommand + * @param {String} cmd Command to perform for example Bold. + * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not. + * @param {String} value Optional value parameter like for example an URL to a link. + * @return {Boolean} true/false if the command was executed or not. + */ + execCommand: function (cmd, ui, value) { + var self = this, editor = self.get(value); - handlePos = constrain(startHandlePos + delta, 0, maxHandlePos); - handleEl.style[stylePosName] = handlePos + 'px'; + // Manager commands + switch (cmd) { + case "mceAddEditor": + if (!self.get(value)) { + new Editor(value, self.settings, self).render(); + } - value = minValue + (handlePos / maxHandlePos) * (maxValue - minValue); - self.value(value); + return true; - self.tooltip().text('' + self.settings.previewFilter(value)).show().moveRel(handleEl, 'bc tc'); + case "mceRemoveEditor": + if (editor) { + editor.remove(); + } - self.fire('drag', { value: value }); - }, + return true; - stop: function () { - self.tooltip().hide(); - self.fire('dragend', { value: value }); + case 'mceToggleEditor': + if (!editor) { + self.execCommand('mceAddEditor', 0, value); + return true; } - }); - } - minValue = self._minValue; - maxValue = self._maxValue; + if (editor.isHidden()) { + editor.show(); + } else { + editor.hide(); + } - if (self.settings.orientation == "v") { - screenCordName = "screenY"; - stylePosName = "top"; - sizeName = "height"; - shortSizeName = "h"; - } else { - screenCordName = "screenX"; - stylePosName = "left"; - sizeName = "width"; - shortSizeName = "w"; + return true; } - self._super(); - - handleKeyboard(minValue, maxValue, self.getEl('handle')); - handleDrag(minValue, maxValue, self.getEl('handle')); - }, + // Run command on active editor + if (self.activeEditor) { + return self.activeEditor.execCommand(cmd, ui, value); + } - repaint: function () { - this._super(); - updateSliderHandle(this, this.value()); + return false; }, - bindStates: function () { - var self = this; - - self.state.on('change:value', function (e) { - updateSliderHandle(self, e.value); + /** + * Calls the save method on all editor instances in the collection. This can be useful when a form is to be submitted. + * + * @method triggerSave + * @example + * // Saves all contents + * tinyMCE.triggerSave(); + */ + triggerSave: function () { + each(editors, function (editor) { + editor.save(); }); + }, - return self._super(); - } - }); - } -); -/** - * Spacer.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 - */ + /** + * Adds a language pack, this gets called by the loaded language files like en.js. + * + * @method addI18n + * @param {String} code Optional language code. + * @param {Object} items Name/value object with translations. + */ + addI18n: function (code, items) { + I18n.add(code, items); + }, -/** - * Creates a spacer. This control is used in flex layouts for example. - * - * @-x-less Spacer.less - * @class tinymce.ui.Spacer - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.Spacer', - [ - "tinymce.core.ui.Widget" - ], - function (Widget) { - "use strict"; + /** + * Translates the specified string using the language pack items. + * + * @method translate + * @param {String/Array/Object} text String to translate + * @return {String} Translated string. + */ + translate: function (text) { + return I18n.translate(text); + }, - return Widget.extend({ /** - * Renders the control as a HTML string. + * Sets the active editor instance and fires the deactivate/activate events. * - * @method renderHtml - * @return {String} HTML representing the control. + * @method setActive + * @param {tinymce.Editor} editor Editor instance to set as the active instance. */ - renderHtml: function () { - var self = this; + setActive: function (editor) { + var activeEditor = this.activeEditor; + + if (this.activeEditor != editor) { + if (activeEditor) { + activeEditor.fire('deactivate', { relatedTarget: editor }); + } - self.classes.add('spacer'); - self.canFocus = false; + editor.fire('activate', { relatedTarget: activeEditor }); + } - return '
    '; + this.activeEditor = editor; } - }); + }; + + extend(EditorManager, Observable); + + EditorManager.setup(); + LegacyInput.register(EditorManager); + + return EditorManager; } ); + /** - * SplitButton.js + * Rect.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -55744,185 +42316,216 @@ define( */ /** - * Creates a split button. + * Contains various tools for rect/position calculation. * - * @-x-less SplitButton.less - * @class tinymce.ui.SplitButton - * @extends tinymce.ui.Button + * @class tinymce.geom.Rect */ define( - 'tinymce.core.ui.SplitButton', + 'tinymce.core.geom.Rect', [ - "tinymce.core.ui.MenuButton", - "tinymce.core.ui.DomUtils", - "tinymce.core.dom.DomQuery" ], - function (MenuButton, DomUtils, $) { - return MenuButton.extend({ - Defaults: { - classes: "widget btn splitbtn", - role: "button" - }, + function () { + "use strict"; - /** - * Repaints the control after a layout operation. - * - * @method repaint - */ - repaint: function () { - var self = this, elm = self.getEl(), rect = self.layoutRect(), mainButtonElm, menuButtonElm; + var min = Math.min, max = Math.max, round = Math.round; - self._super(); + /** + * Returns the rect positioned based on the relative position name + * to the target rect. + * + * @method relativePosition + * @param {Rect} rect Source rect to modify into a new rect. + * @param {Rect} targetRect Rect to move relative to based on the rel option. + * @param {String} rel Relative position. For example: tr-bl. + */ + function relativePosition(rect, targetRect, rel) { + var x, y, w, h, targetW, targetH; - mainButtonElm = elm.firstChild; - menuButtonElm = elm.lastChild; + x = targetRect.x; + y = targetRect.y; + w = rect.w; + h = rect.h; + targetW = targetRect.w; + targetH = targetRect.h; - $(mainButtonElm).css({ - width: rect.w - DomUtils.getSize(menuButtonElm).width, - height: rect.h - 2 - }); + rel = (rel || '').split(''); - $(menuButtonElm).css({ - height: rect.h - 2 - }); + if (rel[0] === 'b') { + y += targetH; + } - return self; - }, + if (rel[1] === 'r') { + x += targetW; + } - /** - * Sets the active menu state. - * - * @private - */ - activeMenu: function (state) { - var self = this; + if (rel[0] === 'c') { + y += round(targetH / 2); + } - $(self.getEl().lastChild).toggleClass(self.classPrefix + 'active', state); - }, + if (rel[1] === 'c') { + x += round(targetW / 2); + } - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, id = self._id, prefix = self.classPrefix, image; - var icon = self.state.get('icon'), text = self.state.get('text'), - textHtml = ''; + if (rel[3] === 'b') { + y -= h; + } - image = self.settings.image; - if (image) { - icon = 'none'; + if (rel[4] === 'r') { + x -= w; + } - // Support for [high dpi, low dpi] image sources - if (typeof image != "string") { - image = window.getSelection ? image[0] : image[1]; - } + if (rel[3] === 'c') { + y -= round(h / 2); + } - image = ' style="background-image: url(\'' + image + '\')"'; - } else { - image = ''; - } + if (rel[4] === 'c') { + x -= round(w / 2); + } + + return create(x, y, w, h); + } + + /** + * Tests various positions to get the most suitable one. + * + * @method findBestRelativePosition + * @param {Rect} rect Rect to use as source. + * @param {Rect} targetRect Rect to move relative to. + * @param {Rect} constrainRect Rect to constrain within. + * @param {Array} rels Array of relative positions to test against. + */ + function findBestRelativePosition(rect, targetRect, constrainRect, rels) { + var pos, i; - icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : ''; + for (i = 0; i < rels.length; i++) { + pos = relativePosition(rect, targetRect, rels[i]); - if (text) { - self.classes.add('btn-has-text'); - textHtml = '' + self.encode(text) + ''; + if (pos.x >= constrainRect.x && pos.x + pos.w <= constrainRect.w + constrainRect.x && + pos.y >= constrainRect.y && pos.y + pos.h <= constrainRect.h + constrainRect.y) { + return rels[i]; } + } - return ( - '
    ' + - '' + - '' + - '
    ' - ); - }, + return null; + } - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this, onClickHandler = self.settings.onclick; + /** + * Inflates the rect in all directions. + * + * @method inflate + * @param {Rect} rect Rect to expand. + * @param {Number} w Relative width to expand by. + * @param {Number} h Relative height to expand by. + * @return {Rect} New expanded rect. + */ + function inflate(rect, w, h) { + return create(rect.x - w, rect.y - h, rect.w + w * 2, rect.h + h * 2); + } - self.on('click', function (e) { - var node = e.target; + /** + * Returns the intersection of the specified rectangles. + * + * @method intersect + * @param {Rect} rect The first rectangle to compare. + * @param {Rect} cropRect The second rectangle to compare. + * @return {Rect} The intersection of the two rectangles or null if they don't intersect. + */ + function intersect(rect, cropRect) { + var x1, y1, x2, y2; - if (e.control == this) { - // Find clicks that is on the main button - while (node) { - if ((e.aria && e.aria.key != 'down') || (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1)) { - e.stopImmediatePropagation(); + x1 = max(rect.x, cropRect.x); + y1 = max(rect.y, cropRect.y); + x2 = min(rect.x + rect.w, cropRect.x + cropRect.w); + y2 = min(rect.y + rect.h, cropRect.y + cropRect.h); - if (onClickHandler) { - onClickHandler.call(this, e); - } + if (x2 - x1 < 0 || y2 - y1 < 0) { + return null; + } - return; - } + return create(x1, y1, x2 - x1, y2 - y1); + } - node = node.parentNode; - } - } - }); + /** + * Returns a rect clamped within the specified clamp rect. This forces the + * rect to be inside the clamp rect. + * + * @method clamp + * @param {Rect} rect Rectangle to force within clamp rect. + * @param {Rect} clampRect Rectable to force within. + * @param {Boolean} fixedSize True/false if size should be fixed. + * @return {Rect} Clamped rect. + */ + function clamp(rect, clampRect, fixedSize) { + var underflowX1, underflowY1, overflowX2, overflowY2, + x1, y1, x2, y2, cx2, cy2; + + x1 = rect.x; + y1 = rect.y; + x2 = rect.x + rect.w; + y2 = rect.y + rect.h; + cx2 = clampRect.x + clampRect.w; + cy2 = clampRect.y + clampRect.h; + + underflowX1 = max(0, clampRect.x - x1); + underflowY1 = max(0, clampRect.y - y1); + overflowX2 = max(0, x2 - cx2); + overflowY2 = max(0, y2 - cy2); - delete self.settings.onclick; + x1 += underflowX1; + y1 += underflowY1; - return self._super(); + if (fixedSize) { + x2 += underflowX1; + y2 += underflowY1; + x1 -= overflowX2; + y1 -= overflowY2; } - }); - } -); -/** - * StackLayout.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 layout uses the browsers layout when the items are blocks. - * - * @-x-less StackLayout.less - * @class tinymce.ui.StackLayout - * @extends tinymce.ui.FlowLayout - */ -define( - 'tinymce.core.ui.StackLayout', - [ - "tinymce.core.ui.FlowLayout" - ], - function (FlowLayout) { - "use strict"; + x2 -= overflowX2; + y2 -= overflowY2; - return FlowLayout.extend({ - Defaults: { - containerClass: 'stack-layout', - controlClass: 'stack-layout-item', - endClass: 'break' - }, + return create(x1, y1, x2 - x1, y2 - y1); + } - isNative: function () { - return true; - } - }); + /** + * Creates a new rectangle object. + * + * @method create + * @param {Number} x Rectangle x location. + * @param {Number} y Rectangle y location. + * @param {Number} w Rectangle width. + * @param {Number} h Rectangle height. + * @return {Rect} New rectangle object. + */ + function create(x, y, w, h) { + return { x: x, y: y, w: w, h: h }; + } + + /** + * Creates a new rectangle object form a clientRects object. + * + * @method fromClientRect + * @param {ClientRect} clientRect DOM ClientRect object. + * @return {Rect} New rectangle object. + */ + function fromClientRect(clientRect) { + return create(clientRect.left, clientRect.top, clientRect.width, clientRect.height); + } + + return { + inflate: inflate, + relativePosition: relativePosition, + findBestRelativePosition: findBestRelativePosition, + intersect: intersect, + clamp: clamp, + create: create, + fromClientRect: fromClientRect + }; } ); + /** - * TabPanel.js + * Factory.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -55932,180 +42535,111 @@ define( */ /** - * Creates a tab panel control. + * This class is a factory for control instances. This enables you + * to create instances of controls without having to require the UI controls directly. * - * @-x-less TabPanel.less - * @class tinymce.ui.TabPanel - * @extends tinymce.ui.Panel + * It also allow you to override or add new control types. * - * @setting {Number} activeTab Active tab index. + * @class tinymce.ui.Factory */ define( - 'tinymce.core.ui.TabPanel', + 'tinymce.core.ui.Factory', [ - "tinymce.core.ui.Panel", - "tinymce.core.dom.DomQuery", - "tinymce.core.ui.DomUtils" ], - function (Panel, $, DomUtils) { + function () { "use strict"; - return Panel.extend({ - Defaults: { - layout: 'absolute', - defaults: { - type: 'panel' - } - }, + var types = {}; + return { /** - * Activates the specified tab by index. + * Adds a new control instance type to the factory. * - * @method activateTab - * @param {Number} idx Index of the tab to activate. + * @method add + * @param {String} type Type name for example "button". + * @param {function} typeClass Class type function. */ - activateTab: function (idx) { - var activeTabElm; - - if (this.activeTabId) { - activeTabElm = this.getEl(this.activeTabId); - $(activeTabElm).removeClass(this.classPrefix + 'active'); - activeTabElm.setAttribute('aria-selected', "false"); - } - - this.activeTabId = 't' + idx; - - activeTabElm = this.getEl('t' + idx); - activeTabElm.setAttribute('aria-selected', "true"); - $(activeTabElm).addClass(this.classPrefix + 'active'); - - this.items()[idx].show().fire('showtab'); - this.reflow(); - - this.items().each(function (item, i) { - if (idx != i) { - item.hide(); - } - }); + add: function (type, typeClass) { + types[type.toLowerCase()] = typeClass; }, /** - * Renders the control as a HTML string. + * Returns true/false if the specified type exists or not. * - * @method renderHtml - * @return {String} HTML representing the control. + * @method has + * @param {String} type Type to look for. + * @return {Boolean} true/false if the control by name exists. */ - renderHtml: function () { - var self = this, layout = self._layout, tabsHtml = '', prefix = self.classPrefix; - - self.preRender(); - layout.preRender(self); - - self.items().each(function (ctrl, i) { - var id = self._id + '-t' + i; - - ctrl.aria('role', 'tabpanel'); - ctrl.aria('labelledby', id); - - tabsHtml += ( - '' - ); - }); - - return ( - '
    ' + - '
    ' + - tabsHtml + - '
    ' + - '
    ' + - layout.renderHtml(self) + - '
    ' + - '
    ' - ); + has: function (type) { + return !!types[type.toLowerCase()]; }, /** - * Called after the control has been rendered. + * Returns ui control module by name. * - * @method postRender + * @method get + * @param {String} type Type get. + * @return {Object} Module or undefined. */ - postRender: function () { - var self = this; - - self._super(); - - self.settings.activeTab = self.settings.activeTab || 0; - self.activateTab(self.settings.activeTab); - - this.on('click', function (e) { - var targetParent = e.target.parentNode; - - if (targetParent && targetParent.id == self._id + '-head') { - var i = targetParent.childNodes.length; + get: function (type) { + var lctype = type.toLowerCase(); + var controlType = types.hasOwnProperty(lctype) ? types[lctype] : null; + if (controlType === null) { + throw new Error("Could not find module for type: " + type); + } - while (i--) { - if (targetParent.childNodes[i] == e.target) { - self.activateTab(i); - } - } - } - }); + return controlType; }, /** - * Initializes the current controls layout rect. - * This will be executed by the layout managers to determine the - * default minWidth/minHeight etc. + * Creates a new control instance based on the settings provided. The instance created will be + * based on the specified type property it can also create whole structures of components out of + * the specified JSON object. + * + * @example + * tinymce.ui.Factory.create({ + * type: 'button', + * text: 'Hello world!' + * }); * - * @method initLayoutRect - * @return {Object} Layout rect instance. + * @method create + * @param {Object/String} settings Name/Value object with items used to create the type. + * @return {tinymce.ui.Control} Control instance based on the specified type. */ - initLayoutRect: function () { - var self = this, rect, minW, minH; + create: function (type, settings) { + var ControlType; - minW = DomUtils.getSize(self.getEl('head')).width; - minW = minW < 0 ? 0 : minW; - minH = 0; + // If string is specified then use it as the type + if (typeof type == 'string') { + settings = settings || {}; + settings.type = type; + } else { + settings = type; + type = settings.type; + } - self.items().each(function (item) { - minW = Math.max(minW, item.layoutRect().minW); - minH = Math.max(minH, item.layoutRect().minH); - }); + // Find control type + type = type.toLowerCase(); + ControlType = types[type]; - self.items().each(function (ctrl) { - ctrl.settings.x = 0; - ctrl.settings.y = 0; - ctrl.settings.w = minW; - ctrl.settings.h = minH; - - ctrl.layoutRect({ - x: 0, - y: 0, - w: minW, - h: minH - }); - }); + // #if debug - var headH = DomUtils.getSize(self.getEl('head')).height; + if (!ControlType) { + throw new Error("Could not find control by type: " + type); + } - self.settings.minWidth = minW; - self.settings.minHeight = minH + headH; + // #endif - rect = self._super(); - rect.deltaH += headH; - rect.innerH = rect.h - rect.deltaH; + ControlType = new ControlType(settings); + ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine - return rect; + return ControlType; } - }); + }; } ); - /** - * TextBox.js + * Class.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -56115,207 +42649,168 @@ define( */ /** - * Creates a new textbox. + * This utilitiy class is used for easier inheritance. * - * @-x-less TextBox.less - * @class tinymce.ui.TextBox - * @extends tinymce.ui.Widget + * Features: + * * Exposed super functions: this._super(); + * * Mixins + * * Dummy functions + * * Property functions: var value = object.value(); and object.value(newValue); + * * Static functions + * * Defaults settings */ define( - 'tinymce.core.ui.TextBox', + 'tinymce.core.util.Class', [ - "tinymce.core.ui.Widget", - "tinymce.core.util.Tools", - "tinymce.core.ui.DomUtils" + "tinymce.core.util.Tools" ], - function (Widget, Tools, DomUtils) { - return Widget.extend({ - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} multiline True if the textbox is a multiline control. - * @setting {Number} maxLength Max length for the textbox. - * @setting {Number} size Size of the textbox in characters. - */ - init: function (settings) { - var self = this; + function (Tools) { + var each = Tools.each, extend = Tools.extend; - self._super(settings); + var extendClass, initializing; - self.classes.add('textbox'); + function Class() { + } - if (settings.multiline) { - self.classes.add('multiline'); - } else { - self.on('keydown', function (e) { - var rootControl; + // Provides classical inheritance, based on code made by John Resig + Class.extend = extendClass = function (prop) { + var self = this, _super = self.prototype, prototype, name, member; - if (e.keyCode == 13) { - e.preventDefault(); + // The dummy class constructor + function Class() { + var i, mixins, mixin, self = this; - // Find root control that we can do toJSON on - self.parents().reverse().each(function (ctrl) { - if (ctrl.toJSON) { - rootControl = ctrl; - return false; - } - }); + // All construction is actually done in the init method + if (!initializing) { + // Run class constuctor + if (self.init) { + self.init.apply(self, arguments); + } - // Fire event on current text box with the serialized data of the whole form - self.fire('submit', { data: rootControl.toJSON() }); + // Run mixin constructors + mixins = self.Mixins; + if (mixins) { + i = mixins.length; + while (i--) { + mixin = mixins[i]; + if (mixin.init) { + mixin.init.apply(self, arguments); + } } - }); - - self.on('keyup', function (e) { - self.state.set('value', e.target.value); - }); + } } - }, + } - /** - * Repaints the control after a layout operation. - * - * @method repaint - */ - repaint: function () { - var self = this, style, rect, borderBox, borderW, borderH = 0, lastRepaintRect; + // Dummy function, needs to be extended in order to provide functionality + function dummy() { + return this; + } - style = self.getEl().style; - rect = self._layoutRect; - lastRepaintRect = self._lastRepaintRect || {}; + // Creates a overloaded method for the class + // this enables you to use this._super(); to call the super function + function createMethod(name, fn) { + return function () { + var self = this, tmp = self._super, ret; - // Detect old IE 7+8 add lineHeight to align caret vertically in the middle - var doc = document; - if (!self.settings.multiline && doc.all && (!doc.documentMode || doc.documentMode <= 8)) { - style.lineHeight = (rect.h - borderH) + 'px'; - } + self._super = _super[name]; + ret = fn.apply(self, arguments); + self._super = tmp; - borderBox = self.borderBox; - borderW = borderBox.left + borderBox.right + 8; - borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0); + return ret; + }; + } - if (rect.x !== lastRepaintRect.x) { - style.left = rect.x + 'px'; - lastRepaintRect.x = rect.x; - } + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; - if (rect.y !== lastRepaintRect.y) { - style.top = rect.y + 'px'; - lastRepaintRect.y = rect.y; - } + /*eslint new-cap:0 */ + prototype = new self(); + initializing = false; - if (rect.w !== lastRepaintRect.w) { - style.width = (rect.w - borderW) + 'px'; - lastRepaintRect.w = rect.w; - } + // Add mixins + if (prop.Mixins) { + each(prop.Mixins, function (mixin) { + for (var name in mixin) { + if (name !== "init") { + prop[name] = mixin[name]; + } + } + }); - if (rect.h !== lastRepaintRect.h) { - style.height = (rect.h - borderH) + 'px'; - lastRepaintRect.h = rect.h; + if (_super.Mixins) { + prop.Mixins = _super.Mixins.concat(prop.Mixins); } + } - self._lastRepaintRect = lastRepaintRect; - self.fire('repaint', {}, false); - - return self; - }, - - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, settings = self.settings, attrs, elm; - - attrs = { - id: self._id, - hidefocus: '1' - }; - - Tools.each([ - 'rows', 'spellcheck', 'maxLength', 'size', 'readonly', 'min', - 'max', 'step', 'list', 'pattern', 'placeholder', 'required', 'multiple' - ], function (name) { - attrs[name] = settings[name]; + // Generate dummy methods + if (prop.Methods) { + each(prop.Methods.split(','), function (name) { + prop[name] = dummy; }); + } - if (self.disabled()) { - attrs.disabled = 'disabled'; - } - - if (settings.subtype) { - attrs.type = settings.subtype; - } + // Generate property methods + if (prop.Properties) { + each(prop.Properties.split(','), function (name) { + var fieldName = '_' + name; - elm = DomUtils.create(settings.multiline ? 'textarea' : 'input', attrs); - elm.value = self.state.get('value'); - elm.className = self.classes; + prop[name] = function (value) { + var self = this, undef; - return elm.outerHTML; - }, + // Set value + if (value !== undef) { + self[fieldName] = value; - value: function (value) { - if (arguments.length) { - this.state.set('value', value); - return this; - } + return self; + } - // Make sure the real state is in sync - if (this.state.get('rendered')) { - this.state.set('value', this.getEl().value); - } + // Get value + return self[fieldName]; + }; + }); + } - return this.state.get('value'); - }, + // Static functions + if (prop.Statics) { + each(prop.Statics, function (func, name) { + Class[name] = func; + }); + } - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this; + // Default settings + if (prop.Defaults && _super.Defaults) { + prop.Defaults = extend({}, _super.Defaults, prop.Defaults); + } - self.getEl().value = self.state.get('value'); - self._super(); + // Copy the properties over onto the new prototype + for (name in prop) { + member = prop[name]; - self.$el.on('change', function (e) { - self.state.set('value', e.target.value); - self.fire('change', e); - }); - }, + if (typeof member == "function" && _super[name]) { + prototype[name] = createMethod(name, member); + } else { + prototype[name] = member; + } + } - bindStates: function () { - var self = this; + // Populate our constructed prototype object + Class.prototype = prototype; - self.state.on('change:value', function (e) { - if (self.getEl().value != e.value) { - self.getEl().value = e.value; - } - }); + // Enforce the constructor to be what we expect + Class.constructor = Class; - self.state.on('change:disabled', function (e) { - self.getEl().disabled = e.value; - }); + // And make this class extendible + Class.extend = extendClass; - return self._super(); - }, + return Class; + }; - remove: function () { - this.$el.off(); - this._super(); - } - }); + return Class; } ); - -defineGlobal("global!RegExp", RegExp); /** - * DropZone.js + * Color.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved @@ -56325,444 +42820,236 @@ defineGlobal("global!RegExp", RegExp); */ /** - * Creates a new dropzone. + * This class lets you parse/serialize colors and convert rgb/hsb. + * + * @class tinymce.util.Color + * @example + * var white = new tinymce.util.Color({r: 255, g: 255, b: 255}); + * var red = new tinymce.util.Color('#FF0000'); * - * @-x-less DropZone.less - * @class tinymce.ui.DropZone - * @extends tinymce.ui.Widget + * console.log(white.toHex(), red.toHsv()); */ define( - 'tinymce.core.ui.DropZone', + 'tinymce.core.util.Color', [ - 'tinymce.core.ui.Widget', - 'tinymce.core.util.Tools', - 'tinymce.core.ui.DomUtils', - 'global!RegExp' ], - function (Widget, Tools, DomUtils, RegExp) { - return Widget.extend({ - /** - * Constructs a instance with the specified settings. - * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} multiple True if the dropzone is a multiple control. - * @setting {Number} maxLength Max length for the dropzone. - * @setting {Number} size Size of the dropzone in characters. - */ - init: function (settings) { - var self = this; - - settings = Tools.extend({ - height: 100, - text: "Drop an image here", - multiple: false, - accept: null // by default accept any files - }, settings); - - self._super(settings); - - self.classes.add('dropzone'); + function () { + var min = Math.min, max = Math.max, round = Math.round; - if (settings.multiple) { - self.classes.add('multiple'); - } - }, + /** + * Constructs a new color instance. + * + * @constructor + * @method Color + * @param {String} value Optional initial value to parse. + */ + function Color(value) { + var self = this, r = 0, g = 0, b = 0; - /** - * Renders the control as a HTML string. - * - * @method renderHtml - * @return {String} HTML representing the control. - */ - renderHtml: function () { - var self = this, attrs, elm; - var cfg = self.settings; + function rgb2hsv(r, g, b) { + var h, s, v, d, minRGB, maxRGB; - attrs = { - id: self._id, - hidefocus: '1' - }; + h = 0; + s = 0; + v = 0; + r = r / 255; + g = g / 255; + b = b / 255; - elm = DomUtils.create('div', attrs, '' + this.translate(cfg.text) + ''); + minRGB = min(r, min(g, b)); + maxRGB = max(r, max(g, b)); - if (cfg.height) { - DomUtils.css(elm, 'height', cfg.height + 'px'); - } + if (minRGB == maxRGB) { + v = minRGB; - if (cfg.width) { - DomUtils.css(elm, 'width', cfg.width + 'px'); + return { + h: 0, + s: 0, + v: v * 100 + }; } - elm.className = self.classes; - - return elm.outerHTML; - }, - - - /** - * Called after the control has been rendered. - * - * @method postRender - */ - postRender: function () { - var self = this; + /*eslint no-nested-ternary:0 */ + d = (r == minRGB) ? g - b : ((b == minRGB) ? r - g : b - r); + h = (r == minRGB) ? 3 : ((b == minRGB) ? 1 : 5); + h = 60 * (h - d / (maxRGB - minRGB)); + s = (maxRGB - minRGB) / maxRGB; + v = maxRGB; - var toggleDragClass = function (e) { - e.preventDefault(); - self.classes.toggle('dragenter'); - self.getEl().className = self.classes; + return { + h: round(h), + s: round(s * 100), + v: round(v * 100) }; + } - var filter = function (files) { - var accept = self.settings.accept; - if (typeof accept !== 'string') { - return files; - } + function hsvToRgb(hue, saturation, brightness) { + var side, chroma, x, match; - var re = new RegExp('(' + accept.split(/\s*,\s*/).join('|') + ')$', 'i'); - return Tools.grep(files, function (file) { - return re.test(file.name); - }); - }; + hue = (parseInt(hue, 10) || 0) % 360; + saturation = parseInt(saturation, 10) / 100; + brightness = parseInt(brightness, 10) / 100; + saturation = max(0, min(saturation, 1)); + brightness = max(0, min(brightness, 1)); - self._super(); + if (saturation === 0) { + r = g = b = round(255 * brightness); + return; + } - self.$el.on('dragover', function (e) { - e.preventDefault(); - }); + side = hue / 60; + chroma = brightness * saturation; + x = chroma * (1 - Math.abs(side % 2 - 1)); + match = brightness - chroma; - self.$el.on('dragenter', toggleDragClass); - self.$el.on('dragleave', toggleDragClass); + switch (Math.floor(side)) { + case 0: + r = chroma; + g = x; + b = 0; + break; - self.$el.on('drop', function (e) { - e.preventDefault(); + case 1: + r = x; + g = chroma; + b = 0; + break; - if (self.state.get('disabled')) { - return; - } + case 2: + r = 0; + g = chroma; + b = x; + break; - var files = filter(e.dataTransfer.files); + case 3: + r = 0; + g = x; + b = chroma; + break; - self.value = function () { - if (!files.length) { - return null; - } else if (self.settings.multiple) { - return files; - } else { - return files[0]; - } - }; + case 4: + r = x; + g = 0; + b = chroma; + break; - if (files.length) { - self.fire('change', e); - } - }); - }, + case 5: + r = chroma; + g = 0; + b = x; + break; - remove: function () { - this.$el.off(); - this._super(); - } - }); - } -); + default: + r = g = b = 0; + } -/** - * BrowseButton.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 - */ + r = round(255 * (r + match)); + g = round(255 * (g + match)); + b = round(255 * (b + match)); + } -/** - * Creates a new browse button. - * - * @-x-less BrowseButton.less - * @class tinymce.ui.BrowseButton - * @extends tinymce.ui.Widget - */ -define( - 'tinymce.core.ui.BrowseButton', - [ - 'tinymce.core.ui.Button', - 'tinymce.core.util.Tools', - 'tinymce.core.ui.DomUtils', - 'tinymce.core.dom.DomQuery', - 'global!RegExp' - ], - function (Button, Tools, DomUtils, $, RegExp) { - return Button.extend({ /** - * Constructs a instance with the specified settings. + * Returns the hex string of the current color. For example: #ff00ff * - * @constructor - * @param {Object} settings Name/value object with settings. - * @setting {Boolean} multiple True if the dropzone is a multiple control. - * @setting {Number} maxLength Max length for the dropzone. - * @setting {Number} size Size of the dropzone in characters. + * @method toHex + * @return {String} Hex string of current color. */ - init: function (settings) { - var self = this; - - settings = Tools.extend({ - text: "Browse...", - multiple: false, - accept: null // by default accept any files - }, settings); - - self._super(settings); - - self.classes.add('browsebutton'); + function toHex() { + function hex(val) { + val = parseInt(val, 10).toString(16); - if (settings.multiple) { - self.classes.add('multiple'); + return val.length > 1 ? val : '0' + val; } - }, - /** - * Called after the control has been rendered. + return '#' + hex(r) + hex(g) + hex(b); + } + + /** + * Returns the r, g, b values of the color. Each channel has a range from 0-255. * - * @method postRender + * @method toRgb + * @return {Object} Object with r, g, b fields. */ - postRender: function () { - var self = this; - - var input = DomUtils.create('input', { - type: 'file', - id: self._id + '-browse', - accept: self.settings.accept - }); - - self._super(); - - $(input).on('change', function (e) { - var files = e.target.files; + function toRgb() { + return { + r: r, + g: g, + b: b + }; + } - self.value = function () { - if (!files.length) { - return null; - } else if (self.settings.multiple) { - return files; - } else { - return files[0]; - } - }; + /** + * Returns the h, s, v values of the color. Ranges: h=0-360, s=0-100, v=0-100. + * + * @method toHsv + * @return {Object} Object with h, s, v fields. + */ + function toHsv() { + return rgb2hsv(r, g, b); + } - e.preventDefault(); + /** + * Parses the specified value and populates the color instance. + * + * Supported format examples: + * * rbg(255,0,0) + * * #ff0000 + * * #fff + * * {r: 255, g: 0, b: 0} + * * {h: 360, s: 100, v: 100} + * + * @method parse + * @param {Object/String} value Color value to parse. + * @return {tinymce.util.Color} Current color instance. + */ + function parse(value) { + var matches; - if (files.length) { - self.fire('change', e); + if (typeof value == 'object') { + if ("r" in value) { + r = value.r; + g = value.g; + b = value.b; + } else if ("v" in value) { + hsvToRgb(value.h, value.s, value.v); } - }); - - // ui.Button prevents default on click, so we shouldn't let the click to propagate up to it - $(input).on('click', function (e) { - e.stopPropagation(); - }); - - $(self.getEl('button')).on('click', function (e) { - e.stopPropagation(); - input.click(); - }); - - // in newer browsers input doesn't have to be attached to dom to trigger browser dialog - // however older IE11 (< 11.1358.14393.0) still requires this - self.getEl().appendChild(input); - }, - + } else { + if ((matches = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(value))) { + r = parseInt(matches[1], 10); + g = parseInt(matches[2], 10); + b = parseInt(matches[3], 10); + } else if ((matches = /#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(value))) { + r = parseInt(matches[1], 16); + g = parseInt(matches[2], 16); + b = parseInt(matches[3], 16); + } else if ((matches = /#([0-F])([0-F])([0-F])/gi.exec(value))) { + r = parseInt(matches[1] + matches[1], 16); + g = parseInt(matches[2] + matches[2], 16); + b = parseInt(matches[3] + matches[3], 16); + } + } - remove: function () { - $(this.getEl('button')).off(); - $(this.getEl('input')).off(); + r = r < 0 ? 0 : (r > 255 ? 255 : r); + g = g < 0 ? 0 : (g > 255 ? 255 : g); + b = b < 0 ? 0 : (b > 255 ? 255 : b); - this._super(); + return self; } - }); - } -); -/** - * Api.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 - */ + if (value) { + parse(value); + } -define( - 'tinymce.core.ui.Api', - [ - 'tinymce.core.ui.Selector', - 'tinymce.core.ui.Collection', - 'tinymce.core.ui.ReflowQueue', - 'tinymce.core.ui.Control', - 'tinymce.core.ui.Factory', - 'tinymce.core.ui.KeyboardNavigation', - 'tinymce.core.ui.Container', - 'tinymce.core.ui.DragHelper', - 'tinymce.core.ui.Scrollable', - 'tinymce.core.ui.Panel', - 'tinymce.core.ui.Movable', - 'tinymce.core.ui.Resizable', - 'tinymce.core.ui.FloatPanel', - 'tinymce.core.ui.Window', - 'tinymce.core.ui.MessageBox', - 'tinymce.core.ui.Tooltip', - 'tinymce.core.ui.Widget', - 'tinymce.core.ui.Progress', - 'tinymce.core.ui.Notification', - 'tinymce.core.ui.Layout', - 'tinymce.core.ui.AbsoluteLayout', - 'tinymce.core.ui.Button', - 'tinymce.core.ui.ButtonGroup', - 'tinymce.core.ui.Checkbox', - 'tinymce.core.ui.ComboBox', - 'tinymce.core.ui.ColorBox', - 'tinymce.core.ui.PanelButton', - 'tinymce.core.ui.ColorButton', - 'tinymce.core.ui.ColorPicker', - 'tinymce.core.ui.Path', - 'tinymce.core.ui.ElementPath', - 'tinymce.core.ui.FormItem', - 'tinymce.core.ui.Form', - 'tinymce.core.ui.FieldSet', - 'tinymce.core.ui.FilePicker', - 'tinymce.core.ui.FitLayout', - 'tinymce.core.ui.FlexLayout', - 'tinymce.core.ui.FlowLayout', - 'tinymce.core.ui.FormatControls', - 'tinymce.core.ui.GridLayout', - 'tinymce.core.ui.Iframe', - 'tinymce.core.ui.InfoBox', - 'tinymce.core.ui.Label', - 'tinymce.core.ui.Toolbar', - 'tinymce.core.ui.MenuBar', - 'tinymce.core.ui.MenuButton', - 'tinymce.core.ui.MenuItem', - 'tinymce.core.ui.Throbber', - 'tinymce.core.ui.Menu', - 'tinymce.core.ui.ListBox', - 'tinymce.core.ui.Radio', - 'tinymce.core.ui.ResizeHandle', - 'tinymce.core.ui.SelectBox', - 'tinymce.core.ui.Slider', - 'tinymce.core.ui.Spacer', - 'tinymce.core.ui.SplitButton', - 'tinymce.core.ui.StackLayout', - 'tinymce.core.ui.TabPanel', - 'tinymce.core.ui.TextBox', - 'tinymce.core.ui.DropZone', - 'tinymce.core.ui.BrowseButton' - ], - function ( - Selector, Collection, ReflowQueue, Control, Factory, KeyboardNavigation, Container, DragHelper, Scrollable, Panel, Movable, - Resizable, FloatPanel, Window, MessageBox, Tooltip, Widget, Progress, Notification, Layout, AbsoluteLayout, Button, - ButtonGroup, Checkbox, ComboBox, ColorBox, PanelButton, ColorButton, ColorPicker, Path, ElementPath, FormItem, Form, - FieldSet, FilePicker, FitLayout, FlexLayout, FlowLayout, FormatControls, GridLayout, Iframe, InfoBox, Label, Toolbar, - MenuBar, MenuButton, MenuItem, Throbber, Menu, ListBox, Radio, ResizeHandle, SelectBox, Slider, Spacer, SplitButton, - StackLayout, TabPanel, TextBox, DropZone, BrowseButton - ) { - "use strict"; + self.toRgb = toRgb; + self.toHsv = toHsv; + self.toHex = toHex; + self.parse = parse; + } - var registerToFactory = function (id, ref) { - Factory.add(id.split('.').pop(), ref); - }; - - var expose = function (target, id, ref) { - var i, fragments; - - fragments = id.split(/[.\/]/); - for (i = 0; i < fragments.length - 1; ++i) { - if (target[fragments[i]] === undefined) { - target[fragments[i]] = {}; - } - - target = target[fragments[i]]; - } - - target[fragments[fragments.length - 1]] = ref; - - registerToFactory(id, ref); - }; - - var appendTo = function (target) { - expose(target, 'ui.Selector', Selector); - expose(target, 'ui.Collection', Collection); - expose(target, 'ui.ReflowQueue', ReflowQueue); - expose(target, 'ui.Control', Control); - expose(target, 'ui.Factory', Factory); - expose(target, 'ui.KeyboardNavigation', KeyboardNavigation); - expose(target, 'ui.Container', Container); - expose(target, 'ui.DragHelper', DragHelper); - expose(target, 'ui.Scrollable', Scrollable); - expose(target, 'ui.Panel', Panel); - expose(target, 'ui.Movable', Movable); - expose(target, 'ui.Resizable', Resizable); - expose(target, 'ui.FloatPanel', FloatPanel); - expose(target, 'ui.Window', Window); - expose(target, 'ui.MessageBox', MessageBox); - expose(target, 'ui.Tooltip', Tooltip); - expose(target, 'ui.Widget', Widget); - expose(target, 'ui.Progress', Progress); - expose(target, 'ui.Notification', Notification); - expose(target, 'ui.Layout', Layout); - expose(target, 'ui.AbsoluteLayout', AbsoluteLayout); - expose(target, 'ui.Button', Button); - expose(target, 'ui.ButtonGroup', ButtonGroup); - expose(target, 'ui.Checkbox', Checkbox); - expose(target, 'ui.ComboBox', ComboBox); - expose(target, 'ui.ColorBox', ColorBox); - expose(target, 'ui.PanelButton', PanelButton); - expose(target, 'ui.ColorButton', ColorButton); - expose(target, 'ui.ColorPicker', ColorPicker); - expose(target, 'ui.Path', Path); - expose(target, 'ui.ElementPath', ElementPath); - expose(target, 'ui.FormItem', FormItem); - expose(target, 'ui.Form', Form); - expose(target, 'ui.FieldSet', FieldSet); - expose(target, 'ui.FilePicker', FilePicker); - expose(target, 'ui.FitLayout', FitLayout); - expose(target, 'ui.FlexLayout', FlexLayout); - expose(target, 'ui.FlowLayout', FlowLayout); - expose(target, 'ui.FormatControls', FormatControls); - expose(target, 'ui.GridLayout', GridLayout); - expose(target, 'ui.Iframe', Iframe); - expose(target, 'ui.InfoBox', InfoBox); - expose(target, 'ui.Label', Label); - expose(target, 'ui.Toolbar', Toolbar); - expose(target, 'ui.MenuBar', MenuBar); - expose(target, 'ui.MenuButton', MenuButton); - expose(target, 'ui.MenuItem', MenuItem); - expose(target, 'ui.Throbber', Throbber); - expose(target, 'ui.Menu', Menu); - expose(target, 'ui.ListBox', ListBox); - expose(target, 'ui.Radio', Radio); - expose(target, 'ui.ResizeHandle', ResizeHandle); - expose(target, 'ui.SelectBox', SelectBox); - expose(target, 'ui.Slider', Slider); - expose(target, 'ui.Spacer', Spacer); - expose(target, 'ui.SplitButton', SplitButton); - expose(target, 'ui.StackLayout', StackLayout); - expose(target, 'ui.TabPanel', TabPanel); - expose(target, 'ui.TextBox', TextBox); - expose(target, 'ui.DropZone', DropZone); - expose(target, 'ui.BrowseButton', BrowseButton); - expose(target, 'ui.Api', Api); - }; - - var Api = { - appendTo: appendTo - }; - - return Api; + return Color; } ); + /** * JSON.js * @@ -56788,8 +43075,9 @@ define( define( 'tinymce.core.util.JSON', [ + 'global!window' ], - function () { + function (window) { function serialize(o, quote) { var i, v, t, name; @@ -56953,10 +43241,12 @@ define( define( 'tinymce.core.util.XHR', [ - "tinymce.core.util.Observable", - "tinymce.core.util.Tools" + 'ephox.sand.api.XMLHttpRequest', + 'global!setTimeout', + 'tinymce.core.util.Observable', + 'tinymce.core.util.Tools' ], - function (Observable, Tools) { + function (XMLHttpRequest, setTimeout, Observable, Tools) { var XHR = { /** * Sends a XMLHTTPRequest. @@ -57182,14 +43472,16 @@ define( define( 'tinymce.core.util.LocalStorage', [ + 'global!document', + 'global!window' ], - function () { + function (document, window) { var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport; // Check for native support try { if (window.localStorage) { - return localStorage; + return window.localStorage; } } catch (ex) { // Ignore @@ -57413,7 +43705,7 @@ define( 'tinymce.core.html.Styles', 'tinymce.core.html.Writer', 'tinymce.core.Shortcuts', - 'tinymce.core.ui.Api', + 'tinymce.core.ui.Factory', 'tinymce.core.UndoManager', 'tinymce.core.util.Class', 'tinymce.core.util.Color', @@ -57434,8 +43726,8 @@ define( function ( AddOnManager, Formatter, NotificationManager, WindowManager, BookmarkManager, ControlSelection, DomQuery, DOMUtils, EventUtils, RangeUtils, ScriptLoader, Selection, DomSerializer, Sizzle, TreeWalker, Editor, EditorCommands, EditorManager, EditorObservable, Env, FocusManager, Rect, DomParser, Entities, Node, - SaxParser, Schema, HtmlSerializer, Styles, Writer, Shortcuts, UiApi, UndoManager, Class, Color, Delay, EventDispatcher, I18n, JSON, JSONP, JSONRequest, LocalStorage, - Observable, Promise, Tools, URI, VK, XHR + SaxParser, Schema, HtmlSerializer, Styles, Writer, Shortcuts, Factory, UndoManager, Class, Color, Delay, EventDispatcher, I18n, JSON, JSONP, JSONRequest, + LocalStorage, Observable, Promise, Tools, URI, VK, XHR ) { var tinymce = EditorManager; @@ -57491,6 +43783,10 @@ define( Serializer: HtmlSerializer }, + ui: { + Factory: Factory + }, + Env: Env, AddOnManager: AddOnManager, Formatter: Formatter, @@ -57537,7 +43833,6 @@ define( }; tinymce = Tools.extend(tinymce, publicApi); - UiApi.appendTo(tinymce); return tinymce; } @@ -57557,9 +43852,10 @@ define( 'tinymce.core.api.Main', [ 'ephox.katamari.api.Fun', + 'global!window', 'tinymce.core.api.Tinymce' ], - function (Fun, Tinymce) { + function (Fun, window, Tinymce) { /*eslint consistent-this: 0 */ var context = this || window; diff --git a/media/vendor/tinymce/tinymce.min.js b/media/vendor/tinymce/tinymce.min.js index 34932efd9c454..70c5b080eb4bd 100644 --- a/media/vendor/tinymce/tinymce.min.js +++ b/media/vendor/tinymce/tinymce.min.js @@ -1,17 +1,13 @@ -// 4.6.7 (2017-09-18) -!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=534;return{opera:b,webkit:c,ie:d,gecko:g,mac:h,iOS:i,android:j,contentEditable:q,transparentSrc:"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",caretAfter:8!=d,range:window.getSelection&&"Range"in window,documentMode:d&&!f?document.documentMode||7:10,fileApi:k,ceFalse:d===!1||d>8,canHaveCSP:d===!1||d>11,desktop:!l&&!m,windowsPhone:n}}),g("d",["14","o"],function(a,b){"use strict";function c(a,b,c,d){a.addEventListener?a.addEventListener(b,c,d||!1):a.attachEvent&&a.attachEvent("on"+b,c)}function d(a,b,c,d){a.removeEventListener?a.removeEventListener(b,c,d||!1):a.detachEvent&&a.detachEvent("on"+b,c)}function e(a,b){var c,d=b;return c=a.path,c&&c.length>0&&(d=c[0]),a.deepPath&&(c=a.deepPath(),c&&c.length>0&&(d=c[0])),d}function f(a,c){var d,f,g=c||{};for(d in a)k[d]||(g[d]=a[d]);if(g.target||(g.target=g.srcElement||document),b.experimentalShadowDom&&(g.target=e(a,g.target)),a&&j.test(a.type)&&a.pageX===f&&a.clientX!==f){var h=g.target.ownerDocument||document,i=h.documentElement,o=h.body;g.pageX=a.clientX+(i&&i.scrollLeft||o&&o.scrollLeft||0)-(i&&i.clientLeft||o&&o.clientLeft||0),g.pageY=a.clientY+(i&&i.scrollTop||o&&o.scrollTop||0)-(i&&i.clientTop||o&&o.clientTop||0)}return g.preventDefault=function(){g.isDefaultPrevented=n,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},g.stopPropagation=function(){g.isPropagationStopped=n,a&&(a.stopPropagation?a.stopPropagation():a.cancelBubble=!0)},g.stopImmediatePropagation=function(){g.isImmediatePropagationStopped=n,g.stopPropagation()},l(g)===!1&&(g.isDefaultPrevented=m,g.isPropagationStopped=m,g.isImmediatePropagationStopped=m),"undefined"==typeof g.metaKey&&(g.metaKey=!1),g}function g(e,f,g){function h(){return"complete"===l.readyState||"interactive"===l.readyState&&l.body}function i(){g.domLoaded||(g.domLoaded=!0,f(m))}function j(){h()&&(d(l,"readystatechange",j),i())}function k(){try{l.documentElement.doScroll("left")}catch(b){return void a.setTimeout(k)}i()}var l=e.document,m={type:"ready"};return g.domLoaded?void f(m):(!l.addEventListener||b.ie&&b.ie<11?(c(l,"readystatechange",j),l.documentElement.doScroll&&e.self===e.top&&k()):h()?i():c(e,"DOMContentLoaded",i),void c(e,"load",i))}function h(){function a(a,b){var c,d,e,f,g=m[b];if(c=g&&g[a.type])for(d=0,e=c.length;dt.cacheLength&&delete a[b.shift()],a[c+" "]=d}var b=[];return a}function c(a){return a[K]=!0,a}function d(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||T)-(~a.sourceIndex||T);if(d)return d;if(c)for(;c=c.nextSibling;)if(c===b)return-1;return a?1:-1}function e(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function f(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function g(a){return c(function(b){return b=+b,c(function(c,d){for(var e,f=a([],c.length,b),g=f.length;g--;)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function h(a){return a&&typeof a.getElementsByTagName!==S&&a}function i(){}function j(a){for(var b=0,c=a.length,d="";b1?function(b,c,d){for(var e=a.length;e--;)if(!a[e](b,c,d))return!1;return!0}:a[0]}function m(b,c,d){for(var e=0,f=c.length;e-1&&(c[j]=!(g[j]=l))}}else t=n(t===g?t.splice(q,t.length):t),f?f(null,g,t,i):Y.apply(g,t)})}function p(a){for(var b,c,d,e=a.length,f=t.relative[a[0].type],g=f||t.relative[" "],h=f?1:0,i=k(function(a){return a===b},g,!0),m=k(function(a){return $.call(b,a)>-1},g,!0),n=[function(a,c,d){return!f&&(d||c!==z)||((b=c).nodeType?i(a,c,d):m(a,c,d))}];h1&&l(n),h>1&&j(a.slice(0,h-1).concat({value:" "===a[h-2].type?"*":""})).replace(ea,"$1"),c,h0,f=b.length>0,g=function(c,g,h,i,j){var k,l,m,o=0,p="0",q=c&&[],r=[],s=z,u=c||f&&t.find.TAG("*",j),v=M+=null==s?1:Math.random()||.1,w=u.length;for(j&&(z=g!==D&&g);p!==w&&null!=(k=u[p]);p++){if(f&&k){for(l=0;m=b[l++];)if(m(k,g,h)){i.push(k);break}j&&(M=v)}e&&((k=!m&&k)&&o--,c&&q.push(k))}if(o+=p,e&&p!==o){for(l=0;m=d[l++];)m(q,r,g,h);if(c){if(o>0)for(;p--;)q[p]||r[p]||(r[p]=W.call(i));r=n(r)}Y.apply(i,r),j&&!c&&r.length>0&&o+d.length>1&&a.uniqueSort(i)}return j&&(M=v,z=s),q};return e?c(g):g}var r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K="sizzle"+-new Date,L=window.document,M=0,N=0,O=b(),P=b(),Q=b(),R=function(a,b){return a===b&&(B=!0),0},S="undefined",T=1<<31,U={}.hasOwnProperty,V=[],W=V.pop,X=V.push,Y=V.push,Z=V.slice,$=V.indexOf||function(a){for(var b=0,c=this.length;b+~]|"+aa+")"+aa+"*"),ha=new RegExp("="+aa+"*([^\\]'\"]*?)"+aa+"*\\]","g"),ia=new RegExp(da),ja=new RegExp("^"+ba+"$"),ka={ID:new RegExp("^#("+ba+")"),CLASS:new RegExp("^\\.("+ba+")"),TAG:new RegExp("^("+ba+"|[*])"),ATTR:new RegExp("^"+ca),PSEUDO:new RegExp("^"+da),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+aa+"*(even|odd|(([+-]|)(\\d*)n|)"+aa+"*(?:([+-]|)"+aa+"*(\\d+)|))"+aa+"*\\)|)","i"),bool:new RegExp("^(?:"+_+")$","i"),needsContext:new RegExp("^"+aa+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+aa+"*((?:-\\d)?\\d*)"+aa+"*\\)|)(?=[^-]|$)","i")},la=/^(?:input|select|textarea|button)$/i,ma=/^h\d$/i,na=/^[^{]+\{\s*\[native \w/,oa=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,pa=/[+~]/,qa=/'|\\/g,ra=new RegExp("\\\\([\\da-f]{1,6}"+aa+"?|("+aa+")|.)","ig"),sa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{Y.apply(V=Z.call(L.childNodes),L.childNodes),V[L.childNodes.length].nodeType}catch(a){Y={apply:V.length?function(a,b){X.apply(a,Z.call(b))}:function(a,b){for(var c=a.length,d=0;a[c++]=b[d++];);a.length=c-1}}}s=a.support={},v=a.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},C=a.setDocument=function(a){function b(a){try{return a.top}catch(a){}return null}var c,e=a?a.ownerDocument||a:L,f=e.defaultView;return e!==D&&9===e.nodeType&&e.documentElement?(D=e,E=e.documentElement,F=!v(e),f&&f!==b(f)&&(f.addEventListener?f.addEventListener("unload",function(){C()},!1):f.attachEvent&&f.attachEvent("onunload",function(){C()})),s.attributes=!0,s.getElementsByTagName=!0,s.getElementsByClassName=na.test(e.getElementsByClassName),s.getById=!0,t.find.ID=function(a,b){if(typeof b.getElementById!==S&&F){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},t.filter.ID=function(a){var b=a.replace(ra,sa);return function(a){return a.getAttribute("id")===b}},t.find.TAG=s.getElementsByTagName?function(a,b){if(typeof b.getElementsByTagName!==S)return b.getElementsByTagName(a)}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){for(;c=f[e++];)1===c.nodeType&&d.push(c);return d}return f},t.find.CLASS=s.getElementsByClassName&&function(a,b){if(F)return b.getElementsByClassName(a)},H=[],G=[],s.disconnectedMatch=!0,G=G.length&&new RegExp(G.join("|")),H=H.length&&new RegExp(H.join("|")),c=na.test(E.compareDocumentPosition),J=c||na.test(E.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)for(;b=b.parentNode;)if(b===a)return!0;return!1},R=c?function(a,b){if(a===b)return B=!0,0;var c=!a.compareDocumentPosition-!b.compareDocumentPosition;return c?c:(c=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&c||!s.sortDetached&&b.compareDocumentPosition(a)===c?a===e||a.ownerDocument===L&&J(L,a)?-1:b===e||b.ownerDocument===L&&J(L,b)?1:A?$.call(A,a)-$.call(A,b):0:4&c?-1:1)}:function(a,b){if(a===b)return B=!0,0;var c,f=0,g=a.parentNode,h=b.parentNode,i=[a],j=[b];if(!g||!h)return a===e?-1:b===e?1:g?-1:h?1:A?$.call(A,a)-$.call(A,b):0;if(g===h)return d(a,b);for(c=a;c=c.parentNode;)i.unshift(c);for(c=b;c=c.parentNode;)j.unshift(c);for(;i[f]===j[f];)f++;return f?d(i[f],j[f]):i[f]===L?-1:j[f]===L?1:0},e):D},a.matches=function(b,c){return a(b,null,null,c)},a.matchesSelector=function(b,c){if((b.ownerDocument||b)!==D&&C(b),c=c.replace(ha,"='$1']"),s.matchesSelector&&F&&(!H||!H.test(c))&&(!G||!G.test(c)))try{var d=I.call(b,c);if(d||s.disconnectedMatch||b.document&&11!==b.document.nodeType)return d}catch(a){}return a(c,D,null,[b]).length>0},a.contains=function(a,b){return(a.ownerDocument||a)!==D&&C(a),J(a,b)},a.attr=function(a,b){(a.ownerDocument||a)!==D&&C(a);var c=t.attrHandle[b.toLowerCase()],d=c&&U.call(t.attrHandle,b.toLowerCase())?c(a,b,!F):void 0;return void 0!==d?d:s.attributes||!F?a.getAttribute(b):(d=a.getAttributeNode(b))&&d.specified?d.value:null},a.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},a.uniqueSort=function(a){var b,c=[],d=0,e=0;if(B=!s.detectDuplicates,A=!s.sortStable&&a.slice(0),a.sort(R),B){for(;b=a[e++];)b===a[e]&&(d=c.push(e));for(;d--;)a.splice(c[d],1)}return A=null,a},u=a.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(1===e||9===e||11===e){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=u(a)}else if(3===e||4===e)return a.nodeValue}else for(;b=a[d++];)c+=u(b);return c},t=a.selectors={cacheLength:50,createPseudo:c,match:ka,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ra,sa),a[3]=(a[3]||a[4]||a[5]||"").replace(ra,sa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(b){return b[1]=b[1].toLowerCase(),"nth"===b[1].slice(0,3)?(b[3]||a.error(b[0]),b[4]=+(b[4]?b[5]+(b[6]||1):2*("even"===b[3]||"odd"===b[3])),b[5]=+(b[7]+b[8]||"odd"===b[3])):b[3]&&a.error(b[0]),b},PSEUDO:function(a){var b,c=!a[6]&&a[2];return ka.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&ia.test(c)&&(b=w(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ra,sa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=O[a+" "];return b||(b=new RegExp("(^|"+aa+")"+a+"("+aa+"|$)"))&&O(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==S&&a.getAttribute("class")||"")})},ATTR:function(b,c,d){return function(e){var f=a.attr(e,b);return null==f?"!="===c:!c||(f+="","="===c?f===d:"!="===c?f!==d:"^="===c?d&&0===f.indexOf(d):"*="===c?d&&f.indexOf(d)>-1:"$="===c?d&&f.slice(-d.length)===d:"~="===c?(" "+f+" ").indexOf(d)>-1:"|="===c&&(f===d||f.slice(0,d.length+1)===d+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){for(;p;){for(l=b;l=l[p];)if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){for(k=q[K]||(q[K]={}),j=k[a]||[],n=j[0]===M&&j[1],m=j[0]===M&&j[2],l=n&&q.childNodes[n];l=++n&&l&&l[p]||(m=n=0)||o.pop();)if(1===l.nodeType&&++m&&l===b){k[a]=[M,n,m];break}}else if(s&&(j=(b[K]||(b[K]={}))[a])&&j[0]===M)m=j[1];else for(;(l=++n&&l&&l[p]||(m=n=0)||o.pop())&&((h?l.nodeName.toLowerCase()!==r:1!==l.nodeType)||!++m||(s&&((l[K]||(l[K]={}))[a]=[M,m]),l!==b)););return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(b,d){var e,f=t.pseudos[b]||t.setFilters[b.toLowerCase()]||a.error("unsupported pseudo: "+b);return f[K]?f(d):f.length>1?(e=[b,b,"",d],t.setFilters.hasOwnProperty(b.toLowerCase())?c(function(a,b){for(var c,e=f(a,d),g=e.length;g--;)c=$.call(a,e[g]),a[c]=!(b[c]=e[g])}):function(a){return f(a,0,e)}):f}},pseudos:{not:c(function(a){var b=[],d=[],e=x(a.replace(ea,"$1"));return e[K]?c(function(a,b,c,d){for(var f,g=e(a,null,d,[]),h=a.length;h--;)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,c,f){return b[0]=a,e(b,null,f,d),!d.pop()}}),has:c(function(b){return function(c){return a(b,c).length>0}}),contains:c(function(a){return a=a.replace(ra,sa),function(b){return(b.textContent||b.innerText||u(b)).indexOf(a)>-1}}),lang:c(function(b){return ja.test(b||"")||a.error("unsupported lang: "+b),b=b.replace(ra,sa).toLowerCase(),function(a){var c;do if(c=F?a.lang:a.getAttribute("xml:lang")||a.getAttribute("lang"))return c=c.toLowerCase(),c===b||0===c.indexOf(b+"-");while((a=a.parentNode)&&1===a.nodeType);return!1}}),target:function(a){var b=window.location&&window.location.hash;return b&&b.slice(1)===a.id},root:function(a){return a===E},focus:function(a){return a===D.activeElement&&(!D.hasFocus||D.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!t.pseudos.empty(a)},header:function(a){return ma.test(a.nodeName)},input:function(a){return la.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:g(function(){return[0]}),last:g(function(a,b){return[b-1]}),eq:g(function(a,b,c){return[c<0?c+b:c]}),even:g(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:g(function(a,b,c){for(var d=c<0?c+b:c;++d2&&"ID"===(g=f[0]).type&&s.getById&&9===b.nodeType&&F&&t.relative[f[1].type]){if(b=(t.find.ID(g.matches[0].replace(ra,sa),b)||[])[0],!b)return c;l&&(b=b.parentNode),a=a.slice(f.shift().value.length)}for(e=ka.needsContext.test(a)?0:f.length;e--&&(g=f[e],!t.relative[i=g.type]);)if((k=t.find[i])&&(d=k(g.matches[0].replace(ra,sa),pa.test(f[0].type)&&h(b.parentNode)||b))){if(f.splice(e,1),a=d.length&&j(f),!a)return Y.apply(c,d),c;break}}return(l||x(a,m))(d,b,!F,c,pa.test(a)&&h(b.parentNode)||b),c},s.sortStable=K.split("").sort(R).join("")===K,s.detectDuplicates=!!B,C(),s.sortDetached=!0,a}),g("1h",[],function(){function a(a){var b,c,d=a;if(!j(a))for(d=[],b=0,c=a.length;b=0;e--)i(a,b[e],c,d);else for(e=0;e)[^>]*$|#([\w\-]*)$)/,A=a.Event,B=c.makeMap("children,contents,next,prev"),C=c.makeMap("fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom"," "),D=c.makeMap("checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected"," "),E={"for":"htmlFor","class":"className",readonly:"readOnly"},F={"float":"cssFloat"},G={},H={},I=/^\s*|\s*$/g;return l.fn=l.prototype={constructor:l,selector:"",context:null, -length:0,init:function(a,b){var c,d,e=this;if(!a)return e;if(a.nodeType)return e.context=e[0]=a,e.length=1,e;if(b&&b.nodeType)e.context=b;else{if(b)return l(a).attr(b);e.context=b=document}if(f(a)){if(e.selector=a,c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c)return l(b).find(a);if(c[1])for(d=h(a,q(b)).firstChild;d;)x.call(e,d),d=d.nextSibling;else{if(d=q(b).getElementById(c[2]),!d)return e;if(d.id!==c[2])return e.find(a);e.length=1,e[0]=d}}else this.add(a,!1);return e},toArray:function(){return c.toArray(this)},add:function(a,b){var c,d,e=this;if(f(a))return e.add(l(a));if(b!==!1)for(c=l.unique(e.toArray().concat(l.makeArray(a))),e.length=c.length,d=0;d1&&(B[a]||(e=l.unique(e)),0===a.indexOf("parents")&&(e=e.reverse())),e=l(e),c?e.filter(c):e}}),o({parentsUntil:function(a,b){return r(a,"parentNode",b)},nextUntil:function(a,b){return s(a,"nextSibling",1,b).slice(1)},prevUntil:function(a,b){return s(a,"previousSibling",1,b).slice(1)}},function(a,b){l.fn[a]=function(c,d){var e=this,f=[];return e.each(function(){var a=b.call(f,this,c,f);a&&(l.isArray(a)?f.push.apply(f,a):f.push(a))}),this.length>1&&(f=l.unique(f),0!==a.indexOf("parents")&&"prevUntil"!==a||(f=f.reverse())),f=l(f),d?f.filter(d):f}}),l.fn.is=function(a){return!!a&&this.filter(a).length>0},l.fn.init.prototype=l.fn,l.overrideDefaults=function(a){function b(d,e){return c=c||a(),0===arguments.length&&(d=c.element),e||(e=c.context),new b.fn.init(d,e)}var c;return l.extend(b,this),b},d.ie&&d.ie<8&&(u(G,"get",{maxlength:function(a){var b=a.maxLength;return 2147483647===b?v:b},size:function(a){var b=a.size;return 20===b?v:b},"class":function(a){return a.className},style:function(a){var b=a.style.cssText;return 0===b.length?v:b}}),u(G,"set",{"class":function(a,b){a.className=b},style:function(a,b){a.style.cssText=b}})),d.ie&&d.ie<9&&(F["float"]="styleFloat",u(H,"set",{opacity:function(a,b){var c=a.style;null===b||""===b?c.removeAttribute("filter"):(c.zoom=1,c.filter="alpha(opacity="+100*b+")")}})),l.attrHooks=G,l.cssHooks=H,l}),g("1i",["1d"],function(a){function b(c){function d(){return J.createDocumentFragment()}function e(a,b){x(N,a,b)}function f(a,b){x(O,a,b)}function g(a){e(a.parentNode,U(a))}function h(a){e(a.parentNode,U(a)+1)}function i(a){f(a.parentNode,U(a))}function j(a){f(a.parentNode,U(a)+1)}function k(a){a?(I[R]=I[Q],I[S]=I[P]):(I[Q]=I[R],I[P]=I[S]),I.collapsed=N}function l(a){g(a),j(a)}function m(a){e(a,0),f(a,1===a.nodeType?a.childNodes.length:a.nodeValue.length)}function n(a,b){var c=I[Q],d=I[P],e=I[R],f=I[S],g=b.startContainer,h=b.startOffset,i=b.endContainer,j=b.endOffset;return 0===a?w(c,d,g,h):1===a?w(e,f,g,h):2===a?w(e,f,i,j):3===a?w(c,d,i,j):void 0}function o(){y(M)}function p(){return y(K)}function q(){return y(L)}function r(a){var b,d,e=this[Q],f=this[P];3!==e.nodeType&&4!==e.nodeType||!e.nodeValue?(e.childNodes.length>0&&(d=e.childNodes[f]),d?e.insertBefore(a,d):3==e.nodeType?c.insertAfter(a,e):e.appendChild(a)):f?f>=e.nodeValue.length?c.insertAfter(a,e):(b=e.splitText(f),e.parentNode.insertBefore(a,b)):e.parentNode.insertBefore(a,e)}function s(a){var b=I.extractContents();I.insertNode(a),a.appendChild(b),I.selectNode(a)}function t(){return T(new b(c),{startContainer:I[Q],startOffset:I[P],endContainer:I[R],endOffset:I[S],collapsed:I.collapsed,commonAncestorContainer:I.commonAncestorContainer})}function u(a,b){var c;if(3==a.nodeType)return a;if(b<0)return a;for(c=a.firstChild;c&&b>0;)--b,c=c.nextSibling;return c?c:a}function v(){return I[Q]==I[R]&&I[P]==I[S]}function w(a,b,d,e){var f,g,h,i,j,k;if(a==d)return b==e?0:b0&&I.collapse(a):I.collapse(a),I.collapsed=v(),I.commonAncestorContainer=c.findCommonAncestor(I[Q],I[R])}function y(a){var b,c,d,e,f,g,h,i=0,j=0;if(I[Q]==I[R])return z(a);for(b=I[R],c=b.parentNode;c;b=c,c=c.parentNode){if(c==I[Q])return A(b,a);++i}for(b=I[Q],c=b.parentNode;c;b=c,c=c.parentNode){if(c==I[R])return B(b,a);++j}for(d=j-i,e=I[Q];d>0;)e=e.parentNode,d--;for(f=I[R];d<0;)f=f.parentNode,d++;for(g=e.parentNode,h=f.parentNode;g!=h;g=g.parentNode,h=h.parentNode)e=g,f=h;return C(e,f,a)}function z(a){var b,c,e,f,g,h,i,j,k;if(a!=M&&(b=d()),I[P]==I[S])return b;if(3==I[Q].nodeType){if(c=I[Q].nodeValue,e=c.substring(I[P],I[S]),a!=L&&(f=I[Q],j=I[P],k=I[S]-I[P],0===j&&k>=f.nodeValue.length-1?f.parentNode.removeChild(f):f.deleteData(j,k),I.collapse(N)),a==M)return;return e.length>0&&b.appendChild(J.createTextNode(e)),b}for(f=u(I[Q],I[P]),g=I[S]-I[P];f&&g>0;)h=f.nextSibling,i=G(f,a),b&&b.appendChild(i),--g,f=h;return a!=L&&I.collapse(N),b}function A(a,b){var c,e,f,g,h,i;if(b!=M&&(c=d()),e=D(a,b),c&&c.appendChild(e),f=U(a),g=f-I[P],g<=0)return b!=L&&(I.setEndBefore(a),I.collapse(O)),c;for(e=a.previousSibling;g>0;)h=e.previousSibling,i=G(e,b),c&&c.insertBefore(i,c.firstChild),--g,e=h;return b!=L&&(I.setEndBefore(a),I.collapse(O)),c}function B(a,b){var c,e,f,g,h,i;for(b!=M&&(c=d()),f=E(a,b),c&&c.appendChild(f),e=U(a),++e,g=I[S]-e,f=a.nextSibling;f&&g>0;)h=f.nextSibling,i=G(f,b),c&&c.appendChild(i),--g,f=h;return b!=L&&(I.setStartAfter(a),I.collapse(N)),c}function C(a,b,c){var e,f,g,h,i,j,k;for(c!=M&&(f=d()),e=E(a,c),f&&f.appendChild(e),g=U(a),h=U(b),++g,i=h-g,j=a.nextSibling;i>0;)k=j.nextSibling,e=G(j,c),f&&f.appendChild(e),j=k,--i;return e=D(b,c),f&&f.appendChild(e),c!=L&&(I.setStartAfter(a),I.collapse(N)),f}function D(a,b){var c,d,e,f,g,h=u(I[R],I[S]-1),i=h!=I[R];if(h==a)return F(h,i,O,b);for(c=h.parentNode,d=F(c,O,O,b);c;){for(;h;)e=h.previousSibling,f=F(h,i,O,b),b!=M&&d.insertBefore(f,d.firstChild),i=N,h=e;if(c==a)return d;h=c.previousSibling,c=c.parentNode,g=F(c,O,O,b),b!=M&&g.appendChild(d),d=g}}function E(a,b){var c,d,e,f,g,h=u(I[Q],I[P]),i=h!=I[Q];if(h==a)return F(h,i,N,b);for(c=h.parentNode,d=F(c,O,N,b);c;){for(;h;)e=h.nextSibling,f=F(h,i,N,b),b!=M&&d.appendChild(f),i=N,h=e;if(c==a)return d;h=c.nextSibling,c=c.parentNode,g=F(c,O,N,b),b!=M&&g.appendChild(d),d=g}}function F(a,b,d,e){var f,g,h,i,j;if(b)return G(a,e);if(3==a.nodeType){if(f=a.nodeValue,d?(i=I[P],g=f.substring(i),h=f.substring(0,i)):(i=I[S],g=f.substring(0,i),h=f.substring(i)),e!=L&&(a.nodeValue=h),e==M)return;return j=c.clone(a,O),j.nodeValue=g,j}if(e!=M)return c.clone(a,O)}function G(a,b){return b!=M?b==L?c.clone(a,N):a:void a.parentNode.removeChild(a)}function H(){return c.create("body",null,q()).outerText}var I=this,J=c.doc,K=0,L=1,M=2,N=!0,O=!1,P="startOffset",Q="startContainer",R="endContainer",S="endOffset",T=a.extend,U=c.nodeIndex;return T(I,{startContainer:J,startOffset:0,endContainer:J,endOffset:0,collapsed:N,commonAncestorContainer:J,START_TO_START:0,START_TO_END:1,END_TO_END:2,END_TO_START:3,setStart:e,setEnd:f,setStartBefore:g,setStartAfter:h,setEndBefore:i,setEndAfter:j,collapse:k,selectNode:l,selectNodeContents:m,compareBoundaryPoints:n,deleteContents:o,extractContents:p,cloneContents:q,insertNode:r,surroundContents:s,cloneRange:t,toStringIE:H}),I}return b.prototype.toString=function(){return this.toStringIE()},b}),h("4d",Object),g("1u",["1","4d"],function(a,b){var c=a.never,d=a.always,e=function(){return f},f=function(){var f=function(a){return a.isNone()},g=function(a){return a()},h=function(a){return a},i=function(){},j={fold:function(a,b){return a()},is:c,isSome:c,isNone:d,getOr:h,getOrThunk:g,getOrDie:function(a){throw new Error(a||"error: getOrDie called on none.")},or:h,orThunk:g,map:e,ap:e,each:i,bind:e,flatten:e,exists:c,forall:d,filter:e,equals:f,equals_:f,toArray:function(){return[]},toString:a.constant("none()")};return b.freeze&&b.freeze(j),j}(),g=function(a){var b=function(){return a},h=function(){return k},i=function(b){return g(b(a))},j=function(b){return b(a)},k={fold:function(b,c){return c(a)},is:function(b){return a===b},isSome:d,isNone:c,getOr:b,getOrThunk:b,getOrDie:b,or:h,orThunk:h,map:i,ap:function(b){return b.fold(e,function(b){return g(b(a))})},each:function(b){b(a)},bind:j,flatten:b,exists:j,forall:j,filter:function(b){return b(a)?k:f},equals:function(b){return b.is(a)},equals_:function(b,d){return b.fold(c,function(b){return d(a,b)})},toArray:function(){return[a]},toString:function(){return"some("+a+")"}};return k},h=function(a){return null===a||void 0===a?f:g(a)};return{some:g,none:e,from:h}}),h("4e",String),g("1t",["1u","3","4","4e"],function(a,b,c,d){var e=function(){var a=b.prototype.indexOf,c=function(b,c){return a.call(b,c)},d=function(a,b){return u(a,b)};return void 0===a?d:c}(),f=function(b,c){var d=e(b,c);return d===-1?a.none():a.some(d)},g=function(a,b){return e(a,b)>-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e=b.length&&c(d)}};0===b.length?c([]):a.each(b,function(a,b){a.get(f(b))})})};return{par:b}}),g("4g",["1t","4f","61"],function(a,b,c){var d=function(a){return c.par(a,b.nu)},e=function(b,c){var e=a.map(b,c);return d(e)},f=function(a,b){return function(c){return b(c).bind(a)}};return{par:d,mapM:e,compose:f}}),g("4h",["1","1u"],function(a,b){var c=function(d){var e=function(a){return d===a},f=function(a){return c(d)},g=function(a){return c(d)},h=function(a){return c(a(d))},i=function(a){a(d)},j=function(a){return a(d)},k=function(a,b){return b(d)},l=function(a){return a(d)},m=function(a){return a(d)},n=function(){return b.some(d)};return{is:e,isValue:a.constant(!0),isError:a.constant(!1),getOr:a.constant(d),getOrThunk:a.constant(d),getOrDie:a.constant(d),or:f,orThunk:g,fold:k,map:h,each:i,bind:j,exists:l,forall:m,toOption:n}},d=function(c){var e=function(a){return a()},f=function(){return a.die(c)()},g=function(a){return a},h=function(a){return a()},i=function(a){return d(c)},j=function(a){return d(c)},k=function(a,b){return a(c)};return{is:a.constant(!1),isValue:a.constant(!1),isError:a.constant(!0),getOr:a.identity,getOrThunk:e,getOrDie:f,or:g,orThunk:h,fold:k,map:i,each:a.noop,bind:j,exists:a.constant(!1),forall:a.constant(!0),toOption:b.none}};return{value:c,error:d}}),g("1j",["1t","1","4f","4g","4h","14","1d"],function(a,b,c,d,e,f,g){"use strict";return function(h,i){function j(a){h.getElementsByTagName("head")[0].appendChild(a)}function k(a,b,c){function d(){for(var a=t.passed,b=a.length;b--;)a[b]();t.status=2,t.passed=[],t.failed=[]}function e(){for(var a=t.failed,b=a.length;b--;)a[b]();t.status=3,t.passed=[],t.failed=[]}function i(){var a=navigator.userAgent.match(/WebKit\/(\d*)/);return!!(a&&a[1]<536)}function k(a,b){a()||((new Date).getTime()-s0)return r=h.createElement("style"),r.textContent='@import "'+a+'"',p(),void j(r);o()}j(q),q.href=a}}var l,m=0,n={};i=i||{},l=i.maxLoadTime||5e3;var o=function(a){return c.nu(function(c){k(a,b.compose(c,b.constant(e.value(a))),b.compose(c,b.constant(e.error(a))))})},p=function(a){return a.fold(b.identity,b.identity)},q=function(b,c,e){d.par(a.map(b,o)).get(function(b){var d=a.partition(b,function(a){return a.isValue()});d.fail.length>0?e(d.fail.map(p)):c(d.pass.map(p))})};return{load:k,loadAll:q}}}),g("j",[],function(){return function(a,b){function c(a,c,d,e){var f,g;if(a){if(!e&&a[c])return a[c];if(a!=b){if(f=a[d])return f;for(g=a.parentNode;g&&g!=b;g=g.parentNode)if(f=g[d])return f}}}function d(a,c,d,e){var f,g,h;if(a){if(f=a[d],b&&f===b)return;if(f){if(!e)for(h=f[c];h;h=h[c])if(!h[c])return h;return f}if(g=a.parentNode,g&&g!==b)return g}}var e=a;this.current=function(){return e},this.next=function(a){return e=c(e,"firstChild","nextSibling",a)},this.prev=function(a){return e=c(e,"lastChild","previousSibling",a)},this.prev2=function(a){return e=d(e,"lastChild","previousSibling",a)}}}),g("s",["1d"],function(a){function b(a){var b;return b=document.createElement("div"),b.innerHTML=a,b.textContent||b.innerText||a}function c(a,b){var c,d,f,g={};if(a){for(a=a.split(","),b=b||10,c=0;c\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,i=/[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,j=/[<>&\"\']/g,k=/&#([a-z0-9]+);?|&([a-z0-9]+);/gi,l={128:"\u20ac",130:"\u201a",131:"\u0192",132:"\u201e",133:"\u2026",134:"\u2020",135:"\u2021",136:"\u02c6",137:"\u2030",138:"\u0160",139:"\u2039",140:"\u0152",142:"\u017d",145:"\u2018",146:"\u2019",147:"\u201c",148:"\u201d",149:"\u2022",150:"\u2013",151:"\u2014",152:"\u02dc",153:"\u2122",154:"\u0161",155:"\u203a",156:"\u0153",158:"\u017e",159:"\u0178"};e={'"':""","'":"'","<":"<",">":">","&":"&","`":"`"},f={"<":"<",">":">","&":"&",""":'"',"'":"'"},d=c("50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,t9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro",32);var m={encodeRaw:function(a,b){return a.replace(b?h:i,function(a){return e[a]||a})},encodeAllRaw:function(a){return(""+a).replace(j,function(a){return e[a]||a})},encodeNumeric:function(a,b){return a.replace(b?h:i,function(a){return a.length>1?"&#"+(1024*(a.charCodeAt(0)-55296)+(a.charCodeAt(1)-56320)+65536)+";":e[a]||"&#"+a.charCodeAt(0)+";"})},encodeNamed:function(a,b,c){return c=c||d,a.replace(b?h:i,function(a){return e[a]||c[a]||a})},getEncodeFunc:function(a,b){function f(a,c){return a.replace(c?h:i,function(a){return void 0!==e[a]?e[a]:void 0!==b[a]?b[a]:a.length>1?"&#"+(1024*(a.charCodeAt(0)-55296)+(a.charCodeAt(1)-56320)+65536)+";":"&#"+a.charCodeAt(0)+";"})}function j(a,c){return m.encodeNamed(a,c,b)}return b=c(b)||d,a=g(a.replace(/\+/g,",")),a.named&&a.numeric?f:a.named?b?j:m.encodeNamed:a.numeric?m.encodeNumeric:m.encodeRaw},decode:function(a){return a.replace(k,function(a,c){return c?(c="x"===c.charAt(0).toLowerCase()?parseInt(c.substr(1),16):parseInt(c,10),c>65535?(c-=65536,String.fromCharCode(55296+(c>>10),56320+(1023&c))):l[c]||String.fromCharCode(c)):f[a]||d[a]||b(a)})}};return m}),g("v",["1d"],function(a){function b(b,c){return b=a.trim(b),b?b.split(c||" "):[]}function c(a){function c(a,c,d){function e(a,b){var c,d,e={};for(c=0,d=a.length;c
    ")}var f=this,g=f._id,h=f.settings,i=f.classPrefix,j=f.state.get("text"),k=f.settings.icon,l="",m=h.shortcut,n=f.encode(h.url),o="";return k&&f.parent().classes.add("menu-has-icons"),h.image&&(l=" style=\"background-image: url('"+h.image+"')\""),m&&(m=a(m)),k=i+"ico "+i+"i-"+(f.settings.icon||"none"),o="-"!==j?'\xa0":"",j=e(f.encode(d(j))),n=e(f.encode(d(n))),'
    '+o+("-"!==j?''+j+"":"")+(m?'
    '+m+"
    ":"")+(h.menu?'
    ':"")+(n?'":"")+"
    "},postRender:function(){var a=this,b=a.settings,c=b.textStyle;if("function"==typeof c&&(c=c.call(this)),c){var e=a.getEl("text");e&&e.setAttribute("style",c)}return a.on("mouseenter click",function(c){c.control===a&&(b.menu||"click"!==c.type?(a.showMenu(),c.aria&&a.menu.focus(!0)):(a.fire("select"),d.requestAnimationFrame(function(){a.parent().hideAll()})))}),a._super(),a},hover:function(){var a=this;return a.parent().items().each(function(a){a.classes.remove("selected")}),a.classes.toggle("selected",!0),a},active:function(a){return"undefined"!=typeof a&&this.aria("checked",a),this._super(a)},remove:function(){this._super(),this.menu&&this.menu.remove()}})}),g("3y",["b","2q","14"],function(a,b,c){"use strict";return function(d,e){var f,g,h=this,i=b.classPrefix;h.show=function(b,j){function k(){f&&(a(d).append('
    '),j&&j())}return h.hide(),f=!0,b?g=c.setTimeout(k,b):k(),h},h.hide=function(){var a=d.lastChild;return c.clearTimeout(g),a&&a.className.indexOf("throbber")!=-1&&a.parentNode.removeChild(a),f=!1,h}}}),g("3z",["2z","3x","3y","1d"],function(a,b,c,d){"use strict";return a.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(a){var b=this;if(a.autohide=!0,a.constrainToViewport=!0,"function"==typeof a.items&&(a.itemsFactory=a.items,a.items=[]),a.itemDefaults)for(var c=a.items,e=c.length;e--;)c[e]=d.extend({},a.itemDefaults,c[e]);b._super(a),b.classes.add("menu")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){var a=this;a.hideAll(),a.fire("select")},load:function(){function a(){e.throbber&&(e.throbber.hide(),e.throbber=null)}var b,d,e=this;d=e.settings.itemsFactory,d&&(e.throbber||(e.throbber=new c(e.getEl("body"),!0),0===e.items().length?(e.throbber.show(),e.fire("loading")):e.throbber.show(100,function(){e.items().remove(),e.fire("loading")}),e.on("hide close",a)),e.requestTime=b=(new Date).getTime(),e.settings.itemsFactory(function(c){return 0===c.length?void e.hide():void(e.requestTime===b&&(e.getEl().style.width="",e.getEl("body").style.width="",a(),e.items().remove(),e.getEl("body").innerHTML="",e.add(c),e.renderNew(),e.fire("loaded")))}))},hideAll:function(){var a=this;return this.find("menuitem").exec("hideMenu"),a._super()},preRender:function(){var a=this;return a.items().each(function(b){var c=b.settings;if(c.icon||c.image||c.selectable)return a._hasIcons=!0,!1}),a.settings.itemsFactory&&a.on("postrender",function(){a.settings.itemsFactory&&a.load()}),a._super()}})}),g("40",["3w","3z"],function(a,b){"use strict";return a.extend({init:function(a){function b(c){for(var f=0;f0&&(e=c[0].text,g.state.set("value",c[0].value)),g.state.set("menu",c)),g.state.set("text",a.text||e),g.classes.add("listbox"),g.on("select",function(b){var c=b.control;f&&(b.lastControl=f),a.multiple?c.active(!c.active()):g.value(b.control.value()),f=c})},bindStates:function(){function a(a,c){a instanceof b&&a.items().each(function(a){a.hasMenus()||a.active(a.value()===c)})}function c(a,b){var d;if(a)for(var e=0;e'},postRender:function(){var a=this;a._super(),a.resizeDragHelper=new b(this._id,{start:function(){a.fire("ResizeStart")},drag:function(b){"both"!=a.settings.direction&&(b.deltaX=0),a.fire("Resize",b)},stop:function(){a.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}})}),g("43",["33"],function(a){"use strict";function b(a){var b="";if(a)for(var c=0;c'+a[c]+"";return b}return a.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(a){var b=this;b._super(a),b.settings.size&&(b.size=b.settings.size),b.settings.options&&(b._options=b.settings.options),b.on("keydown",function(a){var c;13==a.keyCode&&(a.preventDefault(),b.parents().reverse().each(function(a){if(a.toJSON)return c=a,!1}),b.fire("submit",{data:c.toJSON()}))})},options:function(a){return arguments.length?(this.state.set("options",a),this):this.state.get("options")},renderHtml:function(){var a,c=this,d="";return a=b(c._options),c.size&&(d=' size = "'+c.size+'"'),'"},bindStates:function(){var a=this;return a.state.on("change:options",function(c){a.getEl().innerHTML=b(c.value)}),a._super()}})}),g("44",["33","2u","4z"],function(a,b,c){"use strict";function d(a,b,c){return ac&&(a=c),a}function e(a,b,c){a.setAttribute("aria-"+b,c)}function f(a,b){var d,f,g,h,i,j;"v"==a.settings.orientation?(h="top",g="height",f="h"):(h="left",g="width",f="w"),j=a.getEl("handle"),d=(a.layoutRect()[f]||100)-c.getSize(j)[g],i=d*((b-a._minValue)/(a._maxValue-a._minValue))+"px",j.style[h]=i,j.style.height=a.layoutRect().h+"px",e(j,"valuenow",b),e(j,"valuetext",""+a.settings.previewFilter(b)),e(j,"valuemin",a._minValue),e(j,"valuemax",a._maxValue)}return a.extend({init:function(a){var b=this;a.previewFilter||(a.previewFilter=function(a){return Math.round(100*a)/100}),b._super(a),b.classes.add("slider"),"v"==a.orientation&&b.classes.add("vertical"),b._minValue=a.minValue||0,b._maxValue=a.maxValue||100,b._initValue=b.state.get("value")},renderHtml:function(){var a=this,b=a._id,c=a.classPrefix;return'
    '},reset:function(){this.value(this._initValue).repaint()},postRender:function(){function a(a,b,c){return(c+a)/(b-a)}function e(a,b,c){return c*(b-a)-a}function f(b,c){function f(f){var g;g=n.value(),g=e(b,c,a(b,c,g)+.05*f),g=d(g,b,c),n.value(g),n.fire("dragstart",{value:g}),n.fire("drag",{value:g}),n.fire("dragend",{value:g})}n.on("keydown",function(a){switch(a.keyCode){case 37:case 38:f(-1);break;case 39:case 40:f(1)}})}function g(a,e,f){var g,h,i,o,p;n._dragHelper=new b(n._id,{handle:n._id+"-handle",start:function(a){g=a[j],h=parseInt(n.getEl("handle").style[k],10),i=(n.layoutRect()[m]||100)-c.getSize(f)[l],n.fire("dragstart",{value:p})},drag:function(b){var c=b[j]-g;o=d(h+c,0,i),f.style[k]=o+"px",p=a+o/i*(e-a),n.value(p),n.tooltip().text(""+n.settings.previewFilter(p)).show().moveRel(f,"bc tc"),n.fire("drag",{value:p})},stop:function(){n.tooltip().hide(),n.fire("dragend",{value:p})}})}var h,i,j,k,l,m,n=this;h=n._minValue,i=n._maxValue,"v"==n.settings.orientation?(j="screenY",k="top",l="height",m="h"):(j="screenX",k="left",l="width",m="w"),n._super(),f(h,i,n.getEl("handle")),g(h,i,n.getEl("handle"))},repaint:function(){this._super(),f(this,this.value())},bindStates:function(){var a=this;return a.state.on("change:value",function(b){f(a,b.value)}),a._super()}})}),g("45",["33"],function(a){"use strict";return a.extend({renderHtml:function(){var a=this;return a.classes.add("spacer"),a.canFocus=!1,'
    '}})}),g("46",["3w","4z","b"],function(a,b,c){return a.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var a,d,e=this,f=e.getEl(),g=e.layoutRect();return e._super(),a=f.firstChild,d=f.lastChild,c(a).css({width:g.w-b.getSize(d).width,height:g.h-2}),c(d).css({height:g.h-2}),e},activeMenu:function(a){var b=this;c(b.getEl().lastChild).toggleClass(b.classPrefix+"active",a)},renderHtml:function(){var a,b=this,c=b._id,d=b.classPrefix,e=b.state.get("icon"),f=b.state.get("text"),g="";return a=b.settings.image,a?(e="none","string"!=typeof a&&(a=window.getSelection?a[0]:a[1]),a=" style=\"background-image: url('"+a+"')\""):a="",e=b.settings.icon?d+"ico "+d+"i-"+e:"",f&&(b.classes.add("btn-has-text"),g=''+b.encode(f)+""),'
    '},postRender:function(){var a=this,b=a.settings.onclick;return a.on("click",function(a){var c=a.target;if(a.control==this)for(;c;){if(a.aria&&"down"!=a.aria.key||"BUTTON"==c.nodeName&&c.className.indexOf("open")==-1)return a.stopImmediatePropagation(),void(b&&b.call(this,a));c=c.parentNode}}),delete a.settings.onclick,a._super()}})}),g("47",["3o"],function(a){"use strict";return a.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}})}),g("48",["2w","b","4z"],function(a,b,c){"use strict";return a.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(a){var c;this.activeTabId&&(c=this.getEl(this.activeTabId),b(c).removeClass(this.classPrefix+"active"),c.setAttribute("aria-selected","false")),this.activeTabId="t"+a,c=this.getEl("t"+a),c.setAttribute("aria-selected","true"),b(c).addClass(this.classPrefix+"active"),this.items()[a].show().fire("showtab"),this.reflow(),this.items().each(function(b,c){a!=c&&b.hide()})},renderHtml:function(){var a=this,b=a._layout,c="",d=a.classPrefix;return a.preRender(),b.preRender(a),a.items().each(function(b,e){var f=a._id+"-t"+e;b.aria("role","tabpanel"),b.aria("labelledby",f),c+='"}),'
    '+c+'
    '+b.renderHtml(a)+"
    "},postRender:function(){var a=this;a._super(),a.settings.activeTab=a.settings.activeTab||0,a.activateTab(a.settings.activeTab),this.on("click",function(b){var c=b.target.parentNode;if(c&&c.id==a._id+"-head")for(var d=c.childNodes.length;d--;)c.childNodes[d]==b.target&&a.activateTab(d)})},initLayoutRect:function(){var a,b,d,e=this;b=c.getSize(e.getEl("head")).width,b=b<0?0:b,d=0,e.items().each(function(a){b=Math.max(b,a.layoutRect().minW),d=Math.max(d,a.layoutRect().minH)}),e.items().each(function(a){a.settings.x=0,a.settings.y=0,a.settings.w=b,a.settings.h=d,a.layoutRect({x:0,y:0,w:b,h:d})});var f=c.getSize(e.getEl("head")).height;return e.settings.minWidth=b,e.settings.minHeight=d+f,a=e._super(),a.deltaH+=f,a.innerH=a.h-a.deltaH,a}})}),g("49",["33","1d","4z"],function(a,b,c){return a.extend({init:function(a){var b=this;b._super(a),b.classes.add("textbox"),a.multiline?b.classes.add("multiline"):(b.on("keydown",function(a){var c;13==a.keyCode&&(a.preventDefault(),b.parents().reverse().each(function(a){if(a.toJSON)return c=a,!1}),b.fire("submit",{data:c.toJSON()}))}),b.on("keyup",function(a){b.state.set("value",a.target.value)}))},repaint:function(){var a,b,c,d,e,f=this,g=0;a=f.getEl().style,b=f._layoutRect,e=f._lastRepaintRect||{};var h=document;return!f.settings.multiline&&h.all&&(!h.documentMode||h.documentMode<=8)&&(a.lineHeight=b.h-g+"px"),c=f.borderBox,d=c.left+c.right+8,g=c.top+c.bottom+(f.settings.multiline?8:0),b.x!==e.x&&(a.left=b.x+"px",e.x=b.x),b.y!==e.y&&(a.top=b.y+"px",e.y=b.y),b.w!==e.w&&(a.width=b.w-d+"px",e.w=b.w),b.h!==e.h&&(a.height=b.h-g+"px",e.h=b.h),f._lastRepaintRect=e,f.fire("repaint",{},!1),f},renderHtml:function(){var a,d,e=this,f=e.settings;return a={id:e._id,hidefocus:"1"},b.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(b){a[b]=f[b]}),e.disabled()&&(a.disabled="disabled"),f.subtype&&(a.type=f.subtype),d=c.create(f.multiline?"textarea":"input",a),d.value=e.state.get("value"),d.className=e.classes,d.outerHTML},value:function(a){return arguments.length?(this.state.set("value",a),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var a=this;a.getEl().value=a.state.get("value"),a._super(),a.$el.on("change",function(b){a.state.set("value",b.target.value),a.fire("change",b)})},bindStates:function(){var a=this;return a.state.on("change:value",function(b){a.getEl().value!=b.value&&(a.getEl().value=b.value)}),a.state.on("change:disabled",function(b){a.getEl().disabled=b.value}),a._super()},remove:function(){this.$el.off(),this._super()}})}),h("5x",RegExp),g("4a",["33","1d","4z","5x"],function(a,b,c,d){return a.extend({init:function(a){var c=this;a=b.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},a),c._super(a),c.classes.add("dropzone"),a.multiple&&c.classes.add("multiple")},renderHtml:function(){var a,b,d=this,e=d.settings;return a={id:d._id,hidefocus:"1"},b=c.create("div",a,""+this.translate(e.text)+""),e.height&&c.css(b,"height",e.height+"px"),e.width&&c.css(b,"width",e.width+"px"),b.className=d.classes,b.outerHTML},postRender:function(){var a=this,c=function(b){b.preventDefault(),a.classes.toggle("dragenter"),a.getEl().className=a.classes},e=function(c){var e=a.settings.accept;if("string"!=typeof e)return c;var f=new d("("+e.split(/\s*,\s*/).join("|")+")$","i");return b.grep(c,function(a){return f.test(a.name)})};a._super(),a.$el.on("dragover",function(a){a.preventDefault()}),a.$el.on("dragenter",c),a.$el.on("dragleave",c),a.$el.on("drop",function(b){if(b.preventDefault(),!a.state.get("disabled")){var c=e(b.dataTransfer.files);a.value=function(){return c.length?a.settings.multiple?c:c[0]:null},c.length&&a.fire("change",b)}})},remove:function(){this.$el.off(),this._super()}})}),g("4b",["38","1d","4z","b","5x"],function(a,b,c,d,e){return a.extend({init:function(a){var c=this;a=b.extend({text:"Browse...",multiple:!1,accept:null},a),c._super(a),c.classes.add("browsebutton"),a.multiple&&c.classes.add("multiple")},postRender:function(){var a=this,b=c.create("input",{type:"file",id:a._id+"-browse",accept:a.settings.accept});a._super(),d(b).on("change",function(b){var c=b.target.files;a.value=function(){return c.length?a.settings.multiple?c:c[0]:null},b.preventDefault(),c.length&&a.fire("change",b)}),d(b).on("click",function(a){a.stopPropagation()}),d(a.getEl("button")).on("click",function(a){a.stopPropagation(),b.click()}),a.getEl().appendChild(b)},remove:function(){d(this.getEl("button")).off(),d(this.getEl("input")).off(),this._super()}})}),g("10",["2n","2o","2p","2q","2r","2s","2t","2u","2v","2w","2x","2y","2z","30","31","32","33","34","35","36","37","38","39","3a","3b","3c","3d","3e","3f","3g","3h","3i","3j","3k","3l","3m","3n","3o","3p","3q","3r","3s","3t","3u","3v","3w","3x","3y","3z","40","41","42","43","44","45","46","47","48","49","4a","4b"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_,aa,ba,ca,da,ea,fa,ga){"use strict";var ha=function(a,b){e.add(a.split(".").pop(),b)},ia=function(a,b,c){var d,e;for(e=b.split(/[.\/]/),d=0;d0?",":"")+a(b[d],c);return e+"]"}e="{";for(g in b)b.hasOwnProperty(g)&&(e+="function"!=typeof b[g]?(e.length>1?","+c:c)+g+c+":"+a(b[g],c):"");return e+"}"}return""+b}return{serialize:a,parse:function(a){try{return window[String.fromCharCode(101)+"val"]("("+a+")")}catch(a){}}}}),g("18",["c"],function(a){return{callbacks:{},count:0,send:function(b){var c=this,d=a.DOM,e=void 0!==b.count?b.count:c.count,f="tinymce_jsonp_"+e;c.callbacks[e]=function(a){d.remove(f),delete c.callbacks[e],b.callback(a)},d.add(d.doc.body,"script",{id:f,src:b.url,type:"text/javascript"}),c.count++}}}),g("1g",["1b","1d"],function(a,b){var c={send:function(a){function d(){!a.async||4==e.readyState||f++>1e4?(a.success&&f<1e4&&200==e.status?a.success.call(a.success_scope,""+e.responseText,e,a):a.error&&a.error.call(a.error_scope,f>1e4?"TIMED_OUT":"GENERAL",e,a),e=null):setTimeout(d,10)}var e,f=0;if(a.scope=a.scope||this,a.success_scope=a.success_scope||a.scope,a.error_scope=a.error_scope||a.scope,a.async=a.async!==!1,a.data=a.data||"",c.fire("beforeInitialize",{settings:a}),e=new XMLHttpRequest){if(e.overrideMimeType&&e.overrideMimeType(a.content_type),e.open(a.type||(a.data?"POST":"GET"),a.url,a.async),a.crossDomain&&(e.withCredentials=!0),a.content_type&&e.setRequestHeader("Content-Type",a.content_type),a.requestheaders&&b.each(a.requestheaders,function(a){e.setRequestHeader(a.key,a.value)}),e.setRequestHeader("X-Requested-With","XMLHttpRequest"),e=c.fire("beforeSend",{xhr:e,settings:a}).xhr,e.send(a.data),!a.async)return d();setTimeout(d,10)}}};return b.extend(c,a),c}),g("19",["17","1g","1d"],function(a,b,c){function d(a){this.settings=e({},a),this.count=0}var e=c.extend;return d.sendRPC=function(a){return(new d).send(a)},d.prototype={send:function(c){var d=c.error,f=c.success;c=e(this.settings,c),c.success=function(b,e){b=a.parse(b),"undefined"==typeof b&&(b={error:"JSON Parse error."}),b.error?d.call(c.error_scope||c.scope,b.error,e):f.call(c.success_scope||c.scope,b.result)},c.error=function(a,b){d&&d.call(c.error_scope||c.scope,a,b)},c.data=a.serialize({id:c.id||"c"+this.count++,method:c.method,params:c.params}),c.content_type="application/json",b.send(c)}},d}),g("1a",[],function(){function a(){g=[];for(var a in f)g.push(a);d.length=g.length}function b(){function b(a){var b,c;return c=void 0!==a?j+a:d.indexOf(",",j),c===-1||c>d.length?null:(b=d.substring(j,c),j=c+1,b)}var c,d,g,j=0;if(f={},i){e.load(h),d=e.getAttribute(h)||"";do{var k=b();if(null===k)break;if(c=b(parseInt(k,32)||0),null!==c){if(k=b(),null===k)break;g=b(parseInt(k,32)||0),c&&(f[c]=g)}}while(null!==c);a()}}function c(){var b,c="";if(i){for(var d in f)b=f[d],c+=(c?",":"")+d.length.toString(32)+","+d+","+b.length.toString(32)+","+b;e.setAttribute(h,c);try{e.save(h)}catch(a){}a()}}var d,e,f,g,h,i;try{if(window.localStorage)return localStorage}catch(a){}return h="tinymce",e=document.documentElement,i=!!e.addBehavior,i&&e.addBehavior("#default#userData"),d={key:function(a){return g[a]},getItem:function(a){return a in f?f[a]:null},setItem:function(a,b){f[a]=""+b,c()},removeItem:function(a){delete f[a],c()},clear:function(){f={},c()}},b(),d}),g("2",["5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","10","11","12","13","14","15","16","17","18","19","1a","1b","1c","1d","1e","1f","1g"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V){var W=r,X={geom:{Rect:v},util:{Promise:R,Delay:J,Tools:S,VK:U,URI:T,Class:H,EventDispatcher:K,Observable:Q,I18n:L,XHR:V,JSON:M,JSONRequest:O,JSONP:N,LocalStorage:P,Color:I},dom:{EventUtils:i,Sizzle:n,DomQuery:g,TreeWalker:o,DOMUtils:h,ScriptLoader:k,RangeUtils:j,Serializer:m,ControlSelection:f,BookmarkManager:e,Selection:l,Event:i.Event},html:{Styles:C,Entities:x,Node:y,Schema:A,SaxParser:z,DomParser:w,Writer:D,Serializer:B},Env:t,AddOnManager:a,Formatter:b,UndoManager:G,EditorCommands:q,WindowManager:d,NotificationManager:c,EditorObservable:s,Shortcuts:E,Editor:p,FocusManager:u,EditorManager:r,DOM:h.DOM,ScriptLoader:k.ScriptLoader,PluginManager:a.PluginManager,ThemeManager:a.ThemeManager,trim:S.trim,isArray:S.isArray,is:S.is,toArray:S.toArray,makeMap:S.makeMap,each:S.each,map:S.map,grep:S.grep,inArray:S.inArray,extend:S.extend,create:S.create,walk:S.walk,createNS:S.createNS,resolve:S.resolve,explode:S.explode,_addCacheSuffix:S._addCacheSuffix,isOpera:t.opera,isWebKit:t.webkit,isIE:t.ie,isGecko:t.gecko,isMac:t.mac};return W=S.extend(W,X),F.appendTo(W),W}),g("0",["1","2"],function(a,b){var c=this||window,d=function(b){"function"!=typeof c.define||c.define.amd||(c.define("ephox/tinymce",[],a.constant(b)),c.define("m",[],a.constant(b))),"object"==typeof module&&(module.exports=b); -},e=function(a){window.tinymce=a,window.tinyMCE=a};return function(){return e(b),d(b),b}}),d("0")()}(); \ No newline at end of file +// 4.7.0 (2017-10-03) +!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-1},h=function(a,b){return t(a,b).isSome()},i=function(a,b){for(var c=[],d=0;d=0;c--){var d=a[c];b(d,c,a)}},n=function(a,b){for(var c=[],d=[],e=0,f=a.length;e=534;return{opera:g,webkit:h,ie:i,gecko:l,mac:m,iOS:n,android:o,contentEditable:v,transparentSrc:"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",caretAfter:8!=i,range:e.getSelection&&"Range"in e,documentMode:i&&!k?b.documentMode||7:10,fileApi:p,ceFalse:i===!1||i>8,canHaveCSP:i===!1||i>11,desktop:!q&&!r,windowsPhone:s}}),h("1n",clearInterval),h("1o",clearTimeout),h("1p",setInterval),h("1q",setTimeout),g("1d",[],function(){function a(a,b){return function(){a.apply(b,arguments)}}function b(b){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof b)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],h(b,a(d,this),a(e,this))}function c(a){var b=this;return null===this._state?void this._deferreds.push(a):void i(function(){var c=b._state?a.onFulfilled:a.onRejected;if(null===c)return void(b._state?a.resolve:a.reject)(b._value);var d;try{d=c(b._value)}catch(b){return void a.reject(b)}a.resolve(d)})}function d(b){try{if(b===this)throw new TypeError("A promise cannot be resolved with itself.");if(b&&("object"==typeof b||"function"==typeof b)){var c=b.then;if("function"==typeof c)return void h(a(c,b),a(d,this),a(e,this))}this._state=!0,this._value=b,f.call(this)}catch(a){e.call(this,a)}}function e(a){this._state=!1,this._value=a,f.call(this)}function f(){for(var a=0,b=this._deferreds.length;a0&&(d=c[0]),a.deepPath&&(c=a.deepPath(),c&&c.length>0&&(d=c[0])),d}function h(b,d){var e,f,h=d||{};for(e in b)m[e]||(h[e]=b[e]);if(h.target||(h.target=h.srcElement||a),c.experimentalShadowDom&&(h.target=g(b,h.target)),b&&l.test(b.type)&&b.pageX===f&&b.clientX!==f){var i=h.target.ownerDocument||a,j=i.documentElement,k=i.body;h.pageX=b.clientX+(j&&j.scrollLeft||k&&k.scrollLeft||0)-(j&&j.clientLeft||k&&k.clientLeft||0),h.pageY=b.clientY+(j&&j.scrollTop||k&&k.scrollTop||0)-(j&&j.clientTop||k&&k.clientTop||0)}return h.preventDefault=function(){h.isDefaultPrevented=p,b&&(b.preventDefault?b.preventDefault():b.returnValue=!1)},h.stopPropagation=function(){h.isPropagationStopped=p,b&&(b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)},h.stopImmediatePropagation=function(){h.isImmediatePropagationStopped=p,h.stopPropagation()},n(h)===!1&&(h.isDefaultPrevented=o,h.isPropagationStopped=o,h.isImmediatePropagationStopped=o),"undefined"==typeof h.metaKey&&(h.metaKey=!1),h}function i(a,b,g){function h(){return"complete"===l.readyState||"interactive"===l.readyState&&l.body}function i(){g.domLoaded||(g.domLoaded=!0,b(m))}function j(){h()&&(f(l,"readystatechange",j),i())}function k(){try{l.documentElement.doScroll("left")}catch(a){return void d.setTimeout(k)}i()}var l=a.document,m={type:"ready"};return g.domLoaded?void b(m):(!l.addEventListener||c.ie&&c.ie<11?(e(l,"readystatechange",j),l.documentElement.doScroll&&a.self===a.top&&k()):h()?i():e(a,"DOMContentLoaded",i),void e(a,"load",i))}function j(){function c(a,b){var c,d,e,f,g=o[b];if(c=g&&g[a.type])for(d=0,e=c.length;dt.cacheLength&&delete a[b.shift()],a[c+" "]=d}var b=[];return a}function c(a){return a[K]=!0,a}function d(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||T)-(~a.sourceIndex||T);if(d)return d;if(c)for(;c=c.nextSibling;)if(c===b)return-1;return a?1:-1}function e(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function f(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function g(a){return c(function(b){return b=+b,c(function(c,d){for(var e,f=a([],c.length,b),g=f.length;g--;)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function h(a){return a&&typeof a.getElementsByTagName!==S&&a}function i(){}function j(a){for(var b=0,c=a.length,d="";b1?function(b,c,d){for(var e=a.length;e--;)if(!a[e](b,c,d))return!1;return!0}:a[0]}function m(b,c,d){for(var e=0,f=c.length;e-1&&(c[j]=!(g[j]=l))}}else t=n(t===g?t.splice(q,t.length):t),f?f(null,g,t,i):Y.apply(g,t)})}function p(a){for(var b,c,d,e=a.length,f=t.relative[a[0].type],g=f||t.relative[" "],h=f?1:0,i=k(function(a){return a===b},g,!0),m=k(function(a){return $.call(b,a)>-1},g,!0),n=[function(a,c,d){return!f&&(d||c!==z)||((b=c).nodeType?i(a,c,d):m(a,c,d))}];h1&&l(n),h>1&&j(a.slice(0,h-1).concat({value:" "===a[h-2].type?"*":""})).replace(ea,"$1"),c,h0,f=b.length>0,g=function(c,g,h,i,j){var k,l,m,o=0,p="0",q=c&&[],r=[],s=z,u=c||f&&t.find.TAG("*",j),v=M+=null==s?1:Math.random()||.1,w=u.length;for(j&&(z=g!==D&&g);p!==w&&null!=(k=u[p]);p++){if(f&&k){for(l=0;m=b[l++];)if(m(k,g,h)){i.push(k);break}j&&(M=v)}e&&((k=!m&&k)&&o--,c&&q.push(k))}if(o+=p,e&&p!==o){for(l=0;m=d[l++];)m(q,r,g,h);if(c){if(o>0)for(;p--;)q[p]||r[p]||(r[p]=W.call(i));r=n(r)}Y.apply(i,r),j&&!c&&r.length>0&&o+d.length>1&&a.uniqueSort(i)}return j&&(M=v,z=s),q};return e?c(g):g}var r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K="sizzle"+-new Date,L=window.document,M=0,N=0,O=b(),P=b(),Q=b(),R=function(a,b){return a===b&&(B=!0),0},S="undefined",T=1<<31,U={}.hasOwnProperty,V=[],W=V.pop,X=V.push,Y=V.push,Z=V.slice,$=V.indexOf||function(a){for(var b=0,c=this.length;b+~]|"+aa+")"+aa+"*"),ha=new RegExp("="+aa+"*([^\\]'\"]*?)"+aa+"*\\]","g"),ia=new RegExp(da),ja=new RegExp("^"+ba+"$"),ka={ID:new RegExp("^#("+ba+")"),CLASS:new RegExp("^\\.("+ba+")"),TAG:new RegExp("^("+ba+"|[*])"),ATTR:new RegExp("^"+ca),PSEUDO:new RegExp("^"+da),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+aa+"*(even|odd|(([+-]|)(\\d*)n|)"+aa+"*(?:([+-]|)"+aa+"*(\\d+)|))"+aa+"*\\)|)","i"),bool:new RegExp("^(?:"+_+")$","i"),needsContext:new RegExp("^"+aa+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+aa+"*((?:-\\d)?\\d*)"+aa+"*\\)|)(?=[^-]|$)","i")},la=/^(?:input|select|textarea|button)$/i,ma=/^h\d$/i,na=/^[^{]+\{\s*\[native \w/,oa=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,pa=/[+~]/,qa=/'|\\/g,ra=new RegExp("\\\\([\\da-f]{1,6}"+aa+"?|("+aa+")|.)","ig"),sa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{Y.apply(V=Z.call(L.childNodes),L.childNodes),V[L.childNodes.length].nodeType}catch(a){Y={apply:V.length?function(a,b){X.apply(a,Z.call(b))}:function(a,b){for(var c=a.length,d=0;a[c++]=b[d++];);a.length=c-1}}}s=a.support={},v=a.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},C=a.setDocument=function(a){function b(a){try{return a.top}catch(a){}return null}var c,e=a?a.ownerDocument||a:L,f=e.defaultView;return e!==D&&9===e.nodeType&&e.documentElement?(D=e,E=e.documentElement,F=!v(e),f&&f!==b(f)&&(f.addEventListener?f.addEventListener("unload",function(){C()},!1):f.attachEvent&&f.attachEvent("onunload",function(){C()})),s.attributes=!0,s.getElementsByTagName=!0,s.getElementsByClassName=na.test(e.getElementsByClassName),s.getById=!0,t.find.ID=function(a,b){if(typeof b.getElementById!==S&&F){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},t.filter.ID=function(a){var b=a.replace(ra,sa);return function(a){return a.getAttribute("id")===b}},t.find.TAG=s.getElementsByTagName?function(a,b){if(typeof b.getElementsByTagName!==S)return b.getElementsByTagName(a)}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){for(;c=f[e++];)1===c.nodeType&&d.push(c);return d}return f},t.find.CLASS=s.getElementsByClassName&&function(a,b){if(F)return b.getElementsByClassName(a)},H=[],G=[],s.disconnectedMatch=!0,G=G.length&&new RegExp(G.join("|")),H=H.length&&new RegExp(H.join("|")),c=na.test(E.compareDocumentPosition),J=c||na.test(E.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)for(;b=b.parentNode;)if(b===a)return!0;return!1},R=c?function(a,b){if(a===b)return B=!0,0;var c=!a.compareDocumentPosition-!b.compareDocumentPosition;return c?c:(c=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&c||!s.sortDetached&&b.compareDocumentPosition(a)===c?a===e||a.ownerDocument===L&&J(L,a)?-1:b===e||b.ownerDocument===L&&J(L,b)?1:A?$.call(A,a)-$.call(A,b):0:4&c?-1:1)}:function(a,b){if(a===b)return B=!0,0;var c,f=0,g=a.parentNode,h=b.parentNode,i=[a],j=[b];if(!g||!h)return a===e?-1:b===e?1:g?-1:h?1:A?$.call(A,a)-$.call(A,b):0;if(g===h)return d(a,b);for(c=a;c=c.parentNode;)i.unshift(c);for(c=b;c=c.parentNode;)j.unshift(c);for(;i[f]===j[f];)f++;return f?d(i[f],j[f]):i[f]===L?-1:j[f]===L?1:0},e):D},a.matches=function(b,c){return a(b,null,null,c)},a.matchesSelector=function(b,c){if((b.ownerDocument||b)!==D&&C(b),c=c.replace(ha,"='$1']"),s.matchesSelector&&F&&(!H||!H.test(c))&&(!G||!G.test(c)))try{var d=I.call(b,c);if(d||s.disconnectedMatch||b.document&&11!==b.document.nodeType)return d}catch(a){}return a(c,D,null,[b]).length>0},a.contains=function(a,b){return(a.ownerDocument||a)!==D&&C(a),J(a,b)},a.attr=function(a,b){(a.ownerDocument||a)!==D&&C(a);var c=t.attrHandle[b.toLowerCase()],d=c&&U.call(t.attrHandle,b.toLowerCase())?c(a,b,!F):void 0;return void 0!==d?d:s.attributes||!F?a.getAttribute(b):(d=a.getAttributeNode(b))&&d.specified?d.value:null},a.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},a.uniqueSort=function(a){var b,c=[],d=0,e=0;if(B=!s.detectDuplicates,A=!s.sortStable&&a.slice(0),a.sort(R),B){for(;b=a[e++];)b===a[e]&&(d=c.push(e));for(;d--;)a.splice(c[d],1)}return A=null,a},u=a.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(1===e||9===e||11===e){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=u(a)}else if(3===e||4===e)return a.nodeValue}else for(;b=a[d++];)c+=u(b);return c},t=a.selectors={cacheLength:50,createPseudo:c,match:ka,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ra,sa),a[3]=(a[3]||a[4]||a[5]||"").replace(ra,sa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(b){return b[1]=b[1].toLowerCase(),"nth"===b[1].slice(0,3)?(b[3]||a.error(b[0]),b[4]=+(b[4]?b[5]+(b[6]||1):2*("even"===b[3]||"odd"===b[3])),b[5]=+(b[7]+b[8]||"odd"===b[3])):b[3]&&a.error(b[0]),b},PSEUDO:function(a){var b,c=!a[6]&&a[2];return ka.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&ia.test(c)&&(b=w(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ra,sa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=O[a+" "];return b||(b=new RegExp("(^|"+aa+")"+a+"("+aa+"|$)"))&&O(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==S&&a.getAttribute("class")||"")})},ATTR:function(b,c,d){return function(e){var f=a.attr(e,b);return null==f?"!="===c:!c||(f+="","="===c?f===d:"!="===c?f!==d:"^="===c?d&&0===f.indexOf(d):"*="===c?d&&f.indexOf(d)>-1:"$="===c?d&&f.slice(-d.length)===d:"~="===c?(" "+f+" ").indexOf(d)>-1:"|="===c&&(f===d||f.slice(0,d.length+1)===d+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){for(;p;){for(l=b;l=l[p];)if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){for(k=q[K]||(q[K]={}),j=k[a]||[],n=j[0]===M&&j[1],m=j[0]===M&&j[2],l=n&&q.childNodes[n];l=++n&&l&&l[p]||(m=n=0)||o.pop();)if(1===l.nodeType&&++m&&l===b){k[a]=[M,n,m];break}}else if(s&&(j=(b[K]||(b[K]={}))[a])&&j[0]===M)m=j[1];else for(;(l=++n&&l&&l[p]||(m=n=0)||o.pop())&&((h?l.nodeName.toLowerCase()!==r:1!==l.nodeType)||!++m||(s&&((l[K]||(l[K]={}))[a]=[M,m]),l!==b)););return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(b,d){var e,f=t.pseudos[b]||t.setFilters[b.toLowerCase()]||a.error("unsupported pseudo: "+b);return f[K]?f(d):f.length>1?(e=[b,b,"",d],t.setFilters.hasOwnProperty(b.toLowerCase())?c(function(a,b){for(var c,e=f(a,d),g=e.length;g--;)c=$.call(a,e[g]),a[c]=!(b[c]=e[g])}):function(a){return f(a,0,e)}):f}},pseudos:{not:c(function(a){var b=[],d=[],e=x(a.replace(ea,"$1"));return e[K]?c(function(a,b,c,d){for(var f,g=e(a,null,d,[]),h=a.length;h--;)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,c,f){return b[0]=a,e(b,null,f,d),!d.pop()}}),has:c(function(b){return function(c){return a(b,c).length>0}}),contains:c(function(a){return a=a.replace(ra,sa),function(b){return(b.textContent||b.innerText||u(b)).indexOf(a)>-1}}),lang:c(function(b){return ja.test(b||"")||a.error("unsupported lang: "+b),b=b.replace(ra,sa).toLowerCase(),function(a){var c;do if(c=F?a.lang:a.getAttribute("xml:lang")||a.getAttribute("lang"))return c=c.toLowerCase(),c===b||0===c.indexOf(b+"-");while((a=a.parentNode)&&1===a.nodeType);return!1}}),target:function(a){var b=window.location&&window.location.hash;return b&&b.slice(1)===a.id},root:function(a){return a===E},focus:function(a){return a===D.activeElement&&(!D.hasFocus||D.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!t.pseudos.empty(a)},header:function(a){return ma.test(a.nodeName)},input:function(a){return la.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:g(function(){return[0]}),last:g(function(a,b){return[b-1]}),eq:g(function(a,b,c){return[c<0?c+b:c]}),even:g(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:g(function(a,b,c){for(var d=c<0?c+b:c;++d2&&"ID"===(g=f[0]).type&&s.getById&&9===b.nodeType&&F&&t.relative[f[1].type]){if(b=(t.find.ID(g.matches[0].replace(ra,sa),b)||[])[0],!b)return c;l&&(b=b.parentNode),a=a.slice(f.shift().value.length)}for(e=ka.needsContext.test(a)?0:f.length;e--&&(g=f[e],!t.relative[i=g.type]);)if((k=t.find[i])&&(d=k(g.matches[0].replace(ra,sa),pa.test(f[0].type)&&h(b.parentNode)||b))){if(f.splice(e,1),a=d.length&&j(f),!a)return Y.apply(c,d),c;break}}return(l||x(a,m))(d,b,!F,c,pa.test(a)&&h(b.parentNode)||b),c},s.sortStable=K.split("").sort(R).join("")===K,s.detectDuplicates=!!B,C(),s.sortDetached=!0,a}),g("1r",[],function(){function a(a){var b,c,d=a;if(!j(a))for(d=[],b=0,c=a.length;b=0;e--)j(a,b[e],c,d);else for(e=0;e)[^>]*$|#([\w\-]*)$)/,B=b.Event,C=e.makeMap("children,contents,next,prev"),D=e.makeMap("fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom"," "),E=e.makeMap("checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected"," "),F={"for":"htmlFor","class":"className",readonly:"readOnly"},G={"float":"cssFloat"},H={},I={},J=/^\s*|\s*$/g;return m.fn=m.prototype={constructor:m,selector:"",context:null,length:0,init:function(b,c){var d,e,f=this;if(!b)return f;if(b.nodeType)return f.context=f[0]=b,f.length=1,f;if(c&&c.nodeType)f.context=c;else{if(c)return m(b).attr(c);f.context=c=a}if(g(b)){if(f.selector=b,d="<"===b.charAt(0)&&">"===b.charAt(b.length-1)&&b.length>=3?[null,b,null]:A.exec(b),!d)return m(c).find(b);if(d[1])for(e=i(b,r(c)).firstChild;e;)y.call(f,e),e=e.nextSibling;else{if(e=r(c).getElementById(d[2]),!e)return f;if(e.id!==d[2])return f.find(b);f.length=1,f[0]=e}}else this.add(b,!1);return f},toArray:function(){return e.toArray(this)},add:function(a,b){var c,d,e=this;if(g(a))return e.add(m(a));if(b!==!1)for(c=m.unique(e.toArray().concat(m.makeArray(a))),e.length=c.length,d=0;d1&&(C[a]||(e=m.unique(e)),0===a.indexOf("parents")&&(e=e.reverse())),e=m(e),c?e.filter(c):e}}),p({parentsUntil:function(a,b){return s(a,"parentNode",b)},nextUntil:function(a,b){return t(a,"nextSibling",1,b).slice(1)},prevUntil:function(a,b){return t(a,"previousSibling",1,b).slice(1)}},function(a,b){m.fn[a]=function(c,d){var e=this,f=[];return e.each(function(){var a=b.call(f,this,c,f);a&&(m.isArray(a)?f.push.apply(f,a):f.push(a))}),this.length>1&&(f=m.unique(f),0!==a.indexOf("parents")&&"prevUntil"!==a||(f=f.reverse())),f=m(f),d?f.filter(d):f}}),m.fn.is=function(a){return!!a&&this.filter(a).length>0},m.fn.init.prototype=m.fn,m.overrideDefaults=function(a){function b(d,e){return c=c||a(),0===arguments.length&&(d=c.element),e||(e=c.context),new b.fn.init(d,e)}var c;return m.extend(b,this),b},d.ie&&d.ie<8&&(v(H,"get",{maxlength:function(a){var b=a.maxLength;return 2147483647===b?w:b},size:function(a){var b=a.size;return 20===b?w:b},"class":function(a){return a.className},style:function(a){var b=a.style.cssText;return 0===b.length?w:b}}),v(H,"set",{"class":function(a,b){a.className=b},style:function(a,b){a.style.cssText=b}})),d.ie&&d.ie<9&&(G["float"]="styleFloat",v(I,"set",{opacity:function(a,b){var c=a.style;null===b||""===b?c.removeAttribute("filter"):(c.zoom=1,c.filter="alpha(opacity="+100*b+")")}})),m.attrHooks=H,m.cssHooks=I,m}),g("4b",["1i","23","1q"],function(a,b,c){var d=function(e){var f=b.none(),g=[],h=function(a){return d(function(b){i(function(c){b(a(c))})})},i=function(a){k()?m(a):g.push(a)},j=function(a){f=b.some(a),l(g),g=[]},k=function(){return f.isSome()},l=function(b){a.each(b,m)},m=function(a){f.each(function(b){c(function(){a(b)},0)})};return e(j),{get:i,map:h,isReady:k}},e=function(a){return d(function(b){b(a)})};return{nu:d,pure:e}}),g("4c",["4","1q"],function(a,b){var c=function(c){return function(){var d=a.prototype.slice.call(arguments),e=this;b(function(){c.apply(e,d)},0)}};return{bounce:c}}),g("31",["4b","4c"],function(a,b){var c=function(d){var e=function(a){d(b.bounce(a))},f=function(a){return c(function(b){e(function(c){var d=a(c);b(d)})})},g=function(a){return c(function(b){e(function(c){a(c).get(b)})})},h=function(a){return c(function(b){e(function(c){a.get(b)})})},i=function(){return a.nu(e)};return{map:f,bind:g,anonBind:h,toLazy:i,get:e}},d=function(a){return c(function(b){b(a)})};return{nu:c,pure:d}}),g("4d",["1i"],function(a){var b=function(b,c){return c(function(c){var d=[],e=0,f=function(a){return function(f){d[a]=f,e++,e>=b.length&&c(d)}};0===b.length?c([]):a.each(b,function(a,b){a.get(f(b))})})};return{par:b}}),g("32",["1i","31","4d"],function(a,b,c){var d=function(a){return c.par(a,b.nu)},e=function(b,c){var e=a.map(b,c);return d(e)},f=function(a,b){return function(c){return b(c).bind(a)}};return{par:d,mapM:e,compose:f}}),g("33",["1","23"],function(a,b){var c=function(d){var e=function(a){return d===a},f=function(a){return c(d)},g=function(a){return c(d)},h=function(a){return c(a(d))},i=function(a){a(d)},j=function(a){return a(d)},k=function(a,b){return b(d)},l=function(a){return a(d)},m=function(a){return a(d)},n=function(){return b.some(d)};return{is:e,isValue:a.constant(!0),isError:a.constant(!1),getOr:a.constant(d),getOrThunk:a.constant(d),getOrDie:a.constant(d),or:f,orThunk:g,fold:k,map:h,each:i,bind:j,exists:l,forall:m,toOption:n}},d=function(c){var e=function(a){return a()},f=function(){return a.die(c)()},g=function(a){return a},h=function(a){return a()},i=function(a){return d(c)},j=function(a){return d(c)},k=function(a,b){return a(c)};return{is:a.constant(!1),isValue:a.constant(!1),isError:a.constant(!0),getOr:a.identity,getOrThunk:e,getOrDie:f,or:g,orThunk:h,fold:k,map:i,each:a.noop,bind:j,exists:a.constant(!1),forall:a.constant(!0),toOption:b.none}};return{value:c,error:d}}),g("1s",["1i","1","31","32","33","1m","15","1e"],function(a,b,c,d,e,f,g,h){"use strict";return function(i,j){function k(a){i.getElementsByTagName("head")[0].appendChild(a)}function l(a,b,c){function d(){for(var a=u.passed,b=a.length;b--;)a[b]();u.status=2,u.passed=[],u.failed=[]}function e(){for(var a=u.failed,b=a.length;b--;)a[b]();u.status=3,u.passed=[],u.failed=[]}function j(){var a=f.userAgent.match(/WebKit\/(\d*)/);return!!(a&&a[1]<536)}function l(a,b){a()||((new Date).getTime()-t0)return s=i.createElement("style"),s.textContent='@import "'+a+'"',q(),void k(s);p()}k(r),r.href=a}}var m,n=0,o={};j=j||{},m=j.maxLoadTime||5e3;var p=function(a){return c.nu(function(c){l(a,b.compose(c,b.constant(e.value(a))),b.compose(c,b.constant(e.error(a))))})},q=function(a){return a.fold(b.identity,b.identity)},r=function(b,c,e){d.par(a.map(b,p)).get(function(b){var d=a.partition(b,function(a){return a.isValue()});d.fail.length>0?e(d.fail.map(q)):c(d.pass.map(q))})};return{load:l,loadAll:r}}}),g("k",[],function(){return function(a,b){function c(a,c,d,e){var f,g;if(a){if(!e&&a[c])return a[c];if(a!=b){if(f=a[d])return f;for(g=a.parentNode;g&&g!=b;g=g.parentNode)if(f=g[d])return f}}}function d(a,c,d,e){var f,g,h;if(a){if(f=a[d],b&&f===b)return;if(f){if(!e)for(h=f[c];h;h=h[c])if(!h[c])return h;return f}if(g=a.parentNode,g&&g!==b)return g}}var e=a;this.current=function(){return e},this.next=function(a){return e=c(e,"firstChild","nextSibling",a)},this.prev=function(a){return e=c(e,"lastChild","previousSibling",a)},this.prev2=function(a){return e=d(e,"lastChild","previousSibling",a)}}}),g("34",[],function(){return"undefined"==typeof console&&(console={log:function(){}}),console}),g("1t",["1","5","34","1j"],function(a,b,c,d){var e=function(a,b){var e=b||d,f=e.createElement("div");if(f.innerHTML=a,!f.hasChildNodes()||f.childNodes.length>1)throw c.error("HTML does not have a single root node",a),"HTML must have a single root node";return h(f.childNodes[0])},f=function(a,b){var c=b||d,e=c.createElement(a);return h(e)},g=function(a,b){var c=b||d,e=c.createTextNode(a);return h(e)},h=function(c){if(null===c||void 0===c)throw new b("Node cannot be null or undefined");return{dom:a.constant(c)}};return{fromHtml:e,fromTag:f,fromText:g,fromDom:h}}),g("t",["1t","1e"],function(a,b){function c(b){var c;return c=a.fromTag("div").dom(),c.innerHTML=b,c.textContent||c.innerText||b}function d(a,b){var c,d,e,g={};if(a){for(a=a.split(","),b=b||10,c=0;c\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,j=/[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,k=/[<>&\"\']/g,l=/&#([a-z0-9]+);?|&([a-z0-9]+);/gi,m={128:"\u20ac",130:"\u201a",131:"\u0192",132:"\u201e",133:"\u2026",134:"\u2020",135:"\u2021",136:"\u02c6",137:"\u2030",138:"\u0160",139:"\u2039",140:"\u0152",142:"\u017d",145:"\u2018",146:"\u2019",147:"\u201c",148:"\u201d",149:"\u2022",150:"\u2013",151:"\u2014",152:"\u02dc",153:"\u2122",154:"\u0161",155:"\u203a",156:"\u0153",158:"\u017e",159:"\u0178"};f={'"':""","'":"'","<":"<",">":">","&":"&","`":"`"},g={"<":"<",">":">","&":"&",""":'"',"'":"'"},e=d("50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,t9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro",32);var n={encodeRaw:function(a,b){return a.replace(b?i:j,function(a){return f[a]||a})},encodeAllRaw:function(a){return(""+a).replace(k,function(a){return f[a]||a})},encodeNumeric:function(a,b){return a.replace(b?i:j,function(a){return a.length>1?"&#"+(1024*(a.charCodeAt(0)-55296)+(a.charCodeAt(1)-56320)+65536)+";":f[a]||"&#"+a.charCodeAt(0)+";"})},encodeNamed:function(a,b,c){return c=c||e,a.replace(b?i:j,function(a){return f[a]||c[a]||a})},getEncodeFunc:function(a,b){function c(a,c){return a.replace(c?i:j,function(a){return void 0!==f[a]?f[a]:void 0!==b[a]?b[a]:a.length>1?"&#"+(1024*(a.charCodeAt(0)-55296)+(a.charCodeAt(1)-56320)+65536)+";":"&#"+a.charCodeAt(0)+";"})}function g(a,c){return n.encodeNamed(a,c,b)}return b=d(b)||e,a=h(a.replace(/\+/g,",")),a.named&&a.numeric?c:a.named?b?g:n.encodeNamed:a.numeric?n.encodeNumeric:n.encodeRaw},decode:function(a){return a.replace(l,function(a,b){return b?(b="x"===b.charAt(0).toLowerCase()?parseInt(b.substr(1),16):parseInt(b,10),b>65535?(b-=65536,String.fromCharCode(55296+(b>>10),56320+(1023&b))):m[b]||String.fromCharCode(b)):g[a]||e[a]||c(a)})}};return n}),g("w",["1e"],function(a){function b(b,c){return b=a.trim(b),b?b.split(c||" "):[]}function c(a){function c(a,c,d){function e(a,b){var c,d,e={};for(c=0,d=a.length;c