2 * Copyright (C) 2015 "IoT.bzh"
3 * Author "Fulup Ar Foll"
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 * Bugs: Input with Callback SHOULD BE get 'required' class
20 * ref: https://developer.mozilla.org/en-US/docs/Web/Events/mouseover
26 id="my-slider-name" // only use as an argument to callback
27 class="my-custom-class" // default class is ibz-range-slider
28 placeholder="Track Date Selection" // place holder for date readonly input zone
30 <!-- Foundation classes -->
31 class="radius" // check Zurb foundation doc for further info.
32 class="ibz-handle-display" // increase handle width to hold slider current value
34 <!-- Angular Scope Variables -->
35 callback="myCallBack" // $scope.myCallBack(sliderhandle) is called when ever slider handle blur
36 formatter="SliderFormatCB" // $scope.myFormatter(value, sliderid) when exist is call when ever slider handle moves. Should return external form of slider value.
37 ng-model="xxxxxx" // xxx Must be defined, script will store a new RangerObject within provided ng-model variable.
38 start-at="ScopeVar" // Dynamic limitation when slider is constrains by an external componant [ex: check in/out]
39 stop-at="ScopeVar" // Idem but for end.
41 <!-- Angular Directive Attributes -->
42 not-less="integer" // Fixed starting value for slider [default 0]
43 not-more="integer" // Fixed end value for sliders [default 100]
44 by-step="+-integer" // If by-step is >0 then slider use it as step-value, when negative use it for decimal precision
45 display-target="handle" // display slider external formated value in the handle [requirer calss="ibz-handle-display"]
46 dual-handles='true' // add a second handle to slider for min/max range
47 initial='value|[start/stop]' // slider initial value [dual-handles] may have initial values
54 var RangeSlider = angular.module('RangeSlider',[]);
56 function RangeSliderHandle (scope) {
60 this.getId = function() {
61 return scope.sliderid;
64 this.getCbHandle = function() {
65 return scope.cbhandle;
68 this.getView= function (handle) {
69 if (!handle) handle = 0;
71 // if value did not change return current external representation
72 if (scope.value[handle] === internals[handle]) return externals[handle];
74 // build external representation and save it for further requests
75 internals[handle] = scope.value[handle];
76 if (scope.formatter) externals[handle] = scope.formatter(scope.value[handle], scope.ctrlhandle);
77 else externals[handle] = scope.value[handle];
79 return externals[handle];
82 this.updateClass = function (classe, status) {
83 scope.updateClass (classe, status);
86 this.forceRefresh = function (timer) {
87 scope.forceRefresh(timer);
90 this.getValue= function (handle) {
91 if (!handle) handle = 0;
92 return scope.value[handle];
95 this.getRelative= function (handle) {
96 if (!handle) handle = 0;
97 return scope.relative[handle];
100 this.setValue= function (value, handle) {
101 if (!handle) handle = 0;
102 scope.setValue (value, handle);
105 this.setDisable= function (flag) {
106 scope.setDisable(flag);
110 RangeSlider.directive('rangeSlider', function ($log, $document, $timeout) {
112 var template= '<div class="ibz-range-slider range-slider" title="{{title}}"data-slider>'+
113 '<span class="range-slider-handle handle-min" ng-mousedown="handleCB($event,0)" ng-focus="focusCB(true)" ng-blur="focusCB(false)" role="slider" tabindex="0"></span>'+
114 '<span class="handle-max" ng-mousedown="handleCB($event,1)" ng-focus="focusCB(true)" ng-blur="focusCB(false)" role="slider" tabindex="0"></span>'+
115 '<span class="range-slider-active-segment"></span>'+
116 '<span class="ibz-range-slider-start" ></span> '+
117 '<span class="ibz-range-slider-stop"></span> '+
118 '<input id={{sliderid}} type="hidden">'+
122 function link (scope, element, attrs, model) {
123 // full initialisation of slider from a single object
124 scope.initWidget = function (initvalues) {
126 if (initvalues.byStep) scope.byStep = parseInt(initvalues.byStep);
127 if (initvalues.notMore) scope.notMore = parseInt(initvalues.notMore);
128 if (initvalues.notLess) scope.notLess = parseInt(initvalues.notLess);
129 if (initvalues.id) scope.sliderid= initvalues.id;
131 // hugely but in some case DOM is not finish when we try to set values !!!
132 if (initvalues.value !== undefined) {
133 scope.value = initvalues.value;
134 scope.forceRefresh (50); // wait 50ms for DOM to be ready
138 // this function recompute slide positioning
139 scope.forceRefresh = function (timer) {
140 var value = scope.value;
141 scope.value = [undefined,undefined];
142 $timeout (function() {
143 scope.setValue(value[0],0);
144 if (scope.dual) scope.setValue(value[1],1);
148 // handler to change class from slider handle
149 scope.updateClass = function (classe, status) {
151 if (status) element.addClass (classe);
152 else element.removeClass (classe);
155 scope.setDisable = function (disabled) {
158 element.addClass ("disable");
159 scope.handles[0].css ('visibility','hidden');
161 scope.handles[1].css ('visibility','hidden');
164 element.removeClass ("disable");
165 scope.handles[0].css ('visibility','visible');
166 if (scope.dual) scope.handles[1].css ('visibility','visible');
171 scope.normalize = function (value) {
173 var range = scope.notMore - scope.notLess;
174 var point = value * range;
176 // if step is positive let's round step by step
177 if (scope.byStep > 0) {
178 var mod = (point - (point % scope.byStep)) / scope.byStep;
179 var rem = point % scope.byStep;
181 var round = (rem >= scope.byStep * 0.5 ? scope.byStep : 0);
182 result= (mod * scope.byStep + round) + scope.notLess;
183 //console.log ("range=%d value=%d point=%d mod=%d rem=%d round=%d result=%d", range, value, point, mod, rem, round, result)
187 // if step is negative return round to asked decimal
188 if (scope.byStep < 0) {
189 var power = Math.pow (10,(scope.byStep * -1));
190 result = scope.notLess + parseInt (point * power) / power;
194 // if step is null return full value
198 // return current value
199 scope.getValue = function (offset, handle) {
200 if (scope.vertical) {
201 scope.relative[handle] = (offset - scope.bounds.handles[handle].getBoundingClientRect().height) / (scope.bounds.bar.getBoundingClientRect().height - scope.bounds.handles[handle].getBoundingClientRect().height);
203 scope.relative[handle] = offset / (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
206 var newvalue = scope.normalize (scope.relative[handle]);
209 // if internal value change update or model
210 if (newvalue !== scope.value[handle]) {
211 if (newvalue < scope.startValue) newvalue=scope.startValue;
212 if (newvalue > scope.stopValue) newvalue=scope.stopValue;
215 if (scope.formatter) {
216 scope.viewValue = scope.formatter (newvalue, scope.ctrlhandle);
218 scope.viewValue = newvalue;
220 if (scope.displays[handle]) {
221 scope.displays[handle].html (scope.viewValue);
224 // update external representation of the model
225 scope.value[handle] = newvalue;
226 if (model) model.$setViewValue (scope.viewValue);
228 if (newvalue > scope.startValue && newvalue < scope.stopValue) scope.translate(offset, handle);
233 scope.setStart = function (value) {
236 if (value > scope.value[0]) {
237 if (!scope.dual) scope.setValue (value,0);
238 else scope.setValue (value,1);
241 if (scope.vertical) {
242 offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
243 scope.start.css('height',offset + 'px');
245 offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
246 scope.start.css('width',offset + 'px');
249 scope.startValue= value;
252 scope.setStop = function (value) {
255 if (value < scope.value[0]) {
256 if (!scope.dual) scope.setValue (value,0);
257 else scope.setValue (value,1);
260 if (scope.vertical) {
261 offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
262 scope.start.css('height',offset + 'px');
264 offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
265 scope.stop.css({'right': 0, 'width': (scope.bounds.bar.getBoundingClientRect().width - offset) + 'px'});
268 scope.stopValue= value;
271 scope.translate = function (offset, handle) {
274 if (scope.vertical) {
275 // take handle size in account to compute middle
276 var voffset = scope.bounds.bar.getBoundingClientRect().height - offset;
278 scope.handles[handle].css({
279 '-webkit-transform': 'translateY(' + voffset + 'px)',
280 '-moz-transform': 'translateY(' + voffset + 'px)',
281 '-ms-transform': 'translateY(' + voffset + 'px)',
282 '-o-transform': 'translateY(' + voffset + 'px)',
283 'transform': 'translateY(' + voffset + 'px)'
285 if (!scope.dual) scope.slider.css('height', offset + 'px');
286 else if (scope.relative[1] && scope.relative[0]) {
287 var height = (scope.relative[1] - scope.relative[0]) * scope.bounds.bar.getBoundingClientRect().height;
288 start = (scope.relative[0] * scope.bounds.bar.getBoundingClientRect().height);
289 scope.slider.css ({'bottom': start+'px','height': height + 'px'});
293 scope.handles[handle].css({
294 '-webkit-transform': 'translateX(' + offset + 'px)',
295 '-moz-transform': 'translateX(' + offset + 'px)',
296 '-ms-transform': 'translateX(' + offset + 'px)',
297 '-o-transform': 'translateX(' + offset + 'px)',
298 'transform': 'translateX(' + offset + 'px)'
300 if (!scope.dual) scope.slider.css('width',offset + 'px');
301 else if (scope.relative[1] && scope.relative[0]) {
302 var width = (scope.relative[1] - scope.relative[0]) * scope.bounds.bar.getBoundingClientRect().width;
303 start = (scope.relative[0] * scope.bounds.bar.getBoundingClientRect().width);
304 scope.slider.css ({'left': start+'px','width': width + 'px'});
309 // position handle on the bar depending a given value
310 scope.setValue = function (value , handle) {
313 // if value did not change ignore
314 if (value === scope.value[handle]) return;
315 if (value === undefined) value=0;
316 if (value > scope.notMore) value=scope.notMore;
317 if (value < scope.notLess) value=scope.notLess;
319 if (scope.vertical) {
320 scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
321 if (handle === 0) offset = (scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height) + scope.bounds.handles[handle].getBoundingClientRect().height/2;
322 if (handle === 1) offset = scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height;
325 scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
326 offset = scope.relative[handle] * (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
329 scope.translate (offset,handle);
330 scope.value[handle] = value;
332 if (scope.formatter) {
333 // when call through setValue we do not pass cbHandle
334 scope.viewValue = scope.formatter (value, undefined);
336 scope.viewValue = value;
339 if (model) model.$setViewValue( scope.viewValue);
341 if (scope.displays[handle]) {
342 scope.displays[handle].html (scope.viewValue);
347 // Minimal keystroke handling to close picker with ESC [scope.actif is current handle index]
348 scope.keydown= function(e){
353 if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+scope.byStep), scope.actif));
354 if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+(1 / Math.pow(10, scope.byStep*-1))),scope.actif));
355 if (scope.callback) scope.callback (scope.value[scope.actif], scope.ctrlhandle);
359 if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - scope.byStep), scope.actif));
360 if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - (1 / Math.pow(10, scope.byStep*-1))),scope.actif));
361 if (scope.callback) scope.callback (scope.value[scope.actif], scope.ctrlhandle);
364 scope.handles[scope.actif][0].blur();
368 scope.moveHandle = function (handle, clientX, clientY) {
370 if (scope.vertical) {
371 offset = scope.bounds.bar.getBoundingClientRect().bottom - clientY;
372 if (offset > scope.bounds.bar.getBoundingClientRect().height) offset = scope.bounds.bar.getBoundingClientRect().height;
373 if (offset < scope.bounds.handles[handle].getBoundingClientRect().height) offset = scope.bounds.handles[handle].getBoundingClientRect().height;
375 offset = clientX - scope.bounds.bar.getBoundingClientRect().left;
377 if (offset < 0) offset = 0;
378 if ((clientX + scope.bounds.handles[handle].getBoundingClientRect().width) > scope.bounds.bar.getBoundingClientRect().right) {
379 offset = scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width;
383 scope.getValue (offset, handle);
385 // prevent dual handle to cross
386 if (scope.dual && scope.value [0] > scope.value[1]) {
387 if (handle === 0) scope.setValue (scope.value[0] , 1);
388 else scope.setValue(scope.value[1],0);
393 scope.focusCB = function (inside) {
395 $document.on('keydown',scope.keydown);
397 $document.unbind('keydown',scope.keydown);
401 // bar was touch let move handle to this point
402 scope.touchBarCB = function (event) {
405 var touches = event.changedTouches;
406 var oldvalue = scope.value[handle];
408 event.preventDefault();
410 // if we have two handles select closest one from touch point
412 if (scope.vertical) relative = (touches[0].pageY - scope.bounds.bar.getBoundingClientRect().bottom) / scope.bounds.bar.getBoundingClientRect().height;
413 else relative= (touches[0].pageX - scope.bounds.bar.getBoundingClientRect().left) / scope.bounds.bar.getBoundingClientRect().width;
415 var distance0 = Math.abs(relative - scope.relative[0]);
416 var distance1 = Math.abs(relative - scope.relative[1]);
417 if (distance1 < distance0) handle=1;
420 // move handle to new place
421 scope.moveHandle (handle,touches[0].pageX, touches[0].pageY);
422 if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
425 // handle was touch and drag
426 scope.touchHandleCB = function (touchevt, handle) {
427 var oldvalue = scope.value[handle];
429 touchevt.preventDefault();
430 $document.on('touchmove',touchmove);
431 $document.on('touchend' ,touchend);
432 element.unbind('touchstart', scope.touchBarCB);
434 function touchmove(event) {
435 event.preventDefault();
436 var touches = event.changedTouches;
437 for (var idx = 0; idx < touches.length; idx++) {
438 scope.moveHandle (handle,touches[idx].pageX, touches[idx].pageY);
442 function touchend(event) {
443 $document.unbind('touchmove',touchmove);
444 $document.unbind('touchend' ,touchend);
445 element.on('touchstart', scope.touchBarCB);
447 // if value change notify application callback
448 if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
452 scope.handleCB = function (clickevent, handle) {
454 if (attrs.automatic) return;
456 var oldvalue = scope.value[handle];
457 // register mouse event to track handle
458 clickevent.preventDefault();
460 $document.on('mousemove',mousemove);
461 $document.on('mouseup', mouseup);
462 scope.handles[handle][0].focus();
465 // slider handle is moving
466 function mousemove(event) {
467 scope.moveHandle (handle, event.clientX, event.clientY);
470 // mouse is up dans leave slider send resize events
472 $document.unbind('mousemove', mousemove);
473 $document.unbind('mouseup', mouseup);
475 // if value change notify application callback
476 if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
480 // simulate jquery find by classes capabilities [warning only return 1st elements]
481 scope.find = function (select, elem) {
484 if (elem) domelem = elem[0].querySelector(select);
485 else domelem = element[0].querySelector(select);
487 var angelem = angular.element(domelem);
493 scope.initialSettings = function (initial) {
494 var decimal_places_match_result;
495 scope.value=[]; // store low/height value when two handles
498 if (scope.precision === null) {
499 decimal_places_match_result = ('' + scope.byStep).match(/\.([\d]*)/);
500 scope.precision = decimal_places_match_result && decimal_places_match_result[1] ? decimal_places_match_result[1].length : 0;
503 // position handle to initial value(s)
504 element.on('touchstart', scope.touchBarCB);
505 scope.handles[0].on('touchstart', function(evt){scope.touchHandleCB(evt,0);});
507 // this slider has two handles low/hight
509 scope.handles[1].addClass('range-slider-handle');
510 scope.handles[1].on('touchstart', function(evt){scope.touchHandleCB(evt,1);});
511 if (!scope.initvalues) scope.setValue (initial[1],1);
514 // if we have an initstate object apply it
515 if (scope.initvalues) scope.initWidget (scope.initvalues);
516 else scope.setValue (initial[0],0);
519 scope.init = function () {
520 scope.sliderid = attrs.id || "slider-" + parseInt (Math.random() * 1000);
521 scope.startValue = -Infinity;
522 scope.stopValue = Infinity;
523 scope.byStep = parseInt(attrs.byStep) || 1;
524 scope.vertical = attrs.vertical || false;
525 scope.dual = attrs.dualHandles|| false;
526 scope.trigger_input_change= false;
527 scope.notMore = parseInt(attrs.notMore) || 100;
528 scope.notLess = parseInt(attrs.notLess) || 0;
530 if (scope.vertical) element.addClass("vertical-range");
532 scope.handles= [scope.find('.handle-min'), scope.find('.handle-max')];
534 scope.slider = scope.find('.range-slider-active-segment');
535 scope.start = scope.find('.ibz-range-slider-start');
536 scope.stop = scope.find('.ibz-range-slider-stop');
537 scope.disable= attrs.disable || false;
539 scope.ctrlhandle = new RangeSliderHandle (scope);
541 // prepare DOM object pointer to compute size dynamically
544 handles: [scope.handles[0][0], scope.handles[1][0]]
547 if (attrs.disable === 'true') scope.setDisable(true);
549 if (attrs.displayTarget) {
550 switch (attrs.displayTarget) {
553 scope.displays = scope.handles;
554 scope.handles[0].addClass('ibz-range-slider-display');
555 if (scope.dual) scope.handles[1].addClass('ibz-range-slider-display');
558 scope.displays = [$document.getElementById (attrs.displayTarget)];
560 } else scope.displays=[];
562 // extract initial values from attrs and parse into int
563 if (!attrs.initial) {
564 scope.initial = [scope.ngModel, scope.ngModel]; // initialize to model values
566 var initial = attrs.initial.split(',');
568 initial[0] !== undefined ? parseInt (initial[0]) : scope.notLess,
569 initial[1] !== undefined ? parseInt (initial[1]) : scope.notMore
573 // Monitor any changes on start/stop dates.
574 scope.$watch('startAt', function() {
575 if (scope.value < scope.startAt ) {
576 //scope.setValue (scope.startAt);
578 if (scope.startAt) scope.setStart (scope.startAt);
581 scope.$watch('stopAt' , function() {
582 if (scope.value > scope.stopAt) {
583 //scope.setValue (scope.stopAt);
585 if (scope.stopAt) scope.setStop (scope.stopAt);
588 // finish widget initialisation
589 scope.initialSettings (scope.initial);
595 // slider is ready provide control handle to application controller
596 scope.$watch ('inithook', function () { // init Values may arrive late
597 if (scope.inithook) scope.inithook (scope.ctrlhandle);
600 scope.$watch ('initvalues', function () { // init Values may arrive late
601 if (scope.initvalues) scope.initWidget(scope.initvalues);
604 // two-way binding if model value changes
605 scope.$watch ('ngModel', function (newValue) {
606 scope.setValue(newValue, 0);
611 restrict: "E", // restrict to <range-slider> HTML element name
613 startAt :'=', // First acceptable date
614 stopAt :'=', // Last acceptable date
615 callback :'=', // Callback to actif when a date is selected
616 formatter:'=', // Callback for drag event call each time internal value changes
617 inithook :'=', // Hook point to control slider from API
618 cbhandle :'=', // Argument added to every callback
619 initvalues:'=', // Initial values as a single object
620 ngModel: '=' // the model value
623 template: template, // html template is build from JS
624 replace: true, // replace current directive with template while inheriting of class
625 link: link // pickadate object's methods
629 console.log ("RangeSlider Loaded");