ldx-widgets
Version:
widgets
299 lines (224 loc) • 9.31 kB
text/coffeescript
###
Instructions for implementing this mixin
Requires 2 properties to be defined in the component it is mixed into:
@ROW_HEIGHT - defaults to 30, the static height of each list item
@SPACE_ABOVE_CONTENT - defaults to 68, The space in the app above of the list (for calculating the list height)
**OPTIONAL
@SPACE_BELOW_CONTENT - defaults to 0, The space in the app below the list (for calculating the list height)
Require the following props to be passed to the component it is mixed into:
@props.records - the array of objects being rendered into the list
@props.loadNextRecordset - a method that triggers the server GET for the next 'page' of records
Additional info
After a records GET is complete, the doneWithFetch method must be called
EG, if the component with this mixin is the 'results' ref, you would call the following from it's parent
@refs.results.doneWithFetch({recordsExhausted, newSearch})
The options object has two boolean attributes:
recordsExhausted - means there are no more records to fetch from the server
newSearch - means a new list is being rendered, and the scroll states should be reset
For examples of what the rende method of the results component should look like, look at one of the components that implements this mixin.
Public Methods to call from within render method
calculateRange
returns an object with start and end properties, which is the range of indexes from the records array that should be rendered
buildLoadingRow
returns an li component with a spinner for the end of the list
buildScrollBar
build the verical scrollbar, can pass optionaly className and key props
###
React = require 'react'
Animation = require 'ainojs-animation'
easing = require 'ainojs-easing'
Spinner = React.createFactory(require '../components/spinner')
{HOME, END, PAGE_UP, PAGE_DOWN, UP_ARROW, DOWN_ARROW} = require('../constants/keyboard')
{div, button, li} = React.DOM
SCROLL_THUMB_MIN = 25
HEADER_HEIGHT = 0
module.exports =
# Scroll bar properties
numOfScreens: 1
translate: 0
height: 0
getInitialState: ->
scrollY: 0
contentAreaHeight: 0
maxY: 0
noResults: no
recordsExhausted: no
componentWillReceiveProps: (nextProps) ->
if not nextProps.records.length
@setState
noResults: true
getContentAreaHeight: ->
return window.innerHeight - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT
calculateContentAreaHeight: ->
@setState
contentAreaHeight: @getContentAreaHeight()
calculateMaxY: (visibleRowCount, recordsExhausted = no) ->
if visibleRowCount?
buffer = if recordsExhausted then 0 else @ROW_HEIGHT
contentHeight = (visibleRowCount * @ROW_HEIGHT) + buffer
contentAreaHeight = @getContentAreaHeight()
if contentHeight < contentAreaHeight then return null
else return (0 - contentHeight) + contentAreaHeight
componentWillMount: ->
# Create defaults for required properties if they haven't been defined
@ROW_HEIGHT = 30 unless @ROW_HEIGHT?
@SPACE_ABOVE_CONTENT = 68 unless @SPACE_ABOVE_CONTENT?
@SPACE_BELOW_CONTENT = 0 unless @SPACE_BELOW_CONTENT?
@oldLength = null
componentDidMount: ->
@calculateContentAreaHeight()
window.addEventListener('resize', @calculateContentAreaHeight)
window.addEventListener('keydown', @handleScrollKeys)
componentWillUnmount: ->
window.removeEventListener('resize', @calculateContentAreaHeight)
window.removeEventListener('keydown', @handleScrollKeys)
if @animation?.isAnimating() then @animation.end()
checkContentPosition: (e) ->
# Only run this method if there are already results populated
unless @props.records.length then return
@handleScrollY(e) if e?
# Once at the bottom 250px of the page, fetch more records
# Only fetch if records aren't exhausted
{scrollY, maxY, recordsExhausted} = @state
{records} = @props
if scrollY - maxY <= 250
newLength = records.length
if newLength isnt @oldLength and not recordsExhausted
@oldLength = newLength
@props.loadNextRecordset()
doneWithFetch: (options) ->
{recordsExhausted, newSearch} = options
state =
maxY: @calculateMaxY(@props.records.length, recordsExhausted)
recordsExhausted: recordsExhausted
if newSearch
state.scrollY = 0
@oldLength = null
@setState state
calculateRange: (records, buffer = 1) ->
{contentAreaHeight, scrollY} = @state
# The number of rows that can fit in the content area, with some buffer
start = Math.floor(-scrollY / @ROW_HEIGHT) - buffer
end = Math.ceil((-(scrollY - contentAreaHeight)) / @ROW_HEIGHT) + buffer
{
start: if start >= 0 then start else 0
end: if end <= records.length then end else records.length
}
buildLoadingRow: (index) ->
li {
key: 'loadingRow'
className: 'grid grid-pad full result-row loading-row'
style:
height: @ROW_HEIGHT
top: @ROW_HEIGHT * index
}, Spinner {length: 5}
# Scroll Bar Methods
handleScrollKeys: (e) ->
{keyCode, metaKey, ctrlKey} = e
return unless keyCode in [HOME, END, PAGE_UP, PAGE_DOWN, UP_ARROW, DOWN_ARROW]
switch keyCode
when HOME then @animateScrollTo 0
when END then @animateScrollTo @state.maxY
when PAGE_UP then @page 1
when PAGE_DOWN then @page -1
when UP_ARROW
if metaKey or ctrlKey then @page 1
else @handleScrollY {deltaY: -@ROW_HEIGHT}, @checkContentPosition
when DOWN_ARROW
if metaKey or ctrlKey then @page -1
else @handleScrollY {deltaY: @ROW_HEIGHT}, @checkContentPosition
page: (direction) ->
{scrollY, maxY} = @state
pageDistance = window.innerHeight - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT
newScrollPos = scrollY + (pageDistance * direction)
newScrollPos = if newScrollPos > 0 then 0 else newScrollPos
newScrollPos = if newScrollPos < maxY then maxY else newScrollPos
@animateScrollTo newScrollPos
animateScrollTo: (newScrollPos) ->
if @animation?.isAnimating() then @animation.end()
{scrollY} = @state
@animation = new Animation
duration: 300
easing: easing('easeOutCirc')
.init {scrollY}
.on 'frame', @onFrame
.on 'complete', @checkContentPosition
.animateTo {scrollY: newScrollPos}
onFrame: (e) ->
@setState e.values
clickScroll: (e) ->
@scrollPos = e.clientY
document.addEventListener('mousemove', @handleMouseMoveY, false)
document.addEventListener('mouseup', @handleMouseUpY, false)
# Ends a drag of the vertical scroll thumb button
handleMouseUpY: (e) ->
document.removeEventListener('mousemove', @handleMouseMoveY, false)
document.removeEventListener('mouseup', @handleMouseUpY, false)
@handleScrollY
deltaY: 0 - (@scrollPos - e.clientY) * @numOfScreens
@checkContentPosition()
# handles drag of the vertical scroll thumb button
handleMouseMoveY: (e) ->
@handleScrollY
deltaY: 0 - (@scrollPos - e.clientY) * @numOfScreens
@scrollPos = e.clientY
handleScrollY: (e, cb) ->
{scrollY, maxY} = @state
{deltaY} = e
newState = {}
if deltaY isnt 0
y = scrollY - deltaY
newY = if y < 0 then y else 0
newY = if newY > maxY then newY else maxY
newState.scrollY = newY
@setState newState, -> cb?()
handleTrackClick: (e) ->
{scrollY, maxY} = @state
{clientY} = e
deltaY = (->
if clientY - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT > @height + @translate then @height
else if clientY - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT < @translate then -@height
else null
)()
unless deltaY? then return
@handleScrollY
deltaY: deltaY * 2
@scrollPos = clientY
buildScrollBar: (props = {}) ->
{className, key} = props
{scrollY, maxY, contentAreaHeight} = @state
finalClass = "thumb-btn-vertical-track"
finalClass += " #{className}" if className?
# Height of the scroll track
availableHeight = contentAreaHeight
# Number of screen heights in the content area
@numOfScreens = (-maxY + contentAreaHeight) / availableHeight if availableHeight
visibleClass = if @numOfScreens <= 1 then ' is-hidden' else ''
# Thumb button height
@height = availableHeight / @numOfScreens
# 25px is the smallest
if @height < SCROLL_THUMB_MIN then @height = SCROLL_THUMB_MIN
# Empty space left in the track when the thumb button is in it
availableTrackSpace = availableHeight - @height
# The percent the content is currently scrolled
scrollPercent = scrollY / maxY
# How far down to move the scrollBar
@translate = scrollPercent * availableTrackSpace
div {
key: key or 'vScroll'
className: finalClass
onClick: @handleTrackClick
},
button {
key: 'thumb-btn-vertical'
ref: 'scrollButton'
className: 'thumb-btn-vertical' + visibleClass
type: 'button'
style:
transform: "translateY(#{@translate}px)"
WebkitTransform: "translateY(#{@translate}px)"
msTransform: "translateY(#{@translate}px)"
top: HEADER_HEIGHT + 1
height: @height
onMouseDown: @clickScroll
}