source: Dev/trunk/src/client/dijit/CalendarLite.js @ 513

Last change on this file since 513 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 17.8 KB
Line 
1define([
2        "dojo/_base/array", // array.forEach array.map
3        "dojo/_base/declare", // declare
4        "dojo/cldr/supplemental", // cldrSupplemental.getFirstDayOfWeek
5        "dojo/date", // date
6        "dojo/date/locale",
7        "dojo/date/stamp", // stamp.fromISOString
8        "dojo/dom", // dom.setSelectable
9        "dojo/dom-class", // domClass.contains
10        "dojo/_base/lang", // lang.getObject, lang.hitch
11        "dojo/on",
12        "dojo/sniff", // has("ie") has("webkit")
13        "dojo/string", // string.substitute
14        "./_WidgetBase",
15        "./_TemplatedMixin",
16        "dojo/text!./templates/Calendar.html",
17        "./a11yclick",  // not used directly, but template has ondijitclick in it
18        "./hccss"    // not used directly, but sets CSS class on <body>
19], function(array, declare, cldrSupplemental, date, locale, stamp, dom, domClass, lang, on, has, string, _WidgetBase, _TemplatedMixin, template){
20
21
22        // module:
23        //              dijit/CalendarLite
24
25        var CalendarLite = declare("dijit.CalendarLite", [_WidgetBase, _TemplatedMixin], {
26                // summary:
27                //              Lightweight version of Calendar widget aimed towards mobile use
28                //
29                // description:
30                //              A simple GUI for choosing a date in the context of a monthly calendar.
31                //              This widget can't be used in a form because it doesn't serialize the date to an
32                //              `<input>` field.  For a form element, use dijit/form/DateTextBox instead.
33                //
34                //              Note that the parser takes all dates attributes passed in the
35                //              [RFC 3339 format](http://www.faqs.org/rfcs/rfc3339.html), e.g. `2005-06-30T08:05:00-07:00`
36                //              so that they are serializable and locale-independent.
37                //
38                //              Also note that this widget isn't keyboard accessible; use dijit.Calendar for that
39                // example:
40                //      |       var calendar = new dijit.CalendarLite({}, dojo.byId("calendarNode"));
41                //
42                // example:
43                //      |       <div data-dojo-type="dijit/CalendarLite"></div>
44
45                // Template for main calendar
46                templateString: template,
47
48                // Template for cell for a day of the week (ex: M)
49                dowTemplateString: '<th class="dijitReset dijitCalendarDayLabelTemplate" role="columnheader" scope="col"><span class="dijitCalendarDayLabel">${d}</span></th>',
50
51                // Templates for a single date (ex: 13), and for a row for a week (ex: 20 21 22 23 24 25 26)
52                dateTemplateString: '<td class="dijitReset" role="gridcell" data-dojo-attach-point="dateCells"><span class="dijitCalendarDateLabel" data-dojo-attach-point="dateLabels"></span></td>',
53                weekTemplateString: '<tr class="dijitReset dijitCalendarWeekTemplate" role="row">${d}${d}${d}${d}${d}${d}${d}</tr>',
54
55                // value: Date
56                //              The currently selected Date, initially set to invalid date to indicate no selection.
57                value: new Date(""),
58                // TODO: for 2.0 make this a string (ISO format) rather than a Date
59
60                // datePackage: String
61                //              JavaScript namespace to find calendar routines.  If unspecified, uses Gregorian calendar routines
62                //              at dojo/date and dojo/date/locale.
63                datePackage: "",
64                //              TODO: for 2.0, replace datePackage with dateModule and dateLocalModule attributes specifying MIDs,
65                //              or alternately just get rid of this completely and tell user to use module ID remapping
66                //              via require
67
68                // dayWidth: String
69                //              How to represent the days of the week in the calendar header. See locale
70                dayWidth: "narrow",
71
72                // tabIndex: String
73                //              Order fields are traversed when user hits the tab key
74                tabIndex: "0",
75
76                // currentFocus: Date
77                //              Date object containing the currently focused date, or the date which would be focused
78                //              if the calendar itself was focused.   Also indicates which year and month to display,
79                //              i.e. the current "page" the calendar is on.
80                currentFocus: new Date(),
81
82                // Put the summary to the node with role=grid
83                _setSummaryAttr: "gridNode",
84
85                baseClass: "dijitCalendar",
86
87                _isValidDate: function(/*Date*/ value){
88                        // summary:
89                        //              Runs various tests on the value, checking that it's a valid date, rather
90                        //              than blank or NaN.
91                        // tags:
92                        //              private
93                        return value && !isNaN(value) && typeof value == "object" &&
94                                value.toString() != this.constructor.prototype.value.toString();
95                },
96
97                _getValueAttr: function(){
98                        // summary:
99                        //              Support get('value')
100
101                        // this.value is set to 1AM, but return midnight, local time for back-compat
102                        var storedVal = this._get("value");
103                        if(storedVal && !isNaN(storedVal)){
104                                var value = new this.dateClassObj(storedVal);
105                                value.setHours(0, 0, 0, 0);
106
107                                // If daylight savings pushes midnight to the previous date, fix the Date
108                                // object to point at 1am so it will represent the correct day. See #9366
109                                if(value.getDate() < storedVal.getDate()){
110                                        value = this.dateModule.add(value, "hour", 1);
111                                }
112                                return value;
113                        }else{
114                                return null;
115                        }
116                },
117
118                _setValueAttr: function(/*Date|Number*/ value, /*Boolean*/ priorityChange){
119                        // summary:
120                        //              Support set("value", ...)
121                        // description:
122                        //              Set the current date and update the UI.  If the date is disabled, the value will
123                        //              not change, but the display will change to the corresponding month.
124                        // value:
125                        //              Either a Date or the number of seconds since 1970.
126                        // tags:
127                        //              protected
128                        if(typeof value == "string"){
129                                value = stamp.fromISOString(value);
130                        }
131                        value = this._patchDate(value);
132
133                        if(this._isValidDate(value) && !this.isDisabledDate(value, this.lang)){
134                                this._set("value", value);
135
136                                // Set focus cell to the new value.   Arguably this should only happen when there isn't a current
137                                // focus point.   This will also repopulate the grid to new month/year if necessary.
138                                this.set("currentFocus", value);
139
140                                // Mark the selected date
141                                this._markSelectedDates([value]);
142
143                                if(this._created && (priorityChange || typeof priorityChange == "undefined")){
144                                        this.onChange(this.get('value'));
145                                }
146                        }else{
147                                // clear value, and mark all dates as unselected
148                                this._set("value", null);
149                                this._markSelectedDates([]);
150                        }
151                },
152
153                _patchDate: function(/*Date|Number*/ value){
154                        // summary:
155                        //              Convert Number into Date, or copy Date object.   Then, round to nearest day,
156                        //              setting to 1am to avoid issues when DST shift occurs at midnight, see #8521, #9366)
157                        if(value){
158                                value = new this.dateClassObj(value);
159                                value.setHours(1, 0, 0, 0);
160                        }
161                        return value;
162                },
163
164                _setText: function(node, text){
165                        // summary:
166                        //              This just sets the content of node to the specified text.
167                        //              Can't do "node.innerHTML=text" because of an IE bug w/tables, see #3434.
168                        // tags:
169                        //              private
170                        while(node.firstChild){
171                                node.removeChild(node.firstChild);
172                        }
173                        node.appendChild(node.ownerDocument.createTextNode(text));
174                },
175
176                _populateGrid: function(){
177                        // summary:
178                        //              Fills in the calendar grid with each day (1-31).
179                        //              Call this on creation, when moving to a new month.
180                        // tags:
181                        //              private
182
183                        var month = new this.dateClassObj(this.currentFocus);
184                        month.setDate(1);
185
186                        var firstDay = month.getDay(),
187                                daysInMonth = this.dateModule.getDaysInMonth(month),
188                                daysInPreviousMonth = this.dateModule.getDaysInMonth(this.dateModule.add(month, "month", -1)),
189                                today = new this.dateClassObj(),
190                                dayOffset = cldrSupplemental.getFirstDayOfWeek(this.lang);
191                        if(dayOffset > firstDay){
192                                dayOffset -= 7;
193                        }
194
195                        // If they didn't provide a summary, change the default summary to match with the new month
196                        if(!this.summary){
197                                var monthNames = this.dateLocaleModule.getNames('months', 'wide', 'standAlone', this.lang, month)
198                                this.gridNode.setAttribute("summary", monthNames[month.getMonth()]);
199                        }
200
201                        // Mapping from date (as specified by number returned from Date.valueOf()) to corresponding <td>
202                        this._date2cell = {};
203
204                        // Iterate through dates in the calendar and fill in date numbers and style info
205                        array.forEach(this.dateCells, function(template, idx){
206                                var i = idx + dayOffset;
207                                var date = new this.dateClassObj(month),
208                                        number, clazz = "dijitCalendar", adj = 0;
209
210                                if(i < firstDay){
211                                        number = daysInPreviousMonth - firstDay + i + 1;
212                                        adj = -1;
213                                        clazz += "Previous";
214                                }else if(i >= (firstDay + daysInMonth)){
215                                        number = i - firstDay - daysInMonth + 1;
216                                        adj = 1;
217                                        clazz += "Next";
218                                }else{
219                                        number = i - firstDay + 1;
220                                        clazz += "Current";
221                                }
222
223                                if(adj){
224                                        date = this.dateModule.add(date, "month", adj);
225                                }
226                                date.setDate(number);
227
228                                if(!this.dateModule.compare(date, today, "date")){
229                                        clazz = "dijitCalendarCurrentDate " + clazz;
230                                }
231
232                                if(this.isDisabledDate(date, this.lang)){
233                                        clazz = "dijitCalendarDisabledDate " + clazz;
234                                        template.setAttribute("aria-disabled", "true");
235                                }else{
236                                        clazz = "dijitCalendarEnabledDate " + clazz;
237                                        template.removeAttribute("aria-disabled");
238                                        template.setAttribute("aria-selected", "false");
239                                }
240
241                                var clazz2 = this.getClassForDate(date, this.lang);
242                                if(clazz2){
243                                        clazz = clazz2 + " " + clazz;
244                                }
245
246                                template.className = clazz + "Month dijitCalendarDateTemplate";
247
248                                // Each cell has an associated integer value representing it's date
249                                var dateVal = date.valueOf();
250                                this._date2cell[dateVal] = template;
251                                template.dijitDateValue = dateVal;
252
253                                // Set Date string (ex: "13").
254                                this._setText(this.dateLabels[idx], date.getDateLocalized ? date.getDateLocalized(this.lang) : date.getDate());
255                        }, this);
256                },
257
258                _populateControls: function(){
259                        // summary:
260                        //              Fill in localized month, and prev/current/next years
261                        // tags:
262                        //              protected
263
264                        var month = new this.dateClassObj(this.currentFocus);
265                        month.setDate(1);
266
267                        // set name of this month
268                        this.monthWidget.set("month", month);
269
270                        var y = month.getFullYear() - 1;
271                        var d = new this.dateClassObj();
272                        array.forEach(["previous", "current", "next"], function(name){
273                                d.setFullYear(y++);
274                                this._setText(this[name + "YearLabelNode"],
275                                        this.dateLocaleModule.format(d, {selector: 'year', locale: this.lang}));
276                        }, this);
277                },
278
279                goToToday: function(){
280                        // summary:
281                        //              Sets calendar's value to today's date
282                        this.set('value', new this.dateClassObj());
283                },
284
285                constructor: function(params /*===== , srcNodeRef =====*/){
286                        // summary:
287                        //              Create the widget.
288                        // params: Object|null
289                        //              Hash of initialization parameters for widget, including scalar values (like title, duration etc.)
290                        //              and functions, typically callbacks like onClick.
291                        //              The hash can contain any of the widget's properties, excluding read-only properties.
292                        // srcNodeRef: DOMNode|String?
293                        //              If a srcNodeRef (DOM node) is specified, replace srcNodeRef with my generated DOM tree
294
295                        this.dateModule = params.datePackage ? lang.getObject(params.datePackage, false) : date;
296                        this.dateClassObj = this.dateModule.Date || Date;
297                        this.dateLocaleModule = params.datePackage ? lang.getObject(params.datePackage + ".locale", false) : locale;
298                },
299
300                _createMonthWidget: function(){
301                        // summary:
302                        //              Creates the drop down button that displays the current month and lets user pick a new one
303
304                        return CalendarLite._MonthWidget({
305                                id: this.id + "_mddb",
306                                lang: this.lang,
307                                dateLocaleModule: this.dateLocaleModule
308                        }, this.monthNode);
309                },
310
311                buildRendering: function(){
312                        // Markup for days of the week (referenced from template)
313                        var d = this.dowTemplateString,
314                                dayNames = this.dateLocaleModule.getNames('days', this.dayWidth, 'standAlone', this.lang),
315                                dayOffset = cldrSupplemental.getFirstDayOfWeek(this.lang);
316                        this.dayCellsHtml = string.substitute([d, d, d, d, d, d, d].join(""), {d: ""}, function(){
317                                return dayNames[dayOffset++ % 7];
318                        });
319
320                        // Markup for dates of the month (referenced from template), but without numbers filled in
321                        var r = string.substitute(this.weekTemplateString, {d: this.dateTemplateString});
322                        this.dateRowsHtml = [r, r, r, r, r, r].join("");
323
324                        // Instantiate from template.
325                        // dateCells and dateLabels arrays filled when _Templated parses my template.
326                        this.dateCells = [];
327                        this.dateLabels = [];
328                        this.inherited(arguments);
329
330                        dom.setSelectable(this.domNode, false);
331
332                        var dateObj = new this.dateClassObj(this.currentFocus);
333
334                        this.monthWidget = this._createMonthWidget();
335
336                        this.set('currentFocus', dateObj, false);       // draw the grid to the month specified by currentFocus
337                },
338
339                postCreate: function(){
340                        this.inherited(arguments);
341                        this._connectControls();
342                },
343
344                _connectControls: function(){
345                        // summary:
346                        //              Set up connects for increment/decrement of months/years
347                        // tags:
348                        //              protected
349
350                        var connect = lang.hitch(this, function(nodeProp, part, amount){
351                                return on(this[nodeProp], "click", lang.hitch(this, function(){
352                                        this._setCurrentFocusAttr(this.dateModule.add(this.currentFocus, part, amount));
353                                }));
354                        });
355
356                        this.own(
357                                connect("incrementMonth", "month", 1),
358                                connect("decrementMonth", "month", -1),
359                                connect("nextYearLabelNode", "year", 1),
360                                connect("previousYearLabelNode", "year", -1)
361                        );
362                },
363
364                _setCurrentFocusAttr: function(/*Date*/ date, /*Boolean*/ forceFocus){
365                        // summary:
366                        //              If the calendar currently has focus, then focuses specified date,
367                        //              changing the currently displayed month/year if necessary.
368                        //              If the calendar doesn't have focus, updates currently
369                        //              displayed month/year, and sets the cell that will get focus
370                        //              when Calendar is focused.
371                        // forceFocus:
372                        //              If true, will focus() the cell even if calendar itself doesn't have focus
373
374                        var oldFocus = this.currentFocus,
375                                oldCell = this._getNodeByDate(oldFocus);
376                        date = this._patchDate(date);
377
378                        this._set("currentFocus", date);
379
380                        // If the focus is on a different month than the current calendar month, switch the displayed month.
381                        // Also will populate the grid initially, on Calendar creation.
382                        if(!this._date2cell || this.dateModule.difference(oldFocus, date, "month") != 0){
383                                this._populateGrid();
384                                this._populateControls();
385                                this._markSelectedDates([this.value]);
386                        }
387
388                        // set tabIndex=0 on new cell, and focus it (but only if Calendar itself is focused)
389                        var newCell = this._getNodeByDate(date);
390                        newCell.setAttribute("tabIndex", this.tabIndex);
391                        if(this.focused || forceFocus){
392                                newCell.focus();
393                        }
394
395                        // set tabIndex=-1 on old focusable cell
396                        if(oldCell && oldCell != newCell){
397                                if(has("webkit")){    // see #11064 about webkit bug
398                                        oldCell.setAttribute("tabIndex", "-1");
399                                }else{
400                                        oldCell.removeAttribute("tabIndex");
401                                }
402                        }
403                },
404
405                focus: function(){
406                        // summary:
407                        //              Focus the calendar by focusing one of the calendar cells
408                        this._setCurrentFocusAttr(this.currentFocus, true);
409                },
410
411                _onDayClick: function(/*Event*/ evt){
412                        // summary:
413                        //              Handler for day clicks, selects the date if appropriate
414                        // tags:
415                        //              protected
416                        evt.stopPropagation();
417                        evt.preventDefault();
418                        for(var node = evt.target; node && !node.dijitDateValue; node = node.parentNode){
419                                ;
420                        }
421                        if(node && !domClass.contains(node, "dijitCalendarDisabledDate")){
422                                this.set('value', node.dijitDateValue);
423                        }
424                },
425
426                _getNodeByDate: function(/*Date*/ value){
427                        // summary:
428                        //              Returns the cell corresponding to the date, or null if the date is not within the currently
429                        //              displayed month.
430                        value = this._patchDate(value);
431                        return value && this._date2cell ? this._date2cell[value.valueOf()] : null;
432                },
433
434                _markSelectedDates: function(/*Date[]*/ dates){
435                        // summary:
436                        //              Marks the specified cells as selected, and clears cells previously marked as selected.
437                        //              For CalendarLite at most one cell is selected at any point, but this allows an array
438                        //              for easy subclassing.
439
440                        // Function to mark a cell as selected or unselected
441                        function mark(/*Boolean*/ selected, /*DomNode*/ cell){
442                                domClass.toggle(cell, "dijitCalendarSelectedDate", selected);
443                                cell.setAttribute("aria-selected", selected ? "true" : "false");
444                        }
445
446                        // Clear previously selected cells.
447                        array.forEach(this._selectedCells || [], lang.partial(mark, false));
448
449                        // Mark newly selected cells.  Ignore dates outside the currently displayed month.
450                        this._selectedCells = array.filter(array.map(dates, this._getNodeByDate, this), function(n){
451                                return n;
452                        });
453                        array.forEach(this._selectedCells, lang.partial(mark, true));
454                },
455
456                onChange: function(/*Date*/ /*===== date =====*/){
457                        // summary:
458                        //              Called only when the selected date has changed
459                },
460
461                isDisabledDate: function(/*===== dateObject, locale =====*/){
462                        // summary:
463                        //              May be overridden to disable certain dates in the calendar e.g. `isDisabledDate=dojo.date.locale.isWeekend`
464                        // dateObject: Date
465                        // locale: String?
466                        // tags:
467                        //              extension
468                        /*=====
469                         return false; // Boolean
470                         =====*/
471                },
472
473                getClassForDate: function(/*===== dateObject, locale =====*/){
474                        // summary:
475                        //              May be overridden to return CSS classes to associate with the date entry for the given dateObject,
476                        //              for example to indicate a holiday in specified locale.
477                        // dateObject: Date
478                        // locale: String?
479                        // tags:
480                        //              extension
481
482                        /*=====
483                         return ""; // String
484                         =====*/
485                }
486        });
487
488        CalendarLite._MonthWidget = declare("dijit.CalendarLite._MonthWidget", _WidgetBase, {
489                // summary:
490                //              Displays name of current month padded to the width of the month
491                //              w/the longest name, so that changing months doesn't change width.
492                //
493                //              Create as:
494                // |    new Calendar._MonthWidget({
495                // |                    lang: ...,
496                // |                    dateLocaleModule: ...
497                // |            })
498
499                _setMonthAttr: function(month){
500                        // summary:
501                        //              Set the current month to display as a label
502                        var monthNames = this.dateLocaleModule.getNames('months', 'wide', 'standAlone', this.lang, month),
503                                spacer =
504                                        (has("ie") == 6 ? "" : "<div class='dijitSpacer'>" +
505                                                array.map(monthNames,function(s){
506                                                        return "<div>" + s + "</div>";
507                                                }).join("") + "</div>");
508
509                        // Set name of current month and also fill in spacer element with all the month names
510                        // (invisible) so that the maximum width will affect layout.   But not on IE6 because then
511                        // the center <TH> overlaps the right <TH> (due to a browser bug).
512                        this.domNode.innerHTML =
513                                spacer +
514                                        "<div class='dijitCalendarMonthLabel dijitCalendarCurrentMonthLabel'>" +
515                                        monthNames[month.getMonth()] + "</div>";
516                }
517        });
518
519        return CalendarLite;
520});
Note: See TracBrowser for help on using the repository browser.