init
This commit is contained in:
1 parent
fac05a7b6c
commit
476ade10e7
1085 files changed
+259628
No files matched your search
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* AddOnManager.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class handles the loading of themes/plugins or other add-ons and their language packs.
|
||||
*
|
||||
* @class tinymce.AddOnManager
|
||||
*/
|
||||
define("tinymce/AddOnManager", [
|
||||
"tinymce/dom/ScriptLoader",
|
||||
"tinymce/util/Tools"
|
||||
], function(ScriptLoader, Tools) {
|
||||
var each = Tools.each;
|
||||
|
||||
function AddOnManager() {
|
||||
var self = this;
|
||||
|
||||
self.items = [];
|
||||
self.urls = {};
|
||||
self.lookup = {};
|
||||
}
|
||||
|
||||
AddOnManager.prototype = {
|
||||
/**
|
||||
* Returns the specified add on by the short name.
|
||||
*
|
||||
* @method get
|
||||
* @param {String} name Add-on to look for.
|
||||
* @return {tinymce.Theme/tinymce.Plugin} Theme or plugin add-on instance or undefined.
|
||||
*/
|
||||
get: function(name) {
|
||||
if (this.lookup[name]) {
|
||||
return this.lookup[name].instance;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
dependencies: function(name) {
|
||||
var result;
|
||||
|
||||
if (this.lookup[name]) {
|
||||
result = this.lookup[name].dependencies;
|
||||
}
|
||||
|
||||
return result || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads a language pack for the specified add-on.
|
||||
*
|
||||
* @method requireLangPack
|
||||
* @param {String} name Short name of the add-on.
|
||||
*/
|
||||
requireLangPack: function(name) {
|
||||
if (AddOnManager.language && AddOnManager.languageLoad !== false) {
|
||||
ScriptLoader.ScriptLoader.add(this.urls[name] + '/langs/' + AddOnManager.language + '.js');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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};
|
||||
|
||||
return addOn;
|
||||
},
|
||||
|
||||
createUrl: function(baseUrl, dep) {
|
||||
if (typeof dep === "object") {
|
||||
return dep;
|
||||
} else {
|
||||
return {prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
||||
each(scripts, function(script) {
|
||||
ScriptLoader.ScriptLoader.add(pluginUrl + "/" + script);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads an add-on from a specific url.
|
||||
*
|
||||
* @method load
|
||||
* @param {String} n Short name of the add-on that gets loaded.
|
||||
* @param {String} u URL to the add-on that will get loaded.
|
||||
* @param {function} cb Optional callback to execute ones the add-on is loaded.
|
||||
* @param {Object} s Optional scope to execute the callback in.
|
||||
* @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(n, u, cb, s) {
|
||||
var t = this, url = u;
|
||||
|
||||
function loadDependencies() {
|
||||
var dependencies = t.dependencies(n);
|
||||
|
||||
each(dependencies, function(dep) {
|
||||
var newUrl = t.createUrl(u, dep);
|
||||
|
||||
t.load(newUrl.resource, newUrl, undefined, undefined);
|
||||
});
|
||||
|
||||
if (cb) {
|
||||
if (s) {
|
||||
cb.call(s);
|
||||
} else {
|
||||
cb.call(ScriptLoader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (t.urls[n]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof u === "object") {
|
||||
url = u.prefix + u.resource + u.suffix;
|
||||
}
|
||||
|
||||
if (url.indexOf('/') !== 0 && url.indexOf('://') == -1) {
|
||||
url = AddOnManager.baseURL + '/' + url;
|
||||
}
|
||||
|
||||
t.urls[n] = url.substring(0, url.lastIndexOf('/'));
|
||||
|
||||
if (t.lookup[n]) {
|
||||
loadDependencies();
|
||||
} else {
|
||||
ScriptLoader.ScriptLoader.add(url, loadDependencies, s);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddOnManager.PluginManager = new AddOnManager();
|
||||
AddOnManager.ThemeManager = new AddOnManager();
|
||||
|
||||
return AddOnManager;
|
||||
});
|
||||
|
||||
/**
|
||||
* TinyMCE theme class.
|
||||
*
|
||||
* @class tinymce.Theme
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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'
|
||||
* }]
|
||||
* });
|
||||
* }
|
||||
* });
|
||||
* });
|
||||
*/
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Compat.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* TinyMCE core class.
|
||||
*
|
||||
* @static
|
||||
* @class tinymce
|
||||
* @borrow-members tinymce.EditorManager
|
||||
* @borrow-members tinymce.util.Tools
|
||||
*/
|
||||
define("tinymce/Compat", [
|
||||
"tinymce/dom/DOMUtils",
|
||||
"tinymce/dom/EventUtils",
|
||||
"tinymce/dom/ScriptLoader",
|
||||
"tinymce/AddOnManager",
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/Env"
|
||||
], function(DOMUtils, EventUtils, ScriptLoader, AddOnManager, Tools, Env) {
|
||||
var tinymce = window.tinymce;
|
||||
|
||||
/**
|
||||
* @property {tinymce.dom.DOMUtils} DOM Global DOM instance.
|
||||
* @property {tinymce.dom.ScriptLoader} ScriptLoader Global ScriptLoader instance.
|
||||
* @property {tinymce.AddOnManager} PluginManager Global PluginManager instance.
|
||||
* @property {tinymce.AddOnManager} ThemeManager Global ThemeManager instance.
|
||||
*/
|
||||
tinymce.DOM = DOMUtils.DOM;
|
||||
tinymce.ScriptLoader = ScriptLoader.ScriptLoader;
|
||||
tinymce.PluginManager = AddOnManager.PluginManager;
|
||||
tinymce.ThemeManager = AddOnManager.ThemeManager;
|
||||
|
||||
tinymce.dom = tinymce.dom || {};
|
||||
tinymce.dom.Event = EventUtils.Event;
|
||||
|
||||
Tools.each(Tools, function(func, key) {
|
||||
tinymce[key] = func;
|
||||
});
|
||||
|
||||
Tools.each('isOpera isWebKit isIE isGecko isMac'.split(' '), function(name) {
|
||||
tinymce[name] = Env[name.substr(2).toLowerCase()];
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
// Describe the different namespaces
|
||||
|
||||
/**
|
||||
* Root level namespace this contains classes directly releated to the TinyMCE editor.
|
||||
*
|
||||
* @namespace tinymce
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains classes for handling the browsers DOM.
|
||||
*
|
||||
* @namespace tinymce.dom
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains html parser and serializer logic.
|
||||
*
|
||||
* @namespace tinymce.html
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains the different UI types such as buttons, listboxes etc.
|
||||
*
|
||||
* @namespace tinymce.ui
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains various utility classes such as json parser, cookies etc.
|
||||
*
|
||||
* @namespace tinymce.util
|
||||
*/
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,700 @@
|
||||
/**
|
||||
* EditorCommands.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class enables you to add custom editor commands and it contains
|
||||
* overrides for native browser commands to address various bugs and issues.
|
||||
*
|
||||
* @class tinymce.EditorCommands
|
||||
*/
|
||||
define("tinymce/EditorCommands", [
|
||||
"tinymce/html/Serializer",
|
||||
"tinymce/Env",
|
||||
"tinymce/util/Tools"
|
||||
], function(Serializer, Env, Tools) {
|
||||
// Added for compression purposes
|
||||
var each = Tools.each, extend = Tools.extend;
|
||||
var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode;
|
||||
var isGecko = Env.gecko, isIE = Env.ie;
|
||||
var TRUE = true, FALSE = false;
|
||||
|
||||
return function(editor) {
|
||||
var dom = editor.dom,
|
||||
selection = editor.selection,
|
||||
commands = {state: {}, exec: {}, value: {}},
|
||||
settings = editor.settings,
|
||||
formatter = editor.formatter,
|
||||
bookmark;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {Boolean} true/false if the command was found or not.
|
||||
*/
|
||||
function execCommand(command, ui, value) {
|
||||
var func;
|
||||
|
||||
command = command.toLowerCase();
|
||||
if ((func = commands.exec[command])) {
|
||||
func(command, ui, value);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the current state for a command for example if the current selection is "bold".
|
||||
*
|
||||
* @method queryCommandState
|
||||
* @param {String} command Command to check the state of.
|
||||
* @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
|
||||
*/
|
||||
function queryCommandState(command) {
|
||||
var func;
|
||||
|
||||
command = command.toLowerCase();
|
||||
if ((func = commands.state[command])) {
|
||||
return func(command);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
command = command.toLowerCase();
|
||||
if ((func = commands.value[command])) {
|
||||
return func(command);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds commands to the command collection.
|
||||
*
|
||||
* @method addCommands
|
||||
* @param {Object} command_list 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(command_list, type) {
|
||||
type = type || 'exec';
|
||||
|
||||
each(command_list, function(callback, command) {
|
||||
each(command.toLowerCase().split(','), function(command) {
|
||||
commands[type][command] = callback;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Expose public methods
|
||||
extend(this, {
|
||||
execCommand: execCommand,
|
||||
queryCommandState: queryCommandState,
|
||||
queryCommandValue: queryCommandValue,
|
||||
addCommands: addCommands
|
||||
});
|
||||
|
||||
// Private methods
|
||||
|
||||
function execNativeCommand(command, ui, value) {
|
||||
if (ui === undefined) {
|
||||
ui = FALSE;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
return editor.getDoc().execCommand(command, ui, value);
|
||||
}
|
||||
|
||||
function isFormatMatch(name) {
|
||||
return formatter.match(name);
|
||||
}
|
||||
|
||||
function toggleFormat(name, value) {
|
||||
formatter.toggle(name, value ? {value: value} : undefined);
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
function storeSelection(type) {
|
||||
bookmark = selection.getBookmark(type);
|
||||
}
|
||||
|
||||
function restoreSelection() {
|
||||
selection.moveToBookmark(bookmark);
|
||||
}
|
||||
|
||||
// Add execCommand overrides
|
||||
addCommands({
|
||||
// Ignore these, added for compatibility
|
||||
'mceResetDesignMode,mceBeginUndoLevel': function() {},
|
||||
|
||||
// Add undo manager logic
|
||||
'mceEndUndoLevel,mceAddUndoLevel': function() {
|
||||
editor.undoManager.add();
|
||||
},
|
||||
|
||||
'Cut,Copy,Paste': function(command) {
|
||||
var doc = editor.getDoc(), failed;
|
||||
|
||||
// Try executing the native command
|
||||
try {
|
||||
execNativeCommand(command);
|
||||
} catch (ex) {
|
||||
// Command failed
|
||||
failed = TRUE;
|
||||
}
|
||||
|
||||
// Present alert message about clipboard access not being available
|
||||
if (failed || !doc.queryCommandSupported(command)) {
|
||||
editor.windowManager.alert(
|
||||
"Your browser doesn't support direct access to the clipboard. " +
|
||||
"Please use the Ctrl+X/C/V keyboard shortcuts instead."
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Override unlink command
|
||||
unlink: function(command) {
|
||||
if (selection.isCollapsed()) {
|
||||
selection.select(selection.getNode());
|
||||
}
|
||||
|
||||
execNativeCommand(command);
|
||||
selection.collapse(FALSE);
|
||||
},
|
||||
|
||||
// Override justify commands to use the text formatter engine
|
||||
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) {
|
||||
var align = command.substring(7);
|
||||
|
||||
if (align == 'full') {
|
||||
align = 'justify';
|
||||
}
|
||||
|
||||
// Remove all other alignments first
|
||||
each('left,center,right,justify'.split(','), function(name) {
|
||||
if (align != name) {
|
||||
formatter.remove('align' + name);
|
||||
}
|
||||
});
|
||||
|
||||
toggleFormat('align' + align);
|
||||
execCommand('mceRepaint');
|
||||
},
|
||||
|
||||
// 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) {
|
||||
var parser, serializer, parentNode, rootNode, fragment, args;
|
||||
var marker, rng, node, node2, bookmarkHtml;
|
||||
|
||||
function trimOrPaddLeftRight(html) {
|
||||
var rng, container, offset;
|
||||
|
||||
rng = selection.getRng(true);
|
||||
container = rng.startContainer;
|
||||
offset = rng.startOffset;
|
||||
|
||||
function hasSiblingText(siblingName) {
|
||||
return container[siblingName] && container[siblingName].nodeType == 3;
|
||||
}
|
||||
|
||||
if (container.nodeType == 3) {
|
||||
if (offset > 0) {
|
||||
html = html.replace(/^ /, ' ');
|
||||
} else if (!hasSiblingText('previousSibling')) {
|
||||
html = html.replace(/^ /, ' ');
|
||||
}
|
||||
|
||||
if (offset < container.length) {
|
||||
html = html.replace(/ (<br>|)$/, ' ');
|
||||
} else if (!hasSiblingText('nextSibling')) {
|
||||
html = html.replace(/( | )(<br>|)$/, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Check for whitespace before/after value
|
||||
if (/^ | $/.test(value)) {
|
||||
value = trimOrPaddLeftRight(value);
|
||||
}
|
||||
|
||||
// Setup parser and serializer
|
||||
parser = editor.parser;
|
||||
serializer = new Serializer({}, editor.schema);
|
||||
bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark"></span>';
|
||||
|
||||
// Run beforeSetContent handlers on the HTML to be inserted
|
||||
args = {content: value, format: 'html', selection: true};
|
||||
editor.fire('BeforeSetContent', args);
|
||||
value = args.content;
|
||||
|
||||
// Add caret at end of contents if it's missing
|
||||
if (value.indexOf('{$caret}') == -1) {
|
||||
value += '{$caret}';
|
||||
}
|
||||
|
||||
// Replace the caret marker with a span bookmark element
|
||||
value = value.replace(/\{\$caret\}/, bookmarkHtml);
|
||||
|
||||
// If selection is at <body>|<p></p> then move it into <body><p>|</p>
|
||||
var body = editor.getBody();
|
||||
if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) {
|
||||
body.firstChild.appendChild(dom.doc.createTextNode('\u00a0'));
|
||||
selection.select(body.firstChild, true);
|
||||
dom.remove(body.firstChild.lastChild);
|
||||
}
|
||||
|
||||
// Insert node maker where we will insert the new HTML and get it's parent
|
||||
if (!selection.isCollapsed()) {
|
||||
editor.getDoc().execCommand('Delete', false, null);
|
||||
}
|
||||
|
||||
parentNode = selection.getNode();
|
||||
|
||||
// Parse the fragment within the context of the parent node
|
||||
var parserArgs = {context: parentNode.nodeName.toLowerCase()};
|
||||
fragment = parser.parse(value, parserArgs);
|
||||
|
||||
// Move the caret to a more suitable location
|
||||
node = fragment.lastChild;
|
||||
if (node.attr('id') == 'mce_marker') {
|
||||
marker = node;
|
||||
|
||||
for (node = node.prev; node; node = node.walk(true)) {
|
||||
if (node.type == 3 || !dom.isBlock(node.name)) {
|
||||
node.parent.insert(marker, node, node.name === 'br');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If parser says valid we can insert the contents into that parent
|
||||
if (!parserArgs.invalid) {
|
||||
value = serializer.serialize(fragment);
|
||||
|
||||
// Check if parent is empty or only has one BR element then set the innerHTML of that parent
|
||||
node = parentNode.firstChild;
|
||||
node2 = parentNode.lastChild;
|
||||
if (!node || (node === node2 && node.nodeName === 'BR')) {
|
||||
dom.setHTML(parentNode, value);
|
||||
} else {
|
||||
selection.setContent(value);
|
||||
}
|
||||
} 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(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/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);
|
||||
}
|
||||
}
|
||||
|
||||
marker = dom.get('mce_marker');
|
||||
selection.scrollIntoView(marker);
|
||||
|
||||
// Move selection before marker and remove it
|
||||
rng = dom.createRng();
|
||||
|
||||
// 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);
|
||||
|
||||
// TODO: Why can't we normalize on IE
|
||||
if (!isIE) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Remove the marker node and set the new range
|
||||
dom.remove(marker);
|
||||
selection.setRng(rng);
|
||||
|
||||
// Dispatch after event and add any visual elements needed
|
||||
editor.fire('SetContent', args);
|
||||
editor.addVisual();
|
||||
},
|
||||
|
||||
// life 修改
|
||||
mceInsertRawHTML: function(command, ui, value) {
|
||||
selection.setContent('tiny_mce_marker');
|
||||
|
||||
// start----------
|
||||
// 为了定位到粘贴后
|
||||
var bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark"></span>';
|
||||
if (value.indexOf('{$caret}') == -1) {
|
||||
value += '{$caret}';
|
||||
}
|
||||
// Replace the caret marker with a span bookmark element
|
||||
value = value.replace(/\{\$caret\}/, bookmarkHtml);
|
||||
|
||||
editor.setContent(
|
||||
editor.getContent().replace(/tiny_mce_marker/g, function() {
|
||||
return value;
|
||||
})
|
||||
);
|
||||
var marker = dom.get('mce_marker');
|
||||
var rng = dom.createRng();
|
||||
rng.setStartBefore(marker);
|
||||
rng.setEndBefore(marker);
|
||||
|
||||
// Remove the marker node and set the new range
|
||||
dom.remove(marker);
|
||||
selection.setRng(rng);
|
||||
//--------end
|
||||
},
|
||||
|
||||
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) {
|
||||
var indentStyleName;
|
||||
|
||||
if (element.nodeName != "LI") {
|
||||
indentStyleName = dom.getStyle(element, 'direction', true) == 'rtl' ? 'paddingRight' : 'paddingLeft';
|
||||
|
||||
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() {
|
||||
if (isGecko) {
|
||||
try {
|
||||
storeSelection(TRUE);
|
||||
|
||||
if (selection.getSel()) {
|
||||
selection.getSel().selectAllChildren(editor.getBody());
|
||||
}
|
||||
|
||||
selection.collapse(TRUE);
|
||||
restoreSelection();
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
InsertHorizontalRule: function() {
|
||||
editor.execCommand('mceInsertContent', false, '<hr />');
|
||||
},
|
||||
|
||||
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 = dom.createRng();
|
||||
|
||||
// Old IE does a better job with selectall than new versions
|
||||
if (selection.getRng().setStart) {
|
||||
rng.setStart(root, 0);
|
||||
rng.setEnd(root, root.childNodes.length);
|
||||
|
||||
selection.setRng(rng);
|
||||
} else {
|
||||
execNativeCommand('SelectAll');
|
||||
}
|
||||
},
|
||||
|
||||
mceNewDocument: function() {
|
||||
editor.setContent('');
|
||||
}
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,570 @@
|
||||
/**
|
||||
* EditorManager.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class used as a factory for manager for tinymce.Editor instances.
|
||||
*
|
||||
* @example
|
||||
* tinymce.EditorManager.init({});
|
||||
*
|
||||
* @class tinymce.EditorManager
|
||||
* @mixes tinymce.util.Observable
|
||||
* @static
|
||||
*/
|
||||
define("tinymce/EditorManager", [
|
||||
"tinymce/Editor",
|
||||
"tinymce/dom/DOMUtils",
|
||||
"tinymce/util/URI",
|
||||
"tinymce/Env",
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/util/Observable",
|
||||
"tinymce/util/I18n",
|
||||
"tinymce/FocusManager"
|
||||
], function(Editor, DOMUtils, URI, Env, Tools, Observable, I18n, FocusManager) {
|
||||
var DOM = DOMUtils.DOM;
|
||||
var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
|
||||
var instanceCounter = 0, beforeUnloadDelegate;
|
||||
|
||||
var EditorManager = {
|
||||
/**
|
||||
* Major version of TinyMCE build.
|
||||
*
|
||||
* @property majorVersion
|
||||
* @type String
|
||||
*/
|
||||
majorVersion : '@@majorVersion@@',
|
||||
|
||||
/**
|
||||
* Minor version of TinyMCE build.
|
||||
*
|
||||
* @property minorVersion
|
||||
* @type String
|
||||
*/
|
||||
minorVersion : '@@minorVersion@@',
|
||||
|
||||
/**
|
||||
* Release date of TinyMCE build.
|
||||
*
|
||||
* @property releaseDate
|
||||
* @type String
|
||||
*/
|
||||
releaseDate: '@@releaseDate@@',
|
||||
|
||||
/**
|
||||
* Collection of editor instances.
|
||||
*
|
||||
* @property editors
|
||||
* @type Object
|
||||
* @example
|
||||
* for (edId in tinymce.editors)
|
||||
* tinymce.editors[edId].save();
|
||||
*/
|
||||
editors: [],
|
||||
|
||||
/**
|
||||
* Collection of language pack data.
|
||||
*
|
||||
* @property i18n
|
||||
* @type Object
|
||||
*/
|
||||
i18n: I18n,
|
||||
|
||||
/**
|
||||
* Currently active editor instance.
|
||||
*
|
||||
* @property activeEditor
|
||||
* @type tinymce.Editor
|
||||
* @example
|
||||
* tinyMCE.activeEditor.selection.getContent();
|
||||
* tinymce.EditorManager.activeEditor.selection.getContent();
|
||||
*/
|
||||
activeEditor: null,
|
||||
|
||||
setup: function() {
|
||||
var self = this, baseURL, documentBaseURL, suffix = "", preInit;
|
||||
|
||||
// Get base URL for the current document
|
||||
documentBaseURL = document.location.href.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
|
||||
if (!/[\/\\]$/.test(documentBaseURL)) {
|
||||
documentBaseURL += '/';
|
||||
}
|
||||
|
||||
// If tinymce is defined and has a base use that or use the old tinyMCEPreInit
|
||||
preInit = window.tinymce || window.tinyMCEPreInit;
|
||||
if (preInit) {
|
||||
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++) {
|
||||
var src = scripts[i].src;
|
||||
|
||||
if (/tinymce(\.jquery|)(\.min|\.dev|\.full\.min|)\.js/.test(src)) { // life fix fug full.min
|
||||
if (src.indexOf('.min') != -1) {
|
||||
suffix = '.min';
|
||||
}
|
||||
|
||||
baseURL = src.substring(0, src.lastIndexOf('/'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL where the root directory if TinyMCE is located.
|
||||
*
|
||||
* @property baseURL
|
||||
* @type String
|
||||
*/
|
||||
self.baseURL = new URI(documentBaseURL).toAbsolute(baseURL);
|
||||
|
||||
/**
|
||||
* Document base URL where the current document is located.
|
||||
*
|
||||
* @property documentBaseURL
|
||||
* @type String
|
||||
*/
|
||||
self.documentBaseURL = documentBaseURL;
|
||||
|
||||
/**
|
||||
* Absolute baseURI for the installation path of TinyMCE.
|
||||
*
|
||||
* @property baseURI
|
||||
* @type tinymce.util.URI
|
||||
*/
|
||||
self.baseURI = new URI(self.baseURL);
|
||||
|
||||
/**
|
||||
* Current suffix to add to each plugin/theme that gets loaded for example ".min".
|
||||
*
|
||||
* @property suffix
|
||||
* @type String
|
||||
*/
|
||||
self.suffix = suffix;
|
||||
|
||||
self.focusManager = new FocusManager(self);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes a set of editors. This method will create editors based on various settings.
|
||||
*
|
||||
* @method init
|
||||
* @param {Object} settings Settings object to be passed to each editor instance.
|
||||
* @example
|
||||
* // Initializes a editor using the longer method
|
||||
* tinymce.EditorManager.init({
|
||||
* some_settings : 'some value'
|
||||
* });
|
||||
*
|
||||
* // Initializes a editor instance using the shorter version
|
||||
* tinyMCE.init({
|
||||
* some_settings : 'some value'
|
||||
* });
|
||||
*/
|
||||
init: function(settings) {
|
||||
var self = this, editors = [], editor;
|
||||
|
||||
function createId(elm) {
|
||||
var id = elm.id;
|
||||
|
||||
// Use element id, or unique name or generate a unique id
|
||||
if (!id) {
|
||||
id = elm.name;
|
||||
|
||||
if (id && !DOM.get(id)) {
|
||||
id = elm.name;
|
||||
} else {
|
||||
// Generate unique name
|
||||
id = DOM.uniqueId();
|
||||
}
|
||||
|
||||
elm.setAttribute('id', id);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function execCallback(se, n, s) {
|
||||
var f = se[n];
|
||||
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
return f.apply(s || this, Array.prototype.slice.call(arguments, 2));
|
||||
}
|
||||
|
||||
function hasClass(n, c) {
|
||||
return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c);
|
||||
}
|
||||
|
||||
function readyHandler() {
|
||||
var l, co;
|
||||
|
||||
DOM.unbind(window, 'ready', readyHandler);
|
||||
|
||||
execCallback(settings, 'onpageload');
|
||||
|
||||
if (settings.types) {
|
||||
// Process type specific selector
|
||||
each(settings.types, function(type) {
|
||||
each(DOM.select(type.selector), function(elm) {
|
||||
var editor = new Editor(createId(elm), extend({}, settings, type), self);
|
||||
editors.push(editor);
|
||||
editor.render(1);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (settings.selector) {
|
||||
// Process global selector
|
||||
each(DOM.select(settings.selector), function(elm) {
|
||||
var editor = new Editor(createId(elm), settings, self);
|
||||
editors.push(editor);
|
||||
editor.render(1);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to old setting
|
||||
switch (settings.mode) {
|
||||
case "exact":
|
||||
l = settings.elements || '';
|
||||
|
||||
if(l.length > 0) {
|
||||
each(explode(l), function(v) {
|
||||
if (DOM.get(v)) {
|
||||
editor = new Editor(v, settings, self);
|
||||
editors.push(editor);
|
||||
editor.render(true);
|
||||
} else {
|
||||
each(document.forms, function(f) {
|
||||
each(f.elements, function(e) {
|
||||
if (e.name === v) {
|
||||
v = 'mce_editor_' + instanceCounter++;
|
||||
DOM.setAttrib(e, 'id', v);
|
||||
|
||||
editor = new Editor(v, settings, self);
|
||||
editors.push(editor);
|
||||
editor.render(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "textareas":
|
||||
case "specific_textareas":
|
||||
each(DOM.select('textarea'), function(elm) {
|
||||
if (settings.editor_deselector && hasClass(elm, settings.editor_deselector)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings.editor_selector || hasClass(elm, settings.editor_selector)) {
|
||||
editor = new Editor(createId(elm), settings, self);
|
||||
editors.push(editor);
|
||||
editor.render(true);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Call onInit when all editors are initialized
|
||||
if (settings.oninit) {
|
||||
l = co = 0;
|
||||
|
||||
each(editors, function(ed) {
|
||||
co++;
|
||||
|
||||
if (!ed.initialized) {
|
||||
// Wait for it
|
||||
ed.on('init', function() {
|
||||
l++;
|
||||
|
||||
// All done
|
||||
if (l == co) {
|
||||
execCallback(settings, 'oninit');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
l++;
|
||||
}
|
||||
|
||||
// All done
|
||||
if (l == co) {
|
||||
execCallback(settings, 'oninit');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.settings = settings;
|
||||
|
||||
DOM.bind(window, 'ready', readyHandler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a editor instance by id.
|
||||
*
|
||||
* @method get
|
||||
* @param {String/Number} id Editor instance id or index to return.
|
||||
* @return {tinymce.Editor} Editor instance to return.
|
||||
* @example
|
||||
* // Adds an onclick event to an editor by id (shorter version)
|
||||
* tinymce.get('mytextbox').on('click', function(e) {
|
||||
* ed.windowManager.alert('Hello world!');
|
||||
* });
|
||||
*
|
||||
* // Adds an onclick event to an editor by id (longer version)
|
||||
* tinymce.EditorManager.get('mytextbox').on('click', function(e) {
|
||||
* ed.windowManager.alert('Hello world!');
|
||||
* });
|
||||
*/
|
||||
get: function(id) {
|
||||
if (id === undefined) {
|
||||
return this.editors;
|
||||
}
|
||||
|
||||
return this.editors[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* 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, editors = self.editors;
|
||||
|
||||
// Add named and index editor instance
|
||||
editors[editor.id] = editor;
|
||||
editors.push(editor);
|
||||
|
||||
self.activeEditor = editor;
|
||||
|
||||
/**
|
||||
* Fires when an editor is added to the EditorManager collection.
|
||||
*
|
||||
* @event AddEditor
|
||||
* @param {Object} e Event arguments.
|
||||
*/
|
||||
self.fire('AddEditor', {editor: editor});
|
||||
|
||||
if (!beforeUnloadDelegate) {
|
||||
beforeUnloadDelegate = function() {
|
||||
self.fire('BeforeUnload');
|
||||
};
|
||||
|
||||
DOM.bind(window, 'beforeunload', beforeUnloadDelegate);
|
||||
}
|
||||
|
||||
return editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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, editors = self.editors, editor, removedFromList;
|
||||
|
||||
// Remove all editors
|
||||
if (!selector) {
|
||||
for (i = editors.length - 1; i >= 0; i--) {
|
||||
self.remove(editors[i]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove editors by selector
|
||||
if (typeof(selector) == "string") {
|
||||
selector = selector.selector || selector;
|
||||
|
||||
each(DOM.select(selector), function(elm) {
|
||||
self.remove(editors[elm.id]);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove specific editor
|
||||
editor = selector;
|
||||
|
||||
// Not in the collection
|
||||
if (!editors[editor.id]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
delete editors[editor.id];
|
||||
|
||||
for (i = 0; i < editors.length; i++) {
|
||||
if (editors[i] == editor) {
|
||||
editors.splice(i, 1);
|
||||
removedFromList = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Select another editor since the active one was removed
|
||||
if (self.activeEditor == editor) {
|
||||
self.activeEditor = editors[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when an editor is removed from EditorManager collection.
|
||||
*
|
||||
* @event RemoveEditor
|
||||
* @param {Object} e Event arguments.
|
||||
*/
|
||||
if (removedFromList) {
|
||||
self.fire('RemoveEditor', {editor: editor});
|
||||
}
|
||||
|
||||
if (!editors.length) {
|
||||
DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
|
||||
}
|
||||
|
||||
editor.remove();
|
||||
|
||||
return editor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes a specific command on the currently active editor.
|
||||
*
|
||||
* @method execCommand
|
||||
* @param {String} c Command to perform for example Bold.
|
||||
* @param {Boolean} u Optional boolean state if a UI should be presented for the command or not.
|
||||
* @param {String} v 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);
|
||||
|
||||
// Manager commands
|
||||
switch (cmd) {
|
||||
case "mceAddEditor":
|
||||
if (!self.get(value)) {
|
||||
new Editor(value, self.settings, self).render();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case "mceRemoveEditor":
|
||||
if (editor) {
|
||||
editor.remove();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 'mceToggleEditor':
|
||||
if (!editor) {
|
||||
self.execCommand('mceAddEditor', 0, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (editor.isHidden()) {
|
||||
editor.show();
|
||||
} else {
|
||||
editor.hide();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run command on active editor
|
||||
if (self.activeEditor) {
|
||||
return self.activeEditor.execCommand(cmd, ui, value);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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(this.editors, function(editor) {
|
||||
editor.save();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
|
||||
extend(EditorManager, Observable);
|
||||
|
||||
EditorManager.setup();
|
||||
|
||||
// Export EditorManager as tinymce/tinymce in global namespace
|
||||
window.tinymce = window.tinyMCE = EditorManager;
|
||||
|
||||
return EditorManager;
|
||||
});
|
||||
@@ -0,0 +1,656 @@
|
||||
/**
|
||||
* EnterKey.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains logic for handling the enter key to split/generate block elements.
|
||||
*/
|
||||
define("tinymce/EnterKey", [
|
||||
"tinymce/dom/TreeWalker",
|
||||
"tinymce/Env"
|
||||
], function(TreeWalker, Env) {
|
||||
var isIE = Env.ie && Env.ie < 11;
|
||||
|
||||
return function(editor) {
|
||||
var dom = editor.dom, selection = editor.selection, settings = editor.settings;
|
||||
var undoManager = editor.undoManager, schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements();
|
||||
|
||||
function handleEnterKey(evt) {
|
||||
var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey,
|
||||
newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer;
|
||||
|
||||
// Returns true if the block can be split into two blocks or not
|
||||
function canSplitBlock(node) {
|
||||
return node &&
|
||||
dom.isBlock(node) &&
|
||||
!/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
|
||||
!/^(fixed|absolute)/i.test(node.style.position) &&
|
||||
dom.getContentEditable(node) !== "true";
|
||||
}
|
||||
|
||||
// Renders empty block on IE
|
||||
function renderBlockOnIE(block) {
|
||||
var oldRng;
|
||||
|
||||
if (dom.isBlock(block)) {
|
||||
oldRng = selection.getRng();
|
||||
block.appendChild(dom.create('span', null, '\u00a0'));
|
||||
selection.select(block);
|
||||
block.lastChild.outerHTML = '';
|
||||
selection.setRng(oldRng);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p>
|
||||
function trimInlineElementsOnLeftSideOfBlock(block) {
|
||||
var node = block, firstChilds = [], i;
|
||||
|
||||
// Find inner most first child ex: <p><i><b>*</b></i></p>
|
||||
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 {
|
||||
// Remove <a> </a> see #5381
|
||||
if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') {
|
||||
dom.remove(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
function firstNonWhiteSpaceNodeSibling(node) {
|
||||
while (node) {
|
||||
if (node.nodeType == 1 || (node.nodeType == 3 && node.data && /[\r\n\s]/.test(node.data))) {
|
||||
return node;
|
||||
}
|
||||
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
if (root.nodeName == 'LI') {
|
||||
var firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild);
|
||||
|
||||
if (firstChild && /^(UL|OL)$/.test(firstChild.nodeName)) {
|
||||
root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
rng = dom.createRng();
|
||||
|
||||
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 (nonEmptyElementsMap[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)) {
|
||||
// Trick on older IE versions to render the caret before the BR between two lists
|
||||
if (!documentMode || documentMode < 9) {
|
||||
tempElm = dom.create('br');
|
||||
root.parentNode.insertBefore(tempElm, root);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function setForcedBlockAttrs(node) {
|
||||
var forcedRootBlockName = settings.forced_root_block;
|
||||
|
||||
if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) {
|
||||
dom.setAttribs(node, settings.forced_root_block_attrs);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
if (name || parentBlockName == "TABLE") {
|
||||
block = dom.create(name || newBlockName);
|
||||
setForcedBlockAttrs(block);
|
||||
} else {
|
||||
block = parentBlock.cloneNode(false);
|
||||
}
|
||||
|
||||
caretNode = block;
|
||||
|
||||
// Clone any parent styles
|
||||
if (settings.keep_styles !== false) {
|
||||
do {
|
||||
if (/^(SPAN|STRONG|B|EM|I|FONT|STRIKE|U)$/.test(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));
|
||||
}
|
||||
|
||||
// BR is needed in empty blocks on non IE browsers
|
||||
if (!isIE) {
|
||||
caretNode.innerHTML = '<br data-mce-bogus="1">';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Caret is in the middle of a text node like "a|b"
|
||||
if (container.nodeType == 3 && (start ? offset > 0 : offset < 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
|
||||
if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) {
|
||||
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 && offset === 0) {
|
||||
walker.prev();
|
||||
} else if (!start && offset == 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 <img /> <input /> but not trailing br:s like <p>text|<br></p>
|
||||
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;
|
||||
}
|
||||
|
||||
// Wraps any text nodes or inline elements in the specified forced root block name
|
||||
function wrapSelfAndSiblingsInDefaultBlock(container, offset) {
|
||||
var newBlock, parentBlock, startNode, node, next, rootBlockName, blockName = newBlockName || 'P';
|
||||
|
||||
// Not in a block element or in a table cell or caption
|
||||
parentBlock = dom.getParent(container, dom.isBlock);
|
||||
rootBlockName = editor.getBody().nodeName.toLowerCase();
|
||||
if (!parentBlock || !canSplitBlock(parentBlock)) {
|
||||
parentBlock = parentBlock || editableRoot;
|
||||
|
||||
if (!parentBlock.hasChildNodes()) {
|
||||
newBlock = dom.create(blockName);
|
||||
setForcedBlockAttrs(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 && schema.isValidChild(rootBlockName, blockName.toLowerCase())) {
|
||||
newBlock = dom.create(blockName);
|
||||
setForcedBlockAttrs(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;
|
||||
}
|
||||
|
||||
// Inserts a block or br before/after or in the middle of a split list of the LI is empty
|
||||
function handleEmptyListItem() {
|
||||
function isFirstOrLastLi(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;
|
||||
}
|
||||
|
||||
function getContainerBlock() {
|
||||
var containerBlockParent = containerBlock.parentNode;
|
||||
|
||||
if (containerBlockParent.nodeName == 'LI') {
|
||||
return containerBlockParent;
|
||||
}
|
||||
|
||||
return containerBlock;
|
||||
}
|
||||
|
||||
// Check if we are in an nested list
|
||||
var containerBlockParentName = containerBlock.parentNode.nodeName;
|
||||
if (/^(OL|UL|LI)$/.test(containerBlockParentName)) {
|
||||
newBlockName = 'LI';
|
||||
}
|
||||
|
||||
newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR');
|
||||
|
||||
if (isFirstOrLastLi(true) && isFirstOrLastLi()) {
|
||||
if (containerBlockParentName == 'LI') {
|
||||
// Nested list is inside a LI
|
||||
dom.insertAfter(newBlock, getContainerBlock());
|
||||
} else {
|
||||
// Is first and last list item then replace the OL/UL with a text block
|
||||
dom.replace(newBlock, containerBlock);
|
||||
}
|
||||
} else if (isFirstOrLastLi(true)) {
|
||||
if (containerBlockParentName == 'LI') {
|
||||
// List nested in an LI then move the list to a new sibling LI
|
||||
dom.insertAfter(newBlock, getContainerBlock());
|
||||
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()) {
|
||||
// Last LI in list then remove LI and add text block after list
|
||||
dom.insertAfter(newBlock, getContainerBlock());
|
||||
renderBlockOnIE(newBlock);
|
||||
} 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();
|
||||
tmpRng = rng.cloneRange();
|
||||
tmpRng.setStartAfter(parentBlock);
|
||||
tmpRng.setEndAfter(containerBlock);
|
||||
fragment = tmpRng.extractContents();
|
||||
|
||||
if (newBlockName == 'LI' && fragment.firstChild.nodeName == 'LI') {
|
||||
newBlock = fragment.firstChild;
|
||||
dom.insertAfter(fragment, containerBlock);
|
||||
} else {
|
||||
dom.insertAfter(fragment, containerBlock);
|
||||
dom.insertAfter(newBlock, containerBlock);
|
||||
}
|
||||
}
|
||||
|
||||
dom.remove(parentBlock);
|
||||
moveToCaretPosition(newBlock);
|
||||
undoManager.add();
|
||||
}
|
||||
|
||||
// Walks the parent block to the right and look for BR elements
|
||||
function hasRightSideContent() {
|
||||
var walker = new TreeWalker(container, parentBlock), node;
|
||||
|
||||
while ((node = walker.next())) {
|
||||
if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inserts a BR element if the forced_root_block option is set to false or empty string
|
||||
function insertBr() {
|
||||
var brElm, extraBr, marker;
|
||||
|
||||
if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
|
||||
// Insert extra BR element at the end block elements
|
||||
if (!isIE && !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
|
||||
if (isIE && 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);
|
||||
undoManager.add();
|
||||
}
|
||||
|
||||
// Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element
|
||||
function trimLeadingLineBreaks(node) {
|
||||
do {
|
||||
if (node.nodeType === 3) {
|
||||
node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, '');
|
||||
}
|
||||
|
||||
node = node.firstChild;
|
||||
} while (node);
|
||||
}
|
||||
|
||||
function getEditableRoot(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;
|
||||
}
|
||||
|
||||
// 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
|
||||
function addBrToBlockIfNeeded(block) {
|
||||
var lastChild;
|
||||
|
||||
// IE will render the blocks correctly other browsers needs a BR
|
||||
if (!isIE) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any selected contents
|
||||
if (!rng.collapsed) {
|
||||
editor.execCommand('Delete');
|
||||
return;
|
||||
}
|
||||
|
||||
// Event is blocked by some other handler for example the lists plugin
|
||||
if (evt.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup range items and newBlockName
|
||||
container = rng.startContainer;
|
||||
offset = rng.startOffset;
|
||||
newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block;
|
||||
newBlockName = newBlockName ? newBlockName.toUpperCase() : '';
|
||||
documentMode = dom.doc.documentMode;
|
||||
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 normaly the body element but sometimes a div or span
|
||||
editableRoot = getEditableRoot(container);
|
||||
|
||||
// If there is no editable root then enter is done inside a contentEditable false element
|
||||
if (!editableRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
undoManager.beforeChange();
|
||||
|
||||
// If editable root isn't block nor the root of the editor
|
||||
if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) {
|
||||
if (!newBlockName || shiftKey) {
|
||||
insertBr();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrap the current node and it's sibling in a default block if it's needed.
|
||||
// for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td>
|
||||
// This won't happen if root blocks are disabled or the shiftKey is pressed
|
||||
if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) {
|
||||
container = wrapSelfAndSiblingsInDefaultBlock(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;
|
||||
parentBlockName = containerBlockName;
|
||||
}
|
||||
|
||||
// Handle enter in LI
|
||||
if (parentBlockName == 'LI') {
|
||||
if (!newBlockName && shiftKey) {
|
||||
insertBr();
|
||||
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();
|
||||
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();
|
||||
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 (isCaretAtStartOrEndOfBlock()) {
|
||||
// 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(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);
|
||||
} else if (isCaretAtStartOrEndOfBlock(true)) {
|
||||
// Insert new block before
|
||||
newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock);
|
||||
renderBlockOnIE(newBlock);
|
||||
moveToCaretPosition(parentBlock);
|
||||
} else {
|
||||
// Extract after fragment and insert it after the current block
|
||||
tmpRng = rng.cloneRange();
|
||||
tmpRng.setEndAfter(parentBlock);
|
||||
fragment = tmpRng.extractContents();
|
||||
trimLeadingLineBreaks(fragment);
|
||||
newBlock = fragment.firstChild;
|
||||
dom.insertAfter(fragment, parentBlock);
|
||||
trimInlineElementsOnLeftSideOfBlock(newBlock);
|
||||
addBrToBlockIfNeeded(parentBlock);
|
||||
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 });
|
||||
|
||||
undoManager.add();
|
||||
}
|
||||
|
||||
editor.on('keydown', function(evt) {
|
||||
if (evt.keyCode == 13) {
|
||||
if (handleEnterKey(evt) !== false) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Env.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class contains various environment constrants like browser versions etc.
|
||||
* Normally you don't want to sniff specific browser versions but sometimes you have
|
||||
* to when it's impossible to feature detect. So use this with care.
|
||||
*
|
||||
* @class tinymce.Env
|
||||
* @static
|
||||
*/
|
||||
define("tinymce/Env", [], function() {
|
||||
var nav = navigator, userAgent = nav.userAgent;
|
||||
var opera, webkit, ie, ie11, gecko, mac, iDevice;
|
||||
|
||||
opera = window.opera && window.opera.buildNumber;
|
||||
webkit = /WebKit/.test(userAgent);
|
||||
ie = !webkit && !opera && (/MSIE/gi).test(userAgent) && (/Explorer/gi).test(nav.appName);
|
||||
ie = ie && /MSIE (\w+)\./.exec(userAgent)[1];
|
||||
ie11 = userAgent.indexOf('Trident/') != -1 && (userAgent.indexOf('rv:') != -1 || nav.appName.indexOf('Netscape') != -1) ? 11 : false;
|
||||
ie = ie || ie11;
|
||||
gecko = !webkit && !ie11 && /Gecko/.test(userAgent);
|
||||
mac = userAgent.indexOf('Mac') != -1;
|
||||
iDevice = /(iPad|iPhone)/.test(userAgent);
|
||||
|
||||
// Is a iPad/iPhone and not on iOS5 sniff the WebKit version since older iOS WebKit versions
|
||||
// says it has contentEditable support but there is no visible caret.
|
||||
var contentEditable = !iDevice || userAgent.match(/AppleWebKit\/(\d*)/)[1] >= 534;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Constant that is true if the browser is Opera.
|
||||
*
|
||||
* @property opera
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
opera: opera,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser is WebKit (Safari/Chrome).
|
||||
*
|
||||
* @property webKit
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
webkit: webkit,
|
||||
|
||||
/**
|
||||
* Constant that is more than zero if the browser is IE.
|
||||
*
|
||||
* @property ie
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
ie: ie,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser is Gecko.
|
||||
*
|
||||
* @property gecko
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
gecko: gecko,
|
||||
|
||||
/**
|
||||
* Constant that is true if the os is Mac OS.
|
||||
*
|
||||
* @property mac
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
mac: mac,
|
||||
|
||||
/**
|
||||
* Constant that is true if the os is iOS.
|
||||
*
|
||||
* @property iOS
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
iOS: iDevice,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser supports editing.
|
||||
*
|
||||
* @property contentEditable
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
contentEditable: contentEditable,
|
||||
|
||||
/**
|
||||
* Transparent image data url.
|
||||
*
|
||||
* @property transparentSrc
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
transparentSrc: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
|
||||
|
||||
/**
|
||||
* Returns true/false if the browser can or can't place the caret after a inline block like an image.
|
||||
*
|
||||
* @property noCaretAfter
|
||||
* @type Boolean
|
||||
* @final
|
||||
*/
|
||||
caretAfter: ie != 8,
|
||||
|
||||
/**
|
||||
* Constant that is true if the browser supports native DOM Ranges. IE 9+.
|
||||
*
|
||||
* @property range
|
||||
* @type Boolean
|
||||
*/
|
||||
range: window.getSelection && "Range" in window,
|
||||
|
||||
/**
|
||||
* Returns the IE document mode for non IE browsers this will fake IE 10.
|
||||
*
|
||||
* @property documentMode
|
||||
* @type Number
|
||||
*/
|
||||
documentMode: ie ? (document.documentMode || 7) : 10
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* FocusManager.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @class tinymce.FocusManager
|
||||
*/
|
||||
define("tinymce/FocusManager", [
|
||||
"tinymce/dom/DOMUtils",
|
||||
"tinymce/Env"
|
||||
], function(DOMUtils, Env) {
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't store a real range on IE 11 since it gets mutated so we need to use a bookmark object
|
||||
// TODO: Move this to a separate range utils class since it's it's logic is present in Selection as well.
|
||||
function createBookmark(rng) {
|
||||
if (rng && rng.startContainer) {
|
||||
return {
|
||||
startContainer: rng.startContainer,
|
||||
startOffset: rng.startOffset,
|
||||
endContainer: rng.endContainer,
|
||||
endOffset: rng.endOffset
|
||||
};
|
||||
}
|
||||
|
||||
return rng;
|
||||
}
|
||||
|
||||
function bookmarkToRng(editor, bookmark) {
|
||||
var rng;
|
||||
|
||||
if (bookmark.startContainer) {
|
||||
rng = editor.getDoc().createRange();
|
||||
rng.setStart(bookmark.startContainer, bookmark.startOffset);
|
||||
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
|
||||
} else {
|
||||
rng = bookmark;
|
||||
}
|
||||
|
||||
return rng;
|
||||
}
|
||||
|
||||
function registerEvents(e) {
|
||||
var editor = e.editor, lastRng, selectionChangeHandler;
|
||||
|
||||
function isUIElement(elm) {
|
||||
return !!DOMUtils.DOM.getParent(elm, FocusManager.isEditorUIElement);
|
||||
}
|
||||
|
||||
editor.on('init', function() {
|
||||
// On IE take selection snapshot onbeforedeactivate
|
||||
if ("onbeforedeactivate" in document && Env.ie < 11) {
|
||||
editor.dom.bind(editor.getBody(), 'beforedeactivate', function() {
|
||||
var ieSelection = editor.getDoc().selection;
|
||||
|
||||
try {
|
||||
lastRng = ieSelection && ieSelection.createRange ? ieSelection.createRange() : editor.selection.getRng();
|
||||
} catch (ex) {
|
||||
// IE throws "Unexcpected call to method or property access" some times so lets ignore it
|
||||
}
|
||||
});
|
||||
} else if (editor.inline || Env.ie > 10) {
|
||||
// On other browsers take snapshot on nodechange in inline mode since they have Ghost selections for iframes
|
||||
editor.on('nodechange keyup', function() {
|
||||
var isInBody, node = document.activeElement;
|
||||
|
||||
// IE 11 reports active element as iframe not body of iframe
|
||||
if (node && node.id == editor.id + '_ifr') {
|
||||
node = editor.getBody();
|
||||
}
|
||||
|
||||
// Check if selection is within editor body
|
||||
while (node) {
|
||||
if (node == editor.getBody()) {
|
||||
isInBody = true;
|
||||
break;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
if (isInBody) {
|
||||
lastRng = editor.selection.getRng();
|
||||
}
|
||||
});
|
||||
|
||||
// Handles the issue with WebKit not retaining selection within inline document
|
||||
// If the user releases the mouse out side the body while selecting a nodeChange won't
|
||||
// fire and there for the selection snapshot won't be stored
|
||||
// TODO: Optimize this since we only need to bind these on the active editor
|
||||
if (Env.webkit) {
|
||||
selectionChangeHandler = function() {
|
||||
var rng = editor.selection.getRng();
|
||||
|
||||
// Store when it's non collapsed
|
||||
if (!rng.collapsed) {
|
||||
lastRng = rng;
|
||||
}
|
||||
};
|
||||
|
||||
// Bind selection handler
|
||||
DOMUtils.DOM.bind(document, 'selectionchange', selectionChangeHandler);
|
||||
|
||||
editor.on('remove', function() {
|
||||
DOMUtils.DOM.unbind(document, 'selectionchange', selectionChangeHandler);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('setcontent', function() {
|
||||
lastRng = null;
|
||||
});
|
||||
|
||||
// Remove last selection bookmark on mousedown see #6305
|
||||
editor.on('mousedown', function() {
|
||||
editor.selection.lastFocusBookmark = null;
|
||||
});
|
||||
|
||||
editor.on('focusin', function() {
|
||||
var focusedEditor = editorManager.focusedEditor;
|
||||
|
||||
if (editor.selection.lastFocusBookmark) {
|
||||
editor.selection.setRng(bookmarkToRng(editor, editor.selection.lastFocusBookmark));
|
||||
editor.selection.lastFocusBookmark = null;
|
||||
}
|
||||
|
||||
if (focusedEditor != editor) {
|
||||
if (focusedEditor) {
|
||||
focusedEditor.fire('blur', {focusedEditor: editor});
|
||||
}
|
||||
|
||||
editorManager.activeEditor = editor;
|
||||
editor.fire('focus', {blurredEditor: focusedEditor});
|
||||
editor.focus(false);
|
||||
editorManager.focusedEditor = editor;
|
||||
}
|
||||
|
||||
lastRng = null;
|
||||
});
|
||||
|
||||
editor.on('focusout', function() {
|
||||
editor.selection.lastFocusBookmark = createBookmark(lastRng);
|
||||
|
||||
window.setTimeout(function() {
|
||||
var focusedEditor = editorManager.focusedEditor;
|
||||
|
||||
// Focus from editorA into editorB then don't restore selection
|
||||
if (focusedEditor != editor) {
|
||||
editor.selection.lastFocusBookmark = null;
|
||||
}
|
||||
|
||||
// Still the same editor the the blur was outside any editor UI
|
||||
if (!isUIElement(getActiveElement()) && focusedEditor == editor) {
|
||||
editor.fire('blur', {focusedEditor: null});
|
||||
editorManager.focusedEditor = null;
|
||||
editor.selection.lastFocusBookmark = null;
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
editorManager.on('AddEditor', registerEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return elm.className.indexOf('mce-') !== -1;
|
||||
};
|
||||
|
||||
return FocusManager;
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* ForceBlocks.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
define("tinymce/ForceBlocks", [], function() {
|
||||
return function(editor) {
|
||||
var settings = editor.settings, dom = editor.dom, selection = editor.selection;
|
||||
var schema = editor.schema, blockElements = schema.getBlockElements();
|
||||
|
||||
function addRootBlocks() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Force root blocks
|
||||
if (settings.forced_root_block) {
|
||||
editor.on('NodeChange', addRootBlocks);
|
||||
}
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* LegacyInput.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
define("tinymce/LegacyInput", [
|
||||
"tinymce/EditorManager",
|
||||
"tinymce/util/Tools"
|
||||
], function(EditorManager, Tools) {
|
||||
var each = Tools.each, explode = Tools.explode;
|
||||
|
||||
EditorManager.on('AddEditor', function(e) {
|
||||
var editor = e.editor;
|
||||
|
||||
editor.on('preInit', function() {
|
||||
var filters, fontSizes, dom, settings = editor.settings;
|
||||
|
||||
function replaceWithSpan(node, styles) {
|
||||
each(styles, function(value, name) {
|
||||
if (value) {
|
||||
dom.setStyle(node, name, value);
|
||||
}
|
||||
});
|
||||
|
||||
dom.rename(node, 'span');
|
||||
}
|
||||
|
||||
function convert(e) {
|
||||
dom = editor.dom;
|
||||
|
||||
if (settings.convert_fonts_to_spans) {
|
||||
each(dom.select('font,u,strike', e.node), function(node) {
|
||||
filters[node.nodeName.toLowerCase()](dom, node);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.inline_styles) {
|
||||
fontSizes = explode(settings.font_size_legacy_values);
|
||||
|
||||
filters = {
|
||||
font: function(dom, node) {
|
||||
replaceWithSpan(node, {
|
||||
backgroundColor: node.style.backgroundColor,
|
||||
color: node.color,
|
||||
fontFamily: node.face,
|
||||
fontSize: fontSizes[parseInt(node.size, 10) - 1]
|
||||
});
|
||||
},
|
||||
|
||||
u: function(dom, node) {
|
||||
replaceWithSpan(node, {
|
||||
textDecoration: 'underline'
|
||||
});
|
||||
},
|
||||
|
||||
strike: function(dom, node) {
|
||||
replaceWithSpan(node, {
|
||||
textDecoration: 'line-through'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
editor.on('PreProcess SetContent', convert);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Shortcuts.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contains all logic for handling of keyboard shortcuts.
|
||||
*/
|
||||
define("tinymce/Shortcuts", [
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/Env"
|
||||
], function(Tools, Env) {
|
||||
var each = Tools.each, explode = Tools.explode;
|
||||
|
||||
var keyCodeLookup = {
|
||||
"f9": 120,
|
||||
"f10": 121,
|
||||
"f11": 122
|
||||
};
|
||||
|
||||
return function(editor) {
|
||||
var self = this, shortcuts = {};
|
||||
|
||||
editor.on('keyup keypress keydown', function(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) {
|
||||
each(shortcuts, function(shortcut) {
|
||||
var ctrlKey = Env.mac ? (e.ctrlKey || e.metaKey) : e.ctrlKey;
|
||||
|
||||
if (shortcut.ctrl != ctrlKey || shortcut.alt != e.altKey || shortcut.shift != e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode == shortcut.keyCode || (e.charCode && e.charCode == shortcut.charCode)) {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.type == "keydown") {
|
||||
shortcut.func.call(shortcut.scope);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
self.add = function(pattern, desc, cmdFunc, scope) {
|
||||
var cmd;
|
||||
|
||||
cmd = cmdFunc;
|
||||
|
||||
if (typeof(cmdFunc) === 'string') {
|
||||
cmdFunc = function() {
|
||||
editor.execCommand(cmd, false, null);
|
||||
};
|
||||
} else if (Tools.isArray(cmd)) {
|
||||
cmdFunc = function() {
|
||||
editor.execCommand(cmd[0], cmd[1], cmd[2]);
|
||||
};
|
||||
}
|
||||
|
||||
each(explode(pattern.toLowerCase()), function(pattern) {
|
||||
var shortcut = {
|
||||
func: cmdFunc,
|
||||
scope: scope || editor,
|
||||
desc: editor.translate(desc),
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
shift: false
|
||||
};
|
||||
|
||||
each(explode(pattern, '+'), function(value) {
|
||||
switch (value) {
|
||||
case 'alt':
|
||||
case 'ctrl':
|
||||
case 'shift':
|
||||
shortcut[value] = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
shortcut.charCode = value.charCodeAt(0);
|
||||
shortcut.keyCode = keyCodeLookup[value] || value.toUpperCase().charCodeAt(0);
|
||||
}
|
||||
});
|
||||
|
||||
shortcuts[
|
||||
(shortcut.ctrl ? 'ctrl' : '') + ',' +
|
||||
(shortcut.alt ? 'alt' : '') + ',' +
|
||||
(shortcut.shift ? 'shift' : '') + ',' +
|
||||
shortcut.keyCode
|
||||
] = shortcut;
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* UndoManager.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class handles the undo/redo history levels for the editor. Since the build in undo/redo has major drawbacks a custom one was needed.
|
||||
*
|
||||
* @class tinymce.UndoManager
|
||||
*/
|
||||
define("tinymce/UndoManager", [
|
||||
"tinymce/Env",
|
||||
"tinymce/util/Tools"
|
||||
], function(Env, Tools) {
|
||||
var trim = Tools.trim, trimContentRegExp;
|
||||
|
||||
trimContentRegExp = new RegExp([
|
||||
'<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
|
||||
'<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles
|
||||
'\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
|
||||
].join('|'), 'gi');
|
||||
|
||||
return function(editor) {
|
||||
var self, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, lock;
|
||||
|
||||
// Returns a trimmed version of the current editor contents
|
||||
function getContent() {
|
||||
return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, ''));
|
||||
}
|
||||
|
||||
function addNonTypingUndoLevel() {
|
||||
self.typing = 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') {
|
||||
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') {
|
||||
self.add();
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('ObjectResizeStart', function() {
|
||||
self.beforeChange();
|
||||
});
|
||||
|
||||
editor.on('SaveContent ObjectResized', addNonTypingUndoLevel);
|
||||
editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);
|
||||
editor.dom.bind(editor.getBody(), 'focusout', function() {
|
||||
if (!editor.removed && self.typing) {
|
||||
addNonTypingUndoLevel();
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('KeyUp', function(e) {
|
||||
var keyCode = e.keyCode;
|
||||
|
||||
if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
|
||||
addNonTypingUndoLevel();
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) {
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
// Fire a TypingUndo event on the first character entered
|
||||
if (isFirstTypedCharacter && self.typing) {
|
||||
// Make the it dirty if the content was changed after typing the first character
|
||||
if (!editor.isDirty()) {
|
||||
editor.isNotDirty = !data[0] || getContent() == data[0].content;
|
||||
|
||||
// Fire initial change event
|
||||
if (!editor.isNotDirty) {
|
||||
editor.fire('change', {level: data[0], lastLevel: null});
|
||||
}
|
||||
}
|
||||
|
||||
editor.fire('TypingUndo');
|
||||
isFirstTypedCharacter = false;
|
||||
editor.nodeChanged();
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('KeyDown', function(e) {
|
||||
var keyCode = e.keyCode;
|
||||
|
||||
// Is caracter positon 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();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If key isn't shift,ctrl,alt,capslock,metakey
|
||||
if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) {
|
||||
self.beforeChange();
|
||||
self.typing = true;
|
||||
self.add();
|
||||
isFirstTypedCharacter = true;
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('MouseDown', function() {
|
||||
if (self.typing) {
|
||||
addNonTypingUndoLevel();
|
||||
}
|
||||
});
|
||||
|
||||
// Add keyboard shortcuts for undo/redo keys
|
||||
editor.addShortcut('ctrl+z', '', 'Undo');
|
||||
editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo');
|
||||
|
||||
editor.on('AddUndo Undo Redo ClearUndos MouseUp', function(e) {
|
||||
if (!e.isDefaultPrevented()) {
|
||||
editor.nodeChanged();
|
||||
}
|
||||
});
|
||||
|
||||
self = {
|
||||
// Explose 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 (!lock) {
|
||||
beforeBookmark = editor.selection.getBookmark(2, true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new undo level/snapshot to the undo list.
|
||||
*
|
||||
* @method add
|
||||
* @param {Object} l Optional undo level object to add.
|
||||
* @return {Object} Undo level that got added or null it a level wasn't needed.
|
||||
*/
|
||||
add: function(level) {
|
||||
var i, settings = editor.settings, lastLevel;
|
||||
|
||||
level = level || {};
|
||||
level.content = getContent();
|
||||
|
||||
if (lock || editor.fire('BeforeAddUndo', {level: level}).isDefaultPrevented()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Add undo level if needed
|
||||
lastLevel = data[index];
|
||||
if (lastLevel && lastLevel.content == level.content) {
|
||||
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};
|
||||
|
||||
editor.fire('AddUndo', args);
|
||||
|
||||
if (index > 0) {
|
||||
editor.fire('change', args);
|
||||
editor.isNotDirty = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (index > 0) {
|
||||
level = data[--index];
|
||||
|
||||
// Undo to first index then set dirty state to false
|
||||
if (index === 0) {
|
||||
editor.isNotDirty = true;
|
||||
}
|
||||
|
||||
editor.setContent(level.content, {format: 'raw'});
|
||||
editor.selection.moveToBookmark(level.beforeBookmark);
|
||||
|
||||
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];
|
||||
|
||||
editor.setContent(level.content, {format: 'raw'});
|
||||
editor.selection.moveToBookmark(level.bookmark);
|
||||
|
||||
editor.fire('redo', {level: level});
|
||||
}
|
||||
|
||||
return level;
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all undo levels.
|
||||
*
|
||||
* @method clear
|
||||
*/
|
||||
clear: function() {
|
||||
data = [];
|
||||
index = 0;
|
||||
self.typing = false;
|
||||
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] && getContent() != data[0].content);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 && !this.typing;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executes the specified function in an undo transation. 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 methods within the transation that adds undo levels will
|
||||
* be ignored. So a transation can include calls to execCommand or editor.insertContent.
|
||||
*
|
||||
* @method transact
|
||||
* @param {function} callback Function to execute dom manipulation logic in.
|
||||
*/
|
||||
transact: function(callback) {
|
||||
self.beforeChange();
|
||||
|
||||
lock = true;
|
||||
callback();
|
||||
lock = false;
|
||||
|
||||
self.add();
|
||||
}
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* WindowManager.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class handles the 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/WindowManager", [
|
||||
"tinymce/ui/Window",
|
||||
"tinymce/ui/MessageBox"
|
||||
], function(Window, MessageBox) {
|
||||
return function(editor) {
|
||||
var self = this, windows = [];
|
||||
|
||||
function getTopMostWindow() {
|
||||
if (windows.length) {
|
||||
return windows[windows.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
self.windows = windows;
|
||||
|
||||
/**
|
||||
* Opens a new window.
|
||||
*
|
||||
* @method open
|
||||
* @param {Object} args Optional name/value settings collection contains things like width/height/url 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} resizable Specifies whether the popup window is resizable or not.
|
||||
* @option {Boolean} maximizable Specifies whether the popup window has a "maximize" button and can get maximized or not.
|
||||
* @option {String/bool} scrollbars Specifies whether the popup window can have scrollbars if required (i.e. content
|
||||
* larger than the popup size specified).
|
||||
*/
|
||||
self.open = function(args, params) {
|
||||
var win;
|
||||
|
||||
editor.editorManager.activeEditor = editor;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.url && !args.buttons) {
|
||||
args.buttons = [
|
||||
{text: 'Ok', subtype: 'primary', onclick: function() {
|
||||
win.find('form')[0].submit();
|
||||
win.close();
|
||||
}},
|
||||
|
||||
{text: 'Cancel', onclick: function() {
|
||||
win.close();
|
||||
}}
|
||||
];
|
||||
}
|
||||
|
||||
win = new Window(args);
|
||||
windows.push(win);
|
||||
|
||||
win.on('close', function() {
|
||||
var i = windows.length;
|
||||
|
||||
while (i--) {
|
||||
if (windows[i] === win) {
|
||||
windows.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
editor.focus();
|
||||
});
|
||||
|
||||
// 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 || {};
|
||||
|
||||
// Takes a snapshot in the FocusManager of the selection before focus is lost to dialog
|
||||
editor.nodeChanged();
|
||||
|
||||
return win.renderTo(document.body).reflow();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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!');
|
||||
*/
|
||||
self.alert = function(message, callback, scope) {
|
||||
MessageBox.alert(message, function() {
|
||||
if (callback) {
|
||||
callback.call(scope || this);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 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} messageText 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");
|
||||
* });
|
||||
*/
|
||||
self.confirm = function(message, callback, scope) {
|
||||
MessageBox.confirm(message, function(state) {
|
||||
callback.call(scope || this, state);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the top most window.
|
||||
*
|
||||
* @method close
|
||||
*/
|
||||
self.close = function() {
|
||||
if (getTopMostWindow()) {
|
||||
getTopMostWindow().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.
|
||||
*/
|
||||
self.getParams = function() {
|
||||
return getTopMostWindow() ? getTopMostWindow().params : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the params of the last opened window.
|
||||
*
|
||||
* @method setParams
|
||||
* @param {Object} params Params object to set for the last opened window.
|
||||
*/
|
||||
self.setParams = function(params) {
|
||||
if (getTopMostWindow()) {
|
||||
getTopMostWindow().params = params;
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* ControlSelection.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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/dom/ControlSelection", [
|
||||
"tinymce/util/VK",
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/Env"
|
||||
], function(VK, Tools, Env) {
|
||||
return function(selection, editor) {
|
||||
var dom = editor.dom, each = Tools.each;
|
||||
var selectedElm, selectedElmGhost, 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;
|
||||
|
||||
// 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]
|
||||
};
|
||||
|
||||
// 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;' +
|
||||
'background: #FFF;' +
|
||||
'width: 5px;' +
|
||||
'height: 5px;' +
|
||||
'z-index: 10000' +
|
||||
'}' +
|
||||
rootClass + ' .mce-resizehandle:hover {' +
|
||||
'background: #000' +
|
||||
'}' +
|
||||
rootClass + ' img[data-mce-selected], 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' +
|
||||
'}'
|
||||
);
|
||||
|
||||
function isResizable(elm) {
|
||||
if (editor.settings.object_resizing === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/TABLE|IMG|DIV/.test(elm.nodeName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elm.getAttribute('data-mce-resize') === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function resizeGhostElement(e) {
|
||||
var deltaX, deltaY;
|
||||
|
||||
// Calc new width/height
|
||||
deltaX = e.screenX - startX;
|
||||
deltaY = e.screenY - startY;
|
||||
|
||||
// Calc new size
|
||||
width = deltaX * selectedHandle[2] + startW;
|
||||
height = deltaY * selectedHandle[3] + startH;
|
||||
|
||||
// Never scale down lower than 5 pixels
|
||||
width = width < 5 ? 5 : width;
|
||||
height = height < 5 ? 5 : height;
|
||||
|
||||
// Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image
|
||||
if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) {
|
||||
width = Math.round(height / ratio);
|
||||
height = Math.round(width * ratio);
|
||||
}
|
||||
|
||||
// Update ghost size
|
||||
dom.setStyles(selectedElmGhost, {
|
||||
width: width,
|
||||
height: height
|
||||
});
|
||||
|
||||
// Update ghost X position if needed
|
||||
if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) {
|
||||
dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width));
|
||||
}
|
||||
|
||||
// Update ghost Y position if needed
|
||||
if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) {
|
||||
dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height));
|
||||
}
|
||||
|
||||
if (!resizeStarted) {
|
||||
editor.fire('ObjectResizeStart', {target: selectedElm, width: startW, height: startH});
|
||||
resizeStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
function endGhostResize() {
|
||||
resizeStarted = false;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set width/height properties
|
||||
setSizeProp('width', width);
|
||||
setSizeProp('height', height);
|
||||
|
||||
dom.unbind(editableDoc, 'mousemove', resizeGhostElement);
|
||||
dom.unbind(editableDoc, 'mouseup', endGhostResize);
|
||||
|
||||
if (rootDocument != editableDoc) {
|
||||
dom.unbind(rootDocument, 'mousemove', resizeGhostElement);
|
||||
dom.unbind(rootDocument, 'mouseup', endGhostResize);
|
||||
}
|
||||
|
||||
// Remove ghost and update resize handle positions
|
||||
dom.remove(selectedElmGhost);
|
||||
|
||||
if (!isIE || selectedElm.nodeName == "TABLE") {
|
||||
showResizeRect(selectedElm);
|
||||
}
|
||||
|
||||
editor.fire('ObjectResized', {target: selectedElm, width: width, height: height});
|
||||
editor.nodeChanged();
|
||||
}
|
||||
|
||||
function showResizeRect(targetElm, mouseDownHandleName, mouseDownEvent) {
|
||||
var position, targetWidth, targetHeight, e, rect, offsetParent = editor.getBody();
|
||||
|
||||
// Get position and size of target
|
||||
position = dom.getPos(targetElm, offsetParent);
|
||||
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);
|
||||
|
||||
// Reset width/height if user selects a new image/table
|
||||
if (selectedElm != targetElm) {
|
||||
detachResizeStartListener();
|
||||
selectedElm = targetElm;
|
||||
width = height = 0;
|
||||
}
|
||||
|
||||
// Makes it possible to disable resizing
|
||||
e = editor.fire('ObjectSelected', {target: targetElm});
|
||||
|
||||
if (isResizable(targetElm) && !e.isDefaultPrevented()) {
|
||||
each(resizeHandles, function(handle, name) {
|
||||
var handleElm, handlerContainerElm;
|
||||
|
||||
function startDrag(e) {
|
||||
resizeStarted = true;
|
||||
|
||||
startX = e.screenX;
|
||||
startY = e.screenY;
|
||||
startW = selectedElm.clientWidth;
|
||||
startH = selectedElm.clientHeight;
|
||||
ratio = startH / startW;
|
||||
selectedHandle = handle;
|
||||
|
||||
selectedElmGhost = selectedElm.cloneNode(true);
|
||||
dom.addClass(selectedElmGhost, 'mce-clonedresizable');
|
||||
selectedElmGhost.contentEditable = false; // Hides IE move layer cursor
|
||||
selectedElmGhost.unSelectabe = true;
|
||||
dom.setStyles(selectedElmGhost, {
|
||||
left: selectedElmX,
|
||||
top: selectedElmY,
|
||||
margin: 0
|
||||
});
|
||||
|
||||
selectedElmGhost.removeAttribute('data-mce-selected');
|
||||
editor.getBody().appendChild(selectedElmGhost);
|
||||
|
||||
dom.bind(editableDoc, 'mousemove', resizeGhostElement);
|
||||
dom.bind(editableDoc, 'mouseup', endGhostResize);
|
||||
|
||||
if (rootDocument != editableDoc) {
|
||||
dom.bind(rootDocument, 'mousemove', resizeGhostElement);
|
||||
dom.bind(rootDocument, 'mouseup', endGhostResize);
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseDownHandleName) {
|
||||
// Drag started by IE native resizestart
|
||||
if (name == mouseDownHandleName) {
|
||||
startDrag(mouseDownEvent);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing or render resize handle
|
||||
handleElm = dom.get('mceResizeHandle' + name);
|
||||
if (!handleElm) {
|
||||
handlerContainerElm = editor.getBody();
|
||||
|
||||
handleElm = dom.add(handlerContainerElm, 'div', {
|
||||
id: 'mceResizeHandle' + name,
|
||||
'data-mce-bogus': true,
|
||||
'class': 'mce-resizehandle',
|
||||
contentEditable: false, // Hides IE move layer cursor
|
||||
unSelectabe: true,
|
||||
style: 'cursor:' + name + '-resize; margin:0; padding:0'
|
||||
});
|
||||
|
||||
dom.bind(handleElm, 'mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
startDrag(e);
|
||||
});
|
||||
} else {
|
||||
dom.show(handleElm);
|
||||
}
|
||||
|
||||
/*
|
||||
var halfHandleW = handleElm.offsetWidth / 2;
|
||||
var halfHandleH = handleElm.offsetHeight / 2;
|
||||
|
||||
// Position element
|
||||
dom.setStyles(handleElm, {
|
||||
left: Math.floor((targetWidth * handle[0] + selectedElmX) - halfHandleW + (handle[2] * halfHandleW)),
|
||||
top: Math.floor((targetHeight * handle[1] + selectedElmY) - halfHandleH + (handle[3] * halfHandleH))
|
||||
});
|
||||
*/
|
||||
|
||||
// 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;
|
||||
|
||||
if (selectedElm) {
|
||||
selectedElm.removeAttribute('data-mce-selected');
|
||||
}
|
||||
|
||||
for (name in resizeHandles) {
|
||||
handleElm = dom.get('mceResizeHandle' + name);
|
||||
if (handleElm) {
|
||||
dom.unbind(handleElm);
|
||||
dom.remove(handleElm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateResizeRect(e) {
|
||||
var controlElm;
|
||||
|
||||
function isChildOrEqual(node, parent) {
|
||||
do {
|
||||
if (node === parent) {
|
||||
return true;
|
||||
}
|
||||
} while ((node = node.parentNode));
|
||||
}
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
controlElm = e.type == 'mousedown' ? e.target : selection.getNode();
|
||||
controlElm = dom.getParent(controlElm, isIE ? 'table' : 'table,img,hr');
|
||||
|
||||
if (controlElm) {
|
||||
disableGeckoResize();
|
||||
|
||||
if (isChildOrEqual(selection.getStart(), controlElm) && isChildOrEqual(selection.getEnd(), controlElm)) {
|
||||
if (!isIE || (controlElm != selection.getStart() && selection.getStart().nodeName !== 'IMG')) {
|
||||
showResizeRect(controlElm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideResizeRect();
|
||||
}
|
||||
|
||||
function attachEvent(elm, name, func) {
|
||||
if (elm && elm.attachEvent) {
|
||||
elm.attachEvent('on' + name, func);
|
||||
}
|
||||
}
|
||||
|
||||
function detachEvent(elm, name, func) {
|
||||
if (elm && elm.detachEvent) {
|
||||
elm.detachEvent('on' + name, func);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeNativeStart(e) {
|
||||
var target = e.srcElement, pos, name, corner, cornerX, cornerY, relativeX, relativeY;
|
||||
|
||||
pos = target.getBoundingClientRect();
|
||||
relativeX = lastMouseDownEvent.clientX - pos.left;
|
||||
relativeY = lastMouseDownEvent.clientY - pos.top;
|
||||
|
||||
// Figure out what corner we are draging on
|
||||
for (name in resizeHandles) {
|
||||
corner = resizeHandles[name];
|
||||
|
||||
cornerX = target.offsetWidth * corner[0];
|
||||
cornerY = target.offsetHeight * corner[1];
|
||||
|
||||
if (Math.abs(cornerX - relativeX) < 8 && Math.abs(cornerY - relativeY) < 8) {
|
||||
selectedHandle = corner;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove native selection and let the magic begin
|
||||
resizeStarted = true;
|
||||
editor.getDoc().selection.empty();
|
||||
showResizeRect(target, name, lastMouseDownEvent);
|
||||
}
|
||||
|
||||
function nativeControlSelect(e) {
|
||||
var target = e.srcElement;
|
||||
|
||||
if (target != selectedElm) {
|
||||
detachResizeStartListener();
|
||||
|
||||
if (target.id.indexOf('mceResizeHandle') === 0) {
|
||||
e.returnValue = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.nodeName == 'IMG' || target.nodeName == 'TABLE') {
|
||||
hideResizeRect();
|
||||
selectedElm = target;
|
||||
attachEvent(target, 'resizestart', resizeNativeStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function detachResizeStartListener() {
|
||||
detachEvent(selectedElm, 'resizestart', resizeNativeStart);
|
||||
}
|
||||
|
||||
function disableGeckoResize() {
|
||||
try {
|
||||
// Disable object resizing on Gecko
|
||||
editor.getDoc().execCommand('enableObjectResizing', false, false);
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
function controlSelect(elm) {
|
||||
var ctrlRng;
|
||||
|
||||
if (!isIE) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctrlRng = editableDoc.body.createControlRange();
|
||||
|
||||
try {
|
||||
ctrlRng.addElement(elm);
|
||||
ctrlRng.select();
|
||||
return true;
|
||||
} catch (ex) {
|
||||
// Ignore since the element can't be control selected for example a P tag
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
attachEvent(editor.getBody(), 'controlselect', nativeControlSelect);
|
||||
|
||||
editor.on('mousedown', function(e) {
|
||||
lastMouseDownEvent = e;
|
||||
});
|
||||
} else {
|
||||
disableGeckoResize();
|
||||
|
||||
if (Env.ie >= 11) {
|
||||
// TODO: Drag/drop doesn't work
|
||||
editor.on('mouseup', function(e) {
|
||||
var nodeName = e.target.nodeName;
|
||||
|
||||
if (/^(TABLE|IMG|HR)$/.test(nodeName)) {
|
||||
editor.selection.select(e.target, nodeName == 'TABLE');
|
||||
editor.nodeChanged();
|
||||
}
|
||||
});
|
||||
|
||||
editor.dom.bind(editor.getBody(), 'mscontrolselect', function(e) {
|
||||
if (/^(TABLE|IMG|HR)$/.test(e.target.nodeName)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editor.on('nodechange mousedown mouseup ResizeEditor', updateResizeRect);
|
||||
|
||||
// Update resize rect while typing in a table
|
||||
editor.on('keydown keyup', function(e) {
|
||||
if (selectedElm && selectedElm.nodeName == "TABLE") {
|
||||
updateResizeRect(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Hide rect on focusout since it would float on top of windows otherwise
|
||||
//editor.on('focusout', hideResizeRect);
|
||||
});
|
||||
|
||||
function destroy() {
|
||||
selectedElm = selectedElmGhost = null;
|
||||
|
||||
if (isIE) {
|
||||
detachResizeStartListener();
|
||||
detachEvent(editor.getBody(), 'controlselect', nativeControlSelect);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
controlSelect: controlSelect,
|
||||
destroy: destroy
|
||||
};
|
||||
};
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,724 @@
|
||||
/**
|
||||
* DomQuery.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*
|
||||
* Some of this logic is based on jQuery code that is released under
|
||||
* MIT license that grants us to sublicense it under LGPL.
|
||||
*
|
||||
* @ignore-file
|
||||
*/
|
||||
|
||||
/**
|
||||
* @class tinymce.dom.DomQuery
|
||||
*/
|
||||
define("tinymce/dom/DomQuery", [
|
||||
"tinymce/dom/EventUtils",
|
||||
"tinymce/dom/Sizzle"
|
||||
], function(EventUtils, Sizzle) {
|
||||
var doc = document, push = Array.prototype.push, slice = Array.prototype.slice;
|
||||
var rquickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/;
|
||||
var Event = EventUtils.Event;
|
||||
|
||||
function isDefined(obj) {
|
||||
return typeof obj !== "undefined";
|
||||
}
|
||||
|
||||
function isString(obj) {
|
||||
return typeof obj === "string";
|
||||
}
|
||||
|
||||
function createFragment(html) {
|
||||
var frag, node, container;
|
||||
|
||||
container = doc.createElement("div");
|
||||
frag = doc.createDocumentFragment();
|
||||
container.innerHTML = html;
|
||||
|
||||
while ((node = container.firstChild)) {
|
||||
frag.appendChild(node);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function domManipulate(targetNodes, sourceItem, callback) {
|
||||
var i;
|
||||
|
||||
if (typeof sourceItem === "string") {
|
||||
sourceItem = createFragment(sourceItem);
|
||||
} else if (sourceItem.length) {
|
||||
for (i = 0; i < sourceItem.length; i++) {
|
||||
domManipulate(targetNodes, sourceItem[i], callback);
|
||||
}
|
||||
|
||||
return targetNodes;
|
||||
}
|
||||
|
||||
i = targetNodes.length;
|
||||
while (i--) {
|
||||
callback.call(targetNodes[i], sourceItem.parentNode ? sourceItem : sourceItem);
|
||||
}
|
||||
|
||||
return targetNodes;
|
||||
}
|
||||
|
||||
function hasClass(node, className) {
|
||||
return node && className && (' ' + node.className + ' ').indexOf(' ' + className + ' ') !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a map object out of a string that gets separated by a delimiter.
|
||||
*
|
||||
* @method makeMap
|
||||
* @param {String} items Item string to split.
|
||||
* @param {Object} map Optional object to add items to.
|
||||
* @return {Object} name/value object with items as keys.
|
||||
*/
|
||||
function makeMap(items, map) {
|
||||
var i;
|
||||
|
||||
items = items || [];
|
||||
|
||||
if (typeof(items) == "string") {
|
||||
items = items.split(' ');
|
||||
}
|
||||
|
||||
map = map || {};
|
||||
|
||||
i = items.length;
|
||||
while (i--) {
|
||||
map[items[i]] = {};
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
var numericCssMap = makeMap('fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom');
|
||||
|
||||
function DomQuery(selector, context) {
|
||||
return new DomQuery.fn.init(selector, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the specified object with another object.
|
||||
*
|
||||
* @method extend
|
||||
* @param {Object} target Object to extend.
|
||||
* @param {Object..} obj Multiple objects to extend with.
|
||||
* @return {Object} Same as target, the extended object.
|
||||
*/
|
||||
function extend(target) {
|
||||
var args = arguments, arg, i, key;
|
||||
|
||||
for (i = 1; i < args.length; i++) {
|
||||
arg = args[i];
|
||||
|
||||
for (key in arg) {
|
||||
target[key] = arg[key];
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specified object into a real JavaScript array.
|
||||
*
|
||||
* @method toArray
|
||||
* @param {Object} obj Object to convert into array.
|
||||
* @return {Array} Array object based in input.
|
||||
*/
|
||||
function toArray(obj) {
|
||||
var array = [], i, l;
|
||||
|
||||
for (i = 0, l = obj.length; i < l; i++) {
|
||||
array[i] = obj[i];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the specified item inside the array.
|
||||
*
|
||||
* @method inArray
|
||||
* @param {Object} item Item to look for.
|
||||
* @param {Array} array Array to look for item in.
|
||||
* @return {Number} Index of the item or -1.
|
||||
*/
|
||||
function inArray(item, array) {
|
||||
var i;
|
||||
|
||||
if (array.indexOf) {
|
||||
return array.indexOf(item);
|
||||
}
|
||||
|
||||
i = array.length;
|
||||
while (i--) {
|
||||
if (array[i] === item) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true/false if the specified object is an array.
|
||||
*
|
||||
* @method isArray
|
||||
* @param {Object} obj Object to check if it's an array.
|
||||
* @return {Boolean} true/false if the input object is array or not.
|
||||
*/
|
||||
var isArray = Array.isArray || function(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Array]";
|
||||
};
|
||||
|
||||
var whiteSpaceRegExp = /^\s*|\s*$/g;
|
||||
var trim = function(str) {
|
||||
return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes the callback function for each item in array/object. If you return false in the
|
||||
* callback it will break the loop.
|
||||
*
|
||||
* @method each
|
||||
* @param {Object} obj Object to iterate.
|
||||
* @param {function} callback Callback function to execute for each item.
|
||||
*/
|
||||
function each(obj, callback) {
|
||||
var length, key, i, undef, value;
|
||||
|
||||
if (obj) {
|
||||
length = obj.length;
|
||||
|
||||
if (length === undef) {
|
||||
// Loop object items
|
||||
for (key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
value = obj[key];
|
||||
if (callback.call(value, value, key) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Loop array items
|
||||
for (i = 0; i < length; i++) {
|
||||
value = obj[i];
|
||||
if (callback.call(value, value, key) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
DomQuery.fn = DomQuery.prototype = {
|
||||
constructor: DomQuery,
|
||||
selector: "",
|
||||
length: 0,
|
||||
|
||||
init: function(selector, context) {
|
||||
var self = this, match, node;
|
||||
|
||||
if (!selector) {
|
||||
return self;
|
||||
}
|
||||
|
||||
if (selector.nodeType) {
|
||||
self.context = self[0] = selector;
|
||||
self.length = 1;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
if (isString(selector)) {
|
||||
if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
|
||||
match = [null, selector, null];
|
||||
} else {
|
||||
match = rquickExpr.exec(selector);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (match[1]) {
|
||||
node = createFragment(selector).firstChild;
|
||||
while (node) {
|
||||
this.add(node);
|
||||
node = node.nextSibling;
|
||||
}
|
||||
} else {
|
||||
node = doc.getElementById(match[2]);
|
||||
|
||||
if (node.id !== match[2]) {
|
||||
return self.find(selector);
|
||||
}
|
||||
|
||||
self.length = 1;
|
||||
self[0] = node;
|
||||
}
|
||||
} else {
|
||||
return DomQuery(context || document).find(selector);
|
||||
}
|
||||
} else {
|
||||
this.add(selector);
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
toArray: function() {
|
||||
return toArray(this);
|
||||
},
|
||||
|
||||
add: function(items) {
|
||||
var self = this;
|
||||
|
||||
// Force single item into array
|
||||
if (!isArray(items)) {
|
||||
if (items instanceof DomQuery) {
|
||||
self.add(items.toArray());
|
||||
} else {
|
||||
push.call(self, items);
|
||||
}
|
||||
} else {
|
||||
push.apply(self, items);
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
attr: function(name, value) {
|
||||
var self = this;
|
||||
|
||||
if (typeof name === "object") {
|
||||
each(name, function(value, name) {
|
||||
self.attr(name, value);
|
||||
});
|
||||
} else if (isDefined(value)) {
|
||||
this.each(function() {
|
||||
if (this.nodeType === 1) {
|
||||
this.setAttribute(name, value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self[0] && self[0].nodeType === 1 ? self[0].getAttribute(name) : undefined;
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
css: function(name, value) {
|
||||
var self = this;
|
||||
|
||||
if (typeof name === "object") {
|
||||
each(name, function(value, name) {
|
||||
self.css(name, value);
|
||||
});
|
||||
} else {
|
||||
// Camelcase it, if needed
|
||||
name = name.replace(/-(\D)/g, function(a, b) {
|
||||
return b.toUpperCase();
|
||||
});
|
||||
|
||||
if (isDefined(value)) {
|
||||
// Default px suffix on these
|
||||
if (typeof(value) === 'number' && !numericCssMap[name]) {
|
||||
value += 'px';
|
||||
}
|
||||
|
||||
self.each(function() {
|
||||
var style = this.style;
|
||||
|
||||
// IE specific opacity
|
||||
if (name === "opacity" && this.runtimeStyle && typeof(this.runtimeStyle.opacity) === "undefined") {
|
||||
style.filter = value === '' ? '' : "alpha(opacity=" + (value * 100) + ")";
|
||||
}
|
||||
|
||||
try {
|
||||
style[name] = value;
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return self[0] ? self[0].style[name] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
var self = this, node, i = this.length;
|
||||
|
||||
while (i--) {
|
||||
node = self[i];
|
||||
Event.clean(node);
|
||||
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
empty: function() {
|
||||
var self = this, node, i = this.length;
|
||||
|
||||
while (i--) {
|
||||
node = self[i];
|
||||
while (node.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
html: function(value) {
|
||||
var self = this, i;
|
||||
|
||||
if (isDefined(value)) {
|
||||
i = self.length;
|
||||
while (i--) {
|
||||
self[i].innerHTML = value;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return self[0] ? self[0].innerHTML : '';
|
||||
},
|
||||
|
||||
text: function(value) {
|
||||
var self = this, i;
|
||||
|
||||
if (isDefined(value)) {
|
||||
i = self.length;
|
||||
while (i--) {
|
||||
self[i].innerText = self[0].textContent = value;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return self[0] ? self[0].innerText || self[0].textContent : '';
|
||||
},
|
||||
|
||||
append: function() {
|
||||
return domManipulate(this, arguments, function(node) {
|
||||
if (this.nodeType === 1) {
|
||||
this.appendChild(node);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
prepend: function() {
|
||||
return domManipulate(this, arguments, function(node) {
|
||||
if (this.nodeType === 1) {
|
||||
this.insertBefore(node, this.firstChild);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
before: function() {
|
||||
var self = this;
|
||||
|
||||
if (self[0] && self[0].parentNode) {
|
||||
return domManipulate(self, arguments, function(node) {
|
||||
this.parentNode.insertBefore(node, this.nextSibling);
|
||||
});
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
after: function() {
|
||||
var self = this;
|
||||
|
||||
if (self[0] && self[0].parentNode) {
|
||||
return domManipulate(self, arguments, function(node) {
|
||||
this.parentNode.insertBefore(node, this);
|
||||
});
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
appendTo: function(val) {
|
||||
DomQuery(val).append(this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
addClass: function(className) {
|
||||
return this.toggleClass(className, true);
|
||||
},
|
||||
|
||||
removeClass: function(className) {
|
||||
return this.toggleClass(className, false);
|
||||
},
|
||||
|
||||
toggleClass: function(className, state) {
|
||||
var self = this;
|
||||
|
||||
if (className.indexOf(' ') !== -1) {
|
||||
each(className.split(' '), function() {
|
||||
self.toggleClass(this, state);
|
||||
});
|
||||
} else {
|
||||
self.each(function() {
|
||||
var node = this, existingClassName;
|
||||
|
||||
if (hasClass(node, className) !== state) {
|
||||
existingClassName = node.className;
|
||||
|
||||
if (state) {
|
||||
node.className += existingClassName ? ' ' + className : className;
|
||||
} else {
|
||||
node.className = trim((" " + existingClassName + " ").replace(' ' + className + ' ', ' '));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
hasClass: function(className) {
|
||||
return hasClass(this[0], className);
|
||||
},
|
||||
|
||||
each: function(callback) {
|
||||
return each(this, callback);
|
||||
},
|
||||
|
||||
on: function(name, callback) {
|
||||
return this.each(function() {
|
||||
Event.bind(this, name, callback);
|
||||
});
|
||||
},
|
||||
|
||||
off: function(name, callback) {
|
||||
return this.each(function() {
|
||||
Event.unbind(this, name, callback);
|
||||
});
|
||||
},
|
||||
|
||||
show: function() {
|
||||
return this.css('display', '');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
return this.css('display', 'none');
|
||||
},
|
||||
|
||||
slice: function() {
|
||||
return new DomQuery(slice.apply(this, arguments));
|
||||
},
|
||||
|
||||
eq: function(index) {
|
||||
return index === -1 ? this.slice(index) : this.slice(index, +index + 1);
|
||||
},
|
||||
|
||||
first: function() {
|
||||
return this.eq(0);
|
||||
},
|
||||
|
||||
last: function() {
|
||||
return this.eq(-1);
|
||||
},
|
||||
|
||||
replaceWith: function(content) {
|
||||
var self = this;
|
||||
|
||||
if (self[0]) {
|
||||
self[0].parentNode.replaceChild(DomQuery(content)[0], self[0]);
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
wrap: function(wrapper) {
|
||||
wrapper = DomQuery(wrapper)[0];
|
||||
|
||||
return this.each(function() {
|
||||
var self = this, newWrapper = wrapper.cloneNode(false);
|
||||
self.parentNode.insertBefore(newWrapper, self);
|
||||
newWrapper.appendChild(self);
|
||||
});
|
||||
},
|
||||
|
||||
unwrap: function() {
|
||||
return this.each(function() {
|
||||
var self = this, node = self.firstChild, currentNode;
|
||||
|
||||
while (node) {
|
||||
currentNode = node;
|
||||
node = node.nextSibling;
|
||||
self.parentNode.insertBefore(currentNode, self);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
clone: function() {
|
||||
var result = [];
|
||||
|
||||
this.each(function() {
|
||||
result.push(this.cloneNode(true));
|
||||
});
|
||||
|
||||
return DomQuery(result);
|
||||
},
|
||||
|
||||
find: function(selector) {
|
||||
var i, l, ret = [];
|
||||
|
||||
for (i = 0, l = this.length; i < l; i++) {
|
||||
DomQuery.find(selector, this[i], ret);
|
||||
}
|
||||
|
||||
return DomQuery(ret);
|
||||
},
|
||||
|
||||
push: push,
|
||||
sort: [].sort,
|
||||
splice: [].splice
|
||||
};
|
||||
|
||||
// Static members
|
||||
extend(DomQuery, {
|
||||
extend: extend,
|
||||
toArray: toArray,
|
||||
inArray: inArray,
|
||||
isArray: isArray,
|
||||
each: each,
|
||||
trim: trim,
|
||||
makeMap: makeMap,
|
||||
|
||||
// Sizzle
|
||||
find: Sizzle,
|
||||
expr: Sizzle.selectors,
|
||||
unique: Sizzle.uniqueSort,
|
||||
text: Sizzle.getText,
|
||||
isXMLDoc: Sizzle.isXML,
|
||||
contains: Sizzle.contains,
|
||||
filter: function(expr, elems, not) {
|
||||
if (not) {
|
||||
expr = ":not(" + expr + ")";
|
||||
}
|
||||
|
||||
return elems.length === 1 ?
|
||||
DomQuery.find.matchesSelector(elems[0], expr) ? [elems[0]] : [] :
|
||||
DomQuery.find.matches(expr, elems);
|
||||
}
|
||||
});
|
||||
|
||||
function dir(el, prop, until) {
|
||||
var matched = [], cur = el[prop];
|
||||
|
||||
while (cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !DomQuery(cur).is(until))) {
|
||||
if (cur.nodeType === 1) {
|
||||
matched.push(cur);
|
||||
}
|
||||
|
||||
cur = cur[prop];
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
function sibling(n, el, siblingName, nodeType) {
|
||||
var r = [];
|
||||
|
||||
for(; n; n = n[siblingName]) {
|
||||
if ((!nodeType || n.nodeType === nodeType) && n !== el) {
|
||||
r.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
each({
|
||||
parent: function(node) {
|
||||
var parent = node.parentNode;
|
||||
|
||||
return parent && parent.nodeType !== 11 ? parent : null;
|
||||
},
|
||||
|
||||
parents: function(node) {
|
||||
return dir(node, "parentNode");
|
||||
},
|
||||
|
||||
parentsUntil: function(node, until) {
|
||||
return dir(node, "parentNode", until);
|
||||
},
|
||||
|
||||
next: function(node) {
|
||||
return sibling(node, 'nextSibling', 1);
|
||||
},
|
||||
|
||||
prev: function(node) {
|
||||
return sibling(node, 'previousSibling', 1);
|
||||
},
|
||||
|
||||
nextNodes: function(node) {
|
||||
return sibling(node, 'nextSibling');
|
||||
},
|
||||
|
||||
prevNodes: function(node) {
|
||||
return sibling(node, 'previousSibling');
|
||||
},
|
||||
|
||||
children: function(node) {
|
||||
return sibling(node.firstChild, 'nextSibling', 1);
|
||||
},
|
||||
|
||||
contents: function(node) {
|
||||
return toArray((node.nodeName === "iframe" ? node.contentDocument || node.contentWindow.document : node).childNodes);
|
||||
}
|
||||
}, function(name, fn){
|
||||
DomQuery.fn[name] = function(selector) {
|
||||
var self = this, result;
|
||||
|
||||
if (self.length > 1) {
|
||||
throw new Error("DomQuery only supports traverse functions on a single node.");
|
||||
}
|
||||
|
||||
if (self[0]) {
|
||||
result = fn(self[0], selector);
|
||||
}
|
||||
|
||||
result = DomQuery(result);
|
||||
|
||||
if (selector && name !== "parentsUntil") {
|
||||
return result.filter(selector);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
DomQuery.fn.filter = function(selector) {
|
||||
return DomQuery.filter(selector);
|
||||
};
|
||||
|
||||
DomQuery.fn.is = function(selector) {
|
||||
return !!selector && this.filter(selector).length > 0;
|
||||
};
|
||||
|
||||
DomQuery.fn.init.prototype = DomQuery.fn;
|
||||
|
||||
return DomQuery;
|
||||
});
|
||||
@@ -0,0 +1,551 @@
|
||||
/**
|
||||
* EventUtils.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*jshint loopfunc:true*/
|
||||
|
||||
define("tinymce/dom/EventUtils", [], function() {
|
||||
"use strict";
|
||||
|
||||
var eventExpandoPrefix = "mce-data-";
|
||||
var mouseEventRe = /^(?:mouse|contextmenu)|click/;
|
||||
var deprecated = {keyLocation: 1, layerX: 1, layerY: 1, returnValue: 1};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a native event object or just adds the event specific methods on a custom event.
|
||||
*/
|
||||
function fix(originalEvent, data) {
|
||||
var name, event = data || {}, undef;
|
||||
|
||||
// Dummy function that gets replaced on the delegation state functions
|
||||
function returnFalse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Dummy function that gets replaced on the delegation state functions
|
||||
function returnTrue() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Copy all properties from the original event
|
||||
for (name in originalEvent) {
|
||||
// layerX/layerY is deprecated in Chrome and produces a warning
|
||||
if (!deprecated[name]) {
|
||||
event[name] = originalEvent[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize target IE uses srcElement
|
||||
if (!event.target) {
|
||||
event.target = event.srcElement || document;
|
||||
}
|
||||
|
||||
// Calculate pageX/Y if missing and clientX/Y available
|
||||
if (originalEvent && mouseEventRe.test(originalEvent.type) && originalEvent.pageX === undef && originalEvent.clientX !== undef) {
|
||||
var eventDoc = event.target.ownerDocument || document;
|
||||
var doc = eventDoc.documentElement;
|
||||
var body = eventDoc.body;
|
||||
|
||||
event.pageX = originalEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
|
||||
( doc && doc.clientLeft || body && body.clientLeft || 0);
|
||||
|
||||
event.pageY = originalEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0 ) -
|
||||
( doc && doc.clientTop || body && body.clientTop || 0);
|
||||
}
|
||||
|
||||
// Add preventDefault method
|
||||
event.preventDefault = function() {
|
||||
event.isDefaultPrevented = returnTrue;
|
||||
|
||||
// Execute preventDefault on the original event object
|
||||
if (originalEvent) {
|
||||
if (originalEvent.preventDefault) {
|
||||
originalEvent.preventDefault();
|
||||
} else {
|
||||
originalEvent.returnValue = false; // IE
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add stopPropagation
|
||||
event.stopPropagation = function() {
|
||||
event.isPropagationStopped = returnTrue;
|
||||
|
||||
// Execute stopPropagation on the original event object
|
||||
if (originalEvent) {
|
||||
if (originalEvent.stopPropagation) {
|
||||
originalEvent.stopPropagation();
|
||||
} else {
|
||||
originalEvent.cancelBubble = true; // IE
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add stopImmediatePropagation
|
||||
event.stopImmediatePropagation = function() {
|
||||
event.isImmediatePropagationStopped = returnTrue;
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
// Add event delegation states
|
||||
if (!event.isDefaultPrevented) {
|
||||
event.isDefaultPrevented = returnFalse;
|
||||
event.isPropagationStopped = returnFalse;
|
||||
event.isImmediatePropagationStopped = returnFalse;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a DOMContentLoaded event across browsers and executes the callback once the page DOM is initialized.
|
||||
* It will also set/check the domLoaded state of the event_utils instance so ready isn't called multiple times.
|
||||
*/
|
||||
function bindOnReady(win, callback, eventUtils) {
|
||||
var doc = win.document, event = {type: 'ready'};
|
||||
|
||||
if (eventUtils.domLoaded) {
|
||||
callback(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Gets called when the DOM is ready
|
||||
function readyHandler() {
|
||||
if (!eventUtils.domLoaded) {
|
||||
eventUtils.domLoaded = true;
|
||||
callback(event);
|
||||
}
|
||||
}
|
||||
|
||||
function waitForDomLoaded() {
|
||||
if (doc.readyState === "complete") {
|
||||
removeEvent(doc, "readystatechange", waitForDomLoaded);
|
||||
readyHandler();
|
||||
}
|
||||
}
|
||||
|
||||
function tryScroll() {
|
||||
try {
|
||||
// If IE is used, use the trick by Diego Perini licensed under MIT by request to the author.
|
||||
// http://javascript.nwbox.com/IEContentLoaded/
|
||||
doc.documentElement.doScroll("left");
|
||||
} catch (ex) {
|
||||
setTimeout(tryScroll, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
readyHandler();
|
||||
}
|
||||
|
||||
// Use W3C method
|
||||
if (doc.addEventListener) {
|
||||
if (doc.readyState === "complete") {
|
||||
readyHandler();
|
||||
} else {
|
||||
addEvent(win, 'DOMContentLoaded', readyHandler);
|
||||
}
|
||||
} else {
|
||||
// Use IE method
|
||||
addEvent(doc, "readystatechange", waitForDomLoaded);
|
||||
|
||||
// Wait until we can scroll, when we can the DOM is initialized
|
||||
if (doc.documentElement.doScroll && win === win.top) {
|
||||
tryScroll();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if any of the above methods should fail for some odd reason
|
||||
addEvent(win, 'load', readyHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class enables you to bind/unbind native events to elements and normalize it's behavior across browsers.
|
||||
*/
|
||||
function EventUtils() {
|
||||
var self = this, events = {}, count, expando, hasFocusIn, hasMouseEnterLeave, mouseEnterLeave;
|
||||
|
||||
expando = eventExpandoPrefix + (+new Date()).toString(32);
|
||||
hasMouseEnterLeave = "onmouseenter" in document.documentElement;
|
||||
hasFocusIn = "onfocusin" in document.documentElement;
|
||||
mouseEnterLeave = {mouseenter: 'mouseover', mouseleave: 'mouseout'};
|
||||
count = 1;
|
||||
|
||||
// State if the DOMContentLoaded was executed or not
|
||||
self.domLoaded = false;
|
||||
self.events = events;
|
||||
|
||||
/**
|
||||
* Executes all event handler callbacks for a specific event.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} evt Event object.
|
||||
* @param {String} id Expando id value to look for.
|
||||
*/
|
||||
function executeHandlers(evt, id) {
|
||||
var callbackList, i, l, callback, container = events[id];
|
||||
|
||||
callbackList = container && container[evt.type];
|
||||
if (callbackList) {
|
||||
for (i = 0, l = callbackList.length; i < l; i++) {
|
||||
callback = callbackList[i];
|
||||
|
||||
// Check if callback exists might be removed if a unbind is called inside the callback
|
||||
if (callback && callback.func.call(callback.scope, evt) === false) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
// Should we stop propagation to immediate listeners
|
||||
if (evt.isImmediatePropagationStopped()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a callback to an event on the specified target.
|
||||
*
|
||||
* @method bind
|
||||
* @param {Object} target Target node/window or custom object.
|
||||
* @param {String} names Name of the event to bind.
|
||||
* @param {function} callback Callback function to execute when the event occurs.
|
||||
* @param {Object} scope Scope to call the callback function on, defaults to target.
|
||||
* @return {function} Callback function that got bound.
|
||||
*/
|
||||
self.bind = function(target, names, callback, scope) {
|
||||
var id, callbackList, i, name, fakeName, nativeHandler, capture, win = window;
|
||||
|
||||
// Native event handler function patches the event and executes the callbacks for the expando
|
||||
function defaultNativeHandler(evt) {
|
||||
executeHandlers(fix(evt || win.event), id);
|
||||
}
|
||||
|
||||
// Don't bind to text nodes or comments
|
||||
if (!target || target.nodeType === 3 || target.nodeType === 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or get events id for the target
|
||||
if (!target[expando]) {
|
||||
id = count++;
|
||||
target[expando] = id;
|
||||
events[id] = {};
|
||||
} else {
|
||||
id = target[expando];
|
||||
}
|
||||
|
||||
// Setup the specified scope or use the target as a default
|
||||
scope = scope || target;
|
||||
|
||||
// Split names and bind each event, enables you to bind multiple events with one call
|
||||
names = names.split(' ');
|
||||
i = names.length;
|
||||
while (i--) {
|
||||
name = names[i];
|
||||
nativeHandler = defaultNativeHandler;
|
||||
fakeName = capture = false;
|
||||
|
||||
// Use ready instead of DOMContentLoaded
|
||||
if (name === "DOMContentLoaded") {
|
||||
name = "ready";
|
||||
}
|
||||
|
||||
// DOM is already ready
|
||||
if (self.domLoaded && name === "ready" && target.readyState == 'complete') {
|
||||
callback.call(scope, fix({type: name}));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle mouseenter/mouseleaver
|
||||
if (!hasMouseEnterLeave) {
|
||||
fakeName = mouseEnterLeave[name];
|
||||
|
||||
if (fakeName) {
|
||||
nativeHandler = function(evt) {
|
||||
var current, related;
|
||||
|
||||
current = evt.currentTarget;
|
||||
related = evt.relatedTarget;
|
||||
|
||||
// Check if related is inside the current target if it's not then the event should
|
||||
// be ignored since it's a mouseover/mouseout inside the element
|
||||
if (related && current.contains) {
|
||||
// Use contains for performance
|
||||
related = current.contains(related);
|
||||
} else {
|
||||
while (related && related !== current) {
|
||||
related = related.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Fire fake event
|
||||
if (!related) {
|
||||
evt = fix(evt || win.event);
|
||||
evt.type = evt.type === 'mouseout' ? 'mouseleave' : 'mouseenter';
|
||||
evt.target = current;
|
||||
executeHandlers(evt, id);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fake bubbeling of focusin/focusout
|
||||
if (!hasFocusIn && (name === "focusin" || name === "focusout")) {
|
||||
capture = true;
|
||||
fakeName = name === "focusin" ? "focus" : "blur";
|
||||
nativeHandler = function(evt) {
|
||||
evt = fix(evt || win.event);
|
||||
evt.type = evt.type === 'focus' ? 'focusin' : 'focusout';
|
||||
executeHandlers(evt, id);
|
||||
};
|
||||
}
|
||||
|
||||
// Setup callback list and bind native event
|
||||
callbackList = events[id][name];
|
||||
if (!callbackList) {
|
||||
events[id][name] = callbackList = [{func: callback, scope: scope}];
|
||||
callbackList.fakeName = fakeName;
|
||||
callbackList.capture = capture;
|
||||
|
||||
// Add the nativeHandler to the callback list so that we can later unbind it
|
||||
callbackList.nativeHandler = nativeHandler;
|
||||
|
||||
// Check if the target has native events support
|
||||
|
||||
if (name === "ready") {
|
||||
bindOnReady(target, nativeHandler, self);
|
||||
} else {
|
||||
addEvent(target, fakeName || name, nativeHandler, capture);
|
||||
}
|
||||
} else {
|
||||
if (name === "ready" && self.domLoaded) {
|
||||
callback({type: name});
|
||||
} else {
|
||||
// If it already has an native handler then just push the callback
|
||||
callbackList.push({func: callback, scope: scope});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target = callbackList = 0; // Clean memory for IE
|
||||
|
||||
return callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbinds the specified event by name, name and callback or all events on the target.
|
||||
*
|
||||
* @method unbind
|
||||
* @param {Object} target Target node/window or custom object.
|
||||
* @param {String} names Optional event name to unbind.
|
||||
* @param {function} callback Optional callback function to unbind.
|
||||
* @return {EventUtils} Event utils instance.
|
||||
*/
|
||||
self.unbind = function(target, names, callback) {
|
||||
var id, callbackList, i, ci, name, eventMap;
|
||||
|
||||
// Don't bind to text nodes or comments
|
||||
if (!target || target.nodeType === 3 || target.nodeType === 8) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Unbind event or events if the target has the expando
|
||||
id = target[expando];
|
||||
if (id) {
|
||||
eventMap = events[id];
|
||||
|
||||
// Specific callback
|
||||
if (names) {
|
||||
names = names.split(' ');
|
||||
i = names.length;
|
||||
while (i--) {
|
||||
name = names[i];
|
||||
callbackList = eventMap[name];
|
||||
|
||||
// Unbind the event if it exists in the map
|
||||
if (callbackList) {
|
||||
// Remove specified callback
|
||||
if (callback) {
|
||||
ci = callbackList.length;
|
||||
while (ci--) {
|
||||
if (callbackList[ci].func === callback) {
|
||||
var nativeHandler = callbackList.nativeHandler;
|
||||
|
||||
// Clone callbackList since unbind inside a callback would otherwise break the handlers loop
|
||||
callbackList = callbackList.slice(0, ci).concat(callbackList.slice(ci + 1));
|
||||
callbackList.nativeHandler = nativeHandler;
|
||||
|
||||
eventMap[name] = callbackList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all callbacks if there isn't a specified callback or there is no callbacks left
|
||||
if (!callback || callbackList.length === 0) {
|
||||
delete eventMap[name];
|
||||
removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// All events for a specific element
|
||||
for (name in eventMap) {
|
||||
callbackList = eventMap[name];
|
||||
removeEvent(target, callbackList.fakeName || name, callbackList.nativeHandler, callbackList.capture);
|
||||
}
|
||||
|
||||
eventMap = {};
|
||||
}
|
||||
|
||||
// Check if object is empty, if it isn't then we won't remove the expando map
|
||||
for (name in eventMap) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Delete event object
|
||||
delete events[id];
|
||||
|
||||
// Remove expando from target
|
||||
try {
|
||||
// IE will fail here since it can't delete properties from window
|
||||
delete target[expando];
|
||||
} catch (ex) {
|
||||
// IE will set it to null
|
||||
target[expando] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fires the specified event on the specified target.
|
||||
*
|
||||
* @method fire
|
||||
* @param {Object} target Target node/window or custom object.
|
||||
* @param {String} name Event name to fire.
|
||||
* @param {Object} args Optional arguments to send to the observers.
|
||||
* @return {EventUtils} Event utils instance.
|
||||
*/
|
||||
self.fire = function(target, name, args) {
|
||||
var id;
|
||||
|
||||
// Don't bind to text nodes or comments
|
||||
if (!target || target.nodeType === 3 || target.nodeType === 8) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Build event object by patching the args
|
||||
args = fix(null, args);
|
||||
args.type = name;
|
||||
args.target = target;
|
||||
|
||||
do {
|
||||
// Found an expando that means there is listeners to execute
|
||||
id = target[expando];
|
||||
if (id) {
|
||||
executeHandlers(args, id);
|
||||
}
|
||||
|
||||
// Walk up the DOM
|
||||
target = target.parentNode || target.ownerDocument || target.defaultView || target.parentWindow;
|
||||
} while (target && !args.isPropagationStopped());
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all bound event listeners for the specified target. This will also remove any bound
|
||||
* listeners to child nodes within that target.
|
||||
*
|
||||
* @method clean
|
||||
* @param {Object} target Target node/window object.
|
||||
* @return {EventUtils} Event utils instance.
|
||||
*/
|
||||
self.clean = function(target) {
|
||||
var i, children, unbind = self.unbind;
|
||||
|
||||
// Don't bind to text nodes or comments
|
||||
if (!target || target.nodeType === 3 || target.nodeType === 8) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Unbind any element on the specificed target
|
||||
if (target[expando]) {
|
||||
unbind(target);
|
||||
}
|
||||
|
||||
// Target doesn't have getElementsByTagName it's probably a window object then use it's document to find the children
|
||||
if (!target.getElementsByTagName) {
|
||||
target = target.document;
|
||||
}
|
||||
|
||||
// Remove events from each child element
|
||||
if (target && target.getElementsByTagName) {
|
||||
unbind(target);
|
||||
|
||||
children = target.getElementsByTagName('*');
|
||||
i = children.length;
|
||||
while (i--) {
|
||||
target = children[i];
|
||||
|
||||
if (target[expando]) {
|
||||
unbind(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the event object. Call this on IE to remove memory leaks.
|
||||
*/
|
||||
self.destroy = function() {
|
||||
events = {};
|
||||
};
|
||||
|
||||
// Legacy function for canceling events
|
||||
self.cancel = function(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
EventUtils.Event = new EventUtils();
|
||||
EventUtils.Event.bind(window, 'ready', function() {});
|
||||
|
||||
return EventUtils;
|
||||
});
|
||||
@@ -0,0 +1,778 @@
|
||||
/**
|
||||
* Range.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
define("tinymce/dom/Range", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
// Range constructor
|
||||
function Range(dom) {
|
||||
var t = 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 setStart(n, o) {
|
||||
_setEndPoint(TRUE, n, o);
|
||||
}
|
||||
|
||||
function setEnd(n, o) {
|
||||
_setEndPoint(FALSE, n, o);
|
||||
}
|
||||
|
||||
function setStartBefore(n) {
|
||||
setStart(n.parentNode, nodeIndex(n));
|
||||
}
|
||||
|
||||
function setStartAfter(n) {
|
||||
setStart(n.parentNode, nodeIndex(n) + 1);
|
||||
}
|
||||
|
||||
function setEndBefore(n) {
|
||||
setEnd(n.parentNode, nodeIndex(n));
|
||||
}
|
||||
|
||||
function setEndAfter(n) {
|
||||
setEnd(n.parentNode, nodeIndex(n) + 1);
|
||||
}
|
||||
|
||||
function collapse(ts) {
|
||||
if (ts) {
|
||||
t[END_CONTAINER] = t[START_CONTAINER];
|
||||
t[END_OFFSET] = t[START_OFFSET];
|
||||
} else {
|
||||
t[START_CONTAINER] = t[END_CONTAINER];
|
||||
t[START_OFFSET] = t[END_OFFSET];
|
||||
}
|
||||
|
||||
t.collapsed = TRUE;
|
||||
}
|
||||
|
||||
function selectNode(n) {
|
||||
setStartBefore(n);
|
||||
setEndAfter(n);
|
||||
}
|
||||
|
||||
function selectNodeContents(n) {
|
||||
setStart(n, 0);
|
||||
setEnd(n, n.nodeType === 1 ? n.childNodes.length : n.nodeValue.length);
|
||||
}
|
||||
|
||||
function compareBoundaryPoints(h, r) {
|
||||
var sc = t[START_CONTAINER], so = t[START_OFFSET], ec = t[END_CONTAINER], eo = t[END_OFFSET],
|
||||
rsc = r.startContainer, rso = r.startOffset, rec = r.endContainer, reo = r.endOffset;
|
||||
|
||||
// Check START_TO_START
|
||||
if (h === 0) {
|
||||
return _compareBoundaryPoints(sc, so, rsc, rso);
|
||||
}
|
||||
|
||||
// Check START_TO_END
|
||||
if (h === 1) {
|
||||
return _compareBoundaryPoints(ec, eo, rsc, rso);
|
||||
}
|
||||
|
||||
// Check END_TO_END
|
||||
if (h === 2) {
|
||||
return _compareBoundaryPoints(ec, eo, rec, reo);
|
||||
}
|
||||
|
||||
// Check END_TO_START
|
||||
if (h === 3) {
|
||||
return _compareBoundaryPoints(sc, so, rec, reo);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteContents() {
|
||||
_traverse(DELETE);
|
||||
}
|
||||
|
||||
function extractContents() {
|
||||
return _traverse(EXTRACT);
|
||||
}
|
||||
|
||||
function cloneContents() {
|
||||
return _traverse(CLONE);
|
||||
}
|
||||
|
||||
function insertNode(n) {
|
||||
var startContainer = this[START_CONTAINER],
|
||||
startOffset = this[START_OFFSET], nn, o;
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
if (o) {
|
||||
startContainer.insertBefore(n, o);
|
||||
} else {
|
||||
if (startContainer.nodeType == 3) {
|
||||
dom.insertAfter(n, startContainer);
|
||||
} else {
|
||||
startContainer.appendChild(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function surroundContents(n) {
|
||||
var f = t.extractContents();
|
||||
|
||||
t.insertNode(n);
|
||||
n.appendChild(f);
|
||||
t.selectNode(n);
|
||||
}
|
||||
|
||||
function cloneRange() {
|
||||
return extend(new Range(dom), {
|
||||
startContainer: t[START_CONTAINER],
|
||||
startOffset: t[START_OFFSET],
|
||||
endContainer: t[END_CONTAINER],
|
||||
endOffset: t[END_OFFSET],
|
||||
collapsed: t.collapsed,
|
||||
commonAncestorContainer: t.commonAncestorContainer
|
||||
});
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
function _getSelectedNode(container, offset) {
|
||||
var child;
|
||||
|
||||
if (container.nodeType == 3 /* TEXT_NODE */) {
|
||||
return container;
|
||||
}
|
||||
|
||||
if (offset < 0) {
|
||||
return container;
|
||||
}
|
||||
|
||||
child = container.firstChild;
|
||||
while (child && offset > 0) {
|
||||
--offset;
|
||||
child = child.nextSibling;
|
||||
}
|
||||
|
||||
if (child) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
function _isCollapsed() {
|
||||
return (t[START_CONTAINER] == t[END_CONTAINER] && t[START_OFFSET] == t[END_OFFSET]);
|
||||
}
|
||||
|
||||
function _compareBoundaryPoints(containerA, offsetA, containerB, offsetB) {
|
||||
var c, offsetC, n, cmnRoot, childA, childB;
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if (offsetA < offsetB) {
|
||||
return -1; // before
|
||||
}
|
||||
|
||||
return 1; // after
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (c) {
|
||||
offsetC = 0;
|
||||
n = containerA.firstChild;
|
||||
|
||||
while (n != c && offsetC < offsetA) {
|
||||
offsetC++;
|
||||
n = n.nextSibling;
|
||||
}
|
||||
|
||||
if (offsetA <= offsetC) {
|
||||
return -1; // before
|
||||
}
|
||||
|
||||
return 1; // after
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (c) {
|
||||
offsetC = 0;
|
||||
n = containerB.firstChild;
|
||||
|
||||
while (n != c && offsetC < offsetB) {
|
||||
offsetC++;
|
||||
n = n.nextSibling;
|
||||
}
|
||||
|
||||
if (offsetC < offsetB) {
|
||||
return -1; // before
|
||||
}
|
||||
|
||||
return 1; // after
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
while (childA && childA.parentNode != cmnRoot) {
|
||||
childA = childA.parentNode;
|
||||
}
|
||||
|
||||
if (!childA) {
|
||||
childA = cmnRoot;
|
||||
}
|
||||
|
||||
childB = containerB;
|
||||
while (childB && childB.parentNode != cmnRoot) {
|
||||
childB = childB.parentNode;
|
||||
}
|
||||
|
||||
if (!childB) {
|
||||
childB = cmnRoot;
|
||||
}
|
||||
|
||||
if (childA == childB) {
|
||||
return 0; // equal
|
||||
}
|
||||
|
||||
n = cmnRoot.firstChild;
|
||||
while (n) {
|
||||
if (n == childA) {
|
||||
return -1; // before
|
||||
}
|
||||
|
||||
if (n == childB) {
|
||||
return 1; // after
|
||||
}
|
||||
|
||||
n = n.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
function _setEndPoint(st, n, o) {
|
||||
var ec, sc;
|
||||
|
||||
if (st) {
|
||||
t[START_CONTAINER] = n;
|
||||
t[START_OFFSET] = o;
|
||||
} else {
|
||||
t[END_CONTAINER] = n;
|
||||
t[END_OFFSET] = o;
|
||||
}
|
||||
|
||||
// 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 = t[END_CONTAINER];
|
||||
while (ec.parentNode) {
|
||||
ec = ec.parentNode;
|
||||
}
|
||||
|
||||
sc = t[START_CONTAINER];
|
||||
while (sc.parentNode) {
|
||||
sc = sc.parentNode;
|
||||
}
|
||||
|
||||
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(t[START_CONTAINER], t[START_OFFSET], t[END_CONTAINER], t[END_OFFSET]) > 0) {
|
||||
t.collapse(st);
|
||||
}
|
||||
} else {
|
||||
t.collapse(st);
|
||||
}
|
||||
|
||||
t.collapsed = _isCollapsed();
|
||||
t.commonAncestorContainer = dom.findCommonAncestor(t[START_CONTAINER], t[END_CONTAINER]);
|
||||
}
|
||||
|
||||
function _traverse(how) {
|
||||
var c, endContainerDepth = 0, startContainerDepth = 0, p, depthDiff, startNode, endNode, sp, ep;
|
||||
|
||||
if (t[START_CONTAINER] == t[END_CONTAINER]) {
|
||||
return _traverseSameContainer(how);
|
||||
}
|
||||
|
||||
for (c = t[END_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
|
||||
if (p == t[START_CONTAINER]) {
|
||||
return _traverseCommonStartContainer(c, how);
|
||||
}
|
||||
|
||||
++endContainerDepth;
|
||||
}
|
||||
|
||||
for (c = t[START_CONTAINER], p = c.parentNode; p; c = p, p = p.parentNode) {
|
||||
if (p == t[END_CONTAINER]) {
|
||||
return _traverseCommonEndContainer(c, how);
|
||||
}
|
||||
|
||||
++startContainerDepth;
|
||||
}
|
||||
|
||||
depthDiff = startContainerDepth - endContainerDepth;
|
||||
|
||||
startNode = t[START_CONTAINER];
|
||||
while (depthDiff > 0) {
|
||||
startNode = startNode.parentNode;
|
||||
depthDiff--;
|
||||
}
|
||||
|
||||
endNode = t[END_CONTAINER];
|
||||
while (depthDiff < 0) {
|
||||
endNode = endNode.parentNode;
|
||||
depthDiff++;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 (t[START_OFFSET] == t[END_OFFSET]) {
|
||||
return frag;
|
||||
}
|
||||
|
||||
// Text node needs special case handling
|
||||
if (t[START_CONTAINER].nodeType == 3 /* TEXT_NODE */) {
|
||||
// get the substring
|
||||
s = t[START_CONTAINER].nodeValue;
|
||||
sub = s.substring(t[START_OFFSET], t[END_OFFSET]);
|
||||
|
||||
// set the original text node to its new value
|
||||
if (how != CLONE) {
|
||||
n = t[START_CONTAINER];
|
||||
start = t[START_OFFSET];
|
||||
len = t[END_OFFSET] - t[START_OFFSET];
|
||||
|
||||
if (start === 0 && len >= n.nodeValue.length - 1) {
|
||||
n.parentNode.removeChild(n);
|
||||
} else {
|
||||
n.deleteData(start, len);
|
||||
}
|
||||
|
||||
// Nothing is partially selected, so collapse to start point
|
||||
t.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(t[START_CONTAINER], t[START_OFFSET]);
|
||||
cnt = t[END_OFFSET] - t[START_OFFSET];
|
||||
|
||||
while (n && cnt > 0) {
|
||||
sibling = n.nextSibling;
|
||||
xferNode = _traverseFullySelected(n, how);
|
||||
|
||||
if (frag) {
|
||||
frag.appendChild(xferNode);
|
||||
}
|
||||
|
||||
--cnt;
|
||||
n = sibling;
|
||||
}
|
||||
|
||||
// Nothing is partially selected, so collapse to start point
|
||||
if (how != CLONE) {
|
||||
t.collapse(TRUE);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function _traverseCommonStartContainer(endAncestor, how) {
|
||||
var frag, n, endIdx, cnt, sibling, xferNode;
|
||||
|
||||
if (how != DELETE) {
|
||||
frag = createDocumentFragment();
|
||||
}
|
||||
|
||||
n = _traverseRightBoundary(endAncestor, how);
|
||||
|
||||
if (frag) {
|
||||
frag.appendChild(n);
|
||||
}
|
||||
|
||||
endIdx = nodeIndex(endAncestor);
|
||||
cnt = endIdx - t[START_OFFSET];
|
||||
|
||||
if (cnt <= 0) {
|
||||
// Collapse to just before the endAncestor, which
|
||||
// is partially selected.
|
||||
if (how != CLONE) {
|
||||
t.setEndBefore(endAncestor);
|
||||
t.collapse(FALSE);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
n = endAncestor.previousSibling;
|
||||
while (cnt > 0) {
|
||||
sibling = n.previousSibling;
|
||||
xferNode = _traverseFullySelected(n, how);
|
||||
|
||||
if (frag) {
|
||||
frag.insertBefore(xferNode, frag.firstChild);
|
||||
}
|
||||
|
||||
--cnt;
|
||||
n = sibling;
|
||||
}
|
||||
|
||||
// Collapse to just before the endAncestor, which
|
||||
// is partially selected.
|
||||
if (how != CLONE) {
|
||||
t.setEndBefore(endAncestor);
|
||||
t.collapse(FALSE);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function _traverseCommonEndContainer(startAncestor, how) {
|
||||
var frag, startIdx, n, cnt, sibling, xferNode;
|
||||
|
||||
if (how != DELETE) {
|
||||
frag = createDocumentFragment();
|
||||
}
|
||||
|
||||
n = _traverseLeftBoundary(startAncestor, how);
|
||||
if (frag) {
|
||||
frag.appendChild(n);
|
||||
}
|
||||
|
||||
startIdx = nodeIndex(startAncestor);
|
||||
++startIdx; // Because we already traversed it
|
||||
|
||||
cnt = t[END_OFFSET] - startIdx;
|
||||
n = startAncestor.nextSibling;
|
||||
while (n && cnt > 0) {
|
||||
sibling = n.nextSibling;
|
||||
xferNode = _traverseFullySelected(n, how);
|
||||
|
||||
if (frag) {
|
||||
frag.appendChild(xferNode);
|
||||
}
|
||||
|
||||
--cnt;
|
||||
n = sibling;
|
||||
}
|
||||
|
||||
if (how != CLONE) {
|
||||
t.setStartAfter(startAncestor);
|
||||
t.collapse(TRUE);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function _traverseCommonAncestors(startAncestor, endAncestor, how) {
|
||||
var n, frag, commonParent, startOffset, endOffset, cnt, sibling, nextSibling;
|
||||
|
||||
if (how != DELETE) {
|
||||
frag = createDocumentFragment();
|
||||
}
|
||||
|
||||
n = _traverseLeftBoundary(startAncestor, how);
|
||||
if (frag) {
|
||||
frag.appendChild(n);
|
||||
}
|
||||
|
||||
commonParent = startAncestor.parentNode;
|
||||
startOffset = nodeIndex(startAncestor);
|
||||
endOffset = nodeIndex(endAncestor);
|
||||
++startOffset;
|
||||
|
||||
cnt = endOffset - startOffset;
|
||||
sibling = startAncestor.nextSibling;
|
||||
|
||||
while (cnt > 0) {
|
||||
nextSibling = sibling.nextSibling;
|
||||
n = _traverseFullySelected(sibling, how);
|
||||
|
||||
if (frag) {
|
||||
frag.appendChild(n);
|
||||
}
|
||||
|
||||
sibling = nextSibling;
|
||||
--cnt;
|
||||
}
|
||||
|
||||
n = _traverseRightBoundary(endAncestor, how);
|
||||
|
||||
if (frag) {
|
||||
frag.appendChild(n);
|
||||
}
|
||||
|
||||
if (how != CLONE) {
|
||||
t.setStartAfter(startAncestor);
|
||||
t.collapse(TRUE);
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
function _traverseRightBoundary(root, how) {
|
||||
var next = _getSelectedNode(t[END_CONTAINER], t[END_OFFSET] - 1), parent, clonedParent;
|
||||
var prevSibling, clonedChild, clonedGrandParent, isFullySelected = next != t[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;
|
||||
}
|
||||
|
||||
if (parent == root) {
|
||||
return clonedParent;
|
||||
}
|
||||
|
||||
next = parent.previousSibling;
|
||||
parent = parent.parentNode;
|
||||
|
||||
clonedGrandParent = _traverseNode(parent, FALSE, FALSE, how);
|
||||
|
||||
if (how != DELETE) {
|
||||
clonedGrandParent.appendChild(clonedParent);
|
||||
}
|
||||
|
||||
clonedParent = clonedGrandParent;
|
||||
}
|
||||
}
|
||||
|
||||
function _traverseLeftBoundary(root, how) {
|
||||
var next = _getSelectedNode(t[START_CONTAINER], t[START_OFFSET]), isFullySelected = next != t[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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
clonedParent = clonedGrandParent;
|
||||
}
|
||||
}
|
||||
|
||||
function _traverseNode(n, isFullySelected, isLeft, how) {
|
||||
var txtValue, newNodeValue, oldNodeValue, offset, newNode;
|
||||
|
||||
if (isFullySelected) {
|
||||
return _traverseFullySelected(n, how);
|
||||
}
|
||||
|
||||
if (n.nodeType == 3 /* TEXT_NODE */) {
|
||||
txtValue = n.nodeValue;
|
||||
|
||||
if (isLeft) {
|
||||
offset = t[START_OFFSET];
|
||||
newNodeValue = txtValue.substring(offset);
|
||||
oldNodeValue = txtValue.substring(0, offset);
|
||||
} else {
|
||||
offset = t[END_OFFSET];
|
||||
newNodeValue = txtValue.substring(0, offset);
|
||||
oldNodeValue = txtValue.substring(offset);
|
||||
}
|
||||
|
||||
if (how != CLONE) {
|
||||
n.nodeValue = oldNodeValue;
|
||||
}
|
||||
|
||||
if (how == DELETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
newNode = dom.clone(n, FALSE);
|
||||
newNode.nodeValue = newNodeValue;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
if (how == DELETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
return dom.clone(n, FALSE);
|
||||
}
|
||||
|
||||
function _traverseFullySelected(n, how) {
|
||||
if (how != DELETE) {
|
||||
return how == CLONE ? dom.clone(n, TRUE) : n;
|
||||
}
|
||||
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
|
||||
function toStringIE() {
|
||||
return dom.create('body', null, cloneContents()).outerText;
|
||||
}
|
||||
|
||||
extend(t, {
|
||||
// Inital 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,
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
// 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();
|
||||
};
|
||||
|
||||
return Range;
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Range.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* RangeUtils
|
||||
*
|
||||
* @class tinymce.dom.RangeUtils
|
||||
* @private
|
||||
*/
|
||||
define("tinymce/dom/RangeUtils", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var each = Tools.each;
|
||||
|
||||
function RangeUtils(dom) {
|
||||
/**
|
||||
* Walks the specified range like object and executes the callback for each sibling collection it finds.
|
||||
*
|
||||
* @method walk
|
||||
* @param {Object} rng Range like object.
|
||||
* @param {function} callback Callback function to execute for each sibling collection.
|
||||
*/
|
||||
this.walk = function(rng, callback) {
|
||||
var startContainer = rng.startContainer,
|
||||
startOffset = rng.startOffset,
|
||||
endContainer = rng.endContainer,
|
||||
endOffset = rng.endOffset,
|
||||
ancestor, startPoint,
|
||||
endPoint, node, parent, siblings, nodes;
|
||||
|
||||
// Handle table cell selection the table plugin enables
|
||||
// you to fake select table cells and perform formatting actions on them
|
||||
nodes = dom.select('td.mce-item-selected,th.mce-item-selected');
|
||||
if (nodes.length > 0) {
|
||||
each(nodes, function(node) {
|
||||
callback([node]);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes start/end text node if they are out side the range
|
||||
*
|
||||
* @private
|
||||
* @param {Array} nodes Nodes to exclude items from.
|
||||
* @return {Array} Array with nodes excluding the start/end container if needed.
|
||||
*/
|
||||
function exclude(nodes) {
|
||||
var node;
|
||||
|
||||
// First node is excluded
|
||||
node = nodes[0];
|
||||
if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
|
||||
nodes.splice(0, 1);
|
||||
}
|
||||
|
||||
// Last node is excluded
|
||||
node = nodes[nodes.length - 1];
|
||||
if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
|
||||
nodes.splice(nodes.length - 1, 1);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects siblings
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node Node to collect siblings from.
|
||||
* @param {String} name Name of the sibling to check for.
|
||||
* @return {Array} Array of collected siblings.
|
||||
*/
|
||||
function collectSiblings(node, name, end_node) {
|
||||
var siblings = [];
|
||||
|
||||
for (; node && node != end_node; node = node[name]) {
|
||||
siblings.push(node);
|
||||
}
|
||||
|
||||
return siblings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an end point this is the node just before the common ancestor root.
|
||||
*
|
||||
* @private
|
||||
* @param {Node} node Node to start at.
|
||||
* @param {Node} root Root/ancestor element to stop just before.
|
||||
* @return {Node} Node just before the root element.
|
||||
*/
|
||||
function findEndPoint(node, root) {
|
||||
do {
|
||||
if (node.parentNode == root) {
|
||||
return node;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
} while(node);
|
||||
}
|
||||
|
||||
function walkBoundary(start_node, end_node, next) {
|
||||
var siblingName = next ? 'nextSibling' : 'previousSibling';
|
||||
|
||||
for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
|
||||
parent = node.parentNode;
|
||||
siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
|
||||
|
||||
if (siblings.length) {
|
||||
if (!next) {
|
||||
siblings.reverse();
|
||||
}
|
||||
|
||||
callback(exclude(siblings));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If index based start position then resolve it
|
||||
if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
|
||||
startContainer = startContainer.childNodes[startOffset];
|
||||
}
|
||||
|
||||
// If index based end position then resolve it
|
||||
if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
|
||||
endContainer = endContainer.childNodes[Math.min(endOffset - 1, endContainer.childNodes.length - 1)];
|
||||
}
|
||||
|
||||
// Same container
|
||||
if (startContainer == endContainer) {
|
||||
return callback(exclude([startContainer]));
|
||||
}
|
||||
|
||||
// Find common ancestor and end points
|
||||
ancestor = dom.findCommonAncestor(startContainer, endContainer);
|
||||
|
||||
// Process left side
|
||||
for (node = startContainer; node; node = node.parentNode) {
|
||||
if (node === endContainer) {
|
||||
return walkBoundary(startContainer, ancestor, true);
|
||||
}
|
||||
|
||||
if (node === ancestor) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process right side
|
||||
for (node = endContainer; node; node = node.parentNode) {
|
||||
if (node === startContainer) {
|
||||
return walkBoundary(endContainer, ancestor);
|
||||
}
|
||||
|
||||
if (node === ancestor) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find start/end point
|
||||
startPoint = findEndPoint(startContainer, ancestor) || startContainer;
|
||||
endPoint = findEndPoint(endContainer, ancestor) || endContainer;
|
||||
|
||||
// Walk left leaf
|
||||
walkBoundary(startContainer, startPoint, true);
|
||||
|
||||
// Walk the middle from start to end point
|
||||
siblings = collectSiblings(
|
||||
startPoint == startContainer ? startPoint : startPoint.nextSibling,
|
||||
'nextSibling',
|
||||
endPoint == endContainer ? endPoint.nextSibling : endPoint
|
||||
);
|
||||
|
||||
if (siblings.length) {
|
||||
callback(exclude(siblings));
|
||||
}
|
||||
|
||||
// Walk right leaf
|
||||
walkBoundary(endContainer, endPoint);
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits the specified range at it's start/end points.
|
||||
*
|
||||
* @private
|
||||
* @param {Range/RangeObject} rng Range to split.
|
||||
* @return {Object} Range position object.
|
||||
*/
|
||||
this.split = function(rng) {
|
||||
var startContainer = rng.startContainer,
|
||||
startOffset = rng.startOffset,
|
||||
endContainer = rng.endContainer,
|
||||
endOffset = rng.endOffset;
|
||||
|
||||
function splitText(node, offset) {
|
||||
return node.splitText(offset);
|
||||
}
|
||||
|
||||
// Handle single text node
|
||||
if (startContainer == endContainer && startContainer.nodeType == 3) {
|
||||
if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
|
||||
endContainer = splitText(startContainer, startOffset);
|
||||
startContainer = endContainer.previousSibling;
|
||||
|
||||
if (endOffset > startOffset) {
|
||||
endOffset = endOffset - startOffset;
|
||||
startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
|
||||
endOffset = endContainer.nodeValue.length;
|
||||
startOffset = 0;
|
||||
} else {
|
||||
endOffset = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Split startContainer text node if needed
|
||||
if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
|
||||
startContainer = splitText(startContainer, startOffset);
|
||||
startOffset = 0;
|
||||
}
|
||||
|
||||
// Split endContainer text node if needed
|
||||
if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
|
||||
endContainer = splitText(endContainer, endOffset).previousSibling;
|
||||
endOffset = endContainer.nodeValue.length;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
startContainer: startContainer,
|
||||
startOffset: startOffset,
|
||||
endContainer: endContainer,
|
||||
endOffset: endOffset
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two ranges and checks if they are equal.
|
||||
*
|
||||
* @static
|
||||
* @method compareRanges
|
||||
* @param {DOMRange} rng1 First range to compare.
|
||||
* @param {DOMRange} rng2 First range to compare.
|
||||
* @return {Boolean} true/false if the ranges are equal.
|
||||
*/
|
||||
RangeUtils.compareRanges = function(rng1, rng2) {
|
||||
if (rng1 && rng2) {
|
||||
// Compare native IE ranges
|
||||
if (rng1.item || rng1.duplicate) {
|
||||
// Both are control ranges and the selected element matches
|
||||
if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Both are text ranges and the range matches
|
||||
if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Compare w3c ranges
|
||||
return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return RangeUtils;
|
||||
});
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* ScriptLoader.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* 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');
|
||||
*
|
||||
* scriptLoader.loadQueue(function() {
|
||||
* alert('All scripts are now loaded.');
|
||||
* });
|
||||
*/
|
||||
define("tinymce/dom/ScriptLoader", [
|
||||
"tinymce/dom/DOMUtils",
|
||||
"tinymce/util/Tools"
|
||||
], function(DOMUtils, Tools) {
|
||||
var DOM = DOMUtils.DOM;
|
||||
var each = Tools.each, grep = Tools.grep;
|
||||
|
||||
function ScriptLoader() {
|
||||
var QUEUED = 0,
|
||||
LOADING = 1,
|
||||
LOADED = 2,
|
||||
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 callback function to execute ones this script gets loaded.
|
||||
* @param {Object} scope Optional scope to execute callback in.
|
||||
*/
|
||||
function loadScript(url, callback) {
|
||||
var dom = DOM, elm, id;
|
||||
|
||||
// Execute callback when script is loaded
|
||||
function done() {
|
||||
dom.remove(id);
|
||||
|
||||
if (elm) {
|
||||
elm.onreadystatechange = elm.onload = elm = null;
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
function error() {
|
||||
// Report the error so it's easier for people to spot loading errors
|
||||
if (typeof(console) !== "undefined" && console.log) {
|
||||
console.log("Failed to load: " + url);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
id = dom.uniqueId();
|
||||
|
||||
// Create new script element
|
||||
elm = document.createElement('script');
|
||||
elm.id = id;
|
||||
elm.type = 'text/javascript';
|
||||
elm.src = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 markDone
|
||||
* @param {string} u Absolute URL to the script to mark as loaded.
|
||||
*/
|
||||
this.markDone = function(url) {
|
||||
states[url] = LOADED;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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} callback Optional callback function to execute ones this script gets loaded.
|
||||
* @param {Object} scope Optional scope to execute callback in.
|
||||
*/
|
||||
this.add = this.load = function(url, callback, scope) {
|
||||
var state = states[url];
|
||||
|
||||
// Add url to load queue
|
||||
if (state == undef) {
|
||||
queue.push(url);
|
||||
states[url] = QUEUED;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
// Store away callback for later execution
|
||||
if (!scriptLoadedCallbacks[url]) {
|
||||
scriptLoadedCallbacks[url] = [];
|
||||
}
|
||||
|
||||
scriptLoadedCallbacks[url].push({
|
||||
func: callback,
|
||||
scope: scope || this
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the loading of the queue.
|
||||
*
|
||||
* @method loadQueue
|
||||
* @param {function} callback Optional callback to execute when all queued items are loaded.
|
||||
* @param {Object} scope Optional scope to execute the callback in.
|
||||
*/
|
||||
this.loadQueue = function(callback, scope) {
|
||||
this.loadScripts(queue, callback, scope);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 ones all items are loaded.
|
||||
* @param {Object} scope Optional scope to execute callback in.
|
||||
*/
|
||||
this.loadScripts = function(scripts, callback, scope) {
|
||||
var loadScripts;
|
||||
|
||||
function execScriptLoadedCallbacks(url) {
|
||||
// Execute URL callback functions
|
||||
each(scriptLoadedCallbacks[url], function(callback) {
|
||||
callback.func.call(callback.scope);
|
||||
});
|
||||
|
||||
scriptLoadedCallbacks[url] = undef;
|
||||
}
|
||||
|
||||
queueLoadedCallbacks.push({
|
||||
func: callback,
|
||||
scope: scope || this
|
||||
});
|
||||
|
||||
loadScripts = function() {
|
||||
var loadingScripts = grep(scripts);
|
||||
|
||||
// Current scripts has been handled
|
||||
scripts.length = 0;
|
||||
|
||||
// Load scripts that needs to be loaded
|
||||
each(loadingScripts, function(url) {
|
||||
// Script is already loaded then execute script callbacks directly
|
||||
if (states[url] == LOADED) {
|
||||
execScriptLoadedCallbacks(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// Is script not loading then start loading it
|
||||
if (states[url] != LOADING) {
|
||||
states[url] = LOADING;
|
||||
loading++;
|
||||
|
||||
loadScript(url, function() {
|
||||
states[url] = LOADED;
|
||||
loading--;
|
||||
|
||||
execScriptLoadedCallbacks(url);
|
||||
|
||||
// Load more scripts if they where added by the recently loaded script
|
||||
loadScripts();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// No scripts are currently loading then execute all pending queue loaded callbacks
|
||||
if (!loading) {
|
||||
each(queueLoadedCallbacks, function(callback) {
|
||||
callback.func.call(callback.scope);
|
||||
});
|
||||
|
||||
queueLoadedCallbacks.length = 0;
|
||||
}
|
||||
};
|
||||
|
||||
loadScripts();
|
||||
};
|
||||
}
|
||||
|
||||
ScriptLoader.ScriptLoader = new ScriptLoader();
|
||||
|
||||
return ScriptLoader;
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Serializer.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is used to serialize DOM trees into a string. Consult the TinyMCE Wiki API for
|
||||
* more details and examples on how to use this class.
|
||||
*
|
||||
* @class tinymce.dom.Serializer
|
||||
*/
|
||||
define("tinymce/dom/Serializer", [
|
||||
"tinymce/dom/DOMUtils",
|
||||
"tinymce/html/DomParser",
|
||||
"tinymce/html/Entities",
|
||||
"tinymce/html/Serializer",
|
||||
"tinymce/html/Node",
|
||||
"tinymce/html/Schema",
|
||||
"tinymce/Env",
|
||||
"tinymce/util/Tools"
|
||||
], function(DOMUtils, DomParser, Entities, Serializer, Node, Schema, Env, Tools) {
|
||||
var each = Tools.each, trim = Tools.trim;
|
||||
var DOM = DOMUtils.DOM;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (editor) {
|
||||
dom = editor.dom;
|
||||
schema = editor.schema;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
htmlParser = new DomParser(settings, schema);
|
||||
|
||||
// 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;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
|
||||
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];
|
||||
|
||||
if (name === "style") {
|
||||
value = dom.serializeStyle(dom.parseStyle(value), node.name);
|
||||
} else if (urlConverter) {
|
||||
value = urlConverter.call(urlConverterScope, value, name, node.name);
|
||||
}
|
||||
|
||||
node.attr(name, value.length > 0 ? value : null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove internal classes mceItem<..> or mceSelected
|
||||
htmlParser.addAttributeFilter('class', function(nodes) {
|
||||
var i = nodes.length, node, value;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
value = node.attr('class').replace(/(?:^|\s)mce-item-\w+(?!\S)/g, '');
|
||||
node.attr('class', value.length > 0 ? value : null);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove bookmark elements
|
||||
htmlParser.addAttributeFilter('data-mce-type', function(nodes, name, args) {
|
||||
var i = nodes.length, node;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
|
||||
if (node.attributes.map['data-mce-type'] === 'bookmark' && !args.cleanup) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove expando attributes
|
||||
htmlParser.addAttributeFilter('data-mce-expando', function(nodes, name) {
|
||||
var i = nodes.length;
|
||||
|
||||
while (i--) {
|
||||
nodes[i].attr(name, null);
|
||||
}
|
||||
});
|
||||
|
||||
htmlParser.addNodeFilter('noscript', function(nodes) {
|
||||
var i = nodes.length, node;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i].firstChild;
|
||||
|
||||
if (node) {
|
||||
node.value = Entities.decode(node.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
function trim(value) {
|
||||
/*jshint maxlen:255 */
|
||||
return value.replace(/(<!--\[CDATA\[|\]\]-->)/g, '\n')
|
||||
.replace(/^[\r\n]*|[\r\n]*$/g, '')
|
||||
.replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi, '')
|
||||
.replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g, '');
|
||||
}
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
value = node.firstChild ? node.firstChild.value : '';
|
||||
|
||||
if (name === "script") {
|
||||
// Remove mce- prefix from script elements and remove default text/javascript mime type (HTML5)
|
||||
var type = (node.attr('type') || 'text/javascript').replace(/^mce\-/, '');
|
||||
node.attr('type', type === 'text/javascript' ? null : type);
|
||||
|
||||
if (value.length > 0) {
|
||||
node.firstChild.value = '// <![CDATA[\n' + trim(value) + '\n// ]]>';
|
||||
}
|
||||
} else {
|
||||
if (value.length > 0) {
|
||||
node.firstChild.value = '<!--\n' + trim(value) + '\n-->';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert comments to cdata and handle protected comments
|
||||
htmlParser.addNodeFilter('#comment', function(nodes) {
|
||||
var i = nodes.length, node;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
htmlParser.addNodeFilter('xml:namespace,input', function(nodes, name) {
|
||||
var i = nodes.length, node;
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fix list elements, TODO: Replace this later
|
||||
if (settings.fix_list_elements) {
|
||||
htmlParser.addNodeFilter('ul,ol', function(nodes) {
|
||||
var i = nodes.length, node, parentNode;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
parentNode = node.parent;
|
||||
|
||||
if (parentNode.name === 'ul' || parentNode.name === 'ol') {
|
||||
if (node.prev && node.prev.name === 'li') {
|
||||
node.prev.append(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove internal data attributes
|
||||
htmlParser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style,data-mce-selected', function(nodes, name) {
|
||||
var i = nodes.length;
|
||||
|
||||
while (i--) {
|
||||
nodes[i].attr(name, null);
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
// 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 = node.ownerDocument.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);
|
||||
}
|
||||
|
||||
// Setup serializer
|
||||
htmlSerializer = new Serializer(settings, schema);
|
||||
|
||||
// Parse and serialize HTML
|
||||
args.content = htmlSerializer.serialize(
|
||||
htmlParser.parse(trim(args.getInner ? node.innerHTML : dom.getOuterHTML(node)), args)
|
||||
);
|
||||
|
||||
// Replace all BOM characters for now until we can find a better solution
|
||||
if (!args.cleanup) {
|
||||
args.content = args.content.replace(/\uFEFF/g, '');
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Sizzle.jQuery.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global jQuery:true */
|
||||
|
||||
/*
|
||||
* Fake Sizzle using jQuery.
|
||||
*/
|
||||
define("tinymce/dom/Sizzle", [], function() {
|
||||
// Detect if jQuery is loaded
|
||||
if (!window.jQuery) {
|
||||
throw new Error("Load jQuery first");
|
||||
}
|
||||
|
||||
return jQuery.find;
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* TreeWalker.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* TreeWalker class enables you to walk the DOM in a linear manner.
|
||||
*
|
||||
* @class tinymce.dom.TreeWalker
|
||||
*/
|
||||
define("tinymce/dom/TreeWalker", [], function() {
|
||||
return function(start_node, root_node) {
|
||||
var node = start_node;
|
||||
|
||||
function findSibling(node, start_name, sibling_name, shallow) {
|
||||
var sibling, parent;
|
||||
|
||||
if (node) {
|
||||
// Walk into nodes if it has a start
|
||||
if (!shallow && node[start_name]) {
|
||||
return node[start_name];
|
||||
}
|
||||
|
||||
// Return the sibling if it has one
|
||||
if (node != root_node) {
|
||||
sibling = node[sibling_name];
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
|
||||
// Walk up the parents to look for siblings
|
||||
for (parent = node.parentNode; parent && parent != root_node; parent = parent.parentNode) {
|
||||
sibling = parent[sibling_name];
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current node.
|
||||
*
|
||||
* @method current
|
||||
* @return {Node} Current node where the walker is.
|
||||
*/
|
||||
this.current = function() {
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* TridentSelection.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Selection class for old explorer versions. This one fakes the
|
||||
* native selection object available on modern browsers.
|
||||
*
|
||||
* @class tinymce.dom.TridentSelection
|
||||
*/
|
||||
define("tinymce/dom/TridentSelection", [], 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;
|
||||
|
||||
// Setup test range, collapse it and get the parent
|
||||
checkRng = rng.duplicate();
|
||||
checkRng.collapse(start);
|
||||
parent = checkRng.parentElement();
|
||||
|
||||
// Check if the selection is within the right document
|
||||
if (parent.ownerDocument !== selection.dom.doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// IE will report non editable elements as it's parent so look for an editable one
|
||||
while (parent.contentEditable === "false") {
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
// If parent doesn't have any children then return that we are inside the element
|
||||
if (!parent.hasChildNodes()) {
|
||||
return {node: parent, inside: 1};
|
||||
}
|
||||
|
||||
// Setup node list and endIndex
|
||||
children = parent.children;
|
||||
endIndex = children.length - 1;
|
||||
|
||||
// Perform a binary search for the position
|
||||
while (startIndex <= endIndex) {
|
||||
index = Math.floor((startIndex + endIndex) / 2);
|
||||
|
||||
// Move selection to node and compare the ranges
|
||||
child = children[index];
|
||||
checkRng.moveToElementText(child);
|
||||
position = checkRng.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', rng);
|
||||
|
||||
// Before/after or an exact match
|
||||
if (position > 0) {
|
||||
endIndex = index - 1;
|
||||
} else if (position < 0) {
|
||||
startIndex = index + 1;
|
||||
} else {
|
||||
return {node: child};
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
} else {
|
||||
// Child position is after the selection endpoint
|
||||
checkRng.collapse(true);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
|
||||
return {node: child, position: position, offset: offset, inside: inside};
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
collapsed = selection.isCollapsed();
|
||||
|
||||
// Handle control selection
|
||||
if (ieRange.item) {
|
||||
domRange.setStart(element.parentNode, dom.nodeIndex(element));
|
||||
domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);
|
||||
|
||||
return domRange;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (offset === undef) {
|
||||
domRange[start ? 'setStartBefore' : 'setEndAfter'](container);
|
||||
return;
|
||||
}
|
||||
|
||||
if (endPoint.position < 0) {
|
||||
sibling = endPoint.inside ? container.firstChild : container.nextSibling;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the text node and offset
|
||||
while (sibling) {
|
||||
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;
|
||||
}
|
||||
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
} else {
|
||||
// Find the text node and offset
|
||||
sibling = container.previousSibling;
|
||||
|
||||
if (!sibling) {
|
||||
return domRange[start ? 'setStartBefore' : 'setEndBefore'](container);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
while (sibling) {
|
||||
textNodeOffset += sibling.nodeValue.length;
|
||||
|
||||
// We are at or passed the position we where looking for
|
||||
if (textNodeOffset >= offset) {
|
||||
container = sibling;
|
||||
textNodeOffset -= offset;
|
||||
break;
|
||||
}
|
||||
|
||||
sibling = sibling.previousSibling;
|
||||
}
|
||||
}
|
||||
|
||||
domRange[start ? 'setStart' : 'setEnd'](container, textNodeOffset);
|
||||
}
|
||||
|
||||
try {
|
||||
// Find start point
|
||||
findEndPoint(true);
|
||||
|
||||
// Find end point if needed
|
||||
if (!collapsed) {
|
||||
findEndPoint();
|
||||
}
|
||||
} 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 happend when
|
||||
// text nodes are split into two nodes by a delete/backspace call. So lets detect it and try to fix it.
|
||||
if (ex.number == -2147024809) {
|
||||
// Get the current selection
|
||||
bookmark = self.getBookmark(2);
|
||||
|
||||
// Get start element
|
||||
tmpRange = ieRange.duplicate();
|
||||
tmpRange.collapse(true);
|
||||
element = tmpRange.parentElement();
|
||||
|
||||
// Get end element
|
||||
if (!collapsed) {
|
||||
tmpRange = ieRange.duplicate();
|
||||
tmpRange.collapse(false);
|
||||
element2 = tmpRange.parentElement();
|
||||
element2.innerHTML = element2.innerHTML;
|
||||
}
|
||||
|
||||
// Remove the broken elements
|
||||
element.innerHTML = element.innerHTML;
|
||||
|
||||
// Restore the selection
|
||||
self.moveToBookmark(bookmark);
|
||||
|
||||
// Since the range has moved we need to re-get it
|
||||
ieRange = selection.getRng();
|
||||
|
||||
// Find start point
|
||||
findEndPoint(true);
|
||||
|
||||
// Find end point if needed
|
||||
if (!collapsed) {
|
||||
findEndPoint();
|
||||
}
|
||||
} else {
|
||||
throw ex; // Throw other errors
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
i = children.length;
|
||||
while (i--) {
|
||||
if (node === children[i]) {
|
||||
indexes.push(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
node = parent;
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
function getBookmarkEndPoint(start) {
|
||||
var position;
|
||||
|
||||
position = getPosition(rng, start);
|
||||
if (position) {
|
||||
return {
|
||||
position: position.position,
|
||||
offset: position.offset,
|
||||
indexes: getIndexes(position.node),
|
||||
inside: position.inside
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Non ubstructive bookmark
|
||||
if (type === 2) {
|
||||
// Handle text selection
|
||||
if (!rng.item) {
|
||||
bookmark.start = getBookmarkEndPoint(true);
|
||||
|
||||
if (!selection.isCollapsed()) {
|
||||
bookmark.end = getBookmarkEndPoint();
|
||||
}
|
||||
} else {
|
||||
bookmark.start = {ctrl: true, indexes: getIndexes(rng.item(0))};
|
||||
}
|
||||
}
|
||||
|
||||
return bookmark;
|
||||
};
|
||||
|
||||
this.moveToBookmark = function(bookmark) {
|
||||
var rng, body = dom.doc.body;
|
||||
|
||||
function resolveIndexes(indexes) {
|
||||
var node, i, idx, children;
|
||||
|
||||
node = dom.getRoot();
|
||||
for (i = indexes.length - 1; i >= 0; i--) {
|
||||
children = node.children;
|
||||
idx = indexes[i];
|
||||
|
||||
if (idx <= children.length - 1) {
|
||||
node = children[idx];
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function setBookmarkEndPoint(start) {
|
||||
var endPoint = bookmark[start ? 'start' : 'end'], moveLeft, moveRng, undef, offset;
|
||||
|
||||
if (endPoint) {
|
||||
moveLeft = endPoint.position > 0;
|
||||
|
||||
moveRng = body.createTextRange();
|
||||
moveRng.moveToElementText(resolveIndexes(endPoint.indexes));
|
||||
|
||||
offset = endPoint.offset;
|
||||
if (offset !== undef) {
|
||||
moveRng.collapse(endPoint.inside || moveLeft);
|
||||
moveRng.moveStart('character', moveLeft ? -offset : offset);
|
||||
} else {
|
||||
moveRng.collapse(start);
|
||||
}
|
||||
|
||||
rng.setEndPoint(start ? 'StartToStart' : 'EndToStart', moveRng);
|
||||
|
||||
if (start) {
|
||||
rng.collapse(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
marker = dom.create('a');
|
||||
container = start ? startContainer : endContainer;
|
||||
offset = start ? startOffset : endOffset;
|
||||
tmpRng = ieRng.duplicate();
|
||||
|
||||
if (container == doc || container == doc.documentElement) {
|
||||
container = body;
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (nodes.length) {
|
||||
if (offset >= nodes.length) {
|
||||
dom.insertAfter(marker, nodes[nodes.length - 1]);
|
||||
} else {
|
||||
container.insertBefore(marker, nodes[offset]);
|
||||
}
|
||||
|
||||
tmpRng.moveToElementText(marker);
|
||||
} else if (container.canHaveHTML) {
|
||||
// Empty node selection for example <div>|</div>
|
||||
// Setting innerHTML with a span marker then remove that marker seems to keep empty block elements open
|
||||
container.innerHTML = '<span></span>';
|
||||
marker = container.firstChild;
|
||||
tmpRng.moveToElementText(marker);
|
||||
tmpRng.collapse(FALSE); // Collapse false works better than true for some odd reason
|
||||
}
|
||||
|
||||
ieRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', tmpRng);
|
||||
dom.remove(marker);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 <p></p>
|
||||
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: <p></p><p>|</p> would become this: <p>|</p><p></p>
|
||||
sibling = startContainer.previousSibling;
|
||||
if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) {
|
||||
sibling.innerHTML = '';
|
||||
} else {
|
||||
sibling = null;
|
||||
}
|
||||
|
||||
startContainer.innerHTML = '<span></span><span></span>';
|
||||
ieRng.moveToElementText(startContainer.lastChild);
|
||||
ieRng.select();
|
||||
dom.doc.selection.clear();
|
||||
startContainer.innerHTML = '';
|
||||
|
||||
if (sibling) {
|
||||
sibling.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
startOffset = dom.nodeIndex(startContainer);
|
||||
startContainer = startContainer.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
if (startOffset == endOffset - 1) {
|
||||
try {
|
||||
ctrlElm = startContainer.childNodes[startOffset];
|
||||
ctrlRng = body.createControlRange();
|
||||
ctrlRng.addElement(ctrlElm);
|
||||
ctrlRng.select();
|
||||
|
||||
// Check if the range produced is on the correct element and is a control range
|
||||
// On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398
|
||||
nativeRng = selection.getRng();
|
||||
if (nativeRng.item && ctrlElm === nativeRng.item(0)) {
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set start/end point of selection
|
||||
setEndPoint(true);
|
||||
setEndPoint();
|
||||
|
||||
// Select the new range and scroll it into view
|
||||
ieRng.select();
|
||||
};
|
||||
|
||||
// Expose range method
|
||||
this.getRangeAt = getRange;
|
||||
}
|
||||
|
||||
return Selection;
|
||||
});
|
||||
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* DomParser.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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: <p>a<p>b</p>c</p> will become <p>a</p><p>b</p><p>c</p>
|
||||
*
|
||||
* @example
|
||||
* var parser = new tinymce.html.DomParser({validate: true}, schema);
|
||||
* var rootNode = parser.parse('<h1>content</h1>');
|
||||
*
|
||||
* @class tinymce.html.DomParser
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/DomParser", [
|
||||
"tinymce/html/Node",
|
||||
"tinymce/html/Schema",
|
||||
"tinymce/html/SaxParser",
|
||||
"tinymce/util/Tools"
|
||||
], function(Node, Schema, SaxParser, Tools) {
|
||||
var makeMap = Tools.makeMap, each = Tools.each, explode = Tools.explode, extend = Tools.extend;
|
||||
|
||||
/**
|
||||
* 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, nonSplitableElements, textBlockElements, sibling, nextNode;
|
||||
|
||||
nonSplitableElements = makeMap('tr,td,th,tbody,thead,tfoot,table');
|
||||
nonEmptyElements = schema.getNonEmptyElements();
|
||||
textBlockElements = schema.getTextBlockElements();
|
||||
|
||||
for (ni = 0; ni < nodes.length; ni++) {
|
||||
node = nodes[ni];
|
||||
|
||||
// Already removed or fixed
|
||||
if (!node.parent || node.fixed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
sibling = sibling.next;
|
||||
}
|
||||
|
||||
// Unwrap current text block
|
||||
node.unwrap(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (childNode = parents[i].firstChild; childNode && childNode != parents[i + 1]; ) {
|
||||
nextNode = childNode.next;
|
||||
tempNode.append(childNode);
|
||||
childNode = nextNode;
|
||||
}
|
||||
|
||||
currentNode = tempNode;
|
||||
}
|
||||
|
||||
if (!newParent.isEmpty(nonEmptyElements)) {
|
||||
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 <p><br /></p>
|
||||
parent = parents[0];
|
||||
if (parent.isEmpty(nonEmptyElements) || parent.firstChild === parent.lastChild && parent.firstChild.name === '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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 (node.name === 'style' || node.name === 'script') {
|
||||
node.empty().remove();
|
||||
} else {
|
||||
node.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Run element filters
|
||||
if (name in nodeFilters) {
|
||||
list = matchedNodes[name];
|
||||
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
matchedNodes[name] = [node];
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
||||
if (!list) {
|
||||
nodeFilters[name] = list = [];
|
||||
}
|
||||
|
||||
list.push(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]});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the specified HTML string into a DOM like node tree and returns the result.
|
||||
*
|
||||
* @example
|
||||
* var rootNode = new DomParser({...}).parse('<b>text</b>');
|
||||
* @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;
|
||||
|
||||
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;
|
||||
|
||||
whiteSpaceElements = schema.getWhiteSpaceElements();
|
||||
startWhiteSpaceRegExp = /^[ \t\r\n]+/;
|
||||
endWhiteSpaceRegExp = /[ \t\r\n]+$/;
|
||||
allWhiteSpaceRegExp = /[ \t\r\n]+/g;
|
||||
isAllWhiteSpaceRegExp = /^[ \t\r\n]+$/;
|
||||
|
||||
function addRootBlocks() {
|
||||
var node = rootNode.firstChild, next, rootBlockNode;
|
||||
|
||||
// Removes whitespace at beginning and end of block so:
|
||||
// <p> x </p> -> <p>x</p>
|
||||
function trim(rootBlockNode) {
|
||||
if (rootBlockNode) {
|
||||
node = rootBlockNode.firstChild;
|
||||
if (node && node.type == 3) {
|
||||
node.value = node.value.replace(startWhiteSpaceRegExp, '');
|
||||
}
|
||||
|
||||
node = rootBlockNode.lastChild;
|
||||
if (node && node.type == 3) {
|
||||
node.value = node.value.replace(endWhiteSpaceRegExp, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root
|
||||
if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node) {
|
||||
next = node.next;
|
||||
|
||||
if (node.type == 3 || (node.type == 1 && node.name !== 'p' &&
|
||||
!blockElements[node.name] && !node.attr('data-mce-type'))) {
|
||||
if (!rootBlockNode) {
|
||||
// Create a new root block element
|
||||
rootBlockNode = createNode(rootBlockName, 1);
|
||||
rootBlockNode.attr(settings.forced_root_block_attrs);
|
||||
rootNode.insert(rootBlockNode, node);
|
||||
rootBlockNode.append(node);
|
||||
} else {
|
||||
rootBlockNode.append(node);
|
||||
}
|
||||
} else {
|
||||
trim(rootBlockNode);
|
||||
rootBlockNode = null;
|
||||
}
|
||||
|
||||
node = next;
|
||||
}
|
||||
|
||||
trim(rootBlockNode);
|
||||
}
|
||||
|
||||
function createNode(name, type) {
|
||||
var node = new Node(name, type), list;
|
||||
|
||||
if (name in nodeFilters) {
|
||||
list = matchedNodes[name];
|
||||
|
||||
if (list) {
|
||||
list.push(node);
|
||||
} else {
|
||||
matchedNodes[name] = [node];
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function removeWhitespaceBefore(node) {
|
||||
var textNode, textVal, sibling;
|
||||
|
||||
for (textNode = node.prev; textNode && textNode.type === 3; ) {
|
||||
textVal = textNode.value.replace(endWhiteSpaceRegExp, '');
|
||||
|
||||
if (textVal.length > 0) {
|
||||
textNode.value = textVal;
|
||||
textNode = textNode.prev;
|
||||
} else {
|
||||
sibling = textNode.prev;
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cloneAndExcludeBlocks(input) {
|
||||
var name, output = {};
|
||||
|
||||
for (name in input) {
|
||||
if (name !== 'li' && name != 'p') {
|
||||
output[name] = input[name];
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
parser = new SaxParser({
|
||||
validate: validate,
|
||||
allow_script_urls: settings.allow_script_urls,
|
||||
allow_conditional_comments: settings.allow_conditional_comments,
|
||||
|
||||
// Exclude P and LI from DOM parsing since it's treated better by the DOM parser
|
||||
self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()),
|
||||
|
||||
cdata: function(text) {
|
||||
node.append(createNode('#cdata', 4)).value = text;
|
||||
},
|
||||
|
||||
text: function(text, raw) {
|
||||
var textNode;
|
||||
|
||||
// Trim all redundant whitespace on non white space elements
|
||||
if (!isInWhiteSpacePreservedElement) {
|
||||
text = text.replace(allWhiteSpaceRegExp, ' ');
|
||||
|
||||
if (node.lastChild && blockElements[node.lastChild.name]) {
|
||||
text = text.replace(startWhiteSpaceRegExp, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Do we need to create the node
|
||||
if (text.length !== 0) {
|
||||
textNode = createNode('#text', 3);
|
||||
textNode.raw = !!raw;
|
||||
node.append(textNode).value = text;
|
||||
}
|
||||
},
|
||||
|
||||
comment: function(text) {
|
||||
node.append(createNode('#comment', 8)).value = text;
|
||||
},
|
||||
|
||||
pi: function(name, text) {
|
||||
node.append(createNode(name, 7)).value = text;
|
||||
removeWhitespaceBefore(node);
|
||||
},
|
||||
|
||||
doctype: function(text) {
|
||||
var newNode;
|
||||
|
||||
newNode = node.append(createNode('#doctype', 10));
|
||||
newNode.value = text;
|
||||
removeWhitespaceBefore(node);
|
||||
},
|
||||
|
||||
start: function(name, attrs, empty) {
|
||||
var newNode, attrFiltersLen, elementRule, attrName, parent;
|
||||
|
||||
elementRule = validate ? schema.getElementRule(name) : {};
|
||||
if (elementRule) {
|
||||
newNode = createNode(elementRule.outputName || name, 1);
|
||||
newNode.attributes = attrs;
|
||||
newNode.shortEnded = empty;
|
||||
|
||||
node.append(newNode);
|
||||
|
||||
// Check if node is valid child of the parent node is the child is
|
||||
// unknown we don't collect it since it's probably a custom element
|
||||
parent = children[node.name];
|
||||
if (parent && children[newNode.name] && !parent[newNode.name]) {
|
||||
invalidChildren.push(newNode);
|
||||
}
|
||||
|
||||
attrFiltersLen = attributeFilters.length;
|
||||
while (attrFiltersLen--) {
|
||||
attrName = attributeFilters[attrFiltersLen].name;
|
||||
|
||||
if (attrName in attrs.map) {
|
||||
list = matchedAttributes[attrName];
|
||||
|
||||
if (list) {
|
||||
list.push(newNode);
|
||||
} else {
|
||||
matchedAttributes[attrName] = [newNode];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim whitespace before block
|
||||
if (blockElements[name]) {
|
||||
removeWhitespaceBefore(newNode);
|
||||
}
|
||||
|
||||
// Change current node if the element wasn't empty i.e not <br /> or <img />
|
||||
if (!empty) {
|
||||
node = newNode;
|
||||
}
|
||||
|
||||
// Check if we are inside a whitespace preserved element
|
||||
if (!isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
|
||||
isInWhiteSpacePreservedElement = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
end: function(name) {
|
||||
var textNode, elementRule, text, sibling, tempNode;
|
||||
|
||||
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, '');
|
||||
|
||||
// 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;
|
||||
|
||||
// Remove any pure whitespace siblings
|
||||
while (textNode && textNode.type === 3) {
|
||||
text = textNode.value;
|
||||
sibling = textNode.next;
|
||||
|
||||
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
}
|
||||
|
||||
textNode = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim whitespace of the last node in a block
|
||||
textNode = node.lastChild;
|
||||
if (textNode && textNode.type === 3) {
|
||||
text = textNode.value.replace(endWhiteSpaceRegExp, '');
|
||||
|
||||
// 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;
|
||||
|
||||
// Remove any pure whitespace siblings
|
||||
while (textNode && textNode.type === 3) {
|
||||
text = textNode.value;
|
||||
sibling = textNode.prev;
|
||||
|
||||
if (text.length === 0 || isAllWhiteSpaceRegExp.test(text)) {
|
||||
textNode.remove();
|
||||
textNode = sibling;
|
||||
}
|
||||
|
||||
textNode = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim start white space
|
||||
// Removed due to: #5424
|
||||
/*textNode = node.prev;
|
||||
if (textNode && textNode.type === 3) {
|
||||
text = textNode.value.replace(startWhiteSpaceRegExp, '');
|
||||
|
||||
if (text.length > 0)
|
||||
textNode.value = text;
|
||||
else
|
||||
textNode.remove();
|
||||
}*/
|
||||
}
|
||||
|
||||
// Check if we exited a whitespace preserved element
|
||||
if (isInWhiteSpacePreservedElement && whiteSpaceElements[name]) {
|
||||
isInWhiteSpacePreservedElement = false;
|
||||
}
|
||||
|
||||
// Handle empty nodes
|
||||
if (elementRule.removeEmpty || elementRule.paddEmpty) {
|
||||
if (node.isEmpty(nonEmptyElements)) {
|
||||
if (elementRule.paddEmpty) {
|
||||
node.empty().append(new Node('#text', '3')).value = '\u00a0';
|
||||
} else {
|
||||
// Leave nodes that have a name like <a name="name">
|
||||
if (!node.attributes.map.name && !node.attributes.map.id) {
|
||||
tempNode = node.parent;
|
||||
node.empty().remove();
|
||||
node = tempNode;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node = node.parent;
|
||||
}
|
||||
}
|
||||
}, schema);
|
||||
|
||||
rootNode = node = new Node(args.context || settings.root_name, 11);
|
||||
|
||||
parser.parse(html);
|
||||
|
||||
// Fix invalid children or report invalid children in a contextual parsing
|
||||
if (validate && invalidChildren.length) {
|
||||
if (!args.context) {
|
||||
fixInvalidChildren(invalidChildren);
|
||||
} else {
|
||||
args.invalid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap nodes in the root into block elements if the root is body
|
||||
if (rootBlockName && (rootNode.name == 'body' || args.isRootContent)) {
|
||||
addRootBlocks();
|
||||
}
|
||||
|
||||
// Run filters only when the contents is valid
|
||||
if (!args.invalid) {
|
||||
// Run node filters
|
||||
for (name in matchedNodes) {
|
||||
list = nodeFilters[name];
|
||||
nodes = matchedNodes[name];
|
||||
|
||||
// Remove already removed children
|
||||
fi = nodes.length;
|
||||
while (fi--) {
|
||||
if (!nodes[fi].parent) {
|
||||
nodes.splice(fi, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, l = list.length; i < l; i++) {
|
||||
list[i](nodes, name, args);
|
||||
}
|
||||
}
|
||||
|
||||
// Run attribute filters
|
||||
for (i = 0, l = attributeFilters.length; i < l; i++) {
|
||||
list = attributeFilters[i];
|
||||
|
||||
if (list.name in matchedAttributes) {
|
||||
nodes = matchedAttributes[list.name];
|
||||
|
||||
// Remove already removed children
|
||||
fi = nodes.length;
|
||||
while (fi--) {
|
||||
if (!nodes[fi].parent) {
|
||||
nodes.splice(fi, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (fi = 0, fl = list.callbacks.length; fi < fl; fi++) {
|
||||
list.callbacks[fi](nodes, list.name, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNode;
|
||||
};
|
||||
|
||||
// Remove <br> 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 elementRule, textNode;
|
||||
|
||||
// Remove brs from body element as well
|
||||
blockElements.body = 1;
|
||||
|
||||
// Must loop forwards since it will otherwise remove all brs in <p>a<br><br><br></p>
|
||||
for (i = 0; i < l; i++) {
|
||||
node = nodes[i];
|
||||
parent = node.parent;
|
||||
|
||||
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;
|
||||
|
||||
// Ignore bookmarks
|
||||
if (prevName !== "span" || prev.attr('data-mce-type') !== 'bookmark') {
|
||||
// Found a non BR element
|
||||
if (prevName !== "br") {
|
||||
break;
|
||||
}
|
||||
|
||||
// Found another br it's a <br><br> structure then don't remove anything
|
||||
if (prevName === 'br') {
|
||||
node = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prev = prev.prev;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
node.remove();
|
||||
|
||||
// Is the parent to be considered empty after we removed the BR
|
||||
if (parent.isEmpty(nonEmptyElements)) {
|
||||
elementRule = schema.getElementRule(parent.name);
|
||||
|
||||
// Remove or padd the element depending on schema rule
|
||||
if (elementRule) {
|
||||
if (elementRule.removeEmpty) {
|
||||
parent.remove();
|
||||
} else if (elementRule.paddEmpty) {
|
||||
parent.empty().append(new Node('#text', 3)).value = '\u00a0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Replaces BR elements inside inline elements like <p><b><i><br></i></b></p>
|
||||
// so they become <p><b><i> </i></b></p>
|
||||
lastParent = node;
|
||||
while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) {
|
||||
lastParent = parent;
|
||||
|
||||
if (blockElements[parent.name]) {
|
||||
break;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
if (lastParent === parent) {
|
||||
textNode = new Node('#text', 3);
|
||||
textNode.value = '\u00a0';
|
||||
node.replace(textNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included.
|
||||
if (!settings.allow_html_in_named_anchor) {
|
||||
self.addAttributeFilter('id,name', function(nodes) {
|
||||
var i = nodes.length, sibling, prevSibling, parent, node;
|
||||
|
||||
while (i--) {
|
||||
node = nodes[i];
|
||||
if (node.name === 'a' && node.firstChild && !node.attr('href')) {
|
||||
parent = node.parent;
|
||||
|
||||
// Move children after current node
|
||||
sibling = node.lastChild;
|
||||
do {
|
||||
prevSibling = sibling.prev;
|
||||
parent.insert(sibling, node);
|
||||
sibling = prevSibling;
|
||||
} while (sibling);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Entities.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*jshint bitwise:false */
|
||||
|
||||
/**
|
||||
* Entity encoder class.
|
||||
*
|
||||
* @class tinymce.html.Entities
|
||||
* @static
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Entities", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var makeMap = Tools.makeMap;
|
||||
|
||||
var namedEntities, baseEntities, reverseEntities,
|
||||
attrsCharsRegExp = /[&<>\"\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
textCharsRegExp = /[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
rawCharsRegExp = /[<>&\"\']/g,
|
||||
entityRegExp = /&(#x|#)?([\w]+);/g,
|
||||
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"
|
||||
};
|
||||
|
||||
// Raw entities
|
||||
baseEntities = {
|
||||
'\"': '"', // Needs to be escaped since the YUI compressor would otherwise break the code
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&'
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
// Build entities lookup table
|
||||
for (i = 0; i < items.length; i += 2) {
|
||||
chr = String.fromCharCode(parseInt(items[i], radix));
|
||||
|
||||
// Only add non base entities
|
||||
if (!baseEntities[chr]) {
|
||||
entity = '&' + items[i + 1] + ';';
|
||||
lookup[chr] = entity;
|
||||
lookup[entity] = chr;
|
||||
}
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
var Entities = {
|
||||
/**
|
||||
* Encodes the specified string using raw entities. This means only the required XML base entities will be endoded.
|
||||
*
|
||||
* @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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) + ';';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
|
||||
return baseEntities[chr] || entities[chr] || chr;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
function encodeNamedAndNumeric(text, attr) {
|
||||
return text.replace(attr ? attrsCharsRegExp : textCharsRegExp, function(chr) {
|
||||
return baseEntities[chr] || entities[chr] || '&#' + chr.charCodeAt(0) + ';' || chr;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return Entities.encodeNamed;
|
||||
}
|
||||
|
||||
// Numeric
|
||||
if (name.numeric) {
|
||||
return Entities.encodeNumeric;
|
||||
}
|
||||
|
||||
// Raw encoder
|
||||
return Entities.encodeRaw;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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, value) {
|
||||
if (numeric) {
|
||||
value = parseInt(value, numeric.length === 2 ? 16 : 10);
|
||||
|
||||
// Support upper UTF
|
||||
if (value > 0xFFFF) {
|
||||
value -= 0x10000;
|
||||
|
||||
return String.fromCharCode(0xD800 + (value >> 10), 0xDC00 + (value & 0x3FF));
|
||||
} else {
|
||||
return asciiMap[value] || String.fromCharCode(value);
|
||||
}
|
||||
}
|
||||
|
||||
return reverseEntities[all] || namedEntities[all] || nativeDecode(all);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return Entities;
|
||||
});
|
||||
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* Node.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* 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/html/Node", [], function() {
|
||||
var whiteSpaceRegExp = /^[ \t\r\n]*$/, typeLookup = {
|
||||
'#text': 3,
|
||||
'#comment': 8,
|
||||
'#cdata': 4,
|
||||
'#pi': 7,
|
||||
'#doctype': 10,
|
||||
'#document-fragment': 11
|
||||
};
|
||||
|
||||
// Walks the tree left/right
|
||||
function walk(node, root_node, prev) {
|
||||
var sibling, parent, startName = prev ? 'lastChild' : 'firstChild', siblingName = prev ? 'prev' : 'next';
|
||||
|
||||
// Walk into nodes if it has a start
|
||||
if (node[startName]) {
|
||||
return node[startName];
|
||||
}
|
||||
|
||||
// Return the sibling if it has one
|
||||
if (node !== root_node) {
|
||||
sibling = node[siblingName];
|
||||
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
|
||||
// Walk up the parents to look for siblings
|
||||
for (parent = node.parent; parent && parent !== root_node; parent = parent.parent) {
|
||||
sibling = parent[siblingName];
|
||||
|
||||
if (sibling) {
|
||||
return sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if (type === 1) {
|
||||
this.attributes = [];
|
||||
this.attributes.map = {};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (node.parent) {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
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 self;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// 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});
|
||||
}
|
||||
|
||||
attrs.map[name] = value;
|
||||
|
||||
return self;
|
||||
} else {
|
||||
return attrs.map[name];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// Clone element attributes
|
||||
if ((selfAttrs = self.attributes)) {
|
||||
cloneAttrs = [];
|
||||
cloneAttrs.map = {};
|
||||
|
||||
for (i = 0, l = selfAttrs.length; i < l; i++) {
|
||||
selfAttr = selfAttrs[i];
|
||||
|
||||
// Clone everything except id
|
||||
if (selfAttr.name !== 'id') {
|
||||
cloneAttrs[cloneAttrs.length] = {name: selfAttr.name, value: selfAttr.value};
|
||||
cloneAttrs.map[selfAttr.name] = selfAttr.value;
|
||||
}
|
||||
}
|
||||
|
||||
clone.attributes = cloneAttrs;
|
||||
}
|
||||
|
||||
clone.value = self.value;
|
||||
clone.shortEnded = self.shortEnded;
|
||||
|
||||
return clone;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps the node in in another node.
|
||||
*
|
||||
* @example
|
||||
* node.wrap(wrapperNode);
|
||||
*
|
||||
* @method wrap
|
||||
*/
|
||||
wrap: function(wrapper) {
|
||||
var self = this;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
last = self.lastChild;
|
||||
if (last) {
|
||||
last.next = node;
|
||||
node.prev = last;
|
||||
self.lastChild = node;
|
||||
} 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} ref_node 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, ref_node, before) {
|
||||
var parent;
|
||||
|
||||
if (node.parent) {
|
||||
node.remove();
|
||||
}
|
||||
|
||||
parent = ref_node.parent || this;
|
||||
|
||||
if (before) {
|
||||
if (ref_node === parent.firstChild) {
|
||||
parent.firstChild = node;
|
||||
} else {
|
||||
ref_node.prev.next = node;
|
||||
}
|
||||
|
||||
node.prev = ref_node.prev;
|
||||
node.next = ref_node;
|
||||
ref_node.prev = node;
|
||||
} else {
|
||||
if (ref_node === parent.lastChild) {
|
||||
parent.lastChild = node;
|
||||
} else {
|
||||
ref_node.next.prev = node;
|
||||
}
|
||||
|
||||
node.next = ref_node.next;
|
||||
node.prev = ref_node;
|
||||
ref_node.next = node;
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
for (node = self.firstChild; node; node = walk(node, self)) {
|
||||
if (node.name === name) {
|
||||
collection.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
self.firstChild = self.lastChild = null;
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {Boolean} true/false if the node is empty or not.
|
||||
*/
|
||||
isEmpty: function(elements) {
|
||||
var self = this, node = self.firstChild, i, name;
|
||||
|
||||
if (node) {
|
||||
do {
|
||||
if (node.type === 1) {
|
||||
// Ignore bogus elements
|
||||
if (node.attributes.map['data-mce-bogus']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep empty elements like <img />
|
||||
if (elements[node.name]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep elements with data attributes or name attribute like <a name="1"></a>
|
||||
i = node.attributes.length;
|
||||
while (i--) {
|
||||
name = node.attributes[i].name;
|
||||
if (name === "name" || name.indexOf('data-mce-') === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep comments
|
||||
if (node.type === 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep non whitespace text nodes
|
||||
if ((node.type === 3 && !whiteSpaceRegExp.test(node.value))) {
|
||||
return false;
|
||||
}
|
||||
} while ((node = walk(node, self)));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Walks to the next or previous node and returns that node or null if it wasn't found.
|
||||
*
|
||||
* @method walk
|
||||
* @param {Boolean} prev Optional previous node state defaults to false.
|
||||
* @return {tinymce.html.Node} Node that is next to or previous of the current node.
|
||||
*/
|
||||
walk: function(prev) {
|
||||
return walk(this, null, prev);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a node of a specific type.
|
||||
*
|
||||
* @static
|
||||
* @method create
|
||||
* @param {String} name Name of the node type to create for example "b" or "#text".
|
||||
* @param {Object} attrs Name/value collection of attributes that will be applied to elements.
|
||||
*/
|
||||
Node.create = function(name, attrs) {
|
||||
var node, attrName;
|
||||
|
||||
// Create node
|
||||
node = new Node(name, typeLookup[name] || 1);
|
||||
|
||||
// Add attributes if needed
|
||||
if (attrs) {
|
||||
for (attrName in attrs) {
|
||||
node.attr(attrName, attrs[attrName]);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
return Node;
|
||||
});
|
||||
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* SaxParser.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 <b><p></b></p>. 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/html/SaxParser", [
|
||||
"tinymce/html/Schema",
|
||||
"tinymce/html/Entities",
|
||||
"tinymce/util/Tools"
|
||||
], function(Schema, Entities, Tools) {
|
||||
var each = Tools.each;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
return function(settings, schema) {
|
||||
var self = this, noop = function() {};
|
||||
|
||||
settings = settings || {};
|
||||
self.schema = schema = schema || new Schema();
|
||||
|
||||
if (settings.fix_self_closing !== false) {
|
||||
settings.fix_self_closing = true;
|
||||
}
|
||||
|
||||
// Add handler functions from settings and setup default handlers
|
||||
each('comment cdata text start end pi doctype'.split(' '), function(name) {
|
||||
if (name) {
|
||||
self[name] = settings[name] || noop;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Parses the specified HTML string and executes the callbacks for each item it finds.
|
||||
*
|
||||
* @example
|
||||
* new SaxParser({...}).parse('<b>text</b>');
|
||||
* @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;
|
||||
var anyAttributesRequired, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0;
|
||||
var decode = Entities.decode, fixSelfClosing, filteredAttrs = Tools.makeMap('src,href');
|
||||
|
||||
function processEndTag(name) {
|
||||
var pos, i;
|
||||
|
||||
// Find position of parent of the same type
|
||||
pos = stack.length;
|
||||
while (pos--) {
|
||||
if (stack[pos].name === name) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Found parent
|
||||
if (pos >= 0) {
|
||||
// Close all the open elements
|
||||
for (i = stack.length - 1; i >= pos; i--) {
|
||||
name = stack[i];
|
||||
|
||||
if (name.valid) {
|
||||
self.end(name.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
|
||||
function parseAttribute(match, name, value, val2, val3) {
|
||||
var attrRule, i, trimRegExp = /[\s\u0000-\u001F]+/g;
|
||||
|
||||
name = name.toLowerCase();
|
||||
value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute
|
||||
|
||||
// Validate name and value pass through all data- attributes
|
||||
if (validate && !isInternalElement && name.indexOf('data-') !== 0) {
|
||||
attrRule = validAttributesMap[name];
|
||||
|
||||
// Find rule by pattern matching
|
||||
if (!attrRule && validAttributePatterns) {
|
||||
i = validAttributePatterns.length;
|
||||
while (i--) {
|
||||
attrRule = validAttributePatterns[i];
|
||||
if (attrRule.pattern.test(name)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No rule matched
|
||||
if (i === -1) {
|
||||
attrRule = null;
|
||||
}
|
||||
}
|
||||
|
||||
// No attribute rule found
|
||||
if (!attrRule) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate value
|
||||
if (attrRule.validValues && !(value in attrRule.validValues)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredAttrs[name] && !settings.allow_script_urls) {
|
||||
if (/(java|vb)script:/i.test(decodeURIComponent(value.replace(trimRegExp, '')))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Add attribute to list and map
|
||||
attrList.map[name] = value;
|
||||
attrList.push({
|
||||
name: name,
|
||||
value: value
|
||||
});
|
||||
}
|
||||
|
||||
// Precompile RegExps and map objects
|
||||
tokenRegExp = new RegExp('<(?:' +
|
||||
'(?:!--([\\w\\W]*?)-->)|' + // Comment
|
||||
'(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA
|
||||
'(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE
|
||||
'(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI
|
||||
'(?:\\/([^>]+)>)|' + // End element
|
||||
'(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element
|
||||
')', 'g');
|
||||
|
||||
attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g;
|
||||
|
||||
// Setup lookup tables for empty elements and boolean attributes
|
||||
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();
|
||||
|
||||
while ((matches = tokenRegExp.exec(html))) {
|
||||
// Text
|
||||
if (index < matches.index) {
|
||||
self.text(decode(html.substr(index, matches.index - index)));
|
||||
}
|
||||
|
||||
if ((value = matches[6])) { // End element
|
||||
value = value.toLowerCase();
|
||||
|
||||
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
|
||||
if (value.charAt(0) === ':') {
|
||||
value = value.substr(1);
|
||||
}
|
||||
|
||||
processEndTag(value);
|
||||
} else if ((value = matches[7])) { // Start element
|
||||
value = value.toLowerCase();
|
||||
|
||||
// IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements
|
||||
if (value.charAt(0) === ':') {
|
||||
value = value.substr(1);
|
||||
}
|
||||
|
||||
isShortEnded = value in shortEndedElements;
|
||||
|
||||
// Is self closing tag for example an <li> after an open <li>
|
||||
if (fixSelfClosing && selfClosing[value] && stack.length > 0 && stack[stack.length - 1].name === value) {
|
||||
processEndTag(value);
|
||||
}
|
||||
|
||||
// Validate element
|
||||
if (!validate || (elementRule = schema.getElementRule(value))) {
|
||||
isValidElement = true;
|
||||
|
||||
// Grab attributes map and patters when validation is enabled
|
||||
if (validate) {
|
||||
validAttributesMap = elementRule.attributes;
|
||||
validAttributePatterns = elementRule.attributePatterns;
|
||||
}
|
||||
|
||||
// Parse attributes
|
||||
if ((attribsValue = matches[8])) {
|
||||
isInternalElement = attribsValue.indexOf('data-mce-type') !== -1; // Check if the element is an internal element
|
||||
|
||||
// If the element has internal attributes then remove it if we are told to do so
|
||||
if (isInternalElement && removeInternalElements) {
|
||||
isValidElement = false;
|
||||
}
|
||||
|
||||
attrList = [];
|
||||
attrList.map = {};
|
||||
|
||||
attribsValue.replace(attrRegExp, parseAttribute);
|
||||
} else {
|
||||
attrList = [];
|
||||
attrList.map = {};
|
||||
}
|
||||
|
||||
// Process attributes if validation is enabled
|
||||
if (validate && !isInternalElement) {
|
||||
attributesRequired = elementRule.attributesRequired;
|
||||
attributesDefault = elementRule.attributesDefault;
|
||||
attributesForced = elementRule.attributesForced;
|
||||
anyAttributesRequired = elementRule.removeEmptyAttrs;
|
||||
|
||||
// Check if any attribute exists
|
||||
if (anyAttributesRequired && !attrList.length) {
|
||||
isValidElement = false;
|
||||
}
|
||||
|
||||
// Handle forced attributes
|
||||
if (attributesForced) {
|
||||
i = attributesForced.length;
|
||||
while (i--) {
|
||||
attr = attributesForced[i];
|
||||
name = attr.name;
|
||||
attrValue = attr.value;
|
||||
|
||||
if (attrValue === '{$uid}') {
|
||||
attrValue = 'mce_' + idCount++;
|
||||
}
|
||||
|
||||
attrList.map[name] = attrValue;
|
||||
attrList.push({name: name, value: attrValue});
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (attrList.map['data-mce-bogus']) {
|
||||
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) === '[if') {
|
||||
value = ' ' + value;
|
||||
}
|
||||
|
||||
self.comment(value);
|
||||
} else if ((value = matches[2])) { // CDATA
|
||||
self.cdata(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];
|
||||
|
||||
if (value.valid) {
|
||||
self.end(value.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,930 @@
|
||||
/**
|
||||
* Schema.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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/html/Schema", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var mapCache = {};
|
||||
var makeMap = Tools.makeMap, each = Tools.each, extend = Tools.extend, explode = Tools.explode, inArray = Tools.inArray;
|
||||
|
||||
function split(items, delim) {
|
||||
return items ? items.split(delim || ' ') : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, eventAttributes, blockContent;
|
||||
var phrasingContent, flowContent, html4BlockContent, html4PhrasingContent;
|
||||
|
||||
function add(name, attributes, children) {
|
||||
var ni, i, attributesOrder, args = arguments;
|
||||
|
||||
function arrayToMap(array) {
|
||||
var map = {}, i, l;
|
||||
|
||||
for (i = 0, l = array.length; i < l; i++) {
|
||||
map[array[i]] = {};
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
children = children || [];
|
||||
attributes = attributes || "";
|
||||
|
||||
if (typeof(children) === "string") {
|
||||
children = split(children);
|
||||
}
|
||||
|
||||
// Split string children
|
||||
for (i = 3; i < args.length; i++) {
|
||||
if (typeof(args[i]) === "string") {
|
||||
args[i] = split(args[i]);
|
||||
}
|
||||
|
||||
children.push.apply(children, args[i]);
|
||||
}
|
||||
|
||||
name = split(name);
|
||||
ni = name.length;
|
||||
while (ni--) {
|
||||
attributesOrder = [].concat(globalAttributes, split(attributes));
|
||||
schema[name[ni]] = {
|
||||
attributes: arrayToMap(attributesOrder),
|
||||
attributesOrder: attributesOrder,
|
||||
children: arrayToMap(children)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addAttrs(name, attributes) {
|
||||
var ni, schemaItem, i, l;
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use cached schema
|
||||
if (mapCache[type]) {
|
||||
return mapCache[type];
|
||||
}
|
||||
|
||||
// Attributes present on all elements
|
||||
globalAttributes = split("id accesskey class dir lang style tabindex title");
|
||||
|
||||
// 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"
|
||||
);
|
||||
|
||||
// Block content elements
|
||||
blockContent = split(
|
||||
"address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul"
|
||||
);
|
||||
|
||||
// Phrasing content elements from the HTML5 spec (inline)
|
||||
phrasingContent = split(
|
||||
"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.push.apply(globalAttributes, split("contenteditable contextmenu draggable dropzone " +
|
||||
"hidden spellcheck translate"));
|
||||
blockContent.push.apply(blockContent, split("article aside details dialog figure header footer hgroup section nav"));
|
||||
phrasingContent.push.apply(phrasingContent, split("audio canvas command datalist mark meter output progress time wbr " +
|
||||
"video ruby bdi keygen"));
|
||||
}
|
||||
|
||||
// Add HTML4 elements unless it's html5-strict
|
||||
if (type != "html5-strict") {
|
||||
globalAttributes.push("xml:lang");
|
||||
|
||||
html4PhrasingContent = split("acronym applet basefont big font strike tt");
|
||||
phrasingContent.push.apply(phrasingContent, html4PhrasingContent);
|
||||
|
||||
each(html4PhrasingContent, function(name) {
|
||||
add(name, "", phrasingContent);
|
||||
});
|
||||
|
||||
html4BlockContent = split("center dir isindex noframes");
|
||||
blockContent.push.apply(blockContent, html4BlockContent);
|
||||
|
||||
// Flow content elements from the HTML5 spec (block+inline)
|
||||
flowContent = [].concat(blockContent, phrasingContent);
|
||||
|
||||
each(html4BlockContent, function(name) {
|
||||
add(name, "", flowContent);
|
||||
});
|
||||
}
|
||||
|
||||
// Flow content elements from the HTML5 spec (block+inline)
|
||||
flowContent = flowContent || [].concat(blockContent, phrasingContent);
|
||||
|
||||
// HTML4 base schema TODO: Move HTML5 specific attributes to HTML5 specific if statement
|
||||
// Schema items <element name>, <specific attributes>, <children ..>
|
||||
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 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");
|
||||
add("param", "name value");
|
||||
add("map", "name", flowContent, "area");
|
||||
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");
|
||||
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");
|
||||
add("noscript", "", flowContent);
|
||||
|
||||
// Extend with HTML5 elements
|
||||
if (type != "html4") {
|
||||
add("wbr");
|
||||
add("ruby", "", phrasingContent, "rt rp");
|
||||
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", flowContent, "track source");
|
||||
add("audio", "src crossorigin preload autoplay mediagroup loop muted controls", flowContent, "track source");
|
||||
add("source", "src type media");
|
||||
add("track", "kind src srclang label default");
|
||||
add("datalist", "", phrasingContent, "option");
|
||||
add("article section nav aside header footer", "", flowContent);
|
||||
add("hgroup", "", "h1 h2 h3 h4 h5 h6");
|
||||
add("figure", "", flowContent, "figcaption");
|
||||
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");
|
||||
add("keygen", "autofocus challenge disabled form keytype name");
|
||||
}
|
||||
|
||||
// Extend with HTML4 attributes unless it's html5-strict
|
||||
if (type != "html5-strict") {
|
||||
addAttrs("script", "language xml:space");
|
||||
addAttrs("style", "xml:space");
|
||||
addAttrs("object", "declare classid codebase codetype archive standby align border hspace vspace");
|
||||
addAttrs("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");
|
||||
}
|
||||
|
||||
// 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", "srcdoc sandbox seamless allowfullscreen");
|
||||
}
|
||||
|
||||
// Special: iframe, ruby, video, audio, label
|
||||
|
||||
// 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];
|
||||
}
|
||||
});
|
||||
|
||||
// Delete header, footer, sectioning and heading content descendants
|
||||
/*each('dt th address', function(name) {
|
||||
delete schema[name].children[name];
|
||||
});*/
|
||||
|
||||
// Caption can't have tables
|
||||
delete schema.caption.children.table;
|
||||
|
||||
// TODO: LI:s can only have value if parent is OL
|
||||
|
||||
// TODO: Handle transparent elements
|
||||
// a ins del canvas map
|
||||
|
||||
mapCache[type] = schema;
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new Schema instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Schema
|
||||
* @param {Object} settings Name/value settings object.
|
||||
*/
|
||||
return function(settings) {
|
||||
var self = this, elements = {}, children = {}, patternElements = [], validStyles, schemaItems;
|
||||
var whiteSpaceElementsMap, selfClosingElementsMap, shortEndedElementsMap, boolAttrMap;
|
||||
var blockElementsMap, nonEmptyElementsMap, textBlockElementsMap, customElementsMap = {}, specialElements = {};
|
||||
|
||||
// Creates an lookup table map object for the specified option or the default value
|
||||
function createLookupTable(option, default_value, extendWith) {
|
||||
var value = settings[option];
|
||||
|
||||
if (!value) {
|
||||
// Get cached default map or make it if needed
|
||||
value = mapCache[option];
|
||||
|
||||
if (!value) {
|
||||
value = makeMap(default_value, ' ', makeMap(default_value.toUpperCase(), ' '));
|
||||
value = extend(value, extendWith);
|
||||
|
||||
mapCache[option] = value;
|
||||
}
|
||||
} 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 = '*[*]';
|
||||
}
|
||||
|
||||
// Build styles list
|
||||
if (settings.valid_styles) {
|
||||
validStyles = {};
|
||||
|
||||
// Convert styles into a rule list
|
||||
each(settings.valid_styles, function(value, key) {
|
||||
validStyles[key] = explode(value);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup map objects
|
||||
whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script noscript style textarea video audio iframe object');
|
||||
selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr');
|
||||
shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link ' +
|
||||
'meta param embed source wbr track');
|
||||
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', shortEndedElementsMap);
|
||||
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 samp option ' +
|
||||
'datalist select optgroup', textBlockElementsMap);
|
||||
|
||||
each((settings.special || 'script noscript style textarea').split(' '), function(name) {
|
||||
specialElements[name] = new RegExp('<\/' + name + '[^>]*>','gi');
|
||||
});
|
||||
|
||||
// Converts a wildcard expression string to a regexp for example *a will become /.*a/.
|
||||
function patternToRegExp(str) {
|
||||
return new RegExp('^' + str.replace(/([?+*])/g, '.$1') + '$');
|
||||
}
|
||||
|
||||
// Parses the specified valid_elements string and adds to the current rules
|
||||
// This function is a bit hard to read since it's heavily optimized for speed
|
||||
function addValidElements(valid_elements) {
|
||||
var ei, el, ai, al, matches, element, attr, attrData, elementName, attrName, attrType, attributes, attributesOrder,
|
||||
prefix, outputName, globalAttributes, globalAttributesOrder, key, value,
|
||||
elementRuleRegExp = /^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,
|
||||
attrRuleRegExp = /^([!\-])?(\w+::\w+|[^=:<]+)?(?:([=:<])(.*))?$/,
|
||||
hasPatternsRegExp = /[*?+]/;
|
||||
|
||||
if (valid_elements) {
|
||||
// Split valid elements into an array with rules
|
||||
valid_elements = split(valid_elements, ',');
|
||||
|
||||
if (elements['@']) {
|
||||
globalAttributes = elements['@'].attributes;
|
||||
globalAttributesOrder = elements['@'].attributesOrder;
|
||||
}
|
||||
|
||||
// Loop all rules
|
||||
for (ei = 0, el = valid_elements.length; ei < el; ei++) {
|
||||
// Parse element rule
|
||||
matches = elementRuleRegExp.exec(valid_elements[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
|
||||
};
|
||||
|
||||
// Padd empty elements prefix
|
||||
if (prefix === '#') {
|
||||
element.paddEmpty = true;
|
||||
}
|
||||
|
||||
// Remove empty elements prefix
|
||||
if (prefix === '-') {
|
||||
element.removeEmpty = true;
|
||||
}
|
||||
|
||||
if (matches[4] === '!') {
|
||||
element.removeEmptyAttrs = true;
|
||||
}
|
||||
|
||||
// Copy attributes from global rule into current rule
|
||||
if (globalAttributes) {
|
||||
for (key in globalAttributes) {
|
||||
attributes[key] = globalAttributes[key];
|
||||
}
|
||||
|
||||
attributesOrder.push.apply(attributesOrder, globalAttributesOrder);
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
// Required
|
||||
if (attrType === '!') {
|
||||
element.attributesRequired = element.attributesRequired || [];
|
||||
element.attributesRequired.push(attrName);
|
||||
attr.required = true;
|
||||
}
|
||||
|
||||
// Denied from global
|
||||
if (attrType === '-') {
|
||||
delete attributes[attrName];
|
||||
attributesOrder.splice(inArray(attributesOrder, attrName), 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default value
|
||||
if (prefix) {
|
||||
// Default value
|
||||
if (prefix === '=') {
|
||||
element.attributesDefault = element.attributesDefault || [];
|
||||
element.attributesDefault.push({name: attrName, value: value});
|
||||
attr.defaultValue = value;
|
||||
}
|
||||
|
||||
// Forced value
|
||||
if (prefix === ':') {
|
||||
element.attributesForced = element.attributesForced || [];
|
||||
element.attributesForced.push({name: attrName, value: value});
|
||||
attr.forcedValue = value;
|
||||
}
|
||||
|
||||
// Required values
|
||||
if (prefix === '<') {
|
||||
attr.validValues = makeMap(value, '?');
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
attributes[attrName] = attr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global rule, store away these for later usage
|
||||
if (!globalAttributes && elementName == '@') {
|
||||
globalAttributes = attributes;
|
||||
globalAttributesOrder = attributesOrder;
|
||||
}
|
||||
|
||||
// Handle substitute elements such as b/strong
|
||||
if (outputName) {
|
||||
element.outputName = elementName;
|
||||
elements[outputName] = element;
|
||||
}
|
||||
|
||||
// Add pattern or exact element
|
||||
if (hasPatternsRegExp.test(elementName)) {
|
||||
element.pattern = patternToRegExp(elementName);
|
||||
patternElements.push(element);
|
||||
} else {
|
||||
elements[elementName] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setValidElements(valid_elements) {
|
||||
elements = {};
|
||||
patternElements = [];
|
||||
|
||||
addValidElements(valid_elements);
|
||||
|
||||
each(schemaItems, function(element, name) {
|
||||
children[name] = element.children;
|
||||
});
|
||||
}
|
||||
|
||||
// Adds custom non HTML elements to the schema
|
||||
function addCustomElements(custom_elements) {
|
||||
var customElementRegExp = /^(~)?(.+)$/;
|
||||
|
||||
if (custom_elements) {
|
||||
each(split(custom_elements, ','), 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) {
|
||||
if (element[cloneName]) {
|
||||
element[name] = element[cloneName];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Adds valid children to the schema object
|
||||
function addValidChildren(valid_children) {
|
||||
var childRuleRegExp = /^([+\-]?)(\w+)\[([^\]]+)\]$/;
|
||||
|
||||
if (valid_children) {
|
||||
each(split(valid_children, ','), 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': {}};
|
||||
}
|
||||
|
||||
parent = children[matches[2]];
|
||||
|
||||
each(split(matches[3], '|'), function(child) {
|
||||
if (prefix === '-') {
|
||||
delete parent[child];
|
||||
} else {
|
||||
parent[child] = {};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getElementRule(name) {
|
||||
var element = elements[name], i;
|
||||
|
||||
// Exact match found
|
||||
if (element) {
|
||||
return element;
|
||||
}
|
||||
|
||||
// No exact match then try the patterns
|
||||
i = patternElements.length;
|
||||
while (i--) {
|
||||
element = patternElements[i];
|
||||
|
||||
if (element.pattern.test(name)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
children[name] = element.children;
|
||||
});
|
||||
|
||||
// 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];
|
||||
});
|
||||
}
|
||||
|
||||
// Add default alt attribute for images
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
// Remove these by default
|
||||
// TODO: Reenable in 4.1
|
||||
/*each(split('script style'), function(name) {
|
||||
delete elements[name];
|
||||
});*/
|
||||
} else {
|
||||
setValidElements(settings.valid_elements);
|
||||
}
|
||||
|
||||
addCustomElements(settings.custom_elements);
|
||||
addValidChildren(settings.valid_children);
|
||||
addValidElements(settings.extended_valid_elements);
|
||||
|
||||
// Todo: Remove this when we fix list handling to be valid
|
||||
addValidChildren('+ol[ul|ol],+ul[ul|ol]');
|
||||
|
||||
// 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|*]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Name/value map object with valid parents and children to those parents.
|
||||
*
|
||||
* @example
|
||||
* children = {
|
||||
* div:{p:{}, h1:{}}
|
||||
* };
|
||||
* @field children
|
||||
* @type Object
|
||||
*/
|
||||
self.children = children;
|
||||
|
||||
/**
|
||||
* Name/value map object with valid styles for each element.
|
||||
*
|
||||
* @field styles
|
||||
* @type Object
|
||||
*/
|
||||
self.styles = validStyles;
|
||||
|
||||
/**
|
||||
* Returns a map with boolean attributes.
|
||||
*
|
||||
* @method getBoolAttrs
|
||||
* @return {Object} Name/value lookup map for boolean attributes.
|
||||
*/
|
||||
self.getBoolAttrs = function() {
|
||||
return boolAttrMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with block elements.
|
||||
*
|
||||
* @method getBlockElements
|
||||
* @return {Object} Name/value lookup map for block elements.
|
||||
*/
|
||||
self.getBlockElements = function() {
|
||||
return blockElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with self closing tags such as <li>.
|
||||
*
|
||||
* @method getSelfClosingElements
|
||||
* @return {Object} Name/value lookup map for self closing tags elements.
|
||||
*/
|
||||
self.getSelfClosingElements = function() {
|
||||
return selfClosingElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 getNonEmptyElements
|
||||
* @return {Object} Name/value lookup map for non empty elements.
|
||||
*/
|
||||
self.getNonEmptyElements = function() {
|
||||
return nonEmptyElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map with elements where white space is to be preserved like PRE or SCRIPT.
|
||||
*
|
||||
* @method getWhiteSpaceElements
|
||||
* @return {Object} Name/value lookup map for white space elements.
|
||||
*/
|
||||
self.getWhiteSpaceElements = function() {
|
||||
return whiteSpaceElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true/false if the specified element and it's child is valid or not
|
||||
* according to the schema.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
self.isValidChild = function(name, child) {
|
||||
var parent = children[name];
|
||||
|
||||
return !!(parent && parent[child]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true/false if the specified element name and optional attribute is
|
||||
* valid according to the schema.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
self.isValid = function(name, attr) {
|
||||
var attrPatterns, i, rule = getElementRule(name);
|
||||
|
||||
// Check if it's a valid element
|
||||
if (rule) {
|
||||
if (attr) {
|
||||
// Check if attribute name exists
|
||||
if (rule.attributes[attr]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Returns an map object of all custom elements.
|
||||
*
|
||||
* @method getCustomElements
|
||||
* @return {Object} Name/value map object of all custom elements.
|
||||
*/
|
||||
self.getCustomElements = function() {
|
||||
return customElementsMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
self.elements = elements;
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Serializer.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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('<p>text</p>'));
|
||||
* @class tinymce.html.Serializer
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Serializer", [
|
||||
"tinymce/html/Writer",
|
||||
"tinymce/html/Schema"
|
||||
], function(Writer, Schema) {
|
||||
/**
|
||||
* Constructs a new Serializer instance.
|
||||
*
|
||||
* @constructor
|
||||
* @method Serializer
|
||||
* @param {Object} settings Name/value settings object.
|
||||
* @param {tinymce.html.Schema} schema Schema instance to use.
|
||||
*/
|
||||
return function(settings, schema) {
|
||||
var self = this, writer = new Writer(settings);
|
||||
|
||||
settings = settings || {};
|
||||
settings.validate = "validate" in settings ? settings.validate : true;
|
||||
|
||||
self.schema = schema = schema || new Schema();
|
||||
self.writer = writer;
|
||||
|
||||
/**
|
||||
* Serializes the specified node into a string.
|
||||
*
|
||||
* @example
|
||||
* new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('<p>text</p>'));
|
||||
* @method serialize
|
||||
* @param {tinymce.html.Node} node Node instance to serialize.
|
||||
* @return {String} String with HTML based on DOM tree.
|
||||
*/
|
||||
self.serialize = function(node) {
|
||||
var handlers, validate;
|
||||
|
||||
validate = settings.validate;
|
||||
|
||||
handlers = {
|
||||
// #text
|
||||
3: function(node) {
|
||||
writer.text(node.value, node.raw);
|
||||
},
|
||||
|
||||
// #comment
|
||||
8: function(node) {
|
||||
writer.comment(node.value);
|
||||
},
|
||||
|
||||
// Processing instruction
|
||||
7: function(node) {
|
||||
writer.pi(node.name, node.value);
|
||||
},
|
||||
|
||||
// Doctype
|
||||
10: function(node) {
|
||||
writer.doctype(node.value);
|
||||
},
|
||||
|
||||
// CDATA
|
||||
4: function(node) {
|
||||
writer.cdata(node.value);
|
||||
},
|
||||
|
||||
// Document fragment
|
||||
11: function(node) {
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
walk(node);
|
||||
} while ((node = node.next));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
writer.reset();
|
||||
|
||||
function walk(node) {
|
||||
var handler = handlers[node.type], name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule;
|
||||
|
||||
if (!handler) {
|
||||
name = node.name;
|
||||
isEmpty = node.shortEnded;
|
||||
attrs = node.attributes;
|
||||
|
||||
// Sort attributes
|
||||
if (validate && attrs && attrs.length > 1) {
|
||||
sortedAttrs = [];
|
||||
sortedAttrs.map = {};
|
||||
|
||||
elementRule = schema.getElementRule(node.name);
|
||||
for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) {
|
||||
attrName = elementRule.attributesOrder[i];
|
||||
|
||||
if (attrName in attrs.map) {
|
||||
attrValue = attrs.map[attrName];
|
||||
sortedAttrs.map[attrName] = attrValue;
|
||||
sortedAttrs.push({name: attrName, value: attrValue});
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0, l = attrs.length; i < l; i++) {
|
||||
attrName = attrs[i].name;
|
||||
|
||||
if (!(attrName in sortedAttrs.map)) {
|
||||
attrValue = attrs.map[attrName];
|
||||
sortedAttrs.map[attrName] = attrValue;
|
||||
sortedAttrs.push({name: attrName, value: attrValue});
|
||||
}
|
||||
}
|
||||
|
||||
attrs = sortedAttrs;
|
||||
}
|
||||
|
||||
writer.start(node.name, attrs, isEmpty);
|
||||
|
||||
if (!isEmpty) {
|
||||
if ((node = node.firstChild)) {
|
||||
do {
|
||||
walk(node);
|
||||
} while ((node = node.next));
|
||||
}
|
||||
|
||||
writer.end(name);
|
||||
}
|
||||
} else {
|
||||
handler(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize element and treat all non elements as fragments
|
||||
if (node.type == 1 && !settings.inner) {
|
||||
walk(node);
|
||||
} else {
|
||||
handlers[11](node);
|
||||
}
|
||||
|
||||
return writer.getContent();
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Styles.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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/html/Styles", [], function() {
|
||||
return function(settings, schema) {
|
||||
/*jshint maxlen:255 */
|
||||
var rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,
|
||||
urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,
|
||||
styleRegExp = /\s*([^:]+):\s*([^;]+);?/g,
|
||||
trimRightRegExp = /\s+$/,
|
||||
undef, i, encodingLookup = {}, encodingItems, invisibleChar = '\uFEFF';
|
||||
|
||||
settings = settings || {};
|
||||
|
||||
encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' ');
|
||||
for (i = 0; i < encodingItems.length; i++) {
|
||||
encodingLookup[encodingItems[i]] = invisibleChar + i;
|
||||
encodingLookup[invisibleChar + i] = encodingItems[i];
|
||||
}
|
||||
|
||||
function toHex(match, r, g, b) {
|
||||
function hex(val) {
|
||||
val = parseInt(val, 10).toString(16);
|
||||
|
||||
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) {
|
||||
var top, right, bottom, left;
|
||||
|
||||
// IE 11 will produce a border-image: none when getting the style attribute from <p style="border: 1px solid red"></p>
|
||||
// So lets asume it shouldn't be there
|
||||
if (styles['border-image'] === 'none') {
|
||||
delete styles['border-image'];
|
||||
}
|
||||
|
||||
// Get values and check it it needs compressing
|
||||
top = styles[prefix + '-top' + suffix];
|
||||
if (!top) {
|
||||
return;
|
||||
}
|
||||
|
||||
right = styles[prefix + '-right' + suffix];
|
||||
if (top != right) {
|
||||
return;
|
||||
}
|
||||
|
||||
bottom = styles[prefix + '-bottom' + suffix];
|
||||
if (right != bottom) {
|
||||
return;
|
||||
}
|
||||
|
||||
left = styles[prefix + '-left' + suffix];
|
||||
if (bottom != left) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compress
|
||||
styles[prefix + suffix] = left;
|
||||
delete styles[prefix + '-top' + suffix];
|
||||
delete styles[prefix + '-right' + suffix];
|
||||
delete styles[prefix + '-bottom' + suffix];
|
||||
delete styles[prefix + '-left' + suffix];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || value.indexOf(' ') < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = value.split(' ');
|
||||
i = value.length;
|
||||
while (i--) {
|
||||
if (value[i] !== value[0]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
styles[key] = value[0];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses multiple styles into one style.
|
||||
*/
|
||||
function compress2(target, a, b, c) {
|
||||
if (!canCompress(a)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canCompress(b)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canCompress(c)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compress
|
||||
styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c];
|
||||
delete styles[a];
|
||||
delete styles[b];
|
||||
delete styles[c];
|
||||
}
|
||||
|
||||
// Encodes the specified string by replacing all \" \' ; : with _<num>
|
||||
function encode(str) {
|
||||
isEncoded = true;
|
||||
|
||||
return encodingLookup[str];
|
||||
}
|
||||
|
||||
// Decodes the specified string by replacing all _<num> with it's original value \" \' etc
|
||||
// It will also decode the \" \' if keep_slashes is set to fale or omitted
|
||||
function decode(str, keep_slashes) {
|
||||
if (isEncoded) {
|
||||
str = str.replace(/\uFEFF[0-9]/g, function(str) {
|
||||
return encodingLookup[str];
|
||||
});
|
||||
}
|
||||
|
||||
if (!keep_slashes) {
|
||||
str = str.replace(/\\([\'\";:])/g, "$1");
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function processUrl(match, url, url2, url3, str, str2) {
|
||||
str = str || str2;
|
||||
|
||||
if (str) {
|
||||
str = decode(str);
|
||||
|
||||
// Force strings into single quote format
|
||||
return "'" + str.replace(/\'/g, "\\'") + "'";
|
||||
}
|
||||
|
||||
url = decode(url || url2 || url3);
|
||||
|
||||
if (!settings.allow_script_urls && /(java|vb)script:/i.test(url.replace(/[\s\r\n]+/, ''))) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Convert the URL to relative/absolute depending on config
|
||||
if (urlConverter) {
|
||||
url = urlConverter.call(urlConverterScope, url, 'style');
|
||||
}
|
||||
|
||||
// Output new URL format
|
||||
return "url('" + url.replace(/\'/g, "\\'") + "')";
|
||||
}
|
||||
|
||||
if (css) {
|
||||
css = css.replace(/[\u0000-\u001F]/g, '');
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Parse styles
|
||||
while ((matches = styleRegExp.exec(css))) {
|
||||
name = matches[1].replace(trimRightRegExp, '').toLowerCase();
|
||||
value = matches[2].replace(trimRightRegExp, '');
|
||||
|
||||
if (name && value.length > 0) {
|
||||
if (!settings.allow_script_urls && (name == "behavior" || /expression\s*\(/.test(value))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Convert RGB colors to HEX
|
||||
value = value.replace(rgbRegExp, toHex);
|
||||
|
||||
// Convert URLs and force them into url('value') format
|
||||
value = value.replace(urlOrStrRegExp, processUrl);
|
||||
styles[name] = isEncoded ? decode(value, true) : value;
|
||||
}
|
||||
|
||||
styleRegExp.lastIndex = matches.index + matches[0].length;
|
||||
}
|
||||
|
||||
// Compress the styles to reduce it's size for example IE will expand styles
|
||||
compress("border", "");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return styles;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes the specified style object into a string.
|
||||
*
|
||||
* @method serialize
|
||||
* @param {Object} styles Object to serialize as string for example: {border: '1px solid red'}
|
||||
* @param {String} element_name Optional element name, if specified only the styles that matches the schema will be serialized.
|
||||
* @return {String} String representation of the style object for example: border: 1px solid red.
|
||||
*/
|
||||
serialize: function(styles, element_name) {
|
||||
var css = '', name, value;
|
||||
|
||||
function serializeStyles(name) {
|
||||
var styleList, i, l, value;
|
||||
|
||||
styleList = schema.styles[name];
|
||||
if (styleList) {
|
||||
for (i = 0, l = styleList.length; i < l; i++) {
|
||||
name = styleList[i];
|
||||
value = styles[name];
|
||||
|
||||
if (value !== undef && value.length > 0) {
|
||||
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize styles according to schema
|
||||
if (element_name && schema && schema.styles) {
|
||||
// Serialize global styles and element specific styles
|
||||
serializeStyles('*');
|
||||
serializeStyles(element_name);
|
||||
} else {
|
||||
// Output the styles in the order they are inside the object
|
||||
for (name in styles) {
|
||||
value = styles[name];
|
||||
|
||||
if (value !== undef && value.length > 0) {
|
||||
css += (css.length > 0 ? ' ' : '') + name + ': ' + value + ';';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Writer.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is used to write HTML tags out it can be used with the Serializer or the SaxParser.
|
||||
*
|
||||
* @class tinymce.html.Writer
|
||||
* @example
|
||||
* var writer = new tinymce.html.Writer({indent: true});
|
||||
* var parser = new tinymce.html.SaxParser(writer).parse('<p><br></p>');
|
||||
* console.log(writer.getContent());
|
||||
*
|
||||
* @class tinymce.html.Writer
|
||||
* @version 3.4
|
||||
*/
|
||||
define("tinymce/html/Writer", [
|
||||
"tinymce/html/Entities",
|
||||
"tinymce/util/Tools"
|
||||
], function(Entities, Tools) {
|
||||
var makeMap = Tools.makeMap;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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";
|
||||
|
||||
return {
|
||||
/**
|
||||
* Writes the a start element such as <p id="a">.
|
||||
*
|
||||
* @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 <br />.
|
||||
*/
|
||||
start: function(name, attrs, empty) {
|
||||
var i, l, attr, value;
|
||||
|
||||
if (indent && indentBefore[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n') {
|
||||
html.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
html.push('<', name);
|
||||
|
||||
if (attrs) {
|
||||
for (i = 0, l = attrs.length; i < l; i++) {
|
||||
attr = attrs[i];
|
||||
html.push(' ', attr.name, '="', encode(attr.value, true), '"');
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty || htmlOutput) {
|
||||
html[html.length] = '>';
|
||||
} else {
|
||||
html[html.length] = ' />';
|
||||
}
|
||||
|
||||
if (empty && indent && indentAfter[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n') {
|
||||
html.push('\n');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes the a end element such as </p>.
|
||||
*
|
||||
* @method end
|
||||
* @param {String} name Name of the element.
|
||||
*/
|
||||
end: function(name) {
|
||||
var value;
|
||||
|
||||
/*if (indent && indentBefore[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n')
|
||||
html.push('\n');
|
||||
}*/
|
||||
|
||||
html.push('</', name, '>');
|
||||
|
||||
if (indent && indentAfter[name] && html.length > 0) {
|
||||
value = html[html.length - 1];
|
||||
|
||||
if (value.length > 0 && value !== '\n') {
|
||||
html.push('\n');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 <![CDATA[data]]>.
|
||||
*
|
||||
* @method cdata
|
||||
* @param {String} text String to write out inside the cdata.
|
||||
*/
|
||||
cdata: function(text) {
|
||||
html.push('<![CDATA[', text, ']]>');
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a comment node such as <!-- Comment -->.
|
||||
*
|
||||
* @method cdata
|
||||
* @param {String} text String to write out inside the comment.
|
||||
*/
|
||||
comment: function(text) {
|
||||
html.push('<!--', text, '-->');
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a PI node such as <?xml attr="value" ?>.
|
||||
*
|
||||
* @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('<?', name, ' ', text, '?>');
|
||||
} else {
|
||||
html.push('<?', name, '?>');
|
||||
}
|
||||
|
||||
if (indent) {
|
||||
html.push('\n');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Writes a doctype node such as <!DOCTYPE data>.
|
||||
*
|
||||
* @method doctype
|
||||
* @param {String} text String to write out inside the doctype.
|
||||
*/
|
||||
doctype: function(text) {
|
||||
html.push('<!DOCTYPE', text, '>', 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$/, '');
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* jquery.tinymce.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true, jQuery */
|
||||
|
||||
(function($) {
|
||||
var undef,
|
||||
lazyLoading,
|
||||
delayedInits = [],
|
||||
win = window;
|
||||
|
||||
$.fn.tinymce = function(settings) {
|
||||
var self = this, url, base, lang, suffix = "";
|
||||
|
||||
// No match then just ignore the call
|
||||
if (!self.length) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Get editor instance
|
||||
if (!settings) {
|
||||
return tinymce.get(self[0].id);
|
||||
}
|
||||
|
||||
self.css('visibility', 'hidden'); // Hide textarea to avoid flicker
|
||||
|
||||
function init() {
|
||||
var editors = [], initCount = 0;
|
||||
|
||||
// Apply patches to the jQuery object, only once
|
||||
if (applyPatch) {
|
||||
applyPatch();
|
||||
applyPatch = null;
|
||||
}
|
||||
|
||||
// Create an editor instance for each matched node
|
||||
self.each(function(i, node) {
|
||||
var ed, id = node.id, oninit = settings.oninit;
|
||||
|
||||
// Generate unique id for target element if needed
|
||||
if (!id) {
|
||||
node.id = id = tinymce.DOM.uniqueId();
|
||||
}
|
||||
|
||||
// Only init the editor once
|
||||
if (tinymce.get(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create editor instance and render it
|
||||
ed = new tinymce.Editor(id, settings, tinymce.EditorManager);
|
||||
editors.push(ed);
|
||||
|
||||
ed.on('init', function() {
|
||||
var scope, func = oninit;
|
||||
|
||||
self.css('visibility', '');
|
||||
|
||||
// Run this if the oninit setting is defined
|
||||
// this logic will fire the oninit callback ones each
|
||||
// matched editor instance is initialized
|
||||
if (oninit) {
|
||||
// Fire the oninit event ones each editor instance is initialized
|
||||
if (++initCount == editors.length) {
|
||||
if (typeof(func) === "string") {
|
||||
scope = (func.indexOf(".") === -1) ? null : tinymce.resolve(func.replace(/\.\w+$/, ""));
|
||||
func = tinymce.resolve(func);
|
||||
}
|
||||
|
||||
// Call the oninit function with the object
|
||||
func.apply(scope || tinymce, editors);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Render the editor instances in a separate loop since we
|
||||
// need to have the full editors array used in the onInit calls
|
||||
$.each(editors, function(i, ed) {
|
||||
ed.render();
|
||||
});
|
||||
}
|
||||
|
||||
// Load TinyMCE on demand, if we need to
|
||||
if (!win.tinymce && !lazyLoading && (url = settings.script_url)) {
|
||||
lazyLoading = 1;
|
||||
base = url.substring(0, url.lastIndexOf("/"));
|
||||
|
||||
// Check if it's a dev/src version they want to load then
|
||||
// make sure that all plugins, themes etc are loaded in source mode as well
|
||||
if (url.indexOf('.min') != -1) {
|
||||
suffix = ".min";
|
||||
}
|
||||
|
||||
// Setup tinyMCEPreInit object this will later be used by the TinyMCE
|
||||
// core script to locate other resources like CSS files, dialogs etc
|
||||
// You can also predefined a tinyMCEPreInit object and then it will use that instead
|
||||
win.tinymce = win.tinyMCEPreInit || {
|
||||
base: base,
|
||||
suffix: suffix
|
||||
};
|
||||
|
||||
// url contains gzip then we assume it's a compressor
|
||||
if (url.indexOf('gzip') != -1) {
|
||||
lang = settings.language || "en";
|
||||
url = url + (/\?/.test(url) ? '&' : '?') + "js=true&core=true&suffix=" + escape(suffix) +
|
||||
"&themes=" + escape(settings.theme || '') + "&plugins=" + escape(settings.plugins || '') + "&languages=" + (lang || '');
|
||||
|
||||
// Check if compressor script is already loaded otherwise setup a basic one
|
||||
if (!win.tinyMCE_GZ) {
|
||||
win.tinyMCE_GZ = {
|
||||
start: function() {
|
||||
function load(url) {
|
||||
tinymce.ScriptLoader.markDone(tinymce.baseURI.toAbsolute(url));
|
||||
}
|
||||
|
||||
// Add core languages
|
||||
load("langs/" + lang + ".js");
|
||||
|
||||
// Add themes with languages
|
||||
load("themes/" + settings.theme + "/theme" + suffix + ".js");
|
||||
load("themes/" + settings.theme + "/langs/" + lang + ".js");
|
||||
|
||||
// Add plugins with languages
|
||||
$.each(settings.plugins.split(","), function(i, name) {
|
||||
if (name) {
|
||||
load("plugins/" + name + "/plugin" + suffix + ".js");
|
||||
load("plugins/" + name + "/langs/" + lang + ".js");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
end: function() {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.onload = script.onreadystatechange = function(e) {
|
||||
e = e || event;
|
||||
|
||||
if (lazyLoading !== 2 && (e.type == 'load' || /complete|loaded/.test(script.readyState))) {
|
||||
tinymce.dom.Event.domLoaded = 1;
|
||||
lazyLoading = 2;
|
||||
|
||||
// Execute callback after mainscript has been loaded and before the initialization occurs
|
||||
if (settings.script_loaded) {
|
||||
settings.script_loaded();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
$.each(delayedInits, function(i, init) {
|
||||
init();
|
||||
});
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
// Delay the init call until tinymce is loaded
|
||||
if (lazyLoading === 1) {
|
||||
delayedInits.push(init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
// Add :tinymce psuedo selector this will select elements that has been converted into editor instances
|
||||
// it's now possible to use things like $('*:tinymce') to get all TinyMCE bound elements.
|
||||
$.extend($.expr[":"], {
|
||||
tinymce: function(e) {
|
||||
return !!(e.id && "tinymce" in window && tinymce.get(e.id));
|
||||
}
|
||||
});
|
||||
|
||||
// This function patches internal jQuery functions so that if
|
||||
// you for example remove an div element containing an editor it's
|
||||
// automatically destroyed by the TinyMCE API
|
||||
function applyPatch() {
|
||||
// Removes any child editor instances by looking for editor wrapper elements
|
||||
function removeEditors(name) {
|
||||
// If the function is remove
|
||||
if (name === "remove") {
|
||||
this.each(function(i, node) {
|
||||
var ed = tinyMCEInstance(node);
|
||||
|
||||
if (ed) {
|
||||
ed.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.find("span.mceEditor,div.mceEditor").each(function(i, node) {
|
||||
var ed = tinymce.get(node.id.replace(/_parent$/, ""));
|
||||
|
||||
if (ed) {
|
||||
ed.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Loads or saves contents from/to textarea if the value
|
||||
// argument is defined it will set the TinyMCE internal contents
|
||||
function loadOrSave(value) {
|
||||
var self = this, ed;
|
||||
|
||||
// Handle set value
|
||||
/*jshint eqnull:true */
|
||||
if (value != null) {
|
||||
removeEditors.call(self);
|
||||
|
||||
// Saves the contents before get/set value of textarea/div
|
||||
self.each(function(i, node) {
|
||||
var ed;
|
||||
|
||||
if ((ed = tinymce.get(node.id))) {
|
||||
ed.setContent(value);
|
||||
}
|
||||
});
|
||||
} else if (self.length > 0) {
|
||||
// Handle get value
|
||||
if ((ed = tinymce.get(self[0].id))) {
|
||||
return ed.getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns tinymce instance for the specified element or null if it wasn't found
|
||||
function tinyMCEInstance(element) {
|
||||
var ed = null;
|
||||
|
||||
if (element && element.id && win.tinymce) {
|
||||
ed = tinymce.get(element.id);
|
||||
}
|
||||
|
||||
return ed;
|
||||
}
|
||||
|
||||
// Checks if the specified set contains tinymce instances
|
||||
function containsTinyMCE(matchedSet) {
|
||||
return !!((matchedSet) && (matchedSet.length) && (win.tinymce) && (matchedSet.is(":tinymce")));
|
||||
}
|
||||
|
||||
// Patch various jQuery functions
|
||||
var jQueryFn = {};
|
||||
|
||||
// Patch some setter/getter functions these will
|
||||
// now be able to set/get the contents of editor instances for
|
||||
// example $('#editorid').html('Content'); will update the TinyMCE iframe instance
|
||||
$.each(["text", "html", "val"], function(i, name) {
|
||||
var origFn = jQueryFn[name] = $.fn[name],
|
||||
textProc = (name === "text");
|
||||
|
||||
$.fn[name] = function(value) {
|
||||
var self = this;
|
||||
|
||||
if (!containsTinyMCE(self)) {
|
||||
return origFn.apply(self, arguments);
|
||||
}
|
||||
|
||||
if (value !== undef) {
|
||||
loadOrSave.call(self.filter(":tinymce"), value);
|
||||
origFn.apply(self.not(":tinymce"), arguments);
|
||||
|
||||
return self; // return original set for chaining
|
||||
} else {
|
||||
var ret = "";
|
||||
var args = arguments;
|
||||
|
||||
(textProc ? self : self.eq(0)).each(function(i, node) {
|
||||
var ed = tinyMCEInstance(node);
|
||||
|
||||
if (ed) {
|
||||
ret += textProc ? ed.getContent().replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, "") : ed.getContent({save: true});
|
||||
} else {
|
||||
ret += origFn.apply($(node), args);
|
||||
}
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Makes it possible to use $('#id').append("content"); to append contents to the TinyMCE editor iframe
|
||||
$.each(["append", "prepend"], function(i, name) {
|
||||
var origFn = jQueryFn[name] = $.fn[name],
|
||||
prepend = (name === "prepend");
|
||||
|
||||
$.fn[name] = function(value) {
|
||||
var self = this;
|
||||
|
||||
if (!containsTinyMCE(self)) {
|
||||
return origFn.apply(self, arguments);
|
||||
}
|
||||
|
||||
if (value !== undef) {
|
||||
self.filter(":tinymce").each(function(i, node) {
|
||||
var ed = tinyMCEInstance(node);
|
||||
|
||||
if (ed) {
|
||||
ed.setContent(prepend ? value + ed.getContent() : ed.getContent() + value);
|
||||
}
|
||||
});
|
||||
|
||||
origFn.apply(self.not(":tinymce"), arguments);
|
||||
|
||||
return self; // return original set for chaining
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Makes sure that the editor instance gets properly destroyed when the parent element is removed
|
||||
$.each(["remove", "replaceWith", "replaceAll", "empty"], function(i, name) {
|
||||
var origFn = jQueryFn[name] = $.fn[name];
|
||||
|
||||
$.fn[name] = function() {
|
||||
removeEditors.call(this, name);
|
||||
|
||||
return origFn.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
|
||||
jQueryFn.attr = $.fn.attr;
|
||||
|
||||
// Makes sure that $('#tinymce_id').attr('value') gets the editors current HTML contents
|
||||
$.fn.attr = function(name, value) {
|
||||
var self = this, args = arguments;
|
||||
|
||||
if ((!name) || (name !== "value") || (!containsTinyMCE(self))) {
|
||||
if (value !== undef) {
|
||||
return jQueryFn.attr.apply(self, args);
|
||||
} else {
|
||||
return jQueryFn.attr.apply(self, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (value !== undef) {
|
||||
loadOrSave.call(self.filter(":tinymce"), value);
|
||||
jQueryFn.attr.apply(self.not(":tinymce"), args);
|
||||
|
||||
return self; // return original set for chaining
|
||||
} else {
|
||||
var node = self[0], ed = tinyMCEInstance(node);
|
||||
|
||||
return ed ? ed.getContent({save: true}) : jQueryFn.attr.apply($(node), args);
|
||||
}
|
||||
};
|
||||
}
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* AbsoluteLayout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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 '<div id="' + container._id + '-absend" class="' + container.classPrefix + 'abs-end"></div>' + this._super(container);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Button.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/Widget"
|
||||
], function(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.on('click mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
self._super(settings);
|
||||
size = settings.size;
|
||||
|
||||
if (settings.subtype) {
|
||||
self.addClass(settings.subtype);
|
||||
}
|
||||
|
||||
if (size) {
|
||||
self.addClass('btn-' + size);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Repaints the button for example after it's been resizes by a layout engine.
|
||||
*
|
||||
* @method repaint
|
||||
*/
|
||||
repaint: function() {
|
||||
var btnStyle = this.getEl().firstChild.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.settings.icon, image = '';
|
||||
|
||||
if (self.settings.image) {
|
||||
icon = 'none';
|
||||
image = ' style="background-image: url(\'' + self.settings.image + '\')"';
|
||||
}
|
||||
|
||||
icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '';
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' +
|
||||
'<button role="presentation" type="button" tabindex="-1">' +
|
||||
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
|
||||
(self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* ButtonGroup.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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: 'toolbar'
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the control as a HTML string.
|
||||
*
|
||||
* @method renderHtml
|
||||
* @return {String} HTML representing the control.
|
||||
*/
|
||||
renderHtml: function() {
|
||||
var self = this, layout = self._layout;
|
||||
|
||||
self.addClass('btn-group');
|
||||
self.preRender();
|
||||
layout.preRender(self);
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '">'+
|
||||
'<div id="' + self._id + '-body">'+
|
||||
(self.settings.html || '') + layout.renderHtml(self) +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Checkbox.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.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", [
|
||||
"tinymce/ui/Widget"
|
||||
], function(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) {
|
||||
var self = this;
|
||||
|
||||
if (typeof state != "undefined") {
|
||||
if (state) {
|
||||
self.addClass('checked');
|
||||
} else {
|
||||
self.removeClass('checked');
|
||||
}
|
||||
|
||||
self._checked = state;
|
||||
self.aria('checked', state);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return self._checked;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 (
|
||||
'<div id="' + id + '" class="' + self.classes() + '" unselectable="on" aria-labeledby="' + id + '-al" tabindex="-1">' +
|
||||
'<i class="' + prefix + 'ico ' + prefix + 'i-checkbox"></i>' +
|
||||
'<span id="' + id +'-al" class="' + prefix + 'label">' + self.encode(self._text) + '</span>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* Collection.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/util/Tools",
|
||||
"tinymce/ui/Selector",
|
||||
"tinymce/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].hasClass(cls) : false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the specific property on the items in the collection. The same as executing control.<property>(<value>);
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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) {}, -- Generated by code below
|
||||
|
||||
/**
|
||||
* 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) {}, -- Generated by code below
|
||||
};
|
||||
|
||||
// Extend tinymce.ui.Collection prototype with some generated control specific methods
|
||||
Tools.each('fire on off show hide addClass removeClass 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;
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* ColorButton.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/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.addClass('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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
|
||||
var image = self.settings.image ? ' style="background-image: url(\'' + self.settings.image + '\')"' : '';
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '">' +
|
||||
'<button role="presentation" hidefocus type="button" tabindex="-1">' +
|
||||
(icon ? '<i class="' + icon + '"' + image + '></i>' : '') +
|
||||
'<span id="' + id + '-preview" class="' + prefix + 'preview"></span>' +
|
||||
(self._text ? (icon ? ' ' : '') + (self._text) : '') +
|
||||
'</button>' +
|
||||
'<button type="button" class="' + prefix + 'open" hidefocus tabindex="-1">' +
|
||||
' <i class="' + prefix + 'caret"></i>' +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.control == self && !DOM.getParent(e.target, '.' + self.classPrefix + 'open')) {
|
||||
e.stopImmediatePropagation();
|
||||
onClickHandler.call(self, e);
|
||||
}
|
||||
});
|
||||
|
||||
delete self.settings.onclick;
|
||||
|
||||
return self._super();
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* ComboBox.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/Widget",
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(Widget, DomUtils) {
|
||||
"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);
|
||||
self.addClass('combobox');
|
||||
|
||||
self.on('click', function(e) {
|
||||
var elm = e.target;
|
||||
|
||||
while (elm) {
|
||||
if (elm.id && elm.id.indexOf('-open') != -1) {
|
||||
self.fire('action');
|
||||
}
|
||||
|
||||
elm = elm.parentNode;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Rework this
|
||||
self.on('keydown', function(e) {
|
||||
if (e.target.nodeName == "INPUT" && e.keyCode == 13) {
|
||||
self.parents().reverse().each(function(ctrl) {
|
||||
e.preventDefault();
|
||||
self.fire('change');
|
||||
|
||||
if (ctrl.hasEventListeners('submit') && ctrl.toJSON) {
|
||||
ctrl.fire('submit', {data: ctrl.toJSON()});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (settings.placeholder) {
|
||||
self.addClass('placeholder');
|
||||
|
||||
self.on('focusin', function() {
|
||||
if (!self._hasOnChange) {
|
||||
DomUtils.on(self.getEl('inp'), 'change', function() {
|
||||
self.fire('change');
|
||||
});
|
||||
|
||||
self._hasOnChange = true;
|
||||
}
|
||||
|
||||
if (self.hasClass('placeholder')) {
|
||||
self.getEl('inp').value = '';
|
||||
self.removeClass('placeholder');
|
||||
}
|
||||
});
|
||||
|
||||
self.on('focusout', function() {
|
||||
if (self.value().length === 0) {
|
||||
self.getEl('inp').value = settings.placeholder;
|
||||
self.addClass('placeholder');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter/setter function for the control value.
|
||||
*
|
||||
* @method value
|
||||
* @param {String} [value] Value to be set.
|
||||
* @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation.
|
||||
*/
|
||||
value: function(value) {
|
||||
var self = this;
|
||||
|
||||
if (typeof(value) != "undefined") {
|
||||
self._value = value;
|
||||
self.removeClass('placeholder');
|
||||
|
||||
if (self._rendered) {
|
||||
self.getEl('inp').value = value;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
if (self._rendered) {
|
||||
value = self.getEl('inp').value;
|
||||
|
||||
if (value != self.settings.placeholder) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
return self._value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter/setter function for the disabled state.
|
||||
*
|
||||
* @method value
|
||||
* @param {Boolean} [state] State to be set.
|
||||
* @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation.
|
||||
*/
|
||||
disabled: function(state) {
|
||||
var self = this;
|
||||
|
||||
if (self._rendered && typeof(state) != 'undefined') {
|
||||
self.getEl('inp').disabled = state;
|
||||
}
|
||||
|
||||
return self._super(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
DomUtils.css(elm.firstChild, {
|
||||
width: width,
|
||||
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;
|
||||
|
||||
DomUtils.on(this.getEl('inp'), 'change', function() {
|
||||
self.fire('change');
|
||||
});
|
||||
|
||||
return self._super();
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
DomUtils.off(this.getEl('inp'));
|
||||
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, settings = self.settings, prefix = self.classPrefix;
|
||||
var value = settings.value || settings.placeholder || '';
|
||||
var icon, text, openBtnHtml = '';
|
||||
|
||||
icon = settings.icon ? prefix + 'ico ' + prefix + 'i-' + settings.icon : '';
|
||||
text = self._text;
|
||||
|
||||
if (icon || text) {
|
||||
openBtnHtml = (
|
||||
'<div id="' + id + '-open" class="' + prefix + 'btn ' + prefix + 'open" tabIndex="-1">' +
|
||||
'<button id="' + id + '-action" type="button" hidefocus tabindex="-1">' +
|
||||
(icon ? '<i class="' + icon + '"></i>' : '<i class="' + prefix + 'caret"></i>') +
|
||||
(text ? (icon ? ' ' : '') + text : '') +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
self.addClass('has-open');
|
||||
}
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '">' +
|
||||
'<input id="' + id + '-inp" class="' + prefix + 'textbox ' + prefix + 'placeholder" value="' +
|
||||
value + '" hidefocus="true"' + (self.disabled() ? ' disabled="disabled"' : '') + '>' +
|
||||
openBtnHtml +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,462 @@
|
||||
/**
|
||||
* Container.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/ui/Factory",
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(Control, Collection, Selector, Factory, Tools, DomUtils) {
|
||||
"use strict";
|
||||
|
||||
var selectorCache = {};
|
||||
|
||||
return Control.extend({
|
||||
layout: '',
|
||||
innerClass: 'container-inner',
|
||||
|
||||
/**
|
||||
* 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;
|
||||
self._fixed = settings.fixed;
|
||||
self._items = new Collection();
|
||||
|
||||
if (self.isRtl()) {
|
||||
self.addClass('rtl');
|
||||
}
|
||||
|
||||
self.addClass('container');
|
||||
self.addClass('container-body', 'body');
|
||||
|
||||
if (settings.containerCls) {
|
||||
self.addClass(settings.containerCls);
|
||||
}
|
||||
|
||||
self._layout = Factory.create((settings.layout || self.layout) + 'layout');
|
||||
|
||||
if (self.settings.items) {
|
||||
self.add(self.settings.items);
|
||||
}
|
||||
|
||||
// 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
|
||||
* @return {tinymce.ui.Collection} Current instance.
|
||||
*/
|
||||
focus: function() {
|
||||
var self = this;
|
||||
|
||||
if (self.keyNav) {
|
||||
self.keyNav.focusFirst();
|
||||
} else {
|
||||
self._super();
|
||||
}
|
||||
|
||||
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, fragment;
|
||||
|
||||
ctrl.parent(self);
|
||||
|
||||
if (!ctrl._rendered) {
|
||||
containerElm = self.getEl('body');
|
||||
fragment = DomUtils.createFragment(ctrl.renderHtml());
|
||||
|
||||
// Insert or append the item
|
||||
if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) {
|
||||
containerElm.insertBefore(fragment, containerElm.childNodes[index]);
|
||||
} else {
|
||||
containerElm.appendChild(fragment);
|
||||
}
|
||||
|
||||
ctrl.postRender();
|
||||
}
|
||||
});
|
||||
|
||||
self._layout.applyClasses(self);
|
||||
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;
|
||||
},
|
||||
|
||||
preRender: function() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the control as a HTML string.
|
||||
*
|
||||
* @method renderHtml
|
||||
* @return {String} HTML representing the control.
|
||||
*/
|
||||
renderHtml: function() {
|
||||
var self = this, layout = self._layout;
|
||||
|
||||
self.preRender();
|
||||
layout.preRender(self);
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '" role="' + this.settings.role + '">' +
|
||||
'<div id="' + self._id + '-body" class="' + self.classes('body') + '">'+
|
||||
(self.settings.html || '') + layout.renderHtml(self) +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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._rendered = true;
|
||||
|
||||
if (self.settings.style) {
|
||||
DomUtils.css(self.getEl(), self.settings.style);
|
||||
}
|
||||
|
||||
if (self.settings.border) {
|
||||
box = self.borderBox();
|
||||
DomUtils.css(self.getEl(), {
|
||||
'border-top-width': box.top,
|
||||
'border-right-width': box.right,
|
||||
'border-bottom-width': box.bottom,
|
||||
'border-left-width': box.left
|
||||
});
|
||||
}
|
||||
|
||||
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, items;
|
||||
|
||||
if (this.visible()) {
|
||||
Control.repaintControls = [];
|
||||
Control.repaintControls.map = {};
|
||||
|
||||
items = 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* DOMUtils.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
define("tinymce/ui/DomUtils", [
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/dom/DOMUtils"
|
||||
], function(Tools, DOMUtils) {
|
||||
"use strict";
|
||||
|
||||
return {
|
||||
id: function() {
|
||||
return DOMUtils.DOM.uniqueId();
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
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 <div> in <p> bug on IE 8 #6178
|
||||
DOMUtils.DOM.setHTML(elm, html);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* DragHelper.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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(evt) {
|
||||
* },
|
||||
*
|
||||
* drag: function(evt) {
|
||||
* },
|
||||
*
|
||||
* end: function(evt) {
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @class tinymce.ui.DragHelper
|
||||
*/
|
||||
define("tinymce/ui/DragHelper", [
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(DomUtils) {
|
||||
"use strict";
|
||||
|
||||
function getDocumentSize() {
|
||||
var doc = document, 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
|
||||
};
|
||||
}
|
||||
|
||||
return function(id, settings) {
|
||||
var eventOverlayElm, doc = document, downButton, start, stop, drag, startX, startY;
|
||||
|
||||
settings = settings || {};
|
||||
|
||||
function getHandleElm() {
|
||||
return doc.getElementById(settings.handle || id);
|
||||
}
|
||||
|
||||
start = function(e) {
|
||||
var docSize = getDocumentSize(), handleElm, cursor;
|
||||
|
||||
e.preventDefault();
|
||||
downButton = e.button;
|
||||
handleElm = getHandleElm();
|
||||
startX = e.screenX;
|
||||
startY = e.screenY;
|
||||
|
||||
// Grab cursor from handle
|
||||
if (window.getComputedStyle) {
|
||||
cursor = window.getComputedStyle(handleElm, null).getPropertyValue("cursor");
|
||||
} else {
|
||||
cursor = handleElm.runtimeStyle.cursor;
|
||||
}
|
||||
|
||||
// Create event overlay and add it to document
|
||||
eventOverlayElm = doc.createElement('div');
|
||||
DomUtils.css(eventOverlayElm, {
|
||||
position: "absolute",
|
||||
top: 0, left: 0,
|
||||
width: docSize.width,
|
||||
height: docSize.height,
|
||||
zIndex: 0x7FFFFFFF,
|
||||
opacity: 0.0001,
|
||||
background: 'red',
|
||||
cursor: cursor
|
||||
});
|
||||
|
||||
doc.body.appendChild(eventOverlayElm);
|
||||
|
||||
// Bind mouse events
|
||||
DomUtils.on(doc, 'mousemove', drag);
|
||||
DomUtils.on(doc, 'mouseup', stop);
|
||||
|
||||
// Begin drag
|
||||
settings.start(e);
|
||||
};
|
||||
|
||||
drag = function(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) {
|
||||
DomUtils.off(doc, 'mousemove', drag);
|
||||
DomUtils.off(doc, 'mouseup', stop);
|
||||
|
||||
eventOverlayElm.parentNode.removeChild(eventOverlayElm);
|
||||
|
||||
if (settings.stop) {
|
||||
settings.stop(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroys the drag/drop helper instance.
|
||||
*
|
||||
* @method destroy
|
||||
*/
|
||||
this.destroy = function() {
|
||||
DomUtils.off(getHandleElm());
|
||||
};
|
||||
|
||||
DomUtils.on(getHandleElm(), 'mousedown', start);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* ElementPath.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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",
|
||||
"tinymce/EditorManager"
|
||||
], function(Path, EditorManager) {
|
||||
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 = EditorManager.activeEditor;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
self.on('select', function(e) {
|
||||
var parents = [], node, body = editor.getBody();
|
||||
|
||||
editor.focus();
|
||||
|
||||
node = editor.selection.getStart();
|
||||
while (node && node != body) {
|
||||
if (!isHidden(node)) {
|
||||
parents.push(node);
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
editor.selection.select(parents[parents.length - 1 - e.index]);
|
||||
editor.nodeChanged();
|
||||
});
|
||||
|
||||
editor.on('nodeChange', function(e) {
|
||||
var parents = [], selectionParents = e.parents, i = selectionParents.length;
|
||||
|
||||
while (i--) {
|
||||
if (selectionParents[i].nodeType == 1 && !isHidden(selectionParents[i])) {
|
||||
var args = editor.fire('ResolveName', {
|
||||
name: selectionParents[i].nodeName.toLowerCase(),
|
||||
target: selectionParents[i]
|
||||
});
|
||||
|
||||
parents.push({name: args.name});
|
||||
}
|
||||
}
|
||||
|
||||
self.data(parents);
|
||||
});
|
||||
|
||||
return self._super();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Factory.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
/**
|
||||
* 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/ui/Factory", [], function() {
|
||||
"use strict";
|
||||
|
||||
var types = {}, namespaceInit;
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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()];
|
||||
},
|
||||
|
||||
/**
|
||||
* 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, name, namespace;
|
||||
|
||||
// Build type lookup
|
||||
if (!namespaceInit) {
|
||||
namespace = tinymce.ui;
|
||||
|
||||
for (name in namespace) {
|
||||
types[name.toLowerCase()] = namespace[name];
|
||||
}
|
||||
|
||||
namespaceInit = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Find control type
|
||||
type = type.toLowerCase();
|
||||
ControlType = types[type];
|
||||
|
||||
// #if debug
|
||||
|
||||
if (!ControlType) {
|
||||
throw new Error("Could not find control by type: " + type);
|
||||
}
|
||||
|
||||
// #endif
|
||||
|
||||
ControlType = new ControlType(settings);
|
||||
ControlType.type = type; // Set the type on the instance, this will be used by the Selector engine
|
||||
|
||||
return ControlType;
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* FieldSet.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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 (
|
||||
'<fieldset id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' +
|
||||
(self.settings.title ? ('<legend id="' + self._id + '-title" class="' + prefix + 'fieldset-title">' +
|
||||
self.settings.title + '</legend>') : '') +
|
||||
'<div id="' + self._id + '-body" class="' + self.classes('body') + '">' +
|
||||
(self.settings.html || '') + layout.renderHtml(self) +
|
||||
'</div>' +
|
||||
'</fieldset>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* FilePicker.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/*global tinymce:true */
|
||||
|
||||
/**
|
||||
* This class creates a file picker control.
|
||||
*
|
||||
* @class tinymce.ui.FilePicker
|
||||
* @extends tinymce.ui.ComboBox
|
||||
*/
|
||||
define("tinymce/ui/FilePicker", [
|
||||
"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, editor = tinymce.activeEditor, fileBrowserCallback;
|
||||
|
||||
settings.spellcheck = false;
|
||||
|
||||
fileBrowserCallback = editor.settings.file_browser_callback;
|
||||
if (fileBrowserCallback) {
|
||||
settings.icon = 'browse';
|
||||
|
||||
settings.onaction = function() {
|
||||
fileBrowserCallback(
|
||||
self.getEl('inp').id,
|
||||
self.getEl('inp').value,
|
||||
settings.filetype,
|
||||
window
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
self._super(settings);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* FitLayout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* FlexLayout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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, afterName, deltaSizeName, contentSizeName;
|
||||
var alignAxisName, alignInnerSizeName, alignSizeName, alignMinSizeName, alignMaxSizeName, 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';
|
||||
afterName = 'bottom';
|
||||
deltaSizeName = "deltaH";
|
||||
contentSizeName = "contentH";
|
||||
|
||||
alignBeforeName = "left";
|
||||
alignSizeName = "w";
|
||||
alignAxisName = "x";
|
||||
alignInnerSizeName = "innerW";
|
||||
alignMinSizeName = "minW";
|
||||
alignMaxSizeName = "maxW";
|
||||
alignAfterName = "right";
|
||||
alignDeltaSizeName = "deltaW";
|
||||
alignContentSizeName = "contentW";
|
||||
} else {
|
||||
posName = "x";
|
||||
sizeName = "w";
|
||||
minSizeName = "minW";
|
||||
maxSizeName = "maxW";
|
||||
innerSizeName = "innerW";
|
||||
beforeName = 'left';
|
||||
afterName = 'right';
|
||||
deltaSizeName = "deltaW";
|
||||
contentSizeName = "contentW";
|
||||
|
||||
alignBeforeName = "top";
|
||||
alignSizeName = "h";
|
||||
alignAxisName = "y";
|
||||
alignInnerSizeName = "innerH";
|
||||
alignMinSizeName = "minH";
|
||||
alignMaxSizeName = "maxH";
|
||||
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] + Math.ceil(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 += Math.ceil(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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* FloatPanel.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/Panel",
|
||||
"tinymce/ui/Movable",
|
||||
"tinymce/ui/Resizable",
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(Panel, Movable, Resizable, DomUtils) {
|
||||
"use strict";
|
||||
|
||||
var documentClickHandler, documentScrollHandler, visiblePanels = [];
|
||||
var zOrder = [], hasModal;
|
||||
|
||||
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;
|
||||
|
||||
function reorder() {
|
||||
var i, zIndex = FloatPanel.zIndex || 0xFFFF, topModal;
|
||||
|
||||
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 = document.getElementById(self.classPrefix + 'modal-block');
|
||||
|
||||
if (topModal) {
|
||||
DomUtils.css(modalBlockEl, 'z-index', topModal.zIndex - 1);
|
||||
} else if (modalBlockEl) {
|
||||
modalBlockEl.parentNode.removeChild(modalBlockEl);
|
||||
hasModal = false;
|
||||
}
|
||||
|
||||
FloatPanel.currentZIndex = zIndex;
|
||||
}
|
||||
|
||||
function isChildOf(ctrl, parent) {
|
||||
while (ctrl) {
|
||||
if (ctrl == parent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ctrl = ctrl.parent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repositions the panel to the top of page if the panel is outside of the visual viewport. It will
|
||||
* also reposition all child panels of the current panel.
|
||||
*/
|
||||
function repositionPanel(panel) {
|
||||
var scrollY = DomUtils.getViewPort().y;
|
||||
|
||||
function toggleFixedChildPanels(fixed, deltaY) {
|
||||
var parent;
|
||||
|
||||
for (var i = 0; i < visiblePanels.length; i++) {
|
||||
if (visiblePanels[i] != panel) {
|
||||
parent = visiblePanels[i].parent();
|
||||
|
||||
while (parent && (parent = parent.parent())) {
|
||||
if (parent == panel) {
|
||||
visiblePanels[i].fixed(fixed).moveBy(0, deltaY).repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.settings.autofix) {
|
||||
if (!panel._fixed) {
|
||||
panel._autoFixY = panel.layoutRect().y;
|
||||
|
||||
if (panel._autoFixY < scrollY) {
|
||||
panel.fixed(true).layoutRect({y: 0}).repaint();
|
||||
toggleFixedChildPanels(true, scrollY - panel._autoFixY);
|
||||
}
|
||||
} else {
|
||||
if (panel._autoFixY > scrollY) {
|
||||
panel.fixed(false).layoutRect({y: panel._autoFixY}).repaint();
|
||||
toggleFixedChildPanels(false, panel._autoFixY - scrollY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self._super(settings);
|
||||
self._eventsRoot = self;
|
||||
|
||||
self.addClass('floatpanel');
|
||||
|
||||
// Hide floatpanes on click out side the root button
|
||||
if (settings.autohide) {
|
||||
if (!documentClickHandler) {
|
||||
documentClickHandler = function(e) {
|
||||
var i, clickCtrl = self.getParentCtrl(e.target);
|
||||
|
||||
// Hide any float panel when a click is out side that float panel and the
|
||||
// float panels direct parent for example a click on a menu button
|
||||
i = visiblePanels.length;
|
||||
while (i--) {
|
||||
var panel = visiblePanels[i];
|
||||
|
||||
if (panel.settings.autohide) {
|
||||
if (clickCtrl) {
|
||||
if (isChildOf(clickCtrl, panel) || panel.parent() === clickCtrl) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
e = panel.fire('autohide', {target: e.target});
|
||||
if (!e.isDefaultPrevented()) {
|
||||
panel.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DomUtils.on(document, 'click', documentClickHandler);
|
||||
}
|
||||
|
||||
visiblePanels.push(self);
|
||||
}
|
||||
|
||||
if (settings.autofix) {
|
||||
if (!documentScrollHandler) {
|
||||
documentScrollHandler = function() {
|
||||
var i;
|
||||
|
||||
i = visiblePanels.length;
|
||||
while (i--) {
|
||||
repositionPanel(visiblePanels[i]);
|
||||
}
|
||||
};
|
||||
|
||||
DomUtils.on(window, 'scroll', documentScrollHandler);
|
||||
}
|
||||
|
||||
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 = DomUtils.createFragment('<div id="' + prefix + 'modal-block" class="' +
|
||||
prefix + 'reset ' + prefix + 'fade"></div>');
|
||||
modalBlockEl = modalBlockEl.firstChild;
|
||||
|
||||
self.getContainerElm().appendChild(modalBlockEl);
|
||||
|
||||
setTimeout(function() {
|
||||
DomUtils.addClass(modalBlockEl, prefix + 'in');
|
||||
DomUtils.addClass(self.getEl(), prefix + 'in');
|
||||
}, 0);
|
||||
|
||||
hasModal = true;
|
||||
}
|
||||
|
||||
zOrder.push(self);
|
||||
reorder();
|
||||
}
|
||||
});
|
||||
|
||||
self.on('close hide', function(e) {
|
||||
if (e.control == self) {
|
||||
var i = zOrder.length;
|
||||
|
||||
while (i--) {
|
||||
if (zOrder[i] === self) {
|
||||
zOrder.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
reorder();
|
||||
}
|
||||
});
|
||||
|
||||
self.on('show', function() {
|
||||
self.parents().each(function(ctrl) {
|
||||
if (ctrl._fixed) {
|
||||
self.fixed(true);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (settings.popover) {
|
||||
self._preBodyHtml = '<div class="' + self.classPrefix + 'arrow"></div>';
|
||||
self.addClass('popover').addClass('bottom').addClass(self.isRtl() ? 'end' : 'start');
|
||||
}
|
||||
},
|
||||
|
||||
fixed: function(state) {
|
||||
var self = this;
|
||||
|
||||
if (self._fixed != state) {
|
||||
if (self._rendered) {
|
||||
var viewport = DomUtils.getViewPort();
|
||||
|
||||
if (state) {
|
||||
self.layoutRect().y -= viewport.y;
|
||||
} else {
|
||||
self.layoutRect().y += viewport.y;
|
||||
}
|
||||
}
|
||||
|
||||
self.toggleClass('fixed', state);
|
||||
self._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);
|
||||
return this._super();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides all visible the float 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;
|
||||
|
||||
self.fire('close');
|
||||
|
||||
return self.remove();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the float panel from page.
|
||||
*
|
||||
* @method remove
|
||||
*/
|
||||
remove: function() {
|
||||
removeVisiblePanel(this);
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Hides all visible the float panels.
|
||||
*
|
||||
* @static
|
||||
* @method hideAll
|
||||
*/
|
||||
FloatPanel.hideAll = function() {
|
||||
var i = visiblePanels.length;
|
||||
|
||||
while (i--) {
|
||||
var panel = visiblePanels[i];
|
||||
|
||||
if (panel.settings.autohide) {
|
||||
panel.fire('cancel', {}, false);
|
||||
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;
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* FlowLayout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Form.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.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"
|
||||
], function(Container, FormItem) {
|
||||
"use strict";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method gets invoked before the control is rendered.
|
||||
*
|
||||
* @method preRender
|
||||
*/
|
||||
preRender: function() {
|
||||
var self = this, items = self.items();
|
||||
|
||||
// Wrap any labeled items in FormItems
|
||||
items.each(function(ctrl) {
|
||||
var formItem, label = ctrl.settings.label;
|
||||
|
||||
if (label) {
|
||||
formItem = new FormItem({
|
||||
layout: 'flex',
|
||||
autoResize: "overflow",
|
||||
defaults: {flex: 1},
|
||||
items: [
|
||||
{type: 'label', text: label, flex: 0, forId: ctrl._id}
|
||||
]
|
||||
});
|
||||
|
||||
formItem.type = 'formitem';
|
||||
|
||||
if (typeof(ctrl.settings.flex) == "undefined") {
|
||||
ctrl.settings.flex = 1;
|
||||
}
|
||||
|
||||
self.replace(ctrl, formItem);
|
||||
formItem.add(ctrl);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Recalcs label widths.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
recalcLabels: function() {
|
||||
var self = this, maxLabelWidth = 0, labels = [], i, labelGap;
|
||||
|
||||
if (self.settings.labelGapCalc === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.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;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter/setter for the visibility state.
|
||||
*
|
||||
* @method visible
|
||||
* @param {Boolean} [state] True/false state to show/hide.
|
||||
* @return {tinymce.ui.Form|Boolean} True/false state or current control.
|
||||
*/
|
||||
visible: function(state) {
|
||||
var val = this._super(state);
|
||||
|
||||
if (state === true && this._rendered) {
|
||||
this.recalcLabels();
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.recalcLabels();
|
||||
self.fromJSON(self.settings.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* FormItem.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.addClass('formitem');
|
||||
layout.preRender(self);
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' +
|
||||
(self.settings.title ? ('<div id="' + self._id + '-title" class="' + prefix + 'title">' +
|
||||
self.settings.title + '</div>') : '') +
|
||||
'<div id="' + self._id + '-body" class="' + self.classes('body') + '">' +
|
||||
(self.settings.html || '') + layout.renderHtml(self) +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,640 @@
|
||||
/**
|
||||
* FormatControls.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/Control",
|
||||
"tinymce/ui/Widget",
|
||||
"tinymce/ui/FloatPanel",
|
||||
"tinymce/util/Tools",
|
||||
"tinymce/EditorManager",
|
||||
"tinymce/Env"
|
||||
], function(Control, Widget, FloatPanel, Tools, EditorManager, Env) {
|
||||
var each = Tools.each;
|
||||
|
||||
EditorManager.on('AddEditor', function(e) {
|
||||
if (e.editor.rtl) {
|
||||
Control.rtl = true;
|
||||
}
|
||||
|
||||
registerControls(e.editor);
|
||||
});
|
||||
|
||||
Control.translate = function(text) {
|
||||
return EditorManager.translate(text);
|
||||
};
|
||||
|
||||
Widget.tooltips = !Env.iOS;
|
||||
|
||||
function registerControls(editor) {
|
||||
var formatMenu;
|
||||
|
||||
// Generates a preview for a format
|
||||
function getPreviewCss(format) {
|
||||
var name, previewElm, dom = editor.dom;
|
||||
var previewCss = '', parentFontSize, previewStyles;
|
||||
|
||||
previewStyles = editor.settings.preview_styles;
|
||||
|
||||
// No preview forced
|
||||
if (previewStyles === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Default preview
|
||||
if (!previewStyles) {
|
||||
previewStyles = 'font-family font-size font-weight text-decoration ' +
|
||||
'text-transform color background-color border border-radius';
|
||||
}
|
||||
|
||||
// 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
|
||||
format = editor.formatter.get(format);
|
||||
if (!format) {
|
||||
return;
|
||||
}
|
||||
|
||||
format = format[0];
|
||||
name = format.block || format.inline || 'span';
|
||||
previewElm = dom.create(name);
|
||||
|
||||
// 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(previewElm, {position: 'absolute', left: -0xFFFF});
|
||||
editor.getBody().appendChild(previewElm);
|
||||
|
||||
// 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(previewElm);
|
||||
|
||||
return previewCss;
|
||||
}
|
||||
|
||||
function createListBoxChangeHandler(items, formatName) {
|
||||
return function() {
|
||||
var self = this;
|
||||
|
||||
editor.on('nodeChange', function(e) {
|
||||
var formatter = editor.formatter;
|
||||
var value = null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (value) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
self.value(value);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createFormats(formats) {
|
||||
formats = formats.split(';');
|
||||
|
||||
var i = formats.length;
|
||||
while (i--) {
|
||||
formats[i] = formats[i].split('=');
|
||||
}
|
||||
|
||||
return formats;
|
||||
}
|
||||
|
||||
function createFormatMenu() {
|
||||
var count = 0, newFormats = [];
|
||||
|
||||
var defaultStyleFormats = [
|
||||
{title: 'Headers', items: [
|
||||
{title: 'Header 1', format: 'h1'},
|
||||
{title: 'Header 2', format: 'h2'},
|
||||
{title: 'Header 3', format: 'h3'},
|
||||
{title: 'Header 4', format: 'h4'},
|
||||
{title: 'Header 5', format: 'h5'},
|
||||
{title: 'Header 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;
|
||||
}
|
||||
|
||||
menu.push(menuItem);
|
||||
});
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
editor.on('init', function() {
|
||||
each(newFormats, function(format) {
|
||||
editor.formatter.register(format.name, format);
|
||||
});
|
||||
});
|
||||
|
||||
var menu = createMenu(editor.settings.style_formats || defaultStyleFormats);
|
||||
|
||||
menu = {
|
||||
type: 'menu',
|
||||
items: menu,
|
||||
onPostRender: function(e) {
|
||||
editor.fire('renderFormatsMenu', {control: e.control});
|
||||
},
|
||||
itemDefaults: {
|
||||
preview: true,
|
||||
|
||||
textStyle: function() {
|
||||
if (this.settings.format) {
|
||||
return getPreviewCss(this.settings.format);
|
||||
}
|
||||
},
|
||||
|
||||
onPostRender: function() {
|
||||
var self = this, formatName = this.settings.format;
|
||||
|
||||
if (formatName) {
|
||||
self.parent().on('show', function() {
|
||||
self.disabled(!editor.formatter.canApply(formatName));
|
||||
self.active(editor.formatter.match(formatName));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onclick: function() {
|
||||
if (this.settings.format) {
|
||||
toggleFormat(this.settings.format);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
formatMenu = createFormatMenu();
|
||||
|
||||
// Simple format controls <control/format>:<UI text>
|
||||
each({
|
||||
bold: 'Bold',
|
||||
italic: 'Italic',
|
||||
underline: 'Underline',
|
||||
strikethrough: 'Strikethrough',
|
||||
subscript: 'Subscript',
|
||||
superscript: 'Superscript'
|
||||
}, function(text, name) {
|
||||
editor.addButton(name, {
|
||||
tooltip: text,
|
||||
onPostRender: 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
onclick: function() {
|
||||
toggleFormat(name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Simple command controls <control>:[<UI text>,<Command>]
|
||||
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'],
|
||||
hr: ['Insert horizontal rule', 'InsertHorizontalRule'],
|
||||
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: ['Toggle blockquote', 'mceBlockQuote'],
|
||||
numlist: ['Numbered list', 'InsertOrderedList'],
|
||||
bullist: ['Bullet list', 'InsertUnorderedList'],
|
||||
subscript: ['Subscript', 'Subscript'],
|
||||
superscript: ['Superscript', 'Superscript'],
|
||||
alignleft: ['Align left', 'JustifyLeft'],
|
||||
aligncenter: ['Align center', 'JustifyCenter'],
|
||||
alignright: ['Align right', 'JustifyRight'],
|
||||
alignjustify: ['Justify', 'JustifyFull']
|
||||
}, function(item, name) {
|
||||
editor.addButton(name, {
|
||||
tooltip: item[0],
|
||||
cmd: item[1],
|
||||
onPostRender: 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function hasUndo() {
|
||||
return editor.undoManager ? editor.undoManager.hasUndo() : false;
|
||||
}
|
||||
|
||||
function hasRedo() {
|
||||
return editor.undoManager ? editor.undoManager.hasRedo() : false;
|
||||
}
|
||||
|
||||
function toggleUndoState() {
|
||||
var self = this;
|
||||
|
||||
self.disabled(!hasUndo());
|
||||
editor.on('Undo Redo AddUndo TypingUndo', function() {
|
||||
self.disabled(!hasUndo());
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRedoState() {
|
||||
var self = this;
|
||||
|
||||
self.disabled(!hasRedo());
|
||||
editor.on('Undo Redo AddUndo TypingUndo', function() {
|
||||
self.disabled(!hasRedo());
|
||||
});
|
||||
}
|
||||
|
||||
function toggleVisualAidState() {
|
||||
var self = this;
|
||||
|
||||
editor.on('VisualAid', function(e) {
|
||||
self.active(e.hasVisual);
|
||||
});
|
||||
|
||||
self.active(editor.hasVisual);
|
||||
}
|
||||
|
||||
editor.addButton('undo', {
|
||||
tooltip: 'Undo',
|
||||
onPostRender: toggleUndoState,
|
||||
cmd: 'undo'
|
||||
});
|
||||
|
||||
editor.addButton('redo', {
|
||||
tooltip: 'Redo',
|
||||
onPostRender: toggleRedoState,
|
||||
cmd: 'redo'
|
||||
});
|
||||
|
||||
editor.addMenuItem('newdocument', {
|
||||
text: 'New document',
|
||||
shortcut: 'Ctrl+N',
|
||||
icon: 'newdocument',
|
||||
cmd: 'mceNewDocument'
|
||||
});
|
||||
|
||||
editor.addMenuItem('undo', {
|
||||
text: 'Undo',
|
||||
icon: 'undo',
|
||||
shortcut: 'Ctrl+Z',
|
||||
onPostRender: toggleUndoState,
|
||||
cmd: 'undo'
|
||||
});
|
||||
|
||||
editor.addMenuItem('redo', {
|
||||
text: 'Redo',
|
||||
icon: 'redo',
|
||||
shortcut: 'Ctrl+Y',
|
||||
onPostRender: toggleRedoState,
|
||||
cmd: 'redo'
|
||||
});
|
||||
|
||||
editor.addMenuItem('visualaid', {
|
||||
text: 'Visual aids',
|
||||
selectable: true,
|
||||
onPostRender: toggleVisualAidState,
|
||||
cmd: 'mceToggleVisualAid'
|
||||
});
|
||||
|
||||
each({
|
||||
cut: ['Cut', 'Cut', 'Ctrl+X'],
|
||||
copy: ['Copy', 'Copy', 'Ctrl+C'],
|
||||
paste: ['Paste', 'Paste', 'Ctrl+V'],
|
||||
selectall: ['Select all', 'SelectAll', 'Ctrl+A'],
|
||||
bold: ['Bold', 'Bold', 'Ctrl+B'],
|
||||
italic: ['Italic', 'Italic', 'Ctrl+I'],
|
||||
underline: ['Underline', 'Underline'],
|
||||
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.on('mousedown', function() {
|
||||
FloatPanel.hideAll();
|
||||
});
|
||||
|
||||
function toggleFormat(fmt) {
|
||||
if (fmt.control) {
|
||||
fmt = fmt.control.value();
|
||||
}
|
||||
|
||||
if (fmt) {
|
||||
editor.execCommand('mceToggleFormat', false, fmt);
|
||||
}
|
||||
}
|
||||
|
||||
editor.addButton('styleselect', {
|
||||
type: 'menubutton',
|
||||
text: 'Formats',
|
||||
menu: formatMenu
|
||||
});
|
||||
|
||||
editor.addButton('formatselect', function() {
|
||||
var items = [], blocks = createFormats(editor.settings.block_formats ||
|
||||
'Paragraph=p;' +
|
||||
'Address=address;' +
|
||||
'Pre=pre;' +
|
||||
'Header 1=h1;' +
|
||||
'Header 2=h2;' +
|
||||
'Header 3=h3;' +
|
||||
'Header 4=h4;' +
|
||||
'Header 5=h5;' +
|
||||
'Header 6=h6'
|
||||
);
|
||||
|
||||
each(blocks, function(block) {
|
||||
items.push({
|
||||
text: block[0],
|
||||
value: block[1],
|
||||
textStyle: function() {
|
||||
return getPreviewCss(block[1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'listbox',
|
||||
text: {raw: blocks[0][0]},
|
||||
values: items,
|
||||
fixedWidth: true,
|
||||
onselect: toggleFormat,
|
||||
onPostRender: createListBoxChangeHandler(items)
|
||||
};
|
||||
});
|
||||
|
||||
editor.addButton('fontselect', function() {
|
||||
var defaultFontsFormats =
|
||||
'Andale Mono=andale mono,times;' +
|
||||
'Arial=arial,helvetica,sans-serif;' +
|
||||
'Arial Black=arial black,avant garde;' +
|
||||
'Book Antiqua=book antiqua,palatino;' +
|
||||
'Comic Sans MS=comic sans ms,sans-serif;' +
|
||||
'Courier New=courier new,courier;' +
|
||||
'Georgia=georgia,palatino;' +
|
||||
'Helvetica=helvetica;' +
|
||||
'Impact=impact,chicago;' +
|
||||
'Symbol=symbol;' +
|
||||
'Tahoma=tahoma,arial,helvetica,sans-serif;' +
|
||||
'Terminal=terminal,monaco;' +
|
||||
'Times New Roman=times new roman,times;' +
|
||||
'Trebuchet MS=trebuchet ms,geneva;' +
|
||||
'Verdana=verdana,geneva;' +
|
||||
'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: createListBoxChangeHandler(items, 'fontname'),
|
||||
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 fontsize_formats = editor.settings.fontsize_formats || defaultFontsizeFormats;
|
||||
|
||||
each(fontsize_formats.split(' '), function(item) {
|
||||
items.push({text: item, value: item});
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'listbox',
|
||||
text: 'Font Sizes',
|
||||
tooltip: 'Font Sizes',
|
||||
values: items,
|
||||
fixedWidth: true,
|
||||
onPostRender: createListBoxChangeHandler(items, 'fontsize'),
|
||||
onclick: function(e) {
|
||||
if (e.control.settings.value) {
|
||||
editor.execCommand('FontSize', false, e.control.settings.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
editor.addMenuItem('formats', {
|
||||
text: 'Formats',
|
||||
menu: formatMenu
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* GridLayout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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 = container.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, alignX, alignY, availableWidth, availableHeight;
|
||||
|
||||
// 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;
|
||||
|
||||
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 ? Math.ceil(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++) {
|
||||
ctrl = items[y * cols + x];
|
||||
|
||||
// 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);
|
||||
alignX = alignY = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Iframe.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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"
|
||||
], 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.addClass('iframe');
|
||||
self.canFocus = false;
|
||||
|
||||
return (
|
||||
'<iframe id="' + self._id + '" class="' + self.classes() + '" tabindex="-1" src="' +
|
||||
(self.settings.url || "javascript:\'\'") + '" frameborder="0"></iframe>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
setTimeout(function() {
|
||||
self.html(html);
|
||||
}, 0);
|
||||
} else {
|
||||
body.innerHTML = html;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* KeyboardNavigation.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(DomUtils) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Create a new KeyboardNavigation instance to handle the focus for a specific element.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} settings the settings object to define how keyboard navigation works.
|
||||
*
|
||||
* @setting {tinymce.ui.Control} root the root control navigation focus movement is scoped to this root.
|
||||
* @setting {Array} items an array containing the items to move focus between. Every object in this array must have an
|
||||
* id attribute which maps to the actual DOM element and it must be able to have focus i.e. tabIndex=-1.
|
||||
* @setting {Function} onCancel the callback for when the user presses escape or otherwise indicates canceling.
|
||||
* @setting {Function} onAction (optional) the action handler to call when the user activates an item.
|
||||
* @setting {Boolean} enableLeftRight (optional, default) when true, the up/down arrows move through items.
|
||||
* @setting {Boolean} enableUpDown (optional) when true, the up/down arrows move through items.
|
||||
* Note for both up/down and left/right explicitly set both enableLeftRight and enableUpDown to true.
|
||||
*/
|
||||
return function(settings) {
|
||||
var root = settings.root, enableUpDown = settings.enableUpDown !== false;
|
||||
var enableLeftRight = settings.enableLeftRight !== false;
|
||||
var items = settings.items, focussedId;
|
||||
|
||||
/**
|
||||
* Initializes the items array if needed. This will collect items/elements
|
||||
* from the specified root control.
|
||||
*/
|
||||
function initItems() {
|
||||
if (!items) {
|
||||
items = [];
|
||||
|
||||
if (root.find) {
|
||||
// Root is a container then get child elements using the UI API
|
||||
root.find('*').each(function(ctrl) {
|
||||
if (ctrl.canFocus) {
|
||||
items.push(ctrl.getEl());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Root is a control/widget then get the child elements of that control
|
||||
var elements = root.getEl().getElementsByTagName('*');
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
if (elements[i].id && elements[i]) {
|
||||
items.push(elements[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently focused element.
|
||||
*
|
||||
* @private
|
||||
* @return {Element} Currently focused element.
|
||||
*/
|
||||
function getFocusElement() {
|
||||
return document.getElementById(focussedId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently focused elements wai aria role.
|
||||
*
|
||||
* @private
|
||||
* @param {Element} elm Optional element to get role from.
|
||||
* @return {String} Role of specified element.
|
||||
*/
|
||||
function getRole(elm) {
|
||||
elm = elm || getFocusElement();
|
||||
|
||||
return elm && elm.getAttribute('role');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the role of the parent 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 || getFocusElement();
|
||||
|
||||
while ((parent = parent.parentNode)) {
|
||||
if ((role = getRole(parent))) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an wai aria property by name.
|
||||
*
|
||||
* @private
|
||||
* @param {String} name Name of the aria property to get for example "disabled".
|
||||
* @return {String} Aria property value.
|
||||
*/
|
||||
function getAriaProp(name) {
|
||||
var elm = document.getElementById(focussedId);
|
||||
|
||||
if (elm) {
|
||||
return elm.getAttribute('aria-' + name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the onAction event callback. This is when the user presses enter/space.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function action() {
|
||||
var focusElm = getFocusElement();
|
||||
|
||||
if (focusElm && (focusElm.nodeName == "TEXTAREA" || focusElm.type == "text")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.onAction) {
|
||||
settings.onAction(focussedId);
|
||||
} else {
|
||||
DomUtils.fire(getFocusElement(), 'click', {keyboard: true});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current navigation. The same as pressing the Esc key.
|
||||
*
|
||||
* @method cancel
|
||||
*/
|
||||
function cancel() {
|
||||
var focusElm;
|
||||
|
||||
if (settings.onCancel) {
|
||||
if ((focusElm = getFocusElement())) {
|
||||
focusElm.blur();
|
||||
}
|
||||
|
||||
settings.onCancel();
|
||||
} else {
|
||||
settings.root.fire('cancel');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the focus to the next or previous item. It will wrap to start/end if it can't move.
|
||||
*
|
||||
* @method moveFocus
|
||||
* @param {Number} dir Direction for move -1 or 1.
|
||||
*/
|
||||
function moveFocus(dir) {
|
||||
var idx = -1, focusElm, i;
|
||||
var visibleItems = [];
|
||||
|
||||
function isVisible(elm) {
|
||||
var rootElm = root ? root.getEl() : document.body;
|
||||
|
||||
while (elm && elm != rootElm) {
|
||||
if (elm.style.display == 'none') {
|
||||
return false;
|
||||
}
|
||||
|
||||
elm = elm.parentNode;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
initItems();
|
||||
|
||||
// TODO: Optimize this, will be slow on lots of items
|
||||
i = visibleItems.length;
|
||||
for (i = 0; i < items.length; i++) {
|
||||
if (isVisible(items[i])) {
|
||||
visibleItems.push(items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
i = visibleItems.length;
|
||||
while (i--) {
|
||||
if (visibleItems[i].id === focussedId) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
idx += dir;
|
||||
if (idx < 0) {
|
||||
idx = visibleItems.length - 1;
|
||||
} else if (idx >= visibleItems.length) {
|
||||
idx = 0;
|
||||
}
|
||||
|
||||
focusElm = visibleItems[idx];
|
||||
focusElm.focus();
|
||||
focussedId = focusElm.id;
|
||||
|
||||
if (settings.actOnFocus) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves focus to the first item or the last focused item if root is a toolbar.
|
||||
*
|
||||
* @method focusFirst
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
function focusFirst() {
|
||||
var i, rootRole;
|
||||
|
||||
rootRole = getRole(settings.root.getEl());
|
||||
initItems();
|
||||
|
||||
i = items.length;
|
||||
while (i--) {
|
||||
if (rootRole == 'toolbar' && items[i].id === focussedId) {
|
||||
items[i].focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
items[0].focus();
|
||||
}
|
||||
|
||||
// Handle accessible keys
|
||||
root.on('keydown', function(e) {
|
||||
var DOM_VK_LEFT = 37, DOM_VK_RIGHT = 39, DOM_VK_UP = 38, DOM_VK_DOWN = 40;
|
||||
var DOM_VK_ESCAPE = 27, DOM_VK_ENTER = 14, DOM_VK_RETURN = 13, DOM_VK_SPACE = 32, DOM_VK_TAB = 9;
|
||||
var preventDefault;
|
||||
|
||||
switch (e.keyCode) {
|
||||
case DOM_VK_LEFT:
|
||||
if (enableLeftRight) {
|
||||
if (settings.leftAction) {
|
||||
settings.leftAction();
|
||||
} else {
|
||||
moveFocus(-1);
|
||||
}
|
||||
|
||||
preventDefault = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOM_VK_RIGHT:
|
||||
if (enableLeftRight) {
|
||||
if (getRole() == 'menuitem' && getParentRole() == 'menu') {
|
||||
if (getAriaProp('haspopup')) {
|
||||
action();
|
||||
}
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
|
||||
preventDefault = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOM_VK_UP:
|
||||
if (enableUpDown) {
|
||||
moveFocus(-1);
|
||||
preventDefault = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOM_VK_DOWN:
|
||||
if (enableUpDown) {
|
||||
if (getRole() == 'menuitem' && getParentRole() == 'menubar') {
|
||||
action();
|
||||
} else if (getRole() == 'button' && getAriaProp('haspopup')) {
|
||||
action();
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
|
||||
preventDefault = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case DOM_VK_TAB:
|
||||
preventDefault = true;
|
||||
|
||||
if (e.shiftKey) {
|
||||
moveFocus(-1);
|
||||
} else {
|
||||
moveFocus(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case DOM_VK_ESCAPE:
|
||||
preventDefault = true;
|
||||
cancel();
|
||||
break;
|
||||
|
||||
case DOM_VK_ENTER:
|
||||
case DOM_VK_RETURN:
|
||||
case DOM_VK_SPACE:
|
||||
preventDefault = action();
|
||||
break;
|
||||
}
|
||||
|
||||
if (preventDefault) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Init on focus in
|
||||
root.on('focusin', function(e) {
|
||||
initItems();
|
||||
focussedId = e.target.id;
|
||||
});
|
||||
|
||||
return {
|
||||
moveFocus: moveFocus,
|
||||
focusFirst: focusFirst,
|
||||
cancel: cancel
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Label.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.
|
||||
* @param {Boolean} multiline Multiline label.
|
||||
*/
|
||||
init: function(settings) {
|
||||
var self = this;
|
||||
|
||||
self._super(settings);
|
||||
self.addClass('widget');
|
||||
self.addClass('label');
|
||||
self.canFocus = false;
|
||||
|
||||
if (settings.multiline) {
|
||||
self.addClass('autoscroll');
|
||||
}
|
||||
|
||||
if (settings.strong) {
|
||||
self.addClass('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.addClass('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();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the current label text.
|
||||
*
|
||||
* @method text
|
||||
* @param {String} [text] New label text.
|
||||
* @return {String|tinymce.ui.Label} Current text or current label instance.
|
||||
*/
|
||||
text: function(text) {
|
||||
var self = this;
|
||||
|
||||
if (self._rendered && text) {
|
||||
this.innerHtml(self.encode(text));
|
||||
}
|
||||
|
||||
return self._super(text);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the control as a HTML string.
|
||||
*
|
||||
* @method renderHtml
|
||||
* @return {String} HTML representing the control.
|
||||
*/
|
||||
renderHtml: function() {
|
||||
var self = this, forId = self.settings.forId;
|
||||
|
||||
return (
|
||||
'<label id="' + self._id + '" class="' + self.classes() + '"' + (forId ? ' for="' + forId : '') + '">' +
|
||||
self.encode(self._text) +
|
||||
'</label>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Layout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/util/Class",
|
||||
"tinymce/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.addClass(this.settings.containerClass, 'body');
|
||||
},
|
||||
|
||||
/**
|
||||
* Applies layout classes to the container.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
applyClasses: function(container) {
|
||||
var self = this, settings = self.settings, items, firstClass, lastClass;
|
||||
|
||||
items = container.items().filter(':visible');
|
||||
firstClass = settings.firstControlClass;
|
||||
lastClass = settings.lastControlClass;
|
||||
|
||||
items.each(function(item) {
|
||||
item.removeClass(firstClass).removeClass(lastClass);
|
||||
|
||||
if (settings.controlClass) {
|
||||
item.addClass(settings.controlClass);
|
||||
}
|
||||
});
|
||||
|
||||
items.eq(0).addClass(firstClass);
|
||||
items.eq(-1).addClass(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, settings = self.settings, items, html = '';
|
||||
|
||||
items = container.items();
|
||||
items.eq(0).addClass(settings.firstControlClass);
|
||||
items.eq(-1).addClass(settings.lastControlClass);
|
||||
|
||||
items.each(function(item) {
|
||||
if (settings.controlClass) {
|
||||
item.addClass(settings.controlClass);
|
||||
}
|
||||
|
||||
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() {
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* ListBox.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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"
|
||||
], function(MenuButton) {
|
||||
"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, i, selected, selectedText, lastItemCtrl;
|
||||
|
||||
self._values = values = settings.values;
|
||||
if (values) {
|
||||
for (i = 0; i < values.length; i++) {
|
||||
selected = values[i].selected || settings.value === values[i].value;
|
||||
|
||||
if (selected) {
|
||||
selectedText = selectedText || values[i].text;
|
||||
self._value = values[i].value;
|
||||
}
|
||||
}
|
||||
|
||||
settings.menu = values;
|
||||
}
|
||||
|
||||
settings.text = settings.text || selectedText || values[0].text;
|
||||
|
||||
self._super(settings);
|
||||
self.addClass('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.settings.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.
|
||||
*/
|
||||
value: function(value) {
|
||||
var self = this, active, selectedText, menu, i;
|
||||
|
||||
function activateByValue(menu, value) {
|
||||
menu.items().each(function(ctrl) {
|
||||
active = ctrl.value() === value;
|
||||
|
||||
if (active) {
|
||||
selectedText = selectedText || ctrl.text();
|
||||
}
|
||||
|
||||
ctrl.active(active);
|
||||
|
||||
if (ctrl.menu) {
|
||||
activateByValue(ctrl.menu, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof(value) != "undefined") {
|
||||
if (self.menu) {
|
||||
activateByValue(self.menu, value);
|
||||
} else {
|
||||
menu = self.settings.menu;
|
||||
for (i = 0; i < menu.length; i++) {
|
||||
active = menu[i].value == value;
|
||||
|
||||
if (active) {
|
||||
selectedText = selectedText || menu[i].text;
|
||||
}
|
||||
|
||||
menu[i].active = active;
|
||||
}
|
||||
}
|
||||
|
||||
self.text(selectedText || this.settings.text);
|
||||
}
|
||||
|
||||
return self._super(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Menu.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/ui/FloatPanel",
|
||||
"tinymce/ui/KeyboardNavigation",
|
||||
"tinymce/ui/MenuItem",
|
||||
"tinymce/util/Tools"
|
||||
], function(FloatPanel, KeyboardNavigation, MenuItem, Tools) {
|
||||
"use strict";
|
||||
|
||||
var Menu = FloatPanel.extend({
|
||||
Defaults: {
|
||||
defaultType: 'menuitem',
|
||||
border: 1,
|
||||
layout: 'stack',
|
||||
role: 'menu'
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (settings.itemDefaults) {
|
||||
var items = settings.items, i = items.length;
|
||||
|
||||
while (i--) {
|
||||
items[i] = Tools.extend({}, settings.itemDefaults, items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
self._super(settings);
|
||||
self.addClass('menu');
|
||||
|
||||
self.keyNav = new KeyboardNavigation({
|
||||
root: self,
|
||||
enableUpDown: true,
|
||||
enableLeftRight: true,
|
||||
|
||||
leftAction: function() {
|
||||
if (self.parent() instanceof MenuItem) {
|
||||
self.keyNav.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
self.fire('cancel', {}, false);
|
||||
self.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Repaints the control after a layout operation.
|
||||
*
|
||||
* @method repaint
|
||||
*/
|
||||
repaint: function() {
|
||||
this.toggleClass('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('cancel');
|
||||
self.fire('select');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.selectable) {
|
||||
self._hasIcons = true;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return self._super();
|
||||
}
|
||||
});
|
||||
|
||||
return Menu;
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* MenuBar.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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',
|
||||
defaults: {
|
||||
type: 'menubutton'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* MenuButton.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/Button",
|
||||
"tinymce/ui/Factory",
|
||||
"tinymce/ui/MenuBar"
|
||||
], 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);
|
||||
|
||||
self.addClass('menubtn');
|
||||
|
||||
if (settings.fixedWidth) {
|
||||
self.addClass('fixed-width');
|
||||
}
|
||||
|
||||
self.aria('haspopup', true);
|
||||
self.hasPopup = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the menu for the button.
|
||||
*
|
||||
* @method showMenu
|
||||
*/
|
||||
showMenu: function() {
|
||||
var self = this, settings = self.settings, menu;
|
||||
|
||||
if (self.menu && self.menu.visible()) {
|
||||
return self.hideMenu();
|
||||
}
|
||||
|
||||
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) {
|
||||
if (e.control == self.menu) {
|
||||
self.activeMenu(e.type == 'show');
|
||||
}
|
||||
}).fire('show');
|
||||
|
||||
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']);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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();
|
||||
self.aria('expanded', false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the active menu state.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
activeMenu: function(state) {
|
||||
this.toggleClass('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 ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
|
||||
|
||||
self.aria('role', self.parent() instanceof MenuBar ? 'menuitem' : 'button');
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' +
|
||||
'<button id="' + id + '-open" role="presentation" type="button" tabindex="-1">' +
|
||||
(icon ? '<i class="' + icon + '"></i>' : '') +
|
||||
'<span>' + (self._text ? (icon ? '\u00a0' : '') + self.encode(self._text) : '') + '</span>' +
|
||||
' <i class="' + prefix + 'caret"></i>' +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.showMenu();
|
||||
|
||||
if (e.keyboard) {
|
||||
self.menu.items()[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();
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the current button text.
|
||||
*
|
||||
* @method text
|
||||
* @param {String} [text] New button text.
|
||||
* @return {String|tinymce.ui.MenuButton} Current text or current MenuButton instance.
|
||||
*/
|
||||
text: function(text) {
|
||||
var self = this, i, children;
|
||||
|
||||
if (self._rendered) {
|
||||
children = self.getEl('open').getElementsByTagName('span');
|
||||
for (i = 0; i < children.length; i++) {
|
||||
children[i].innerHTML = self.encode(text);
|
||||
}
|
||||
}
|
||||
|
||||
return this._super(text);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the control and it's menus.
|
||||
*
|
||||
* @method remove
|
||||
*/
|
||||
remove: function() {
|
||||
this._super();
|
||||
|
||||
if (this.menu) {
|
||||
this.menu.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return MenuButton;
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* MenuItem.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/ui/Factory"
|
||||
], function(Widget, Factory) {
|
||||
"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;
|
||||
|
||||
self.hasPopup = true;
|
||||
|
||||
self._super(settings);
|
||||
|
||||
settings = self.settings;
|
||||
|
||||
self.addClass('menu-item');
|
||||
|
||||
if (settings.menu) {
|
||||
self.addClass('menu-item-expand');
|
||||
}
|
||||
|
||||
if (settings.preview) {
|
||||
self.addClass('menu-item-preview');
|
||||
}
|
||||
|
||||
if (self._text === '-' || self._text === '|') {
|
||||
self.addClass('menu-item-sep');
|
||||
self.aria('role', 'separator');
|
||||
self.canFocus = false;
|
||||
self._text = '-';
|
||||
}
|
||||
|
||||
if (settings.selectable) {
|
||||
self.aria('role', 'menuitemcheckbox');
|
||||
self.aria('checked', true);
|
||||
self.addClass('menu-item-checkbox');
|
||||
settings.icon = 'selected';
|
||||
}
|
||||
|
||||
if (!settings.preview && !settings.selectable) {
|
||||
self.addClass('menu-item-normal');
|
||||
}
|
||||
|
||||
self.on('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
self.on('mouseenter click', function(e) {
|
||||
if (e.control === self) {
|
||||
if (!settings.menu && e.type === 'click') {
|
||||
self.parent().hideAll();
|
||||
self.fire('cancel');
|
||||
self.fire('select');
|
||||
} else {
|
||||
self.showMenu();
|
||||
|
||||
if (e.keyboard) {
|
||||
setTimeout(function() {
|
||||
self.menu.items()[0].focus();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (settings.menu) {
|
||||
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',
|
||||
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(self.getContainerElm());
|
||||
menu.reflow();
|
||||
menu.fire('show');
|
||||
menu.on('cancel', function() {
|
||||
self.focus();
|
||||
});
|
||||
|
||||
menu.on('hide', function(e) {
|
||||
if (e.control === menu) {
|
||||
self.removeClass('selected');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
menu.show();
|
||||
}
|
||||
|
||||
menu._parentMenu = parent;
|
||||
|
||||
menu.addClass('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.removeClass(menu._lastRel);
|
||||
menu.addClass(rel);
|
||||
menu._lastRel = rel;
|
||||
|
||||
self.addClass('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.encode(self._text);
|
||||
var icon = self.settings.icon, image = '';
|
||||
|
||||
if (icon) {
|
||||
self.parent().addClass('menu-has-icons');
|
||||
}
|
||||
|
||||
if (settings.image) {
|
||||
icon = 'none';
|
||||
image = ' style="background-image: url(\'' + settings.image + '\')"';
|
||||
}
|
||||
|
||||
icon = prefix + 'ico ' + prefix + 'i-' + (self.settings.icon || 'none');
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '" tabindex="-1">' +
|
||||
(text !== '-' ? '<i class="' + icon + '"' + image + '></i> ' : '') +
|
||||
(text !== '-' ? '<span id="' + id + '-text" class="' + prefix + 'text">' + text + '</span>' : '') +
|
||||
(settings.shortcut ? '<div id="' + id + '-shortcut" class="' + prefix + 'menu-shortcut">' +
|
||||
settings.shortcut + '</div>' : '') +
|
||||
(settings.menu ? '<div class="' + prefix + 'caret"></div>' : '') +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
return self._super();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the control and it's menus.
|
||||
*
|
||||
* @method remove
|
||||
*/
|
||||
remove: function() {
|
||||
this._super();
|
||||
|
||||
if (this.menu) {
|
||||
this.menu.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* MessageBox.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.Window
|
||||
* @extends tinymce.ui.FloatPanel
|
||||
*/
|
||||
define("tinymce/ui/MessageBox", [
|
||||
"tinymce/ui/Window"
|
||||
], function(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() {};
|
||||
|
||||
switch (settings.buttons) {
|
||||
case MessageBox.OK_CANCEL:
|
||||
buttons = [
|
||||
{type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
|
||||
e.control.parents()[1].close();
|
||||
callback(true);
|
||||
}},
|
||||
|
||||
{type: "button", text: "Cancel", onClick: function(e) {
|
||||
e.control.parents()[1].close();
|
||||
callback(false);
|
||||
}}
|
||||
];
|
||||
break;
|
||||
|
||||
case MessageBox.YES_NO:
|
||||
buttons = [
|
||||
{type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
|
||||
e.control.parents()[1].close();
|
||||
callback(true);
|
||||
}}
|
||||
];
|
||||
break;
|
||||
|
||||
case MessageBox.YES_NO_CANCEL:
|
||||
buttons = [
|
||||
{type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
|
||||
e.control.parents()[1].close();
|
||||
}}
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
buttons = [
|
||||
{type: "button", text: "Ok", subtype: "primary", onClick: function(e) {
|
||||
e.control.parents()[1].close();
|
||||
callback(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,
|
||||
items: {
|
||||
type: "label",
|
||||
multiline: true,
|
||||
maxWidth: 500,
|
||||
maxHeight: 200,
|
||||
text: settings.text
|
||||
},
|
||||
onClose: settings.onClose
|
||||
}).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;
|
||||
});
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Movable.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(DomUtils) {
|
||||
"use strict";
|
||||
|
||||
function calculateRelativePosition(ctrl, targetElm, rel) {
|
||||
var ctrlElm, pos, x, y, selfW, selfH, targetW, targetH, viewport, size;
|
||||
|
||||
viewport = DomUtils.getViewPort();
|
||||
|
||||
// Get pos of target
|
||||
pos = DomUtils.getPos(targetElm);
|
||||
x = pos.x;
|
||||
y = pos.y;
|
||||
|
||||
if (ctrl._fixed) {
|
||||
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._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 contrain(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 = contrain(x, viewPortRect.w + viewPortRect.x, layoutRect.w);
|
||||
y = contrain(y, viewPortRect.h + viewPortRect.y, layoutRect.h);
|
||||
}
|
||||
|
||||
if (self._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;
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Panel.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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 = (
|
||||
'<div id="' + self._id + '-body" class="' + self.classes('body') + '">' +
|
||||
layout.renderHtml(self) +
|
||||
'</div>'
|
||||
);
|
||||
} else {
|
||||
if (typeof(innerHtml) == 'function') {
|
||||
innerHtml = innerHtml.call(self);
|
||||
}
|
||||
|
||||
self._hasBody = false;
|
||||
}
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' +
|
||||
(self._preBodyHtml || '') +
|
||||
innerHtml +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* PanelButton.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.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.popover = true;
|
||||
panelSettings.autohide = true;
|
||||
|
||||
self.panel = new FloatPanel(panelSettings).on('hide', function() {
|
||||
self.active(false);
|
||||
}).parent(self).renderTo(self.getContainerElm());
|
||||
self.panel.fire('show');
|
||||
self.panel.reflow();
|
||||
} else {
|
||||
self.panel.show();
|
||||
}
|
||||
|
||||
self.panel.moveRel(self.getEl(), settings.popoverAlign || (self.isRtl() ? ['bc-tr', 'bc-tc'] : ['bc-tl', 'bc-tc']));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.on('click', function(e) {
|
||||
if (e.control === self) {
|
||||
if (self.panel && self.panel.visible()) {
|
||||
self.hidePanel();
|
||||
} else {
|
||||
self.showPanel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return self._super();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Path.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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",
|
||||
"tinymce/ui/KeyboardNavigation"
|
||||
], function(Widget, KeyboardNavigation) {
|
||||
"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 items in path.
|
||||
*/
|
||||
init: function(settings) {
|
||||
var self = this;
|
||||
|
||||
if (!settings.delimiter) {
|
||||
settings.delimiter = '\u00BB';
|
||||
}
|
||||
|
||||
self._super(settings);
|
||||
self.addClass('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.data()[index], index: index});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Focuses the current control.
|
||||
*
|
||||
* @method focus
|
||||
* @return {tinymce.ui.Control} Current control instance.
|
||||
*/
|
||||
focus: function() {
|
||||
var self = this;
|
||||
|
||||
self.keyNav = new KeyboardNavigation({
|
||||
root: self,
|
||||
enableLeftRight: true
|
||||
});
|
||||
|
||||
self.keyNav.focusFirst();
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the data to be used for the path.
|
||||
*
|
||||
* @method data
|
||||
* @param {Array} data Array with items name is rendered to path.
|
||||
*/
|
||||
data: function(data) {
|
||||
var self = this;
|
||||
|
||||
if (typeof(data) !== "undefined") {
|
||||
self._data = data;
|
||||
self.update();
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return self._data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updated the path.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
update: function() {
|
||||
this.innerHtml(this._getPathHtml());
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after the control has been rendered.
|
||||
*
|
||||
* @method postRender
|
||||
*/
|
||||
postRender: function() {
|
||||
var self = this;
|
||||
|
||||
self._super();
|
||||
|
||||
self.data(self.settings.data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the control as a HTML string.
|
||||
*
|
||||
* @method renderHtml
|
||||
* @return {String} HTML representing the control.
|
||||
*/
|
||||
renderHtml: function() {
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '">' +
|
||||
self._getPathHtml() +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
_getPathHtml: function() {
|
||||
var self = this, parts = self._data || [], i, l, html = '', prefix = self.classPrefix;
|
||||
|
||||
for (i = 0, l = parts.length; i < l; i++) {
|
||||
html += (
|
||||
(i > 0 ? '<div class="'+ prefix + 'divider" aria-hidden="true"> ' + self.settings.delimiter + ' </div>' : '') +
|
||||
'<div role="button" class="' + prefix + 'path-item' + (i == l - 1 ? ' ' + prefix + 'last' : '') + '" data-index="' +
|
||||
i + '" tabindex="-1" id="' + self._id + '-' + i +'">' + parts[i].name + '</div>'
|
||||
);
|
||||
}
|
||||
|
||||
if (!html) {
|
||||
html = '<div class="' + prefix + 'path-item"> </div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Radio.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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"
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Resizable.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* ResizeHandle.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.addClass('resizehandle');
|
||||
|
||||
if (self.settings.direction == "both") {
|
||||
self.addClass('resizehandle-both');
|
||||
}
|
||||
|
||||
self.canFocus = false;
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '">' +
|
||||
'<i class="' + prefix + 'ico ' + prefix + 'i-resize"></i>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
end: function() {
|
||||
self.fire('ResizeEnd');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
if (this.resizeDragHelper) {
|
||||
this.resizeDragHelper.destroy();
|
||||
}
|
||||
|
||||
return this._super();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Scrollable.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/ui/DomUtils",
|
||||
"tinymce/ui/DragHelper"
|
||||
], function(DomUtils, 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();
|
||||
|
||||
if (self.getEl('absend')) {
|
||||
DomUtils.css(self.getEl('absend'), posNameLower, self.layoutRect()[contentSizeName] - 1);
|
||||
}
|
||||
|
||||
if (!hasScroll) {
|
||||
DomUtils.css(scrollBarElm, 'display', 'none');
|
||||
return;
|
||||
}
|
||||
|
||||
DomUtils.css(scrollBarElm, '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;
|
||||
DomUtils.css(scrollBarElm, rect);
|
||||
|
||||
rect = {};
|
||||
rect[posNameLower] = containerElm["scroll" + posName] * ratio;
|
||||
rect[sizeNameLower] = containerSize * ratio;
|
||||
DomUtils.css(scrollThumbElm, 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().appendChild(DomUtils.createFragment(
|
||||
'<div id="' + axisId + '" class="' + prefix + 'scrollbar ' + prefix + 'scrollbar-' + axisName + '">' +
|
||||
'<div id="' + axisId + 't" class="' + prefix + 'scrollbar-thumb"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
|
||||
self.draghelper = new DragHelper(axisId + 't', {
|
||||
start: function() {
|
||||
scrollStart = self.getEl('body')["scroll" + posName];
|
||||
DomUtils.addClass(DomUtils.get(axisId), 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() {
|
||||
DomUtils.removeClass(DomUtils.get(axisId), prefix + 'active');
|
||||
}
|
||||
});
|
||||
/*
|
||||
self.on('click', function(e) {
|
||||
if (e.target.id == self._id + '-scrollv') {
|
||||
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
self.addClass('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();
|
||||
});
|
||||
|
||||
DomUtils.on(self.getEl('body'), "scroll", repaintScroll);
|
||||
}
|
||||
|
||||
repaintScroll();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Selector.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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:<state>
|
||||
* element:not(<expression>)
|
||||
* element:first
|
||||
* element:last
|
||||
* element:odd
|
||||
* element:even
|
||||
* element element
|
||||
* element > element
|
||||
*
|
||||
* @class tinymce.ui.Selector
|
||||
*/
|
||||
define("tinymce/ui/Selector", [
|
||||
"tinymce/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 */
|
||||
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.hasClass(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;
|
||||
};
|
||||
} else {
|
||||
// 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 psuedo for performance
|
||||
filters.psuedo = !!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 agains 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 psuedo filter like :first needs it
|
||||
if (filters.psuedo) {
|
||||
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 agains 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;
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Spacer.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.addClass('spacer');
|
||||
self.canFocus = false;
|
||||
|
||||
return '<div id="' + self._id + '" class="' + self.classes() + '"></div>';
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* SplitButton.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/MenuButton",
|
||||
"tinymce/ui/DomUtils"
|
||||
], function(MenuButton, DomUtils) {
|
||||
return MenuButton.extend({
|
||||
Defaults: {
|
||||
classes: "widget btn splitbtn",
|
||||
role: "splitbutton"
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
DomUtils.css(mainButtonElm, {
|
||||
width: rect.w - DomUtils.getSize(menuButtonElm).width,
|
||||
height: rect.h - 2
|
||||
});
|
||||
|
||||
DomUtils.css(menuButtonElm, {
|
||||
height: rect.h - 2
|
||||
});
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the active menu state.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
activeMenu: function(state) {
|
||||
var self = this;
|
||||
|
||||
DomUtils.toggleClass(self.getEl().lastChild, 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;
|
||||
var icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '';
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '">' +
|
||||
'<button type="button" hidefocus tabindex="-1">' +
|
||||
(icon ? '<i class="' + icon + '"></i>' : '') +
|
||||
(self._text ? (icon ? ' ' : '') + self._text : '') +
|
||||
'</button>' +
|
||||
'<button type="button" class="' + prefix + 'open" hidefocus tabindex="-1">' +
|
||||
//(icon ? '<i class="' + icon + '"></i>' : '') +
|
||||
(self._menuBtnText ? (icon ? '\u00a0' : '') + self._menuBtnText : '') +
|
||||
' <i class="' + prefix + 'caret"></i>' +
|
||||
'</button>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (node.nodeName == 'BUTTON' && node.className.indexOf('open') == -1) {
|
||||
e.stopImmediatePropagation();
|
||||
onClickHandler.call(this, e);
|
||||
return;
|
||||
}
|
||||
|
||||
node = node.parentNode;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
delete self.settings.onclick;
|
||||
|
||||
return self._super();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* StackLayout.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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'
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* TabPanel.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/ui/DomUtils"
|
||||
], function(Panel, DomUtils) {
|
||||
"use strict";
|
||||
|
||||
return Panel.extend({
|
||||
lastIdx: 0,
|
||||
|
||||
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) {
|
||||
if (this.activeTabId) {
|
||||
DomUtils.removeClass(this.getEl(this.activeTabId), this.classPrefix + 'active');
|
||||
}
|
||||
|
||||
this.activeTabId = 't' + idx;
|
||||
|
||||
DomUtils.addClass(this.getEl('t' + idx), this.classPrefix + 'active');
|
||||
|
||||
if (idx != this.lastIdx) {
|
||||
this.items()[this.lastIdx].hide();
|
||||
this.lastIdx = idx;
|
||||
}
|
||||
|
||||
this.items()[idx].show().fire('showtab');
|
||||
this.reflow();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
tabsHtml += (
|
||||
'<div id="' + self._id + '-t' + i + '" class="' + prefix + 'tab" unselectable="on">' +
|
||||
self.encode(ctrl.settings.title) +
|
||||
'</div>'
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' +
|
||||
'<div id="' + self._id + '-head" class="' + prefix + 'tabs">' +
|
||||
tabsHtml +
|
||||
'</div>' +
|
||||
'<div id="' + self._id + '-body" class="' + self.classes('body') + '">' +
|
||||
layout.renderHtml(self) +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (e.target.parentNode.id == self._id + '-head') {
|
||||
var i = targetParent.childNodes.length;
|
||||
|
||||
while (i--) {
|
||||
if (targetParent.childNodes[i] == e.target) {
|
||||
self.activateTab(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
minW = DomUtils.getSize(self.getEl('head')).width;
|
||||
minW = minW < 0 ? 0 : minW;
|
||||
minH = 0;
|
||||
self.items().each(function(item, i) {
|
||||
minW = Math.max(minW, item.layoutRect().minW);
|
||||
minH = Math.max(minH, item.layoutRect().minH);
|
||||
if (self.settings.activeTab != i) {
|
||||
item.hide();
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
var headH = DomUtils.getSize(self.getEl('head')).height;
|
||||
|
||||
self.settings.minWidth = minW;
|
||||
self.settings.minHeight = minH + headH;
|
||||
|
||||
rect = self._super();
|
||||
rect.deltaH += headH;
|
||||
rect.innerH = rect.h - rect.deltaH;
|
||||
|
||||
return rect;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* TextBox.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new textbox.
|
||||
*
|
||||
* @-x-less TextBox.less
|
||||
* @class tinymce.ui.TextBox
|
||||
* @extends tinymce.ui.Widget
|
||||
*/
|
||||
define("tinymce/ui/TextBox", [
|
||||
"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 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;
|
||||
|
||||
self._super(settings);
|
||||
|
||||
self._value = settings.value || '';
|
||||
self.addClass('textbox');
|
||||
|
||||
if (settings.multiline) {
|
||||
self.addClass('multiline');
|
||||
} else {
|
||||
// TODO: Rework this
|
||||
self.on('keydown', function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
self.parents().reverse().each(function(ctrl) {
|
||||
e.preventDefault();
|
||||
|
||||
if (ctrl.hasEventListeners('submit') && ctrl.toJSON) {
|
||||
ctrl.fire('submit', {data: ctrl.toJSON()});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter/setter function for the disabled state.
|
||||
*
|
||||
* @method value
|
||||
* @param {Boolean} [state] State to be set.
|
||||
* @return {Boolean|tinymce.ui.ComboBox} True/false or self if it's a set operation.
|
||||
*/
|
||||
disabled: function(state) {
|
||||
var self = this;
|
||||
|
||||
if (self._rendered && typeof(state) != 'undefined') {
|
||||
self.getEl().disabled = state;
|
||||
}
|
||||
|
||||
return self._super(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter/setter function for the control value.
|
||||
*
|
||||
* @method value
|
||||
* @param {String} [value] Value to be set.
|
||||
* @return {String|tinymce.ui.ComboBox} Value or self if it's a set operation.
|
||||
*/
|
||||
value: function(value) {
|
||||
var self = this;
|
||||
|
||||
if (typeof(value) != "undefined") {
|
||||
self._value = value;
|
||||
|
||||
if (self._rendered) {
|
||||
self.getEl().value = value;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
if (self._rendered) {
|
||||
return self.getEl().value;
|
||||
}
|
||||
|
||||
return self._value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Repaints the control after a layout operation.
|
||||
*
|
||||
* @method repaint
|
||||
*/
|
||||
repaint: function() {
|
||||
var self = this, style, rect, borderBox, borderW = 0, borderH = 0, lastRepaintRect;
|
||||
|
||||
style = self.getEl().style;
|
||||
rect = self._layoutRect;
|
||||
lastRepaintRect = self._lastRepaintRect || {};
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
borderBox = self._borderBox;
|
||||
borderW = borderBox.left + borderBox.right + 8;
|
||||
borderH = borderBox.top + borderBox.bottom + (self.settings.multiline ? 8 : 0);
|
||||
|
||||
if (rect.x !== lastRepaintRect.x) {
|
||||
style.left = rect.x + 'px';
|
||||
lastRepaintRect.x = rect.x;
|
||||
}
|
||||
|
||||
if (rect.y !== lastRepaintRect.y) {
|
||||
style.top = rect.y + 'px';
|
||||
lastRepaintRect.y = rect.y;
|
||||
}
|
||||
|
||||
if (rect.w !== lastRepaintRect.w) {
|
||||
style.width = (rect.w - borderW) + 'px';
|
||||
lastRepaintRect.w = rect.w;
|
||||
}
|
||||
|
||||
if (rect.h !== lastRepaintRect.h) {
|
||||
style.height = (rect.h - borderH) + 'px';
|
||||
lastRepaintRect.h = rect.h;
|
||||
}
|
||||
|
||||
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, id = self._id, settings = self.settings, value = self.encode(self._value, false), extraAttrs = '';
|
||||
|
||||
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 + '"';
|
||||
}
|
||||
|
||||
if (self.disabled()) {
|
||||
extraAttrs += ' disabled="disabled"';
|
||||
}
|
||||
|
||||
if (settings.multiline) {
|
||||
return (
|
||||
'<textarea id="' + id + '" class="' + self.classes() + '" ' +
|
||||
(settings.rows ? ' rows="' + settings.rows + '"' : '') +
|
||||
' hidefocus="true"' + extraAttrs + '>' + value +
|
||||
'</textarea>'
|
||||
);
|
||||
}
|
||||
|
||||
return '<input id="' + id + '" class="' + self.classes() + '" value="' + value + '" hidefocus="true"' + extraAttrs + '>';
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after the control has been rendered.
|
||||
*
|
||||
* @method postRender
|
||||
*/
|
||||
postRender: function() {
|
||||
var self = this;
|
||||
|
||||
DomUtils.on(self.getEl(), 'change', function(e) {
|
||||
self.fire('change', e);
|
||||
});
|
||||
|
||||
return self._super();
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
DomUtils.off(this.getEl());
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Throbber.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/ui/DomUtils"
|
||||
], function(DomUtils) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Constructs a new throbber.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Element} elm DOM Html element to display throbber in.
|
||||
*/
|
||||
return function(elm) {
|
||||
var self = this, state;
|
||||
|
||||
/**
|
||||
* Shows the throbber.
|
||||
*
|
||||
* @method show
|
||||
* @param {Number} [time] Time to wait before showing.
|
||||
* @return {tinymce.ui.Throbber} Current throbber instance.
|
||||
*/
|
||||
self.show = function(time) {
|
||||
self.hide();
|
||||
|
||||
state = true;
|
||||
|
||||
window.setTimeout(function() {
|
||||
if (state) {
|
||||
elm.appendChild(DomUtils.createFragment('<div class="mce-throbber"></div>'));
|
||||
}
|
||||
}, time || 0);
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the throbber.
|
||||
*
|
||||
* @method hide
|
||||
* @return {tinymce.ui.Throbber} Current throbber instance.
|
||||
*/
|
||||
self.hide = function() {
|
||||
var child = elm.lastChild;
|
||||
|
||||
if (child && child.className.indexOf('throbber') != -1) {
|
||||
child.parentNode.removeChild(child);
|
||||
}
|
||||
|
||||
state = false;
|
||||
|
||||
return self;
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Toolbar.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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",
|
||||
"tinymce/ui/KeyboardNavigation"
|
||||
], function(Container, KeyboardNavigation) {
|
||||
"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.addClass('toolbar');
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after the control has been rendered.
|
||||
*
|
||||
* @method postRender
|
||||
*/
|
||||
postRender: function() {
|
||||
var self = this;
|
||||
|
||||
self.items().addClass('toolbar-item');
|
||||
|
||||
self.keyNav = new KeyboardNavigation({
|
||||
root: self,
|
||||
enableLeftRight: true
|
||||
});
|
||||
|
||||
return self._super();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Tooltip.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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'
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the current label text.
|
||||
*
|
||||
* @method text
|
||||
* @param {String} [text] New label text.
|
||||
* @return {String|tinymce.ui.Tooltip} Current text or current label instance.
|
||||
*/
|
||||
text: function(value) {
|
||||
var self = this;
|
||||
|
||||
if (typeof(value) != "undefined") {
|
||||
self._value = value;
|
||||
|
||||
if (self._rendered) {
|
||||
self.getEl().lastChild.innerHTML = self.encode(value);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
return self._value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the control as a HTML string.
|
||||
*
|
||||
* @method renderHtml
|
||||
* @return {String} HTML representing the control.
|
||||
*/
|
||||
renderHtml: function() {
|
||||
var self = this, prefix = self.classPrefix;
|
||||
|
||||
return (
|
||||
'<div id="' + self._id + '" class="' + self.classes() + '" role="presentation">' +
|
||||
'<div class="' + prefix + 'tooltip-arrow"></div>' +
|
||||
'<div class="' + prefix + 'tooltip-inner">' + self.encode(self._text) + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Widget.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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);
|
||||
self.canFocus = true;
|
||||
|
||||
if (settings.tooltip && Widget.tooltips !== false) {
|
||||
self.on('mouseenter mouseleave', function(e) {
|
||||
var tooltip = self.tooltip().moveTo(-0xFFFF);
|
||||
|
||||
if (e.control == self && e.type == 'mouseenter') {
|
||||
var rel = tooltip.text(settings.tooltip).show().testMoveRel(self.getEl(), ['bc-tc', 'bc-tl', 'bc-tr']);
|
||||
|
||||
tooltip.toggleClass('tooltip-n', rel == 'bc-tc');
|
||||
tooltip.toggleClass('tooltip-nw', rel == 'bc-tl');
|
||||
tooltip.toggleClass('tooltip-ne', rel == 'bc-tr');
|
||||
|
||||
tooltip.moveRel(self.getEl(), rel);
|
||||
} else {
|
||||
tooltip.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.aria('label', settings.tooltip);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current tooltip instance.
|
||||
*
|
||||
* @method tooltip
|
||||
* @return {tinymce.ui.Tooltip} Tooltip instance.
|
||||
*/
|
||||
tooltip: function() {
|
||||
var self = this;
|
||||
|
||||
if (!tooltip) {
|
||||
tooltip = new Tooltip({type: 'tooltip'});
|
||||
tooltip.renderTo(self.getContainerElm());
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the active state of the widget.
|
||||
*
|
||||
* @method active
|
||||
* @param {Boolean} [state] State if the control is active.
|
||||
* @return {Boolean|tinymce.ui.Widget} True/false or current widget instance.
|
||||
*/
|
||||
active: function(state) {
|
||||
var self = this, undef;
|
||||
|
||||
if (state !== undef) {
|
||||
self.aria('pressed', state);
|
||||
self.toggleClass('active', state);
|
||||
}
|
||||
|
||||
return self._super(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets/gets the disabled state of the widget.
|
||||
*
|
||||
* @method disabled
|
||||
* @param {Boolean} [state] State if the control is disabled.
|
||||
* @return {Boolean|tinymce.ui.Widget} True/false or current widget instance.
|
||||
*/
|
||||
disabled: function(state) {
|
||||
var self = this, undef;
|
||||
|
||||
if (state !== undef) {
|
||||
self.aria('disabled', state);
|
||||
self.toggleClass('disabled', state);
|
||||
}
|
||||
|
||||
return self._super(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called after the control has been rendered.
|
||||
*
|
||||
* @method postRender
|
||||
*/
|
||||
postRender: function() {
|
||||
var self = this, settings = self.settings;
|
||||
|
||||
self._rendered = true;
|
||||
|
||||
self._super();
|
||||
|
||||
if (!self.parent() && (settings.width || settings.height)) {
|
||||
self.initLayoutRect();
|
||||
self.repaint();
|
||||
}
|
||||
|
||||
if (settings.autofocus) {
|
||||
setTimeout(function() {
|
||||
self.focus();
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Window.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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", [
|
||||
"tinymce/ui/FloatPanel",
|
||||
"tinymce/ui/Panel",
|
||||
"tinymce/ui/DomUtils",
|
||||
"tinymce/ui/KeyboardNavigation",
|
||||
"tinymce/ui/DragHelper"
|
||||
], function(FloatPanel, Panel, DomUtils, KeyboardNavigation, DragHelper) {
|
||||
"use strict";
|
||||
|
||||
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.addClass('rtl');
|
||||
}
|
||||
|
||||
self.addClass('window');
|
||||
self._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.addClass('foot');
|
||||
self.statusbar.parent(self);
|
||||
}
|
||||
|
||||
self.on('click', function(e) {
|
||||
if (e.target.className.indexOf(self.classPrefix + 'close') != -1) {
|
||||
self.close();
|
||||
}
|
||||
});
|
||||
|
||||
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 = Math.max(0, rect.w / 2 - layoutRect.w / 2);
|
||||
layoutRect.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 = (
|
||||
'<div id="' + id + '-head" class="' + prefix + 'window-head">' +
|
||||
'<div class="' + prefix + 'title">' + self.encode(settings.title) + '</div>' +
|
||||
'<button type="button" class="' + prefix + 'close" aria-hidden="true">×</button>' +
|
||||
'<div id="' + id + '-dragh" class="' + prefix + 'dragh"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.url) {
|
||||
html = '<iframe src="' + settings.url + '" tabindex="-1"></iframe>';
|
||||
}
|
||||
|
||||
if (typeof(html) == "undefined") {
|
||||
html = layout.renderHtml(self);
|
||||
}
|
||||
|
||||
if (self.statusbar) {
|
||||
footerHtml = self.statusbar.renderHtml();
|
||||
}
|
||||
|
||||
return (
|
||||
'<div id="' + id + '" class="' + self.classes() + '" hideFocus="1" tabIndex="-1">' +
|
||||
headerHtml +
|
||||
'<div id="' + id + '-body" class="' + self.classes('body') + '">' +
|
||||
html +
|
||||
'</div>' +
|
||||
footerHtml +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
DomUtils.on(window, '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 = 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 = self.parseBox(self.settings.border);
|
||||
self.getEl('head').style.display = '';
|
||||
layoutRect.deltaH += layoutRect.headerH;
|
||||
DomUtils.removeClass(documentElement, prefix + 'fullscreen');
|
||||
DomUtils.removeClass(document.body, prefix + 'fullscreen');
|
||||
self.removeClass('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 = self.parseBox('0');
|
||||
self.getEl('head').style.display = 'none';
|
||||
layoutRect.deltaH -= layoutRect.headerH + 2;
|
||||
DomUtils.addClass(documentElement, prefix + 'fullscreen');
|
||||
DomUtils.addClass(document.body, prefix + 'fullscreen');
|
||||
self.addClass('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, items = [], focusCtrl, autoFocusFound, startPos;
|
||||
|
||||
setTimeout(function() {
|
||||
self.addClass('in');
|
||||
}, 0);
|
||||
|
||||
self.keyboardNavigation = new KeyboardNavigation({
|
||||
root: self,
|
||||
enableLeftRight: false,
|
||||
enableUpDown: false,
|
||||
items: items,
|
||||
onCancel: function() {
|
||||
self.close();
|
||||
}
|
||||
});
|
||||
|
||||
self.find('*').each(function(ctrl) {
|
||||
if (ctrl.canFocus) {
|
||||
autoFocusFound = autoFocusFound || ctrl.settings.autofocus;
|
||||
focusCtrl = focusCtrl || ctrl;
|
||||
|
||||
// TODO: Figure out a better way
|
||||
if (ctrl.type == 'filepicker') {
|
||||
items.push(ctrl.getEl('inp'));
|
||||
|
||||
if (ctrl.getEl('open')) {
|
||||
items.push(ctrl.getEl('open'));
|
||||
}
|
||||
} else {
|
||||
items.push(ctrl.getEl());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (self.statusbar) {
|
||||
self.statusbar.find('*').each(function(ctrl) {
|
||||
if (ctrl.canFocus) {
|
||||
autoFocusFound = autoFocusFound || ctrl.settings.autofocus;
|
||||
focusCtrl = focusCtrl || ctrl;
|
||||
items.push(ctrl.getEl());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self._super();
|
||||
|
||||
if (self.statusbar) {
|
||||
self.statusbar.postRender();
|
||||
}
|
||||
|
||||
if (!autoFocusFound && focusCtrl) {
|
||||
focusCtrl.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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
self.dragHelper.destroy();
|
||||
self._super();
|
||||
|
||||
if (self.statusbar) {
|
||||
this.statusbar.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Window;
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Class.js
|
||||
*
|
||||
* Copyright 2003-2012, Moxiecode Systems AB, All rights reserved.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This utilitiy class is used for easier inheritage.
|
||||
*
|
||||
* 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/util/Class", [
|
||||
"tinymce/util/Tools"
|
||||
], 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;
|
||||
|
||||
// The dummy class constructor
|
||||
function Class() {
|
||||
var i, mixins, mixin, self;
|
||||
|
||||
// All construction is actually done in the init method
|
||||
if (!initializing) {
|
||||
self = this;
|
||||
|
||||
// Run class constuctor
|
||||
if (self.init) {
|
||||
self.init.apply(self, arguments);
|
||||
}
|
||||
|
||||
// Run mixin constructors
|
||||
mixins = self.Mixins;
|
||||
if (mixins) {
|
||||
i = mixins.length;
|
||||
while (i--) {
|
||||
mixin = mixins[i];
|
||||
if (mixin.init) {
|
||||
mixin.init.apply(self, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy function, needs to be extended in order to provide functionality
|
||||
function dummy() {
|
||||
return this;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
self._super = _super[name];
|
||||
ret = fn.apply(self, arguments);
|
||||
self._super = tmp;
|
||||
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
// Instantiate a base class (but only create the instance,
|
||||
// don't run the init constructor)
|
||||
initializing = true;
|
||||
prototype = new Self();
|
||||
initializing = false;
|
||||
|
||||
// Add mixins
|
||||
if (prop.Mixins) {
|
||||
each(prop.Mixins, function(mixin) {
|
||||
mixin = mixin;
|
||||
|
||||
for (var name in mixin) {
|
||||
if (name !== "init") {
|
||||
prop[name] = mixin[name];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate property methods
|
||||
if (prop.Properties) {
|
||||
each(prop.Properties.split(','), function(name) {
|
||||
var fieldName = '_' + name;
|
||||
|
||||
prop[name] = function(value) {
|
||||
var self = this, undef;
|
||||
|
||||
// Set value
|
||||
if (value !== undef) {
|
||||
self[fieldName] = value;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Copy the properties over onto the new prototype
|
||||
for (name in prop) {
|
||||
member = prop[name];
|
||||
|
||||
if (typeof member == "function" && _super[name]) {
|
||||
prototype[name] = createMethod(name, member);
|
||||
} else {
|
||||
prototype[name] = member;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate our constructed prototype object
|
||||
Class.prototype = prototype;
|
||||
|
||||
// Enforce the constructor to be what we expect
|
||||
Class.constructor = Class;
|
||||
|
||||
// And make this class extendible
|
||||
Class.extend = extendClass;
|
||||
|
||||
return Class;
|
||||
};
|
||||
|
||||
return Class;
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* I18n.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* I18n class that handles translation of TinyMCE UI.
|
||||
* Uses po style with csharp style parameters.
|
||||
*
|
||||
* @class tinymce.util.I18n
|
||||
*/
|
||||
define("tinymce/util/I18n", [], function() {
|
||||
"use strict";
|
||||
|
||||
var data = {};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Property gets set to true if a RTL language pack was loaded.
|
||||
*
|
||||
* @property rtl
|
||||
* @type {Boolean}
|
||||
*/
|
||||
rtl: false,
|
||||
|
||||
/**
|
||||
* Adds translations for a specific language code.
|
||||
*
|
||||
* @method add
|
||||
* @param {String} code Language code like sv_SE.
|
||||
* @param {Array} items Name/value array with English en_US to sv_SE.
|
||||
*/
|
||||
add: function(code, items) {
|
||||
for (var name in items) {
|
||||
data[name] = items[name];
|
||||
}
|
||||
|
||||
this.rtl = this.rtl || data._dir === 'rtl';
|
||||
},
|
||||
|
||||
/**
|
||||
* Translates the specified text.
|
||||
*
|
||||
* It has a few formats:
|
||||
* I18n.translate("Text");
|
||||
* I18n.translate(["Text {0}/{1}", 0, 1]);
|
||||
* I18n.translate({raw: "Raw string"});
|
||||
*
|
||||
* @method translate
|
||||
* @param {String/Object/Array} text Text to translate.
|
||||
* @return {String} String that got translated.
|
||||
*/
|
||||
translate: function(text) {
|
||||
if (typeof(text) == "undefined") {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (typeof(text) != "string" && text.raw) {
|
||||
return text.raw;
|
||||
}
|
||||
|
||||
if (text.push) {
|
||||
var values = text.slice(1);
|
||||
|
||||
text = (data[text[0]] || text[0]).replace(/\{([^\}]+)\}/g, function(match1, match2) {
|
||||
return values[match2];
|
||||
});
|
||||
}
|
||||
|
||||
return data[text] || text;
|
||||
},
|
||||
|
||||
data: data
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* JSON.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* JSON parser and serializer class.
|
||||
*
|
||||
* @class tinymce.util.JSON
|
||||
* @static
|
||||
* @example
|
||||
* // JSON parse a string into an object
|
||||
* var obj = tinymce.util.JSON.parse(somestring);
|
||||
*
|
||||
* // JSON serialize a object into an string
|
||||
* var str = tinymce.util.JSON.serialize(obj);
|
||||
*/
|
||||
define("tinymce/util/JSON", [], function() {
|
||||
function serialize(o, quote) {
|
||||
var i, v, t, name;
|
||||
|
||||
quote = quote || '"';
|
||||
|
||||
if (o === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
t = typeof o;
|
||||
|
||||
if (t == 'string') {
|
||||
v = '\bb\tt\nn\ff\rr\""\'\'\\\\';
|
||||
|
||||
return quote + o.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g, function(a, b) {
|
||||
// Make sure single quotes never get encoded inside double quotes for JSON compatibility
|
||||
if (quote === '"' && a === "'") {
|
||||
return a;
|
||||
}
|
||||
|
||||
i = v.indexOf(b);
|
||||
|
||||
if (i + 1) {
|
||||
return '\\' + v.charAt(i + 1);
|
||||
}
|
||||
|
||||
a = b.charCodeAt().toString(16);
|
||||
|
||||
return '\\u' + '0000'.substring(a.length) + a;
|
||||
}) + quote;
|
||||
}
|
||||
|
||||
if (t == 'object') {
|
||||
if (o.hasOwnProperty && Object.prototype.toString.call(o) === '[object Array]') {
|
||||
for (i=0, v = '['; i<o.length; i++) {
|
||||
v += (i > 0 ? ',' : '') + serialize(o[i], quote);
|
||||
}
|
||||
|
||||
return v + ']';
|
||||
}
|
||||
|
||||
v = '{';
|
||||
|
||||
for (name in o) {
|
||||
if (o.hasOwnProperty(name)) {
|
||||
v += typeof o[name] != 'function' ? (v.length > 1 ? ',' + quote : quote) + name +
|
||||
quote +':' + serialize(o[name], quote) : '';
|
||||
}
|
||||
}
|
||||
|
||||
return v + '}';
|
||||
}
|
||||
|
||||
return '' + o;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Serializes the specified object as a JSON string.
|
||||
*
|
||||
* @method serialize
|
||||
* @param {Object} obj Object to serialize as a JSON string.
|
||||
* @param {String} quote Optional quote string defaults to ".
|
||||
* @return {string} JSON string serialized from input.
|
||||
*/
|
||||
serialize: serialize,
|
||||
|
||||
/**
|
||||
* Unserializes/parses the specified JSON string into a object.
|
||||
*
|
||||
* @method parse
|
||||
* @param {string} s JSON String to parse into a JavaScript object.
|
||||
* @return {Object} Object from input JSON string or undefined if it failed.
|
||||
*/
|
||||
parse: function(text) {
|
||||
try {
|
||||
// Trick uglify JS
|
||||
return window[String.fromCharCode(101) + 'val']('(' + text + ')');
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**#@-*/
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* JSONP.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
define("tinymce/util/JSONP", [
|
||||
"tinymce/dom/DOMUtils"
|
||||
], function(DOMUtils) {
|
||||
return {
|
||||
callbacks: {},
|
||||
count: 0,
|
||||
|
||||
send: function(settings) {
|
||||
var self = this, dom = DOMUtils.DOM, count = settings.count !== undefined ? settings.count : self.count;
|
||||
var id = 'tinymce_jsonp_' + count;
|
||||
|
||||
self.callbacks[count] = function(json) {
|
||||
dom.remove(id);
|
||||
delete self.callbacks[count];
|
||||
|
||||
settings.callback(json);
|
||||
};
|
||||
|
||||
dom.add(dom.doc.body, 'script', {
|
||||
id: id,
|
||||
src: settings.url,
|
||||
type: 'text/javascript'
|
||||
});
|
||||
|
||||
self.count++;
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* JSONRequest.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class enables you to use JSON-RPC to call backend methods.
|
||||
*
|
||||
* @class tinymce.util.JSONRequest
|
||||
* @example
|
||||
* var json = new tinymce.util.JSONRequest({
|
||||
* url: 'somebackend.php'
|
||||
* });
|
||||
*
|
||||
* // Send RPC call 1
|
||||
* json.send({
|
||||
* method: 'someMethod1',
|
||||
* params: ['a', 'b'],
|
||||
* success: function(result) {
|
||||
* console.dir(result);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Send RPC call 2
|
||||
* json.send({
|
||||
* method: 'someMethod2',
|
||||
* params: ['a', 'b'],
|
||||
* success: function(result) {
|
||||
* console.dir(result);
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
define("tinymce/util/JSONRequest", [
|
||||
"tinymce/util/JSON",
|
||||
"tinymce/util/XHR",
|
||||
"tinymce/util/Tools"
|
||||
], function(JSON, XHR, Tools) {
|
||||
var extend = Tools.extend;
|
||||
|
||||
function JSONRequest(settings) {
|
||||
this.settings = extend({}, settings);
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple helper function to send a JSON-RPC request without the need to initialize an object.
|
||||
* Consult the Wiki API documentation for more details on what you can pass to this function.
|
||||
*
|
||||
* @method sendRPC
|
||||
* @static
|
||||
* @param {Object} o Call object where there are three field id, method and params this object should also contain callbacks etc.
|
||||
*/
|
||||
JSONRequest.sendRPC = function(o) {
|
||||
return new JSONRequest().send(o);
|
||||
};
|
||||
|
||||
JSONRequest.prototype = {
|
||||
/**
|
||||
* Sends a JSON-RPC call. Consult the Wiki API documentation for more details on what you can pass to this function.
|
||||
*
|
||||
* @method send
|
||||
* @param {Object} args Call object where there are three field id, method and params this object should also contain callbacks etc.
|
||||
*/
|
||||
send: function(args) {
|
||||
var ecb = args.error, scb = args.success;
|
||||
|
||||
args = extend(this.settings, args);
|
||||
|
||||
args.success = function(c, x) {
|
||||
c = JSON.parse(c);
|
||||
|
||||
if (typeof(c) == 'undefined') {
|
||||
c = {
|
||||
error : 'JSON Parse error.'
|
||||
};
|
||||
}
|
||||
|
||||
if (c.error) {
|
||||
ecb.call(args.error_scope || args.scope, c.error, x);
|
||||
} else {
|
||||
scb.call(args.success_scope || args.scope, c.result);
|
||||
}
|
||||
};
|
||||
|
||||
args.error = function(ty, x) {
|
||||
if (ecb) {
|
||||
ecb.call(args.error_scope || args.scope, ty, x);
|
||||
}
|
||||
};
|
||||
|
||||
args.data = JSON.serialize({
|
||||
id: args.id || 'c' + (this.count++),
|
||||
method: args.method,
|
||||
params: args.params
|
||||
});
|
||||
|
||||
// JSON content type for Ruby on rails. Bug: #1883287
|
||||
args.content_type = 'application/json';
|
||||
|
||||
XHR.send(args);
|
||||
}
|
||||
};
|
||||
|
||||
return JSONRequest;
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* LocalStorage.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class will simulate LocalStorage on IE 7 and return the native version on modern browsers.
|
||||
* Storage is done using userData on IE 7 and a special serialization format. The format is designed
|
||||
* to be as small as possible by making sure that the keys and values doesn't need to be encoded. This
|
||||
* makes it possible to store for example HTML data.
|
||||
*
|
||||
* Storage format for userData:
|
||||
* <base 32 key length>,<key string>,<base 32 value length>,<value>,...
|
||||
*
|
||||
* For example this data key1=value1,key2=value2 would be:
|
||||
* 4,key1,6,value1,4,key2,6,value2
|
||||
*
|
||||
* @class tinymce.util.LocalStorage
|
||||
* @static
|
||||
* @version 4.0
|
||||
* @example
|
||||
* tinymce.util.LocalStorage.setItem('key', 'value');
|
||||
* var value = tinymce.util.LocalStorage.getItem('key');
|
||||
*/
|
||||
define("tinymce/util/LocalStorage", [], function() {
|
||||
var LocalStorage, storageElm, items, keys, userDataKey, hasOldIEDataSupport;
|
||||
|
||||
// Check for native support
|
||||
try {
|
||||
if (window.localStorage) {
|
||||
return localStorage;
|
||||
}
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
userDataKey = "tinymce";
|
||||
storageElm = document.documentElement;
|
||||
hasOldIEDataSupport = !!storageElm.addBehavior;
|
||||
|
||||
if (hasOldIEDataSupport) {
|
||||
storageElm.addBehavior('#default#userData');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the keys names and updates LocalStorage.length property. Since IE7 doesn't have any getters/setters.
|
||||
*/
|
||||
function updateKeys() {
|
||||
keys = [];
|
||||
|
||||
for (var key in items) {
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
LocalStorage.length = keys.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the userData string and parses it into the items structure.
|
||||
*/
|
||||
function load() {
|
||||
var key, data, value, pos = 0;
|
||||
|
||||
items = {};
|
||||
|
||||
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
|
||||
if (!hasOldIEDataSupport) {
|
||||
return;
|
||||
}
|
||||
|
||||
function next(end) {
|
||||
var value, nextPos;
|
||||
|
||||
nextPos = end !== undefined ? pos + end : data.indexOf(',', pos);
|
||||
if (nextPos === -1 || nextPos > data.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = data.substring(pos, nextPos);
|
||||
pos = nextPos + 1;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
storageElm.load(userDataKey);
|
||||
data = storageElm.getAttribute(userDataKey) || '';
|
||||
|
||||
do {
|
||||
var offset = next();
|
||||
if (offset === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
key = next(parseInt(offset, 32) || 0);
|
||||
if (key !== null) {
|
||||
offset = next();
|
||||
if (offset === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
value = next(parseInt(offset, 32) || 0);
|
||||
|
||||
if (key) {
|
||||
items[key] = value;
|
||||
}
|
||||
}
|
||||
} while (key !== null);
|
||||
|
||||
updateKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the items structure into a the userData format.
|
||||
*/
|
||||
function save() {
|
||||
var value, data = '';
|
||||
|
||||
// localStorage can be disabled on WebKit/Gecko so make a dummy storage
|
||||
if (!hasOldIEDataSupport) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var key in items) {
|
||||
value = items[key];
|
||||
data += (data ? ',' : '') + key.length.toString(32) + ',' + key + ',' + value.length.toString(32) + ',' + value;
|
||||
}
|
||||
|
||||
storageElm.setAttribute(userDataKey, data);
|
||||
|
||||
try {
|
||||
storageElm.save(userDataKey);
|
||||
} catch (ex) {
|
||||
// Ignore disk full
|
||||
}
|
||||
|
||||
updateKeys();
|
||||
}
|
||||
|
||||
LocalStorage = {
|
||||
/**
|
||||
* Length of the number of items in storage.
|
||||
*
|
||||
* @property length
|
||||
* @type Number
|
||||
* @return {Number} Number of items in storage.
|
||||
*/
|
||||
//length:0,
|
||||
|
||||
/**
|
||||
* Returns the key name by index.
|
||||
*
|
||||
* @method key
|
||||
* @param {Number} index Index of key to return.
|
||||
* @return {String} Key value or null if it wasn't found.
|
||||
*/
|
||||
key: function(index) {
|
||||
return keys[index];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the value if the specified key or null if it wasn't found.
|
||||
*
|
||||
* @method getItem
|
||||
* @param {String} key Key of item to retrive.
|
||||
* @return {String} Value of the specified item or null if it wasn't found.
|
||||
*/
|
||||
getItem: function(key) {
|
||||
return key in items ? items[key] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the value of the specified item by it's key.
|
||||
*
|
||||
* @method setItem
|
||||
* @param {String} key Key of the item to set.
|
||||
* @param {String} value Value of the item to set.
|
||||
*/
|
||||
setItem: function(key, value) {
|
||||
items[key] = "" + value;
|
||||
save();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the specified item by key.
|
||||
*
|
||||
* @method removeItem
|
||||
* @param {String} key Key of item to remove.
|
||||
*/
|
||||
removeItem: function(key) {
|
||||
delete items[key];
|
||||
save();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes all items.
|
||||
*
|
||||
* @method clear
|
||||
*/
|
||||
clear: function() {
|
||||
items = {};
|
||||
save();
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
return LocalStorage;
|
||||
});
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Observable.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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/util/Observable", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var bindingsName = "__bindings";
|
||||
var nativeEvents = Tools.makeMap(
|
||||
"focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange" +
|
||||
" mouseout mouseenter mouseleave keydown keypress keyup contextmenu dragend dragover draggesture dragdrop drop drag", ' '
|
||||
);
|
||||
|
||||
function returnFalse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function returnTrue() {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Fires the specified event by name.
|
||||
*
|
||||
* @method fire
|
||||
* @param {String} name Name of the event to fire.
|
||||
* @param {tinymce.Event/Object?} args Event arguments.
|
||||
* @param {Boolean?} bubble True/false if the event is to be bubbled.
|
||||
* @return {tinymce.Event} Event instance passed in converted into tinymce.Event instance.
|
||||
* @example
|
||||
* instance.fire('event', {...});
|
||||
*/
|
||||
fire: function(name, args, bubble) {
|
||||
var self = this, handlers, i, l, callback, parent;
|
||||
|
||||
name = name.toLowerCase();
|
||||
args = args || {};
|
||||
args.type = name;
|
||||
|
||||
// Setup target is there isn't one
|
||||
if (!args.target) {
|
||||
args.target = self;
|
||||
}
|
||||
|
||||
// Add event delegation methods if they are missing
|
||||
if (!args.preventDefault) {
|
||||
// Add preventDefault method
|
||||
args.preventDefault = function() {
|
||||
args.isDefaultPrevented = returnTrue;
|
||||
};
|
||||
|
||||
// Add stopPropagation
|
||||
args.stopPropagation = function() {
|
||||
args.isPropagationStopped = returnTrue;
|
||||
};
|
||||
|
||||
// Add stopImmediatePropagation
|
||||
args.stopImmediatePropagation = function() {
|
||||
args.isImmediatePropagationStopped = returnTrue;
|
||||
};
|
||||
|
||||
// Add event delegation states
|
||||
args.isDefaultPrevented = returnFalse;
|
||||
args.isPropagationStopped = returnFalse;
|
||||
args.isImmediatePropagationStopped = returnFalse;
|
||||
}
|
||||
|
||||
//console.log(name, args);
|
||||
|
||||
if (self[bindingsName]) {
|
||||
handlers = self[bindingsName][name];
|
||||
|
||||
if (handlers) {
|
||||
for (i = 0, l = handlers.length; i < l; i++) {
|
||||
handlers[i] = callback = handlers[i];
|
||||
|
||||
// Stop immediate propagation if needed
|
||||
if (args.isImmediatePropagationStopped()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If callback returns false then prevent default and stop all propagation
|
||||
if (callback.call(self, args) === false) {
|
||||
args.preventDefault();
|
||||
return args;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble event up to parents
|
||||
if (bubble !== false && self.parent) {
|
||||
parent = self.parent();
|
||||
while (parent && !args.isPropagationStopped()) {
|
||||
parent.fire(name, args, false);
|
||||
parent = parent.parent();
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return {Object} Current class instance.
|
||||
* @example
|
||||
* instance.on('event', function(e) {
|
||||
* // Callback logic
|
||||
* });
|
||||
*/
|
||||
on: function(name, callback) {
|
||||
var self = this, bindings, handlers, names, i;
|
||||
|
||||
if (callback === false) {
|
||||
callback = function() {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
names = name.toLowerCase().split(' ');
|
||||
i = names.length;
|
||||
while (i--) {
|
||||
name = names[i];
|
||||
|
||||
bindings = self[bindingsName];
|
||||
if (!bindings) {
|
||||
bindings = self[bindingsName] = {};
|
||||
}
|
||||
|
||||
handlers = bindings[name];
|
||||
if (!handlers) {
|
||||
handlers = bindings[name] = [];
|
||||
if (self.bindNative && nativeEvents[name]) {
|
||||
self.bindNative(name);
|
||||
}
|
||||
}
|
||||
|
||||
handlers.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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();
|
||||
*/
|
||||
off: function(name, callback) {
|
||||
var self = this, i, bindings = self[bindingsName], handlers, bindingName, names, hi;
|
||||
|
||||
if (bindings) {
|
||||
if (name) {
|
||||
names = name.toLowerCase().split(' ');
|
||||
i = names.length;
|
||||
while (i--) {
|
||||
name = names[i];
|
||||
handlers = bindings[name];
|
||||
|
||||
// Unbind all handlers
|
||||
if (!name) {
|
||||
for (bindingName in bindings) {
|
||||
bindings[name].length = 0;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
if (handlers) {
|
||||
// Unbind all by name
|
||||
if (!callback) {
|
||||
handlers.length = 0;
|
||||
} else {
|
||||
// Unbind specific ones
|
||||
hi = handlers.length;
|
||||
while (hi--) {
|
||||
if (handlers[hi] === callback) {
|
||||
handlers.splice(hi, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!handlers.length && self.unbindNative && nativeEvents[name]) {
|
||||
self.unbindNative(name);
|
||||
delete bindings[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (self.unbindNative) {
|
||||
for (name in bindings) {
|
||||
self.unbindNative(name);
|
||||
}
|
||||
}
|
||||
|
||||
self[bindingsName] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
},
|
||||
|
||||
hasEventListeners: function(name) {
|
||||
var bindings = this[bindingsName];
|
||||
|
||||
name = name.toLowerCase();
|
||||
|
||||
return !(!bindings || !bindings[name] || bindings[name].length === 0);
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,1000 @@
|
||||
/**
|
||||
* Quirks.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @class tinymce.util.Quirks
|
||||
*/
|
||||
define("tinymce/util/Quirks", [
|
||||
"tinymce/util/VK",
|
||||
"tinymce/dom/RangeUtils",
|
||||
"tinymce/html/Node",
|
||||
"tinymce/html/Entities",
|
||||
"tinymce/Env",
|
||||
"tinymce/util/Tools"
|
||||
], function(VK, RangeUtils, Node, Entities, Env, Tools) {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes a WebKit bug when deleting contents using backspace or delete key.
|
||||
* WebKit will produce a span element if you delete across two block elements.
|
||||
*
|
||||
* Example:
|
||||
* <h1>a</h1><p>|b</p>
|
||||
*
|
||||
* Will produce this on backspace:
|
||||
* <h1>a<span class="Apple-style-span" style="<all runtime styles>">b</span></p>
|
||||
*
|
||||
* This fixes the backspace to produce:
|
||||
* <h1>a|b</p>
|
||||
*
|
||||
* See bug: https://bugs.webkit.org/show_bug.cgi?id=45784
|
||||
*
|
||||
* This code is a bit of a hack and hopefully it will be fixed soon in WebKit.
|
||||
*/
|
||||
function cleanupStylesWhenDeleting() {
|
||||
function removeMergedFormatSpans(isDelete) {
|
||||
var rng, blockElm, wrapperElm, bookmark, container, offset, elm;
|
||||
|
||||
function isAtStartOrEndOfElm() {
|
||||
if (container.nodeType == 3) {
|
||||
if (isDelete && offset == container.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isDelete && offset === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rng = selection.getRng();
|
||||
var tmpRng = [rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset];
|
||||
|
||||
if (!rng.collapsed) {
|
||||
isDelete = true;
|
||||
}
|
||||
|
||||
container = rng[(isDelete ? 'start' : 'end') + 'Container'];
|
||||
offset = rng[(isDelete ? 'start' : 'end') + 'Offset'];
|
||||
|
||||
if (container.nodeType == 3) {
|
||||
blockElm = dom.getParent(rng.startContainer, dom.isBlock);
|
||||
|
||||
// On delete clone the root span of the next block element
|
||||
if (isDelete) {
|
||||
blockElm = dom.getNext(blockElm, dom.isBlock);
|
||||
}
|
||||
|
||||
if (blockElm && (isAtStartOrEndOfElm() || !rng.collapsed)) {
|
||||
// Wrap children of block in a EM and let WebKit stick is
|
||||
// runtime styles junk into that EM
|
||||
wrapperElm = dom.create('em', {'id': '__mceDel'});
|
||||
|
||||
each(Tools.grep(blockElm.childNodes), function(node) {
|
||||
wrapperElm.appendChild(node);
|
||||
});
|
||||
|
||||
blockElm.appendChild(wrapperElm);
|
||||
}
|
||||
}
|
||||
|
||||
// Do the backspace/delete action
|
||||
rng = dom.createRng();
|
||||
rng.setStart(tmpRng[0], tmpRng[1]);
|
||||
rng.setEnd(tmpRng[2], tmpRng[3]);
|
||||
selection.setRng(rng);
|
||||
editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);
|
||||
|
||||
// Remove temp wrapper element
|
||||
if (wrapperElm) {
|
||||
bookmark = selection.getBookmark();
|
||||
|
||||
while ((elm = dom.get('__mceDel'))) {
|
||||
dom.remove(elm, true);
|
||||
}
|
||||
|
||||
selection.moveToBookmark(bookmark);
|
||||
}
|
||||
}
|
||||
|
||||
editor.on('keydown', function(e) {
|
||||
var isDelete;
|
||||
|
||||
isDelete = e.keyCode == DELETE;
|
||||
if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {
|
||||
e.preventDefault();
|
||||
removeMergedFormatSpans(isDelete);
|
||||
}
|
||||
});
|
||||
|
||||
editor.addCommand('Delete', function() {removeMergedFormatSpans();});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that the editor body becomes empty when backspace or delete is pressed in empty editors.
|
||||
*
|
||||
* For example:
|
||||
* <p><b>|</b></p>
|
||||
*
|
||||
* Or:
|
||||
* <h1>|</h1>
|
||||
*
|
||||
* Or:
|
||||
* [<h1></h1>]
|
||||
*/
|
||||
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) {
|
||||
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;
|
||||
|
||||
// Empty the editor if it's needed for example backspace at <p><b>|</b></p>
|
||||
if (!isDefaultPrevented(e) && (keyCode == DELETE || keyCode == BACKSPACE)) {
|
||||
isCollapsed = editor.selection.isCollapsed();
|
||||
|
||||
// Selection is collapsed but the editor isn't empty
|
||||
if (isCollapsed && !dom.isEmpty(editor.getBody())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// IE deletes all contents correctly when everything is selected
|
||||
if (isIE && !isCollapsed) {
|
||||
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('');
|
||||
editor.selection.setCursorLocation(editor.getBody(), 0);
|
||||
editor.nodeChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* WebKit doesn't select all the nodes in the body when you press Ctrl+A.
|
||||
* This selects the whole body so that backspace/delete logic will delete everything
|
||||
*/
|
||||
function selectAll() {
|
||||
editor.on('keydown', function(e) {
|
||||
if (!isDefaultPrevented(e) && e.keyCode == 65 && VK.metaKeyPressed(e)) {
|
||||
e.preventDefault();
|
||||
editor.execCommand('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
|
||||
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
|
||||
dom.bind(editor.getDoc(), 'mousedown', function(e) {
|
||||
if (e.target == editor.getDoc().documentElement) {
|
||||
editor.getWin().focus();
|
||||
selection.setRng(selection.getRng());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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
|
||||
setTimeout(function() {
|
||||
body.focus();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
e = 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 tobe the setBaseAndExtend or it will fail to select floated images
|
||||
if (/^(IMG|HR)$/.test(e.nodeName)) {
|
||||
selection.getSel().setBaseAndExtent(e, 0, e, 1);
|
||||
}
|
||||
|
||||
if (e.nodeName == 'A' && dom.hasClass(e, 'mce-item-anchor')) {
|
||||
selection.select(e);
|
||||
}
|
||||
|
||||
editor.nodeChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements.
|
||||
*
|
||||
* Fixes do backspace/delete on this:
|
||||
* <p>bla[ck</p><p style="color:red">r]ed</p>
|
||||
*
|
||||
* Would become:
|
||||
* <p>bla|ed</p>
|
||||
*
|
||||
* Instead of:
|
||||
* <p style="color:red">bla|ed</p>
|
||||
*/
|
||||
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();
|
||||
|
||||
setTimeout(function() {
|
||||
applyAttributes();
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a nodeChanged when the selection is changed on WebKit this fixes selection issues on iOS5. It only fires the nodeChange
|
||||
* event every 50ms since it would other wise update the UI when you type and it hogs the CPU.
|
||||
*/
|
||||
function selectionChangeNodeChanged() {
|
||||
var lastRng, selectionTimer;
|
||||
|
||||
editor.on('selectionchange', function() {
|
||||
if (selectionTimer) {
|
||||
clearTimeout(selectionTimer);
|
||||
selectionTimer = 0;
|
||||
}
|
||||
|
||||
selectionTimer = window.setTimeout(function() {
|
||||
var rng = selection.getRng();
|
||||
|
||||
// Compare the ranges to see if it was a real change or not
|
||||
if (!lastRng || !RangeUtils.compareRanges(rng, lastRng)) {
|
||||
editor.nodeChanged();
|
||||
lastRng = rng;
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', '');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Backspace or delete on WebKit will combine all visual styles in a span if the last character is deleted.
|
||||
*
|
||||
* For example backspace on:
|
||||
* <p><b>x|</b></p>
|
||||
*
|
||||
* Will produce:
|
||||
* <p><span style="font-weight: bold">|<br></span></p>
|
||||
*
|
||||
* When it should produce:
|
||||
* <p><b>|<br></b></p>
|
||||
*
|
||||
* See: https://bugs.webkit.org/show_bug.cgi?id=81656
|
||||
*/
|
||||
function keepInlineElementOnDeleteBackspace() {
|
||||
editor.on('keydown', function(e) {
|
||||
var isDelete, rng, container, offset, brElm, sibling, collapsed, nonEmptyElements;
|
||||
|
||||
isDelete = e.keyCode == DELETE;
|
||||
if (!isDefaultPrevented(e) && (isDelete || e.keyCode == BACKSPACE) && !VK.modifierPressed(e)) {
|
||||
rng = selection.getRng();
|
||||
container = rng.startContainer;
|
||||
offset = rng.startOffset;
|
||||
collapsed = rng.collapsed;
|
||||
|
||||
// Override delete if the start container is a text node and is at the beginning of text or
|
||||
// just before/after the last character to be deleted in collapsed mode
|
||||
if (container.nodeType == 3 && container.nodeValue.length > 0 && ((offset === 0 && !collapsed) ||
|
||||
(collapsed && offset === (isDelete ? 0 : 1)))) {
|
||||
// Edge case when deleting <p><b><img> |x</b></p>
|
||||
sibling = container.previousSibling;
|
||||
if (sibling && sibling.nodeName == "IMG") {
|
||||
return;
|
||||
}
|
||||
|
||||
nonEmptyElements = editor.schema.getNonEmptyElements();
|
||||
|
||||
// Prevent default logic since it's broken
|
||||
e.preventDefault();
|
||||
|
||||
// Insert a BR before the text node this will prevent the containing element from being deleted/converted
|
||||
brElm = dom.create('br', {id: '__tmp'});
|
||||
container.parentNode.insertBefore(brElm, container);
|
||||
|
||||
// Do the browser delete
|
||||
editor.getDoc().execCommand(isDelete ? 'ForwardDelete' : 'Delete', false, null);
|
||||
|
||||
// Check if the previous sibling is empty after deleting for example: <p><b></b>|</p>
|
||||
container = selection.getRng().startContainer;
|
||||
sibling = container.previousSibling;
|
||||
if (sibling && sibling.nodeType == 1 && !dom.isBlock(sibling) && dom.isEmpty(sibling) &&
|
||||
!nonEmptyElements[sibling.nodeName.toLowerCase()]) {
|
||||
dom.remove(sibling);
|
||||
}
|
||||
|
||||
// Remove the temp element we inserted
|
||||
dom.remove('__tmp');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a blockquote when backspace is pressed at the beginning of it.
|
||||
*
|
||||
* For example:
|
||||
* <blockquote><p>|x</p></blockquote>
|
||||
*
|
||||
* Becomes:
|
||||
* <p>|x</p>
|
||||
*/
|
||||
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() {
|
||||
editor._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:
|
||||
* <p><b><a href="#">x</a></b></p>
|
||||
*
|
||||
* Becomes this:
|
||||
* <p><b><a href="#">x</a></b><br></p>
|
||||
*/
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes ghost selections from images/tables on Gecko.
|
||||
*/
|
||||
function removeGhostSelection() {
|
||||
editor.on('Undo Redo SetContent', function(e) {
|
||||
if (!e.initial) {
|
||||
editor.execCommand('mceRepaint');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.win.focus();
|
||||
startRng.select();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes selection issues on Gecko where the caret can be placed between two inline elements like <b>a</b>|<b>b</b>
|
||||
* this fix will lean the caret right into the closest inline element.
|
||||
*/
|
||||
function normalizeSelection() {
|
||||
// Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything
|
||||
editor.on('keyup focusin', function(e) {
|
||||
if (e.keyCode != 65 || !VK.metaKeyPressed(e)) {
|
||||
selection.normalize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (e.target.nodeName == 'HTML') {
|
||||
editor.execCommand('SelectAll');
|
||||
editor.selection.collapse(true);
|
||||
editor.nodeChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// All browsers
|
||||
disableBackspaceIntoATable();
|
||||
removeBlockQuoteOnBackSpace();
|
||||
emptyEditorWhenDeleting();
|
||||
normalizeSelection();
|
||||
|
||||
// WebKit
|
||||
if (isWebKit) {
|
||||
keepInlineElementOnDeleteBackspace();
|
||||
cleanupStylesWhenDeleting();
|
||||
inputMethodFocus();
|
||||
selectControlElements();
|
||||
setDefaultBlockType();
|
||||
|
||||
// iOS
|
||||
if (Env.iOS) {
|
||||
selectionChangeNodeChanged();
|
||||
restoreFocusOnKeyDown();
|
||||
} else {
|
||||
selectAll();
|
||||
}
|
||||
}
|
||||
|
||||
// IE
|
||||
if (isIE && Env.ie < 11) {
|
||||
removeHrOnBackspace();
|
||||
ensureBodyHasRoleApplication();
|
||||
addNewLinesBeforeBrInPre();
|
||||
removePreSerializedStylesWhenSelectingControls();
|
||||
deleteControlItemOnBackSpace();
|
||||
renderEmptyBlocksFix();
|
||||
keepNoScriptContents();
|
||||
fixCaretSelectionOfDocumentElementOnIe();
|
||||
}
|
||||
|
||||
if (Env.ie >= 11) {
|
||||
bodyHeight();
|
||||
}
|
||||
|
||||
// Gecko
|
||||
if (isGecko) {
|
||||
removeHrOnBackspace();
|
||||
focusBody();
|
||||
removeStylesWhenDeletingAcrossBlockElements();
|
||||
setGeckoEditingOptions();
|
||||
addBrAfterLastLinks();
|
||||
removeGhostSelection();
|
||||
showBrokenImageIcon();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* Tools.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class contains various utlity functions. These are also exposed
|
||||
* directly on the tinymce namespace.
|
||||
*
|
||||
* @class tinymce.util.Tools
|
||||
*/
|
||||
define("tinymce/util/Tools", [], function() {
|
||||
/**
|
||||
* Removes whitespace from the beginning and end of a string.
|
||||
*
|
||||
* @method trim
|
||||
* @param {String} s String to remove whitespace from.
|
||||
* @return {String} New string with removed whitespace.
|
||||
*/
|
||||
var whiteSpaceRegExp = /^\s*|\s*$/g;
|
||||
var trim = function(str) {
|
||||
return (str === null || str === undefined) ? '' : ("" + str).replace(whiteSpaceRegExp, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true/false if the object is an array or not.
|
||||
*
|
||||
* @method isArray
|
||||
* @param {Object} obj Object to check.
|
||||
* @return {boolean} true/false state if the object is an array or not.
|
||||
*/
|
||||
var isArray = Array.isArray || function(obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Array]";
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a object is of a specific type for example an array.
|
||||
*
|
||||
* @method is
|
||||
* @param {Object} o Object to check type of.
|
||||
* @param {string} t Optional type to check for.
|
||||
* @return {Boolean} true/false if the object is of the specified type.
|
||||
*/
|
||||
function is(o, t) {
|
||||
if (!t) {
|
||||
return o !== undefined;
|
||||
}
|
||||
|
||||
if (t == 'array' && isArray(o)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return typeof(o) == t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specified object into a real JavaScript array.
|
||||
*
|
||||
* @method toArray
|
||||
* @param {Object} obj Object to convert into array.
|
||||
* @return {Array} Array object based in input.
|
||||
*/
|
||||
function toArray(obj) {
|
||||
var array = [], i, l;
|
||||
|
||||
for (i = 0, l = obj.length; i < l; i++) {
|
||||
array[i] = obj[i];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a name/object map out of an array with names.
|
||||
*
|
||||
* @method makeMap
|
||||
* @param {Array/String} items Items to make map out of.
|
||||
* @param {String} delim Optional delimiter to split string by.
|
||||
* @param {Object} map Optional map to add items to.
|
||||
* @return {Object} Name/value map of items.
|
||||
*/
|
||||
function makeMap(items, delim, map) {
|
||||
var i;
|
||||
|
||||
items = items || [];
|
||||
delim = delim || ',';
|
||||
|
||||
if (typeof(items) == "string") {
|
||||
items = items.split(delim);
|
||||
}
|
||||
|
||||
map = map || {};
|
||||
|
||||
i = items.length;
|
||||
while (i--) {
|
||||
map[items[i]] = {};
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an iteration of all items in a collection such as an object or array. This method will execure the
|
||||
* callback function for each item in the collection, if the callback returns false the iteration will terminate.
|
||||
* The callback has the following format: cb(value, key_or_index).
|
||||
*
|
||||
* @method each
|
||||
* @param {Object} o Collection to iterate.
|
||||
* @param {function} cb Callback function to execute for each item.
|
||||
* @param {Object} s Optional scope to execute the callback in.
|
||||
* @example
|
||||
* // Iterate an array
|
||||
* tinymce.each([1,2,3], function(v, i) {
|
||||
* console.debug("Value: " + v + ", Index: " + i);
|
||||
* });
|
||||
*
|
||||
* // Iterate an object
|
||||
* tinymce.each({a: 1, b: 2, c: 3], function(v, k) {
|
||||
* console.debug("Value: " + v + ", Key: " + k);
|
||||
* });
|
||||
*/
|
||||
function each(o, cb, s) {
|
||||
var n, l;
|
||||
|
||||
if (!o) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
s = s || o;
|
||||
|
||||
if (o.length !== undefined) {
|
||||
// Indexed arrays, needed for Safari
|
||||
for (n=0, l = o.length; n < l; n++) {
|
||||
if (cb.call(s, o[n], n, o) === false) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Hashtables
|
||||
for (n in o) {
|
||||
if (o.hasOwnProperty(n)) {
|
||||
if (cb.call(s, o[n], n, o) === false) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new array by the return value of each iteration function call. This enables you to convert
|
||||
* one array list into another.
|
||||
*
|
||||
* @method map
|
||||
* @param {Array} a Array of items to iterate.
|
||||
* @param {function} f Function to call for each item. It's return value will be the new value.
|
||||
* @return {Array} Array with new values based on function return values.
|
||||
*/
|
||||
function map(a, f) {
|
||||
var o = [];
|
||||
|
||||
each(a, function(v) {
|
||||
o.push(f(v));
|
||||
});
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out items from the input array by calling the specified function for each item.
|
||||
* If the function returns false the item will be excluded if it returns true it will be included.
|
||||
*
|
||||
* @method grep
|
||||
* @param {Array} a Array of items to loop though.
|
||||
* @param {function} f Function to call for each item. Include/exclude depends on it's return value.
|
||||
* @return {Array} New array with values imported and filtered based in input.
|
||||
* @example
|
||||
* // Filter out some items, this will return an array with 4 and 5
|
||||
* var items = tinymce.grep([1,2,3,4,5], function(v) {return v > 3;});
|
||||
*/
|
||||
function grep(a, f) {
|
||||
var o = [];
|
||||
|
||||
each(a, function(v) {
|
||||
if (!f || f(v)) {
|
||||
o.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a class, subclass or static singleton.
|
||||
* More details on this method can be found in the Wiki.
|
||||
*
|
||||
* @method create
|
||||
* @param {String} s Class name, inheritage and prefix.
|
||||
* @param {Object} p Collection of methods to add to the class.
|
||||
* @param {Object} root Optional root object defaults to the global window object.
|
||||
* @example
|
||||
* // Creates a basic class
|
||||
* tinymce.create('tinymce.somepackage.SomeClass', {
|
||||
* SomeClass: function() {
|
||||
* // Class constructor
|
||||
* },
|
||||
*
|
||||
* method: function() {
|
||||
* // Some method
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Creates a basic subclass class
|
||||
* tinymce.create('tinymce.somepackage.SomeSubClass:tinymce.somepackage.SomeClass', {
|
||||
* SomeSubClass: function() {
|
||||
* // Class constructor
|
||||
* this.parent(); // Call parent constructor
|
||||
* },
|
||||
*
|
||||
* method: function() {
|
||||
* // Some method
|
||||
* this.parent(); // Call parent method
|
||||
* },
|
||||
*
|
||||
* 'static': {
|
||||
* staticMethod: function() {
|
||||
* // Static method
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Creates a singleton/static class
|
||||
* tinymce.create('static tinymce.somepackage.SomeSingletonClass', {
|
||||
* method: function() {
|
||||
* // Some method
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
function create(s, p, root) {
|
||||
var t = this, sp, ns, cn, scn, c, de = 0;
|
||||
|
||||
// Parse : <prefix> <class>:<super class>
|
||||
s = /^((static) )?([\w.]+)(:([\w.]+))?/.exec(s);
|
||||
cn = s[3].match(/(^|\.)(\w+)$/i)[2]; // Class name
|
||||
|
||||
// Create namespace for new class
|
||||
ns = t.createNS(s[3].replace(/\.\w+$/, ''), root);
|
||||
|
||||
// Class already exists
|
||||
if (ns[cn]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make pure static class
|
||||
if (s[2] == 'static') {
|
||||
ns[cn] = p;
|
||||
|
||||
if (this.onCreate) {
|
||||
this.onCreate(s[2], s[3], ns[cn]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create default constructor
|
||||
if (!p[cn]) {
|
||||
p[cn] = function() {};
|
||||
de = 1;
|
||||
}
|
||||
|
||||
// Add constructor and methods
|
||||
ns[cn] = p[cn];
|
||||
t.extend(ns[cn].prototype, p);
|
||||
|
||||
// Extend
|
||||
if (s[5]) {
|
||||
sp = t.resolve(s[5]).prototype;
|
||||
scn = s[5].match(/\.(\w+)$/i)[1]; // Class name
|
||||
|
||||
// Extend constructor
|
||||
c = ns[cn];
|
||||
if (de) {
|
||||
// Add passthrough constructor
|
||||
ns[cn] = function() {
|
||||
return sp[scn].apply(this, arguments);
|
||||
};
|
||||
} else {
|
||||
// Add inherit constructor
|
||||
ns[cn] = function() {
|
||||
this.parent = sp[scn];
|
||||
return c.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
ns[cn].prototype[cn] = ns[cn];
|
||||
|
||||
// Add super methods
|
||||
t.each(sp, function(f, n) {
|
||||
ns[cn].prototype[n] = sp[n];
|
||||
});
|
||||
|
||||
// Add overridden methods
|
||||
t.each(p, function(f, n) {
|
||||
// Extend methods if needed
|
||||
if (sp[n]) {
|
||||
ns[cn].prototype[n] = function() {
|
||||
this.parent = sp[n];
|
||||
return f.apply(this, arguments);
|
||||
};
|
||||
} else {
|
||||
if (n != cn) {
|
||||
ns[cn].prototype[n] = f;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add static methods
|
||||
/*jshint sub:true*/
|
||||
t.each(p['static'], function(f, n) {
|
||||
ns[cn][n] = f;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of a value in an array, this method will return -1 if the item wasn't found.
|
||||
*
|
||||
* @method inArray
|
||||
* @param {Array} a Array/Object to search for value in.
|
||||
* @param {Object} v Value to check for inside the array.
|
||||
* @return {Number/String} Index of item inside the array inside an object. Or -1 if it wasn't found.
|
||||
* @example
|
||||
* // Get index of value in array this will alert 1 since 2 is at that index
|
||||
* alert(tinymce.inArray([1,2,3], 2));
|
||||
*/
|
||||
function inArray(a, v) {
|
||||
var i, l;
|
||||
|
||||
if (a) {
|
||||
for (i = 0, l = a.length; i < l; i++) {
|
||||
if (a[i] === v) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function extend(obj, ext) {
|
||||
var i, l, name, args = arguments, value;
|
||||
|
||||
for (i = 1, l = args.length; i < l; i++) {
|
||||
ext = args[i];
|
||||
for (name in ext) {
|
||||
if (ext.hasOwnProperty(name)) {
|
||||
value = ext[name];
|
||||
|
||||
if (value !== undefined) {
|
||||
obj[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed the specified function for each item in a object tree.
|
||||
*
|
||||
* @method walk
|
||||
* @param {Object} o Object tree to walk though.
|
||||
* @param {function} f Function to call for each item.
|
||||
* @param {String} n Optional name of collection inside the objects to walk for example childNodes.
|
||||
* @param {String} s Optional scope to execute the function in.
|
||||
*/
|
||||
function walk(o, f, n, s) {
|
||||
s = s || this;
|
||||
|
||||
if (o) {
|
||||
if (n) {
|
||||
o = o[n];
|
||||
}
|
||||
|
||||
each(o, function(o, i) {
|
||||
if (f.call(s, o, i, n) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
walk(o, f, n, s);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a namespace on a specific object.
|
||||
*
|
||||
* @method createNS
|
||||
* @param {String} n Namespace to create for example a.b.c.d.
|
||||
* @param {Object} o Optional object to add namespace to, defaults to window.
|
||||
* @return {Object} New namespace object the last item in path.
|
||||
* @example
|
||||
* // Create some namespace
|
||||
* tinymce.createNS('tinymce.somepackage.subpackage');
|
||||
*
|
||||
* // Add a singleton
|
||||
* var tinymce.somepackage.subpackage.SomeSingleton = {
|
||||
* method: function() {
|
||||
* // Some method
|
||||
* }
|
||||
* };
|
||||
*/
|
||||
function createNS(n, o) {
|
||||
var i, v;
|
||||
|
||||
o = o || window;
|
||||
|
||||
n = n.split('.');
|
||||
for (i=0; i<n.length; i++) {
|
||||
v = n[i];
|
||||
|
||||
if (!o[v]) {
|
||||
o[v] = {};
|
||||
}
|
||||
|
||||
o = o[v];
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a string and returns the object from a specific structure.
|
||||
*
|
||||
* @method resolve
|
||||
* @param {String} n Path to resolve for example a.b.c.d.
|
||||
* @param {Object} o Optional object to search though, defaults to window.
|
||||
* @return {Object} Last object in path or null if it couldn't be resolved.
|
||||
* @example
|
||||
* // Resolve a path into an object reference
|
||||
* var obj = tinymce.resolve('a.b.c.d');
|
||||
*/
|
||||
function resolve(n, o) {
|
||||
var i, l;
|
||||
|
||||
o = o || window;
|
||||
|
||||
n = n.split('.');
|
||||
for (i = 0, l = n.length; i < l; i++) {
|
||||
o = o[n[i]];
|
||||
|
||||
if (!o) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string but removes the whitespace before and after each value.
|
||||
*
|
||||
* @method explode
|
||||
* @param {string} s String to split.
|
||||
* @param {string} d Delimiter to split by.
|
||||
* @example
|
||||
* // Split a string into an array with a,b,c
|
||||
* var arr = tinymce.explode('a, b, c');
|
||||
*/
|
||||
function explode(s, d) {
|
||||
if (!s || is(s, 'array')) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return map(s.split(d || ','), trim);
|
||||
}
|
||||
|
||||
return {
|
||||
trim: trim,
|
||||
isArray: isArray,
|
||||
is: is,
|
||||
toArray: toArray,
|
||||
makeMap: makeMap,
|
||||
each: each,
|
||||
map: map,
|
||||
grep: grep,
|
||||
inArray: inArray,
|
||||
extend: extend,
|
||||
create: create,
|
||||
walk: walk,
|
||||
createNS: createNS,
|
||||
resolve: resolve,
|
||||
explode: explode
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* URI.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class handles parsing, modification and serialization of URI/URL strings.
|
||||
* @class tinymce.util.URI
|
||||
*/
|
||||
define("tinymce/util/URI", [
|
||||
"tinymce/util/Tools"
|
||||
], function(Tools) {
|
||||
var each = Tools.each, trim = Tools.trim;
|
||||
|
||||
/**
|
||||
* 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, base_url;
|
||||
|
||||
// Trim whitespace
|
||||
url = trim(url);
|
||||
|
||||
// Default settings
|
||||
settings = self.settings = settings || {};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Absolute path with no host, fake host and protocol
|
||||
if (url.indexOf('/') === 0 && url.indexOf('//') !== 0) {
|
||||
url = (settings.base_uri ? settings.base_uri.protocol || 'http' : 'http') + '://mce_host' + url;
|
||||
}
|
||||
|
||||
// Relative path http:// or protocol relative //path
|
||||
if (!/^[\w\-]*:?\/\//.test(url)) {
|
||||
base_url = settings.base_uri ? settings.base_uri.path : new URI(location.href).directory;
|
||||
url = ((settings.base_uri && settings.base_uri.protocol) || 'http') + '://mce_host' + self.toAbsPath(base_url, url);
|
||||
}
|
||||
|
||||
// Parse URL (Credits goes to Steave, http://blog.stevenlevithan.com/archives/parseuri)
|
||||
url = url.replace(/@@/g, '(mce_at)'); // Zope 3 workaround, they use @@something
|
||||
|
||||
/*jshint maxlen: 255 */
|
||||
url = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(url);
|
||||
|
||||
each(["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], function(v, i) {
|
||||
var part = url[i];
|
||||
|
||||
// Zope 3 workaround, they use @@something
|
||||
if (part) {
|
||||
part = part.replace(/\(mce_at\)/g, '@@');
|
||||
}
|
||||
|
||||
self[v] = part;
|
||||
});
|
||||
|
||||
baseUri = settings.base_uri;
|
||||
if (baseUri) {
|
||||
if (!self.protocol) {
|
||||
self.protocol = baseUri.protocol;
|
||||
}
|
||||
|
||||
if (!self.userInfo) {
|
||||
self.userInfo = baseUri.userInfo;
|
||||
}
|
||||
|
||||
if (!self.port && self.host === 'mce_host') {
|
||||
self.port = baseUri.port;
|
||||
}
|
||||
|
||||
if (!self.host || self.host === 'mce_host') {
|
||||
self.host = baseUri.host;
|
||||
}
|
||||
|
||||
self.source = '';
|
||||
}
|
||||
|
||||
//t.path = t.path || '/';
|
||||
}
|
||||
|
||||
URI.prototype = {
|
||||
/**
|
||||
* Sets the internal path part of the URI.
|
||||
*
|
||||
* @method setPath
|
||||
* @param {string} path Path string to set.
|
||||
*/
|
||||
setPath: function(path) {
|
||||
var self = this;
|
||||
|
||||
path = /^(.*?)\/?(\w+)?$/.exec(path);
|
||||
|
||||
// Update path parts
|
||||
self.path = path[0];
|
||||
self.directory = path[1];
|
||||
self.file = path[2];
|
||||
|
||||
// Rebuild source
|
||||
self.source = '';
|
||||
self.getURI();
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the specified URI into a relative URI based on the current URI instance location.
|
||||
*
|
||||
* @method toRelative
|
||||
* @param {String} uri URI to convert into a relative path/URI.
|
||||
* @return {String} Relative URI from the point specified in the current URI instance.
|
||||
* @example
|
||||
* // Converts an absolute URL to an relative URL url will be somedir/somefile.htm
|
||||
* var url = new tinymce.util.URI('http://www.site.com/dir/').toRelative('http://www.site.com/dir/somedir/somefile.htm');
|
||||
*/
|
||||
toRelative: function(uri) {
|
||||
var self = this, output;
|
||||
|
||||
if (uri === "./") {
|
||||
return uri;
|
||||
}
|
||||
|
||||
uri = new URI(uri, {base_uri: self});
|
||||
|
||||
// Not on same domain/port or protocol
|
||||
if ((uri.host != 'mce_host' && self.host != uri.host && uri.host) || self.port != uri.port || self.protocol != uri.protocol) {
|
||||
return uri.getURI();
|
||||
}
|
||||
|
||||
var tu = self.getURI(), uu = uri.getURI();
|
||||
|
||||
// Allow usage of the base_uri when relative_urls = true
|
||||
if(tu == uu || (tu.charAt(tu.length - 1) == "/" && tu.substr(0, tu.length - 1) == uu)) {
|
||||
return tu;
|
||||
}
|
||||
|
||||
output = self.toRelPath(self.path, uri.path);
|
||||
|
||||
// Add query
|
||||
if (uri.query) {
|
||||
output += '?' + uri.query;
|
||||
}
|
||||
|
||||
// Add anchor
|
||||
if (uri.anchor) {
|
||||
output += '#' + uri.anchor;
|
||||
}
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the specified URI into a absolute URI based on the current URI instance location.
|
||||
*
|
||||
* @method toAbsolute
|
||||
* @param {String} uri URI to convert into a relative path/URI.
|
||||
* @param {Boolean} noHost No host and protocol prefix.
|
||||
* @return {String} Absolute URI from the point specified in the current URI instance.
|
||||
* @example
|
||||
* // Converts an relative URL to an absolute URL url will be http://www.site.com/dir/somedir/somefile.htm
|
||||
* var url = new tinymce.util.URI('http://www.site.com/dir/').toAbsolute('somedir/somefile.htm');
|
||||
*/
|
||||
toAbsolute: function(uri, noHost) {
|
||||
uri = new URI(uri, {base_uri: this});
|
||||
|
||||
return uri.getURI(this.host == uri.host && this.protocol == uri.protocol ? noHost : 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a absolute path into a relative path.
|
||||
*
|
||||
* @method toRelPath
|
||||
* @param {String} base Base point to convert the path from.
|
||||
* @param {String} path Absolute path to convert into a relative path.
|
||||
*/
|
||||
toRelPath: function(base, path) {
|
||||
var items, breakPoint = 0, out = '', i, l;
|
||||
|
||||
// Split the paths
|
||||
base = base.substring(0, base.lastIndexOf('/'));
|
||||
base = base.split('/');
|
||||
items = path.split('/');
|
||||
|
||||
if (base.length >= items.length) {
|
||||
for (i = 0, l = base.length; i < l; i++) {
|
||||
if (i >= items.length || base[i] != items[i]) {
|
||||
breakPoint = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (base.length < items.length) {
|
||||
for (i = 0, l = items.length; i < l; i++) {
|
||||
if (i >= base.length || base[i] != items[i]) {
|
||||
breakPoint = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (breakPoint === 1) {
|
||||
return path;
|
||||
}
|
||||
|
||||
for (i = 0, l = base.length - (breakPoint - 1); i < l; i++) {
|
||||
out += "../";
|
||||
}
|
||||
|
||||
for (i = breakPoint - 1, l = items.length; i < l; i++) {
|
||||
if (i != breakPoint - 1) {
|
||||
out += "/" + items[i];
|
||||
} else {
|
||||
out += items[i];
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts a relative path into a absolute path.
|
||||
*
|
||||
* @method toAbsPath
|
||||
* @param {String} base Base point to convert the path from.
|
||||
* @param {String} path Relative path to convert into an absolute path.
|
||||
*/
|
||||
toAbsPath: function(base, path) {
|
||||
var i, nb = 0, o = [], tr, outPath;
|
||||
|
||||
// Split paths
|
||||
tr = /\/$/.test(path) ? '/' : '';
|
||||
base = base.split('/');
|
||||
path = path.split('/');
|
||||
|
||||
// Remove empty chunks
|
||||
each(base, function(k) {
|
||||
if (k) {
|
||||
o.push(k);
|
||||
}
|
||||
});
|
||||
|
||||
base = o;
|
||||
|
||||
// Merge relURLParts chunks
|
||||
for (i = path.length - 1, o = []; i >= 0; i--) {
|
||||
// Ignore empty or .
|
||||
if (path[i].length === 0 || path[i] === ".") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is parent
|
||||
if (path[i] === '..') {
|
||||
nb++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move up
|
||||
if (nb > 0) {
|
||||
nb--;
|
||||
continue;
|
||||
}
|
||||
|
||||
o.push(path[i]);
|
||||
}
|
||||
|
||||
i = base.length - nb;
|
||||
|
||||
// If /a/b/c or /
|
||||
if (i <= 0) {
|
||||
outPath = o.reverse().join('/');
|
||||
} else {
|
||||
outPath = base.slice(0, i).join('/') + '/' + o.reverse().join('/');
|
||||
}
|
||||
|
||||
// Add front / if it's needed
|
||||
if (outPath.indexOf('/') !== 0) {
|
||||
outPath = '/' + outPath;
|
||||
}
|
||||
|
||||
// Add traling / if it's needed
|
||||
if (tr && outPath.lastIndexOf('/') !== outPath.length - 1) {
|
||||
outPath += tr;
|
||||
}
|
||||
|
||||
return outPath;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the full URI of the internal structure.
|
||||
*
|
||||
* @method getURI
|
||||
* @param {Boolean} noProtoHost Optional no host and protocol part. Defaults to false.
|
||||
*/
|
||||
getURI: function(noProtoHost) {
|
||||
var s, self = this;
|
||||
|
||||
// Rebuild source
|
||||
if (!self.source || noProtoHost) {
|
||||
s = '';
|
||||
|
||||
if (!noProtoHost) {
|
||||
if (self.protocol) {
|
||||
s += self.protocol + '://';
|
||||
}
|
||||
|
||||
if (self.userInfo) {
|
||||
s += self.userInfo + '@';
|
||||
}
|
||||
|
||||
if (self.host) {
|
||||
s += self.host;
|
||||
}
|
||||
|
||||
if (self.port) {
|
||||
s += ':' + self.port;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.path) {
|
||||
s += self.path;
|
||||
}
|
||||
|
||||
if (self.query) {
|
||||
s += '?' + self.query;
|
||||
}
|
||||
|
||||
if (self.anchor) {
|
||||
s += '#' + self.anchor;
|
||||
}
|
||||
|
||||
self.source = s;
|
||||
}
|
||||
|
||||
return self.source;
|
||||
}
|
||||
};
|
||||
|
||||
return URI;
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* VK.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file exposes a set of the common KeyCodes for use. Please grow it as needed.
|
||||
*/
|
||||
define("tinymce/util/VK", [
|
||||
"tinymce/Env"
|
||||
], function(Env) {
|
||||
return {
|
||||
BACKSPACE: 8,
|
||||
DELETE: 46,
|
||||
DOWN: 40,
|
||||
ENTER: 13,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
SPACEBAR: 32,
|
||||
TAB: 9,
|
||||
UP: 38,
|
||||
|
||||
modifierPressed: function(e) {
|
||||
return e.shiftKey || e.ctrlKey || e.altKey;
|
||||
},
|
||||
|
||||
metaKeyPressed: function(e) {
|
||||
// Check if ctrl or meta key is pressed also check if alt is false for Polish users
|
||||
return (Env.mac ? e.ctrlKey || e.metaKey : e.ctrlKey) && !e.altKey;
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* XHR.js
|
||||
*
|
||||
* Copyright, Moxiecode Systems AB
|
||||
* Released under LGPL License.
|
||||
*
|
||||
* License: http://www.tinymce.com/license
|
||||
* Contributing: http://www.tinymce.com/contributing
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class enables you to send XMLHTTPRequests cross browser.
|
||||
* @class tinymce.util.XHR
|
||||
* @static
|
||||
* @example
|
||||
* // Sends a low level Ajax request
|
||||
* tinymce.util.XHR.send({
|
||||
* url: 'someurl',
|
||||
* success: function(text) {
|
||||
* console.debug(text);
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
define("tinymce/util/XHR", [], function() {
|
||||
return {
|
||||
/**
|
||||
* Sends a XMLHTTPRequest.
|
||||
* Consult the Wiki for details on what settings this method takes.
|
||||
*
|
||||
* @method send
|
||||
* @param {Object} settings Object will target URL, callbacks and other info needed to make the request.
|
||||
*/
|
||||
send: function(settings) {
|
||||
var xhr, count = 0;
|
||||
|
||||
function ready() {
|
||||
if (!settings.async || xhr.readyState == 4 || count++ > 10000) {
|
||||
if (settings.success && count < 10000 && xhr.status == 200) {
|
||||
settings.success.call(settings.success_scope, '' + xhr.responseText, xhr, settings);
|
||||
} else if (settings.error) {
|
||||
settings.error.call(settings.error_scope, count > 10000 ? 'TIMED_OUT' : 'GENERAL', xhr, settings);
|
||||
}
|
||||
|
||||
xhr = null;
|
||||
} else {
|
||||
setTimeout(ready, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Default settings
|
||||
settings.scope = settings.scope || this;
|
||||
settings.success_scope = settings.success_scope || settings.scope;
|
||||
settings.error_scope = settings.error_scope || settings.scope;
|
||||
settings.async = settings.async === false ? false : true;
|
||||
settings.data = settings.data || '';
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
|
||||
if (xhr) {
|
||||
if (xhr.overrideMimeType) {
|
||||
xhr.overrideMimeType(settings.content_type);
|
||||
}
|
||||
|
||||
xhr.open(settings.type || (settings.data ? 'POST' : 'GET'), settings.url, settings.async);
|
||||
|
||||
if (settings.content_type) {
|
||||
xhr.setRequestHeader('Content-Type', settings.content_type);
|
||||
}
|
||||
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
|
||||
xhr.send(settings.data);
|
||||
|
||||
// Syncronous request
|
||||
if (!settings.async) {
|
||||
return ready();
|
||||
}
|
||||
|
||||
// Wait for response, onReadyStateChange can not be used since it leaks memory in IE
|
||||
setTimeout(ready, 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user