Source: argos-sdk/src/Application.js

/* Copyright (c) 2010, Sage Software, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import util from './Utility';
import ModelManager from './Models/Manager';
import Toast from './Dialogs/Toast';
import Modal from './Dialogs/Modal';
import BusyIndicator from './Dialogs/BusyIndicator';
import hash from 'dojo/hash';
import connect from 'dojo/_base/connect';
import ErrorManager from './ErrorManager';
import getResource from './I18n';
import { sdk } from './reducers/index';
import { setConnectionState } from './actions/connection';
import Scene from './Scene';
import { render } from './SohoIcons';


const resource = getResource('sdkApplication');

Function.prototype.bindDelegate = function bindDelegate(scope) { //eslint-disable-line
  const self = this;

  if (arguments.length === 1) {
    return function bound() {
      return self.apply(scope || this, arguments);
    };
  }

  const optional = Array.prototype.slice.call(arguments, 1);
  return function boundWArgs() {
    const called = Array.prototype.slice.call(arguments, 0);
    return self.apply(scope || this, called.concat(optional));
  };
};

/**
 * @alias argos.Application
 * @classdesc Application is a nexus that provides many routing and global application services that may be used
 * from anywhere within the app.
 *
 * It provides a shortcut alias to `window.App` (`App`) with the most common usage being `App.getView(id)`.
 */
class Application {
  constructor() {
    /**
     * @property enableConcurrencyCheck {Boolean} Option to skip concurrency checks to avoid precondition/412 errors.
     */
    this.enableConcurrencyCheck = false;

    this.ReUI = {
      app: null,
      back: function back() {
        if (!this.app) {
          return;
        }
        if (this.app.context &&
              this.app.context.history &&
                this.app.context.history.length > 0) {
          // Note: PageJS will push the page back onto the stack once viewed
          const from = this.app.context.history.pop();
          page.len--;

          const returnTo = from.data && from.data.options && from.data.options.returnTo;

          if (returnTo) {
            let returnIndex = [...this.app.context.history].reverse()
                                  .findIndex(val => val.page === returnTo);
            // Since want to find last index of page, must reverse index
            if (returnIndex !== -1) {
              returnIndex = (this.app.context.history.length - 1) - returnIndex;
            }
            this.app.context.history.splice(returnIndex);
            page.redirect(returnTo);
            return;
          }

          const to = this.app.context.history.pop();
          page.redirect(to.page);
          return;
        }
        page.back(this.app.homeViewId);
      },
      context: {
        history: null,
      },
    };

    /**
     * @property viewShowOptions {Array} Array with one configuration object that gets pushed before showing a view.
     * Allows passing in options via routing. Value gets removed once the view is shown.
     */
    this.viewShowOptions = null;

    /**
     * @property {String}
     * Current orientation of the application. Can be landscape or portrait.
     */
    this.currentOrientation = 'portrait';

    /**
     * Boolean for whether the application is an embedded app or not
     * @property {boolean}
     * @private
     */
    this._embedded = false;

    /**
     * Array of promises to load app state
     * @property {Array}
     * @private
     */
    this._appStatePromises = null;

    /**
     * Signifies the App has been initialized
     * @property {Boolean}
     * @private
     */
    this._started = false;

    this._rootDomNode = null;
    this._containerNode = null;
    this.customizations = null;
    this.services = null; // TODO: Remove
    this._connections = null;
    this.modules = null;
    this.views = null;
    this.hash = hash;
    this.onLine = true;
    this._currentPage = null;
    /**
     * Toolbar instances by key name
     * @property {Object}
     */
    this.bars = null;
    this.enableCaching = false;
    /**
     * The default Sage.SData.Client.SDataService instance
     * @property {Object}
     */
    this.defaultService = null;

    /**
     * The hash to redirect to after login.
     * @property {String}
     */
    this.redirectHash = '';
    /**
     * Signifies the maximum file size that can be uploaded in bytes
     * @property {int}
     */
    this.maxUploadFileSize = 4000000;

    /**
     * Timeout for the connection check.
     */
    this.PING_TIMEOUT = 3000;

    /**
     * Ping debounce time.
     */
    this.PING_DEBOUNCE = 1000;

    /**
     * Number of times to attempt to ping.
     */
    this.PING_RETRY = 5;

    /*
     * Static resource to request on the ping. Should be a small file.
     */
    this.PING_RESOURCE = 'ping.gif';
    /**
     * All options are mixed into App itself
     * @param {Object} options
     */
    this.ModelManager = null;
    this.isDynamicInitialized = false;

    this._appStatePromises = [];

    this.customizations = {};
    this.services = {}; // TODO: Remove
    this._connections = {};
    this.modules = [];
    this.views = {};
    this.bars = {};

    this.context = {
      history: [],
    };
    this.viewShowOptions = [];

    // For routing need to know homeViewId
    this.ReUI.app = this;

    this.ModelManager = ModelManager;

    /**
     * Instance of SoHo Xi applicationmenu.
     */
    this.applicationmenu = null;

    /**
     * Instance of SoHo Xi modal dialog for view settings. This was previously in
     * the right drawer.
     * @type {Modal}
    */
    this.viewSettingsModal = null;

    this.previousState = null;
    /*
     * Resource Strings
     */
    this.viewSettingsText = resource.viewSettingsText;
    this.closeText = resource.closeText;
  }

