[AngularJS] 仿照Angular Bootstrap TimePicker创建一个分钟数-秒数的输入控件(minuteSecondPicker)

来源:互联网 发布:驱魔少年网络大电影 编辑:程序博客网 时间:2021/05/15 19:42

在一个项目中需要一个用来输入分钟数和秒数的控件,然而调查了一些开源项目后并未发现合适的控件。在Angular Bootstrap UI中有一个类似的控件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>&nbsp;</td>            <td>                <a ng-click="incrementSeconds()" class="btn btn-link">                    <span class="glyphicon glyphicon-chevron-up"></span>                </a>            </td>            <td>&nbsp;</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>&nbsp;</td>            <td>                <a ng-click="decrementSeconds()" class="btn btn-link">                    <span class="glyphicon glyphicon-chevron-down"></span>                </a>            </td>            <td>&nbsp;</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>
2 0