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

Commit

Permalink
feat(datepicker): floating calendar panel for date picker.
Browse files Browse the repository at this point in the history
  • Loading branch information
jelbourn committed Aug 13, 2015
1 parent cde67d6 commit b1f6e1a
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 16 deletions.
37 changes: 27 additions & 10 deletions src/components/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@
// TODO(jelbourn): Animations should use `.finally()` instead of `.then()`
// TODO(jelbourn): improve default date parser in locale provider.
// TODO(jelbourn): read-only state.
// TODO(jelbourn): make aria-live element visibly hidden (but still present on the page).

function calendarDirective() {
// Generate a unique ID for each instance of the directive.
var directiveId = 0;

return {
template:
'<div>' +
Expand All @@ -50,7 +48,6 @@
link: function(scope, element, attrs, controllers) {
var ngModelCtrl = controllers[0];
var mdCalendarCtrl = controllers[1];
mdCalendarCtrl.directiveId = directiveId++;
mdCalendarCtrl.configureNgModel(ngModelCtrl);
}
};
Expand All @@ -74,6 +71,9 @@
/** Class applied to the cell for today. */
var TODAY_CLASS = 'md-calendar-date-today';

/** Next idientifier for calendar instance. */
var nextUniqueId = 0;

/**
* Controller for the mdCalendar component.
* @ngInject @constructor
Expand Down Expand Up @@ -117,6 +117,9 @@
/** @final {Date} */
this.today = new Date();

/** @final {number} Unique ID for this calendar instance. */
this.id = nextUniqueId++;

/** @type {!angular.NgModelController} */
this.ngModelCtrl = null;

Expand Down Expand Up @@ -227,7 +230,14 @@
CalendarCtrl.prototype.handleKeyEvent = function(event) {
var self = this;
this.$scope.$apply(function() {
// Handled key events fall into two categories: selection and navigation.
// Capture escape and emit back up so that a wrapping component (such as a date-picker)
// can decide to close.
if (event.which == self.keyCode.ESCAPE) {
self.$scope.$emit('md-calendar-escape');
return;
}

// Remaining key events fall into two categories: selection and navigation.
// Start by checking if this is a selection event.
if (event.which === self.keyCode.ENTER) {
self.setNgModelValue(self.displayDate);
Expand Down Expand Up @@ -292,6 +302,11 @@
var cell = this.calendarElement.querySelector('#' + cellId);
cell.focus();
};

/** Focus the calendar. */
CalendarCtrl.prototype.focus = function() {
this.focusDateElement(this.selectedDate);
};


/*** Animation ***/
Expand Down Expand Up @@ -470,11 +485,15 @@
*/
CalendarCtrl.prototype.changeSelectedDate = function(date) {
var self = this;
var previousSelectedDate = this.selectedDate;
this.selectedDate = date;

this.changeDisplayDate(date).then(function() {

// Remove the selected class from the previously selected date, if any.
if (self.selectedDate) {
var prevDateCell = self.calendarElement.querySelector('#' + self.getDateId_(self.selectedDate));
if (previousSelectedDate) {
var prevDateCell =
self.calendarElement.querySelector('#' + self.getDateId_(previousSelectedDate));
if (prevDateCell) {
prevDateCell.classList.remove(SELECTED_DATE_CLASS);
}
Expand All @@ -487,8 +506,6 @@
dateCell.classList.add(SELECTED_DATE_CLASS);
}
}

self.selectedDate = date;
});
};

Expand Down Expand Up @@ -701,7 +718,7 @@
CalendarCtrl.prototype.getDateId_ = function (date) {
return [
'md',
this.directiveId,
this.id,
date.getFullYear(),
date.getMonth(),
date.getDate()
Expand Down
1 change: 1 addition & 0 deletions src/components/calendar/dateLocaleProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@

window.$locale = $locale;

// TODO(jelbourn): Freeze this object.
return {
months: this.months || $locale.DATETIME_FORMATS.MONTH,
shortMonths: this.shortMonths || $locale.DATETIME_FORMATS.SHORTMONTH,
Expand Down
134 changes: 129 additions & 5 deletions src/components/calendar/datePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,58 @@
function datePickerDirective() {
return {
template:
'<div class="md-date-picker">' +
'<input> <br>' +
'<md-calendar ng-model="ctrl.date"></md-calendar>' +
'</div>',
'<input><button type="button" ng-click="ctrl.openCalendarPane()">📅</button>' +
'<div class="md-date-calendar-pane">' +
'<md-calendar ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen"></md-calendar>' +
'</div>',
// <md-calendar ng-model="ctrl.date"></md-calendar>
require: ['ngModel', 'mdDatePicker'],
scope: {},
controller: DatePickerCtrl,
controllerAs: 'ctrl',
link: function(scope, element, attr, controllers) {
var ngModelCtrl = controllers[0];
var mdDatePickerCtrl = controllers[1];

mdDatePickerCtrl.configureNgModel(ngModelCtrl);
}
};
}

function DatePickerCtrl($scope, $element, $$mdDateLocale, $$mdDateUtil) {
/**
* Controller for md-date-picker.
*
* @ngInject @constructor
*/
function DatePickerCtrl($scope, $element, $compile, $timeout, $mdConstant, $mdUtil,
$$mdDateLocale, $$mdDateUtil) {
/** @final */
this.$compile = $compile;

/** @final */
this.$timeout = $timeout;

/** @final */
this.dateLocale = $$mdDateLocale;

/** @final */
this.dateUtil = $$mdDateUtil;

/** @final */
this.$mdConstant = $mdConstant;

/* @final */
this.$mdUtil = $mdUtil;

/** @type {!angular.NgModelController} */
this.ngModelCtrl = null;

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

/** @type {HTMLElement} Floating calendar pane (instantiated lazily) */
this.calendarPane = $element[0].querySelector('.md-date-calendar-pane');

/** @type {Date} */
this.date = null;

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

/** @type {boolean} Whether the date-picker's calendar pane is open. */
this.isCalendarOpen = false;

/** Pre-bound click handler is saved so that the event listener can be removed. */
this.bodyClickHandler = this.handleBodyClick.bind(this);

this.attachChangeListeners();
this.attachInterationListeners();

var self = this;
$scope.$on('$destroy', function() {
self.detachCalendarPane();
});
}

/**
Expand Down Expand Up @@ -78,6 +113,7 @@
self.$scope.$on('md-calendar-change', function(event, date) {
self.ngModelCtrl.$setViewValue(date);
self.inputElement.value = self.dateLocale.formatDate(date);
self.closeCalendarPane();
});

// TODO(jelbourn): debounce
Expand All @@ -89,4 +125,92 @@
}
});
};

/** Attach event listeners for user interaction. */
DatePickerCtrl.prototype.attachInterationListeners = function() {
var self = this;
var $scope = this.$scope;
var keyCodes = this.$mdConstant.KEY_CODE;

self.inputElement.addEventListener('keydown', function(event) {
$scope.$apply(function() {
if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
self.openCalendarPane();
}
});
});

self.$scope.$on('md-calendar-escape', function() {
self.closeCalendarPane();
});
};

/** Position and attach the floating calendar to the document. */
DatePickerCtrl.prototype.attachCalendarPane = function() {
var elementRect = this.$element[0].getBoundingClientRect();

this.calendarPane.style.left = elementRect.left + 'px';
this.calendarPane.style.top = elementRect.bottom + 'px';
document.body.appendChild(this.calendarPane);
};

/** Detach the floating calendar pane from the document. */
DatePickerCtrl.prototype.detachCalendarPane = function() {
// Use native DOM removal because we do not want any of the angular state of this element
// to be disposed.
this.calendarPane.parentNode.removeChild(this.calendarPane);
};

/** Open the floating calendar pane. */
DatePickerCtrl.prototype.openCalendarPane = function() {
if (!this.isCalendarOpen) {
this.isCalendarOpen = true;
this.attachCalendarPane();
// TODO(jelbourn): dispatch to tell other date pickers to close.
this.focusCalendar();

// Attach click listener inside of a timeout because, if this open call was triggered by a
// click, we don't want it to be immediately propogated up to the body and handled.
var self = this;
this.$timeout(function() {
document.body.addEventListener('click', self.bodyClickHandler);
}, 0, false);
}
};

/** Close the floating calendar pane. */
DatePickerCtrl.prototype.closeCalendarPane = function() {
this.isCalendarOpen = false;
this.detachCalendarPane();
this.inputElement.focus();
document.body.removeEventListener('click', this.bodyClickHandler);
};

/** Gets the controller instance for the calendar in the floating pane. */
DatePickerCtrl.prototype.getCalendarCtrl = function() {
return angular.element(this.calendarPane.querySelector('md-calendar')).controller('mdCalendar');
};

/** Focus the calendar in the floating pane. */
DatePickerCtrl.prototype.focusCalendar = function() {
// Use a timeout in order to allow the calendar to be rendered, as it is gated behind an ng-if.
var self = this;
this.$timeout(function() {
self.getCalendarCtrl().focus();
}, 0, false);
};

/**
* Handles a click on the document body when the floating calendar pane is open.
* Closes the floating calendar pane if the click is not inside of it.
* @param {MouseEvent} event
*/
DatePickerCtrl.prototype.handleBodyClick = function(event) {
if (this.isCalendarOpen) {
var isInCalendar = this.$mdUtil.getClosest(event.target, 'md-calendar');
if (!isInCalendar) {
this.closeCalendarPane();
}
}
};
})();
9 changes: 9 additions & 0 deletions src/components/calendar/datePicker.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.md-date-calendar-pane {
position: absolute;
top: 0;
left: 0;

// DEBUG
box-shadow: 0 4px 4px;
background: white;
}
1 change: 0 additions & 1 deletion src/components/calendar/demoDatePicker/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ <h2>Development tools</h2>
<p>Here is a bunch of stuff after the calendar</p>
<p>Here is a bunch of stuff after the calendar</p>
<p>Here is a bunch of stuff after the calendar</p>
<input>

</md-content>
</div>

0 comments on commit b1f6e1a

Please sign in to comment.