weex-nuke
Version:
基于 Rax 、Weex 的高性能组件体系 ~~
642 lines (520 loc) • 18.2 kB
JavaScript
'use strict';
/** @jsx createElement */
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _class, _temp;
exports.labelMap = labelMap;
exports.valueMap = valueMap;
exports.keyMap = keyMap;
var _rax = require('rax');
var _nukeView = require('../../View/index.js');
var _nukeView2 = _interopRequireDefault(_nukeView);
var _nukeText = require('../../Text/index.js');
var _nukeText2 = _interopRequireDefault(_nukeText);
var _item = require('./item.js');
var _item2 = _interopRequireDefault(_item);
var _nukeThemeProvider = require('../../ThemeProvider/index.js');
var _nukeThemeProvider2 = _interopRequireDefault(_nukeThemeProvider);
var _styles = require('../styles/index.js');
var _styles2 = _interopRequireDefault(_styles);
var _animate = require('./animate.js');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var connectStyle = _nukeThemeProvider2.default.connectStyle;
var noop = function noop() {};
var VISIBLE_ITEM_COUNT = 5;
var MAX_TOUCH_PATH_LENGTH = 20;
var VELOCITY_DETERMINE_PERIOD = 100;
var MILLISECONDS = 1000;
var MIN_VELOCITY = 50;
var VELOCITY_DECREASE_RATE = 0.9;
var BOUNCE_RESISTANCE = 0.5;
var BOUNCE_RESISTANCE_DISTANCE = 200; // px
function getComputedStyleHeight(el) {
if (typeof window === 'undefined') {
return 0;
}
return parseFloat(window.getComputedStyle(el).height);
}
var PickerColumn = (_temp = _class = function (_Component) {
_inherits(PickerColumn, _Component);
function PickerColumn(props, context) {
_classCallCheck(this, PickerColumn);
var _this = _possibleConstructorReturn(this, (PickerColumn.__proto__ || Object.getPrototypeOf(PickerColumn)).call(this, props));
var value = void 0;
if (context.__picker__) {
value = context.selectedValue[props.index];
} else if ('value' in props) {
value = props.value;
} else {
value = props.selectedKey;
}
_this.state = {
value: value
};
_this.bound = null;
_this.isTracking = false;
_this.isDragging = false;
_this.currentScrollTop = 0;
_this.touchPath = []; // use to calculate move velocity
['onChange', 'swipeStart', 'swipeMove', 'swipeEnd'].forEach(function (m) {
_this[m] = _this[m].bind(_this);
});
return _this;
}
_createClass(PickerColumn, [{
key: 'getChildContext',
value: function getChildContext() {
return {
__column__: true,
selectedValue: this.state.value
};
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps, nextContext) {
if (nextContext.__picker__) {
this.setState({
value: nextContext.selectedValue[nextProps.index]
});
} else if ('value' in nextProps) {
this.setState({
value: nextProps.value
});
}
}
}, {
key: 'componentWillUpdate',
value: function componentWillUpdate(nextProps, nextState) {
if (nextProps.dataSource !== this.props.dataSource || JSON.stringify(nextProps.dataSource) !== JSON.stringify(this.props.dataSource)) {
this.changed = true;
}
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
this.componentDidUpdate();
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate() {
// calculate scroll content bound
// inMatrix arg for config platform
if (!this.bound || this.changed) {
this.calculateBound();
}
// set scroll offset according to default value
this.setScroll(this.state.value);
this.changed = false;
}
}, {
key: 'calculateBound',
value: function calculateBound(props) {
props = props || this.props;
var indicator = this.refs.indicator;
var _props = props,
children = _props.children,
dataSource = _props.dataSource;
var top = void 0;var bottom = void 0;var itemHeight = void 0;var length = void 0;
var halfOfVisibleCount = Math.floor(VISIBLE_ITEM_COUNT / 2);
itemHeight = getComputedStyleHeight(indicator);
length = dataSource.length;
bottom = itemHeight * halfOfVisibleCount;
top = -itemHeight * (length - (VISIBLE_ITEM_COUNT - halfOfVisibleCount));
this.bound = {
top: top,
bottom: bottom,
itemHeight: itemHeight
};
}
}, {
key: 'setScroll',
value: function setScroll(value) {
var _bound = this.bound,
itemHeight = _bound.itemHeight,
bottom = _bound.bottom;
var _props2 = this.props,
valueMap = _props2.valueMap,
dataSource = _props2.dataSource,
children = _props2.children,
keyMap = _props2.keyMap;
var index = 0;
var _children = void 0;
var item = void 0;
if (typeof value === 'undefined') {
value = dataSource[0].key;
}
for (var i = 0, l = dataSource.length; i < l; i++) {
if (keyMap(dataSource[i]).toString() === value.toString()) {
index = i;
item = dataSource[i];
break;
}
}
if (this.context.__picker__) {
this.context.onUpdate(item, this.props.index);
}
this.updateOffset(bottom - itemHeight * index);
}
}, {
key: 'startAnimate',
value: function startAnimate(stepFunc, callback, duration, easeFunc) {
this.stopAnimate();
return this.animateId = _animate.Animate.start(stepFunc, callback, duration, easeFunc);
}
}, {
key: 'stopAnimate',
value: function stopAnimate() {
if (this.animateId) {
_animate.Animate.stop(this.animateId);
this.animateId = null;
}
}
}, {
key: 'isInBound',
value: function isInBound(scrollTop) {
var _bound2 = this.bound,
top = _bound2.top,
bottom = _bound2.bottom;
if (scrollTop === undefined) {
scrollTop = this.currentScrollTop;
}
return !(scrollTop > bottom || scrollTop < top);
}
}, {
key: 'getBouncedDiff',
value: function getBouncedDiff(scrollTop) {
var _bound3 = this.bound,
top = _bound3.top,
bottom = _bound3.bottom;
if (!this.isInBound(scrollTop)) {
if (scrollTop > bottom) {
return scrollTop - bottom;
}
if (scrollTop < top) {
return scrollTop - top;
}
}
return 0;
}
}, {
key: 'setTransformStyle',
value: function setTransformStyle(offset) {
var content = this.refs.content;
if (content) {
var style = 'translate(0, ' + offset + 'px)';
// debugger; //bug fix: avoid translate3d, bug in android 4.3
content.style.WebkitTransform = style;
content.style.transform = style;
}
}
}, {
key: 'setOffset',
value: function setOffset(top) {
this.currentScrollTop = top;
this.setTransformStyle(top);
}
}, {
key: 'updateOffset',
value: function updateOffset(top, animation, callback, easeFunc) {
var _this2 = this;
var lastScrollTop = void 0;var diff = void 0;
if (animation) {
lastScrollTop = this.currentScrollTop, diff = top - lastScrollTop;
this.startAnimate(function (percent) {
_this2.setOffset(lastScrollTop + diff * percent);
}, function () {
_this2.setOffset(top);
callback && callback();
}, 200, easeFunc);
} else {
this.setOffset(top);
callback && callback();
}
}
}, {
key: 'restrictScrollBound',
value: function restrictScrollBound(scrollTop) {
var _bound4 = this.bound,
top = _bound4.top,
bottom = _bound4.bottom;
var max = Math.max,
min = Math.min;
return min(max(top, scrollTop), bottom);
}
// when touch move end or animation end,
// adjust scrollTop value to stop by nearest picker item
}, {
key: 'stopBy',
value: function stopBy() {
var _this3 = this;
var abs = Math.abs;
// debugger;
var current = this.currentScrollTop;
var itemHeight = this.bound.itemHeight;
var remainder = current % itemHeight;
var multiple = parseInt(current / itemHeight, 10);
var stopBy = current;
if (abs(remainder) >= itemHeight / 2) {
// reserve the sign that represent scroll direction
stopBy = remainder / abs(remainder) * (abs(multiple) + 1) * itemHeight;
} else {
stopBy = multiple * itemHeight;
}
stopBy = this.restrictScrollBound(stopBy);
this.updateOffset(stopBy, false, function () {
_this3.onChange();
}, _animate.easeInOutQuad);
}
}, {
key: 'swipeStart',
value: function swipeStart(e) {
e.preventDefault();
var pageY = e.touches ? e.touches[0].pageY : e.clientY;
this.isTracking = true;
this.isDragging = false;
this.touchPageY = pageY;
this.lastScrollTop = this.currentScrollTop;
this.touchPath = [{
time: Date.now(),
pageY: pageY
}];
this.stopAnimate();
}
}, {
key: 'swipeMove',
value: function swipeMove(e) {
e.preventDefault();
if (!this.isTracking) {
return;
}
this.isDragging = true;
var abs = Math.abs,
max = Math.max,
min = Math.min;
var initPageY = this.touchPageY;
var currentPageY = e.touches ? e.touches[0].pageY : e.clientY;
var diff = currentPageY - initPageY;
var currentScrollTop = this.lastScrollTop + diff;
// bounce
if (!this.isInBound(currentScrollTop)) {
var bound = void 0;var bounce = void 0;
// bounced diff
diff = this.getBouncedDiff(currentScrollTop);
bound = currentScrollTop - diff;
// apply bounce resistance
diff = min(max(-BOUNCE_RESISTANCE_DISTANCE, diff), BOUNCE_RESISTANCE_DISTANCE);
bounce = diff * (1 - abs(diff) / (BOUNCE_RESISTANCE_DISTANCE * 2));
currentScrollTop = bound + bounce;
}
this.updateOffset(currentScrollTop);
// record move path
this.touchPath.push({
time: Date.now(),
pageY: currentPageY
});
// restrict length
if (this.touchPath.length > MAX_TOUCH_PATH_LENGTH) {
this.touchPath = this.touchPath.slice(MAX_TOUCH_PATH_LENGTH / 2);
}
}
}, {
key: 'swipeEnd',
value: function swipeEnd(e) {
var _this4 = this;
e.preventDefault();
// Ignore event when tracking is not enabled (event might be outside of element)
if (!this.isDragging) {
return;
}
var abs = Math.abs;
// velocity calculation
var lastTouchTime = Date.now();
var startPos = this.touchPath.length - 1;
var endPos = startPos;
var timeDiff = 0;
var velocity = 0;
for (var i = startPos - 1; i >= 0 && timeDiff < VELOCITY_DETERMINE_PERIOD; i--) {
var position = this.touchPath[i];
timeDiff = lastTouchTime - position.time;
endPos = i;
}
if (startPos !== endPos) {
var firstPageY = this.touchPath[endPos].pageY;
var lastPageY = this.touchPath[startPos].pageY;
velocity = (lastPageY - firstPageY) / timeDiff * MILLISECONDS;
}
if (abs(velocity) > MIN_VELOCITY) {
var animateId = this.startAnimate(function (percent, timeDiff, timeFrame) {
var currentScrollTop = _this4.currentScrollTop;
velocity *= VELOCITY_DECREASE_RATE;
// bounce
if (!_this4.isInBound(currentScrollTop)) {
velocity *= BOUNCE_RESISTANCE;
}
var diff = velocity * timeFrame / MILLISECONDS;
_this4.setOffset(currentScrollTop + diff);
if (abs(velocity) < MIN_VELOCITY) {
_animate.Animate.stop(animateId);
_this4.stopBy();
}
});
} else {
this.stopBy();
}
this.isTracking = false;
this.isDragging = false;
}
}, {
key: 'setValue',
value: function setValue(value) {
if (!('value' in this.props)) {
this.setState({
value: value
});
}
}
}, {
key: 'getValue',
value: function getValue() {
return this.state.value;
}
}, {
key: 'onChange',
value: function onChange(e) {
var scrollTop = this.currentScrollTop;
var _bound5 = this.bound,
bottom = _bound5.bottom,
itemHeight = _bound5.itemHeight;
var itemIndex = Math.round((bottom - scrollTop) / itemHeight);
var _props3 = this.props,
index = _props3.index,
children = _props3.children,
dataSource = _props3.dataSource,
valueMap = _props3.valueMap,
keyMap = _props3.keyMap;
var value = void 0;var item = void 0;
if (children) {
item = this.props.children[itemIndex];
value = item.props.value;
} else {
item = dataSource[itemIndex];
value = keyMap(item);
}
if (value === this.state.value) {
return;
}
if (this.context.__picker__) {
this.context.onChange(value, item, index);
} else {
this.setValue(value);
this.props.onChange(value, item);
}
}
}, {
key: 'render',
value: function render() {
var value = this.state.value;
var styles = this.props.themeStyle;
var _props4 = this.props,
dataSource = _props4.dataSource,
labelMap = _props4.labelMap,
valueMap = _props4.valueMap,
className = _props4.className,
_props4$style = _props4.style,
style = _props4$style === undefined ? {} : _props4$style,
_props4$prefix = _props4.prefix,
prefix = _props4$prefix === undefined ? this.defaultPrefix : _props4$prefix;
var children = this.props.children;
if (!children) {
children = dataSource.map(function (item, index) {
return (0, _rax.createElement)(
_item2.default,
{ key: keyMap(item), value: valueMap(item) },
valueMap(item)
);
});
}
return (0, _rax.createElement)(
'div',
{
x: 'column-item',
style: [styles['picker-column'], style],
onMouseDown: this.swipeStart,
onMouseMove: this.swipeMove,
onMouseUp: this.swipeEnd,
onMouseLeave: this.swipeEnd,
onTouchStart: this.swipeStart,
onTouchMove: this.swipeMove,
onTouchEnd: this.swipeEnd,
onTouchCancel: this.swipeEnd
},
(0, _rax.createElement)(
'div',
{
ref: 'content',
x: 'column-item-scroll',
style: styles['picker-column-scroll']
},
children
),
(0, _rax.createElement)('div', { x: 'column-item-mask', style: styles['picker-column-mask'] }),
(0, _rax.createElement)('div', {
x: 'column-item-indicator',
ref: 'indicator',
style: styles['picker-column-indicator']
})
);
}
}]);
return PickerColumn;
}(_rax.Component), _class.propTypes = {
value: _rax.PropTypes.any,
dataSource: _rax.PropTypes.array,
onChange: _rax.PropTypes.func,
labelMap: _rax.PropTypes.func,
valueMap: _rax.PropTypes.func
}, _class.defaultProps = {
selectedKey: null,
dataSource: [],
onChange: noop,
labelMap: labelMap,
valueMap: valueMap
}, _class.contextTypes = {
onUpdate: _rax.PropTypes.func,
onChange: _rax.PropTypes.func,
__picker__: _rax.PropTypes.bool,
selectedValue: _rax.PropTypes.any
}, _class.childContextTypes = {
__column__: _rax.PropTypes.bool,
selectedValue: _rax.PropTypes.any
}, _temp);
PickerColumn.displayName = 'Picker';
var StyledPickerColumn = connectStyle(_styles2.default)(PickerColumn);
exports.default = StyledPickerColumn;
// for label map
function labelMap(item) {
if ((typeof item === 'undefined' ? 'undefined' : _typeof(item)) === 'object') {
return item.label;
}
return item;
}
// for value map
function valueMap(item) {
if ((typeof item === 'undefined' ? 'undefined' : _typeof(item)) === 'object') {
return item.value;
}
return item;
}
// for key map
function keyMap(item) {
if ((typeof item === 'undefined' ? 'undefined' : _typeof(item)) === 'object') {
return item.key;
}
return item;
}