  /**
   * Loops through and disconnections connections and unsubscribes subscriptions.
   * Also calls {@link #uninitialize uninitialize}.
   */
  destroy() {
    $(window).off('resize', this.onResize.bind(this));
    $('body').off('beforetransition', this._onBeforeTransition.bind(this));
    $('body').off('aftertransition', this._onAfterTransition.bind(this));
    $('body').off('show', this._onActivate.bind(this));
    window.removeEventListener('online', this._onOnline.bind(this));
    window.removeEventListener('offline', this._onOffline.bind(this));

    this.uninitialize();
  }

  /**
   * Shelled function that is called from {@link #destroy destroy}, may be used to release any further handles.
   */
  uninitialize() {
  }

  back() {
    if (!this._embedded) {
      ReUI.back();
    }
  }

  /**
   * Initialize the hash and save the redirect hash if any
   */
  initHash() {
    const h = location.hash;
    if (h !== '') {
      this.redirectHash = h;
    }

    if (!this._embedded) {
      location.hash = '';
    }

    // Backwards compatibility for global uses of ReUI
    window.ReUI = this.ReUI;
    window.ReUI.context.history = this.context.history;
  }

  _onOffline() {
    this.ping();
  }

  _onOnline() {
    this.ping();
  }

  _updateConnectionState(online) {
    // Don't fire the onConnectionChange if we are in the same state.
    if (this.onLine === online) {
      return;
    }

    this.onLine = online;
    this.onConnectionChange(online);
  }

  forceOnline() {
    this.store.dispatch(setConnectionState(true));
  }

  forceOffline() {
    this.store.dispatch(setConnectionState(false));
  }

  onConnectionChange(/* online*/) {}

  /**
   * Establishes various connections to events.
   */
  initConnects() {
    $(window).on('resize', this.onResize.bind(this));
    $('body').on('beforetransition', this._onBeforeTransition.bind(this));
    $('body').on('aftertransition', this._onAfterTransition.bind(this));
    $('body').on('show', this._onActivate.bind(this));
    $(document).ready(() => {
      window.addEventListener('online', this._onOnline.bind(this));
      window.addEventListener('offline', this._onOffline.bind(this));
    });

    this.ping();
  }

  /**
   * Returns a promise. The results are true of the resource came back
   * before the PING_TIMEOUT. The promise is rejected if there is timeout or
   * the response is not a 200 or 304.
   */
  _ping() {
    return new Promise((resolve) => {
      const xhr = new XMLHttpRequest();
      xhr.ontimeout = () => resolve(false);
      xhr.onerror = () => resolve(false);
      xhr.onload = () => {
        const DONE = 4;
        const HTTP_OK = 200;
        const HTTP_NOT_MODIFIED = 304;

        if (xhr.readyState === DONE) {
          if (xhr.status === HTTP_OK || xhr.status === HTTP_NOT_MODIFIED) {
            resolve(true);
          } else {
            resolve(false);
          }
        }
      };
      xhr.open('GET', `${this.PING_RESOURCE}?cache=${Math.random()}`);
      xhr.timeout = this.PING_TIMEOUT;
      xhr.send();
    });
  }

