APIs

Show:
/**
  @module ember-flexberry
*/

import Service from '@ember/service';
import Evented from '@ember/object/evented';
import $ from 'jquery';
import { getOwner } from '@ember/application';
import { isBlank } from '@ember/utils';

/**
  Devices detection service.
  Uses <a href="https://github.com/matthewhudson/device.js">devicejs</a> with noConflict,
  duplicates all <a href="https://github.com/matthewhudson/device.js">devicejs</a> methods inside service & implements some new methods.

  @class DeviceService
  @extends <a href="https://emberjs.com/api/ember/release/classes/Service">Service</a>
*/
export default Service.extend(Evented, {
  /**
    If set as `true`, into prefixes for paths, returned by `pathPrefixes` function, will be added prefix in format: 'platformName-deviceType'.

    @property prefixForPlatformAndType
    @type Boolean
    @default false
  */
  prefixForPlatformAndType: false,

  /**
    If set as `true`, each prefix for paths, returned by `pathPrefixes` function, will be duplicated in format: 'prefix-currentOrientation'.

    @property prefixForOrientation
    @type Boolean
    @default false
  */
  prefixForOrientation: false,

  /**
    If set as `true`, into prefixes for paths, returned by `pathPrefixes` function, will be added prefix in format: 'deviceType'.

    @property prefixForType
    @type Boolean
    @default false
  */
  prefixForType: false,

  /**
    Device service cache.

    @property {Object} _cache
    @property {String} _cache.orientation Cached orientation ('portrait' or 'landscape').
    @property {String} _cache.platform Cached platform ('ios', 'windows', 'android', ...).
    @property {String} _cache.type Cached device type ('desktop', 'phone', 'tablet', or 'tv').
    @property {Object} _cache.pathPrefixes Cached path prefixes.
    @property {String[]} _cache.pathPrefixes.landscape  Cached path prefixes for 'landscape' orientation
    (for example 'ipad-landscape', 'ipad', 'tablet-landscape', 'tablet', 'mobile-landscape', 'mobile').
    @property {String[]} _cache.pathPrefixes.portrait  Cached path prefixes for 'portrait' orientation
    (for example 'ipad-portrait', 'ipad', 'tablet-portrait', 'tablet', 'mobile-portrait', 'mobile').
    @private
  */
  _cache: undefined,

  /**
    Initializes service.
  */
  init() {
    this._super(...arguments);

    let devicejs = window.device;
    if (devicejs) {
      // Names of devicejs library features.
      let devicejsPropertiesNames = Object.keys(devicejs);

      // Inject devicejs library features into service.
      for (let i = 0, len = devicejsPropertiesNames.length; i < len; i++) {
        let propertieName = devicejsPropertiesNames[i];
        this.set(propertieName, devicejs[propertieName]);
      }

      let app = getOwner(this).application;
      if (app.deviceService) {
        this.set('prefixForType', !!app.deviceService.prefixForType);
        this.set('prefixForOrientation', !!app.deviceService.prefixForOrientation);
        this.set('prefixForPlatformAndType', !!app.deviceService.prefixForPlatformAndType);
      }

      // Attach orientation change handler.
      this.set('_onOrientationChange', this._onOrientationChange.bind(this));
      $(window).on('resize orientationchange', this._onOrientationChange);
    } else {
      throw new Error('Device service error. Can\'t find device.js entry point.');
    }

    this.set('_cache', {
      orientation: null,
      platform: null,
      type: null,
      pathPrefixes: {
        landscape: null,
        portrait: null
      }
    });
  },

  /**
    Get current device is desktop.

    @method isDesktop
    @return {Boolean} Returns true, if device is desktop.
  */
  isDesktop() {
    return this.desktop();
  },

  /**
    Get current device is tablet.

    @method isTablet
    @return {Boolean} Returns true, if device is tablet.
  */
  isTablet() {
    return this.tablet();
  },

  /**
    Get current device is phone.

    @method isMobile
    @return {Boolean} Returns true, if device is phone.
  */
  isMobile() {
    return this.mobile();
  },

  /**
    Get current device is phone.

    @method isMobile
    @return {Boolean} Returns true, if device is phone.
  */
  isTv() {
    return this.television();
  },

  /**
    Destroys service.
  */
  willDestroy() {
    this._super(...arguments);

    $(window).off('resize orientationchange', this._onOrientationChange);
  },

  /**
    Returns current device orientation.

    @method orientation
    @param useCached {Boolean} Flag: indicates whether to use already cached value or not.
    @return {String} Returns current device orientation ('landscape' or 'portrait').
  */
  orientation(useCached) {
    let orientation;
    if (useCached === true && (orientation = this.get('_cache.orientation')) !== null) {
      return orientation;
    }

    orientation = this.portrait() ? 'portrait' : 'landscape';
    this.set('_cache.orientation', orientation);

    return orientation;
  },

  /**
    Returns current device platform.

    @method platform
    @param useCached {Boolean} Flag: indicates whether to use already cached value or not.
    @return {String} Returns device platform ('windows', ios', 'android', 'blackberry', 'fxos', 'meego', ...).
  */
  platform(useCached) {
    let platform;
    if (useCached === true && (platform = this.get('_cache.platform')) !== null) {
      return platform;
    }

    if (this.windows()) {
      platform = 'windows';
    } else if (this.ios()) {
      platform = 'ios';
    } else if (this.android()) {
      platform = 'android';
    } else if (this.blackberry()) {
      platform = 'blackberry';
    } else if (this.fxos()) {
      platform = 'fxos';
    } else if (this.meego()) {
      platform = 'meego';
    } else {
      platform = '';
    }

    this.set('_cache.platform', platform);

    return platform;
  },

  /**
    Returns current device type.

    @method type
    @param useCached {Boolean} Flag: indicates whether to use already cached value or not.
    @return {String} Returns device type ('desktop', 'phone', 'tablet', 'tv', ...).
  */
  type(useCached) {
    let type;
    if (useCached === true && (type = this.get('_cache.type')) !== null) {
      return type;
    }

    if (this.isDesktop()) {
      type = 'desktop';
    } else if (this.isMobile()) {
      type = 'phone';
    } else if (this.isTablet()) {
      type = 'tablet';
    } else if (this.isTv()) {
      type = 'tv';
    } else {
      type = '';
    }

    this.set('_cache.type', type);

    return type;
  },

  /**
    Returns path prefixes for application resources (such as templates, controllers, ...),
    related to current device platform, type, and orientation.

    @method pathPrefixes
    @param useCached {Boolean} Flag: indicates whether to use already cached value or not.
    @return {String[]} Path prefixes for application resources (such as templates, components...),
    related to current platform, device type, and orientation.
  */
  pathPrefixes(useCached) {
    let currentOrientationPathPrefixes;
    let currentOrientation = this.orientation(useCached);

    // Return already cached prefixes.
    if (useCached === true && (currentOrientationPathPrefixes = this.get('_cache.pathPrefixes.' + currentOrientation)) !== null) {
      return currentOrientationPathPrefixes;
    }

    let pathPrefixes = { landscape: [], portrait: [] };
    if (this.isDesktop()) {
      // No path prefixes for desktop.
      // Cache and return empty array.
      this.set('_cache.pathPrefixes', pathPrefixes);
      currentOrientationPathPrefixes = pathPrefixes[currentOrientation];

      return currentOrientationPathPrefixes;
    }

    let platform = this.platform(useCached);
    let type = this.type(useCached);

    // Path prefix with platform and device type: 'ipad', 'android-tablet', 'windows-phone', etc.
    let pathPrefixWithPlatformAndType = '';
    if (this.get('prefixForPlatformAndType')) {
      if (this.ios()) {
        pathPrefixWithPlatformAndType = this.ipad() ? 'ipad' : this.ipod() ? 'ipod' : 'iphone';
      } else if (!(isBlank(platform) || isBlank(type))) {
        pathPrefixWithPlatformAndType = platform + '-' + type;
      }
    }

    // Path prefix with device type only: 'tablet', 'phone', 'tv' etc.
    let pathPrefixWithType;
    if (this.get('prefixForType')) {
      pathPrefixWithType = type;
    }

    // Path prefix with common type only: 'mobile' for both tablets and phones etc.
    let pathPrefixCommon = this.isMobile() || this.isTablet() ? 'mobile' : '';

    // Path prefixes without orientation.
    let pathPrefixesWithoutOrientation = [
        pathPrefixWithPlatformAndType,
        pathPrefixWithType,
        pathPrefixCommon
    ].filter((pathPrefix) => {
      return !isBlank(pathPrefix);
    });

    // Form resulting path prefixes dictionary with additional prefixes related to orientation:
    // ['prefix1', 'prefix2', ...] -> {
    //   landscape: ['prefix1-landscape', 'prefix1', 'prefix2-landscape', 'prefix2', ...],
    //   portrait: ['prefix1-portrait', 'prefix1', 'prefix2-portrait', 'prefix2', ...]
    // }
    for (let orientation in pathPrefixes) {
      if (!pathPrefixes.hasOwnProperty(orientation)) {
        break;
      }

      for (let i = 0, len = pathPrefixesWithoutOrientation.length; i < len; i++) {
        if (this.get('prefixForOrientation')) {
          pathPrefixes[orientation].push(pathPrefixesWithoutOrientation[i] + '-' + orientation);
        }

        pathPrefixes[orientation].push(pathPrefixesWithoutOrientation[i]);
      }
    }

    // Cache and return resulting array.
    this.set('_cache.pathPrefixes', pathPrefixes);
    currentOrientationPathPrefixes = pathPrefixes[currentOrientation];

    return currentOrientationPathPrefixes;
  },

  /**
    Handles window's 'resize' & 'orientationchange' events.

    @method _onOrientationChange
    @private
  */
  _onOrientationChange: function() {
    let previousOrientation = this.get('_cache.orientation');
    let currentOrientation = this.orientation(false);
    if (previousOrientation !== currentOrientation) {
      this.trigger('orientationchange', { orientation: currentOrientation });
    }
  }
});