diff --git a/UI/WebServerResources/js/Common/sgTimepicker.directive.js b/UI/WebServerResources/js/Common/sgTimepicker.directive.js index 9170938f3..414731d2c 100644 --- a/UI/WebServerResources/js/Common/sgTimepicker.directive.js +++ b/UI/WebServerResources/js/Common/sgTimepicker.directive.js @@ -25,10 +25,12 @@ ' ', ' ', ' ', - '
', - '
', - ' {{minute.displayName}}', + '
', + '
', + '
', + ' {{minute.displayName}}', + '
', '
', '
', '
', @@ -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. '
', '
', - '
', + '
', // using mdColors '
', '
', ' =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. */ diff --git a/UI/WebServerResources/scss/components/timepicker/timepicker.scss b/UI/WebServerResources/scss/components/timepicker/timepicker.scss index f1dc754b1..a02c6e467 100644 --- a/UI/WebServerResources/scss/components/timepicker/timepicker.scss +++ b/UI/WebServerResources/scss/components/timepicker/timepicker.scss @@ -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; } -} \ No newline at end of file +} + +// 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; +}