import { Arr, Obj, Type } from '@ephox/katamari';
import ScriptLoader from './dom/ScriptLoader';
import Editor from './Editor';
import I18n from './util/I18n';
/**
* TinyMCE theme pseudo class. Allows for a custom theme to be used with TinyMCE when registered using the ThemeManager.
*
* @summary This is a pseudo class that describes how to create a custom theme for TinyMCE.
*
* See AddOnManager for more information about the methods available on the ThemeManager instance.
*
* Warning: Much of TinyMCE's functionality is provided by the default Silver theme.
* Creating a custom theme may require the re-implementation of this functionality.
* To change TinyMCE's appearance, Tiny recommends changing the Skin instead.
*
* @class tinymce.Theme
* @example
* tinymce.ThemeManager.add('MyTheme', (editor) => {
* // Setup up custom UI elements in the dom
* const div = document.createElement('div');
* const iframe = document.createElement('iframe');
* document.body.appendChild(div);
* document.body.appendChild(iframe);
*
* // Themes should fire the SkinLoaded event once the UI has been created and all StyleSheets have been loaded.
* editor.on('init', () => {
* editor.fire('SkinLoaded');
* });
*
* // Themes must return a renderUI function that returns the editorContainer. If the editor is not running in inline mode, an iframeContainer should also be returned.
* const renderUI = () => {
* return {
* editorContainer: div,
* iframeContainer: iframe
* };
* };
*
* // Return the renderUI function
* return {
* renderUI: renderUI
* };
* });
*/
/**
* TinyMCE plugin psuedo class. Allows for custom plugins to be added to TinyMCE when registered using the PluginManager.
*
* @summary This is a pseudo class that describes how to create a custom plugin for TinyMCE.
*
* A custom plugin registered using PluginManager.add should either not return any value or return plugin metadata as an object that contains the plugin's name and a URL.
* The URL is intended to link to help documentation.
*
* See AddOnManager for more information about the methods available on the PluginManager instance.
*
* @class tinymce.Plugin
* @example
* tinymce.PluginManager.add('MyPlugin', (editor, url) => {
* // Register a toolbar button that triggers an alert when clicked
* // To show this button in the editor, include it in the toolbar setting
* editor.ui.registry.addButton('myCustomToolbarButton', {
* text: 'My custom button',
* onAction: () => {
* alert('Button clicked!');
* }
* });
*
* // Register a menu item that triggers an alert when clicked
* // To show this menu item in the editor, include it in the menu setting
* editor.ui.registry.addMenuItem('myCustomMenuItem', {
* text: 'My custom menu item',
* onAction: () => {
* alert('Menu item clicked');
* }
* });
*
* // Either return plugin metadata or do not return
* return {
* name: 'MyPlugin',
* url: 'https://mydocs.com/myplugin'
* };
* });
*/
/**
* This class handles the loading of add-ons and their language packs.
* ThemeManager and PluginManager are instances of AddOnManager, and manage themes and plugins.
*
* @class tinymce.AddOnManager
*/
export interface UrlObject {
prefix: string;
resource: string;
suffix: string;
}
type WaitState = 'added' | 'loaded';
export type AddOnConstructor = (editor: Editor, url: string) => T;
interface AddOnManager {
items: AddOnConstructor[];
urls: Record;
lookup: Record }>;
get: (name: string) => AddOnConstructor | undefined;
requireLangPack: (name: string, languages?: string) => void;
add: (id: string, addOn: AddOnConstructor) => AddOnConstructor;
remove: (name: string) => void;
createUrl: (baseUrl: UrlObject, dep: string | UrlObject) => UrlObject;
load: (name: string, addOnUrl: string | UrlObject) => Promise;
waitFor: (name: string, state?: WaitState) => Promise;
}
const AddOnManager = (): AddOnManager => {
const items: AddOnConstructor[] = [];
const urls: Record = {};
const lookup: Record }> = {};
const _listeners: { name: string; state: WaitState; resolve: () => void }[] = [];
const runListeners = (name: string, state: WaitState) => {
const matchedListeners = Arr.filter(_listeners, (listener) => listener.name === name && listener.state === state);
Arr.each(matchedListeners, (listener) => listener.resolve());
};
const isLoaded = (name: string) => Obj.has(urls, name);
const isAdded = (name: string) => Obj.has(lookup, name);
const get = (name: string) => {
if (lookup[name]) {
return lookup[name].instance;
}
return undefined;
};
const loadLanguagePack = (name: string, languages?: string): void => {
const language = I18n.getCode();
const wrappedLanguages = ',' + (languages || '') + ',';
if (!language || languages && wrappedLanguages.indexOf(',' + language + ',') === -1) {
return;
}
ScriptLoader.ScriptLoader.add(urls[name] + '/langs/' + language + '.js');
};
const requireLangPack = (name: string, languages?: string) => {
if (AddOnManager.languageLoad !== false) {
if (isLoaded(name)) {
loadLanguagePack(name, languages);
} else {
waitFor(name, 'loaded').then(() => loadLanguagePack(name, languages));
}
}
};
const add = (id: string, addOn: AddOnConstructor) => {
items.push(addOn);
lookup[id] = { instance: addOn };
runListeners(id, 'added');
return addOn;
};
const remove = (name: string) => {
delete urls[name];
delete lookup[name];
};
const createUrl = (baseUrl: string | UrlObject, dep: string | UrlObject): UrlObject => {
if (Type.isString(dep)) {
return Type.isString(baseUrl) ?
{ prefix: '', resource: dep, suffix: '' } :
{ prefix: baseUrl.prefix, resource: dep, suffix: baseUrl.suffix };
} else {
return dep;
}
};
const load = (name: string, addOnUrl: string | UrlObject): Promise => {
if (urls[name]) {
return Promise.resolve();
}
let urlString = Type.isString(addOnUrl) ? addOnUrl : addOnUrl.prefix + addOnUrl.resource + addOnUrl.suffix;
if (urlString.indexOf('/') !== 0 && urlString.indexOf('://') === -1) {
urlString = AddOnManager.baseURL + '/' + urlString;
}
urls[name] = urlString.substring(0, urlString.lastIndexOf('/'));
const done = () => {
runListeners(name, 'loaded');
return Promise.resolve();
};
if (lookup[name]) {
return done();
} else {
return ScriptLoader.ScriptLoader.add(urlString).then(done);
}
};
const waitFor = (name: string, state: 'added' | 'loaded' = 'added'): Promise => {
if (state === 'added' && isAdded(name)) {
return Promise.resolve();
} else if (state === 'loaded' && isLoaded(name)) {
return Promise.resolve();
} else {
return new Promise((resolve) => {
_listeners.push({ name, state, resolve });
});
}
};
return {
items,
urls,
lookup,
/**
* 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,
/**
* Loads a language pack for the specified add-on.
*
* @method requireLangPack
* @param {String} name Short name of the add-on.
* @param {String} languages Optional comma or space separated list of languages to check if it matches the name.
*/
requireLangPack,
/**
* 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
* const TestPlugin = (ed, url) => {
* ed.on('click', (e) => {
* ed.windowManager.alert('Hello World!');
* });
* };
*
* // Register plugin using the add method
* tinymce.PluginManager.add('test', TestPlugin);
*
* // Initialize TinyMCE
* tinymce.init({
* ...
* plugins: '-test' // Init the plugin but don't try to load it
* });
*/
add,
remove,
createUrl,
/**
* Loads an add-on from a specific url.
*
* @method load
* @param {String} name Short name of the add-on that gets loaded.
* @param {String} addOnUrl URL to the add-on that will get loaded.
* @return {Promise} A promise that will resolve when the add-on is loaded successfully or reject if it failed to load.
* @example
* // Loads a plugin from an external URL
* tinymce.PluginManager.load('myplugin', '/some/dir/someplugin/plugin.js');
*
* // Initialize TinyMCE
* tinymce.init({
* ...
* plugins: '-myplugin' // Don't try to load it again
* });
*/
load,
waitFor
};
};
AddOnManager.languageLoad = true;
AddOnManager.baseURL = '';
AddOnManager.PluginManager = AddOnManager();
AddOnManager.ThemeManager = AddOnManager();
AddOnManager.ModelManager = AddOnManager();
export default AddOnManager;