  /**
   * Executes the chain of promises registered with registerAppStatePromise.
   * When all promises are done, a new promise is returned to the caller, and all
   * registered promises are flushed.
   * Each app state can be processed all at once or in a specfic seqence.
   * Example:
   * We can register  App state seqeunces as the following, where each sequence
   * is proccessed in a desending order form 0 to n. The first two in this example are defeulted to a
   * sequence of zero (0) and are procced first in which after the next sequence (1) is proccessed
   * and once all of its items are finshed then the last sequence 2 will start and process all of its items.
   *
   * If two seqences have the same number then thay will get combinded as if they where registerd together.
   * Aso not all items whith in a process are processed and ansync of each other and may not finish at the same time.
   *
   * To make two items process one after the other simpley put them in to diffrent sequences.
   *
   *   this.registerAppStatePromise(() => {some functions that returns a promise});
   *   this.registerAppStatePromise(() => {some functions that returns a promise});
   *
   *   this.registerAppStatePromise({
   *     seq: 1,
   *     description: 'Sequence 1',
   *     items: [{
   *       name: 'itemA',
   *       description: 'item A',
   *       fn: () => { some functions that returns a promise },
   *       }, {
   *         name: 'itemb',
   *         description: 'Item B',
   *         fn: () => {some functions that returns a promise},
   *       }],
   *   });
   *
   *   this.registerAppStatePromise({
   *     seq: 2,
   *     description: 'Sequence 2',
   *     items: [{
   *       name: 'item C',
   *       description: 'item C',
   *       fn: () => { some functions that returns a promise },
   *       },
   *    });
   *
   * There are there App state seqences re
   *
   * @return {Promise}
   */
  initAppState() {
    return new Promise((resolve, reject) => {
      const sequences = [];
      this._appStatePromises.forEach((item) => {
        let seq;
        if (typeof item === 'function') {
          seq = sequences.find(x => x.seq === 0);
          if (!seq) {
            seq = {
              seq: 0,
              description: resource.loadingApplicationStateText,
              items: [],
            };
            sequences.push(seq);
          }
          seq.items.push({
            name: 'default',
            description: '',
            fn: item,
          });
        } else {
          if (item.seq && item.items) {
            seq = sequences.find(x => x.seq === ((item.seq) ? item.seq : 0));
            if (seq) {
              item.items.forEach((_item) => {
                seq.items.push(_item);
              });
            } else {
              sequences.push(item);
            }
          }
        }
      });
      // Sort the sequence ascending so we can processes them in the right order.
      sequences.sort((a, b) => {
        if (a.seq > b.seq) {
          return 1;
        }

        if (a.seq < b.seq) {
          return -1;
        }

        return 0;
      });

      this._initAppStateSequence(0, sequences).then((results) => {
        this.clearAppStatePromises();
        this.initModulesDynamic();
        resolve(results);
      }, (err) => {
        this.clearAppStatePromises();
        reject(err);
      });
    });
  }

  /**
   * Process a app state sequence and start the next sequnce when done.
   * @param {index) the index of the sequence to start
   * @param {sequences) an array of sequences
   */
  _initAppStateSequence(index, sequences) {
    return new Promise((resolve, reject) => {
      const seq = sequences[index];
      if (seq) { // We need to send an observable and get ride of the ui element.
        const indicator = new BusyIndicator({
          id: `busyIndicator__appState_${seq.seq}`,
          label: `${resource.initializingText} ${seq.description}`,
        });
        this.modal.disableClose = true;
        this.modal.showToolbar = false;
        this.modal.add(indicator);
        indicator.start();
        const promises = seq.items.map((item) => {
          return item.fn();
        });

        Promise.all(promises).then(() => {
          indicator.complete(true);
          this.modal.disableClose = false;
          this.modal.hide();
          this._initAppStateSequence(index + 1, sequences).then((results) => {
            resolve(results);
          }, (err) => {
            indicator.complete(true);
            this.modal.disableClose = false;
            this.modal.hide();
            reject(err);
          });
        }, (err) => {
          ErrorManager.addSimpleError(indicator.label, err);
          indicator.complete(true);
          this.modal.disableClose = false;
          this.modal.hide();
          reject(err);
        });
      } else {
        resolve();
      }
    });
  }

  /**
   * Registers a promise that will resolve when initAppState is invoked.
   * @param {Promise|Function} promise A promise or a function that returns a promise
   */
  registerAppStatePromise(promise) {
    this._appStatePromises.push(promise);
    return this;
  }

  clearAppStatePromises() {
    this._appStatePromises = [];
  }

  onSetOrientation(/* value*/) {}

  /**
   * Loops through connections and calls {@link #registerService registerService} on each.
   */
  initServices() {
    for (const name in this.connections) {
      if (this.connections.hasOwnProperty(name)) {
        this.registerService(name, this.connections[name]);
      }
    }
  }

  /**
   * Loops through modules and calls their `init()` function.
   */
  initModules() {
    for (let i = 0; i < this.modules.length; i++) {
      this.modules[i].init(this);
    }
  }

  /**
   * Loops through modules and calls their `initDynamic()` function.
   */
  initModulesDynamic() {
    if (this.isDynamicInitialized) {
      return;
    }
    for (let i = 0; i < this.modules.length; i++) {
      this.modules[i].initDynamic(this);
    }
    this.isDynamicInitialized = true;
  }

