仿Angular Bootstrap TimePicker創(chuàng)建分鐘數(shù)-秒數(shù)的輸入控件
在一個(gè)項(xiàng)目中需要一個(gè)用來(lái)輸入分鐘數(shù)和秒數(shù)的控件,然而調(diào)查了一些開(kāi)源項(xiàng)目后并未發(fā)現(xiàn)合適的控件。在Angular Bootstrap UI中有一個(gè)類(lèi)似的控件TimePicker,但是它并沒(méi)有深入到分鐘和秒的精度。
因此,決定參考它的源碼然后自己進(jìn)行實(shí)現(xiàn)。
最終的效果如下:

首先是該directive的定義:
app.directive('minuteSecondPicker', function() {
return {
restrict: 'EA',
require: ['minuteSecondPicker', '?^ngModel'],
controller: 'minuteSecondPickerController',
replace: true,
scope: {
validity: '='
},
templateUrl: 'partials/directives/minuteSecondPicker.html',
link: function(scope, element, attrs, ctrls) {
var minuteSecondPickerCtrl = ctrls[0],
ngModelCtrl = ctrls[1];
if(ngModelCtrl) {
minuteSecondPickerCtrl.init(ngModelCtrl, element.find('input'));
}
}
};
});
在以上的link函數(shù)中,ctrls是一個(gè)數(shù)組: ctrls[0]是定義在本directive上的controller實(shí)例,ctrls[1]是ngModelCtrl,即ng-model對(duì)應(yīng)的controller實(shí)例。這個(gè)順序?qū)嶋H上是通過(guò)require: ['minuteSecondPicker', '?^ngModel']定義的。
注意到第一個(gè)依賴(lài)就是directive本身的名字,此時(shí)會(huì)將該directive中controller聲明的對(duì)應(yīng)實(shí)例傳入。第二個(gè)依賴(lài)的寫(xiě)法有些奇怪:"?^ngModel",?的含義是即使沒(méi)有找到該依賴(lài),也不要拋出異常,即該依賴(lài)是一個(gè)可選項(xiàng)。^的含義是查找父元素的controller。
然后,定義該directive中用到的一些默認(rèn)設(shè)置,通過(guò)constant directive實(shí)現(xiàn):
app.constant('minuteSecondPickerConfig', {
minuteStep: 1,
secondStep: 1,
readonlyInput: false,
mousewheel: true
});
緊接著是directive對(duì)應(yīng)的controller,它的聲明如下:
app.controller('minuteSecondPickerController', ['$scope', '$attrs', '$parse', 'minuteSecondPickerConfig',
function($scope, $attrs, $parse, minuteSecondPickerConfig) {
...
}]);
在directive的link函數(shù)中,調(diào)用了此controller的init方法:
this.init = function(ngModelCtrl_, inputs) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
var minutesInputEl = inputs.eq(0),
secondsInputEl = inputs.eq(1);
var mousewheel = angular.isDefined($attrs.mousewheel) ?
$scope.$parent.$eval($attrs.mousewheel) : minuteSecondPickerConfig.mousewheel;
if(mousewheel) {
this.setupMousewheelEvents(minutesInputEl, secondsInputEl);
}
$scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ?
$scope.$parent.$eval($attrs.readonlyInput) : minuteSecondPickerConfig.readonlyInput;
this.setupInputEvents(minutesInputEl, secondsInputEl);
};
init方法接受的第二個(gè)參數(shù)是inputs,在link函數(shù)中傳入的是:element.find('input')。 所以第一個(gè)輸入框用來(lái)輸入分鐘,第二個(gè)輸入框用來(lái)輸入秒。
然后,檢查是否覆蓋了mousewheel屬性,如果沒(méi)有覆蓋則使用在constant中設(shè)置的默認(rèn)mousewheel,并進(jìn)行相關(guān)設(shè)置如下:
// respond on mousewheel spin
this.setupMousewheelEvents = function(minutesInputEl, secondsInputEl) {
var isScrollingUp = function(e) {
if(e.originalEvent) {
e = e.originalEvent;
}
// pick correct delta variable depending on event
var delta = (e.wheelData) ? e.wheelData : -e.deltaY;
return (e.detail || delta > 0);
};
minutesInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply((isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes());
e.preventDefault();
});
secondsInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply((isScrollingUp(e)) ? $scope.incrementSeconds() : $scope.decrementSeconds());
e.preventDefault();
});
};
init方法最后會(huì)對(duì)inputs本身進(jìn)行一些設(shè)置:
// respond on direct input
this.setupInputEvents = function(minutesInputEl, secondsInputEl) {
if($scope.readonlyInput) {
$scope.updateMinutes = angular.noop;
$scope.updateSeconds = angular.noop;
return;
}
var invalidate = function(invalidMinutes, invalidSeconds) {
ngModelCtrl.$setViewValue(null);
ngModelCtrl.$setValidity('time', false);
$scope.validity = false;
if(angular.isDefined(invalidMinutes)) {
$scope.invalidMinutes = invalidMinutes;
}
if(angular.isDefined(invalidSeconds)) {
$scope.invalidSeconds = invalidSeconds;
}
};
$scope.updateMinutes = function() {
var minutes = getMinutesFromTemplate();
if(angular.isDefined(minutes)) {
selected.minutes = minutes;
refresh('m');
} else {
invalidate(true);
}
};
minutesInputEl.bind('blur', function(e) {
if(!$scope.invalidMinutes && $scope.minutes < 10) {
$scope.$apply(function() {
$scope.minutes = pad($scope.minutes);
});
}
});
$scope.updateSeconds = function() {
var seconds = getSecondsFromTemplate();
if(angular.isDefined(seconds)) {
selected.seconds = seconds;
refresh('s');
} else {
invalidate(undefined, true);
}
};
secondsInputEl.bind('blur', function(e) {
if(!$scope.invalidSeconds && $scope.seconds < 10) {
$scope.$apply(function() {
$scope.seconds = pad($scope.seconds);
});
}
});
};
此方法中,聲明了用于設(shè)置輸入非法的invalidate函數(shù),它會(huì)在scope中暴露一個(gè)validity = false屬性讓頁(yè)面有機(jī)會(huì)做出合適的反應(yīng)。
如果用戶使用了一個(gè)變量來(lái)表示minuteStep或者secondStep,那么還需要設(shè)置相應(yīng)的watchers:
var minuteStep = minuteSecondPickerConfig.minuteStep;
if($attrs.minuteStep) {
$scope.parent.$watch($parse($attrs.minuteStep), function(value) {
minuteStep = parseInt(value, 10);
});
}
var secondStep = minuteSecondPickerConfig.secondStep;
if($attrs.secondStep) {
$scope.parent.$watch($parse($attrs.secondStep), function(value) {
secondStep = parseInt(value, 10);
});
}
完整的directive實(shí)現(xiàn)代碼如下:
var app = angular.module("minuteSecondPickerDemo");
app.directive('minuteSecondPicker', function() {
return {
restrict: 'EA',
require: ['minuteSecondPicker', '?^ngModel'],
controller: 'minuteSecondPickerController',
replace: true,
scope: {
validity: '='
},
templateUrl: 'partials/directives/minuteSecondPicker.html',
link: function(scope, element, attrs, ctrls) {
var minuteSecondPickerCtrl = ctrls[0],
ngModelCtrl = ctrls[1];
if(ngModelCtrl) {
minuteSecondPickerCtrl.init(ngModelCtrl, element.find('input'));
}
}
};
});
app.constant('minuteSecondPickerConfig', {
minuteStep: 1,
secondStep: 1,
readonlyInput: false,
mousewheel: true
});
app.controller('minuteSecondPickerController', ['$scope', '$attrs', '$parse', 'minuteSecondPickerConfig',
function($scope, $attrs, $parse, minuteSecondPickerConfig) {
var selected = {
minutes: 0,
seconds: 0
},
ngModelCtrl = {
$setViewValue: angular.noop
};
this.init = function(ngModelCtrl_, inputs) {
ngModelCtrl = ngModelCtrl_;
ngModelCtrl.$render = this.render;
var minutesInputEl = inputs.eq(0),
secondsInputEl = inputs.eq(1);
var mousewheel = angular.isDefined($attrs.mousewheel) ?
$scope.$parent.$eval($attrs.mousewheel) : minuteSecondPickerConfig.mousewheel;
if(mousewheel) {
this.setupMousewheelEvents(minutesInputEl, secondsInputEl);
}
$scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ?
$scope.$parent.$eval($attrs.readonlyInput) : minuteSecondPickerConfig.readonlyInput;
this.setupInputEvents(minutesInputEl, secondsInputEl);
};
var minuteStep = minuteSecondPickerConfig.minuteStep;
if($attrs.minuteStep) {
$scope.parent.$watch($parse($attrs.minuteStep), function(value) {
minuteStep = parseInt(value, 10);
});
}
var secondStep = minuteSecondPickerConfig.secondStep;
if($attrs.secondStep) {
$scope.parent.$watch($parse($attrs.secondStep), function(value) {
secondStep = parseInt(value, 10);
});
}
// respond on mousewheel spin
this.setupMousewheelEvents = function(minutesInputEl, secondsInputEl) {
var isScrollingUp = function(e) {
if(e.originalEvent) {
e = e.originalEvent;
}
// pick correct delta variable depending on event
var delta = (e.wheelData) ? e.wheelData : -e.deltaY;
return (e.detail || delta > 0);
};
minutesInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply((isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes());
e.preventDefault();
});
secondsInputEl.bind('mousewheel wheel', function(e) {
$scope.$apply((isScrollingUp(e)) ? $scope.incrementSeconds() : $scope.decrementSeconds());
e.preventDefault();
});
};
// respond on direct input
this.setupInputEvents = function(minutesInputEl, secondsInputEl) {
if($scope.readonlyInput) {
$scope.updateMinutes = angular.noop;
$scope.updateSeconds = angular.noop;
return;
}
var invalidate = function(invalidMinutes, invalidSeconds) {
ngModelCtrl.$setViewValue(null);
ngModelCtrl.$setValidity('time', false);
$scope.validity = false;
if(angular.isDefined(invalidMinutes)) {
$scope.invalidMinutes = invalidMinutes;
}
if(angular.isDefined(invalidSeconds)) {
$scope.invalidSeconds = invalidSeconds;
}
};
$scope.updateMinutes = function() {
var minutes = getMinutesFromTemplate();
if(angular.isDefined(minutes)) {
selected.minutes = minutes;
refresh('m');
} else {
invalidate(true);
}
};
minutesInputEl.bind('blur', function(e) {
if(!$scope.invalidMinutes && $scope.minutes < 10) {
$scope.$apply(function() {
$scope.minutes = pad($scope.minutes);
});
}
});
$scope.updateSeconds = function() {
var seconds = getSecondsFromTemplate();
if(angular.isDefined(seconds)) {
selected.seconds = seconds;
refresh('s');
} else {
invalidate(undefined, true);
}
};
secondsInputEl.bind('blur', function(e) {
if(!$scope.invalidSeconds && $scope.seconds < 10) {
$scope.$apply(function() {
$scope.seconds = pad($scope.seconds);
});
}
});
};
this.render = function() {
var time = ngModelCtrl.$modelValue ? {
minutes: ngModelCtrl.$modelValue.minutes,
seconds: ngModelCtrl.$modelValue.seconds
} : null;
// adjust the time for invalid value at first time
if(time.minutes < 0) {
time.minutes = 0;
}
if(time.seconds < 0) {
time.seconds = 0;
}
var totalSeconds = time.minutes * 60 + time.seconds;
time = {
minutes: Math.floor(totalSeconds / 60),
seconds: totalSeconds % 60
};
if(time) {
selected = time;
makeValid();
updateTemplate();
}
};
// call internally when the model is valid
function refresh(keyboardChange) {
makeValid();
ngModelCtrl.$setViewValue({
minutes: selected.minutes,
seconds: selected.seconds
});
updateTemplate(keyboardChange);
}
function makeValid() {
ngModelCtrl.$setValidity('time', true);
$scope.validity = true;
$scope.invalidMinutes = false;
$scope.invalidSeconds = false;
}
function updateTemplate(keyboardChange) {
var minutes = selected.minutes,
seconds = selected.seconds;
$scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
$scope.seconds = keyboardChange === 's' ? seconds : pad(seconds);
}
function pad(value) {
return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
}
function getMinutesFromTemplate() {
var minutes = parseInt($scope.minutes, 10);
return (minutes >= 0) ? minutes : undefined;
}
function getSecondsFromTemplate() {
var seconds = parseInt($scope.seconds, 10);
if(seconds >= 60) {
seconds = 59;
}
return (seconds >= 0) ? seconds : undefined;
}
$scope.incrementMinutes = function() {
addSeconds(minuteStep * 60);
};
$scope.decrementMinutes = function() {
addSeconds(-minuteStep * 60);
};
$scope.incrementSeconds = function() {
addSeconds(secondStep);
};
$scope.decrementSeconds = function() {
addSeconds(-secondStep);
};
function addSeconds(seconds) {
var newSeconds = selected.minutes * 60 + selected.seconds + seconds;
if(newSeconds < 0) {
newSeconds = 0;
}
selected = {
minutes: Math.floor(newSeconds / 60),
seconds: newSeconds % 60
};
refresh();
}
$scope.previewTime = function(minutes, seconds) {
var totalSeconds = parseInt(minutes, 10) * 60 + parseInt(seconds, 10),
hh = pad(Math.floor(totalSeconds / 3600)),
mm = pad(minutes % 60),
ss = pad(seconds);
return hh + ':' + mm + ':' + ss;
};
}]);
對(duì)應(yīng)的Template實(shí)現(xiàn):
<table>
<tbody>
<tr class="text-center">
<td>
<a ng-click="incrementMinutes()" class="btn btn-link">
<span class="glyphicon glyphicon-chevron-up"></span>
</a>
</td>
<td> </td>
<td>
<a ng-click="incrementSeconds()" class="btn btn-link">
<span class="glyphicon glyphicon-chevron-up"></span>
</a>
</td>
<td> </td>
</tr>
<tr>
<td style="width:50px;" class="form-group" ng-class="{'has-error': invalidMinutes}">
<input type="text" ng-model="minutes" ng-change="updateMinutes()" class="form-control text-center" ng-mousewheel="incrementMinutes()" ng-readonly="readonlyInput" maxlength="3">
</td>
<td>:</td>
<td style="width:50px;" class="form-group" ng-class="{'has-error': invalidSeconds}">
<input type="text" ng-model="seconds" ng-change="updateSeconds()" class="form-control text-center" ng-mousewheel="incrementSeconds()" ng-readonly="readonlyInput" maxlength="2">
<td>
<!-- preview column -->
<td>
<span class="label label-primary" ng-show="validity">
{{ previewTime(minutes, seconds) }}
</span>
</td>
</tr>
<tr class="text-center">
<td>
<a ng-click="decrementMinutes()" class="btn btn-link">
<span class="glyphicon glyphicon-chevron-down"></span>
</a>
</td>
<td> </td>
<td>
<a ng-click="decrementSeconds()" class="btn btn-link">
<span class="glyphicon glyphicon-chevron-down"></span>
</a>
</td>
<td> </td>
</tr>
</tbody>
</table>
測(cè)試代碼(即前面截圖dialog的源代碼):
<div class="modal-header">
<h3 class="modal-title">Highlight on <span class="label label-primary">{{ movieName }}</span></h3>
</div>
<div class="modal-body">
<div class="row">
<div id="highlight-start" class="col-xs-6">
<h4>Start Time:</h4>
<minute-second-picker ng-model="startTime" validity="startTimeValidity"></minute-second-picker>
</div>
<div id="highlight-end" class="col-xs-6">
<h4>End Time:</h4>
<minute-second-picker ng-model="endTime" validity="endTimeValidity"></minute-second-picker>
</div>
</div>
<div class="row">
<div class="col-xs-2">
Tags:
</div>
<div class="col-xs-10">
<tags model="tags" src="s as s.name for s in sourceTags" options="{ addable: 'true' }"></tags>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()" ng-disabled="!startTimeValidity || !endTimeValidity || durationIncorrect(endTime, startTime)">OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
如果大家還想深入學(xué)習(xí),可以點(diǎn)擊這里進(jìn)行學(xué)習(xí),再為大家附3個(gè)精彩的專(zhuān)題:
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Bootstrap時(shí)間選擇器datetimepicker和daterangepicker使用實(shí)例解析
- angularjs封裝bootstrap時(shí)間插件datetimepicker
- bootstrap datetimepicker日期插件使用方法
- bootstrap-datetimepicker實(shí)現(xiàn)只顯示到日期的方法
- Bootstrap3 datetimepicker控件使用實(shí)例
- bootstrap datetimepicker日期插件超詳細(xì)使用方法介紹
- 基于bootstrap-datetimepicker.js不支持IE8的快速解決方法
- bootstrap datetimepicker實(shí)現(xiàn)秒鐘選擇下拉框
- AngularJs中Bootstrap3 datetimepicker使用實(shí)例
- angular bootstrap timepicker TypeError提示怎么辦
相關(guān)文章
Angular4實(shí)現(xiàn)圖片上傳預(yù)覽路徑不安全的問(wèn)題解決
這篇文章主要給大家介紹了關(guān)于Angular4實(shí)現(xiàn)圖片上傳預(yù)覽路徑不安全問(wèn)題的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12
Angular引入swiper后autoplay失效的解決辦法詳解
這篇文章主要介紹了Angular引入swiper后autoplay失效的解決辦法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
angular4 如何在全局設(shè)置路由跳轉(zhuǎn)動(dòng)畫(huà)的方法
本篇文章主要介紹了angular4 如何在全局設(shè)置路由跳轉(zhuǎn)動(dòng)畫(huà)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
angular ng-model 無(wú)法獲取值的處理方法
今天小編就為大家分享一篇angular ng-model 無(wú)法獲取值的處理方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10
詳解Angular Reactive Form 表單驗(yàn)證
本文我們將介紹 Reactive Form 表單驗(yàn)證的相關(guān)知識(shí),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
Angularjs單選改為多選的開(kāi)發(fā)過(guò)程及問(wèn)題解析
在項(xiàng)目中遇到這樣的需求想把下拉框的單選改為多選,怎么實(shí)現(xiàn)呢?下面小編通過(guò)本文給大家分享angularjs單選改為多選的開(kāi)發(fā)過(guò)程及問(wèn)題解析,需要的朋友參考下2017-02-02

