Skip to content

Instantly share code, notes, and snippets.

@samhains
Created September 8, 2015 15:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samhains/41eb70c4faa4d016e8ac to your computer and use it in GitHub Desktop.
Save samhains/41eb70c4faa4d016e8ac to your computer and use it in GitHub Desktop.
React components for slider
#= require ./diff-slider
#= require ./diff-panel
DiffSlider = window.Aeon.Components.DiffSlider
DiffPanel = window.Aeon.Components.DiffPanel
window.Aeon.Components.RevisionsContainer = React.createClass
setCurrDiff: (diff) ->
@_updateDiffPanel diff, diff+1
componentWillMount: ->
@_updateDiffPanel @state.prevDiffIndex, @state.currDiffIndex
getInitialState: ->
currDiffIndex: @props.versions.length-1,
prevDiffIndex: @props.versions.length-2,
splitHandle: false,
disableHideSign: true,
showHandleInfo: true,
handleInfoPosition: 0,
prevDiff: '',
currDiff: ''
toggleHandle: ->
@setState
splitHandle: !@state.splitHandle
prevDiffIndex: @state.currDiffIndex-1
updateDiffs: (e) ->
if @state.splitHandle
prevIndex = e[0]
currIndex = e[1]
else
currIndex = parseInt(e[0])
prevIndex = currIndex-1
@_updateDiffPanel prevIndex, currIndex
showSign: (index) ->
@setState
showHandleInfo: true
handleInfoPosition: index
hideSign: ->
@setState
showHandleInfo: false
editInformation: ->
currVersion = @props.versions[@state.currDiffIndex].version
<div className='revisions__info-container cf'>
{@_renderHandles()}
<input type="hidden" name="version_id" value={currVersion.id}/>
<button type="submit" className='button button--link revisions__restore-button'> Restore This Version</button>
</div>
_renderHandles: ->
prevDiffIndex = @state.prevDiffIndex
if @state.prevDiffIndex < 0
prevDiffIndex = 0
currVersion = @props.versions[@state.currDiffIndex].version
prevVersion = @props.versions[prevDiffIndex].version
if @state.splitHandle
<div>
{@versionInfoTemplate prevVersion, false, @state.prevDiffIndex}
{@versionInfoTemplate currVersion, true, @state.currDiffIndex}
</div>
else
@versionInfoTemplate currVersion, true, @state.currDiffIndex
returnPopupToCurrPosition: ->
@setState handleInfoPosition: @state.currDiffIndex
versionInfoTemplate: (version, isCurrentVersion, handleIndex) ->
created_at = moment(version.created_at)
time = moment(created_at).format('MMM D @ h:mm')
timeFromNow = moment(created_at, "YYYYMMDD").fromNow()
classStr = if isCurrentVersion then 'revisions__curr-version-info' else 'revisions__prev-version-info'
classStr += ' span-6'
event = version.event
if event == 'create'
event = 'Essay created'
<span className={classStr}>
{event} by {version.user.name}
<div> {timeFromNow}
<span className='revisions__small-time'> ({time}) </span></div>
</span>
_updateDiffPanel: (prevIndex, currIndex) ->
if prevIndex < 0
prevIndex = 0
prevVersion = @props.versions[prevIndex].version
currVersion = @props.versions[currIndex].version
$.ajax
url: "/editorial/essays/diff?prev_id=#{prevVersion.id}&curr_id=#{currVersion.id}",
type: "GET",
success: (data) =>
@setState
prevDiffIndex: prevIndex
currDiffIndex: currIndex
currDiff: data.data[1]
prevDiff: data.data[0]
error: (data) =>
console.log data
render: ->
barWidth = 1/(@props.versions.length)*100
<div>
<span className='revisions__checkbox-text'>Compare Two Revisions
<input className='revisions__checkbox' onClick={@toggleHandle} type='checkbox'/>
</span>
<br/>
<br/>
<DiffSlider
versions={@props.versions}
updateDiffs={@updateDiffs}
splitHandle={@state.splitHandle}
versionInfoTemplate={@versionInfoTemplate}
returnPopupToCurrPosition={@returnPopupToCurrPosition}
handleInfoPos={@state.handleInfoPosition}
currDiffIndex={@state.currDiffIndex}
barWidth={barWidth}
prevDiffIndex={@state.prevDiffIndex}
showSign={@showSign}
/> {@editInformation()} <hr/>
<DiffPanel
prevDiff={@state.prevDiff}
currDiff={@state.currDiff}/>
</div>
window.Aeon.Components.DiffPanel = React.createClass
render: ->
<div className="revisions-panel">
<div className='revisions__prev-essay span-6'>
<div dangerouslySetInnerHTML={__html: @props.prevDiff} />
</div>
<div className='revisions__next-essay span-6'>
<div dangerouslySetInnerHTML={__html: @props.currDiff} />
</div>
</div>
#= require ./slider
ReactSlider = window.Aeon.Components.ReactSlider
window.Aeon.Components.DiffSlider = React.createClass
renderSliderTemplate: (defaultValue, children) ->
<div id='slider' onMouseLeave={@props.hideSign}>
{@_renderHandlePopup()}
<ReactSlider withBars
defaultValue={defaultValue}
key={@props.splitHandle}
min={0}
showSign={@props.showSign}
numOfBars={@props.versions.length}
returnPopupToCurrPosition={@props.returnPopupToCurrPosition}
barWidth={@props.barWidth}
max={@props.versions.length-1}
onAfterChange={@props.updateDiffs} >
{children}
</ReactSlider>
</div>
renderSlider: (splitHandle) ->
if splitHandle
children = <span><div className="handle--P"></div><div className="handle--C"></div></span>
@renderSliderTemplate [@props.prevDiffIndex, @props.currDiffIndex], children.props.children
else
@renderSliderTemplate @props.currDiffIndex, null
_buildPopupStyle: ->
left = @props.handleInfoPos*@props.barWidth
left: (left+1)+'%'
_renderHandlePopup: ->
version = @props.versions[@props.handleInfoPos].version
<div
className='handle__popup-box' style={@_buildPopupStyle()} >
{@props.versionInfoTemplate(version, false, @props.handleInfoPos)}
</div>
render: ->
<div className='slider__container'>
{@renderSlider(@props.splitHandle)}
</div>
pauseEvent = (e) ->
if e.stopPropagation
e.stopPropagation()
if e.preventDefault
e.preventDefault()
e.cancelBubble = true
e.returnValue = false
e.stopPropagation()
stopPropagation = (e) ->
if e.stopPropagation
e.stopPropagation()
e.cancelBubble = true
e.stopPropagation()
linspace = (min, max, count) ->
range = (max - min) / (count - 1)
res = []
i = 0
while i < count
res.push min + range * i
i++
res
ensureArray = (x) ->
unless x? then [] else (if Array.isArray(x) then x else [ x ])
undoEnsureArray = (x) ->
if x != null and x.length == 1 then x[0] else x
window.Aeon.Components.ReactSlider = React.createClass(
displayName: 'ReactSlider'
propTypes:
min: React.PropTypes.number
max: React.PropTypes.number
step: React.PropTypes.number
minDistance: React.PropTypes.number
defaultValue: React.PropTypes.oneOfType([
React.PropTypes.number
React.PropTypes.arrayOf(React.PropTypes.number)
])
value: React.PropTypes.oneOfType([
React.PropTypes.number
React.PropTypes.arrayOf(React.PropTypes.number)
])
orientation: React.PropTypes.oneOf([
'horizontal'
'vertical'
])
className: React.PropTypes.string
handleClassName: React.PropTypes.string
handleActiveClassName: React.PropTypes.string
withBars: React.PropTypes.bool
barClassName: React.PropTypes.string
pearling: React.PropTypes.bool
disabled: React.PropTypes.bool
snapDragDisabled: React.PropTypes.bool
invert: React.PropTypes.bool
onBeforeChange: React.PropTypes.func
onChange: React.PropTypes.func
onAfterChange: React.PropTypes.func
onSliderClick: React.PropTypes.func
getDefaultProps: ->
{
barClassName: 'bar'
className: 'slider'
defaultValue: 0
disabled: false
handleActiveClassName: 'active'
handleClassName: 'handle'
invert: false
max: 100
min: 0
sliderLength: 400
numOfBars: 100
barWidth: 4
minDistance: 0
orientation: 'horizontal'
pearling: false
snapDragDisabled: false
step: 1
withBars: false
}
getInitialState: ->
value = @_or(ensureArray(@props.value), ensureArray(@props.defaultValue))
# reused throughout the component to store results of iterations over `value`
@tempArray = value.slice()
zIndices = []
i = 0
while i < value.length
value[i] = @_trimAlignValue(value[i], @props)
zIndices.push i
i++
{
index: -1
upperBound: 0
sliderLength: 0
value: value
zIndices: zIndices
}
componentWillReceiveProps: (newProps) ->
value = @_or(ensureArray(newProps.value), @state.value)
# ensure the array keeps the same size as `value`
@tempArray = value.slice()
i = 0
while i < value.length
@state.value[i] = @_trimAlignValue(value[i], newProps)
i++
if @state.value.length > value.length
@state.value.length = value.length
# If an upperBound has not yet been determined (due to the component being hidden
# during the mount event, or during the last resize), then calculate it now
if @state.upperBound == 0
@_handleResize()
return
_or: (value, defaultValue) ->
count = React.Children.count(@props.children)
switch count
when 0
if value.length > 0 then value else defaultValue
when value.length
value
when defaultValue.length
defaultValue
else
if value.length != count or defaultValue.length != count
console.warn @constructor.displayName + ': Number of values does not match number of children.'
linspace(@props.min, @props.max, count)
componentDidMount: ->
window.addEventListener 'resize', @_handleResize
@_handleResize()
componentWillUnmount: ->
window.removeEventListener 'resize', @_handleResize
getValue: ->
undoEnsureArray @state.value
_handleResize: ->
# setTimeout of 0 gives element enough time to have assumed its new size if it is being resized
window.setTimeout(=>
slider = @refs.slider.getDOMNode()
handle = @refs.handle0.getDOMNode()
rect = slider.getBoundingClientRect()
size = @_sizeKey()
sliderMax = rect[@_posMaxKey()]
sliderMin = rect[@_posMinKey()]
@setState
upperBound: slider[size] - (handle[size])
sliderLength: Math.abs(sliderMax - sliderMin)
handleSize: handle[size]
sliderStart: if @props.invert then sliderMax else sliderMin
, 0)
_calcOffset: (value) ->
ratio = (value - (@props.min)) / (@props.max - (@props.min))
ratio * @state.upperBound
_calcValue: (offset) ->
ratio = offset / @state.upperBound
ratio * (@props.max - (@props.min)) + @props.min
_buildHandleStyle: (offset, i, value) ->
style =
position: 'absolute'
width: @props.barWidth+'%'
willChange: if @state.index >= 0 then @_posMinKey() else ''
zIndex: @state.zIndices.indexOf(i) + 1
style[@_posMinKey()] = ((value)*@props.barWidth) + '%'
style
_buildBarStyle: (i) ->
position: 'absolute'
left: i*@props.barWidth+'%'
width: @props.barWidth+'%'
_getClosestIndex: (pixelOffset) ->
minDist = Number.MAX_VALUE
closestIndex = -1
value = @state.value
l = value.length
i = 0
while i < l
offset = @_calcOffset(value[i])
dist = Math.abs(pixelOffset - offset)
if dist < minDist
minDist = dist
closestIndex = i
i++
closestIndex
_calcOffsetFromPosition: (position) ->
pixelOffset = position - (@state.sliderStart)
if @props.invert
pixelOffset = @state.sliderLength - pixelOffset
pixelOffset -= @state.handleSize / 2
pixelOffset
_forceValueFromPosition: (position, callback) ->
pixelOffset = @_calcOffsetFromPosition(position)
closestIndex = @_getClosestIndex(pixelOffset)
nextValue = @_trimAlignValue(@_calcValue(pixelOffset))
value = @state.value.slice()
# Clone this.state.value since we'll modify it temporarily
value[closestIndex] = nextValue
# Prevents the slider from shrinking below `props.minDistance`
i = 0
while i < value.length - 1
if value[i + 1] - (value[i]) < @props.minDistance
return
i += 1
@setState { value: value }, callback.bind(this, closestIndex)
_getMousePosition: (e) ->
[
e['page' + @_axisKey()]
e['page' + @_orthogonalAxisKey()]
]
_getTouchPosition: (e) ->
touch = e.touches[0]
[
touch['page' + @_axisKey()]
touch['page' + @_orthogonalAxisKey()]
]
_getMouseEventMap: ->
{
'mousemove': @_onMouseMove
'mouseup': @_onMouseUp
}
_getTouchEventMap: ->
{
'touchmove': @_onTouchMove
'touchend': @_onTouchEnd
}
_createOnMouseDown: (i) ->
(e) =>
if @props.disabled
return
position = @_getMousePosition(e)
@_start i, position[0]
@_addHandlers @_getMouseEventMap()
pauseEvent e
_createOnTouchStart: (i) ->
(e) =>
if @props.disabled or e.touches.length > 1
return
position = @_getTouchPosition(e)
@startPosition = position
@isScrolling = undefined
# don't know yet if the user is trying to scroll
@_start i, position[0]
@_addHandlers @_getTouchEventMap()
stopPropagation e
_addHandlers: (eventMap) ->
for key of eventMap
document.addEventListener key, eventMap[key], false
_removeHandlers: (eventMap) ->
for key of eventMap
document.removeEventListener key, eventMap[key], false
_start: (i, position) ->
# if activeElement is body window will lost focus in IE9
if document.activeElement and document.activeElement != document.body
document.activeElement.blur()
@hasMoved = false
@_fireChangeEvent 'onBeforeChange'
zIndices = @state.zIndices
zIndices.splice zIndices.indexOf(i), 1
# remove wherever the element is
zIndices.push i
# add to end
@setState
startValue: @state.value[i]
startPosition: position
index: i
zIndices: zIndices
_onMouseUp: ->
@_onEnd @_getMouseEventMap()
_onTouchEnd: ->
@_onEnd @_getTouchEventMap()
_onEnd: (eventMap) ->
@_removeHandlers eventMap
@setState { index: -1 }, @_fireChangeEvent.bind(this, 'onAfterChange')
_onMouseMove: (e) ->
position = @_getMousePosition(e)
@_move position[0]
_onTouchMove: (e) ->
if e.touches.length > 1
return
position = @_getTouchPosition(e)
if typeof @isScrolling == 'undefined'
diffMainDir = position[0] - (@startPosition[0])
diffScrollDir = position[1] - (@startPosition[1])
@isScrolling = Math.abs(diffScrollDir) > Math.abs(diffMainDir)
if @isScrolling
@setState index: -1
return
pauseEvent e
@_move position[0]
_move: (position) ->
@hasMoved = true
index = @state.index
value = @state.value
length = value.length
oldValue = value[index]
diffPosition = position - (@state.startPosition)
# if @props.invert
# diffPosition *= -1
diffValue = diffPosition / (@state.sliderLength - (@state.handleSize)) * (@props.max - (@props.min))
newValue = @_trimAlignValue(@state.startValue + diffValue)
minDistance = @props.minDistance
# if "pearling" (= handles pushing each other) is disabled,
# prevent the handle from getting closer than `minDistance` to the previous or next handle.
if !@props.pearling
if index > 0
valueBefore = value[index - 1]
if newValue < valueBefore + minDistance
newValue = valueBefore + minDistance
if index < length - 1
valueAfter = value[index + 1]
if newValue > valueAfter - minDistance
newValue = valueAfter - minDistance
value[index] = newValue
#if "pearling" is enabled, let the current handle push the pre- and succeeding handles.
if @props.pearling and length > 1
if newValue > oldValue
@_pushSucceeding value, minDistance, index
@_trimSucceeding length, value, minDistance, @props.max
else if newValue < oldValue
@_pushPreceding value, minDistance, index
@_trimPreceding length, value, minDistance, @props.min
# Normally you would use `shouldComponentUpdate`, but since the slider is a low-level component,
# the extra complexity might be worth the extra performance.
if newValue != oldValue
@props.showSign(value[index])
@setState { value: value }, @_fireChangeEvent.bind(this, 'onChange')
_pushSucceeding: (value, minDistance, index) ->
i = undefined
padding = undefined
i = index
padding = value[i] + minDistance
while value[i + 1] != null and padding > value[i + 1]
value[i + 1] = @_alignValue(padding)
i++
padding = value[i] + minDistance
return
_trimSucceeding: (length, nextValue, minDistance, max) ->
i = 0
while i < length
padding = max - (i * minDistance)
if nextValue[length - 1 - i] > padding
nextValue[length - 1 - i] = padding
i++
return
_pushPreceding: (value, minDistance, index) ->
i = undefined
padding = undefined
i = index
padding = value[i] - minDistance
while value[i - 1] != null and padding < value[i - 1]
value[i - 1] = @_alignValue(padding)
i--
padding = value[i] - minDistance
return
_trimPreceding: (length, nextValue, minDistance, min) ->
i = 0
while i < length
padding = min + i * minDistance
if nextValue[i] < padding
nextValue[i] = padding
i++
return
_axisKey: ->
orientation = @props.orientation
if orientation == 'horizontal'
return 'X'
if orientation == 'vertical'
return 'Y'
return
_orthogonalAxisKey: ->
orientation = @props.orientation
if orientation == 'horizontal'
return 'Y'
if orientation == 'vertical'
return 'X'
return
_posMinKey: ->
orientation = @props.orientation
if orientation == 'horizontal'
return if @props.invert then 'right' else 'left'
if orientation == 'vertical'
return if @props.invert then 'bottom' else 'top'
return
_posMaxKey: ->
orientation = @props.orientation
if orientation == 'horizontal'
return if @props.invert then 'left' else 'right'
if orientation == 'vertical'
return if @props.invert then 'top' else 'bottom'
return
_sizeKey: ->
orientation = @props.orientation
if orientation == 'horizontal'
return 'clientWidth'
if orientation == 'vertical'
return 'clientHeight'
return
_trimAlignValue: (val, props) ->
@_alignValue @_trimValue(val, props), props
_trimValue: (val, props) ->
props = props or @props
if val <= props.min
val = props.min
if val >= props.max
val = props.max
val
_alignValue: (val, props) ->
props = props or @props
valModStep = (val - (props.min)) % props.step
alignValue = val - valModStep
if Math.abs(valModStep) * 2 >= props.step
alignValue += if valModStep > 0 then props.step else -props.step
parseFloat alignValue.toFixed(5)
_handleMouseEnter: (handleId) ->
@props.showSign(@state.value[handleId])
_renderHandle: (style, child, i) ->
className = @props.handleClassName + ' ' + @props.handleClassName + '-' + i + ' ' + (if @state.index == i then @props.handleActiveClassName else '')
React.createElement 'div', {
ref: 'handle' + i
key: 'handle' + i
className: className
style: style
onMouseEnter: @_handleMouseEnter.bind(@, i)
onMouseDown: @_createOnMouseDown i
onTouchStart: @_createOnTouchStart i
}, child
_renderHandles: (offset, value) ->
length = offset.length
styles = @tempArray
i = 0
while i < length
styles[i] = @_buildHandleStyle(offset[i], i, value[i])
i++
res = @tempArray
renderHandle = @_renderHandle
if React.Children.count(@props.children) > 0
React.Children.forEach @props.children, (child, i) ->
res[i] = renderHandle(styles[i], child, i)
return
else
i = 0
while i < length
res[i] = renderHandle(styles[i], null, i)
i++
res
_barMouseEnter : (index) ->
@props.showSign(index)
_renderBar: (i) ->
React.createElement 'div',
#key: 'bar' + i
ref: 'bar' + i
onMouseLeave: @props.returnPopupToCurrPosition
onMouseEnter: @_barMouseEnter.bind(@, i)
className: @props.barClassName + ' ' + @props.barClassName + '-' + i
style: @_buildBarStyle i
_renderBars: (offset) ->
[0...@props.numOfBars].map (i) =>
@_renderBar(i)
_onSliderMouseDown: (e) ->
if @props.disabled
return
@hasMoved = false
if !@props.snapDragDisabled
position = @_getMousePosition(e)
@_forceValueFromPosition position[0], (i) =>
@_fireChangeEvent 'onChange'
@_start i, position[0]
@_addHandlers @_getMouseEventMap()
pauseEvent e
return
_onSliderClick: (e) ->
if @props.disabled
return
if @props.onSliderClick and !@hasMoved
position = @_getMousePosition(e)
calcOffset = @_calcOffsetFromPosition(position[0])
calcValue = @_calcValue(calcOffset)
valueAtPos = @_trimAlignValue(calcValue)
@props.onSliderClick valueAtPos
return
_fireChangeEvent: (event) ->
if @props[event]
@props[event](@state.value)
render: ->
state = @state
props = @props
offset = @tempArray
value = state.value
l = value.length
i = 0
while i < l
offset[i] = @_calcOffset(value[i], i)
i++
bars = if props.withBars then @_renderBars(offset) else null
handles = @_renderHandles(offset, value)
React.createElement 'div', {
ref: 'slider'
style: position: 'relative'
className: props.className + (if props.disabled then ' disabled' else '')
onMouseDown: @_onSliderMouseDown
onClick: @_onSliderClick
}, bars, handles
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment