Merge pull request #1609 from xtuple/4_5_x
[xtuple] / lib / enyo-x / source / widgets / date.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, enyo:true, _:true, Globalize:true */
4
5 (function () {
6
7   /**
8     @name XV.Date
9         @class An input control used to specify a date.<br />
10         Reformats and sets a date entered either as a date type or a string.
11         If a string is not a recognizable date, sets the input to null.<br />
12         The superkind of {@link XV.DateWidget}.
13         @extends XV.Input
14    */
15   enyo.kind(
16     /** @lends XV.Date# */{
17     name: "XV.Date",
18     kind: "XV.Input",
19     published: {
20       nullValue: null,
21       nullText: ""
22     },
23     /**
24      Returns the value in the input field of the widget.
25      */
26     getValueToString: function (value) {
27       return this.$.input.value;
28     },
29     /**
30       Sets the value of date programatically.
31
32       @param {Date|String} value Can be Date or Date String
33       @param {Object} options
34      */
35     setValue: function (value, options) {
36       var nullValue = this.getNullValue();
37       if (value) {
38         if (_.isString(value)) {
39           value = this.validate(value);
40         } else {
41           value = new Date(value.valueOf()); // clone
42         }
43         if (isNaN(value.getTime())) {
44           value = nullValue;
45         }
46       } else {
47         value = nullValue;
48       }
49       XV.Input.prototype.setValue.call(this, value, options);
50     },
51     /**
52      This function takes the value entered into a DateWidget and returns
53      the correct date object for this value.  If the value does not correspond
54      to a valid date, the function returns false.
55      */
56     textToDate: function (value) {
57       var date = false,
58         daysInMilliseconds = 1000 * 60 * 60 * 24;
59
60       // Try to parse out a date given the various allowable shortcuts
61       if (value === '0' ||
62         value.indexOf('+') === 0 ||
63         value.indexOf('-') === 0) {
64         // 0 means today, +1 means tomorrow, -2 means 2 days ago, etc.
65         date = new Date(new Date().getTime() + value * daysInMilliseconds);
66       } else if (value.indexOf('#') === 0) {
67         // #40 means the fortieth day of this year, so set the month to 0
68         // and set the date accordingly. JS appropriately pushes the date
69         // into subsequent months as necessary
70         // Only allow number strings and not "" after the "#"
71         if (/^\d+$/.test(value.substring(1))) {
72           date = new Date();
73           date.setMonth(0);
74           date.setDate(value.substring(1));
75         }
76       } else if (value.length && !isNaN(value)) {
77         // A positive integer by itself means that day of this month
78         date = new Date();
79         date.setDate(value);
80
81         // This number is limited to the number of days in the current month.
82         // If the user enters 60, the date will be the last day of the month.
83         var lastDayOfMonth = new Date();
84         lastDayOfMonth.setMonth(lastDayOfMonth.getMonth() + 1);
85         lastDayOfMonth.setDate(0);
86         if (lastDayOfMonth.getTime() < date.getTime()) {
87           date = lastDayOfMonth;
88         }
89
90       // If we get to this point, we're assuming that either the user is trying to enter a date
91       // and not one of the functions. We allow the JS Date to parse the date so we can allow
92       // many formats.
93       } else if (value) {
94         // Here we are trying to leniently limit what we pass to the JS Date function since it evaluates
95         // invalid strings such as "%1678" as a date. It may still allow some crazy strings, but these will
96         // return an Invalid Date object and we will catch it later. If we need more stingent checking, we
97         // would have to lock the users into entering dates in a specific format.
98         if (/^\d+[\/\-\.]\d+[\/\-\.]\d+/.test(value)) {
99           date = new Date(value);
100         }
101       }
102
103       // Check here to see if date is actually a Date. If it is "Invalid Date", then it may
104       // pass _.isDate and still not truly be a valid Date object.
105       if (!_.isDate(date) || isNaN(date.getTime())) {
106         return false;
107       }
108
109       // TODO: This little hacky block of code was making all years < 2000 convert to 2000s
110       // Firefox does not assume that these dates are in the 2000s so there's a little trickery here
111       // if (date.getFullYear() < 2000) {
112       //   date.setYear("20" + value.substring(value.length - 2));
113       // }
114
115       // Validate
116       if (date) {
117         date = this.applyTimezoneOffset(date);
118       }
119       return date;
120     },
121     /**
122       This function strips the time from a valid date and mimics the model's
123       action of offseting the time by the timezone for the sake of what the user
124       sees populated in the input box. This action is undone just before the value
125       is set into the model.
126     */
127     applyTimezoneOffset: function (value) {
128       value.setHours(0, 0, 0, 0);
129       return XT.date.applyTimezoneOffset(value, false);
130     },
131     /**
132      This function calls textToDate, which converts the text to a valid date,
133      if possible.
134      */
135     validate: function (value) {
136       value = this.textToDate(value);
137       // if textToDate is not able to convert the entered value, it returns false
138       return ((_.isDate(value) || _.isEmpty(value)) && value !== false) ? value : false;
139     },
140     /**
141       This function puts the date in the correct format based on the locale set in Globalize.
142       It also puts back the timezoneoffset that was done in the validation function before it
143       sends the value to the model.
144      */
145     valueChanged: function (value) {
146       var nullValue = this.getNullValue();
147       if (_.isDate(value) && _.isDate(nullValue) &&
148           XT.date.compareDate(value, nullValue) === 0) {
149         value = this.getNullText();
150       } else if (value) {
151         value = XT.date.applyTimezoneOffset(value, true);
152         value = Globalize.format(value, "d");
153       } else {
154         value = "";
155       }
156       return XV.Input.prototype.valueChanged.call(this, value);
157     }
158   });
159
160   /**
161     @name XV.DateWidget
162     @class An input control consisting of fittable columns.<br />
163     Use to implement a styled input field for entering a date
164     including associated popup menu for selecting a date.<br />
165     Creates an HTML input element.
166     @extends XV.Date
167    */
168   enyo.kind(/** @lends XV.DateWidget# */{
169     name: "XV.DateWidget",
170     kind: "XV.Date",
171     classes: "xv-input xv-datewidget",
172     published: {
173       label: "",
174       showLabel: true
175     },
176     components: [
177       {kind: "FittableColumns", components: [
178         {name: "label", content: "", fit: true, classes: "xv-flexible-label"},
179         // setting the tag to "div" on the decorator
180         // so that clicks of button don't get redirected back
181         // to the input field (default behavior)
182         {kind: "onyx.InputDecorator", name: "decorator", tag: "div",
183           classes: "xv-input-decorator", components: [
184           {name: "input", kind: "onyx.Input", onchange: "inputChanged",
185             classes: "xv-subinput", onkeydown: "keyDown"},
186           {name: "icon", kind: "onyx.Icon", ontap: "iconTapped",
187             classes: "icon-calendar"}
188         ]},
189         {name: "datePickPopup", kind: "onyx.Popup", maxHeight: 400, floating: true,
190             centered: true, modal: true, components: [
191           // TODO: get rid of this inline style
192           {kind: "GTS.DatePicker", name: "datePick", style: "min-width:400px;",
193             onChange: "datePicked"}
194         ]}
195       ]}
196     ],
197     /**
198      @todo Document create method.
199      */
200     create: function () {
201       this.inherited(arguments);
202       this.labelChanged();
203       this.showLabelChanged();
204     },
205
206     /**
207      This function handles a date chosen via the
208      datepicker versus text entered into the input field.
209      */
210     datePicked: function (inSender, inEvent) {
211       var date = inEvent, options = {};
212       // mimic the human-typed behavior
213       this.applyTimezoneOffset(date);
214
215       options.silent = true;
216       this.setValue(date, options);
217       this.$.datePickPopup.hide();
218       this.$.input.focus();
219     },
220     disabledChanged: function () {
221       this.inherited(arguments);
222       this.$.label.addRemoveClass("disabled", this.getDisabled());
223     },
224     /**
225       This function handles the click of the calendar icon
226       that opens the datepicker.
227     */
228     iconTapped: function (inSender, inEvent) {
229       this.$.datePickPopup.show();
230       this.$.datePick.render();
231     },
232     /**
233      Sets the label of the Date Widget with text specified in the kind.
234      */
235     labelChanged: function () {
236       var label = (this.getLabel() || ("_" + this.attr || "").loc()) + ":";
237       this.$.label.setContent(label);
238     },
239     /**
240      Sets the visibility of the label of the date widget based on
241      the value specified in the kind.
242      */
243     showLabelChanged: function () {
244       if (this.getShowLabel()) {
245         this.$.label.show();
246       } else {
247         this.$.label.hide();
248       }
249     },
250     /**
251      Set the date value into the input field and the date picker and
252      sets the value to the model.
253      */
254     valueChanged: function (value) {
255       var dateValue = value;
256       value = XV.Date.prototype.valueChanged.call(this, value);
257       if (!this.$.input.value && this.$.input.attributes.value) {
258         // XXX workaround for incident 19171. Something deep into enyo's
259         // setters are causing the attributes value to be updated when
260         // a value is entered but not updated when the empty string is
261         // entered. Seems to only affect date widgets due to the complicated
262         // two-step of turning a inputted value into an actual date.
263         this.$.input.attributes.value = "";
264       }
265       this.$.datePick.setValue(value ? dateValue : new Date());
266       this.$.datePick.render();
267     }
268   });
269
270 }());