| /**
 * Container.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */
/**
 * Container control. This is extended by all controls that can have
 * children such as panels etc. You can also use this class directly as an
 * generic container instance. The container doesn't have any specific role or style.
 *
 * @-x-less Container.less
 * @class tinymce.ui.Container
 * @extends tinymce.ui.Control
 */
define(
  'tinymce.ui.Container',
  [
    "tinymce.ui.Control",
    "tinymce.ui.Collection",
    "tinymce.ui.Selector",
    "tinymce.core.ui.Factory",
    "tinymce.ui.KeyboardNavigation",
    "tinymce.core.util.Tools",
    "tinymce.core.dom.DomQuery",
    "tinymce.ui.ClassList",
    "tinymce.ui.ReflowQueue"
  ],
  function (Control, Collection, Selector, Factory, KeyboardNavigation, Tools, $, ClassList, ReflowQueue) {
    "use strict";
    var selectorCache = {};
    return Control.extend({
      /**
       * Constructs a new control instance with the specified settings.
       *
       * @constructor
       * @param {Object} settings Name/value object with settings.
       * @setting {Array} items Items to add to container in JSON format or control instances.
       * @setting {String} layout Layout manager by name to use.
       * @setting {Object} defaults Default settings to apply to all items.
       */
      init: function (settings) {
        var self = this;
        self._super(settings);
        settings = self.settings;
        if (settings.fixed) {
          self.state.set('fixed', true);
        }
        self._items = new Collection();
        if (self.isRtl()) {
          self.classes.add('rtl');
        }
        self.bodyClasses = new ClassList(function () {
          if (self.state.get('rendered')) {
            self.getEl('body').className = this.toString();
          }
        });
        self.bodyClasses.prefix = self.classPrefix;
        self.classes.add('container');
        self.bodyClasses.add('container-body');
        if (settings.containerCls) {
          self.classes.add(settings.containerCls);
        }
        self._layout = Factory.create((settings.layout || '') + 'layout');
        if (self.settings.items) {
          self.add(self.settings.items);
        } else {
          self.add(self.render());
        }
        // TODO: Fix this!
        self._hasBody = true;
      },
      /**
       * Returns a collection of child items that the container currently have.
       *
       * @method items
       * @return {tinymce.ui.Collection} Control collection direct child controls.
       */
      items: function () {
        return this._items;
      },
      /**
       * Find child controls by selector.
       *
       * @method find
       * @param {String} selector Selector CSS pattern to find children by.
       * @return {tinymce.ui.Collection} Control collection with child controls.
       */
      find: function (selector) {
        selector = selectorCache[selector] = selectorCache[selector] || new Selector(selector);
        return selector.find(this);
      },
      /**
       * Adds one or many items to the current container. This will create instances of
       * the object representations if needed.
       *
       * @method add
       * @param {Array/Object/tinymce.ui.Control} items Array or item that will be added to the container.
       * @return {tinymce.ui.Collection} Current collection control.
       */
      add: function (items) {
        var self = this;
        self.items().add(self.create(items)).parent(self);
        return self;
      },
      /**
       * Focuses the current container instance. This will look
       * for the first control in the container and focus that.
       *
       * @method focus
       * @param {Boolean} keyboard Optional true/false if the focus was a keyboard focus or not.
       * @return {tinymce.ui.Collection} Current instance.
       */
      focus: function (keyboard) {
        var self = this, focusCtrl, keyboardNav, items;
        if (keyboard) {
          keyboardNav = self.keyboardNav || self.parents().eq(-1)[0].keyboardNav;
          if (keyboardNav) {
            keyboardNav.focusFirst(self);
            return;
          }
        }
        items = self.find('*');
        // TODO: Figure out a better way to auto focus alert dialog buttons
        if (self.statusbar) {
          items.add(self.statusbar.items());
        }
        items.each(function (ctrl) {
          if (ctrl.settings.autofocus) {
            focusCtrl = null;
            return false;
          }
          if (ctrl.canFocus) {
            focusCtrl = focusCtrl || ctrl;
          }
        });
        if (focusCtrl) {
          focusCtrl.focus();
        }
        return self;
      },
      /**
       * Replaces the specified child control with a new control.
       *
       * @method replace
       * @param {tinymce.ui.Control} oldItem Old item to be replaced.
       * @param {tinymce.ui.Control} newItem New item to be inserted.
       */
      replace: function (oldItem, newItem) {
        var ctrlElm, items = this.items(), i = items.length;
        // Replace the item in collection
        while (i--) {
          if (items[i] === oldItem) {
            items[i] = newItem;
            break;
          }
        }
        if (i >= 0) {
          // Remove new item from DOM
          ctrlElm = newItem.getEl();
          if (ctrlElm) {
            ctrlElm.parentNode.removeChild(ctrlElm);
          }
          // Remove old item from DOM
          ctrlElm = oldItem.getEl();
          if (ctrlElm) {
            ctrlElm.parentNode.removeChild(ctrlElm);
          }
        }
        // Adopt the item
        newItem.parent(this);
      },
      /**
       * Creates the specified items. If any of the items is plain JSON style objects
       * it will convert these into real tinymce.ui.Control instances.
       *
       * @method create
       * @param {Array} items Array of items to convert into control instances.
       * @return {Array} Array with control instances.
       */
      create: function (items) {
        var self = this, settings, ctrlItems = [];
        // Non array structure, then force it into an array
        if (!Tools.isArray(items)) {
          items = [items];
        }
        // Add default type to each child control
        Tools.each(items, function (item) {
          if (item) {
            // Construct item if needed
            if (!(item instanceof Control)) {
              // Name only then convert it to an object
              if (typeof item == "string") {
                item = { type: item };
              }
              // Create control instance based on input settings and default settings
              settings = Tools.extend({}, self.settings.defaults, item);
              item.type = settings.type = settings.type || item.type || self.settings.defaultType ||
                (settings.defaults ? settings.defaults.type : null);
              item = Factory.create(settings);
            }
            ctrlItems.push(item);
          }
        });
        return ctrlItems;
      },
      /**
       * Renders new control instances.
       *
       * @private
       */
      renderNew: function () {
        var self = this;
        // Render any new items
        self.items().each(function (ctrl, index) {
          var containerElm;
          ctrl.parent(self);
          if (!ctrl.state.get('rendered')) {
            containerElm = self.getEl('body');
            // Insert or append the item
            if (containerElm.hasChildNodes() && index <= containerElm.childNodes.length - 1) {
              $(containerElm.childNodes[index]).before(ctrl.renderHtml());
            } else {
              $(containerElm).append(ctrl.renderHtml());
            }
            ctrl.postRender();
            ReflowQueue.add(ctrl);
          }
        });
        self._layout.applyClasses(self.items().filter(':visible'));
        self._lastRect = null;
        return self;
      },
      /**
       * Appends new instances to the current container.
       *
       * @method append
       * @param {Array/tinymce.ui.Collection} items Array if controls to append.
       * @return {tinymce.ui.Container} Current container instance.
       */
      append: function (items) {
        return this.add(items).renderNew();
      },
      /**
       * Prepends new instances to the current container.
       *
       * @method prepend
       * @param {Array/tinymce.ui.Collection} items Array if controls to prepend.
       * @return {tinymce.ui.Container} Current container instance.
       */
      prepend: function (items) {
        var self = this;
        self.items().set(self.create(items).concat(self.items().toArray()));
        return self.renderNew();
      },
      /**
       * Inserts an control at a specific index.
       *
       * @method insert
       * @param {Array/tinymce.ui.Collection} items Array if controls to insert.
       * @param {Number} index Index to insert controls at.
       * @param {Boolean} [before=false] Inserts controls before the index.
       */
      insert: function (items, index, before) {
        var self = this, curItems, beforeItems, afterItems;
        items = self.create(items);
        curItems = self.items();
        if (!before && index < curItems.length - 1) {
          index += 1;
        }
        if (index >= 0 && index < curItems.length) {
          beforeItems = curItems.slice(0, index).toArray();
          afterItems = curItems.slice(index).toArray();
          curItems.set(beforeItems.concat(items, afterItems));
        }
        return self.renderNew();
      },
      /**
       * Populates the form fields from the specified JSON data object.
       *
       * Control items in the form that matches the data will have it's value set.
       *
       * @method fromJSON
       * @param {Object} data JSON data object to set control values by.
       * @return {tinymce.ui.Container} Current form instance.
       */
      fromJSON: function (data) {
        var self = this;
        for (var name in data) {
          self.find('#' + name).value(data[name]);
        }
        return self;
      },
      /**
       * Serializes the form into a JSON object by getting all items
       * that has a name and a value.
       *
       * @method toJSON
       * @return {Object} JSON object with form data.
       */
      toJSON: function () {
        var self = this, data = {};
        self.find('*').each(function (ctrl) {
          var name = ctrl.name(), value = ctrl.value();
          if (name && typeof value != "undefined") {
            data[name] = value;
          }
        });
        return data;
      },
      /**
       * Renders the control as a HTML string.
       *
       * @method renderHtml
       * @return {String} HTML representing the control.
       */
      renderHtml: function () {
        var self = this, layout = self._layout, role = this.settings.role;
        self.preRender();
        layout.preRender(self);
        return (
          '<div id="' + self._id + '" class="' + self.classes + '"' + (role ? ' role="' + this.settings.role + '"' : '') + '>' +
          '<div id="' + self._id + '-body" class="' + self.bodyClasses + '">' +
          (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.state.set('rendered', true);
        if (self.settings.style) {
          self.$el.css(self.settings.style);
        }
        if (self.settings.border) {
          box = self.borderBox;
          self.$el.css({
            'border-top-width': box.top,
            'border-right-width': box.right,
            'border-bottom-width': box.bottom,
            'border-left-width': box.left
          });
        }
        if (!self.parent()) {
          self.keyboardNav = new KeyboardNavigation({
            root: self
          });
        }
        return self;
      },
      /**
       * Initializes the current controls layout rect.
       * This will be executed by the layout managers to determine the
       * default minWidth/minHeight etc.
       *
       * @method initLayoutRect
       * @return {Object} Layout rect instance.
       */
      initLayoutRect: function () {
        var self = this, layoutRect = self._super();
        // Recalc container size by asking layout manager
        self._layout.recalc(self);
        return layoutRect;
      },
      /**
       * Recalculates the positions of the controls in the current container.
       * This is invoked by the reflow method and shouldn't be called directly.
       *
       * @method recalc
       */
      recalc: function () {
        var self = this, rect = self._layoutRect, lastRect = self._lastRect;
        if (!lastRect || lastRect.w != rect.w || lastRect.h != rect.h) {
          self._layout.recalc(self);
          rect = self.layoutRect();
          self._lastRect = { x: rect.x, y: rect.y, w: rect.w, h: rect.h };
          return true;
        }
      },
      /**
       * Reflows the current container and it's children and possible parents.
       * This should be used after you for example append children to the current control so
       * that the layout managers know that they need to reposition everything.
       *
       * @example
       * container.append({type: 'button', text: 'My button'}).reflow();
       *
       * @method reflow
       * @return {tinymce.ui.Container} Current container instance.
       */
      reflow: function () {
        var i;
        ReflowQueue.remove(this);
        if (this.visible()) {
          Control.repaintControls = [];
          Control.repaintControls.map = {};
          this.recalc();
          i = Control.repaintControls.length;
          while (i--) {
            Control.repaintControls[i].repaint();
          }
          // TODO: Fix me!
          if (this.settings.layout !== "flow" && this.settings.layout !== "stack") {
            this.repaint();
          }
          Control.repaintControls = [];
        }
        return this;
      }
    });
  }
);
 |