Commit f557cd4d authored by kvindasAB's avatar kvindasAB

Adapting component to render properly on min/max limits changed.

Also adding a force re-render mechanism useful when using tabs.
parent 80e2c59c
...@@ -13,853 +13,886 @@ ...@@ -13,853 +13,886 @@
angular.module('rzModule', []) angular.module('rzModule', [])
.value('throttle', .value('throttle',
/** /**
* throttle * throttle
* *
* Taken from underscore project * Taken from underscore project
* *
* @param {Function} func * @param {Function} func
* @param {number} wait * @param {number} wait
* @param {ThrottleOptions} options * @param {ThrottleOptions} options
* @returns {Function} * @returns {Function}
*/ */
function throttle(func, wait, options) { function throttle(func, wait, options) {
var getTime = (Date.now || function() { var getTime = (Date.now || function() {
return new Date().getTime(); return new Date().getTime();
}); });
var context, args, result; var context, args, result;
var timeout = null; var timeout = null;
var previous = 0; var previous = 0;
options || (options = {}); options || (options = {});
var later = function() { var later = function() {
previous = options.leading === false ? 0 : getTime(); previous = options.leading === false ? 0 : getTime();
timeout = null; timeout = null;
result = func.apply(context, args); result = func.apply(context, args);
context = args = null; context = args = null;
}; };
return function() { return function() {
var now = getTime(); var now = getTime();
if (!previous && options.leading === false) previous = now; if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous); var remaining = wait - (now - previous);
context = this; context = this;
args = arguments; args = arguments;
if (remaining <= 0) { if (remaining <= 0) {
clearTimeout(timeout); clearTimeout(timeout);
timeout = null; timeout = null;
previous = now; previous = now;
result = func.apply(context, args); result = func.apply(context, args);
context = args = null; context = args = null;
} else if (!timeout && options.trailing !== false) { } else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining); timeout = setTimeout(later, remaining);
} }
return result; return result;
}
})
.factory('Slider', ['$timeout', '$document', 'throttle', function($timeout, $document, throttle)
{
/**
* Slider
*
* @param {ngScope} scope The AngularJS scope
* @param {Element} sliderElem The slider directive element wrapped in jqLite
* @param {*} attributes The slider directive attributes
* @constructor
*/
var Slider = function(scope, sliderElem, attributes)
{
/**
* The slider's scope
*
* @type {ngScope}
*/
this.scope = scope;
/**
* The slider attributes
*
* @type {*}
*/
this.attributes = attributes;
/**
* Slider element wrapped in jqLite
*
* @type {jqLite}
*/
this.sliderElem = sliderElem;
/**
* Slider type
*
* @type {string}
*/
this.range = attributes.rzSliderHigh !== undefined && attributes.rzSliderModel !== undefined;
/**
* Half of the width of the slider handles
*
* @type {number}
*/
this.handleHalfWidth = 0;
/**
* Maximum left the slider handle can have
*
* @type {number}
*/
this.maxLeft = 0;
/**
* Precision
*
* @type {number}
*/
this.precision = 0;
/**
* Step
*
* @type {number}
*/
this.step = 0;
/**
* The name of the handle we are currently tracking
*
* @type {string}
*/
this.tracking = '';
/**
* Minimum value (floor) of the model
*
* @type {number}
*/
this.minValue = 0;
/**
* Maximum value (ceiling) of the model
*
* @type {number}
*/
this.maxValue = 0;
/**
* The delta between min and max value
*
* @type {number}
*/
this.valueRange = 0;
/**
* Set to true if init method already executed
*
* @type {boolean}
*/
this.initRun = false;
/**
* Custom translate function
*
* @type {function}
*/
this.customTrFn = null;
// Slider DOM elements wrapped in jqLite
this.fullBar = null; // The whole slider bar
this.selBar = null; // Highlight between two handles
this.minH = null; // Left slider handle
this.maxH = null; // Right slider handle
this.flrLab = null; // Floor label
this.ceilLab = null; // Ceiling label
this.minLab = null; // Label above the low value
this.maxLab = null; // Label above the high value
this.cmbLab = null; // Combined label
// Initialize slider
this.init();
};
// Add instance methods
Slider.prototype = {
/**
* Initialize slider
*
* @returns {undefined}
*/
init: function()
{
var self = this;
if(this.scope.rzSliderTranslate)
{
this.customTrFn = this.scope.rzSliderTranslate();
}
this.initElemHandles();
this.calcViewDimensions();
this.setMinAndMax();
this.precision = this.scope.rzSliderPrecision === undefined ? 0 : +this.scope.rzSliderPrecision;
this.step = this.scope.rzSliderStep === undefined ? 1 : +this.scope.rzSliderStep;
$timeout(function()
{
self.updateCeilLab();
self.updateFloorLab();
self.initHandles();
self.bindEvents();
});
// Recalculate slider view dimensions
this.scope.$on('reCalcViewDimensions', angular.bind(this, this.calcViewDimensions));
// Recalculate stuff if view port dimensions have changed
angular.element(window).on('resize', angular.bind(this, this.calcViewDimensions));
this.initRun = true;
// Watch for changes to the model
var thrLow = throttle(function()
{
self.setMinAndMax();
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
if(self.range)
{
self.updateSelectionBar();
self.updateCmbLabel();
}
}, 350, { leading: false });
var thrHigh = throttle(function()
{
self.setMinAndMax();
self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh));
self.updateSelectionBar();
self.updateCmbLabel();
}, 350, { leading: false });
this.scope.$watch('rzSliderModel', function(newValue, oldValue)
{
if(newValue === oldValue) return;
thrLow();
});
this.scope.$watch('rzSliderHigh', function(newValue, oldValue)
{
if(newValue === oldValue) return;
thrHigh();
});
},
/**
* Initialize slider handles positions and labels
*
* Run only once during initialization and every time view port changes size
*
* @returns {undefined}
*/
initHandles: function()
{
this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel));
if(this.range)
{
this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh));
this.updateSelectionBar();
this.updateCmbLabel();
}
},
/**
* Translate value to human readable format
*
* @param {number|string} value
* @param {jqLite} label
* @param {bool} useCustomTr
* @returns {undefined}
*/
translateFn: function(value, label, useCustomTr)
{
useCustomTr = useCustomTr === undefined ? true : useCustomTr;
var valStr = this.customTrFn && useCustomTr ? '' + this.customTrFn(value) : '' + value,
getWidth = false;
if(label.rzsv === undefined || label.rzsv.length != valStr.length)
{
getWidth = true;
label.rzsv = valStr;
}
label.text(valStr);
// Update width only when length of the label have changed
if(getWidth) { this.getWidth(label); }
},
/**
* Set maximum and minimum values for the slider
*
* @returns {undefined}
*/
setMinAndMax: function()
{
if(this.scope.rzSliderFloor)
{
this.minValue = +this.scope.rzSliderFloor;
}
else
{
this.minValue = this.scope.rzSliderFloor = 0;
}
if(this.scope.rzSliderCeil)
{
this.maxValue = +this.scope.rzSliderCeil;
}
else
{
this.scope.rzSliderCeil = this.maxValue = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel;
}
this.valueRange = this.maxValue - this.minValue;
},
/**
* Set the slider children to variables for easy access
*
* Run only once during initialization
*
* @returns {undefined}
*/
initElemHandles: function()
{
angular.forEach(this.sliderElem.children(), function(elem, index)
{
var _elem = angular.element(elem);
switch(index)
{
case 0: this.fullBar = _elem; break;
case 1: this.selBar = _elem; break;
case 2: this.minH = _elem; break;
case 3: this.maxH = _elem; break;
case 4: this.flrLab = _elem; break;
case 5: this.ceilLab = _elem; break;
case 6: this.minLab = _elem; break;
case 7: this.maxLab = _elem; break;
case 8: this.cmbLab = _elem; break;
}
}, this);
// Initialize offsets
this.fullBar.rzsl = 0;
this.selBar.rzsl = 0;
this.minH.rzsl = 0;
this.maxH.rzsl = 0;
this.flrLab.rzsl = 0;
this.ceilLab.rzsl = 0;
this.minLab.rzsl = 0;
this.maxLab.rzsl = 0;
this.cmbLab.rzsl = 0;
// Remove stuff not needed in single slider
if( ! this.range)
{
this.cmbLab.remove();
this.maxLab.remove();
this.maxH.remove();
this.selBar.remove();
}
},
/**
* Calculate dimensions that are dependent on view port size
*
* Run once during initialization and every time view port changes size.
*
* @returns {undefined}
*/
calcViewDimensions: function ()
{
var handleWidth = this.getWidth(this.minH);
this.handleHalfWidth = handleWidth / 2;
this.barWidth = this.getWidth(this.fullBar);
this.maxLeft = this.barWidth - handleWidth;
this.getWidth(this.sliderElem);
this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left;
if(this.initRun)
{
this.updateCeilLab();
this.initHandles();
}
},
/**
* Update position of the ceiling label
*
* @returns {undefined}
*/
updateCeilLab: function()
{
this.translateFn(this.scope.rzSliderCeil, this.ceilLab);
this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw);
this.getWidth(this.ceilLab);
},
/**
* Update position of the floor label
*
* @returns {undefined}
*/
updateFloorLab: function()
{
this.translateFn(this.scope.rzSliderFloor, this.flrLab);
this.getWidth(this.flrLab);
},
/**
* Update slider handles and label positions
*
* @param {string} which
* @param {number} newOffset
*/
updateHandles: function(which, newOffset)
{
if(which === 'rzSliderModel')
{
this.updateLowHandle(newOffset);
if(this.range)
{
this.updateSelectionBar();
this.updateCmbLabel();
}
return;
}
if(which === 'rzSliderHigh')
{
this.updateHighHandle(newOffset);
if(this.range)
{
this.updateSelectionBar();
this.updateCmbLabel();
} }
return; })
}
// Update both
this.updateLowHandle(newOffset);
this.updateHighHandle(newOffset);
this.updateSelectionBar();
this.updateCmbLabel();
},
/**
* Update low slider handle position and label
*
* @param {number} newOffset
* @returns {undefined}
*/
updateLowHandle: function(newOffset)
{
this.setLeft(this.minH, newOffset);
this.translateFn(this.scope.rzSliderModel, this.minLab);
this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth);
this.shFloorCeil();
},
/**
* Update high slider handle position and label
*
* @param {number} newOffset
* @returns {undefined}
*/
updateHighHandle: function(newOffset)
{
this.setLeft(this.maxH, newOffset);
this.translateFn(this.scope.rzSliderHigh, this.maxLab);
this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth);
this.shFloorCeil(); .factory('Slider', ['$timeout', '$document', 'throttle', function($timeout, $document, throttle)
},
/**
* Show / hide floor / ceiling label
*
* @returns {undefined}
*/
shFloorCeil: function()
{ {
var flHidden = false, clHidden = false; /**
* Slider
if(this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5) *
{ * @param {ngScope} scope The AngularJS scope
flHidden = true; * @param {Element} sliderElem The slider directive element wrapped in jqLite
this.hideEl(this.flrLab); * @param {*} attributes The slider directive attributes
} * @constructor
else */
{ var Slider = function(scope, sliderElem, attributes)
flHidden = false;
this.showEl(this.flrLab);
}
if(this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10)
{
clHidden = true;
this.hideEl(this.ceilLab);
}
else
{
clHidden = false;
this.showEl(this.ceilLab);
}
if(this.range)
{
if(this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10)
{ {
this.hideEl(this.ceilLab); /**
} * The slider's scope
else if( ! clHidden) *
{ * @type {ngScope}
this.showEl(this.ceilLab); */
} this.scope = scope;
// Hide or show floor label /**
if(this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth) * The slider attributes
{ *
this.hideEl(this.flrLab); * @type {*}
} */
else if( ! flHidden) this.attributes = attributes;
{
this.showEl(this.flrLab); /**
} * Slider element wrapped in jqLite
} *
}, * @type {jqLite}
*/
/** this.sliderElem = sliderElem;
* Update slider selection bar, combined label and range label
* /**
* @returns {undefined} * Slider type
*/ *
updateSelectionBar: function() * @type {string}
{ */
this.setWidth(this.selBar, this.maxH.rzsl - this.minH.rzsl); this.range = attributes.rzSliderHigh !== undefined && attributes.rzSliderModel !== undefined;
this.setLeft(this.selBar, this.minH.rzsl + this.handleHalfWidth);
}, /**
* Half of the width of the slider handles
/** *
* Update combined label position and value * @type {number}
* */
* @returns {undefined} this.handleHalfWidth = 0;
*/
updateCmbLabel: function() /**
{ * Maximum left the slider handle can have
var lowTr, highTr; *
* @type {number}
if(this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl) */
{ this.maxLeft = 0;
if(this.customTrFn)
{ /**
lowTr = this.customTrFn(this.scope.rzSliderModel); * Precision
highTr = this.customTrFn(this.scope.rzSliderHigh); *
} * @type {number}
else */
{ this.precision = 0;
lowTr = this.scope.rzSliderModel;
highTr = this.scope.rzSliderHigh; /**
} * Step
*
this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false); * @type {number}
this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2); */
this.hideEl(this.minLab); this.step = 0;
this.hideEl(this.maxLab);
this.showEl(this.cmbLab); /**
} * The name of the handle we are currently tracking
else *
{ * @type {string}
this.showEl(this.maxLab); */
this.showEl(this.minLab); this.tracking = '';
this.hideEl(this.cmbLab);
} /**
}, * Minimum value (floor) of the model
*
/** * @type {number}
* Round value to step and precision */
* this.minValue = 0;
* @param {number} value
* @returns {number} /**
*/ * Maximum value (ceiling) of the model
roundStep: function(value) *
{ * @type {number}
var step = this.step, */
remainder = (value - this.minValue) % step, this.maxValue = 0;
steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder;
/**
return +(steppedValue).toFixed(this.precision); * The delta between min and max value
}, *
* @type {number}
/** */
* Hide element this.valueRange = 0;
*
* @param element /**
* @returns {jqLite} The jqLite wrapped DOM element * Set to true if init method already executed
*/ *
hideEl: function (element) * @type {boolean}
{ */
return element.css({opacity: 0}); this.initRun = false;
},
/**
/** * Custom translate function
* Show element *
* * @type {function}
* @param element The jqLite wrapped DOM element */
* @returns {jqLite} The jqLite this.customTrFn = null;
*/
showEl: function (element) // Slider DOM elements wrapped in jqLite
this.fullBar = null; // The whole slider bar
this.selBar = null; // Highlight between two handles
this.minH = null; // Left slider handle
this.maxH = null; // Right slider handle
this.flrLab = null; // Floor label
this.ceilLab = null; // Ceiling label
this.minLab = null; // Label above the low value
this.maxLab = null; // Label above the high value
this.cmbLab = null; // Combined label
// Initialize slider
this.init();
};
// Add instance methods
Slider.prototype = {
/**
* Initialize slider
*
* @returns {undefined}
*/
init: function()
{
//console.log("slider.init...");
var self = this;
if(this.scope.rzSliderTranslate)
{
this.customTrFn = this.scope.rzSliderTranslate();
}
this.initElemHandles();
this.calcViewDimensions();
this.setMinAndMax();
this.precision = this.scope.rzSliderPrecision === undefined ? 0 : +this.scope.rzSliderPrecision;
this.step = this.scope.rzSliderStep === undefined ? 1 : +this.scope.rzSliderStep;
$timeout(function()
{
self.updateCeilLab();
self.updateFloorLab();
self.initHandles();
self.bindEvents();
},100);
// Recalculate stuff if view port dimensions have changed
angular.element(window).on('resize', angular.bind(this, this.calcViewDimensions));
this.initRun = true;
// Watch for changes to the model
var thrLow = throttle(function()
{
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
if(self.range)
{
self.updateSelectionBar();
self.updateCmbLabel();
}
}, 350, { leading: false });
var thrHigh = throttle(function()
{
self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh));
self.updateSelectionBar();
self.updateCmbLabel();
}, 350, { leading: false });
this.scope.$watch('rzSliderModel', function(newValue, oldValue){
//console.log("sliderModel.changed...");
if(newValue === oldValue) return;
thrLow();
});
this.scope.$watch('rzSliderHigh', function(newValue, oldValue){
//console.log("sliderHigh.changed...");
if(newValue === oldValue) return;
thrHigh();
});
this.scope.$watch('rzSliderFloor', function(newValue, oldValue){
//console.log("sliderFloor.changed...");
if(newValue === oldValue) return;
self.resetSlider();
});
this.scope.$watch('rzSliderCeil', function(newValue, oldValue){
//console.log("sliderCeil.changed...");
if(newValue === oldValue) return;
self.resetSlider();
});
this.scope.$watch('rzSliderForceRender', function(newValue, oldValue){
//console.log("rzSliderForceRender.changed...");
self.resetLabelsWidth();
thrLow();
thrHigh();
self.resetSlider();
});
},
resetLabelsWidth: function() {
this.minLab.rzsv = undefined;
this.maxLab.rzsv = undefined;
},
resetSlider: function() {
this.setMinAndMax();
this.calcViewDimensions();
this.updateCeilLab();
this.updateFloorLab();
},
/**
* Initialize slider handles positions and labels
*
* Run only once during initialization and every time view port changes size
*
* @returns {undefined}
*/
initHandles: function()
{
this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel));
if(this.range)
{
this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh));
this.updateSelectionBar();
this.updateCmbLabel();
}
},
/**
* Translate value to human readable format
*
* @param {number|string} value
* @param {jqLite} label
* @param {bool} useCustomTr
* @returns {undefined}
*/
translateFn: function(value, label, useCustomTr)
{
useCustomTr = useCustomTr === undefined ? true : useCustomTr;
var valStr = this.customTrFn && useCustomTr ? '' + this.customTrFn(value) : '' + value,
getWidth = false;
if(label.rzsv === undefined || label.rzsv.length != valStr.length)
{
getWidth = true;
label.rzsv = valStr;
}
label.text(valStr);
// Update width only when length of the label have changed
if(getWidth) { this.getWidth(label); }
},
/**
* Set maximum and minimum values for the slider
*
* @returns {undefined}
*/
setMinAndMax: function()
{
//console.log("slider.setMinAndMax...");
if(this.scope.rzSliderFloor)
{
this.minValue = +this.scope.rzSliderFloor;
}
else
{
this.minValue = this.scope.rzSliderFloor = 0;
}
if(this.scope.rzSliderCeil)
{
this.maxValue = +this.scope.rzSliderCeil;
}
else
{
this.scope.rzSliderCeil = this.maxValue = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel;
}
this.valueRange = this.maxValue - this.minValue;
},
/**
* Set the slider children to variables for easy access
*
* Run only once during initialization
*
* @returns {undefined}
*/
initElemHandles: function()
{
angular.forEach(this.sliderElem.children(), function(elem, index)
{
var _elem = angular.element(elem);
switch(index)
{
case 0: this.fullBar = _elem; break;
case 1: this.selBar = _elem; break;
case 2: this.minH = _elem; break;
case 3: this.maxH = _elem; break;
case 4: this.flrLab = _elem; break;
case 5: this.ceilLab = _elem; break;
case 6: this.minLab = _elem; break;
case 7: this.maxLab = _elem; break;
case 8: this.cmbLab = _elem; break;
}
}, this);
// Initialize offsets
this.fullBar.rzsl = 0;
this.selBar.rzsl = 0;
this.minH.rzsl = 0;
this.maxH.rzsl = 0;
this.flrLab.rzsl = 0;
this.ceilLab.rzsl = 0;
this.minLab.rzsl = 0;
this.maxLab.rzsl = 0;
this.cmbLab.rzsl = 0;
// Remove stuff not needed in single slider
if( ! this.range)
{
this.cmbLab.remove();
this.maxLab.remove();
this.maxH.remove();
this.selBar.remove();
}
},
/**
* Calculate dimensions that are dependent on view port size
*
* Run once during initialization and every time view port changes size.
*
* @returns {undefined}
*/
calcViewDimensions: function ()
{
var handleWidth = this.getWidth(this.minH);
this.handleHalfWidth = handleWidth / 2;
this.barWidth = this.getWidth(this.fullBar);
this.maxLeft = this.barWidth - handleWidth;
this.getWidth(this.sliderElem);
this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left;
if(this.initRun)
{
this.updateCeilLab();
this.initHandles();
}
},
/**
* Update position of the ceiling label
*
* @returns {undefined}
*/
updateCeilLab: function()
{
this.translateFn(this.scope.rzSliderCeil, this.ceilLab);
this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw);
this.getWidth(this.ceilLab);
},
/**
* Update position of the floor label
*
* @returns {undefined}
*/
updateFloorLab: function()
{
this.translateFn(this.scope.rzSliderFloor, this.flrLab);
this.getWidth(this.flrLab);
},
/**
* Update slider handles and label positions
*
* @param {string} which
* @param {number} newOffset
*/
updateHandles: function(which, newOffset)
{
if(which === 'rzSliderModel')
{
this.updateLowHandle(newOffset);
if(this.range)
{
this.updateSelectionBar();
this.updateCmbLabel();
}
return;
}
if(which === 'rzSliderHigh')
{
this.updateHighHandle(newOffset);
if(this.range)
{
this.updateSelectionBar();
this.updateCmbLabel();
}
return;
}
// Update both
this.updateLowHandle(newOffset);
this.updateHighHandle(newOffset);
this.updateSelectionBar();
this.updateCmbLabel();
},
/**
* Update low slider handle position and label
*
* @param {number} newOffset
* @returns {undefined}
*/
updateLowHandle: function(newOffset)
{
//console.log("updateLowHandle: hhw: " + this.handleHalfWidth + " mh" + newOffset );
this.setLeft(this.minH, newOffset);
this.translateFn(this.scope.rzSliderModel, this.minLab);
this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth);
this.shFloorCeil();
},
/**
* Update high slider handle position and label
*
* @param {number} newOffset
* @returns {undefined}
*/
updateHighHandle: function(newOffset)
{
this.setLeft(this.maxH, newOffset);
this.translateFn(this.scope.rzSliderHigh, this.maxLab);
this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth);
this.shFloorCeil();
},
/**
* Show / hide floor / ceiling label
*
* @returns {undefined}
*/
shFloorCeil: function()
{
var flHidden = false, clHidden = false;
if(this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5)
{
flHidden = true;
this.hideEl(this.flrLab);
}
else
{
flHidden = false;
this.showEl(this.flrLab);
}
if(this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10)
{
clHidden = true;
this.hideEl(this.ceilLab);
}
else
{
clHidden = false;
this.showEl(this.ceilLab);
}
if(this.range)
{
if(this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10)
{
this.hideEl(this.ceilLab);
}
else if( ! clHidden)
{
this.showEl(this.ceilLab);
}
// Hide or show floor label
if(this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth)
{
this.hideEl(this.flrLab);
}
else if( ! flHidden)
{
this.showEl(this.flrLab);
}
}
},
/**
* Update slider selection bar, combined label and range label
*
* @returns {undefined}
*/
updateSelectionBar: function()
{
this.setWidth(this.selBar, this.maxH.rzsl - this.minH.rzsl);
this.setLeft(this.selBar, this.minH.rzsl + this.handleHalfWidth);
},
/**
* Update combined label position and value
*
* @returns {undefined}
*/
updateCmbLabel: function()
{
var lowTr, highTr;
if(this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl)
{
if(this.customTrFn)
{
lowTr = this.customTrFn(this.scope.rzSliderModel);
highTr = this.customTrFn(this.scope.rzSliderHigh);
}
else
{
lowTr = this.scope.rzSliderModel;
highTr = this.scope.rzSliderHigh;
}
this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false);
this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2);
this.hideEl(this.minLab);
this.hideEl(this.maxLab);
this.showEl(this.cmbLab);
}
else
{
this.showEl(this.maxLab);
this.showEl(this.minLab);
this.hideEl(this.cmbLab);
}
},
/**
* Round value to step and precision
*
* @param {number} value
* @returns {number}
*/
roundStep: function(value)
{
var step = this.step,
remainder = (value - this.minValue) % step,
steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder;
return +(steppedValue).toFixed(this.precision);
},
/**
* Hide element
*
* @param element
* @returns {jqLite} The jqLite wrapped DOM element
*/
hideEl: function (element)
{
return element.css({opacity: 0});
},
/**
* Show element
*
* @param element The jqLite wrapped DOM element
* @returns {jqLite} The jqLite
*/
showEl: function (element)
{
return element.css({opacity: 1});
},
/**
* Set element left offset
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} left
* @returns {number}
*/
setLeft: function (elem, left)
{
elem.rzsl = left;
elem.css({left: left + 'px'});
return left;
},
/**
* Get element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @returns {number}
*/
getWidth: function(elem)
{
var val = elem[0].getBoundingClientRect();
elem.rzsw = val.right - val.left;
return elem.rzsw;
},
/**
* Set element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} width
* @returns {*}
*/
setWidth: function(elem, width)
{
elem.rzsw = width;
elem.css({width: width + 'px'});
return width;
},
/**
* Translate value to pixel offset
*
* @param {number} val
* @returns {number}
*/
valueToOffset: function(val)
{
//console.log("valueToOffset..." + val + " minV: " + this.minValue + " maxL " + this.maxLeft + " vr: " + this.valueRange);
return (val - this.minValue) * this.maxLeft / this.valueRange;
},
/**
* Translate offset to model value
*
* @param {number} offset
* @returns {number}
*/
offsetToValue: function(offset)
{
return (offset / this.maxLeft) * this.valueRange + this.minValue;
},
// Events
/**
* Bind mouse and touch events to slider handles
*
* @returns {undefined}
*/
bindEvents: function()
{
this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')) }
this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')) }
},
/**
* onStart event handler
*
* @param {Object} pointer The jqLite wrapped DOM element
* @param {string} ref One of the refLow, refHigh values
* @param {Event} event The event
* @returns {undefined}
*/
onStart: function (pointer, ref, event)
{
event.stopPropagation();
event.preventDefault();
if(this.tracking !== '') { return }
// We have to do this in case the HTML where the sliders are on
// have been animated into view.
this.calcViewDimensions();
this.tracking = ref;
pointer.addClass('active');
if(event.touches)
{
$document.on('touchmove', angular.bind(this, this.onMove, pointer));
$document.on('touchend', angular.bind(this, this.onEnd));
}
else
{
$document.on('mousemove', angular.bind(this, this.onMove, pointer));
$document.on('mouseup', angular.bind(this, this.onEnd));
}
},
/**
* onMove event handler
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onMove: function (pointer, event)
{
var eventX = event.clientX || event.touches[0].clientX,
sliderLO = this.sliderElem.rzsl,
newOffset = eventX - sliderLO - this.handleHalfWidth,
newValue;
if(newOffset <= 0)
{
if(pointer.rzsl !== 0)
{
this.scope[this.tracking] = this.minValue;
this.updateHandles(this.tracking, 0);
this.scope.$apply();
}
return;
}
if(newOffset >= this.maxLeft)
{
if(pointer.rzsl !== this.maxLeft)
{
this.scope[this.tracking] = this.maxValue;
this.updateHandles(this.tracking, this.maxLeft);
this.scope.$apply();
}
return;
}
newValue = this.offsetToValue(newOffset);
newValue = this.roundStep(newValue);
if (this.range)
{
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh)
{
this.scope[this.tracking] = this.scope.rzSliderHigh;
this.updateHandles(this.tracking, this.maxH.rzsl);
this.tracking = 'rzSliderHigh';
this.minH.removeClass('active');
this.maxH.addClass('active');
}
else if(this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel)
{
this.scope[this.tracking] = this.scope.rzSliderModel;
this.updateHandles(this.tracking, this.minH.rzsl);
this.tracking = 'rzSliderModel';
this.maxH.removeClass('active');
this.minH.addClass('active');
}
}
if(this.scope[this.tracking] !== newValue)
{
this.scope[this.tracking] = newValue;
this.updateHandles(this.tracking, newOffset);
this.scope.$apply();
}
},
/**
* onEnd event handler
*
* @param {Event} event The event
* @returns {undefined}
*/
onEnd: function(event)
{
this.minH.removeClass('active');
this.maxH.removeClass('active');
if(event.touches)
{
$document.unbind('touchmove');
$document.unbind('touchend');
}
else
{
$document.unbind('mousemove');
$document.unbind('mouseup');
}
this.tracking = '';
}
};
return Slider;
}])
.directive('rzslider', ['Slider', function(Slider)
{ {
return element.css({opacity: 1}); return {
}, restrict: 'E',
scope: {
/** rzSliderFloor: '=?',
* Set element left offset rzSliderCeil: '=?',
* rzSliderStep: '@',
* @param {jqLite} elem The jqLite wrapped DOM element rzSliderPrecision: '@',
* @param {number} left rzSliderModel: '=?',
* @returns {number} rzSliderHigh: '=?',
*/ rzSliderTranslate: '&',
setLeft: function (elem, left) rzSliderForceRender: '=?'
{ },
elem.rzsl = left; template: '<span class="bar"></span>' + // 0 The slider bar
elem.css({left: left + 'px'});
return left;
},
/**
* Get element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @returns {number}
*/
getWidth: function(elem)
{
var val = elem[0].getBoundingClientRect();
elem.rzsw = val.right - val.left;
return elem.rzsw;
},
/**
* Set element width
*
* @param {jqLite} elem The jqLite wrapped DOM element
* @param {number} width
* @returns {*}
*/
setWidth: function(elem, width)
{
elem.rzsw = width;
elem.css({width: width + 'px'});
return width;
},
/**
* Translate value to pixel offset
*
* @param {number} val
* @returns {number}
*/
valueToOffset: function(val)
{
return (val - this.minValue) * this.maxLeft / this.valueRange;
},
/**
* Translate offset to model value
*
* @param {number} offset
* @returns {number}
*/
offsetToValue: function(offset)
{
return (offset / this.maxLeft) * this.valueRange + this.minValue;
},
// Events
/**
* Bind mouse and touch events to slider handles
*
* @returns {undefined}
*/
bindEvents: function()
{
this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')) }
this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel'));
if(this.range) { this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')) }
},
/**
* onStart event handler
*
* @param {Object} pointer The jqLite wrapped DOM element
* @param {string} ref One of the refLow, refHigh values
* @param {Event} event The event
* @returns {undefined}
*/
onStart: function (pointer, ref, event)
{
event.stopPropagation();
event.preventDefault();
if(this.tracking !== '') { return }
// We have to do this in case the HTML where the sliders are on
// have been animated into view.
this.calcViewDimensions();
this.tracking = ref;
pointer.addClass('active');
if(event.touches)
{
$document.on('touchmove', angular.bind(this, this.onMove, pointer));
$document.on('touchend', angular.bind(this, this.onEnd));
}
else
{
$document.on('mousemove', angular.bind(this, this.onMove, pointer));
$document.on('mouseup', angular.bind(this, this.onEnd));
}
},
/**
* onMove event handler
*
* @param {jqLite} pointer
* @param {Event} event The event
* @returns {undefined}
*/
onMove: function (pointer, event)
{
var eventX = event.clientX || event.touches[0].clientX,
sliderLO = this.sliderElem.rzsl,
newOffset = eventX - sliderLO - this.handleHalfWidth,
newValue;
if(newOffset <= 0)
{
if(pointer.rzsl !== 0)
{
this.scope[this.tracking] = this.minValue;
this.updateHandles(this.tracking, 0);
this.scope.$apply();
}
return;
}
if(newOffset >= this.maxLeft)
{
if(pointer.rzsl !== this.maxLeft)
{
this.scope[this.tracking] = this.maxValue;
this.updateHandles(this.tracking, this.maxLeft);
this.scope.$apply();
}
return;
}
newValue = this.offsetToValue(newOffset);
newValue = this.roundStep(newValue);
if (this.range)
{
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh)
{
this.scope[this.tracking] = this.scope.rzSliderHigh;
this.updateHandles(this.tracking, this.maxH.rzsl);
this.tracking = 'rzSliderHigh';
this.minH.removeClass('active');
this.maxH.addClass('active');
}
else if(this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel)
{
this.scope[this.tracking] = this.scope.rzSliderModel;
this.updateHandles(this.tracking, this.minH.rzsl);
this.tracking = 'rzSliderModel';
this.maxH.removeClass('active');
this.minH.addClass('active');
}
}
if(this.scope[this.tracking] !== newValue)
{
this.scope[this.tracking] = newValue;
this.updateHandles(this.tracking, newOffset);
this.scope.$apply();
}
},
/**
* onEnd event handler
*
* @param {Event} event The event
* @returns {undefined}
*/
onEnd: function(event)
{
this.minH.removeClass('active');
this.maxH.removeClass('active');
if(event.touches)
{
$document.unbind('touchmove');
$document.unbind('touchend');
}
else
{
$document.unbind('mousemove');
$document.unbind('mouseup');
}
this.tracking = '';
}
};
return Slider;
}])
.directive('rzslider', ['Slider', function(Slider)
{
return {
restrict: 'E',
scope: {
rzSliderFloor: '=?',
rzSliderCeil: '=?',
rzSliderStep: '@',
rzSliderPrecision: '@',
rzSliderModel: '=?',
rzSliderHigh: '=?',
rzSliderTranslate: '&'
},
template: '<span class="bar"></span>' + // 0 The slider bar
'<span class="bar selection"></span>' + // 1 Highlight between two handles '<span class="bar selection"></span>' + // 1 Highlight between two handles
'<span class="pointer"></span>' + // 2 Left slider handle '<span class="pointer"></span>' + // 2 Left slider handle
'<span class="pointer"></span>' + // 3 Right slider handle '<span class="pointer"></span>' + // 3 Right slider handle
...@@ -869,12 +902,12 @@ function throttle(func, wait, options) { ...@@ -869,12 +902,12 @@ function throttle(func, wait, options) {
'<span class="bubble"></span>' + // 7 Label above right slider handle '<span class="bubble"></span>' + // 7 Label above right slider handle
'<span class="bubble"></span>', // 8 Range label when the slider handles are close ex. 15 - 17 '<span class="bubble"></span>', // 8 Range label when the slider handles are close ex. 15 - 17
link: function(scope, elem, attr) link: function(scope, elem, attr)
{ {
return new Slider(scope, elem, attr); return new Slider(scope, elem, attr);
} }
}; };
}]); }]);
// IDE assist // IDE assist
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment