ldx-widgets
Version:
widgets
343 lines (321 loc) • 12.3 kB
JavaScript
/*
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
*/
(function() {
var Animation, DOWN_ARROW, END, HEADER_HEIGHT, HOME, PAGE_DOWN, PAGE_UP, React, SCROLL_THUMB_MIN, Spinner, UP_ARROW, button, div, easing, li, ref, ref1;
React = require('react');
Animation = require('ainojs-animation');
easing = require('ainojs-easing');
Spinner = React.createFactory(require('../components/spinner'));
ref = require('../constants/keyboard'), HOME = ref.HOME, END = ref.END, PAGE_UP = ref.PAGE_UP, PAGE_DOWN = ref.PAGE_DOWN, UP_ARROW = ref.UP_ARROW, DOWN_ARROW = ref.DOWN_ARROW;
ref1 = require('react-dom-factories'), div = ref1.div, button = ref1.button, li = ref1.li;
SCROLL_THUMB_MIN = 25;
HEADER_HEIGHT = 0;
module.exports = {
numOfScreens: 1,
translate: 0,
height: 0,
getInitialState: function() {
return {
scrollY: 0,
contentAreaHeight: 0,
maxY: 0,
noResults: false,
recordsExhausted: false
};
},
componentWillReceiveProps: function(nextProps) {
if (!nextProps.records.length) {
return this.setState({
noResults: true
});
}
},
getContentAreaHeight: function() {
return window.innerHeight - this.SPACE_ABOVE_CONTENT - this.SPACE_BELOW_CONTENT;
},
calculateContentAreaHeight: function() {
return this.setState({
contentAreaHeight: this.getContentAreaHeight()
});
},
calculateMaxY: function(visibleRowCount, recordsExhausted) {
var buffer, contentAreaHeight, contentHeight;
if (recordsExhausted == null) {
recordsExhausted = false;
}
if (visibleRowCount != null) {
buffer = recordsExhausted ? 0 : this.ROW_HEIGHT;
contentHeight = (visibleRowCount * this.ROW_HEIGHT) + buffer;
}
contentAreaHeight = this.getContentAreaHeight();
if (contentHeight < contentAreaHeight) {
return null;
} else {
return (0 - contentHeight) + contentAreaHeight;
}
},
componentWillMount: function() {
if (this.ROW_HEIGHT == null) {
this.ROW_HEIGHT = 30;
}
if (this.SPACE_ABOVE_CONTENT == null) {
this.SPACE_ABOVE_CONTENT = 68;
}
if (this.SPACE_BELOW_CONTENT == null) {
this.SPACE_BELOW_CONTENT = 0;
}
return this.oldLength = null;
},
componentDidMount: function() {
this.calculateContentAreaHeight();
window.addEventListener('resize', this.calculateContentAreaHeight);
return window.addEventListener('keydown', this.handleScrollKeys);
},
componentWillUnmount: function() {
var ref2;
window.removeEventListener('resize', this.calculateContentAreaHeight);
window.removeEventListener('keydown', this.handleScrollKeys);
if ((ref2 = this.animation) != null ? ref2.isAnimating() : void 0) {
return this.animation.end();
}
},
checkContentPosition: function(e) {
var maxY, newLength, records, recordsExhausted, ref2, scrollY;
if (!this.props.records.length) {
return;
}
if (e != null) {
this.handleScrollY(e);
}
ref2 = this.state, scrollY = ref2.scrollY, maxY = ref2.maxY, recordsExhausted = ref2.recordsExhausted;
records = this.props.records;
if (scrollY - maxY <= 250) {
newLength = records.length;
if (newLength !== this.oldLength && !recordsExhausted) {
this.oldLength = newLength;
return this.props.loadNextRecordset();
}
}
},
doneWithFetch: function(options) {
var newSearch, recordsExhausted, state;
recordsExhausted = options.recordsExhausted, newSearch = options.newSearch;
state = {
maxY: this.calculateMaxY(this.props.records.length, recordsExhausted),
recordsExhausted: recordsExhausted
};
if (newSearch) {
state.scrollY = 0;
this.oldLength = null;
}
return this.setState(state);
},
calculateRange: function(records, buffer) {
var contentAreaHeight, end, ref2, scrollY, start;
if (buffer == null) {
buffer = 1;
}
ref2 = this.state, contentAreaHeight = ref2.contentAreaHeight, scrollY = ref2.scrollY;
start = Math.floor(-scrollY / this.ROW_HEIGHT) - buffer;
end = Math.ceil((-(scrollY - contentAreaHeight)) / this.ROW_HEIGHT) + buffer;
return {
start: start >= 0 ? start : 0,
end: end <= records.length ? end : records.length
};
},
buildLoadingRow: function(index) {
return li({
key: 'loadingRow',
className: 'grid grid-pad full result-row loading-row',
style: {
height: this.ROW_HEIGHT,
top: this.ROW_HEIGHT * index
}
}, Spinner({
length: 5
}));
},
handleScrollKeys: function(e) {
var ctrlKey, keyCode, metaKey;
keyCode = e.keyCode, metaKey = e.metaKey, ctrlKey = e.ctrlKey;
if (keyCode !== HOME && keyCode !== END && keyCode !== PAGE_UP && keyCode !== PAGE_DOWN && keyCode !== UP_ARROW && keyCode !== DOWN_ARROW) {
return;
}
switch (keyCode) {
case HOME:
return this.animateScrollTo(0);
case END:
return this.animateScrollTo(this.state.maxY);
case PAGE_UP:
return this.page(1);
case PAGE_DOWN:
return this.page(-1);
case UP_ARROW:
if (metaKey || ctrlKey) {
return this.page(1);
} else {
return this.handleScrollY({
deltaY: -this.ROW_HEIGHT
}, this.checkContentPosition);
}
break;
case DOWN_ARROW:
if (metaKey || ctrlKey) {
return this.page(-1);
} else {
return this.handleScrollY({
deltaY: this.ROW_HEIGHT
}, this.checkContentPosition);
}
}
},
page: function(direction) {
var maxY, newScrollPos, pageDistance, ref2, scrollY;
ref2 = this.state, scrollY = ref2.scrollY, maxY = ref2.maxY;
pageDistance = window.innerHeight - this.SPACE_ABOVE_CONTENT - this.SPACE_BELOW_CONTENT;
newScrollPos = scrollY + (pageDistance * direction);
newScrollPos = newScrollPos > 0 ? 0 : newScrollPos;
newScrollPos = newScrollPos < maxY ? maxY : newScrollPos;
return this.animateScrollTo(newScrollPos);
},
animateScrollTo: function(newScrollPos) {
var ref2, scrollY;
if ((ref2 = this.animation) != null ? ref2.isAnimating() : void 0) {
this.animation.end();
}
scrollY = this.state.scrollY;
return this.animation = new Animation({
duration: 300,
easing: easing('easeOutCirc')
}).init({
scrollY: scrollY
}).on('frame', this.onFrame).on('complete', this.checkContentPosition).animateTo({
scrollY: newScrollPos
});
},
onFrame: function(e) {
return this.setState(e.values);
},
clickScroll: function(e) {
this.scrollPos = e.clientY;
document.addEventListener('mousemove', this.handleMouseMoveY, false);
return document.addEventListener('mouseup', this.handleMouseUpY, false);
},
handleMouseUpY: function(e) {
document.removeEventListener('mousemove', this.handleMouseMoveY, false);
document.removeEventListener('mouseup', this.handleMouseUpY, false);
this.handleScrollY({
deltaY: 0 - (this.scrollPos - e.clientY) * this.numOfScreens
});
return this.checkContentPosition();
},
handleMouseMoveY: function(e) {
this.handleScrollY({
deltaY: 0 - (this.scrollPos - e.clientY) * this.numOfScreens
});
return this.scrollPos = e.clientY;
},
handleScrollY: function(e, cb) {
var deltaY, maxY, newState, newY, ref2, scrollY, y;
ref2 = this.state, scrollY = ref2.scrollY, maxY = ref2.maxY;
deltaY = e.deltaY;
newState = {};
if (deltaY !== 0) {
y = scrollY - deltaY;
newY = y < 0 ? y : 0;
newY = newY > maxY ? newY : maxY;
newState.scrollY = newY;
}
return this.setState(newState, function() {
return typeof cb === "function" ? cb() : void 0;
});
},
handleTrackClick: function(e) {
var clientY, deltaY, maxY, ref2, scrollY;
ref2 = this.state, scrollY = ref2.scrollY, maxY = ref2.maxY;
clientY = e.clientY;
deltaY = (function() {
if (clientY - this.SPACE_ABOVE_CONTENT - this.SPACE_BELOW_CONTENT > this.height + this.translate) {
return this.height;
} else if (clientY - this.SPACE_ABOVE_CONTENT - this.SPACE_BELOW_CONTENT < this.translate) {
return -this.height;
} else {
return null;
}
})();
if (deltaY == null) {
return;
}
this.handleScrollY({
deltaY: deltaY * 2
});
return this.scrollPos = clientY;
},
buildScrollBar: function(props) {
var availableHeight, availableTrackSpace, className, contentAreaHeight, finalClass, key, maxY, ref2, scrollPercent, scrollY, visibleClass;
if (props == null) {
props = {};
}
className = props.className, key = props.key;
ref2 = this.state, scrollY = ref2.scrollY, maxY = ref2.maxY, contentAreaHeight = ref2.contentAreaHeight;
finalClass = "thumb-btn-vertical-track";
if (className != null) {
finalClass += " " + className;
}
availableHeight = contentAreaHeight;
if (availableHeight) {
this.numOfScreens = (-maxY + contentAreaHeight) / availableHeight;
}
visibleClass = this.numOfScreens <= 1 ? ' is-hidden' : '';
this.height = availableHeight / this.numOfScreens;
if (this.height < SCROLL_THUMB_MIN) {
this.height = SCROLL_THUMB_MIN;
}
availableTrackSpace = availableHeight - this.height;
scrollPercent = scrollY / maxY;
this.translate = scrollPercent * availableTrackSpace;
return div({
key: key || 'vScroll',
className: finalClass,
onClick: this.handleTrackClick
}, button({
key: 'thumb-btn-vertical',
ref: 'scrollButton',
className: 'thumb-btn-vertical' + visibleClass,
type: 'button',
style: {
transform: "translateY(" + this.translate + "px)",
WebkitTransform: "translateY(" + this.translate + "px)",
msTransform: "translateY(" + this.translate + "px)",
top: HEADER_HEIGHT + 1,
height: this.height
},
onMouseDown: this.clickScroll
}));
}
};
}).call(this);