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