@livingdocs/angular-slider
Version:
Slider directive implementation for AngularJS, without jQuery dependencies.
243 lines (208 loc) • 8.97 kB
text/coffeescript
# CONSTANTS
MODULE_NAME = 'ui.slider'
SLIDER_TAG = 'slider'
# HELPER FUNCTIONS
angularize = (element) -> angular.element element
pixelize = (position) -> "#{position}px"
hide = (element) -> element.css opacity: 0
show = (element) -> element.css opacity: 1
offset = (element, position) -> element.css left: position
halfWidth = (element) -> element[0].offsetWidth / 2
offsetLeft = (element) -> element[0].offsetLeft
width = (element) -> element[0].offsetWidth
gap = (element1, element2) ->
offsetLeft(element2) - offsetLeft(element1) - width(element1)
contain = (value) ->
return value if isNaN value
Math.min Math.max(0, value), 100
roundStep = (value, precision, step, floor = 0) ->
step ?= 1 / Math.pow(10, precision)
remainder = (value - floor) % step
steppedValue =
if remainder > (step / 2)
then value + step - remainder
else value - remainder
decimals = Math.pow 10, precision
roundedValue = steppedValue * decimals / decimals
parseFloat roundedValue.toFixed precision
events =
mouse:
start: 'mousedown'
move: 'mousemove'
end: 'mouseup'
touch:
start: 'touchstart'
move: 'touchmove'
end: 'touchend'
# DIRECTIVE DEFINITION
sliderDirective = ($timeout) ->
restrict: 'E'
scope:
floor: '@'
ceiling: '@'
values: '=?'
step: '@'
highlight: '@'
precision: '@'
buffer: '@'
dragstop: '@'
ngModel: '=?'
ngModelLow: '=?'
ngModelHigh: '=?'
dragEnd: '&'
valueDecorator:'&'
template: '''
<div class="bar"><div class="selection"></div></div>
<div class="handle low"></div><div class="handle high"></div>
<div class="bubble limit low">{{ values.length ? values[floor || 0] : floor }}</div>
<div class="bubble limit high">{{ values.length ? values[ceiling || values.length - 1] : ceiling }}</div>
<div class="bubble value low">{{ values.length ? valueCalculator( {value: values[local.ngModelLow || local.ngModel || 0]} ) : valueCalculator( {value: local.ngModelLow || local.ngModel || 0} ) }}</div>
<div class="bubble value high">{{ values.length ? valueCalculator( {value: values[local.ngModelHigh]} ) : valueCalculator( {value: local.ngModelHigh} ) }}</div>'''
compile: (element, attributes) ->
# Check if it is a range slider
range = !attributes.ngModel? and attributes.ngModelLow? and attributes.ngModelHigh?
low = if range then 'ngModelLow' else 'ngModel'
high = 'ngModelHigh'
# Scope values to watch for changes
watchables = ['floor', 'ceiling', 'values', low]
watchables.push high if range
post: (scope, element, attributes) ->
# just print the value if no decorator is specified
scope.valueCalculator = (value) ->
scope.valueDecorator(value) || value?.value
# Get references to template elements
[bar, minPtr, maxPtr, flrBub, ceilBub, lowBub, highBub] = (angularize(e) for e in element.children())
selection = angularize bar.children()[0]
# Remove range specific elements if not a range slider
unless range
upper.remove() for upper in [maxPtr, highBub]
selection.remove() unless attributes.highlight
scope.local = {}
scope.local[low] = scope[low]
scope.local[high] = scope[high]
bound = false
ngDocument = angularize document
handleHalfWidth = barWidth = minOffset = maxOffset = minValue = maxValue = valueRange = offsetRange = undefined
dimensions = ->
# roundStep the initial score values
scope.step ?= 1
scope.floor ?= 0
scope.precision ?= 0
scope.ngModelLow = scope.ngModel unless range
scope.ceiling ?= scope.values.length - 1 if scope.values?.length
scope.local[low] = scope[low]
scope.local[high] = scope[high]
for value in watchables
scope[value] = roundStep(parseFloat(scope[value]),
parseInt(scope.precision), parseFloat(scope.step),
parseFloat(scope.floor)) if typeof value is 'number'
# Commonly used measurements
handleHalfWidth = halfWidth minPtr
barWidth = width bar
minOffset = 0
maxOffset = barWidth - width(minPtr)
minValue = parseFloat scope.floor
maxValue = parseFloat scope.ceiling
valueRange = maxValue - minValue
offsetRange = maxOffset - minOffset
updateDOM = ->
dimensions()
# Translation functions
percentOffset = (offset) -> contain ((offset - minOffset) / offsetRange) * 100
percentValue = (value) -> contain ((value - minValue) / valueRange) * 100
pixelsToOffset = (percent) -> pixelize percent * offsetRange / 100
setPointers = ->
offset ceilBub, pixelize(barWidth - width(ceilBub))
newLowValue = percentValue scope.local[low]
offset minPtr, pixelsToOffset newLowValue
offset lowBub, pixelize(offsetLeft(minPtr) - (halfWidth lowBub) + handleHalfWidth)
offset selection, pixelize(offsetLeft(minPtr) + handleHalfWidth)
switch true
when range
newHighValue = percentValue scope.local[high]
offset maxPtr, pixelsToOffset newHighValue
offset highBub, pixelize(offsetLeft(maxPtr) - (halfWidth highBub) + handleHalfWidth)
selection.css width: pixelsToOffset newHighValue - newLowValue
when attributes.highlight is 'right'
selection.css width: pixelsToOffset 110 - newLowValue
when attributes.highlight is 'left'
selection.css width: pixelsToOffset newLowValue
offset selection, 0
bind = (handle, bubble, ref, events) ->
currentRef = ref
onEnd = ->
bubble.removeClass 'active'
handle.removeClass 'active'
ngDocument.unbind events.move
ngDocument.unbind events.end
if scope.dragstop
scope[high] = scope.local[high]
scope[low] = scope.local[low]
currentRef = ref
scope.dragEnd()
scope.$apply()
onMove = (event) ->
eventX = event.clientX or event.touches?[0].clientX or event.originalEvent?.changedTouches?[0].clientX or 0
newOffset = eventX - element[0].getBoundingClientRect().left - handleHalfWidth
newOffset = Math.max(Math.min(newOffset, maxOffset), minOffset)
newPercent = percentOffset newOffset
newValue = minValue + (valueRange * newPercent / 100.0)
if range
switch currentRef
when low
if newValue > scope.local[high]
currentRef = high
minPtr.removeClass 'active'
lowBub.removeClass 'active'
maxPtr.addClass 'active'
highBub.addClass 'active'
setPointers()
else if scope.buffer > 0
newValue = Math.min newValue,
scope.local[high] - scope.buffer
when high
if newValue < scope.local[low]
currentRef = low
maxPtr.removeClass 'active'
highBub.removeClass 'active'
minPtr.addClass 'active'
lowBub.addClass 'active'
setPointers()
else if scope.buffer > 0
newValue = Math.max newValue,
parseInt(scope.local[low]) + parseInt(scope.buffer)
newValue = roundStep(newValue, parseInt(scope.precision), parseFloat(scope.step), parseFloat(scope.floor))
scope.local[currentRef] = newValue
unless scope.dragstop
scope[currentRef] = newValue
setPointers()
scope.$apply()
onStart = (event) ->
dimensions()
bubble.addClass 'active'
handle.addClass 'active'
setPointers()
event.stopPropagation()
event.preventDefault()
ngDocument.bind events.move, onMove
ngDocument.bind events.end, onEnd
handle.bind events.start, onStart
setBindings = ->
for method in ['touch', 'mouse']
bind minPtr, lowBub, low, events[method]
bind maxPtr, highBub, high, events[method]
bound = true
setBindings() unless bound
setPointers()
$timeout updateDOM
scope.$watch w, updateDOM, true for w in watchables
window.addEventListener 'resize', updateDOM
qualifiedDirectiveDefinition = [
'$timeout'
sliderDirective
]
module = (window, angular) ->
angular
.module(MODULE_NAME, [])
.directive(SLIDER_TAG, qualifiedDirectiveDefinition)
module window, window.angular