mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-04-18 11:38:53 +00:00
(js, css) Improve timepicker
This commit is contained in:
@@ -25,10 +25,12 @@
|
||||
' </div>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div class="min1" ng-hide="is5min()" layout="row" layout-sm="column" layout-wrap>',
|
||||
' <div ng-repeat="minutesLine in min1" layout="row" layout-align="space-around center" flex="50">',
|
||||
' <md-button class="minuteBtn md-fab md-mini" ng-repeat="minute in minutesLine" id="{{minute.id}}"',
|
||||
' ng-click="minuteClickHandler(minute.displayName)">{{minute.displayName}}</md-button>',
|
||||
' <div class="sg-time-scroll-mask" ng-hide="is5min()">',
|
||||
' <div class="min1" layout="row" layout-sm="column" layout-wrap>',
|
||||
' <div ng-repeat="minutesLine in min1" layout="row" layout-align="space-around center" flex="50">',
|
||||
' <md-button class="minuteBtn md-fab md-mini" ng-repeat="minute in minutesLine" id="{{minute.id}}"',
|
||||
' ng-click="minuteClickHandler(minute.displayName)">{{minute.displayName}}</md-button>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <div flex layout="row" layout-align="center center" class="toggle-pane">',
|
||||
@@ -44,9 +46,8 @@
|
||||
link: function(scope, element, attrs, controllers) {
|
||||
var ngModelCtrl = controllers[0];
|
||||
var sgTimePaneCtrl = controllers[1];
|
||||
console.log(element);
|
||||
var timePaneElement = element;
|
||||
sgTimePaneCtrl.configureNgModel(ngModelCtrl,sgTimePaneCtrl, timePaneElement);
|
||||
sgTimePaneCtrl.configureNgModel(ngModelCtrl, sgTimePaneCtrl, timePaneElement);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -110,15 +111,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
$scope.show5min=true;
|
||||
$scope.show5min = true;
|
||||
$scope.getToggleBtnLbl = function() {
|
||||
return ($scope.is5min()) ? '>>' : '<<';
|
||||
};
|
||||
$scope.toggleManual5min = function() {
|
||||
$scope.manual5min = !$scope.is5min();
|
||||
};
|
||||
$scope.is5min=function(){
|
||||
if($scope.manual5min === true || $scope.manual5min === false) {
|
||||
$scope.is5min = function() {
|
||||
if ($scope.manual5min === true || $scope.manual5min === false) {
|
||||
return $scope.manual5min;
|
||||
}
|
||||
else {
|
||||
@@ -133,7 +134,8 @@
|
||||
var self = this;
|
||||
|
||||
this.hourClickHandler = function(displayVal) {
|
||||
var updated = new Date(self.displayTime).setHours(Number(displayVal));
|
||||
var updated = new Date(self.displayTime);
|
||||
updated.setHours(Number(displayVal));
|
||||
self.setNgModelValue(updated, 'hours');
|
||||
};
|
||||
$scope.hourClickHandler = this.hourClickHandler;
|
||||
@@ -141,7 +143,8 @@
|
||||
this.minuteClickHandler = function(displayVal) {
|
||||
//remove leading ':'
|
||||
var val = displayVal.substr(1);
|
||||
var updated = new Date(self.displayTime).setMinutes(Number(val));
|
||||
var updated = new Date(self.displayTime);
|
||||
updated.setMinutes(Number(val));
|
||||
self.setNgModelValue(updated, 'minutes');
|
||||
};
|
||||
$scope.minuteClickHandler = this.minuteClickHandler;
|
||||
@@ -152,6 +155,7 @@
|
||||
|
||||
TimePaneCtrl.prototype.configureNgModel = function(ngModelCtrl, sgTimePaneCtrl, timePaneElement) {
|
||||
this.ngModelCtrl = ngModelCtrl;
|
||||
|
||||
var self = this;
|
||||
ngModelCtrl.$render = function() {
|
||||
self.changeSelectedTime(self.ngModelCtrl.$viewValue, sgTimePaneCtrl, timePaneElement);
|
||||
@@ -164,8 +168,9 @@
|
||||
TimePaneCtrl.prototype.changeSelectedTime = function(date, sgTimePaneCtrl, timePaneElement) {
|
||||
var self = this;
|
||||
var previousSelectedTime = this.selectedTime;
|
||||
this.selectedTime = new Date(date);
|
||||
this.selectedTime = date;
|
||||
this.changeDisplayTime(date).then(function() {
|
||||
|
||||
// Remove the selected class from the previously selected date, if any.
|
||||
if (previousSelectedTime) {
|
||||
var prevH = previousSelectedTime.getHours();
|
||||
@@ -189,14 +194,13 @@
|
||||
|
||||
// Apply the select class to the new selected date if it is set.
|
||||
if (date) {
|
||||
var d = new Date(date);
|
||||
var newH = d.getHours();
|
||||
var newH = date.getHours();
|
||||
var mCell, hCell = document.getElementById('tp-'+sgTimePaneCtrl.id+'-hour-'+newH);
|
||||
if (hCell) {
|
||||
hCell.classList.add(SELECTED_TIME_CLASS);
|
||||
hCell.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
var newM = d.getMinutes();
|
||||
var newM = date.getMinutes();
|
||||
if (newM % 5 === 0) {
|
||||
sgTimePaneCtrl.$scope.show5min = true;
|
||||
mCell = document.getElementById('tp-'+sgTimePaneCtrl.id+'-minute5-'+newM);
|
||||
@@ -372,7 +376,8 @@
|
||||
// This pane will be detached from here and re-attached to the document body.
|
||||
'<div class="sg-timepicker-time-pane md-whiteframe-z1">',
|
||||
' <div class="sg-timepicker-input-mask">',
|
||||
' <div class="sg-timepicker-input-mask-opaque"></div>',
|
||||
' <div class="sg-timepicker-input-mask-opaque',
|
||||
' md-default-theme md-background md-bg"></div>', // using mdColors
|
||||
' </div>',
|
||||
' <div class="sg-timepicker-time md-default-theme md-bg md-background">',
|
||||
' <sg-time-pane role="dialog" aria-label="{{::ctrl.dateLocale.msgCalendar}}" ',
|
||||
@@ -405,12 +410,33 @@
|
||||
/** Default time in ms to debounce input event by. */
|
||||
var DEFAULT_DEBOUNCE_INTERVAL = 500;
|
||||
|
||||
/**
|
||||
* Height of the calendar pane used to check if the pane is going outside the boundary of
|
||||
* the viewport. See calendar.scss for how $md-calendar-height is computed; an extra 20px is
|
||||
* also added to space the pane away from the exact edge of the screen.
|
||||
*
|
||||
* This is computed statically now, but can be changed to be measured if the circumstances
|
||||
* of calendar sizing are changed.
|
||||
*/
|
||||
var TIME_PANE_HEIGHT = { MIN5: { GTSM: 172 + 20, SM: 292 + 20 },
|
||||
MIN1: { GTSM: 364 + 20, SM: 454 + 20 } };
|
||||
|
||||
/**
|
||||
* Width of the calendar pane used to check if the pane is going outside the boundary of
|
||||
* the viewport. See calendar.scss for how $md-calendar-width is computed; an extra 20px is
|
||||
* also added to space the pane away from the exact edge of the screen.
|
||||
*
|
||||
* This is computed statically now, but can be changed to be measured if the circumstances
|
||||
* of calendar sizing are changed.
|
||||
*/
|
||||
var TIME_PANE_WIDTH = { GTSM: 510 + 20, SM: 272 + 20 };
|
||||
|
||||
/**
|
||||
* Controller for sg-timepicker.
|
||||
*
|
||||
* ngInject @constructor
|
||||
*/
|
||||
function TimePickerCtrl($scope, $element, $attrs, $compile, $timeout, $mdConstant, $mdTheming,
|
||||
function TimePickerCtrl($scope, $element, $attrs, $compile, $timeout, $mdConstant, $mdMedia, $mdTheming,
|
||||
$mdUtil, $mdDateLocale, $$mdDateUtil, $$rAF) {
|
||||
/** @final */
|
||||
this.$compile = $compile;
|
||||
@@ -427,6 +453,9 @@
|
||||
/** @final */
|
||||
this.$mdConstant = $mdConstant;
|
||||
|
||||
/** @final */
|
||||
this.$mdMedia = $mdMedia;
|
||||
|
||||
/* @final */
|
||||
this.$mdUtil = $mdUtil;
|
||||
|
||||
@@ -442,6 +471,9 @@
|
||||
/** @type {HTMLElement} */
|
||||
this.inputContainer = $element[0].querySelector('.sg-timepicker-input-container');
|
||||
|
||||
/** @final {!angular.JQLite} */
|
||||
this.ngInputElement = angular.element(this.inputElement);
|
||||
|
||||
/** @type {HTMLElement} Floating time pane. */
|
||||
this.timePane = $element[0].querySelector('.sg-timepicker-time-pane');
|
||||
|
||||
@@ -490,6 +522,9 @@
|
||||
/** Pre-bound click handler is saved so that the event listener can be removed. */
|
||||
this.bodyClickHandler = angular.bind(this, this.handleBodyClick);
|
||||
|
||||
/** Pre-bound resize handler so that the event listener can be removed. */
|
||||
this.windowResizeHandler = $mdUtil.debounce(angular.bind(this, this.closeTimePane), 100);
|
||||
|
||||
// Unless the user specifies so, the datepicker should not be a tab stop.
|
||||
// This is necessary because ngAria might add a tabindex to anything with an ng-model
|
||||
// (based on whether or not the user has turned that particular feature on/off).
|
||||
@@ -507,7 +542,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
TimePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$mdConstant", "$mdTheming",
|
||||
TimePickerCtrl.$inject = ["$scope", "$element", "$attrs", "$compile", "$timeout", "$mdConstant", "$mdMedia", "$mdTheming",
|
||||
"$mdUtil", "$mdDateLocale", "$$mdDateUtil", "$$rAF"];
|
||||
|
||||
/**
|
||||
@@ -526,10 +561,10 @@
|
||||
|
||||
TimePickerCtrl.prototype.formatTime = function(time) {
|
||||
var t = new Date(time);
|
||||
if(t) {
|
||||
var h= t.getHours();
|
||||
var m= t.getMinutes();
|
||||
return (h<10?('0'+ h) : h) + ':' + (m<10?'0'+ m : m);
|
||||
if (t) {
|
||||
var h = t.getHours();
|
||||
var m = t.getMinutes();
|
||||
return (h < 10? ('0' + h) : h) + ':' + (m < 10? ('0' + m) : m);
|
||||
}
|
||||
else return '';
|
||||
};
|
||||
@@ -546,16 +581,15 @@
|
||||
self.ngModelCtrl.$setViewValue(time);
|
||||
self.time = time;
|
||||
self.inputElement.value = self.formatTime(self.time);
|
||||
if(data.changed == 'minutes') {
|
||||
if (data.changed == 'minutes') {
|
||||
self.closeTimePane();
|
||||
}
|
||||
self.resizeInputElement();
|
||||
self.inputContainer.classList.remove(INVALID_CLASS);
|
||||
});
|
||||
|
||||
var ngElement = angular.element(self.inputElement);
|
||||
ngElement.on('input', angular.bind(self, self.resizeInputElement));
|
||||
ngElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
|
||||
self.ngInputElement.on('input', angular.bind(self, self.resizeInputElement));
|
||||
self.ngInputElement.on('input', self.$mdUtil.debounce(self.handleInputEvent,
|
||||
DEFAULT_DEBOUNCE_INTERVAL, self));
|
||||
};
|
||||
|
||||
@@ -566,7 +600,7 @@
|
||||
var keyCodes = this.$mdConstant.KEY_CODE;
|
||||
|
||||
// Add event listener through angular so that we can triggerHandler in unit tests.
|
||||
angular.element(self.inputElement).on('keydown', function(event) {
|
||||
self.ngInputElement.on('keydown', function(event) {
|
||||
if (event.altKey && event.keyCode == keyCodes.DOWN_ARROW) {
|
||||
self.openTimePane(event);
|
||||
$scope.$digest();
|
||||
@@ -588,9 +622,12 @@
|
||||
if (this.$attrs.ngDisabled) {
|
||||
// The expression is to be evaluated against the directive element's scope and not
|
||||
// the directive's isolate scope.
|
||||
this.$element.scope().$watch(this.$attrs.ngDisabled, function(isDisabled) {
|
||||
self.setDisabled(isDisabled);
|
||||
});
|
||||
var scope = this.$mdUtil.validateScope(this.$element) ? this.$element.scope() : null;
|
||||
if (scope) {
|
||||
scope.$watch(this.$attrs.ngDisabled, function(isDisabled) {
|
||||
self.setDisabled(isDisabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(this, 'placeholder', {
|
||||
@@ -623,21 +660,30 @@
|
||||
TimePickerCtrl.prototype.handleInputEvent = function(self) {
|
||||
var inputString = this.inputElement.value;
|
||||
var arr = inputString.split(':');
|
||||
if(arr.length < 2) {return;}
|
||||
var h=Number(arr[0]);
|
||||
var m=Number(arr[1]);
|
||||
var newVal = new Date(this.time);
|
||||
if (h && h>=0 && h<=23 && m && m>=0 && m<= 59 && angular.isDate(newVal)) {
|
||||
newVal.setHours(h);
|
||||
newVal.setMinutes(m);
|
||||
this.ngModelCtrl.$setViewValue(newVal);
|
||||
this.time = newVal;
|
||||
|
||||
if (inputString === '') {
|
||||
this.ngModelCtrl.$setViewValue(null);
|
||||
this.time = null;
|
||||
this.inputContainer.classList.remove(INVALID_CLASS);
|
||||
}
|
||||
else {
|
||||
// If there's an input string, it's an invalid time.
|
||||
else if (arr.length < 2) {
|
||||
this.inputContainer.classList.toggle(INVALID_CLASS, inputString);
|
||||
}
|
||||
else {
|
||||
var h = Number(arr[0]);
|
||||
var m = Number(arr[1]);
|
||||
var newVal = new Date(this.time);
|
||||
if (h && h >= 0 && h <= 23 && m && m >= 0 && m <= 59 && angular.isDate(newVal)) {
|
||||
newVal.setHours(h);
|
||||
newVal.setMinutes(m);
|
||||
this.ngModelCtrl.$setViewValue(newVal);
|
||||
this.time = newVal;
|
||||
this.inputContainer.classList.remove(INVALID_CLASS);
|
||||
}
|
||||
else {
|
||||
this.inputContainer.classList.toggle(INVALID_CLASS, inputString);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Position and attach the floating calendar to the document. */
|
||||
@@ -649,12 +695,36 @@
|
||||
var elementRect = this.inputContainer.getBoundingClientRect();
|
||||
var bodyRect = document.body.getBoundingClientRect();
|
||||
|
||||
timePane.style.left = (elementRect.left - bodyRect.left) + 'px';
|
||||
timePane.style.top = (elementRect.top - bodyRect.top) + 'px';
|
||||
// Check to see if the calendar pane would go off the screen. If so, adjust position
|
||||
// accordingly to keep it within the viewport.
|
||||
var paneTop = elementRect.top - bodyRect.top;
|
||||
var paneLeft = elementRect.left - bodyRect.left;
|
||||
|
||||
// If the right edge of the pane would be off the screen and shifting it left by the
|
||||
// difference would not go past the left edge of the screen.
|
||||
var paneWidth = this.$mdMedia('sm')? TIME_PANE_WIDTH.SM : TIME_PANE_WIDTH.GTSM;
|
||||
if (paneLeft + paneWidth > bodyRect.right &&
|
||||
bodyRect.right - paneWidth > 0) {
|
||||
paneLeft = bodyRect.right - paneWidth;
|
||||
timePane.classList.add('sg-timepicker-pos-adjusted');
|
||||
}
|
||||
timePane.style.left = paneLeft + 'px';
|
||||
|
||||
// If the bottom edge of the pane would be off the screen and shifting it up by the
|
||||
// difference would not go past the top edge of the screen.
|
||||
var min = (typeof this.time == 'object' && this.time.getMinutes() % 5 === 0)? 'MIN5' : 'MIN1';
|
||||
var paneHeight = this.$mdMedia('sm')? TIME_PANE_HEIGHT[min].SM : TIME_PANE_HEIGHT[min].GTSM;
|
||||
if (paneTop + paneHeight > bodyRect.bottom &&
|
||||
bodyRect.bottom - paneHeight > 0) {
|
||||
paneTop = bodyRect.bottom - paneHeight;
|
||||
timePane.classList.add('sg-timepicker-pos-adjusted');
|
||||
}
|
||||
|
||||
timePane.style.top = paneTop + 'px';
|
||||
document.body.appendChild(this.timePane);
|
||||
|
||||
// The top of the calendar pane is a transparent box that shows the text input underneath.
|
||||
// Since the pane is flowing, though, the page underneath the pane *adjacent* to the input is
|
||||
// Since the pane is floating, though, the page underneath the pane *adjacent* to the input is
|
||||
// also shown unless we cover it up. The inputMask does this by filling up the remaining space
|
||||
// based on the width of the input.
|
||||
this.inputMask.style.left = elementRect.width + 'px';
|
||||
@@ -670,6 +740,7 @@
|
||||
this.$element.removeClass('sg-timepicker-open');
|
||||
this.$element.find('button').removeClass('md-primary');
|
||||
this.timePane.classList.remove('md-pane-open');
|
||||
this.timePane.classList.remove('md-timepicker-pos-adjusted');
|
||||
|
||||
if (this.timePane.parentNode) {
|
||||
// Use native DOM removal because we do not want any of the angular state of this element
|
||||
@@ -702,6 +773,8 @@
|
||||
this.$mdUtil.nextTick(function() {
|
||||
document.body.addEventListener('click', self.bodyClickHandler);
|
||||
}, false);
|
||||
|
||||
window.addEventListener('resize', this.windowResizeHandler);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -714,6 +787,7 @@
|
||||
this.$mdUtil.enableScrolling();
|
||||
|
||||
document.body.removeEventListener('click', this.bodyClickHandler);
|
||||
window.removeEventListener('resize', this.windowResizeHandler);
|
||||
};
|
||||
|
||||
/** Gets the controller instance for the time in the floating pane. */
|
||||
|
||||
@@ -17,6 +17,22 @@ sg-time-pane {
|
||||
border-top: solid 1px rgb(224,224,224);
|
||||
}
|
||||
|
||||
.sg-time-scroll-mask {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
height: 6 * $sg-time-pane-cell-size;
|
||||
width: 100%;
|
||||
|
||||
// These two properties are needed to get touch momentum to work.
|
||||
// See https://css-tricks.com/snippets/css/momentum-scrolling-on-ios-overflow-elements
|
||||
overflow-y: scroll;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hours-pane,
|
||||
.min1,
|
||||
.min5 {
|
||||
@@ -178,4 +194,11 @@ sg-timepicker[disabled] {
|
||||
.sg-timepicker-triangle-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When the position of the floating calendar pane is adjusted to remain inside
|
||||
// of the viewport, hide the inputput mask, as the text input will no longer be
|
||||
// directly underneath it.
|
||||
.sg-timepicker-pos-adjusted .sg-timepicker-input-mask {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user