Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit e6b1d23

Browse files
committed
feat(datepicker): floating calendar panel for date picker.
1 parent cc10fa9 commit e6b1d23

File tree

3 files changed

+130
-6
lines changed

3 files changed

+130
-6
lines changed

src/components/calendar/dateLocaleProvider.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777

7878
window.$locale = $locale;
7979

80+
// TODO(jelbourn): Freeze this object.
8081
return {
8182
months: this.months || $locale.DATETIME_FORMATS.MONTH,
8283
shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,

src/components/calendar/datePicker.js

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,58 @@
1212
function datePickerDirective() {
1313
return {
1414
template:
15-
'<div class="md-date-picker">' +
16-
'<input> <br>' +
17-
'<md-calendar ng-model="ctrl.date"></md-calendar>' +
18-
'</div>',
15+
'<input><button type="button" ng-click="ctrl.openCalendarPane()">📅</button>' +
16+
'<div class="md-date-calendar-pane">' +
17+
'<md-calendar ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen"></md-calendar>' +
18+
'</div>',
19+
// <md-calendar ng-model="ctrl.date"></md-calendar>
1920
require: ['ngModel', 'mdDatePicker'],
2021
scope: {},
2122
controller: DatePickerCtrl,
2223
controllerAs: 'ctrl',
2324
link: function(scope, element, attr, controllers) {
2425
var ngModelCtrl = controllers[0];
2526
var mdDatePickerCtrl = controllers[1];
27+
2628
mdDatePickerCtrl.configureNgModel(ngModelCtrl);
2729
}
2830
};
2931
}
3032

31-
function DatePickerCtrl($scope, $element, $$mdDateLocale, $$mdDateUtil) {
33+
/**
34+
* Controller for md-date-picker.
35+
*
36+
* @ngInject @constructor
37+
*/
38+
function DatePickerCtrl($scope, $element, $compile, $timeout, $mdConstant, $mdUtil,
39+
$$mdDateLocale, $$mdDateUtil) {
40+
/** @final */
41+
this.$compile = $compile;
42+
43+
/** @final */
44+
this.$timeout = $timeout;
45+
3246
/** @final */
3347
this.dateLocale = $$mdDateLocale;
3448

3549
/** @final */
3650
this.dateUtil = $$mdDateUtil;
3751

52+
/** @final */
53+
this.$mdConstant = $mdConstant;
54+
55+
/* @final */
56+
this.$mdUtil = $mdUtil;
57+
3858
/** @type {!angular.NgModelController} */
3959
this.ngModelCtrl = null;
4060

4161
/** @type {HTMLInputElement} */
4262
this.inputElement = $element[0].querySelector('input');
4363

64+
/** @type {HTMLElement} Floating calendar pane (instantiated lazily) */
65+
this.calendarPane = $element[0].querySelector('.md-date-calendar-pane');
66+
4467
/** @type {Date} */
4568
this.date = null;
4669

@@ -50,7 +73,19 @@
5073
/** @final {!angular.Scope} */
5174
this.$scope = $scope;
5275

76+
/** @type {boolean} Whether the date-picker's calendar pane is open. */
77+
this.isCalendarOpen = false;
78+
79+
/** Pre-bound click handler is saved so that the event listener can be removed. */
80+
this.bodyClickHandler = this.handleBodyClick.bind(this);
81+
5382
this.attachChangeListeners();
83+
this.attachInterationListeners();
84+
85+
var self = this;
86+
$scope.$on('$destroy', function() {
87+
self.detachCalendarPane();
88+
});
5489
}
5590

5691
/**
@@ -78,6 +113,7 @@
78113
self.$scope.$on('md-calendar-change', function(event, date) {
79114
self.ngModelCtrl.$setViewValue(date);
80115
self.inputElement.value = self.dateLocale.formatDate(date);
116+
self.closeCalendarPane();
81117
});
82118

83119
// TODO(jelbourn): debounce
@@ -89,4 +125,92 @@
89125
}
90126
});
91127
};
128+
129+
/** Attach event listeners for user interaction. */
130+
DatePickerCtrl.prototype.attachInterationListeners = function() {
131+
var self = this;
132+
var $scope = this.$scope;
133+
var keyCodes = this.$mdConstant.KEY_CODE;
134+
135+
self.inputElement.addEventListener('keydown', function(event) {
136+
$scope.$apply(function() {
137+
if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
138+
self.openCalendarPane();
139+
}
140+
});
141+
});
142+
143+
self.$scope.$on('md-calendar-escape', function() {
144+
self.closeCalendarPane();
145+
});
146+
};
147+
148+
/** Position and attach the floating calendar to the document. */
149+
DatePickerCtrl.prototype.attachCalendarPane = function() {
150+
var elementRect = this.$element[0].getBoundingClientRect();
151+
152+
this.calendarPane.style.left = elementRect.left + 'px';
153+
this.calendarPane.style.top = elementRect.bottom + 'px';
154+
document.body.appendChild(this.calendarPane);
155+
};
156+
157+
/** Detach the floating calendar pane from the document. */
158+
DatePickerCtrl.prototype.detachCalendarPane = function() {
159+
// Use native DOM removal because we do not want any of the angular state of this element
160+
// to be disposed.
161+
this.calendarPane.parentNode.removeChild(this.calendarPane);
162+
};
163+
164+
/** Open the floating calendar pane. */
165+
DatePickerCtrl.prototype.openCalendarPane = function() {
166+
if (!this.isCalendarOpen) {
167+
this.isCalendarOpen = true;
168+
this.attachCalendarPane();
169+
// TODO(jelbourn): dispatch to tell other date pickers to close.
170+
this.focusCalendar();
171+
172+
// Attach click listener inside of a timeout because, if this open call was triggered by a
173+
// click, we don't want it to be immediately propogated up to the body and handled.
174+
var self = this;
175+
this.$timeout(function() {
176+
document.body.addEventListener('click', self.bodyClickHandler);
177+
}, 0, false);
178+
}
179+
};
180+
181+
/** Close the floating calendar pane. */
182+
DatePickerCtrl.prototype.closeCalendarPane = function() {
183+
this.isCalendarOpen = false;
184+
this.detachCalendarPane();
185+
this.inputElement.focus();
186+
document.body.removeEventListener('click', this.bodyClickHandler);
187+
};
188+
189+
/** Gets the controller instance for the calendar in the floating pane. */
190+
DatePickerCtrl.prototype.getCalendarCtrl = function() {
191+
return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
192+
};
193+
194+
/** Focus the calendar in the floating pane. */
195+
DatePickerCtrl.prototype.focusCalendar = function() {
196+
// Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
197+
var self = this;
198+
this.$timeout(function() {
199+
self.getCalendarCtrl().focus();
200+
}, 0, false);
201+
};
202+
203+
/**
204+
* Handles a click on the document body when the floating calendar pane is open.
205+
* Closes the floating calendar pane if the click is not inside of it.
206+
* @param {MouseEvent} event
207+
*/
208+
DatePickerCtrl.prototype.handleBodyClick = function(event) {
209+
if (this.isCalendarOpen) {
210+
var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
211+
if (!isInCalendar) {
212+
this.closeCalendarPane();
213+
}
214+
}
215+
};
92216
})();

src/components/calendar/demoDatePicker/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ <h2>Development tools</h2>
2222
<p>Here is a bunch of stuff after the calendar</p>
2323
<p>Here is a bunch of stuff after the calendar</p>
2424
<p>Here is a bunch of stuff after the calendar</p>
25-
<input>
2625

2726
</md-content>
2827
</div>

0 commit comments

Comments
 (0)