/*
* Copyright (C) 2015 "IoT.bzh"
* Author "Fulup Ar Foll"
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* Bugs: Input with Callback SHOULD BE get 'required' class
*
* ref: https://developer.mozilla.org/en-US/docs/Web/Events/mouseover
*
* usage:
Usage
---------------------
class="radius" // check Zurb foundation doc for further info.
class="ibz-handle-display" // increase handle width to hold slider current value
callback="myCallBack" // $scope.myCallBack(sliderhandle) is called when ever slider handle blur
formatter="SliderFormatCB" // $scope.myFormatter(value, sliderid) when exist is call when ever slider handle moves. Should return external form of slider value.
ng-model="xxxxxx" // xxx Must be defined, script will store a new RangerObject within provided ng-model variable.
start-at="ScopeVar" // Dynamic limitation when slider is constrains by an external componant [ex: check in/out]
stop-at="ScopeVar" // Idem but for end.
not-less="integer" // Fixed starting value for slider [default 0]
not-more="integer" // Fixed end value for sliders [default 100]
by-step="+-integer" // If by-step is >0 then slider use it as step-value, when negative use it for decimal precision
display-target="handle" // display slider external formated value in the handle [requirer calss="ibz-handle-display"]
dual-handles='true' // add a second handle to slider for min/max range
initial='value|[start/stop]' // slider initial value [dual-handles] may have initial values
/>
*/
(function () {
'use strict';
var RangeSlider = angular.module('RangeSlider',[]);
function RangeSliderHandle (scope) {
var internals = [];
var externals = [];
this.getId = function() {
return scope.sliderid;
};
this.getCbHandle = function() {
return scope.cbhandle;
};
this.getView= function (handle) {
if (!handle) handle = 0;
// if value did not change return current external representation
if (scope.value[handle] === internals[handle]) return externals[handle];
// build external representation and save it for further requests
internals[handle] = scope.value[handle];
if (scope.formatter) externals[handle] = scope.formatter(scope.value[handle], scope.ctrlhandle);
else externals[handle] = scope.value[handle];
return externals[handle];
};
this.updateClass = function (classe, status) {
scope.updateClass (classe, status);
};
this.forceRefresh = function (timer) {
scope.forceRefresh(timer);
};
this.getValue= function (handle) {
if (!handle) handle = 0;
return scope.value[handle];
};
this.getRelative= function (handle) {
if (!handle) handle = 0;
return scope.relative[handle];
};
this.setValue= function (value, handle) {
if (!handle) handle = 0;
scope.setValue (value, handle);
};
this.setDisable= function (flag) {
scope.setDisable(flag);
};
}
RangeSlider.directive('rangeSlider', function ($log, $document, $timeout) {
var template= '
'+
''+
''+
''+
' '+
' '+
''+
'
';
function link (scope, element, attrs, model) {
// full initialisation of slider from a single object
scope.initWidget = function (initvalues) {
if (initvalues.byStep) scope.byStep = parseInt(initvalues.byStep);
if (initvalues.notMore) scope.notMore = parseInt(initvalues.notMore);
if (initvalues.notLess) scope.notLess = parseInt(initvalues.notLess);
if (initvalues.id) scope.sliderid= initvalues.id;
// hugely but in some case DOM is not finish when we try to set values !!!
if (initvalues.value !== undefined) {
scope.value = initvalues.value;
scope.forceRefresh (50); // wait 50ms for DOM to be ready
}
};
// this function recompute slide positioning
scope.forceRefresh = function (timer) {
var value = scope.value;
scope.value = [undefined,undefined];
$timeout (function() {
scope.setValue(value[0],0);
if (scope.dual) scope.setValue(value[1],1);
}, timer);
};
// handler to change class from slider handle
scope.updateClass = function (classe, status) {
if (status) element.addClass (classe);
else element.removeClass (classe);
};
scope.setDisable = function (disabled) {
if (disabled) {
element.addClass ("disable");
scope.handles[0].css ('visibility','hidden');
if (scope.dual) {
scope.handles[1].css ('visibility','hidden');
}
} else {
element.removeClass ("disable");
scope.handles[0].css ('visibility','visible');
if (scope.dual) scope.handles[1].css ('visibility','visible');
}
};
scope.normalize = function (value) {
var result;
var range = scope.notMore - scope.notLess;
var point = value * range;
// if step is positive let's round step by step
if (scope.byStep > 0) {
var mod = (point - (point % scope.byStep)) / scope.byStep;
var rem = point % scope.byStep;
var round = (rem >= scope.byStep * 0.5 ? scope.byStep : 0);
result= (mod * scope.byStep + round) + scope.notLess;
//console.log ("range=%d value=%d point=%d mod=%d rem=%d round=%d result=%d", range, value, point, mod, rem, round, result)
return result;
}
// if step is negative return round to asked decimal
if (scope.byStep < 0) {
var power = Math.pow (10,(scope.byStep * -1));
result = scope.notLess + parseInt (point * power) / power;
return (result);
}
// if step is null return full value
return point;
};
// return current value
scope.getValue = function (offset, handle) {
if (scope.vertical) {
scope.relative[handle] = (offset - scope.bounds.handles[handle].getBoundingClientRect().height) / (scope.bounds.bar.getBoundingClientRect().height - scope.bounds.handles[handle].getBoundingClientRect().height);
} else {
scope.relative[handle] = offset / (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
}
var newvalue = scope.normalize (scope.relative[handle]);
// if internal value change update or model
if (newvalue !== scope.value[handle]) {
if (newvalue < scope.startValue) newvalue=scope.startValue;
if (newvalue > scope.stopValue) newvalue=scope.stopValue;
if (scope.formatter) {
scope.viewValue = scope.formatter (newvalue, scope.ctrlhandle);
} else {
scope.viewValue = newvalue;
}
if (scope.displays[handle]) {
scope.displays[handle].html (scope.viewValue);
}
// update external representation of the model
scope.value[handle] = newvalue;
if (model) model.$setViewValue (scope.viewValue);
scope.$apply();
if (newvalue > scope.startValue && newvalue < scope.stopValue) scope.translate(offset, handle);
}
};
scope.setStart = function (value) {
var offset;
if (value > scope.value[0]) {
if (!scope.dual) scope.setValue (value,0);
else scope.setValue (value,1);
}
if (scope.vertical) {
offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
scope.start.css('height',offset + 'px');
} else {
offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
scope.start.css('width',offset + 'px');
}
scope.startValue= value;
};
scope.setStop = function (value) {
var offset;
if (value < scope.value[0]) {
if (!scope.dual) scope.setValue (value,0);
else scope.setValue (value,1);
}
if (scope.vertical) {
offset = scope.bounds.bar.getBoundingClientRect().height * (value - scope.notLess) / (scope.notMore - scope.notLess);
scope.start.css('height',offset + 'px');
} else {
offset = scope.bounds.bar.getBoundingClientRect().width * (value - scope.notLess) / (scope.notMore - scope.notLess);
scope.stop.css({'right': 0, 'width': (scope.bounds.bar.getBoundingClientRect().width - offset) + 'px'});
}
scope.stopValue= value;
};
scope.translate = function (offset, handle) {
var start;
if (scope.vertical) {
// take handle size in account to compute middle
var voffset = scope.bounds.bar.getBoundingClientRect().height - offset;
scope.handles[handle].css({
'-webkit-transform': 'translateY(' + voffset + 'px)',
'-moz-transform': 'translateY(' + voffset + 'px)',
'-ms-transform': 'translateY(' + voffset + 'px)',
'-o-transform': 'translateY(' + voffset + 'px)',
'transform': 'translateY(' + voffset + 'px)'
});
if (!scope.dual) scope.slider.css('height', offset + 'px');
else if (scope.relative[1] && scope.relative[0]) {
var height = (scope.relative[1] - scope.relative[0]) * scope.bounds.bar.getBoundingClientRect().height;
start = (scope.relative[0] * scope.bounds.bar.getBoundingClientRect().height);
scope.slider.css ({'bottom': start+'px','height': height + 'px'});
}
} else {
scope.handles[handle].css({
'-webkit-transform': 'translateX(' + offset + 'px)',
'-moz-transform': 'translateX(' + offset + 'px)',
'-ms-transform': 'translateX(' + offset + 'px)',
'-o-transform': 'translateX(' + offset + 'px)',
'transform': 'translateX(' + offset + 'px)'
});
if (!scope.dual) scope.slider.css('width',offset + 'px');
else if (scope.relative[1] && scope.relative[0]) {
var width = (scope.relative[1] - scope.relative[0]) * scope.bounds.bar.getBoundingClientRect().width;
start = (scope.relative[0] * scope.bounds.bar.getBoundingClientRect().width);
scope.slider.css ({'left': start+'px','width': width + 'px'});
}
}
};
// position handle on the bar depending a given value
scope.setValue = function (value , handle) {
var offset;
// if value did not change ignore
if (value === scope.value[handle]) return;
if (value === undefined) value=0;
if (value > scope.notMore) value=scope.notMore;
if (value < scope.notLess) value=scope.notLess;
if (scope.vertical) {
scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
if (handle === 0) offset = (scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height) + scope.bounds.handles[handle].getBoundingClientRect().height/2;
if (handle === 1) offset = scope.relative[handle] * scope.bounds.bar.getBoundingClientRect().height;
} else {
scope.relative[handle] = (value - scope.notLess) / (scope.notMore - scope.notLess);
offset = scope.relative[handle] * (scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width);
}
scope.translate (offset,handle);
scope.value[handle] = value;
if (scope.formatter) {
// when call through setValue we do not pass cbHandle
scope.viewValue = scope.formatter (value, undefined);
} else {
scope.viewValue = value;
}
if (model) model.$setViewValue( scope.viewValue);
if (scope.displays[handle]) {
scope.displays[handle].html (scope.viewValue);
}
};
// Minimal keystroke handling to close picker with ESC [scope.actif is current handle index]
scope.keydown= function(e){
switch(e.keyCode){
case 39: // Right
case 38: // up
if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+scope.byStep), scope.actif));
if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif]+(1 / Math.pow(10, scope.byStep*-1))),scope.actif));
if (scope.callback) scope.callback (scope.value[scope.actif], scope.ctrlhandle);
break;
case 37: // left
case 40: // down
if (scope.byStep > 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - scope.byStep), scope.actif));
if (scope.byStep < 0) scope.$apply(scope.setValue ((scope.value[scope.actif] - (1 / Math.pow(10, scope.byStep*-1))),scope.actif));
if (scope.callback) scope.callback (scope.value[scope.actif], scope.ctrlhandle);
break;
case 27: // esc
scope.handles[scope.actif][0].blur();
}
};
scope.moveHandle = function (handle, clientX, clientY) {
var offset;
if (scope.vertical) {
offset = scope.bounds.bar.getBoundingClientRect().bottom - clientY;
if (offset > scope.bounds.bar.getBoundingClientRect().height) offset = scope.bounds.bar.getBoundingClientRect().height;
if (offset < scope.bounds.handles[handle].getBoundingClientRect().height) offset = scope.bounds.handles[handle].getBoundingClientRect().height;
} else {
offset = clientX - scope.bounds.bar.getBoundingClientRect().left;
if (offset < 0) offset = 0;
if ((clientX + scope.bounds.handles[handle].getBoundingClientRect().width) > scope.bounds.bar.getBoundingClientRect().right) {
offset = scope.bounds.bar.getBoundingClientRect().width - scope.bounds.handles[handle].getBoundingClientRect().width;
}
}
scope.getValue (offset, handle);
// prevent dual handle to cross
if (scope.dual && scope.value [0] > scope.value[1]) {
if (handle === 0) scope.setValue (scope.value[0] , 1);
else scope.setValue(scope.value[1],0);
}
};
scope.focusCB = function (inside) {
if (inside) {
$document.on('keydown',scope.keydown);
} else {
$document.unbind('keydown',scope.keydown);
}
};
// bar was touch let move handle to this point
scope.touchBarCB = function (event) {
var handle=0;
var relative;
var touches = event.changedTouches;
var oldvalue = scope.value[handle];
event.preventDefault();
// if we have two handles select closest one from touch point
if (scope.dual) {
if (scope.vertical) relative = (touches[0].pageY - scope.bounds.bar.getBoundingClientRect().bottom) / scope.bounds.bar.getBoundingClientRect().height;
else relative= (touches[0].pageX - scope.bounds.bar.getBoundingClientRect().left) / scope.bounds.bar.getBoundingClientRect().width;
var distance0 = Math.abs(relative - scope.relative[0]);
var distance1 = Math.abs(relative - scope.relative[1]);
if (distance1 < distance0) handle=1;
}
// move handle to new place
scope.moveHandle (handle,touches[0].pageX, touches[0].pageY);
if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
};
// handle was touch and drag
scope.touchHandleCB = function (touchevt, handle) {
var oldvalue = scope.value[handle];
touchevt.preventDefault();
$document.on('touchmove',touchmove);
$document.on('touchend' ,touchend);
element.unbind('touchstart', scope.touchBarCB);
function touchmove(event) {
event.preventDefault();
var touches = event.changedTouches;
for (var idx = 0; idx < touches.length; idx++) {
scope.moveHandle (handle,touches[idx].pageX, touches[idx].pageY);
}
}
function touchend(event) {
$document.unbind('touchmove',touchmove);
$document.unbind('touchend' ,touchend);
element.on('touchstart', scope.touchBarCB);
// if value change notify application callback
if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
}
};
scope.handleCB = function (clickevent, handle) {
if (attrs.automatic) return;
var oldvalue = scope.value[handle];
// register mouse event to track handle
clickevent.preventDefault();
$document.on('mousemove',mousemove);
$document.on('mouseup', mouseup);
scope.handles[handle][0].focus();
scope.actif=handle;
// slider handle is moving
function mousemove(event) {
scope.moveHandle (handle, event.clientX, event.clientY);
}
// mouse is up dans leave slider send resize events
function mouseup() {
$document.unbind('mousemove', mousemove);
$document.unbind('mouseup', mouseup);
// if value change notify application callback
if (scope.callback && oldvalue !== scope.value[handle]) scope.callback (scope.value[handle], scope.ctrlhandle);
}
};
// simulate jquery find by classes capabilities [warning only return 1st elements]
scope.find = function (select, elem) {
var domelem;
if (elem) domelem = elem[0].querySelector(select);
else domelem = element[0].querySelector(select);
var angelem = angular.element(domelem);
return (angelem);
};
scope.initialSettings = function (initial) {
var decimal_places_match_result;
scope.value=[]; // store low/height value when two handles
scope.relative=[];
if (scope.precision === null) {
decimal_places_match_result = ('' + scope.byStep).match(/\.([\d]*)/);
scope.precision = decimal_places_match_result && decimal_places_match_result[1] ? decimal_places_match_result[1].length : 0;
}
// position handle to initial value(s)
element.on('touchstart', scope.touchBarCB);
scope.handles[0].on('touchstart', function(evt){scope.touchHandleCB(evt,0);});
// this slider has two handles low/hight
if (scope.dual) {
scope.handles[1].addClass('range-slider-handle');
scope.handles[1].on('touchstart', function(evt){scope.touchHandleCB(evt,1);});
if (!scope.initvalues) scope.setValue (initial[1],1);
}
// if we have an initstate object apply it
if (scope.initvalues) scope.initWidget (scope.initvalues);
else scope.setValue (initial[0],0);
};
scope.init = function () {
scope.sliderid = attrs.id || "slider-" + parseInt (Math.random() * 1000);
scope.startValue = -Infinity;
scope.stopValue = Infinity;
scope.byStep = parseInt(attrs.byStep) || 1;
scope.vertical = attrs.vertical || false;
scope.dual = attrs.dualHandles|| false;
scope.trigger_input_change= false;
scope.notMore = parseInt(attrs.notMore) || 100;
scope.notLess = parseInt(attrs.notLess) || 0;
if (scope.vertical) element.addClass("vertical-range");
scope.handles= [scope.find('.handle-min'), scope.find('.handle-max')];
scope.bar = element;
scope.slider = scope.find('.range-slider-active-segment');
scope.start = scope.find('.ibz-range-slider-start');
scope.stop = scope.find('.ibz-range-slider-stop');
scope.disable= attrs.disable || false;
scope.ctrlhandle = new RangeSliderHandle (scope);
// prepare DOM object pointer to compute size dynamically
scope.bounds = {
bar : element[0],
handles: [scope.handles[0][0], scope.handles[1][0]]
};
if (attrs.disable === 'true') scope.setDisable(true);
if (attrs.displayTarget) {
switch (attrs.displayTarget) {
case true :
case 'handle' :
scope.displays = scope.handles;
scope.handles[0].addClass('ibz-range-slider-display');
if (scope.dual) scope.handles[1].addClass('ibz-range-slider-display');
break;
default:
scope.displays = [$document.getElementById (attrs.displayTarget)];
}
} else scope.displays=[];
// extract initial values from attrs and parse into int
if (!attrs.initial) {
scope.initial = [scope.ngModel, scope.ngModel]; // initialize to model values
} else {
var initial = attrs.initial.split(',');
scope.initial = [
initial[0] !== undefined ? parseInt (initial[0]) : scope.notLess,
initial[1] !== undefined ? parseInt (initial[1]) : scope.notMore
];
}
// Monitor any changes on start/stop dates.
scope.$watch('startAt', function() {
if (scope.value < scope.startAt ) {
//scope.setValue (scope.startAt);
}
if (scope.startAt) scope.setStart (scope.startAt);
});
scope.$watch('stopAt' , function() {
if (scope.value > scope.stopAt) {
//scope.setValue (scope.stopAt);
}
if (scope.stopAt) scope.setStop (scope.stopAt);
});
// finish widget initialisation
scope.initialSettings (scope.initial);
};
scope.init();
// slider is ready provide control handle to application controller
scope.$watch ('inithook', function () { // init Values may arrive late
if (scope.inithook) scope.inithook (scope.ctrlhandle);
});
scope.$watch ('initvalues', function () { // init Values may arrive late
if (scope.initvalues) scope.initWidget(scope.initvalues);
});
// two-way binding if model value changes
scope.$watch ('ngModel', function (newValue) {
scope.setValue(newValue, 0);
});
}
return {
restrict: "E", // restrict to HTML element name
scope: {
startAt :'=', // First acceptable date
stopAt :'=', // Last acceptable date
callback :'=', // Callback to actif when a date is selected
formatter:'=', // Callback for drag event call each time internal value changes
inithook :'=', // Hook point to control slider from API
cbhandle :'=', // Argument added to every callback
initvalues:'=', // Initial values as a single object
ngModel: '=' // the model value
},
require: '?ngModel',
template: template, // html template is build from JS
replace: true, // replace current directive with template while inheriting of class
link: link // pickadate object's methods
};
});
console.log ("RangeSlider Loaded");
})();