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