Created
August 16, 2015 07:02
-
-
Save MeoMix/0736beb6deec47868228 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_.mixin({ | |
// Inspired by: https://gist.github.com/danro/7846358 | |
// Leverage requestAnimationFrame for throttling function calls instead of setTimeout for better perf. | |
throttleFramerate: function(callback) { | |
var wait = false; | |
var args = null; | |
var context = null; | |
return function() { | |
if (!wait) { | |
wait = true; | |
args = arguments; | |
context = this; | |
requestAnimationFrame(function() { | |
wait = false; | |
callback.apply(context, args); | |
}); | |
} | |
}; | |
} | |
}); | |
// There's a lack of support in modern browsers for being notified of a DOM element changing dimensions. | |
// Provide this functionality by leveraging 'scroll' events attached to hidden DOM elements attached to | |
// a given element. | |
// http://stackoverflow.com/questions/19329530/onresize-for-div-elements/19418479#19418479 | |
(function() { | |
function resetTriggers(element) { | |
var triggers = element.__resizeTriggers__, | |
expand = triggers.firstElementChild, | |
contract = triggers.lastElementChild, | |
expandChild = expand.firstElementChild; | |
contract.scrollLeft = contract.scrollWidth; | |
contract.scrollTop = contract.scrollHeight; | |
expandChild.style.width = expand.offsetWidth + 1 + 'px'; | |
expandChild.style.height = expand.offsetHeight + 1 + 'px'; | |
expand.scrollLeft = expand.scrollWidth; | |
expand.scrollTop = expand.scrollHeight; | |
}; | |
function checkTriggers(element) { | |
return element.offsetWidth != element.__resizeLast__.width || element.offsetHeight != element.__resizeLast__.height; | |
} | |
function scrollListener(e) { | |
var element = this; | |
resetTriggers(this); | |
if (this.__resizeRAF__) cancelAnimationFrame(this.__resizeRAF__); | |
this.__resizeRAF__ = requestAnimationFrame(function() { | |
if (checkTriggers(element)) { | |
element.__resizeLast__.width = element.offsetWidth; | |
element.__resizeLast__.height = element.offsetHeight; | |
element.__resizeListeners__.forEach(function(fn) { | |
fn.call(element, e); | |
}); | |
} | |
}); | |
}; | |
window.addResizeListener = function(element, fn) { | |
if (!element.__resizeTriggers__) { | |
if (getComputedStyle(element).position == 'static') element.style.position = 'relative'; | |
element.__resizeLast__ = {}; | |
element.__resizeListeners__ = []; | |
(element.__resizeTriggers__ = document.createElement('div')).className = 'resize-triggers'; | |
element.__resizeTriggers__.innerHTML = '<div class="expand-trigger"><div></div></div>' + | |
'<div class="contract-trigger"></div>'; | |
element.appendChild(element.__resizeTriggers__); | |
resetTriggers(element); | |
element.addEventListener('scroll', scrollListener, true); | |
} | |
element.__resizeListeners__.push(fn); | |
}; | |
window.removeResizeListener = function(element, fn) { | |
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); | |
if (!element.__resizeListeners__.length) { | |
element.removeEventListener('scroll', scrollListener); | |
element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__); | |
} | |
} | |
})(); | |
var Orientation = { | |
Horizontal: 'horizontal', | |
Vertical: 'vertical' | |
}; | |
var SliderView = Marionette.LayoutView.extend({ | |
tagName: 'streamus-slider', | |
template: '#sliderTemplate', | |
ui: { | |
'track': '[data-ui~=track]', | |
'thumb': '[data-ui~=thumb]' | |
}, | |
events: { | |
'mousedown': '_onMouseDown', | |
'wheel': '_onWheel' | |
}, | |
// Values parsed from the HTML declaration. | |
value: -1, | |
maxValue: 100, | |
minValue: 0, | |
step: 1, | |
// Values stored to determine mouse movement amounts. | |
mouseDownValue: 0, | |
totalMouseMovement: 0, | |
// The cached length of the slider. | |
length: -1, | |
orientation: Orientation.Horizontal, | |
isVertical: false, | |
initialize: function() { | |
// It's important to bind pre-emptively or attempts to call removeEventListener will fail to find the appropriate reference. | |
this._onWindowMouseMove = this._onWindowMouseMove.bind(this); | |
this._onWindowMouseUp = this._onWindowMouseUp.bind(this); | |
this._onResize = this._onResize.bind(this); | |
// Provide a throttled version of _onWheel because wheel events can fire at a high rate. | |
// https://developer.mozilla.org/en-US/docs/Web/Events/wheel | |
this._onWheel = _.throttleFramerate(this._onWheel.bind(this)); | |
this._setDefaultValues(); | |
}, | |
onRender: function() { | |
// Use custom logic to monitor element for resizing. | |
// This logic injects a hidden element into the DOM which is used to detect resizes. | |
window.addResizeListener(this.el, this._onResize); | |
}, | |
onAttach: function() { | |
// Cache the length of the slider once it is known. | |
this.length = this._getLength(); | |
// Initialize with default value and update layout. Can only be done once length is known. | |
var valueAttribute = this.$el.attr('value'); | |
var value = _.isUndefined(valueAttribute) ? this._getDefaultValue() : parseInt(valueAttribute, 10); | |
this._setValue(value); | |
}, | |
onBeforeDestroy: function() { | |
this._setWindowEventListeners(false); | |
window.removeResizeListener(this.el, this._onResize); | |
}, | |
// Monitor changes to the user's mouse position after they begin clicking | |
// on the track. Adjust the thumb position based on mouse movements. | |
_onMouseDown: function(event) { | |
// Don't run this logic on right-click. | |
if (event.button === 0) { | |
var target = event.target; | |
// Snap the thumb to the mouse's position, but only do so if the mouse isn't clicking the thumb. | |
if (target !== this.ui.thumb[0]) { | |
var offset = this.isVertical ? event.offsetY : event.offsetX; | |
// The track element has a transform: scale applied to it. | |
// Normalize offset relative to parent by unscaling the offset. | |
if (target === this.ui.track[0]) { | |
offset *= this._getPercentValue(this.value); | |
} | |
var value = this._getValueByPixelValue(offset); | |
this._setValue(value); | |
} | |
// Start keeping track of mouse movements to be able to adjust the thumb position as the mouse moves. | |
this.mouseDownValue = this.value; | |
this._setWindowEventListeners(true); | |
} | |
}, | |
// Update the value by one step. | |
_onWheel: function(event) { | |
var value = this.value + event.originalEvent.deltaY / (-100 / this.step); | |
this._setValue(value); | |
}, | |
// Refresh the cached length and update layout whenever element resizes. | |
_onResize: function() { | |
var length = this._getLength(); | |
if (this.length !== length) { | |
this.length = length; | |
this._updateLayout(this.value); | |
} | |
}, | |
_onWindowMouseMove: function(event) { | |
// Invert movementY because vertical is flipped 180deg. | |
var movement = this.isVertical ? -event.movementY : event.movementX; | |
// No action is needed when moving the mouse perpendicular to our direction | |
if (movement !== 0) { | |
movement *= this._getScaleFactor(); | |
this.totalMouseMovement += movement; | |
// Derive new value from initial + total movement rather than value + movement. | |
// If user drags mouse outside element then current + movement will not equal initial + total movement. | |
var value = this.mouseDownValue + this.totalMouseMovement; | |
this._setValue(value); | |
} | |
}, | |
_onWindowMouseUp: function() { | |
this.totalMouseMovement = 0; | |
this._setWindowEventListeners(false); | |
}, | |
// Read attributes on DOM element and use them if provided. Otherwise, | |
// rely on the HTML5 range input spec for default values. | |
_setDefaultValues: function() { | |
this.orientation = this.$el.attr('orientation') || this.orientation; | |
this.isVertical = this.orientation === Orientation.Vertical; | |
this.minValue = parseInt(this.$el.attr('min'), 10) || this.minValue; | |
this.maxValue = parseInt(this.$el.attr('max'), 10) || this.maxValue; | |
this.step = parseInt(this.$el.attr('step'), 10) || this.step; | |
}, | |
// Temporarily add or remove mouse-monitoring events bound the window. | |
_setWindowEventListeners: function(isAdding) { | |
var action = isAdding ? window.addEventListener : window.removeEventListener; | |
action('mousemove', this._onWindowMouseMove); | |
action('mouseup', this._onWindowMouseUp); | |
}, | |
// Update the slider with the given value after ensuring it is within bounds. | |
_setValue: function(value) { | |
var boundedValue = this._getBoundedValue(value); | |
if (this.value !== boundedValue) { | |
this.value = boundedValue; | |
// Be sure to record value on the element so $.val() and .value will yield proper values. | |
this.el.value = boundedValue; | |
this._updateLayout(boundedValue); | |
this.$el.trigger('input', boundedValue); | |
} | |
}, | |
// Visually update the track and thumb elements. | |
// Set their translate and scale values such that they represent the given value. | |
_updateLayout: function(value) { | |
var percentValue = this._getPercentValue(value); | |
var pixelValue = this._getPixelValue(value); | |
var axis = this.isVertical ? 'Y' : 'X'; | |
this.ui.thumb.css('transform', 'translate' + axis + '(' + pixelValue + 'px)'); | |
this.ui.track.css('transform', 'scale' + axis + '(' + percentValue + ')'); | |
}, | |
// Ensure that a given value falls within the min/max and step parameters. | |
_getBoundedValue: function(value) { | |
var boundedValue = value; | |
// Respect min/max values | |
if (boundedValue > this.maxValue) { | |
boundedValue = this.maxValue; | |
} | |
if (boundedValue < this.minValue) { | |
boundedValue = this.minValue; | |
} | |
// Round value to the nearest number which is divisible by step. | |
// Subtract and re-add minValue so stepping down will always reach minValue. | |
// Do this after min/max because step should be respected before setting to maxValue. | |
boundedValue -= this.minValue; | |
boundedValue = this.step * Math.round(boundedValue / this.step); | |
boundedValue += this.minValue; | |
return boundedValue; | |
}, | |
// Return the average value between minValue and maxValue. | |
// Useful when no value has been provided on the HTML element. | |
_getDefaultValue: function() { | |
return this.minValue + (this.maxValue - this.minValue) / 2; | |
}, | |
// Take a given number and determine what percent of the input the value represents. | |
_getPercentValue: function(value) { | |
return (value - this.minValue) / (this.maxValue - this.minValue); | |
}, | |
// Take a given value and return the pixel length needed to represent that value on the slider. | |
_getPixelValue: function(value) { | |
var percentValue = this._getPercentValue(value); | |
var pixelValue; | |
// Calculating pixelValue for vertical requires inverting the math because | |
// the slider needs to appear flipped 180deg to feel correct. | |
if (this.isVertical) { | |
pixelValue = (1 - percentValue) * this.length; | |
} else { | |
pixelValue = percentValue * this.length; | |
} | |
return pixelValue; | |
}, | |
// Take a given pixelValue and convert it to corresponding slider value. | |
_getValueByPixelValue: function(pixelValue) { | |
// Vertical slider needs to be flipped 180 so inverse the value. | |
if (this.isVertical) { | |
pixelValue = this.length - pixelValue; | |
} | |
// Convert px moved to % distance moved. | |
var offsetPercent = pixelValue / this.length; | |
// Convert % distance moved into corresponding value. | |
var valueDifference = this.maxValue - this.minValue; | |
var value = this.minValue + valueDifference * offsetPercent; | |
return value; | |
}, | |
// Query the DOM for width or height of the slider and return it. | |
// Method is slow and should only be used when cached length is stale. | |
_getLength: function() { | |
return this.isVertical ? this.$el.height() : this.$el.width();; | |
}, | |
// Return a ratio of the range of values able to be iterated over relative to the length of the slider. | |
// Useful for converting a pixel amount to a slider value. | |
_getScaleFactor: function() { | |
return (this.maxValue - this.minValue) / this.length; | |
} | |
}); | |
// Register the SliderView as a Web Component for easier re-use. | |
var SliderPrototype = Object.create(HTMLElement.prototype); | |
var sliderView; | |
SliderPrototype.createdCallback = function() { | |
sliderView = new SliderView({ | |
el: this | |
}); | |
sliderView.render(); | |
}; | |
SliderPrototype.attachedCallback = function() { | |
sliderView.triggerMethod('attach'); | |
}; | |
document.registerElement('streamus-slider', { | |
prototype: SliderPrototype | |
}); | |
var data = $('#data'); | |
$('streamus-slider').on('input', function(event, value) { | |
data.text(value); | |
}); | |
data.text($('streamus-slider').val()); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment