仿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 相关文章推荐
麦鸡的TAB切换功能结合了javascript和css
Dec 17 Javascript
jquery.AutoComplete.js中文修正版(支持firefox)
Apr 09 Javascript
如何在JavaScript中实现私有属性的写类方式(二)
Dec 04 Javascript
js字符串转换成数字与数字转换成字符串的实现方法
Jan 08 Javascript
谷歌浏览器不支持showModalDialog模态对话框的解决方法
Sep 22 Javascript
基于javascript实现彩票随机数生成(升级版)
Apr 17 Javascript
jquery把int类型转换成字符串类型的方法
Oct 07 Javascript
微信小程序scroll-x失效的完美解决方法
Jul 18 Javascript
从零开始用electron手撸一个截屏工具的示例代码
Oct 10 Javascript
Vue 中文本内容超出规定行数后展开收起的处理的实现方法
Apr 28 Javascript
8个有意思的JavaScript面试题
Jul 30 Javascript
nuxt静态部署打包相对路径操作
Nov 06 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
Opcache导致php-fpm崩溃nginx返回502
2015/03/02 PHP
ZF框架实现发送邮件的方法
2015/12/03 PHP
twig里使用js变量的方法
2016/02/05 PHP
Yii2针对游客、用户防范规则和限制的解决方法分析
2016/10/08 PHP
ThinkPHP实现简单登陆功能
2017/04/28 PHP
关于ThinkPhp 框架表单验证及ajax验证问题
2017/07/19 PHP
PHP中引用类型和值类型功能与用法示例
2019/02/26 PHP
javascript scrollTop正解使用方法
2013/11/14 Javascript
jQuery多级弹出菜单插件ZoneMenu
2014/12/18 Javascript
JQuery实现的图文自动轮播效果插件
2015/06/19 Javascript
js 调用百度分享功能
2017/02/27 Javascript
AngularJS使用ng-class动态增减class样式的方法示例
2017/05/18 Javascript
深入理解Angularjs中$http.post与$.post
2017/05/19 Javascript
解决Jquery下拉框数据动态获取的问题
2018/01/25 jQuery
在 Vue-CLI 中引入 simple-mock实现简易的 API Mock 接口数据模拟
2018/11/28 Javascript
微信小程序使用map组件实现获取定位城市天气或者指定城市天气数据功能
2019/01/22 Javascript
jquery更改元素属性attr()方法操作示例
2020/05/22 jQuery
Vue使用CDN引用项目组件,减少项目体积的步骤
2020/10/30 Javascript
[53:44]DOTA2-DPC中国联赛 正赛 PSG.LGD vs Magma BO3 第一场 1月31日
2021/03/11 DOTA
在Python的Django框架中实现Hacker News的一些功能
2015/04/17 Python
Python使用urllib2模块实现断点续传下载的方法
2015/06/17 Python
详解Python命令行解析工具Argparse
2016/04/20 Python
使用rst2pdf实现将sphinx生成PDF
2016/06/07 Python
使用pandas批量处理矢量化字符串的实例讲解
2018/07/10 Python
用python建立两个Y轴的XY曲线图方法
2019/07/08 Python
python读文件的步骤
2019/10/08 Python
python实现文件批量编码转换及注意事项
2019/10/14 Python
python用TensorFlow做图像识别的实现
2020/04/21 Python
Python迭代器协议及for循环工作机制详解
2020/07/14 Python
matplotlib 画动态图以及plt.ion()和plt.ioff()的使用详解
2021/01/05 Python
非功能性需求都包括哪些方面
2013/10/29 面试题
证券期货行业个人的自我评价
2013/12/26 职场文书
教师党的群众路线教育实践活动个人整改方案
2014/10/31 职场文书
长城导游词
2015/01/30 职场文书
python如何进行基准测试
2021/04/26 Python
mysql下的max_allowed_packet参数设置详解
2022/02/12 MySQL