仿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 相关文章推荐
Add Formatted Text to a Word Document
Jun 15 Javascript
JQuery的Validation插件中Remote验证的中文问题
Jul 26 Javascript
关于js遍历表格的实例
Jul 10 Javascript
Javascript中的String对象详谈
Mar 03 Javascript
jQuery中对未来的元素绑定事件用bind、live or on
Apr 17 Javascript
jQuery实现contains方法不区分大小写的方法
Feb 13 Javascript
jquery让指定的元素闪烁显示的方法
Mar 17 Javascript
Angular2表单自定义验证器的实现
Oct 19 Javascript
vue实现的上传图片到数据库并显示到页面功能示例
Mar 17 Javascript
Node.js创建HTTP文件服务器的使用示例
May 11 Javascript
vue+Vue Router多级侧导航切换路由(页面)的实现代码
Dec 20 Javascript
浅析JavaScript 函数柯里化
Sep 08 Javascript
精彩的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
php获取地址栏信息的代码
2008/10/08 PHP
一些PHP Coding Tips(php小技巧)[2011/04/02最后更新]
2011/05/02 PHP
php用户注册页面利用js进行表单验证具体实例
2013/10/17 PHP
php获得网站访问统计信息类Compete API用法实例
2015/04/02 PHP
php实现SAE上使用storage上传与下载文件的方法
2015/06/29 PHP
使用Thinkphp框架开发移动端接口
2015/08/05 PHP
简单谈谈PHP中的include、include_once、require以及require_once语句
2016/04/23 PHP
php 防止表单重复提交两种实现方法
2016/11/03 PHP
javascript ie6兼容position:fixed实现思路
2013/04/01 Javascript
html文件中jquery与velocity变量中的$冲突的解决方法
2013/11/01 Javascript
Javascript获取CSS伪元素属性的实现代码
2014/09/28 Javascript
javascript基于DOM实现权限选择实例分析
2015/05/14 Javascript
深入理解JavaScript的React框架的原理
2015/07/02 Javascript
JS模拟酷狗音乐播放器收缩折叠关闭效果代码
2015/10/29 Javascript
URL中“#” “?” &amp;“”号的作用浅析
2017/02/04 Javascript
React降级配置及Ant Design配置详解
2018/12/27 Javascript
js计算最大公约数和最小公倍数代码实例
2019/09/11 Javascript
vue-router定义元信息meta操作
2020/12/07 Vue.js
举例讲解Python设计模式编程中对抽象工厂模式的运用
2016/03/02 Python
Python内置数据结构与操作符的练习题集锦
2016/07/01 Python
python实现定时提取实时日志程序
2018/06/22 Python
Python requests库用法实例详解
2018/08/14 Python
python解释器spython使用及原理解析
2019/08/24 Python
Java如何基于wsimport调用wcf接口
2020/06/17 Python
德国Discount-Apotheke中文官网:DC德式康线上药房
2020/02/18 全球购物
犯错检讨书
2014/02/21 职场文书
幼儿园见习报告
2014/10/30 职场文书
清明节扫墓活动总结
2015/02/09 职场文书
会计简历自我评价
2015/03/10 职场文书
小学教师个人工作总结2015
2015/04/20 职场文书
裁员通知
2015/04/25 职场文书
公司员工违纪检讨书
2015/05/05 职场文书
卡特教练观后感
2015/06/08 职场文书
2019年特色火锅店的创业计划书模板
2019/08/28 职场文书
教你使用Pandas直接核算Excel中快递费用
2021/05/12 Python
关于HTML编码导致的乱码问题
2021/09/04 HTML / CSS