Merge pull request #1 from shackbarth/keith1
[xtuple] / lib / enyo-x / source / core.js
1 /*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
2 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
3 trailing:true, white:true, strict: false*/
4 /*global XV:true, XM:true, _:true, onyx:true, enyo:true, document:true, XT:true, Globalize:true */
5
6 (function () {
7
8   /**
9     XV is the global namespace for all the "xTuple Views" defined in
10     enyo-x and elsewhere
11
12     @namespace XV
13    */
14   XV = {};
15   XV._modelCaches = {};
16   XV._modelLists = {};
17   XV._modelWorkspaces = {};
18
19   // Class methods
20   enyo.mixin(XV, /** @lends XV */{
21
22     KEY_UP: 38,
23     KEY_DOWN: 40,
24     KEY_TAB: 9,
25     KEY_ENTER: 13,
26
27     /**
28       Key/value mapping of widget class names that correspond with object definitions
29       to implement a corresponding editor widget.
30     */
31     widgetTypeMap: {
32       Cost: "XV.Cost",
33       Date: "XV.DateWidget",
34       DueDate: "XV.DateWidget",
35       ExtendedPrice: {
36         kind: "XV.MoneyWidget",
37         scale: XT.EXTENDED_PRICE_SCALE
38       },
39       Money: {
40         kind: "XV.MoneyWidget",
41         scale: XT.MONEY_SCALE
42       },
43       Number: "XV.NumberWidget",
44       PurchasePrice: {
45         kind: "XV.MoneyWidget",
46         scale: XT.PURCHASE_PRICE_SCALE
47       },
48       Quantity: "XV.QuantityWidget",
49       QuantityPer: "XV.QuantityPerWidget",
50       SalesPrice: {
51         kind: "XV.MoneyWidget",
52         scale: XT.SALES_PRICE_SCALE
53       },
54       String: "XV.InputWidget",
55       Unit: "XV.UnitPicker",
56       UnitRatio: "XV.UnitRatioWidget",
57       UserAccountRelation: "XV.UserAccountWidget",
58       Weight: "XV.WeightWidget"
59     },
60
61     /**
62       Accepts a model and an attribute and returns a standard widget definition
63       mapped to the attribute.
64
65       *Warning* This implementation is incomplete. Widgets that reference object
66       based attributes are not handled well and need to be refactored.
67
68       @param {String} Model class name
69       @param {String} Attribute name
70     */
71     getEditor: function (model, attr) {
72       var Klass = XT.getObjectByName(model),
73         type = Klass.getType(attr),
74         widget = this.widgetTypeMap[type];
75
76       // Handle normal widgets
77       if (_.isString(widget)) {
78         widget = {
79           kind: widget,
80           attr: attr
81         };
82
83       // Handle widgets with complex attributes
84       } else if (widget.kind === "XV.MoneyWidget") {
85         widget.localValue = attr;
86       }
87
88       return widget;
89     },
90
91     /**
92       Add component or array of component view(s) to a view class that
93       has implemented the `ExtensionsMixin`.
94
95       Examples of classes that support extensions are:
96         * `Workspace`
97         * `List`
98         * `ParameterWidget`
99
100       @param {String} Class name
101       @param {Object|Array} Component(s)
102     */
103     appendExtension: function (workspace, extension) {
104       var Klass = XT.getObjectByName(workspace),
105         extensions = Klass.prototype.extensions || [];
106       if (!_.isArray(extension)) {
107         extension = [extension];
108       }
109       Klass.prototype.extensions = extensions.concat(extension);
110     },
111
112     /**
113       Helper function for enyo unit testing
114
115       @param expected
116       @param actual
117       @param {String} message
118          Only displayed in the case of a failed test
119       @return {String} Per enyo's conventions, the empty string means the test is passed.
120      */
121     applyTest: function (expected, actual, message) {
122       if (expected === actual) {
123         return "";
124       } else {
125         if (message) {
126           message = ". " + message;
127         } else {
128           message = ".";
129         }
130         return "Expected " + expected + ", saw " + actual + message;
131       }
132     },
133
134     /**
135       The javascript download method avoids target = "_blank" and the need to
136       reload the app to see the new client. Pop up blockers should not be an issue.
137       @See: http://stackoverflow.com/questions/3749231/download-file-using-javascript-jquery/3749395#3749395
138
139       @param {String} The URL for the download route.
140     */
141     downloadURL: function (url) {
142       var hiddenIFrameID = 'hiddenDownloader',
143         iframe = document.getElementById(hiddenIFrameID);
144
145       if (iframe === null) {
146         iframe = document.createElement('iframe');
147         iframe.id = hiddenIFrameID;
148         iframe.style.display = 'none';
149         document.body.appendChild(iframe);
150       }
151
152       iframe.src = url;
153     },
154
155     getCache: function (recordType) {
156       return XV._modelCaches[recordType];
157     },
158
159     getList: function (recordType) {
160       return XV._modelLists[recordType];
161     },
162
163     getWorkspace: function (recordType) {
164       return XV._modelWorkspaces[recordType];
165     },
166
167     /*
168       Is the ancestor a superkind (or supersuperkind, etc.) of the object?
169
170       @param {Object} intantiated enyo kind
171         You can use Kind.prototype if that's what you have to work with.
172       @param {String} ancestor kind name
173     */
174     inheritsFrom: function (object, ancestor) {
175       if (!object || !object.ctor) {
176         return false;
177       }
178       while (object.kindName !== 'enyo.Object') {
179         if (object.ctor.prototype.base.prototype.kindName === ancestor) {
180           return true;
181         }
182         object = object.ctor.prototype.base.prototype;
183       }
184     },
185
186     registerModelCache: function (recordType, cache) {
187       XV._modelCaches[recordType] = cache;
188     },
189
190     registerModelList: function (recordType, list) {
191       XV._modelLists[recordType] = list;
192     },
193
194     registerModelWorkspace: function (recordType, workspace) {
195       XV._modelWorkspaces[recordType] = workspace;
196     }
197
198   });
199
200   /**
201     @class
202
203     A mixin that allows the components of a class to be extended.
204   */
205   XV.ExtensionsMixin = {
206     extensions: null,
207
208     /**
209       This function should be run in the create function of a class
210       using this mixin. It will add any extensions to the class at run time.
211       @parameter {Boolean} forceDeferred allows us to set a defer attribute
212         on the extension so that it will not get processed during the usual
213         processExtensions calls in the create. If you need to put an extension
214         in a subkind of workspace requires the subkind's create() function
215         to have already run, put a processExtensions(true) at the end
216         of that subkind create() function.
217     */
218     processExtensions: function (forceDeferred) {
219       var extensions = this.extensions || [],
220         ext,
221         containerString,
222         container,
223         i;
224
225       if (this._extLength === undefined) {
226         this._extLength = 0;
227       }
228       if (this._extLength === extensions.length) { return; }
229       for (i = 0; i < extensions.length; i++) {
230         ext = _.clone(this.extensions[i]);
231
232         if (ext.defer !== forceDeferred) {
233           // the workspace is not ready to add this extension yet,
234           // or, it's probably been added already
235           continue;
236         }
237         // Resolve name of container to the instance
238         if (_.isString(ext.container)) {
239           containerString = ext.container;
240           container = this;
241           while (containerString.indexOf(".") >= 0) {
242             container = container.$[containerString.substring(0, containerString.indexOf("."))];
243             containerString = containerString.substring(containerString.indexOf(".") + 1);
244           }
245           if (container) {
246             // avoid a crash if the asked-for container doesn't exist
247             ext.container = container.$[containerString];
248           } else {
249             XT.log("Requested container", ext.container, "not found");
250             return;
251           }
252         }
253         // Resolve `addBefore`
254         if (_.isString(ext.addBefore)) {
255           ext.addBefore = this.$[ext.addBefore];
256         }
257         this.createComponent(ext);
258         this._extLength++;
259       }
260     }
261   };
262
263   /**
264     @class
265
266     A mixin with functions used for formatting display data.
267   */
268   XV.FormattingMixin = /** @lends XV.FormattingMixin# */{
269
270     /**
271       An array of data types that require special formatting in displays
272     */
273     formatted: ["Date", "DueDate", "Cost", "ExtendedPrice", "Hours",
274       "Money", "Percent", "PurchasePrice", "Quantity", "SalesPrice",
275       "UnitRatio", "Weight", "Boolean", "EffectiveDate", "ExpireDate",
276       "AddressInfo"
277     ],
278
279     formatAddressInfo: function (value, view, model) {
280       return XM.Address.formatShort(value);
281     },
282
283     /**
284       Localize a boolean to yes/no text.
285
286       @param {Number} Value
287       @returns {String}
288     */
289     formatBoolean: function (value) {
290       return value ? "_yes".loc() : "_no".loc();
291     },
292
293     /**
294       Localize a number to cost string in the base currency.
295
296       @param {Number} Value
297       @returns {String}
298     */
299     formatCost: function (value) {
300       return Globalize.format(value, "c" + XT.locale.costScale);
301     },
302
303     /**
304       Localize a date.
305
306       @param {Date} Date
307       @returns {String}
308     */
309     formatDate: function (value) {
310       var date = _.isDate(value) ? XT.date.applyTimezoneOffset(value, true) : false;
311       return date ? Globalize.format(date, "d") : "";
312     },
313
314     /**
315       Localize a date and add the class for `error` to the view if the date is before today.
316
317       @param {Date} Date
318       @param {Object} View
319       @param {Object} Model
320       @returns {String}
321     */
322     formatDueDate: function (value, view, model) {
323       var today = XT.date.today(),
324         date = _.isDate(value) ? XT.date.applyTimezoneOffset(value, true) : false,
325         isLate = date ? (model.getValue('isActive') && XT.date.compareDate(value, today) < 1) : false;
326       view.addRemoveClass("error", isLate);
327       return date ? Globalize.format(date, "d") : "";
328     },
329
330     /*
331       Dates greater than today are highlight as errors. Start of time dates return "Always,"
332
333       @param {Date} Date
334       @param {Object} View
335       @returns {String}
336     */
337     formatEffectiveDate: function (value, view) {
338       var date = XT.date.applyTimezoneOffset(value, true),
339         isFuture = (XT.date.compareDate(date, XT.date.today()) === 1);
340       view.addRemoveClass("error", isFuture);
341       if (value.valueOf() === XT.date.startOfTime().valueOf()) {
342         return "_always".loc();
343       }
344       return Globalize.format(date, "d");
345     },
346
347     /*
348       Dates greater than today are highlight as errors. End of time dates return "Never,"
349
350       @param {Date} Date
351       @param {Object} View
352       @returns {String}
353     */
354     formatExpireDate: function (value, view) {
355       var date = XT.date.applyTimezoneOffset(value, true),
356         isExpired = (XT.date.compareDate(date, XT.date.today()) < 1);
357       view.addRemoveClass("error", isExpired);
358       if (value.valueOf() === XT.date.endOfTime().valueOf()) {
359         return "_never".loc();
360       }
361       return Globalize.format(date, "d");
362     },
363
364     /**
365       Localize a number to an extended price string in the base currency.
366
367       @param {Number} Value
368       @returns {String}
369     */
370     formatExtendedPrice: function (value) {
371       return Globalize.format(value, "c" + XT.locale.extendedPriceScale);
372     },
373
374     /**
375       Localize a number to an hours string in the base currency.
376
377       @param {Number} Value
378       @returns {String}
379     */
380     formatHours: function (value, view) {
381       view.addRemoveClass("error", value < 0);
382       return Globalize.format(value, "n" + XT.locale.hoursScale);
383     },
384
385     /**
386       Localize a number to a currency string using the base currency.
387
388       @param {Number} Value
389       @returns {String}
390     */
391     formatMoney: function (value, view) {
392       view.addRemoveClass("error", value < 0);
393       return Globalize.format(value, "c" + XT.locale.currencyScale);
394     },
395
396     /**
397       Localize a number to a percent string.
398
399       @param {Number} Value
400       @returns {String}
401     */
402     formatPercent: function (value) {
403       return Globalize.format(value, "p" + XT.locale.percentScale);
404     },
405
406     /**
407     Localize a number to a purchase price string in the base currency.
408
409       @param {Number} Value
410       @returns {String}
411     */
412     formatPurchasePrice: function (value) {
413       return Globalize.format(value, "c" + XT.locale.purchasePriceScale);
414     },
415
416     /**
417       Localize a number to a quantity string.
418
419       @param {Number} Value
420       @returns {String}
421     */
422     formatQuantity: function (value, view) {
423       view.addRemoveClass("error", value < 0);
424       return Globalize.format(value, "n" + XT.locale.quantityScale);
425     },
426
427     /**
428       Localize a number to a quantity string.
429
430       @param {Number} Value
431       @returns {String}
432     */
433     formatQuantityPer: function (value) {
434       return Globalize.format(value, "n" + XT.locale.quantityPerScale);
435     },
436
437     /**
438       Localize a number to an sales price string in the base currency.
439
440       @param {Number} Value
441       @returns {String}
442     */
443     formatSalesPrice: function (value) {
444       return Globalize.format(value, "c" + XT.locale.salesPriceScale);
445     },
446
447     /**
448       Localize a number to a unit ratio string.
449
450       @param {Number} Value
451       @returns {String}
452     */
453     formatUnitRatio: function (value) {
454       return Globalize.format(value, "n" + XT.locale.unitRatioScale);
455     },
456
457     /**
458       Localize a number to a weight string.
459
460       @param {Number} Value
461       @returns {String}
462     */
463     formatWeight: function (value) {
464       return Globalize.format(value, "n" + XT.locale.weightScale);
465     }
466
467   };
468
469 }());