Skip to content

Instantly share code, notes, and snippets.

@paveltyavin
Created February 12, 2016 01:29
Show Gist options
  • Save paveltyavin/b9895fbc58fbf1c5e750 to your computer and use it in GitHub Desktop.
Save paveltyavin/b9895fbc58fbf1c5e750 to your computer and use it in GitHub Desktop.
module.exports = class Base
edge: (which) ->
if which == 'start'
return 'left'
else if which == 'end'
return 'right'
throw new TypeError('What kind of an edge is ' + which)
edgeProp: (edge, prop) ->
o = @$el[prop]()
o[@edge(edge)]
startProp: (prop) ->
@edgeProp 'start', prop
endProp: (prop) ->
@edgeProp 'end', prop
has = Object::hasOwnProperty
module.exports = (prop, event) ->
if has.call(event, prop)
return event[prop]
else if event.originalEvent and event.originalEvent.touches
return event.originalEvent.touches[0][prop]
else
return 0
$ = require('jquery')
Range = require('./range')
requestAnimationFrame = require('./raf')
class Phantom extends Range
constructor: (options)->
options = $.extend({
readonly: true
label: '+'
}, options)
super options
@$el.addClass 'bbslider-phantom'
@$el.on 'mousedown.bbslider touchstart.bbslider', @mousedown
mousedown: (ev) =>
if ev.which == 1
# left mouse button
range_val = @val()
newRange = @options.perant.addRange(range_val)
@$el.remove()
@options.perant.$el.trigger 'addrange', [
newRange.val()
newRange
]
requestAnimationFrame ->
newRange.$el.find('.bbslider-handle:first-child').trigger ev.type
removePhantom: ->
null
module.exports = Phantom
# thanks to http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
lastTime = 0
vendors = [
'webkit'
'moz'
]
requestAnimationFrame = window.requestAnimationFrame
x = 0
while x < vendors.length and !window.requestAnimationFrame
requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']
++x
if !requestAnimationFrame
requestAnimationFrame = (callback, element) ->
currTime = (new Date).getTime()
timeToCall = Math.max(0, 16 - (currTime - lastTime))
id = window.setTimeout((->
callback currTime + timeToCall
), timeToCall)
lastTime = currTime + timeToCall
id
module.exports = requestAnimationFrame
$ = require('jquery')
Base = require('./base')
getEventProperty = require('./eventprop')
class Range extends Base
constructor: (options) ->
@id = options.id || Math.random().toString(36).substring(7)
@$el = $('<div class="bbslider-range"><span class="bbslider-barlabel">')
@options = options
@perant = options.perant
if @options.rangeClass
@$el.addClass @options.rangeClass
if !@readonly()
@$el.prepend('<div class="bbslider-handle">').append '<div class="bbslider-handle">'
@$el.on 'mouseenter.bbslider touchstart.bbslider', @removePhantom
@$el.on 'mousedown.bbslider touchstart.bbslider', @mousedown
@$el.on 'click', @click
else
@$el.addClass 'bbslider-readonly'
if typeof @options.label == 'function'
@$el.on 'changing', (ev, range) =>
@writeLabel @options.label range.map(@perant.normalise), @
else
@writeLabel @options.label
@range = []
@hasChanged = false
if @options.value
@val @options.value
writeLabel: (text) =>
@$el.find('.bbslider-barlabel')[if @options.htmlLabel then 'html' else 'text'] text
removePhantom: =>
@perant.removePhantom()
readonly: =>
if typeof @options.readonly == 'function'
return @options.readonly.call(@perant, @)
@options.readonly
val: (range, valOpts) =>
if typeof range == 'undefined'
return @range
snap = (val) =>
Math.round(val / @options.snap) * @options.snap
valOpts = $.extend({}, {
dontApplyDelta: false
trigger: true
}, valOpts or {})
next = @perant.nextRange(@$el)
prev = @perant.prevRange(@$el)
delta = range[1] - (range[0])
if @options.snap
range = range.map(snap)
delta = snap(delta)
if next and next.val()[0] <= range[1] and prev and prev.val()[1] >= range[0]
range[1] = next.val()[0]
range[0] = prev.val()[1]
if next and next.val()[0] < range[1]
if !@perant.options.allowSwap or next.val()[1] >= range[0]
range[1] = next.val()[0]
if !valOpts.dontApplyDelta
range[0] = range[1] - delta
else
@perant.repositionRange @, range
if prev and prev.val()[1] > range[0]
if !@perant.options.allowSwap or prev.val()[0] <= range[1]
range[0] = prev.val()[1]
if !valOpts.dontApplyDelta
range[1] = range[0] + delta
else
@perant.repositionRange @, range
if range[1] >= 1
range[1] = 1
if !valOpts.dontApplyDelta
range[0] = 1 - delta
if range[0] <= 0
range[0] = 0
if !valOpts.dontApplyDelta
range[1] = delta
if @perant.options.bound
bound = @perant.options.bound(@)
if bound
if bound.upper and range[1] > @perant.abnormalise(bound.upper)
range[1] = @perant.abnormalise(bound.upper)
if !valOpts.dontApplyDelta
range[0] = range[1] - delta
if bound.lower and range[0] < @perant.abnormalise(bound.lower)
range[0] = @perant.abnormalise(bound.lower)
if !valOpts.dontApplyDelta
range[1] = range[0] + delta
if @range[0] == range[0] and @range[1] == range[1]
return @$el
@range = range
if valOpts.trigger
@$el.triggerHandler 'changing', [
range
@$el
]
@hasChanged = true
start = 100 * range[0] + '%'
size = 100 * (range[1] - (range[0])) + '%'
drawCss =
left: start
minWidth: size
if @drawing
return @$el
requestAnimationFrame =>
@drawing = false
@$el.css drawCss
@drawing = true
@
click: (ev) =>
ev.preventDefault()
mousedown: (ev) =>
ev.stopPropagation()
ev.preventDefault()
@hasChanged = false
if ev.which > 1
return
if $(ev.target).is('.bbslider-handle:first-child')
$('body').addClass 'bbslider-resizing'
$(document).on 'mousemove.bbslider touchmove.bbslider', @resizeStart(ev)
else if $(ev.target).is('.bbslider-handle:last-child')
$('body').addClass 'bbslider-resizing'
$(document).on 'mousemove.bbslider touchmove.bbslider', @resizeEnd(ev)
else
$('body').addClass 'bbslider-dragging'
$(document).on 'mousemove.bbslider touchmove.bbslider', @drag(ev)
$(document).one 'mouseup.bbslider touchend.bbslider', (ev) =>
ev.stopPropagation()
ev.preventDefault()
if @hasChanged and !@swapping
@$el.trigger 'change', [
@range
@$el
]
@swapping = false
$(document).off 'mouseup.bbslider mousemove.bbslider touchend.bbslider touchmove.bbslider'
$('body').removeClass 'bbslider-resizing bbslider-dragging'
drag: (origEv) =>
beginStart = @startProp('offset')
mousePos = getEventProperty('clientX', origEv)
mouseOffset = if mousePos then mousePos - beginStart else 0
beginSize = @$el.width()
perant = @options.perant
perantStart = perant.startProp('offset')
perantSize = perant.$el.width()
return (ev) =>
ev.stopPropagation()
ev.preventDefault()
mousePos = getEventProperty('clientX', ev)
if mousePos
start = mousePos - perantStart - mouseOffset
if start >= 0 and start <= perantSize - beginSize
rangeOffset = start / perantSize - (@range[0])
@val [
start / perantSize
@range[1] + rangeOffset
]
else
mouseOffset = mousePos - @startProp('offset')
resizeEnd: (origEv) =>
beginStart = @startProp('offset')
beginPosStart = @startProp('position')
perant = @options.perant
perantSize = perant.$el.width()
minSize = @options.minSize * perantSize
return (ev) =>
opposite = if ev.type == 'touchmove' then 'touchend' else 'mouseup'
subsequent = if ev.type == 'touchmove' then 'touchstart' else 'mousedown'
ev.stopPropagation()
ev.preventDefault()
mousePos = getEventProperty('clientX', ev)
size = mousePos - beginStart
if mousePos
if size > perantSize - beginPosStart
size = perantSize - beginPosStart
if size >= minSize
@val [
@range[0]
@range[0] + size / perantSize
], dontApplyDelta: true
else if size <= 10
@swapping = true
$(document).trigger opposite + '.bbslider'
@$el.find('.bbslider-handle:first-child').trigger subsequent + '.bbslider'
resizeStart: (origEv) =>
beginStart = @startProp('offset')
beginPosStart = @startProp('position')
mousePos = getEventProperty('clientX', origEv)
mouseOffset = if mousePos then mousePos - beginStart else 0
beginSize = @$el.width()
perant = @options.perant
perantStart = perant.startProp('offset')
perantSize = perant.$el.width()
minSize = @options.minSize * perantSize
return (ev) =>
opposite = if ev.type == 'touchmove' then 'touchend' else 'mouseup'
subsequent = if ev.type == 'touchmove' then 'touchstart' else 'mousedown'
ev.stopPropagation()
ev.preventDefault()
mousePos = getEventProperty('clientX', ev)
start = mousePos - perantStart - mouseOffset
size = beginPosStart + beginSize - start
if mousePos
if start < 0
start = 0
size = beginPosStart + beginSize
if size >= minSize
@val [
start / perantSize
@range[1]
], dontApplyDelta: true
else if size <= 10
@swapping = true
$(document).trigger opposite + '.bbslider'
@$el.find('.bbslider-handle:last-child').trigger subsequent + '.bbslider'
module.exports = Range
Range = require('./range')
Phantom = require('./phantom')
getEventProperty = require('./eventprop')
Base = require('./base')
$ = require('jquery')
class RangeBar extends Base
canAddRange: true
defaultOptions:
min: 0
max: 100
valueFormat: (a) ->
a
valueParse: (a) ->
a
maxRanges: Infinity
readonly: false
htmlLabel: false
allowSwap: true
constructor: (options) ->
options = options or {}
@$el = $('<div class="bbslider-rangebar">')
@options = $.extend({}, @defaultOptions, options)
@options.min = @options.valueParse(@options.min)
@options.max = @options.valueParse(@options.max)
if @options.barClass
@$el.addClass @options.barClass
@ranges = []
@$el.on 'mousemove.bbslider touchmove.bbslider', @mousemove
@$el.on 'mouseleave.bbslider touchleave.bbslider', @removePhantom
if options.values
@setVal options.values
normaliseRaw: (value) =>
@options.min + value * (@options.max - (@options.min))
normalise: (value) =>
@options.valueFormat @normaliseRaw(value)
abnormaliseRaw: (value) =>
(value - (@options.min)) / (@options.max - (@options.min))
abnormalise: (value) =>
@abnormaliseRaw @options.valueParse(value)
findGap: (range) =>
newIndex = 0
@ranges.forEach ($r, i) ->
if $r.val()[0] < range[0] and $r.val()[1] < range[1]
newIndex = i + 1
newIndex
insertRangeIndex: (range, index, avoidList) =>
if !avoidList
@ranges.splice index, 0, range
if @ranges[index - 1]
@ranges[index - 1].$el.after range.$el
else
@$el.prepend range.$el
addRange: (range, options) =>
if options is undefined
options = {}
$range = new Range $.extend options,
perant: @
snap: if @options.snap then @abnormaliseRaw(@options.snap + @options.min) else null
label: @options.label
rangeClass: @options.rangeClass
minSize: if @options.minSize then @abnormaliseRaw(@options.minSize + @options.min) else null
readonly: @options.readonly
htmlLabel: @options.htmlLabel
@insertRangeIndex $range, @findGap(range)
$range.val range
$range.$el.on('changing', (ev, nrange, changed) =>
ev.stopPropagation()
@$el.trigger 'changing', [
@val()
changed
]
).on 'change', (ev, nrange, changed) =>
ev.stopPropagation()
@$el.trigger 'change', [
@val()
changed
]
$range
prevRange: (range) =>
idx = range.index()
if idx >= 0
return @ranges[idx - 1]
nextRange: (range) =>
idx = range.index()
if idx >= 0
return @ranges[if range instanceof Phantom then idx else idx + 1]
setVal: (ranges) =>
if @ranges.length > ranges.length
i = ranges.length - 1
l = @ranges.length - 1
while i < l
@removeRange l
--l
@ranges.length = ranges.length
ranges.forEach (range, i) =>
if @ranges[i]
@ranges[i].val range.map(@abnormalise)
else
@addRange range.map(@abnormalise)
@
val: (ranges) =>
return @ranges.map (range) =>
range.val().map @normalise
removePhantom: =>
if @phantom
@phantom.$el.remove()
@phantom = null
removeRange: (i, noTrigger, preserveEvents) =>
if i instanceof Range
i = @ranges.indexOf(i)
if preserveEvents
method = 'detach'
else
method = 'remove'
range = @ranges.splice(i, 1)[0]
range.$el[method]()
if !noTrigger
@$el.trigger 'change', [@val()]
repositionRange: (range, val) =>
@removeRange range, true, true
@insertRangeIndex range, @findGap(val)
calcGap: (index) =>
start = if @ranges[index - 1] then @ranges[index - 1].val()[1] else 0
end = if @ranges[index] then @ranges[index].val()[0] else 1
@normaliseRaw(end) - @normaliseRaw(start)
readonly: =>
if typeof @options.readonly == 'function'
return @options.readonly.call(@)
@options.readonly
mouseMoveAddRange: (ev, val, range) =>
range.$el.one 'mouseup', =>
@$el.trigger 'change', [
@val()
range
]
@canAddRange = true
mousemove: (ev) =>
w = if @options.minSize then @abnormaliseRaw(@options.minSize + @options.min) else 0.05
pageStart = getEventProperty('pageX', ev)
val = (pageStart - @startProp('offset')) / @$el.width() - (w / 2)
direct = ev.target == ev.currentTarget
phantom = $(ev.target).is('.bbslider-phantom')
if (direct or phantom) and @ranges.length < @options.maxRanges and !$('body').is('.bbslider-dragging, .bbslider-resizing') and !@readonly()
if !@phantom
@phantom = new Phantom
perant: @
snap: if @options.snap then @abnormaliseRaw(@options.snap + @options.min) else null
label: '+'
minSize: if @options.minSize then @abnormaliseRaw(@options.minSize + @options.min) else null
rangeClass: @options.rangeClass
idx = @findGap([
val
val + w
])
if @canAddRange
@$el.one 'addrange', @mouseMoveAddRange
@canAddRange = false
if !@options.minSize or @calcGap(idx) >= @options.minSize
@insertRangeIndex @phantom, idx, true
@phantom.val [
val
val + w
], trigger: false
module.exports = RangeBar
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment