| /**
 * EditorManager.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
/**
 * This class used as a factory for manager for tinymce.Editor instances.
 *
 * @example
 * tinymce.EditorManager.init({});
 *
 * @class tinymce.EditorManager
 * @mixes tinymce.util.Observable
 * @static
 */
define(
  'tinymce.core.EditorManager',
  [
    'ephox.katamari.api.Arr',
    'ephox.katamari.api.Type',
    'global!document',
    'global!window',
    'tinymce.core.AddOnManager',
    'tinymce.core.Editor',
    'tinymce.core.Env',
    'tinymce.core.ErrorReporter',
    'tinymce.core.dom.DOMUtils',
    'tinymce.core.dom.DomQuery',
    'tinymce.core.focus.FocusController',
    'tinymce.core.util.I18n',
    'tinymce.core.util.Observable',
    'tinymce.core.util.Promise',
    'tinymce.core.util.Tools',
    'tinymce.core.util.URI'
  ],
  function (Arr, Type, document, window, AddOnManager, Editor, Env, ErrorReporter, DOMUtils, DomQuery, FocusController, I18n, Observable, Promise, Tools, URI) {
    var DOM = DOMUtils.DOM;
    var explode = Tools.explode, each = Tools.each, extend = Tools.extend;
    var instanceCounter = 0, beforeUnloadDelegate, EditorManager, boundGlobalEvents = false;
    var legacyEditors = [], editors = [];
    var isValidLegacyKey = function (id) {
      // In theory we could filter out any editor id:s that clash
      // with array prototype items but that could break existing integrations
      return id !== 'length';
    };
    var globalEventDelegate = function (e) {
      each(EditorManager.get(), function (editor) {
        if (e.type === 'scroll') {
          editor.fire('ScrollWindow', e);
        } else {
          editor.fire('ResizeWindow', e);
        }
      });
    };
    var toggleGlobalEvents = function (state) {
      if (state !== boundGlobalEvents) {
        if (state) {
          DomQuery(window).on('resize scroll', globalEventDelegate);
        } else {
          DomQuery(window).off('resize scroll', globalEventDelegate);
        }
        boundGlobalEvents = state;
      }
    };
    var removeEditorFromList = function (targetEditor) {
      var oldEditors = editors;
      delete legacyEditors[targetEditor.id];
      for (var i = 0; i < legacyEditors.length; i++) {
        if (legacyEditors[i] === targetEditor) {
          legacyEditors.splice(i, 1);
          break;
        }
      }
      editors = Arr.filter(editors, function (editor) {
        return targetEditor !== editor;
      });
      // Select another editor since the active one was removed
      if (EditorManager.activeEditor === targetEditor) {
        EditorManager.activeEditor = editors.length > 0 ? editors[0] : null;
      }
      // Clear focusedEditor if necessary, so that we don't try to blur the destroyed editor
      if (EditorManager.focusedEditor === targetEditor) {
        EditorManager.focusedEditor = null;
      }
      return oldEditors.length !== editors.length;
    };
    var purgeDestroyedEditor = function (editor) {
      // User has manually destroyed the editor lets clean up the mess
      if (editor && editor.initialized && !(editor.getContainer() || editor.getBody()).parentNode) {
        removeEditorFromList(editor);
        editor.unbindAllNativeEvents();
        editor.destroy(true);
        editor.removed = true;
        editor = null;
      }
      return editor;
    };
    EditorManager = {
      defaultSettings: {},
      /**
       * Dom query instance.
       *
       * @property $
       * @type tinymce.dom.DomQuery
       */
      $: DomQuery,
      /**
       * 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. Deprecated use tinymce.get() instead.
       *
       * @property editors
       * @type Object
       */
      editors: legacyEditors,
      /**
       * 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,
      settings: {},
      setup: function () {
        var self = this, baseURL, documentBaseURL, suffix = "", preInit, src;
        // Get base URL for the current document
        documentBaseURL = URI.getDocumentBaseUrl(document.location);
        // Check if the URL is a document based format like: http://site/dir/file and file:///
        // leave other formats like applewebdata://... intact
        if (/^[^:]+:\/\/\/?[^\/]+\//.test(documentBaseURL)) {
          documentBaseURL = documentBaseURL.replace(/[\?#].*$/, '').replace(/[\/\\][^\/]+$/, '');
          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++) {
            src = scripts[i].src;
            // Script types supported:
            // tinymce.js tinymce.min.js tinymce.dev.js
            // tinymce.jquery.js tinymce.jquery.min.js tinymce.jquery.dev.js
            // tinymce.full.js tinymce.full.min.js tinymce.full.dev.js
            var srcScript = src.substring(src.lastIndexOf('/'));
            if (/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(src)) {
              if (srcScript.indexOf('.min') != -1) {
                suffix = '.min';
              }
              baseURL = src.substring(0, src.lastIndexOf('/'));
              break;
            }
          }
          // We didn't find any baseURL by looking at the script elements
          // Try to use the document.currentScript as a fallback
          if (!baseURL && document.currentScript) {
            src = document.currentScript.src;
            if (src.indexOf('.min') != -1) {
              suffix = '.min';
            }
            baseURL = src.substring(0, src.lastIndexOf('/'));
          }
        }
        /**
         * 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;
        FocusController.setup(self);
      },
      /**
       * Overrides the default settings for editor instances.
       *
       * @method overrideDefaults
       * @param {Object} defaultSettings Defaults settings object.
       */
      overrideDefaults: function (defaultSettings) {
        var baseUrl, suffix;
        baseUrl = defaultSettings.base_url;
        if (baseUrl) {
          this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, ''));
          this.baseURI = new URI(this.baseURL);
        }
        suffix = defaultSettings.suffix;
        if (defaultSettings.suffix) {
          this.suffix = suffix;
        }
        this.defaultSettings = defaultSettings;
        var pluginBaseUrls = defaultSettings.plugin_base_urls;
        for (var name in pluginBaseUrls) {
          AddOnManager.PluginManager.urls[name] = pluginBaseUrls[name];
        }
      },
      /**
       * 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.
       * @return {tinymce.util.Promise} Promise that gets resolved with an array of editors when all editor instances are initialized.
       * @example
       * // Initializes a editor using the longer method
       * tinymce.EditorManager.init({
       *    some_settings : 'some value'
       * });
       *
       * // Initializes a editor instance using the shorter version and with a promise
       * tinymce.init({
       *    some_settings : 'some value'
       * }).then(function(editors) {
       *    ...
       * });
       */
      init: function (settings) {
        var self = this, result, invalidInlineTargets;
        invalidInlineTargets = Tools.makeMap(
          'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' +
          'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu',
          ' '
        );
        var isInvalidInlineTarget = function (settings, elm) {
          return settings.inline && elm.tagName.toLowerCase() in invalidInlineTargets;
        };
        var createId = function (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;
        };
        var execCallback = function (name) {
          var callback = settings[name];
          if (!callback) {
            return;
          }
          return callback.apply(self, Array.prototype.slice.call(arguments, 2));
        };
        var hasClass = function (elm, className) {
          return className.constructor === RegExp ? className.test(elm.className) : DOM.hasClass(elm, className);
        };
        var findTargets = function (settings) {
          var l, targets = [];
          if (Env.ie && Env.ie < 11) {
            ErrorReporter.initError(
              'TinyMCE does not support the browser you are using. For a list of supported' +
              ' browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/'
            );
            return [];
          }
          if (settings.types) {
            each(settings.types, function (type) {
              targets = targets.concat(DOM.select(type.selector));
            });
            return targets;
          } else if (settings.selector) {
            return DOM.select(settings.selector);
          } else if (settings.target) {
            return [settings.target];
          }
          // Fallback to old setting
          switch (settings.mode) {
            case "exact":
              l = settings.elements || '';
              if (l.length > 0) {
                each(explode(l), function (id) {
                  var elm;
                  if ((elm = DOM.get(id))) {
                    targets.push(elm);
                  } else {
                    each(document.forms, function (f) {
                      each(f.elements, function (e) {
                        if (e.name === id) {
                          id = 'mce_editor_' + instanceCounter++;
                          DOM.setAttrib(e, 'id', id);
                          targets.push(e);
                        }
                      });
                    });
                  }
                });
              }
              break;
            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)) {
                  targets.push(elm);
                }
              });
              break;
          }
          return targets;
        };
        var provideResults = function (editors) {
          result = editors;
        };
        var initEditors = function () {
          var initCount = 0, editors = [], targets;
          var createEditor = function (id, settings, targetElm) {
            var editor = new Editor(id, settings, self);
            editors.push(editor);
            editor.on('init', function () {
              if (++initCount === targets.length) {
                provideResults(editors);
              }
            });
            editor.targetElm = editor.targetElm || targetElm;
            editor.render();
          };
          DOM.unbind(window, 'ready', initEditors);
          execCallback('onpageload');
          targets = DomQuery.unique(findTargets(settings));
          // TODO: Deprecate this one
          if (settings.types) {
            each(settings.types, function (type) {
              Tools.each(targets, function (elm) {
                if (DOM.is(elm, type.selector)) {
                  createEditor(createId(elm), extend({}, settings, type), elm);
                  return false;
                }
                return true;
              });
            });
            return;
          }
          Tools.each(targets, function (elm) {
            purgeDestroyedEditor(self.get(elm.id));
          });
          targets = Tools.grep(targets, function (elm) {
            return !self.get(elm.id);
          });
          if (targets.length === 0) {
            provideResults([]);
          } else {
            each(targets, function (elm) {
              if (isInvalidInlineTarget(settings, elm)) {
                ErrorReporter.initError('Could not initialize inline editor on invalid inline target element', elm);
              } else {
                createEditor(createId(elm), settings, elm);
              }
            });
          }
        };
        self.settings = settings;
        DOM.bind(window, 'ready', initEditors);
        return new Promise(function (resolve) {
          if (result) {
            resolve(result);
          } else {
            provideResults = function (editors) {
              resolve(editors);
            };
          }
        });
      },
      /**
       * Returns a editor instance by id.
       *
       * @method get
       * @param {String/Number} id Editor instance id or index to return.
       * @return {tinymce.Editor/Array} Editor instance to return or array of editor instances.
       * @example
       * // Adds an onclick event to an editor by id
       * tinymce.get('mytextbox').on('click', function(e) {
       *    ed.windowManager.alert('Hello world!');
       * });
       *
       * // Adds an onclick event to an editor by index
       * tinymce.get(0).on('click', function(e) {
       *    ed.windowManager.alert('Hello world!');
       * });
       *
       * // Adds an onclick event to an editor by id (longer version)
       * tinymce.EditorManager.get('mytextbox').on('click', function(e) {
       *    ed.windowManager.alert('Hello world!');
       * });
       */
      get: function (id) {
        if (arguments.length === 0) {
          return editors.slice(0);
        } else if (Type.isString(id)) {
          return Arr.find(editors, function (editor) {
            return editor.id === id;
          }).getOr(null);
        } else if (Type.isNumber(id)) {
          return editors[id] ? editors[id] : null;
        } else {
          return null;
        }
      },
      /**
       * Adds an editor instance to the editor collection. This will also set it as the active editor.
       *
       * @method add
       * @param {tinymce.Editor} editor Editor instance to add to the collection.
       * @return {tinymce.Editor} The same instance that got passed in.
       */
      add: function (editor) {
        var self = this, existingEditor;
        // Prevent existing editors from beeing added again this could happen
        // if a user calls createEditor then render or add multiple times.
        existingEditor = legacyEditors[editor.id];
        if (existingEditor === editor) {
          return editor;
        }
        if (self.get(editor.id) === null) {
          // Add to legacy editors array, this is what breaks in HTML5 where ID:s with numbers are valid
          // We can't get rid of this strange object and array at the same time since it seems to be used all over the web
          if (isValidLegacyKey(editor.id)) {
            legacyEditors[editor.id] = editor;
          }
          legacyEditors.push(editor);
          editors.push(editor);
        }
        toggleGlobalEvents(true);
        // Doesn't call setActive method since we don't want
        // to fire a bunch of activate/deactivate calls while initializing
        self.activeEditor = editor;
        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, editor;
        // Remove all editors
        if (!selector) {
          for (i = editors.length - 1; i >= 0; i--) {
            self.remove(editors[i]);
          }
          return;
        }
        // Remove editors by selector
        if (Type.isString(selector)) {
          selector = selector.selector || selector;
          each(DOM.select(selector), function (elm) {
            editor = self.get(elm.id);
            if (editor) {
              self.remove(editor);
            }
          });
          return;
        }
        // Remove specific editor
        editor = selector;
        // Not in the collection
        if (Type.isNull(self.get(editor.id))) {
          return null;
        }
        if (removeEditorFromList(editor)) {
          self.fire('RemoveEditor', { editor: editor });
        }
        if (editors.length === 0) {
          DOM.unbind(window, 'beforeunload', beforeUnloadDelegate);
        }
        editor.remove();
        toggleGlobalEvents(editors.length > 0);
        return editor;
      },
      /**
       * Executes a specific command on the currently active editor.
       *
       * @method execCommand
       * @param {String} cmd Command to perform for example Bold.
       * @param {Boolean} ui Optional boolean state if a UI should be presented for the command or not.
       * @param {String} value Optional value parameter like for example an URL to a link.
       * @return {Boolean} true/false if the command was executed or not.
       */
      execCommand: function (cmd, ui, value) {
        var self = this, editor = self.get(value);
        // 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(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);
      },
      /**
       * Sets the active editor instance and fires the deactivate/activate events.
       *
       * @method setActive
       * @param {tinymce.Editor} editor Editor instance to set as the active instance.
       */
      setActive: function (editor) {
        var activeEditor = this.activeEditor;
        if (this.activeEditor != editor) {
          if (activeEditor) {
            activeEditor.fire('deactivate', { relatedTarget: editor });
          }
          editor.fire('activate', { relatedTarget: activeEditor });
        }
        this.activeEditor = editor;
      }
    };
    extend(EditorManager, Observable);
    EditorManager.setup();
    return EditorManager;
  }
);
 |