Merge pull request #1609 from xtuple/4_5_x
[xtuple] / enyo-client / application / source / widgets / money.js
1 /*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, trailing:true, white:true */
3 /*global XT:true, XV:true, XM:true, Globalize:true, enyo:true, _:true */
4
5 (function () {
6
7   /**
8     The widget is created with an attr value that is an object composed of two key-values pairs.
9     These pairs include the amount (localValue or baseValue) and the currency attribute strings from the model.
10     The workspace replaces the attribute name string value
11     The localValue key should be used if the model stores the local currency and the baseValue key should
12     be used if the models stores the base currency.
13     The effective attribute should contain the date by which calculations
14    */
15   enyo.kind({
16     name: "XV.MoneyWidget",
17     kind: "XV.NumberWidget",
18     classes: "xv-moneywidget",
19     published: {
20       scale: XT.MONEY_SCALE,
21       localValue: null, // {Number} the number in the user field
22       baseValue: null, // {Number} the amount in the base currency
23       effective: null,
24       currency: null,
25       currencyDisabled: false,
26       currencyShowing: true,
27       disabled: false,
28       localMode: true,
29       isEditableProperty: "localValue" // The property mapped to an attribute that checks whether editbale
30     },
31     handlers: {
32       onValueChange: "pickerChanged" // intercept picker events
33     },
34     maxlength: 12,
35     components: [
36       {kind: "FittableColumns", components: [
37         {name: "label", content: "", classes: "xv-label"},
38         {kind: "onyx.InputDecorator", classes: "xv-input-decorator",
39           components: [
40           {name: "input", kind: "onyx.Input",
41             onchange: "inputChanged", onkeydown: "keyDown"}
42         ]},
43         {name: "picker", kind: "XV.CurrencyPicker", showLabel: false}
44       ]},
45       {kind: "FittableColumns", name: "basePanel", showing: false,
46         components: [
47         {name: "spacer", content: "", classes: "xv-label"},
48         {kind: "onyx.InputDecorator", classes: "xv-input-decorator",
49           components: [
50           {name: "baseAmountLabel", classes: "xv-money-label"}
51         ]},
52         {kind: "onyx.InputDecorator", classes: "xv-input-decorator, xv-currency-label",
53           components: [
54           {name: "baseCurrencyLabel"}
55         ]}
56       ]}
57     ],
58
59     /**
60       Set the base price into the base amount label
61     */
62     baseValueChanged: function () {
63       var localMode = this.getLocalMode(),
64         baseValue,
65         scale,
66         content;
67       if (localMode) {
68         baseValue = this.getBaseValue();
69         scale = this.getScale();
70         content = baseValue || baseValue === 0 ?
71           Globalize.format(baseValue, "n" + scale) : "";
72         this.$.baseAmountLabel.setContent(content);
73       } else {
74         this.recalculate();
75       }
76     },
77
78     /**
79       If the effective date is provided, the fixedRate is set and the
80       base panel is shown.
81      */
82     create: function () {
83       this.inherited(arguments);
84
85       var baseCurrency = XT.baseCurrency();
86       if (!baseCurrency) {
87         this.bubble("onNotify", {message: "_baseCurrencyMustBeSet".loc()});
88       }
89
90       this.setEffective(new Date());
91       this.setCurrency(baseCurrency);
92       this.setLocalValue(0);
93       this.$.baseCurrencyLabel.setContent(baseCurrency.get('abbreviation'));
94
95       // the currency picker may be disabled or hidden on creation in certain situations
96       this.currencyDisabledChanged();
97       this.currencyShowingChanged();
98
99       // this is for styling of the picker since the PickerWidget has a built in
100       // input decorator
101       this.$.picker.$.inputWrapper.removeClass("onyx-input-decorator");
102
103       // set the "mode" of this widget, whether or not it directly saves the local
104       // value to the model, or if it converts it to and from the base value.
105       this.setLocalMode(_.has(this.getAttr(), "localValue"));
106     },
107
108     clear: function (options) {
109       this.setValue({localValue: null}, options);
110     },
111
112     currencyChanged: function () {
113       this.$.picker.setValue(this.getCurrency(), {silent: true});
114       this.recalculate();
115     },
116
117     currencyDisabledChanged: function () {
118       this.$.picker.setDisabled(this.getCurrencyDisabled());
119     },
120
121     currencyShowingChanged: function () {
122       this.$.picker.setShowing(this.getCurrencyShowing());
123     },
124
125     /**
126       This setDisabled function is all or nothing for both widgets
127       depending on value
128     */
129     disabledChanged: function () {
130       var disabled = this.getDisabled(),
131         currencyDisabled = this.getCurrencyDisabled();
132       this.$.input.setDisabled(disabled);
133       this.$.picker.setDisabled(disabled || currencyDisabled);
134       this.$.label.addRemoveClass("disabled", this.getDisabled());
135     },
136
137     /**
138     @todo Document the labelChanged method.
139     */
140     labelChanged: function () {
141       var attr = this.getAttr(),
142         valueAttr = attr.localValue || attr.baseValue;
143       var label = (this.getLabel() || ("_" + valueAttr || "").loc());
144       this.$.label.setContent(label + ":");
145     },
146
147     effectiveChanged: function () {
148       this.recalculate();
149     },
150
151     localValueChanged: function () {
152       this.valueChanged(this.getLocalValue()); // forward to XV.Number default for formatting
153       this.recalculate();
154     },
155
156     recalculate: function () {
157       var localMode = this.getLocalMode(),
158         value = localMode ? this.getLocalValue() : this.getBaseValue(),
159         request = localMode ? "toBase" : "fromBase",
160         setter = localMode ? "setBaseValue" : "setLocalValue",
161         currency = this.getCurrency(),
162         effective = this.getEffective(),
163         showing = localMode && _.isDate(effective) && currency &&
164           !currency.get("isBase"),
165         options = {},
166         that = this,
167         i;
168
169       if (!currency || !effective) { return; }
170
171       // Keep track of requests, we'll ignore stale ones
172       this._counter = _.isNumber(this._counter) ? this._counter + 1 : 0;
173       i = this._counter;
174
175       if (_.isNumber(value)) {
176         if (currency.get("isBase")) {
177          // we're at base, so just set the fields with the base value we have
178           this[setter](value);
179         } else {
180           options.success = function (calcValue) {
181             // I only smell freshness
182             if (i < that._counter) { return; }
183             that[setter](calcValue);
184           };
185           currency[request](value, effective, options);
186         }
187       } else { // amount is null
188         that[setter](null);
189       }
190
191       this.$.basePanel.setShowing(showing);
192     },
193
194     setDisabled: function (isDisabled) {
195       this.inherited(arguments);
196       this.$.picker.setDisabled(isDisabled || this.getCurrencyDisabled());
197     },
198
199     /**
200       This setValue function handles a value which is an
201       object potentially consisting of multiple key/value pairs for the
202       amount and currency controls.
203
204       @param {Object} Value
205       @param {Object} [value.currency] Currency
206       @param {Date} [value.effective] Effective date
207       @param {Number} [value.localValue] Local value
208       @param {Number} [value.baseValue] Base value
209     */
210     setValue: function (value, options) {
211       options = options || {};
212       var attr = this.getAttr(),
213         changed = {},
214         keys = _.keys(value),
215         key,
216         set,
217         i;
218
219       // Loop through the properties and update calling
220       // appropriate "set" functions and add to "changed"
221       // object if applicable
222       for (i = 0; i < keys.length; i++) {
223         key = keys[i];
224         set = 'set' + key.slice(0, 1).toUpperCase() + key.slice(1);
225         this[set](value[key]);
226         if (attr[key]) {
227           changed[attr[key]] = value[key];
228         }
229       }
230
231       // Bubble changes if applicable
232       if (!_.isEmpty(changed) && !options.silent) {
233         this.doValueChange({value: changed});
234       }
235     },
236
237     inputChanged: function (inSender, inEvent) {
238       var input = this.$.input.getValue(),
239         value = this.validate(input),
240         prop = this.getLocalMode() ? 'localValue' : 'baseValue',
241         obj = {};
242       if (value !== false) {
243         // is valid!
244         obj[prop] = value;
245         this.setValue(obj);
246       } else {
247         // is invalid!
248         obj[prop] = null;
249         this.setValue(obj);
250         this.valueChanged("");
251       }
252     },
253
254     /**
255       Intercept the valueChanged event from picker.
256     */
257     pickerChanged: function (inSender, inEvent) {
258       var value;
259       if (inEvent.originator.name === "picker") {
260         value = XM.currencies.get(inEvent.value);
261         this.setValue({currency: value});
262         return true;
263       }
264     }
265   });
266
267 }());