Source: enyo-x/source/core.js

/*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
trailing:true, white:true, strict: false*/
/*global XV:true, XM:true, _:true, onyx:true, enyo:true, document:true, XT:true, Globalize:true */

(function () {

  /**
    XV is the global namespace for all the "xTuple Views" defined in
    enyo-x and elsewhere

    @namespace XV
   */
  XV = {};
  XV._modelCaches = {};
  XV._modelLists = {};
  XV._modelWorkspaces = {};

  // Class methods
  enyo.mixin(XV, /** @lends XV */{

    KEY_UP: 38,
    KEY_DOWN: 40,
    KEY_TAB: 9,
    KEY_ENTER: 13,

    /**
      Key/value mapping of widget class names that correspond with object definitions
      to implement a corresponding editor widget.
    */
    widgetTypeMap: {
      Cost: "XV.Cost",
      Date: "XV.DateWidget",
      DueDate: "XV.DateWidget",
      ExtendedPrice: {
        kind: "XV.MoneyWidget",
        scale: XT.EXTENDED_PRICE_SCALE
      },
      Money: {
        kind: "XV.MoneyWidget",
        scale: XT.MONEY_SCALE
      },
      Number: "XV.NumberWidget",
      PurchasePrice: {
        kind: "XV.MoneyWidget",
        scale: XT.PURCHASE_PRICE_SCALE
      },
      Quantity: "XV.QuantityWidget",
      QuantityPer: "XV.QuantityPerWidget",
      SalesPrice: {
        kind: "XV.MoneyWidget",
        scale: XT.SALES_PRICE_SCALE
      },
      String: "XV.InputWidget",
      Unit: "XV.UnitPicker",
      UnitRatio: "XV.UnitRatioWidget",
      UserAccountRelation: "XV.UserAccountWidget",
      Weight: "XV.WeightWidget"
    },

    /**
      Accepts a model and an attribute and returns a standard widget definition
      mapped to the attribute.

      *Warning* This implementation is incomplete. Widgets that reference object
      based attributes are not handled well and need to be refactored.

      @param {String} Model class name
      @param {String} Attribute name
    */
    getEditor: function (model, attr) {
      var Klass = XT.getObjectByName(model),
        type = Klass.getType(attr),
        widget = this.widgetTypeMap[type];

      // Handle normal widgets
      if (_.isString(widget)) {
        widget = {
          kind: widget,
          attr: attr
        };

      // Handle widgets with complex attributes
      } else if (widget.kind === "XV.MoneyWidget") {
        widget.localValue = attr;
      }

      return widget;
    },

    /**
      Add component or array of component view(s) to a view class that
      has implemented the `ExtensionsMixin`.

      Examples of classes that support extensions are:
        * `Workspace`
        * `List`
        * `ParameterWidget`

      @param {String} Class name
      @param {Object|Array} Component(s)
    */
    appendExtension: function (workspace, extension) {
      var Klass = XT.getObjectByName(workspace),
        extensions = Klass.prototype.extensions || [];
      if (!_.isArray(extension)) {
        extension = [extension];
      }
      Klass.prototype.extensions = extensions.concat(extension);
    },

    /**
      Helper function for enyo unit testing

      @param expected
      @param actual
      @param {String} message
         Only displayed in the case of a failed test
      @return {String} Per enyo's conventions, the empty string means the test is passed.
     */
    applyTest: function (expected, actual, message) {
      if (expected === actual) {
        return "";
      } else {
        if (message) {
          message = ". " + message;
        } else {
          message = ".";
        }
        return "Expected " + expected + ", saw " + actual + message;
      }
    },

    /**
      The javascript download method avoids target = "_blank" and the need to
      reload the app to see the new client. Pop up blockers should not be an issue.
      @See: http://stackoverflow.com/questions/3749231/download-file-using-javascript-jquery/3749395#3749395

      @param {String} The URL for the download route.
    */
    downloadURL: function (url) {
      var hiddenIFrameID = 'hiddenDownloader',
        iframe = document.getElementById(hiddenIFrameID);

      if (iframe === null) {
        iframe = document.createElement('iframe');
        iframe.id = hiddenIFrameID;
        iframe.style.display = 'none';
        document.body.appendChild(iframe);
      }

      iframe.src = url;
    },

    getCache: function (recordType) {
      return XV._modelCaches[recordType];
    },

    getList: function (recordType) {
      return XV._modelLists[recordType];
    },

    getWorkspace: function (recordType) {
      return XV._modelWorkspaces[recordType];
    },

    /*
      Is the ancestor a superkind (or supersuperkind, etc.) of the object?

      @param {Object} intantiated enyo kind
        You can use Kind.prototype if that's what you have to work with.
      @param {String} ancestor kind name
    */
    inheritsFrom: function (object, ancestor) {
      if (!object || !object.ctor) {
        return false;
      }
      while (object.kindName !== 'enyo.Object') {
        if (object.ctor.prototype.base.prototype.kindName === ancestor) {
          return true;
        }
        object = object.ctor.prototype.base.prototype;
      }
    },

    navigateToList: function (app, listKind) {
      var navigator = app.$.postbooks.$.navigator,
        myModuleIndex,
        myPanelIndex;

      //
      // Drill down into the appropriate module
      //
      _.each(navigator.modules, function (module, moduleIndex) {
        _.each(module.panels, function (panel, panelIndex) {
          if (listKind && panel.kind === listKind) {
            myModuleIndex = moduleIndex;
            myPanelIndex = panelIndex;
          }
        });
      });
      if (myPanelIndex === undefined) {
        return false;
      }
      navigator.setModule(myModuleIndex);
      navigator.setContentPanel(myPanelIndex);
      return navigator;
    },

    registerModelCache: function (recordType, cache) {
      XV._modelCaches[recordType] = cache;
    },

    registerModelList: function (recordType, list) {
      XV._modelLists[recordType] = list;
    },

    registerModelWorkspace: function (recordType, workspace) {
      XV._modelWorkspaces[recordType] = workspace;
    }

  });

  /**
    @class

    A mixin that allows the components of a class to be extended.
  */
  XV.ExtensionsMixin = {
    extensions: null,

    /**
      This function should be run in the create function of a class
      using this mixin. It will add any extensions to the class at run time.
      @parameter {Boolean} forceDeferred allows us to set a defer attribute
        on the extension so that it will not get processed during the usual
        processExtensions calls in the create. If you need to put an extension
        in a subkind of workspace requires the subkind's create() function
        to have already run, put a processExtensions(true) at the end
        of that subkind create() function.
    */
    processExtensions: function (forceDeferred) {
      var extensions = this.extensions || [],
        ext,
        containerString,
        container,
        i;

      if (this._extLength === undefined) {
        this._extLength = 0;
      }
      if (this._extLength === extensions.length) { return; }
      for (i = 0; i < extensions.length; i++) {
        ext = _.clone(this.extensions[i]);

        if (ext.defer !== forceDeferred) {
          // the workspace is not ready to add this extension yet,
          // or, it's probably been added already
          continue;
        }
        // Resolve name of container to the instance
        if (_.isString(ext.container)) {
          containerString = ext.container;
          container = this;
          while (containerString.indexOf(".") >= 0) {
            container = container.$[containerString.substring(0, containerString.indexOf("."))];
            containerString = containerString.substring(containerString.indexOf(".") + 1);
          }
          if (container) {
            // avoid a crash if the asked-for container doesn't exist
            ext.container = container.$[containerString];
          } else {
            XT.log("Requested container", ext.container, "not found");
            return;
          }
        }
        // Resolve `addBefore`
        if (_.isString(ext.addBefore)) {
          ext.addBefore = this.$[ext.addBefore];
        }
        this.createComponent(ext);
        this._extLength++;
      }
    }
  };

  /**
    @class

    A mixin with functions used for formatting display data.
  */
  XV.FormattingMixin = /** @lends XV.FormattingMixin# */{

    /**
      An array of data types that require special formatting in displays
    */
    formatted: ["Date", "DueDate", "Cost", "ExtendedPrice", "Hours",
      "Money", "Percent", "PurchasePrice", "Quantity", "SalesPrice",
      "UnitRatio", "Weight", "Boolean", "EffectiveDate", "ExpireDate",
      "AddressInfo"
    ],

    formatAddressInfo: function (value, view, model) {
      return XM.Address.formatShort(value);
    },

    /**
      Localize a boolean to yes/no text.

      @param {Number} Value
      @returns {String}
    */
    formatBoolean: function (value) {
      return value ? "_yes".loc() : "_no".loc();
    },

    /**
      Localize a number to cost string in the base currency.

      @param {Number} Value
      @returns {String}
    */
    formatCost: function (value) {
      return Globalize.format(value, "c" + XT.locale.costScale);
    },

    /**
      Localize a date.

      @param {Date} Date
      @returns {String}
    */
    formatDate: function (value) {
      var date = _.isDate(value) ? XT.date.applyTimezoneOffset(value, true) : false;
      return date ? Globalize.format(date, "d") : "";
    },

    /**
      Localize a date and add the class for `error` to the view if the date is before today.

      @param {Date} Date
      @param {Object} View
      @param {Object} Model
      @returns {String}
    */
    formatDueDate: function (value, view, model) {
      var today = XT.date.today(),
        date = _.isDate(value) ? XT.date.applyTimezoneOffset(value, true) : false,
        isLate = date ? (model.getValue('isActive') && XT.date.compareDate(value, today) < 1) : false;
      view.addRemoveClass("error", isLate);
      return date ? Globalize.format(date, "d") : "";
    },

    /*
      Dates greater than today are highlight as errors. Start of time dates return "Always,"

      @param {Date} Date
      @param {Object} View
      @returns {String}
    */
    formatEffectiveDate: function (value, view) {
      var date = XT.date.applyTimezoneOffset(value, true),
        isFuture = (XT.date.compareDate(date, XT.date.today()) === 1);
      view.addRemoveClass("error", isFuture);
      if (value.valueOf() === XT.date.startOfTime().valueOf()) {
        return "_always".loc();
      }
      return Globalize.format(date, "d");
    },

    /*
      Dates greater than today are highlight as errors. End of time dates return "Never,"

      @param {Date} Date
      @param {Object} View
      @returns {String}
    */
    formatExpireDate: function (value, view) {
      var date = XT.date.applyTimezoneOffset(value, true),
        isExpired = (XT.date.compareDate(date, XT.date.today()) < 1);
      view.addRemoveClass("error", isExpired);
      if (value.valueOf() === XT.date.endOfTime().valueOf()) {
        return "_never".loc();
      }
      return Globalize.format(date, "d");
    },

    /**
      Localize a number to an extended price string in the base currency.

      @param {Number} Value
      @returns {String}
    */
    formatExtendedPrice: function (value) {
      return Globalize.format(value, "c" + XT.locale.extendedPriceScale);
    },

    /**
      Localize a number to an hours string in the base currency.

      @param {Number} Value
      @returns {String}
    */
    formatHours: function (value, view) {
      view.addRemoveClass("error", value < 0);
      return Globalize.format(value, "n" + XT.locale.hoursScale);
    },

    /**
      Localize a number to a currency string using the base currency.

      @param {Number} Value
      @returns {String}
    */
    formatMoney: function (value, view) {
      view.addRemoveClass("error", value < 0);
      return Globalize.format(value, "c" + XT.locale.currencyScale);
    },

    /**
      Localize a number to a percent string.

      @param {Number} Value
      @returns {String}
    */
    formatPercent: function (value) {
      return Globalize.format(value, "p" + XT.locale.percentScale);
    },

    /**
    Localize a number to a purchase price string in the base currency.

      @param {Number} Value
      @returns {String}
    */
    formatPurchasePrice: function (value) {
      return Globalize.format(value, "c" + XT.locale.purchasePriceScale);
    },

    /**
      Localize a number to a quantity string.

      @param {Number} Value
      @returns {String}
    */
    formatQuantity: function (value, view) {
      view.addRemoveClass("error", value < 0);
      return Globalize.format(value, "n" + XT.locale.quantityScale);
    },

    /**
      Localize a number to a quantity string.

      @param {Number} Value
      @returns {String}
    */
    formatQuantityPer: function (value) {
      return Globalize.format(value, "n" + XT.locale.quantityPerScale);
    },

    /**
      Localize a number to an sales price string in the base currency.

      @param {Number} Value
      @returns {String}
    */
    formatSalesPrice: function (value) {
      return Globalize.format(value, "c" + XT.locale.salesPriceScale);
    },

    /**
      Localize a number to a unit ratio string.

      @param {Number} Value
      @returns {String}
    */
    formatUnitRatio: function (value) {
      return Globalize.format(value, "n" + XT.locale.unitRatioScale);
    },

    /**
      Localize a number to a weight string.

      @param {Number} Value
      @returns {String}
    */
    formatWeight: function (value) {
      return Globalize.format(value, "n" + XT.locale.weightScale);
    }

  };

}());