web: virtual scrolling

This commit is contained in:
Maximilian Hils 2014-11-28 16:03:56 +01:00
parent 9eecc8d6e2
commit 7ca1ac0f3b
4 changed files with 190 additions and 96 deletions

View File

@ -193,18 +193,22 @@ EventEmitter.prototype.emit = function (event) {
listener.apply(this, args); listener.apply(this, args);
}.bind(this)); }.bind(this));
}; };
EventEmitter.prototype.addListener = function (event, f) { EventEmitter.prototype.addListener = function (events, f) {
events.split(" ").forEach(function (event) {
this.listeners[event] = this.listeners[event] || []; this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(f); this.listeners[event].push(f);
}.bind(this));
}; };
EventEmitter.prototype.removeListener = function (event, f) { EventEmitter.prototype.removeListener = function (events, f) {
if (!(event in this.listeners)) { if (!(events in this.listeners)) {
return false; return false;
} }
events.split(" ").forEach(function (event) {
var index = this.listeners[event].indexOf(f); var index = this.listeners[event].indexOf(f);
if (index >= 0) { if (index >= 0) {
this.listeners[event].splice(index, 1); this.listeners[event].splice(index, 1);
} }
}.bind(this));
}; };
function _SettingsStore() { function _SettingsStore() {
@ -926,11 +930,13 @@ var FlowRow = React.createClass({displayName: 'FlowRow',
)); ));
}, },
shouldComponentUpdate: function (nextProps) { shouldComponentUpdate: function (nextProps) {
var isEqual = ( return true;
this.props.columns.length === nextProps.columns.length && // Further optimization could be done here
this.props.selected === nextProps.selected && // by calling forceUpdate on flow updates, selection changes and column changes.
this.props.flow.response === nextProps.flow.response); //return (
return !isEqual; //(this.props.columns.length !== nextProps.columns.length) ||
//(this.props.selected !== nextProps.selected)
//);
} }
}); });
@ -945,30 +951,51 @@ var FlowTableHead = React.createClass({displayName: 'FlowTableHead',
} }
}); });
var FlowTableBody = React.createClass({displayName: 'FlowTableBody',
render: function () {
var rows = this.props.flows.map(function (flow) {
var selected = (flow == this.props.selected);
return React.createElement(FlowRow, {key: flow.id,
ref: flow.id,
flow: flow,
columns: this.props.columns,
selected: selected,
selectFlow: this.props.selectFlow}
);
}.bind(this));
return React.createElement("tbody", null, rows);
}
});
var ROW_HEIGHT = 32;
var FlowTable = React.createClass({displayName: 'FlowTable', var FlowTable = React.createClass({displayName: 'FlowTable',
mixins: [StickyHeadMixin, AutoScrollMixin], mixins: [StickyHeadMixin, AutoScrollMixin],
getInitialState: function () { getInitialState: function () {
return { return {
columns: all_columns columns: all_columns,
start: 0,
stop: 0
}; };
}, },
componentWillMount: function () {
if (this.props.view) {
this.props.view.addListener("add update remove recalculate", this.onChange);
}
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.view !== this.props.view) {
if (this.props.view) {
this.props.view.removeListener("add update remove recalculate");
}
nextProps.view.addListener("add update remove recalculate", this.onChange);
}
},
componentDidMount: function () {
this.onScroll();
},
onScroll: function () {
this.adjustHead();
var viewport = this.getDOMNode();
var top = viewport.scrollTop;
var height = viewport.offsetHeight;
var start = Math.floor(top / ROW_HEIGHT);
var stop = start + Math.ceil(height / ROW_HEIGHT);
this.setState({
start: start,
stop: stop
});
},
onChange: function () {
console.log("onChange");
this.forceUpdate();
},
scrollIntoView: function (flow) { scrollIntoView: function (flow) {
// Now comes the fun part: Scroll the flow into the view. // Now comes the fun part: Scroll the flow into the view.
var viewport = this.getDOMNode(); var viewport = this.getDOMNode();
@ -989,16 +1016,46 @@ var FlowTable = React.createClass({displayName: 'FlowTable',
} }
}, },
render: function () { render: function () {
var space_top = 0, space_bottom = 0, fix_nth_row = null;
var rows = [];
if (this.props.view) {
var flows = this.props.view.flows;
var max = Math.min(flows.length, this.state.stop);
console.log("render", this.props.view.flows.length, this.state.start, max - this.state.start, flows.length - this.state.stop);
for (var i = this.state.start; i < max; i++) {
var flow = flows[i];
var selected = (flow === this.props.selected);
rows.push(
React.createElement(FlowRow, {key: flow.id,
ref: flow.id,
flow: flow,
columns: this.state.columns,
selected: selected,
selectFlow: this.props.selectFlow}
)
);
}
space_top = this.state.start * ROW_HEIGHT;
space_bottom = Math.max(0, flows.length - this.state.stop) * ROW_HEIGHT;
if(this.state.start % 2 === 1){
fix_nth_row = React.createElement("tr", null);
}
}
return ( return (
React.createElement("div", {className: "flow-table", onScroll: this.adjustHead}, React.createElement("div", {className: "flow-table", onScroll: this.onScroll},
React.createElement("table", null, React.createElement("table", null,
React.createElement(FlowTableHead, {ref: "head", React.createElement(FlowTableHead, {ref: "head",
columns: this.state.columns}), columns: this.state.columns}),
React.createElement(FlowTableBody, {ref: "body", React.createElement("tbody", null,
flows: this.props.flows, React.createElement("tr", {style: {height: space_top}}),
selected: this.props.selected, fix_nth_row,
selectFlow: this.props.selectFlow, rows,
columns: this.state.columns}) React.createElement("tr", {style: {height: space_bottom}})
)
) )
) )
); );
@ -1340,26 +1397,16 @@ var MainView = React.createClass({displayName: 'MainView',
this.setState({ this.setState({
view: view view: view
}); });
view.addListener("add", this.onFlowChange);
view.addListener("update", this.onFlowChange);
view.addListener("remove", this.onFlowChange);
view.addListener("recalculate", this.onFlowChange);
}, },
closeView: function () { closeView: function () {
this.state.view.close(); this.state.view.close();
}, },
componentDidMount: function () { componentWillMount: function () {
this.openView(this.props.flowStore); this.openView(this.props.flowStore);
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
this.closeView(); this.closeView();
}, },
onFlowChange: function () {
console.warn("onFlowChange is deprecated");
this.setState({
flows: this.state.view.flows
});
},
selectFlow: function (flow) { selectFlow: function (flow) {
if (flow) { if (flow) {
this.replaceWith( this.replaceWith(
@ -1455,7 +1502,7 @@ var MainView = React.createClass({displayName: 'MainView',
return ( return (
React.createElement("div", {className: "main-view", onKeyDown: this.onKeyDown, tabIndex: "0"}, React.createElement("div", {className: "main-view", onKeyDown: this.onKeyDown, tabIndex: "0"},
React.createElement(FlowTable, {ref: "flowTable", React.createElement(FlowTable, {ref: "flowTable",
flows: this.state.view ? this.state.view.flows : [], view: this.state.view,
selectFlow: this.selectFlow, selectFlow: this.selectFlow,
selected: selected}), selected: selected}),
details ? React.createElement(Splitter, null) : null, details ? React.createElement(Splitter, null) : null,

View File

@ -14,11 +14,13 @@ var FlowRow = React.createClass({
</tr>); </tr>);
}, },
shouldComponentUpdate: function (nextProps) { shouldComponentUpdate: function (nextProps) {
var isEqual = ( return true;
this.props.columns.length === nextProps.columns.length && // Further optimization could be done here
this.props.selected === nextProps.selected && // by calling forceUpdate on flow updates, selection changes and column changes.
this.props.flow.response === nextProps.flow.response); //return (
return !isEqual; //(this.props.columns.length !== nextProps.columns.length) ||
//(this.props.selected !== nextProps.selected)
//);
} }
}); });
@ -33,30 +35,51 @@ var FlowTableHead = React.createClass({
} }
}); });
var FlowTableBody = React.createClass({
render: function () {
var rows = this.props.flows.map(function (flow) {
var selected = (flow == this.props.selected);
return <FlowRow key={flow.id}
ref={flow.id}
flow={flow}
columns={this.props.columns}
selected={selected}
selectFlow={this.props.selectFlow}
/>;
}.bind(this));
return <tbody>{rows}</tbody>;
}
});
var ROW_HEIGHT = 32;
var FlowTable = React.createClass({ var FlowTable = React.createClass({
mixins: [StickyHeadMixin, AutoScrollMixin], mixins: [StickyHeadMixin, AutoScrollMixin],
getInitialState: function () { getInitialState: function () {
return { return {
columns: all_columns columns: all_columns,
start: 0,
stop: 0
}; };
}, },
componentWillMount: function () {
if (this.props.view) {
this.props.view.addListener("add update remove recalculate", this.onChange);
}
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.view !== this.props.view) {
if (this.props.view) {
this.props.view.removeListener("add update remove recalculate");
}
nextProps.view.addListener("add update remove recalculate", this.onChange);
}
},
componentDidMount: function () {
this.onScroll();
},
onScroll: function () {
this.adjustHead();
var viewport = this.getDOMNode();
var top = viewport.scrollTop;
var height = viewport.offsetHeight;
var start = Math.floor(top / ROW_HEIGHT);
var stop = start + Math.ceil(height / ROW_HEIGHT);
this.setState({
start: start,
stop: stop
});
},
onChange: function () {
console.log("onChange");
this.forceUpdate();
},
scrollIntoView: function (flow) { scrollIntoView: function (flow) {
// Now comes the fun part: Scroll the flow into the view. // Now comes the fun part: Scroll the flow into the view.
var viewport = this.getDOMNode(); var viewport = this.getDOMNode();
@ -77,16 +100,46 @@ var FlowTable = React.createClass({
} }
}, },
render: function () { render: function () {
var space_top = 0, space_bottom = 0, fix_nth_row = null;
var rows = [];
if (this.props.view) {
var flows = this.props.view.flows;
var max = Math.min(flows.length, this.state.stop);
console.log("render", this.props.view.flows.length, this.state.start, max - this.state.start, flows.length - this.state.stop);
for (var i = this.state.start; i < max; i++) {
var flow = flows[i];
var selected = (flow === this.props.selected);
rows.push(
<FlowRow key={flow.id}
ref={flow.id}
flow={flow}
columns={this.state.columns}
selected={selected}
selectFlow={this.props.selectFlow}
/>
);
}
space_top = this.state.start * ROW_HEIGHT;
space_bottom = Math.max(0, flows.length - this.state.stop) * ROW_HEIGHT;
if(this.state.start % 2 === 1){
fix_nth_row = <tr></tr>;
}
}
return ( return (
<div className="flow-table" onScroll={this.adjustHead}> <div className="flow-table" onScroll={this.onScroll}>
<table> <table>
<FlowTableHead ref="head" <FlowTableHead ref="head"
columns={this.state.columns}/> columns={this.state.columns}/>
<FlowTableBody ref="body" <tbody>
flows={this.props.flows} <tr style={{height: space_top}}></tr>
selected={this.props.selected} { fix_nth_row }
selectFlow={this.props.selectFlow} {rows}
columns={this.state.columns}/> <tr style={{height: space_bottom}}></tr>
</tbody>
</table> </table>
</div> </div>
); );

View File

@ -16,26 +16,16 @@ var MainView = React.createClass({
this.setState({ this.setState({
view: view view: view
}); });
view.addListener("add", this.onFlowChange);
view.addListener("update", this.onFlowChange);
view.addListener("remove", this.onFlowChange);
view.addListener("recalculate", this.onFlowChange);
}, },
closeView: function () { closeView: function () {
this.state.view.close(); this.state.view.close();
}, },
componentDidMount: function () { componentWillMount: function () {
this.openView(this.props.flowStore); this.openView(this.props.flowStore);
}, },
componentWillUnmount: function () { componentWillUnmount: function () {
this.closeView(); this.closeView();
}, },
onFlowChange: function () {
console.warn("onFlowChange is deprecated");
this.setState({
flows: this.state.view.flows
});
},
selectFlow: function (flow) { selectFlow: function (flow) {
if (flow) { if (flow) {
this.replaceWith( this.replaceWith(
@ -131,7 +121,7 @@ var MainView = React.createClass({
return ( return (
<div className="main-view" onKeyDown={this.onKeyDown} tabIndex="0"> <div className="main-view" onKeyDown={this.onKeyDown} tabIndex="0">
<FlowTable ref="flowTable" <FlowTable ref="flowTable"
flows={this.state.view ? this.state.view.flows : []} view={this.state.view}
selectFlow={this.selectFlow} selectFlow={this.selectFlow}
selected={selected} /> selected={selected} />
{ details ? <Splitter/> : null } { details ? <Splitter/> : null }

View File

@ -10,16 +10,20 @@ EventEmitter.prototype.emit = function (event) {
listener.apply(this, args); listener.apply(this, args);
}.bind(this)); }.bind(this));
}; };
EventEmitter.prototype.addListener = function (event, f) { EventEmitter.prototype.addListener = function (events, f) {
events.split(" ").forEach(function (event) {
this.listeners[event] = this.listeners[event] || []; this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(f); this.listeners[event].push(f);
}.bind(this));
}; };
EventEmitter.prototype.removeListener = function (event, f) { EventEmitter.prototype.removeListener = function (events, f) {
if (!(event in this.listeners)) { if (!(events in this.listeners)) {
return false; return false;
} }
events.split(" ").forEach(function (event) {
var index = this.listeners[event].indexOf(f); var index = this.listeners[event].indexOf(f);
if (index >= 0) { if (index >= 0) {
this.listeners[event].splice(index, 1); this.listeners[event].splice(index, 1);
} }
}.bind(this));
}; };