[AngularJS] 仿照Angular Bootstrap TimePicker创建一个分钟

页面导航:首页 > 网络编程 > JavaScript > [AngularJS] 仿照Angular Bootstrap TimePicker创建一个分钟

[AngularJS] 仿照Angular Bootstrap TimePicker创建一个分钟

来源: 作者: 时间:2016-02-04 09:15 【

在一个项目中需要一个用来输入分钟数和秒数的控件,然而调查了一些开源项目后并未发现合适的控件。在Angular Bootstrap UI中有一个类 20284;的控件TimePicker,但是它并没有深入到分钟和秒

在一个项目中需要一个用来输入分钟数和秒数的控件,然而调查了一些开源项目后并未发现合适的控件。在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实现:

&nbsp; &nbsp;
: {{ previewTime(minutes, seconds) }}
&nbsp; &nbsp;

测试代码(即前面截图dialog的源代码):



Tags:

文章评论

最 近 更 新
热 点 排 行
Js与CSS工具
代码转换工具

<