UNPKG

ldx-widgets

Version:

widgets

299 lines (224 loc) 9.31 kB
### 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 }