principles-ui-components
Version:
Supporting UI controller for Tizen TV web application, which developed base on React Framework.
462 lines (394 loc) • 16.9 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { is, Map, fromJS } from 'immutable';
import { KEY, VOICE_GUIDE_DELAY, AnimationEffect, TTSTYPE } from './common/CommonDefine';
import ScrollBar from './ScrollBar';
import ImageItem from './ImageItem';
import ScrollText from './common/ScrollText';
import TTS from './common/TTS';
const SCROLL_BAR_WIDTH = 3;
const SCROLL_BAR_TYPE = 'v';
const Offset = {
NONE: 0,
DOWN: 1,
UP: 2,
};
export default class VMultiLineList extends Component {
constructor(props) {
super(props);
this.state = {
focusedIndex: 0, // index of focused item, begin with 0
scrollTranslate: 0, // translate Y for the grid
scrollPercent: 0, // the scroll percent for scroll bar
enter: false, // do enter animation or not
hasFocus: false,
};
this.enterCB = this.enterCB.bind(this);
this.focusedIdx = { index: 0, row: 0, column: 0 }; // the focused item's index and coordinates(i, j)
this.scrollTop = 0; // scrollTop for translate Y
this.focusChangeObject = { voiceGuideText: 'focus changed' };
this.offsetAction = Offset.NONE;
this.dataOffset = 0;
}
componentWillMount() { // when this component getting new props. Even though you call here, setState, Render is not triggered !!
this.setInfo(this.props);
}
componentDidMount() {
}
componentWillReceiveProps(nextProps) {
this.setInfo(nextProps);
}
shouldComponentUpdate(nextProps, nextState) {
return (JSON.stringify(nextProps) !== JSON.stringify(this.props)) || (JSON.stringify(nextState) !== JSON.stringify(this.state));
}
componentDidUpdate(prevProps, prevState) {
// console.error(`componentDidUpdate ${this.listnode.offsetHeight}`);
}
componentWillUnmount() {
}
setInfo(props) {
this.itemW = props.OSD[0].layout.w; // list item width
this.itemH = props.OSD[0].layout.h; // list item height
this.displayRow = Math.ceil(
(props.layout.h - props.list.t) / (this.itemH + props.list.vSpacing));
this.listLength = props.OSD.length; // list data length
this.listRow = Math.ceil(this.listLength / props.countPerRow); // list row number
this.listH = (props.list.t * 2) + (this.itemH * this.listRow)
+ ((this.listRow - 1) * props.list.vSpacing); // list area height with top and bottom gap
this.listW = (props.list.l * 2) + (this.itemW * props.countPerRow)
+ ((props.countPerRow - 1) * props.list.hSpacing); // list area width with left and right gap
this.sequence = [];
const dataBuffer = (props.buffer * 2) + this.displayRow;
for (let idx = 0; idx < dataBuffer; idx++) {
this.sequence.push(idx);
}
}
/*
* function: getDyadicArray
* array: [A,B,..,C,D,..]
* part: count per row
* return: [A,B,..][C,D,..]
*/
getDyadicArray(array, part, offset, buffer) {
const dyadicArray = [];
for (let i = offset * part; i < array.length && i < (offset + buffer) * part; i += part) {
dyadicArray.push(array.slice(i, i + part));
}
// console.log(`[GridList.js::getDyadicArray()]array.length:${array.length}, dyadicArray:${dyadicArray.length}`);
return dyadicArray;
}
getElementPos(elem) {
let x = 0;
let y = 0;
let mElem = elem;
while (mElem != null) {
x += mElem.offsetLeft;
y += mElem.offsetTop;
mElem = mElem.offsetParent;
}
return { x, y };
}
/*
*function: setScrollTop
*description: calculate scrolltop value
*/
setScrollTop() {
const { layout, list } = this.props;
const focusItemY = list.t + (this.focusedIdx.row * (list.vSpacing + this.itemH));
if (focusItemY + this.itemH + this.scrollTop > layout.h - list.t) {
this.scrollTop = (-focusItemY) + (layout.h - this.itemH - list.t);
} else if (focusItemY + this.scrollTop < list.t) {
this.scrollTop = (-focusItemY) + list.t;
}
}
setDataOffset() {
const { layout, list, buffer } = this.props;
if (this.listRow - ((buffer * 2) + this.displayRow) <= 0) {
this.dataOffset = 0;
this.offsetAction = Offset.NONE;
console.log(`data length [${this.listRow}] short. buffer[${buffer}],displayRow[${this.displayRow}]`);
return;
}
const focusItemY = list.t + (this.focusedIdx.row * (list.vSpacing + this.itemH));
if (focusItemY + this.scrollTop > layout.h) {
this.offsetAction = Offset.DOWN;
this.dataOffset = (this.focusedIdx.row + 1) - (buffer + this.displayRow);
// console.error(`offsetAction ${this.offsetAction}, dataOffset ${this.dataOffset}`);
if (this.dataOffset === 0) { // start, '==', bodrder, reset offsetaction
this.offsetAction = Offset.NONE;
}
} else if (focusItemY + this.itemH + this.scrollTop < 0) {
this.offsetAction = Offset.UP;
this.dataOffset = this.focusedIdx.row - buffer;
// console.error(`offsetAction ${this.offsetAction}, dataOffset ${this.dataOffset}`);
if (this.dataOffset === this.listRow - ((buffer * 2) + this.displayRow)) {
this.offsetAction = Offset.NONE;
}
} else {
this.offsetAction = Offset.NONE;
return;
}
if (this.dataOffset < 0) { // start
this.dataOffset = 0;
this.offsetAction = Offset.NONE;
} else if (this.dataOffset > this.listRow - ((buffer * 2) + this.displayRow)) { // end, '==', bodrder, reset offsetaction
this.dataOffset = this.listRow - ((buffer * 2) + this.displayRow);
this.offsetAction = Offset.NONE;
}
if (this.offsetAction === Offset.DOWN) {
this.sequence = [this.sequence[this.sequence.length - 1], ...this.sequence.slice(0, this.sequence.length - 1)];
} else if (this.offsetAction === Offset.UP) {
this.sequence = [...this.sequence.slice(1, this.sequence.length), this.sequence[0]];
}
// console.log(`offsetAction ${this.offsetAction}, dataOffset ${this.dataOffset}`);
}
playTTS(type) {
if (this.props.ttsEnable) {
console.log(`VMultiline list play TTS, type[${type}], index[${this.focusedIdx.index}]`);
if (type === TTSTYPE.LIST) {
this.TTSnode.playTTS();
} else if (type === TTSTYPE.ITEM) {
if (this[`item${this.focusedIdx.index}`]) {
this[`item${this.focusedIdx.index}`].playTTS();
}
}
}
}
keyFocusIn() {
if (this.state.hasFocus === true) {
console.log('list had focus, do nothing!');
return;
}
this.playTTS(TTSTYPE.LIST);
if (this.props.onGridFocus && typeof this.props.onGridFocus === 'function') {
this.props.onGridFocus();
}
this.setState({ hasFocus: true, enter: false });
}
keyFocusOut() {
if (this.state.hasFocus === false) {
console.log('list had lose focus, do nothing!');
return;
}
this.setState({ hasFocus: false, enter: false });
}
handleKey(event) {
if (this.state.hasFocus === false) {
console.log('list have no focus, do nothing!');
return false;
}
const preFocus = this.focusedIdx.index;
const preRow = this.focusedIdx.row;
const { countPerRow } = this.props;
let isEnter = false;
switch (event.keyCode) {
case KEY.LEFT:
if (this.focusedIdx.column === 0) {
// block
} else {
this.focusedIdx.index -= 1;
this.focusedIdx.column -= 1;
}
break;
case KEY.RIGHT:
if (this.focusedIdx.column === countPerRow - 1) {
// block
} else if (this.focusedIdx.index === this.listLength - 1) {
if (this.focusedIdx.row === 0) {
break;
}
this.focusedIdx.row -= 1;
this.focusedIdx.column = countPerRow - 1;
this.focusedIdx.index = (this.focusedIdx.row * countPerRow) + this.focusedIdx.column;
} else {
this.focusedIdx.index += 1;
this.focusedIdx.column += 1;
}
break;
case KEY.UP:
if (this.focusedIdx.row === 0) {
// block
} else {
this.focusedIdx.row -= 1;
this.focusedIdx.index -= countPerRow;
}
break;
case KEY.DOWN:
if (this.focusedIdx.row === (this.listRow - 1)) {
// block
} else {
this.focusedIdx.row += 1;
if ((this.focusedIdx.index + countPerRow) > (this.listLength - 1)) {
this.focusedIdx.index = this.listLength - 1;
this.focusedIdx.column = (this.listLength % countPerRow) - 1;
} else {
this.focusedIdx.index += countPerRow;
}
}
break;
case KEY.ENTER:
isEnter = true;
this.setState({ enter: isEnter });
break;
default:
return false; // false: pass event to base component, true: not pass
// break;
}
// console.log(`[GridList.js::handleKey()] ${event.keyCode} ${JSON.stringify(this.focusedIdx)}`);
if (this.focusedIdx.index !== preFocus) {
this.playTTS(TTSTYPE.ITEM);
if (this.props.onFocusChanged && typeof this.props.onFocusChanged === 'function') {
this.focusChangeObject = this.props.onFocusChanged(preFocus, this.focusedIdx.index);
}
if (this.focusedIdx.row !== preRow) {
this.setDataOffset();
this.setScrollTop();
}
this.setState({
focusedIndex: this.focusedIdx.index,
scrollTranslate: this.scrollTop,
scrollPercent: (-this.scrollTop) / (this.listH - this.props.layout.h),
});
}
return true;
}
enterCB() {
if (this.props.onItemPress && typeof this.props.onItemPress === 'function') {
this.props.onItemPress(this.focusedIdx.index);
}
this.setState({ enter: false });
}
render() {
const { OSD, layout, list, scrollbar, scrollBarEnable, countPerRow,
enlarge, highContrast, ttsEnable, ttsText, buffer } = this.props;
const layoutStyle = {
position: 'absolute',
left: layout.l,
top: layout.t,
width: layout.w,
height: layout.h,
overflow: 'hidden',
};
const listStyle = {
position: 'absolute',
left: list.l - (list.hSpacing / 2),
top: list.t - (list.vSpacing / 2),
width: this.listW,
height: this.listH,
transform: `translateY(${this.state.scrollTranslate}px) translateZ(100px)`,
transition: 'all 0.3s',
};
let scrollbarComponent = null;
if (scrollBarEnable && (this.listH > layout.h)) {
scrollbarComponent = (<ScrollBar
left={scrollbar.l}
top={scrollbar.t}
barType={SCROLL_BAR_TYPE}
bgWidth={SCROLL_BAR_WIDTH}
bgHeight={layout.h - (scrollbar.t * 2)}
barPercent={layout.h / this.listH}
scrollPercent={this.state.scrollPercent}
highContarstValue={highContrast}
/>);
}
let TTSComponent = null;
if (ttsEnable) {
TTSComponent = <TTS ref={(TTSnode) => { this.TTSnode = TTSnode; }} ttsEnable={ttsEnable} ttsText={ttsText} />;
}
const dyadicArray = this.getDyadicArray(OSD, countPerRow, this.dataOffset, (buffer * 2) + this.displayRow);
const rowLength = dyadicArray.length;
const listComponent = [];
for (let idxR = 0; idxR < rowLength; idxR++) {
const rowComponent = [];
const columnLength = dyadicArray[this.sequence[idxR]].length;
for (let idxC = 0; idxC < columnLength; idxC++) {
const itemStyle = {
position: 'absolute',
padding: `${list.vSpacing / 2}px ${list.hSpacing / 2}px`,
width: this.itemW + list.hSpacing,
height: this.itemH + list.vSpacing,
left: idxC * (this.itemW + list.hSpacing),
top: (this.sequence[idxR] + this.dataOffset) * (this.itemH + list.vSpacing),
};
// console.error(`${this.sequence[idxR] + this.dataOffset}`);
const itemIdx = ((this.sequence[idxR] + this.dataOffset) * countPerRow) + idxC;
const focus = (this.state.hasFocus && itemIdx === this.state.focusedIndex);
rowComponent.push(<div key={idxC} style={itemStyle}>
<ImageItem
OSD={dyadicArray[this.sequence[idxR]][idxC]}
ref={(node) => { this[`item${itemIdx}`] = node; }}
focus={focus}
enterCB={this.enterCB}
enter={this.state.enter && focus}
enlarge={enlarge}
highContrast={highContrast}
ttsEnable={ttsEnable}
/>
</div>);
}
listComponent.push(<div key={idxR} style={{}}>{rowComponent}</div>);
}
return (<div style={layoutStyle}>
{scrollbarComponent}
<div style={listStyle} ref={(listnode) => { this.listnode = listnode; }}>
{listComponent}
</div>
{TTSComponent}
</div>);
}
}
VMultiLineList.defaultProps = {
scrollBarEnable: true,
enlarge: false,
highContrast: false,
ttsEnable: false,
ttsText: '',
buffer: 1,
list: {
l: 80,
t: 100,
hSpacing: 80, // left and right side
vSpacing: 145, // spacing between rows
},
title: undefined,
scrollbar: {
l: 30,
t: 1,
},
onItemPress: undefined, // function (wz, itemData, data)
// onEnterKeyLongPressed:undefined // function (wzFocused, itemData, type)
onGridFocus: undefined, // function (wzFocused,itemIndex,groupIndex)
// onGridBlur:undefined // function (wzFocused)
onFocusChanged: undefined, // function (fromItemIndex, toItemIndex)
};
VMultiLineList.propTypes = {
OSD: PropTypes.array.isRequired, // array for imageitem osd,, element type checkeing at image item component
countPerRow: PropTypes.number.isRequired,
buffer: PropTypes.number,
scrollBarEnable: PropTypes.bool,
enlarge: PropTypes.bool,
highContrast: PropTypes.bool,
ttsEnable: PropTypes.bool,
ttsText: PropTypes.string,
layout: PropTypes.shape({
l: PropTypes.number.isRequired,
t: PropTypes.number.isRequired,
w: PropTypes.number.isRequired,
h: PropTypes.number.isRequired,
}).isRequired,
list: PropTypes.shape({
l: PropTypes.number,
t: PropTypes.number,
hSpacing: PropTypes.number,
vSpacing: PropTypes.number,
}),
scrollbar: PropTypes.shape({
l: PropTypes.number,
t: PropTypes.number,
}),
onItemPress: PropTypes.func,
// onEnterKeyLongPressed: PropTypes.func,
onGridFocus: PropTypes.func,
// onGridBlur: PropTypes.func,
onFocusChanged: PropTypes.func,
};