  /**
   * Loops through (tool)bars and calls their `init()` function.
   */
  initToolbars() {
    for (const n in this.bars) {
      if (this.bars.hasOwnProperty(n)) {
        this.bars[n].init(); // todo: change to startup
      }
    }
  }

  /**
   * Sets the global variable `App` to this instance.
   */
  activate() {
    window.App = this;
  }

  /**
   * Initializes this application as well as the toolbar and all currently registered views.
   */
  init(domNode) {
    this.initIcons();
    this.initStore();
    this.initAppDOM(domNode);
    this.initPreferences();
    this.initSoho();
    this.initToasts();
    this.initPing();
    this.initServices(); // TODO: Remove
    this.initConnects();
    this._startupConnections();
    this.initModules();
    this.initToolbars();
    this.initHash();
    this.initModal();
    this.initScene();
  }

  initIcons() {
    render();
  }

  initSoho() {
    const container = this.getAppContainerNode();
    const menu = $('.application-menu', container).first();
    menu.applicationmenu();
    this.applicationmenu = menu.data('applicationmenu');

    const viewSettingsModal = $('.modal.view-settings', container).first();
    viewSettingsModal.modal();
    this.viewSettingsModal = viewSettingsModal.data('modal');
  }

  initScene() {
    this.scene = new Scene(this.store);
  }

