仿Angular Bootstrap TimePicker创建分钟数-秒数的输入控件


Posted in Javascript onJuly 01, 2016

在一个项目中需要一个用来输入分钟数和秒数的控件,然而调查了一些开源项目后并未发现合适的控件。在Angular Bootstrap UI中有一个类似的控件TimePicker,但是它并没有深入到分钟和秒的精度。
因此,决定参考它的源码然后自己进行实现。 
最终的效果如下:

 仿Angular Bootstrap TimePicker创建分钟数-秒数的输入控件

首先是该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函数中,ctrls是一个数组: ctrls[0]是定义在本directive上的controller实例,ctrls[1]是ngModelCtrl,即ng-model对应的controller实例。这个顺序实际上是通过require: ['minuteSecondPicker', '?^ngModel']定义的。
注意到第一个依赖就是directive本身的名字,此时会将该directive中controller声明的对应实例传入。第二个依赖的写法有些奇怪:"?^ngModel",?的含义是即使没有找到该依赖,也不要抛出异常,即该依赖是一个可选项。^的含义是查找父元素的controller。
然后,定义该directive中用到的一些默认设置,通过constant directive实现:

app.constant('minuteSecondPickerConfig', {
 minuteStep: 1,
 secondStep: 1,
 readonlyInput: false,
 mousewheel: true
});

紧接着是directive对应的controller,它的声明如下:

app.controller('minuteSecondPickerController', ['$scope', '$attrs', '$parse', 'minuteSecondPickerConfig', 
 function($scope, $attrs, $parse, minuteSecondPickerConfig) {
 ...
}]);

在directive的link函数中,调用了此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方法接受的第二个参数是inputs,在link函数中传入的是:element.find('input')。 所以第一个输入框用来输入分钟,第二个输入框用来输入秒。
然后,检查是否覆盖了mousewheel属性,如果没有覆盖则使用在constant中设置的默认mousewheel,并进行相关设置如下:

// 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方法最后会对inputs本身进行一些设置: 

// 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);
  });
  }
 });
 };

此方法中,声明了用于设置输入非法的invalidate函数,它会在scope中暴露一个validity = false属性让页面有机会做出合适的反应。
 如果用户使用了一个变量来表示minuteStep或者secondStep,那么还需要设置相应的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实现代码如下:

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;
 };
}]);

对应的Template实现: 

<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>

测试代码(即前面截图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>

如果大家还想深入学习,可以点击这里进行学习,再为大家附3个精彩的专题:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
LBS blog sql注射漏洞[All version]-官方已有补丁
Aug 26 Javascript
javascript this用法小结
Dec 19 Javascript
event.X和event.clientX的区别分析
Oct 06 Javascript
jQuery读取XML文件内容的方法
Mar 09 Javascript
JavaScript中的toLocaleDateString()方法使用简介
Jun 12 Javascript
Highcharts入门之简介
Aug 02 Javascript
jquery插件ContextMenu设置右键菜单
Mar 13 Javascript
AngularJS实现页面定时刷新
Mar 14 Javascript
微信小程序滚动Tab实现左右可滑动切换
Aug 17 Javascript
JS实现页面跳转与刷新的方法汇总
Aug 30 Javascript
微信小程序scroll-view的滚动条设置实现
Mar 02 Javascript
jQuery+Ajax+js实现请求json格式数据并渲染到html页面操作示例
Jun 02 jQuery
精彩的Bootstrap案例分享 重点在注释!(选项卡、栅格布局)
Jul 01 #Javascript
很棒的Bootstrap选项卡切换效果
Jul 01 #Javascript
AngularJS优雅的自定义指令
Jul 01 #Javascript
如何解决手机浏览器页面点击不跳转浏览器双击放大网页
Jul 01 #Javascript
再谈Javascript中的基本类型和引用类型(推荐)
Jul 01 #Javascript
JavaScript中有关一个数组中最大值和最小值及它们的下表的输出的解决办法
Jul 01 #Javascript
Bootstrap编写一个兼容主流浏览器的受众门户式风格页面
Jul 01 #Javascript
You might like
ThinkPHP采用原生query实现关联查询left join实例
2014/12/02 PHP
JS中==与===操作符的比较
2009/03/21 Javascript
jquery分页插件AmSetPager(自写)
2013/04/15 Javascript
getJSON调用后台json数据时函数被调用两次的原因猜想
2013/09/29 Javascript
jquery根据name属性查找的小例子
2013/11/21 Javascript
解决extjs grid 不随窗口大小自适应的改变问题
2014/01/26 Javascript
关于img的href和src取变量及赋值的方法
2014/04/28 Javascript
jQuery插件slick实现响应式移动端幻灯片图片切换特效
2015/04/12 Javascript
轻松掌握JavaScript策略模式
2016/08/25 Javascript
每个程序员都需要学习 JavaScript 的7个理由小结
2016/09/03 Javascript
浅谈jquery上下滑动的注意事项
2016/10/13 Javascript
Vuex之理解Getters的用法实例
2017/04/19 Javascript
详谈Angular 2+ 的表单(一)之模板驱动型表单
2017/04/25 Javascript
vue2.0 中#$emit,$on的使用详解
2017/06/07 Javascript
Vue计算属性的使用
2017/08/04 Javascript
JS实现运动缓冲效果的封装函数示例
2018/02/18 Javascript
Angular5中状态管理的实现
2018/09/03 Javascript
vue页面跳转实现页面缓存操作
2020/07/22 Javascript
[03:07]【DOTA2亚洲邀请赛】我们,梦开始的地方
2017/03/07 DOTA
python使用mailbox打印电子邮件的方法
2015/04/30 Python
Python编程django实现同一个ip十分钟内只能注册一次
2017/11/03 Python
Python对数据进行插值和下采样的方法
2018/07/03 Python
ubuntu 16.04下python版本切换的方法
2019/06/14 Python
Python爬虫之Selenium鼠标事件的实现
2020/12/04 Python
Html5内唤醒百度、高德APP的实现示例
2019/05/20 HTML / CSS
美国知名生活购物网站:Goop
2017/11/03 全球购物
美国波西米亚风格服装品牌:Show Me Your Mumu
2018/01/05 全球购物
日本最大美瞳直送网:Morecontact(中文)
2019/04/03 全球购物
StubHub新西兰:购买和出售你的门票
2019/04/22 全球购物
美国购买体育、音乐会和剧院门票网站:SelectATicket
2019/09/08 全球购物
Penhaligon’s英国官网:成立于1870年的英国香水制造商
2021/02/18 全球购物
应届生骨科医生求职信
2013/10/31 职场文书
初二政治教学反思
2014/01/12 职场文书
淘宝店铺营销方案
2014/02/13 职场文书
企业节能减排实施方案
2014/03/19 职场文书
直播实况, OMG破敌三路五十分钟大战神技局摩托车
2022/04/01 DOTA