  initStore() {
    this.store = Redux.createStore(this.getReducer(),
      this.getInitialState(),
      window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
    this.store.subscribe(this._onStateChange.bind(this));
  }

  _onStateChange() {
    const state = this.store.getState();

    if (this.previousState === null) {
      this.previousState = state;
    }

    this.onStateChange(state);

    const sdkState = state && state.sdk;
    const previousSdkState = this.previousState && this.previousState.sdk;

    if (sdkState && previousSdkState && sdkState.online !== previousSdkState.online) {
      this._updateConnectionState(sdkState.online);
    }

    this.previousState = state;
  }

  onStateChange(state) { // eslint-disable-line
  }

  showApplicationMenuOnLarge() {
    // todo: openOnLarge causes this bug SOHO-6193
    this.applicationmenu.settings.openOnLarge = true;

    if (this.applicationmenu.isLargerThanBreakpoint()) {
      this.applicationmenu.openMenu();
    }
  }

  hideApplicationMenu() {
    this.applicationmenu.closeMenu();
  }

  showApplicationMenu() {
    this.applicationmenu.openMenu();
  }

  getReducer() {
    return sdk;
  }

  getInitialState() {
    return {};
  }

  initToasts() {
    this.toast = new Toast({
      containerNode: this.getContainerNode(),
    });
  }

  initPing() {
    // Lite build, which will not have Rx, disable offline and ping
    if (!Rx) {
      this.ping = () => {
        this.store.dispatch(setConnectionState(true));
      };
      this.enableOfflineSupport = false;
    }

    // this.ping will be set if ping was passed as an options to the ctor
    if (this.ping) {
      return;
    }

    this.ping = util.debounce(() => {
      this.toast.add({
        message: resource.checkingText,
        title: resource.connectionToastTitleText,
        toastTime: this.PING_TIMEOUT,
      });
      const ping$ = Rx.Observable.interval(this.PING_TIMEOUT)
        .flatMap(() => {
          return Rx.Observable.fromPromise(this._ping())
            .flatMap((online) => {
              if (online) {
                return Rx.Observable.of(online);
              }

              return Rx.Observable.throw(new Error());
            });
        })
        .retry(this.PING_RETRY)
        .take(1);

      ping$.subscribe(() => {
        this.store.dispatch(setConnectionState(true));
      }, () => {
        this.store.dispatch(setConnectionState(false));
      });
    }, this.PING_DEBOUNCE);
  }

  initPreferences() {
    this._loadPreferences();
  }

  initModal() {
    this.modal = new Modal();
    this.modal.place(this._appContainerNode)
      .hide();
  }

  is24HourClock() {
    return (JSON.parse(window.localStorage.getItem('use24HourClock') || Mobile.CultureInfo.default24HourClock.toString()) === true);
  }

  /**
   * Check if the browser supports touch events.
   * @return {Boolean} true if the current browser supports touch events, false otherwise.
   */
  supportsTouch() {
    // Taken from https://github.com/Modernizr/Modernizr/ (MIT Licensed)
    return ('ontouchstart' in window) || (window.DocumentTouch && document instanceof window.DocumentTouch);
  }

  supportsFileAPI() {
    if (this.isIE()) {
      return false;
    }

    if (window.File && window.FileReader && window.FileList && window.Blob) {
      return true;
    }

    return false;
  }

  isIE() {
    return /MSIE|Trident/.test(window.navigator.userAgent);
  }

  persistPreferences() {
    try {
      if (window.localStorage) {
        window.localStorage.setItem('preferences', JSON.stringify(this.preferences));
      }
    } catch (e) {
      console.error(e); // eslint-disable-line
    }
  }

  _loadPreferences() {
    try {
      if (window.localStorage) {
        this.preferences = JSON.parse(window.localStorage.getItem('preferences'));
      }
    } catch (e) {
      console.error(e); // eslint-disable-line
    }
  }

  /**
   * Establishes various connections to events.
   */
  _startupConnections() {
    for (const name in this.connections) {
      if (this.connections.hasOwnProperty(name)) {
        if (this.connections.hasOwnProperty(name)) {
          this.registerConnection(name, this.connections[name]);
        }
      }
    }

    /* todo: should we be mixing this in? */
    delete this.connections;
  }

  /**
   * Sets `_started` to true.
   */
  run() {
    this._started = true;
    this.registerOrientationCheck(this.updateOrientationDom.bind(this));
    page({
      dispatch: false,
      hashbang: true,
      usingUrl: !this._embedded,
    });
  }

  /**
   * Returns the `window.navigator.onLine` property for detecting if an internet connection is available.
   */
  isOnline() {
    return this.onLine;
  }

  /**
   * Returns true/false if the current view is the first/initial view.
   * This is useful for disabling the back button (so you don't hit the login page).
   * @returns {boolean}
   */
  isOnFirstView() {}

  /**
   * Optional creates, then registers an Sage.SData.Client.SDataService and adds the result to `App.services`.
   * @param {String} name Unique identifier for the service.
   * @param {Object} service May be a SDataService instance or constructor parameters to create a new SDataService instance.
   * @param {Object} options Optional settings for the registered service.
   */
  registerService(name, service, options = {}) {
    const instance = service instanceof Sage.SData.Client.SDataService ? service : new Sage.SData.Client.SDataService(service);

    this.services[name] = instance;

    instance.on('requesttimeout', this.onRequestTimeout, this);

    if ((options.isDefault || service.isDefault) || !this.defaultService) {
      this.defaultService = instance;
    }

    return this;
  }

  /**
   * Optional creates, then registers an Sage.SData.Client.SDataService and adds the result to `App.services`.
   * @param {String} name Unique identifier for the service.
   * @param {Object} definition May be a SDataService instance or constructor parameters to create a new SDataService instance.
   * @param {Object} options Optional settings for the registered service.
   */
  registerConnection(name, definition, options = {}) {
    const instance = definition instanceof Sage.SData.Client.SDataService ? definition : new Sage.SData.Client.SDataService(definition);

    this._connections[name] = instance;

    instance.on('requesttimeout', this.onRequestTimeout, this);

    if ((options.isDefault || definition.isDefault) || !this._connections.default) {
      this._connections.default = instance;
    }

    return this;
  }

  _onTimeout() {
    this.ping();
  }

  /**
   * Determines the the specified service name is found in the Apps service object.
   * @param {String} name Name of the SDataService to detect
   */
  hasService(name) {
    return !!this.services[name];
  }

  initAppDOM(domNode) {
    if (this._viewContainerNode && this._appContainerNode) {
      return;
    }

    // If a domNode is provided, create the app's dom under this
    if (domNode) {
      this._appContainerNode = domNode;
      this._createViewContainerNode();
      return;
    }

    // Nothing was provided, create a default
    this._createAppContainerNode();
    this._createViewContainerNode();
  }

  _createAppContainerNode() {
    const defaultAppContainerId = 'rootNode';
    $('body').append(`
      <div id="${defaultAppContainerId}">
      </div>
    `);
    this._appContainerNode = $(`#${defaultAppContainerId}`).get(0);
  }

  _createViewContainerNode() {
    if (!this._appContainerNode) {
      throw new Error('Set the app container node before creating the view container node.');
    }

    const defaultViewContainerId = 'viewContainer';
    const defaultViewContainerClasses = 'page-container viewContainer';
    $(this._appContainerNode).append(`
      <nav id="application-menu" data-open-on-large="false" class="application-menu show-shadow"
        data-breakpoint="large">
      </nav>
      <div class="page-container scrollable tbarContainer">
        <div id="${defaultViewContainerId}" class="${defaultViewContainerClasses}"></div>
        <div class="modal view-settings" role="dialog" aria-modal="true" aria-hidden="false">
          <div class="modal-content">
            <div class="modal-header">
              <h1>${this.viewSettingsText}</h1>
            </div>
            <div class="modal-body">
            </div>
            <div class="modal-buttonset">
              <button type="button" class="btn-modal" style="width:100%">${this.closeText}</button>
            </div>
          </div>
        </div>
      </div>
    `);

    this._viewContainerNode = $(`#${defaultViewContainerId}`).get(0);
  }

  /**
   * Returns the dom associated to the container element.
   * @deprecated
   */
  getContainerNode() {
    return this._appContainerNode || this._viewContainerNode;
  }

  getAppContainerNode() {
    return this._appContainerNode;
  }

  getViewContainerNode() {
    return this._viewContainerNode;
  }

  /**
   * Registers a view with the application and renders it to HTML.
   * If the application has already been initialized, the view is immediately initialized as well.
   * @param {View} view A view instance to be registered.
   * @param {domNode} domNode Optional. A DOM node to place the view in.
   */
  registerView(view, domNode) {
    const id = view.id;

    const node = domNode || this._viewContainerNode;
    view._placeAt = node;
    this.views[id] = view;

    this.registerViewRoute(view);

    this.onRegistered(view);

    return this;
  }

  registerViewRoute(view) {
    if (!view || typeof view.getRoute !== 'function') {
      return;
    }

    page(view.getRoute(), view.routeLoad.bind(view), view.routeShow.bind(view));
  }

  /**
   * Registers a toolbar with the application and renders it to HTML.
   * If the application has already been initialized, the toolbar is immediately initialized as well.
   * @param {String} name Unique name of the toolbar
   * @param {Toolbar} tbar Toolbar instance to register
   * @param {domNode} domNode Optional. A DOM node to place the view in.
   */
  registerToolbar(n, t, domNode) {
    let name = n;
    let tbar = t;

    if (typeof name === 'object') {
      tbar = name;
      name = tbar.name;
    }

    this.bars[name] = tbar;

    if (this._started) {
      tbar.init();
    }

    const tbarNode = $('> .tbarContainer', this._appContainerNode).get(0);
    const node = domNode || tbarNode;
    tbar.placeAt(node, 'first');

    return this;
  }

  /**
   * Returns all the registered views.
   * @return {View[]} An array containing the currently registered views.
   */
  getViews() {
    const results = [];

    for (const view in this.views) {
      if (this.views.hasOwnProperty(view)) {
        results.push(this.views[view]);
      }
    }

    return results;
  }

  /**
   * Checks to see if the passed view instance is the currently active one by comparing it to {@link #getPrimaryActiveView primaryActiveView}.
   * @param {View} view
   * @return {Boolean} True if the passed view is the same as the active view.
   */
  isViewActive(view) {
    // todo: add check for multiple active views.
    return (this.getPrimaryActiveView() === view);
  }

  updateOrientationDom(value) {
    const root = $(this.getContainerNode());
    const currentOrient = root.attr('orient');
    if (value === currentOrient) {
      return;
    }

    root.attr('orient', value);

    if (value === 'portrait') {
      root.removeClass('landscape');
      root.addClass('portrait');
    } else if (value === 'landscape') {
      root.removeClass('portrait');
      root.addClass('landscape');
    } else {
      root.removeClass('portrait');
      root.removeClass('landscape');
    }

    this.currentOrientation = value;
    this.onSetOrientation(value);
    connect.publish('/app/setOrientation', [value]);
  }

  registerOrientationCheck(callback) {
    const match = window.matchMedia('(orientation: portrait)');

    const checkMedia = (m) => {
      if (m.matches) {
        callback('portrait');
      } else {
        callback('landscape');
      }
    };
    match.addListener(checkMedia);
    checkMedia(match);
  }

  /**
   * Gets the current page and then returns the result of {@link #getView getView(name)}.
   * @return {View} Returns the active view instance, if no view is active returns null.
   */
  getPrimaryActiveView() {
    const el = this.getCurrentPage();
    if (el) {
      return this.getView(el);
    }
  }

  /**
   * Sets the current page(domNode)
   * @param {DOMNode}
   */
  setCurrentPage(_page) {
    this._currentPage = _page;
  }

  /**
   * Gets the current page(domNode)
   * @returns {DOMNode}
   */
  getCurrentPage() {
    return this._currentPage;
  }

  /**
   * Determines if any registered view has been registered with the provided key.
   * @param {String} key Unique id of the view.
   * @return {Boolean} True if there is a registered view name matching the key.
   */
  hasView(key) {
    return !!this._internalGetView({
      key,
      init: false,
    });
  }

  /**
   * Returns the registered view instance with the associated key.
   * @param {String/Object} key The id of the view to return, if object then `key.id` is used.
   * @return {View} view The requested view.
   */
  getView(key) {
    return this._internalGetView({
      key,
      init: true,
    });
  }
  getViewDetailOnly(key) {
    return this._internalGetView({
      key,
      init: false,
    });
  }

  _internalGetView(options) {
    const key = options && options.key;
    const init = options && options.init;

    if (key) {
      let view;
      if (typeof key === 'string') {
        view = this.views[key];
      } else if (typeof key.id === 'string') {
        view = this.views[key.id];
      }

      if (init && view && !view._started) {
        view.init(this.store);
        view.placeAt(view._placeAt, 'first');
        view._started = true;
        view._placeAt = null;
      }

      return view;
    }

    return null;
  }

  /**
   * Returns the defined security for a specific view
   * @param {String} key Id of the registered view to query.
   * @param access
   */
  getViewSecurity(key, access) {
    const view = this._internalGetView({
      key,
      init: false,
    });
    return (view && view.getSecurity(access));
  }

  /**
   * Returns the registered SDataService instance by name, or returns the default service.
   * @param {String/Boolean} name If string service is looked up by name. If false, default service is returned.
   * @return {Object} The registered Sage.SData.Client.SDataService instance.
   */
  getService(name) {
    if (typeof name === 'string' && this.services[name]) {
      return this.services[name];
    }

    return this.defaultService;
  }

  /**
   * Determines the the specified service name is found in the Apps service object.
   * @param {String} name Name of the SDataService to detect
   */
  hasConnection(name) {
    return !!this._connections[name];
  }

  getConnection(name) {
    if (this._connections[name]) {
      return this._connections[name];
    }

    return this._connections.default;
  }

  /**
   * Sets the applications current title.
   * @param {String} title The new title.
   */
  setPrimaryTitle(title) {
    for (const n in this.bars) {
      if (this.bars.hasOwnProperty(n)) {
        if (this.bars[n].managed) {
          this.bars[n].set('title', title);

          // update soho toolbar when title is changed since it uses text length to calculate header width
          const header = $(this.bars.tbar.domNode);
          this.toolbar = header.find('.toolbar').data('toolbar');
          this.toolbar.updated();
        }
      }
    }

    return this;
  }

  /**
   * Resize handle
   */
  onResize() {
  }

  onRegistered(/* view*/) {}

  onBeforeViewTransitionAway(/* view*/) {}

  onBeforeViewTransitionTo(/* view*/) {}

  onViewTransitionAway(/* view*/) {}

  onViewTransitionTo(/* view*/) {}

  onViewActivate(/* view, tag, data*/) {}

  _onBeforeTransition(evt) {
    const view = this.getView(evt.target);
    if (view) {
      if (evt.out) {
        this._beforeViewTransitionAway(view);
      } else {
        this._beforeViewTransitionTo(view);
      }
    }
  }

  _onAfterTransition(evt) {
    const view = this.getView(evt.target);
    if (view) {
      if (evt.out) {
        this._viewTransitionAway(view);
      } else {
        this._viewTransitionTo(view);
      }
    }
  }

  _onActivate(evt) {
    const view = this.getView(evt.target);
    if (view) {
      this._viewActivate(view, evt.tag, evt.data);
    }
  }

  _beforeViewTransitionAway(view) {
    this.onBeforeViewTransitionAway(view);

    view.beforeTransitionAway();
  }

  _beforeViewTransitionTo(view) {
    this.onBeforeViewTransitionTo(view);

    for (const n in this.bars) {
      if (this.bars[n].managed) {
        this.bars[n].clear();
      }
    }

    view.beforeTransitionTo();
  }

  _viewTransitionAway(view) {
    this.onViewTransitionAway(view);

    view.transitionAway();
  }

  _viewTransitionTo(view) {
    this.onViewTransitionTo(view);

    const tools = (view.options && view.options.tools) || view.getTools() || {};

    for (const n in this.bars) {
      if (this.bars[n].managed) {
        this.bars[n].showTools(tools[n]);
      }
    }

    view.transitionTo();
  }

  _viewActivate(view, tag, data) {
    this.onViewActivate(view);

    view.activate(tag, data);
  }

  /**
   * Searches App.context.history by passing a predicate function that should return true if a match is found, false otherwise.
   * This is similar to queryNavigationContext, however, this function will return an array of found items instead of a single item.
   * @param {Function} predicate
   * @param {Object} scope
   * @return {Array} context history filtered out by the predicate.
   */
  filterNavigationContext(predicate, scope) {
    const list = this.context.history || [];
    const filtered = list.filter((item) => {
      return predicate.call(scope || this, item.data);
    });

    return filtered.map((item) => {
      return item.data;
    });
  }

  /**
   * Searches App.context.history by passing a predicate function that should return true
   * when a match is found.
   * @param {Function} predicate Function that is called in the provided scope with the current history iteration. It should return true if the history item is the desired context.
   * @param {Number} depth
   * @param {Object} scope
   * @return {Object/Boolean} context History data context if found, false if not.
   */
  queryNavigationContext(predicate, d, s) {
    let scope = s;
    let depth = d;

    if (typeof depth !== 'number') {
      scope = depth;
      depth = 0;
    }

    const list = this.context.history || [];

    depth = depth || 0;

    for (let i = list.length - 2, j = 0; i >= 0 && (depth <= 0 || j < depth); i--, j++) {
      if (predicate.call(scope || this, list[i].data)) {
        return list[i].data;
      }
    }

    return false;
  }

  /**
   * Shortcut method to {@link #queryNavigationContext queryNavigationContext} that matches the specified resourceKind provided
   * @param {String/String[]} kind The resourceKind(s) the history item must match
   * @param {Function} predicate Optional. If provided it will be called on matches so you may do an secondary check of the item - returning true for good items.
   * @param {Object} scope Scope the predicate should be called in.
   * @return {Object} context History data context if found, false if not.
   */
  isNavigationFromResourceKind(kind, predicate, scope) {
    const lookup = {};
    if (Array.isArray(kind)) {
      kind.forEach(function forEach(item) {
        this[item] = true;
      }, lookup);
    } else {
      lookup[kind] = true;
    }

    return this.queryNavigationContext(function queryNavigationContext(o) {
      const context = (o.options && o.options.source) || o;
      const resourceKind = context && context.resourceKind;

      // if a predicate is defined, both resourceKind AND predicate must match.
      if (lookup[resourceKind]) {
        if (predicate) {
          if (predicate.call(scope || this, o, context)) {
            return o;
          }
        } else {
          return o;
        }
      }
    });
  }

  /**
   * Registers a customization to a target path.
   *
   * A Customization Spec is a special object with the following keys:
   *
   * * `at`: `function(item)` - passes the current item in the list, the function should return true if this is the item being modified (or is at where you want to insert something).
   * * `at`: `{Number}` - May optionally define the index of the item instead of a function.
   * * `type`: `{String}` - enum of `insert`, `modify`, `replace` or `remove` that indicates the type of customization.
   * * `where`: `{String}` - enum of `before` or `after` only needed when type is `insert`.
   * * `value`: `{Object}` - the entire object to create (insert or replace) or the values to overwrite (modify), not needed for remove.
   * * `value`: `{Object[]}` - if inserting you may pass an array of items to create.
   *
   * Note: This also accepts the legacy signature:
   * `registerCustomization(path, id, spec)`
   * Where the path is `list/tools` and `id` is the view id
   *
   * All customizations are registered to `this.customizations[path]`.
   *
   * @param {String} path The customization set such as `list/tools#account_list` or `detail#contact_detail`. First half being the type of customization and the second the view id.
   * @param {Object} spec The customization specification
   */
  registerCustomization(p, s) {
    let path = p;
    let spec = s;

    if (arguments.length > 2) {
      const customizationSet = arguments[0];
      const id = arguments[1];

      spec = arguments[2];
      path = id ? `${customizationSet}#${id}` : customizationSet;
    }

    const container = this.customizations[path] || (this.customizations[path] = []);
    if (container) {
      container.push(spec);
    }

    return this;
  }

  /**
   * Returns the customizations registered for the provided path.
   *
   * Note: This also accepts the legacy signature:
   * `getCustomizationsFor(set, id)`
   * Where the path is `list/tools` and `id` is the view id
   *
   * @param {String} path The customization set such as `list/tools#account_list` or `detail#contact_detail`. First half being the type of customization and the second the view id.
   */
  getCustomizationsFor(p) {
    let path = p;

    if (arguments.length > 1) {
      path = arguments[1] ? `${arguments[0]}#${arguments[1]}` : arguments[0];
    }

    const segments = path.split('#');
    const customizationSet = segments[0];
    const forPath = this.customizations[path] || [];
    const forSet = this.customizations[customizationSet] || [];

    return forPath.concat(forSet);
  }

  hasAccessTo(/* security*/) {
    return true;
  }

  /**
   * Override this function to load a view in the left drawer.
   */
  showLeftDrawer() {
    return this;
  }

  /**
   * Override this function to load a view in the right drawer.
   */
  showRightDrawer() {
    return this;
  }

  setToolBarMode(onLine) {
    for (const n in this.bars) {
      if (this.bars[n].managed) {
        this.bars[n].setMode(onLine);
      }
    }
  }
}

export default Application;