mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 08:11:00 +00:00
Merge pull request #1004 from gzzhanghao/vscroll
[web] VirtualScroll and AutoScroll helper
This commit is contained in:
commit
4a6edd92e6
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015", "react"]
|
||||
"presets": ["es2015", "react"],
|
||||
"plugins": ["transform-class-properties"]
|
||||
}
|
@ -1,9 +1,3 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
"parser": "babel-eslint"
|
||||
}
|
@ -17,16 +17,20 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.3.6",
|
||||
"classnames": "^2.2.3",
|
||||
"flux": "^2.1.1",
|
||||
"jquery": "^2.2.1",
|
||||
"lodash": "^4.5.1",
|
||||
"react": "^0.14.7",
|
||||
"react-dom": "^0.14.7",
|
||||
"react-router": "^2.0.0"
|
||||
"react-router": "^2.0.0",
|
||||
"shallowequal": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.5.2",
|
||||
"babel-eslint": "^5.0.0",
|
||||
"babel-jest": "^6.0.1",
|
||||
"babel-plugin-transform-class-properties": "^6.6.0",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babelify": "^7.2.0",
|
||||
|
@ -2,34 +2,6 @@ import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
import _ from "lodash"
|
||||
|
||||
// http://blog.vjeux.com/2013/javascript/scroll-position-with-react.html (also contains inverse example)
|
||||
export var AutoScrollMixin = {
|
||||
componentWillUpdate: function () {
|
||||
var node = ReactDOM.findDOMNode(this);
|
||||
this._shouldScrollBottom = (
|
||||
node.scrollTop !== 0 &&
|
||||
node.scrollTop + node.clientHeight === node.scrollHeight
|
||||
);
|
||||
},
|
||||
componentDidUpdate: function () {
|
||||
if (this._shouldScrollBottom) {
|
||||
var node = ReactDOM.findDOMNode(this);
|
||||
node.scrollTop = node.scrollHeight;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export var StickyHeadMixin = {
|
||||
adjustHead: function () {
|
||||
// Abusing CSS transforms to set the element
|
||||
// referenced as head into some kind of position:sticky.
|
||||
var head = ReactDOM.findDOMNode(this.refs.head);
|
||||
head.style.transform = "translate(0," + ReactDOM.findDOMNode(this).scrollTop + "px)";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export var Router = {
|
||||
contextTypes: {
|
||||
location: React.PropTypes.object,
|
||||
|
@ -1,116 +1,151 @@
|
||||
import React from "react"
|
||||
import {AutoScrollMixin, Router} from "./common.js"
|
||||
import ReactDOM from "react-dom"
|
||||
import shallowEqual from "shallowequal"
|
||||
import {Router} from "./common.js"
|
||||
import {Query} from "../actions.js"
|
||||
import { VirtualScrollMixin } from "./virtualscroll.js"
|
||||
import AutoScroll from "./helpers/AutoScroll";
|
||||
import {calcVScroll} from "./helpers/VirtualScroll"
|
||||
import {StoreView} from "../store/view.js"
|
||||
import _ from "lodash"
|
||||
|
||||
var LogMessage = React.createClass({
|
||||
render: function () {
|
||||
var entry = this.props.entry;
|
||||
var indicator;
|
||||
switch (entry.level) {
|
||||
case "web":
|
||||
indicator = <i className="fa fa-fw fa-html5"></i>;
|
||||
break;
|
||||
case "debug":
|
||||
indicator = <i className="fa fa-fw fa-bug"></i>;
|
||||
break;
|
||||
default:
|
||||
indicator = <i className="fa fa-fw fa-info"></i>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{ indicator } {entry.message}
|
||||
</div>
|
||||
class EventLogContents extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
eventStore: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
rowHeight: 18,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.view = new StoreView(
|
||||
this.context.eventStore,
|
||||
entry => this.props.filter[entry.level]
|
||||
);
|
||||
},
|
||||
shouldComponentUpdate: function () {
|
||||
return false; // log entries are immutable.
|
||||
|
||||
this.heights = {};
|
||||
this.state = { entries: this.view.list, vScroll: calcVScroll() };
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onViewportUpdate = this.onViewportUpdate.bind(this);
|
||||
}
|
||||
});
|
||||
|
||||
var EventLogContents = React.createClass({
|
||||
contextTypes: {
|
||||
eventStore: React.PropTypes.object.isRequired
|
||||
},
|
||||
mixins: [AutoScrollMixin, VirtualScrollMixin],
|
||||
getInitialState: function () {
|
||||
var filterFn = function (entry) {
|
||||
return this.props.filter[entry.level];
|
||||
};
|
||||
var view = new StoreView(this.context.eventStore, filterFn.bind(this));
|
||||
view.addListener("add", this.onEventLogChange);
|
||||
view.addListener("recalculate", this.onEventLogChange);
|
||||
componentDidMount() {
|
||||
window.addEventListener("resize", this.onViewportUpdate);
|
||||
this.view.addListener("add", this.onChange);
|
||||
this.view.addListener("recalculate", this.onChange);
|
||||
this.onViewportUpdate();
|
||||
}
|
||||
|
||||
return {
|
||||
view: view
|
||||
};
|
||||
},
|
||||
componentWillUnmount: function () {
|
||||
this.state.view.close();
|
||||
},
|
||||
filter: function (entry) {
|
||||
return this.props.filter[entry.level];
|
||||
},
|
||||
onEventLogChange: function () {
|
||||
this.forceUpdate();
|
||||
},
|
||||
componentWillReceiveProps: function (nextProps) {
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this.onViewportUpdate);
|
||||
this.view.removeListener("add", this.onChange);
|
||||
this.view.removeListener("recalculate", this.onChange);
|
||||
this.view.close();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.onViewportUpdate();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.filter !== this.props.filter) {
|
||||
this.state.view.recalculate(entry =>
|
||||
nextProps.filter[entry.level]
|
||||
this.view.recalculate(
|
||||
entry => nextProps.filter[entry.level]
|
||||
);
|
||||
}
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
rowHeight: 45,
|
||||
rowHeightMin: 15,
|
||||
placeholderTagName: "div"
|
||||
};
|
||||
},
|
||||
renderRow: function (elem) {
|
||||
return <LogMessage key={elem.id} entry={elem}/>;
|
||||
},
|
||||
render: function () {
|
||||
var entries = this.state.view.list;
|
||||
var rows = this.renderRows(entries);
|
||||
|
||||
return <pre onScroll={this.onScroll}>
|
||||
{ this.getPlaceholderTop(entries.length) }
|
||||
{rows}
|
||||
{ this.getPlaceholderBottom(entries.length) }
|
||||
</pre>;
|
||||
}
|
||||
});
|
||||
|
||||
var ToggleFilter = React.createClass({
|
||||
toggle: function (e) {
|
||||
e.preventDefault();
|
||||
return this.props.toggleLevel(this.props.name);
|
||||
},
|
||||
render: function () {
|
||||
var className = "label ";
|
||||
if (this.props.active) {
|
||||
className += "label-primary";
|
||||
} else {
|
||||
className += "label-default";
|
||||
onViewportUpdate() {
|
||||
const viewport = ReactDOM.findDOMNode(this);
|
||||
|
||||
const vScroll = calcVScroll({
|
||||
itemCount: this.state.entries.length,
|
||||
rowHeight: this.props.rowHeight,
|
||||
viewportTop: viewport.scrollTop,
|
||||
viewportHeight: viewport.offsetHeight,
|
||||
itemHeights: this.state.entries.map(entry => this.heights[entry.id]),
|
||||
});
|
||||
|
||||
if (!shallowEqual(this.state.vScroll, vScroll)) {
|
||||
this.setState({ vScroll });
|
||||
}
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({ entries: this.view.list });
|
||||
}
|
||||
|
||||
setHeight(id, ref) {
|
||||
if (ref && !this.heights[id]) {
|
||||
const height = ReactDOM.findDOMNode(ref).offsetHeight;
|
||||
if (this.heights[id] !== height) {
|
||||
this.heights[id] = height;
|
||||
this.onViewportUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getIcon(level) {
|
||||
return { web: "html5", debug: "bug" }[level] || "info";
|
||||
}
|
||||
|
||||
render() {
|
||||
const vScroll = this.state.vScroll;
|
||||
const entries = this.state.entries.slice(vScroll.start, vScroll.end);
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className={className}
|
||||
onClick={this.toggle}>
|
||||
{this.props.name}
|
||||
</a>
|
||||
<pre onScroll={this.onViewportUpdate}>
|
||||
<div style={{ height: vScroll.paddingTop }}></div>
|
||||
{entries.map((entry, index) => (
|
||||
<div key={entry.id} ref={this.setHeight.bind(this, entry.id)}>
|
||||
<i className={`fa fa-fw fa-${this.getIcon(entry.level)}`}></i>
|
||||
{entry.message}
|
||||
</div>
|
||||
))}
|
||||
<div style={{ height: vScroll.paddingBottom }}></div>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ToggleFilter.propTypes = {
|
||||
name: React.PropTypes.string.isRequired,
|
||||
toggleLevel: React.PropTypes.func.isRequired,
|
||||
active: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
function ToggleFilter ({ name, active, toggleLevel }) {
|
||||
let className = "label ";
|
||||
if (active) {
|
||||
className += "label-primary";
|
||||
} else {
|
||||
className += "label-default";
|
||||
}
|
||||
|
||||
function onClick(event) {
|
||||
event.preventDefault();
|
||||
toggleLevel(name);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className={className}
|
||||
onClick={onClick}>
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const AutoScrollEventLog = AutoScroll(EventLogContents);
|
||||
|
||||
var EventLog = React.createClass({
|
||||
mixins: [Router],
|
||||
getInitialState: function () {
|
||||
getInitialState() {
|
||||
return {
|
||||
filter: {
|
||||
"debug": false,
|
||||
@ -119,18 +154,17 @@ var EventLog = React.createClass({
|
||||
}
|
||||
};
|
||||
},
|
||||
close: function () {
|
||||
close() {
|
||||
var d = {};
|
||||
d[Query.SHOW_EVENTLOG] = undefined;
|
||||
|
||||
this.updateLocation(undefined, d);
|
||||
},
|
||||
toggleLevel: function (level) {
|
||||
toggleLevel(level) {
|
||||
var filter = _.extend({}, this.state.filter);
|
||||
filter[level] = !filter[level];
|
||||
this.setState({filter: filter});
|
||||
},
|
||||
render: function () {
|
||||
render() {
|
||||
return (
|
||||
<div className="eventlog">
|
||||
<div>
|
||||
@ -143,10 +177,10 @@ var EventLog = React.createClass({
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<EventLogContents filter={this.state.filter}/>
|
||||
<AutoScrollEventLog filter={this.state.filter}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default EventLog;
|
||||
export default EventLog;
|
||||
|
@ -1,188 +1,216 @@
|
||||
import React from "react";
|
||||
import ReactDOM from 'react-dom';
|
||||
import {StickyHeadMixin, AutoScrollMixin} from "./common.js";
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import {reverseString} from "../utils.js";
|
||||
import _ from "lodash";
|
||||
|
||||
import { VirtualScrollMixin } from "./virtualscroll.js"
|
||||
import shallowEqual from "shallowequal";
|
||||
import AutoScroll from "./helpers/AutoScroll";
|
||||
import {calcVScroll} from "./helpers/VirtualScroll";
|
||||
import flowtable_columns from "./flowtable-columns.js";
|
||||
|
||||
var FlowRow = React.createClass({
|
||||
render: function () {
|
||||
var flow = this.props.flow;
|
||||
var columns = this.props.columns.map(function (Column) {
|
||||
return <Column key={Column.displayName} flow={flow}/>;
|
||||
}.bind(this));
|
||||
var className = "";
|
||||
if (this.props.selected) {
|
||||
className += " selected";
|
||||
}
|
||||
if (this.props.highlighted) {
|
||||
className += " highlighted";
|
||||
}
|
||||
if (flow.intercepted) {
|
||||
className += " intercepted";
|
||||
}
|
||||
if (flow.request) {
|
||||
className += " has-request";
|
||||
}
|
||||
if (flow.response) {
|
||||
className += " has-response";
|
||||
}
|
||||
FlowRow.propTypes = {
|
||||
selectFlow: React.PropTypes.func.isRequired,
|
||||
columns: React.PropTypes.array.isRequired,
|
||||
flow: React.PropTypes.object.isRequired,
|
||||
highlighted: React.PropTypes.bool,
|
||||
selected: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className={className} onClick={this.props.selectFlow.bind(null, flow)}>
|
||||
{columns}
|
||||
</tr>);
|
||||
},
|
||||
shouldComponentUpdate: function (nextProps) {
|
||||
return true;
|
||||
// Further optimization could be done here
|
||||
// by calling forceUpdate on flow updates, selection changes and column changes.
|
||||
//return (
|
||||
//(this.props.columns.length !== nextProps.columns.length) ||
|
||||
//(this.props.selected !== nextProps.selected)
|
||||
//);
|
||||
function FlowRow(props) {
|
||||
const flow = props.flow;
|
||||
|
||||
const className = classNames({
|
||||
"selected": props.selected,
|
||||
"highlighted": props.highlighted,
|
||||
"intercepted": flow.intercepted,
|
||||
"has-request": flow.request,
|
||||
"has-response": flow.response,
|
||||
});
|
||||
|
||||
return (
|
||||
<tr className={className} onClick={() => props.selectFlow(flow)}>
|
||||
{props.columns.map(Column => (
|
||||
<Column key={Column.displayName} flow={flow}/>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
class FlowTableHead extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
setSortKeyFun: React.PropTypes.func.isRequired,
|
||||
columns: React.PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = { sortColumn: undefined, sortDesc: false };
|
||||
}
|
||||
});
|
||||
|
||||
var FlowTableHead = React.createClass({
|
||||
getInitialState: function(){
|
||||
return {
|
||||
sortColumn: undefined,
|
||||
sortDesc: false
|
||||
};
|
||||
},
|
||||
onClick: function(Column){
|
||||
var sortDesc = this.state.sortDesc;
|
||||
var hasSort = Column.sortKeyFun;
|
||||
if(Column === this.state.sortColumn){
|
||||
onClick(Column) {
|
||||
const hasSort = Column.sortKeyFun;
|
||||
|
||||
let sortDesc = this.state.sortDesc;
|
||||
|
||||
if (Column === this.state.sortColumn) {
|
||||
sortDesc = !sortDesc;
|
||||
this.setState({
|
||||
sortDesc: sortDesc
|
||||
});
|
||||
this.setState({ sortDesc });
|
||||
} else {
|
||||
this.setState({
|
||||
sortColumn: hasSort && Column,
|
||||
sortDesc: false
|
||||
})
|
||||
this.setState({ sortColumn: hasSort && Column, sortDesc: false });
|
||||
}
|
||||
var sortKeyFun;
|
||||
if(!sortDesc){
|
||||
sortKeyFun = Column.sortKeyFun;
|
||||
} else {
|
||||
sortKeyFun = hasSort && function(){
|
||||
var k = Column.sortKeyFun.apply(this, arguments);
|
||||
if(_.isString(k)){
|
||||
return reverseString(""+k);
|
||||
} else {
|
||||
return -k;
|
||||
|
||||
let sortKeyFun = Column.sortKeyFun;
|
||||
if (sortDesc) {
|
||||
sortKeyFun = hasSort && function() {
|
||||
const k = Column.sortKeyFun.apply(this, arguments);
|
||||
if (_.isString(k)) {
|
||||
return reverseString("" + k);
|
||||
}
|
||||
}
|
||||
return -k;
|
||||
};
|
||||
}
|
||||
|
||||
this.props.setSortKeyFun(sortKeyFun);
|
||||
},
|
||||
render: function () {
|
||||
var columns = this.props.columns.map(function (Column) {
|
||||
var onClick = this.onClick.bind(this, Column);
|
||||
var className;
|
||||
if(this.state.sortColumn === Column) {
|
||||
if(this.state.sortDesc){
|
||||
className = "sort-desc";
|
||||
} else {
|
||||
className = "sort-asc";
|
||||
}
|
||||
}
|
||||
return <Column.Title
|
||||
key={Column.displayName}
|
||||
onClick={onClick}
|
||||
className={className} />;
|
||||
}.bind(this));
|
||||
return <thead>
|
||||
<tr>{columns}</tr>
|
||||
</thead>;
|
||||
}
|
||||
});
|
||||
|
||||
render() {
|
||||
const sortColumn = this.state.sortColumn;
|
||||
const sortType = this.state.sortDesc ? "sort-desc" : "sort-asc";
|
||||
return (
|
||||
<tr>
|
||||
{this.props.columns.map(Column => (
|
||||
<Column.Title
|
||||
key={Column.displayName}
|
||||
onClick={() => this.onClick(Column)}
|
||||
className={sortColumn === Column && sortType}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var ROW_HEIGHT = 32;
|
||||
class FlowTable extends React.Component {
|
||||
|
||||
var FlowTable = React.createClass({
|
||||
mixins: [StickyHeadMixin, AutoScrollMixin, VirtualScrollMixin],
|
||||
contextTypes: {
|
||||
view: React.PropTypes.object.isRequired
|
||||
},
|
||||
getInitialState: function () {
|
||||
return {
|
||||
columns: flowtable_columns
|
||||
};
|
||||
},
|
||||
componentWillMount: function () {
|
||||
static contextTypes = {
|
||||
view: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
rowHeight: React.PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
rowHeight: 32,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = { flows: [], vScroll: calcVScroll() };
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onViewportUpdate = this.onViewportUpdate.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
window.addEventListener("resize", this.onViewportUpdate);
|
||||
this.context.view.addListener("add", this.onChange);
|
||||
this.context.view.addListener("update", this.onChange);
|
||||
this.context.view.addListener("remove", this.onChange);
|
||||
this.context.view.addListener("recalculate", this.onChange);
|
||||
},
|
||||
componentWillUnmount: function(){
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("resize", this.onViewportUpdate);
|
||||
this.context.view.removeListener("add", this.onChange);
|
||||
this.context.view.removeListener("update", this.onChange);
|
||||
this.context.view.removeListener("remove", this.onChange);
|
||||
this.context.view.removeListener("recalculate", this.onChange);
|
||||
},
|
||||
getDefaultProps: function () {
|
||||
return {
|
||||
rowHeight: ROW_HEIGHT
|
||||
};
|
||||
},
|
||||
onScrollFlowTable: function () {
|
||||
this.adjustHead();
|
||||
this.onScroll();
|
||||
},
|
||||
onChange: function () {
|
||||
this.forceUpdate();
|
||||
},
|
||||
scrollIntoView: function (flow) {
|
||||
this.scrollRowIntoView(
|
||||
this.context.view.indexOf(flow),
|
||||
ReactDOM.findDOMNode(this.refs.body).offsetTop
|
||||
);
|
||||
},
|
||||
renderRow: function (flow) {
|
||||
var selected = (flow === this.props.selected);
|
||||
var highlighted =
|
||||
(
|
||||
this.context.view._highlight &&
|
||||
this.context.view._highlight[flow.id]
|
||||
);
|
||||
}
|
||||
|
||||
return <FlowRow key={flow.id}
|
||||
ref={flow.id}
|
||||
flow={flow}
|
||||
columns={this.state.columns}
|
||||
selected={selected}
|
||||
highlighted={highlighted}
|
||||
selectFlow={this.props.selectFlow}
|
||||
/>;
|
||||
},
|
||||
render: function () {
|
||||
var flows = this.context.view.list;
|
||||
var rows = this.renderRows(flows);
|
||||
componentDidUpdate() {
|
||||
this.onViewportUpdate();
|
||||
}
|
||||
|
||||
onViewportUpdate() {
|
||||
const viewport = ReactDOM.findDOMNode(this);
|
||||
const viewportTop = viewport.scrollTop;
|
||||
|
||||
const vScroll = calcVScroll({
|
||||
viewportTop,
|
||||
viewportHeight: viewport.offsetHeight,
|
||||
itemCount: this.state.flows.length,
|
||||
rowHeight: this.props.rowHeight,
|
||||
});
|
||||
|
||||
if (!shallowEqual(this.state.vScroll, vScroll) ||
|
||||
this.state.viewportTop !== viewportTop) {
|
||||
this.setState({ vScroll, viewportTop });
|
||||
}
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({ flows: this.context.view.list });
|
||||
}
|
||||
|
||||
scrollIntoView(flow) {
|
||||
const viewport = ReactDOM.findDOMNode(this);
|
||||
const index = this.context.view.indexOf(flow);
|
||||
const rowHeight = this.props.rowHeight;
|
||||
const head = ReactDOM.findDOMNode(this.refs.head);
|
||||
|
||||
const headHeight = head ? head.offsetHeight : 0;
|
||||
|
||||
const rowTop = (index * rowHeight) + headHeight;
|
||||
const rowBottom = rowTop + rowHeight;
|
||||
|
||||
const viewportTop = viewport.scrollTop;
|
||||
const viewportHeight = viewport.offsetHeight;
|
||||
|
||||
// Account for pinned thead
|
||||
if (rowTop - headHeight < viewportTop) {
|
||||
viewport.scrollTop = rowTop - headHeight;
|
||||
} else if (rowBottom > viewportTop + viewportHeight) {
|
||||
viewport.scrollTop = rowBottom - viewportHeight;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const vScroll = this.state.vScroll;
|
||||
const highlight = this.context.view._highlight;
|
||||
const flows = this.state.flows.slice(vScroll.start, vScroll.end);
|
||||
|
||||
const transform = `translate(0,${this.state.viewportTop}px)`;
|
||||
|
||||
return (
|
||||
<div className="flow-table" onScroll={this.onScrollFlowTable}>
|
||||
<div className="flow-table" onScroll={this.onViewportUpdate}>
|
||||
<table>
|
||||
<FlowTableHead ref="head"
|
||||
columns={this.state.columns}
|
||||
setSortKeyFun={this.props.setSortKeyFun}/>
|
||||
<tbody ref="body">
|
||||
{ this.getPlaceholderTop(flows.length) }
|
||||
{rows}
|
||||
{ this.getPlaceholderBottom(flows.length) }
|
||||
<thead ref="head" style={{ transform }}>
|
||||
<FlowTableHead
|
||||
columns={flowtable_columns}
|
||||
setSortKeyFun={this.props.setSortKeyFun}
|
||||
/>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style={{ height: vScroll.paddingTop }}></tr>
|
||||
{flows.map(flow => (
|
||||
<FlowRow
|
||||
key={flow.id}
|
||||
flow={flow}
|
||||
columns={flowtable_columns}
|
||||
selected={flow === this.props.selected}
|
||||
highlighted={highlight && highlight[flow.id]}
|
||||
selectFlow={this.props.selectFlow}
|
||||
/>
|
||||
))}
|
||||
<tr style={{ height: vScroll.paddingBottom }}></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default FlowTable;
|
||||
export default AutoScroll(FlowTable);
|
||||
|
25
web/src/js/components/helpers/AutoScroll.js
Normal file
25
web/src/js/components/helpers/AutoScroll.js
Normal file
@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
const symShouldStick = Symbol("shouldStick");
|
||||
const isAtBottom = v => v.scrollTop + v.clientHeight === v.scrollHeight;
|
||||
|
||||
export default Component => Object.assign(class AutoScrollWrapper extends Component {
|
||||
|
||||
static displayName = Component.name;
|
||||
|
||||
componentWillUpdate() {
|
||||
const viewport = ReactDOM.findDOMNode(this);
|
||||
this[symShouldStick] = viewport.scrollTop && isAtBottom(viewport);
|
||||
super.componentWillUpdate && super.componentWillUpdate();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const viewport = ReactDOM.findDOMNode(this);
|
||||
if (this[symShouldStick] && !isAtBottom(viewport)) {
|
||||
viewport.scrollTop = viewport.scrollHeight;
|
||||
}
|
||||
super.componentDidUpdate && super.componentDidUpdate();
|
||||
}
|
||||
|
||||
}, Component);
|
70
web/src/js/components/helpers/VirtualScroll.js
Normal file
70
web/src/js/components/helpers/VirtualScroll.js
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Calculate virtual scroll stuffs
|
||||
*
|
||||
* @param {?Object} opts Options for calculation
|
||||
*
|
||||
* @returns {Object} result
|
||||
*
|
||||
* __opts__ should have following properties:
|
||||
* - {number} itemCount
|
||||
* - {number} rowHeight
|
||||
* - {number} viewportTop
|
||||
* - {number} viewportHeight
|
||||
* - {Array<?number>} [itemHeights]
|
||||
*
|
||||
* __result__ have following properties:
|
||||
* - {number} start
|
||||
* - {number} end
|
||||
* - {number} paddingTop
|
||||
* - {number} paddingBottom
|
||||
*/
|
||||
export function calcVScroll(opts) {
|
||||
if (!opts) {
|
||||
return { start: 0, end: 0, paddingTop: 0, paddingBottom: 0 };
|
||||
}
|
||||
|
||||
const { itemCount, rowHeight, viewportTop, viewportHeight, itemHeights } = opts;
|
||||
const viewportBottom = viewportTop + viewportHeight;
|
||||
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
|
||||
let paddingTop = 0;
|
||||
let paddingBottom = 0;
|
||||
|
||||
if (itemHeights) {
|
||||
|
||||
for (let i = 0, pos = 0; i < itemCount; i++) {
|
||||
const height = itemHeights[i] || rowHeight;
|
||||
|
||||
if (pos <= viewportTop && i % 2 === 0) {
|
||||
paddingTop = pos;
|
||||
start = i;
|
||||
}
|
||||
|
||||
if (pos <= viewportBottom) {
|
||||
end = i + 1;
|
||||
} else {
|
||||
paddingBottom += height;
|
||||
}
|
||||
|
||||
pos += height;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Make sure that we start at an even row so that CSS `:nth-child(even)` is preserved
|
||||
start = Math.max(0, Math.floor(viewportTop / rowHeight) - 1) & ~1;
|
||||
end = Math.min(
|
||||
itemCount,
|
||||
start + Math.ceil(viewportHeight / rowHeight) + 1
|
||||
);
|
||||
|
||||
// When a large trunk of elements is removed from the button, start may be far off the viewport.
|
||||
// To make this issue less severe, limit the top placeholder to the total number of rows.
|
||||
paddingTop = Math.min(start, itemCount) * rowHeight;
|
||||
paddingBottom = Math.max(0, itemCount - end) * rowHeight;
|
||||
}
|
||||
|
||||
return { start, end, paddingTop, paddingBottom };
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
export var VirtualScrollMixin = {
|
||||
getInitialState: function () {
|
||||
return {
|
||||
start: 0,
|
||||
stop: 0
|
||||
};
|
||||
},
|
||||
componentWillMount: function () {
|
||||
if (!this.props.rowHeight) {
|
||||
console.warn("VirtualScrollMixin: No rowHeight specified", this);
|
||||
}
|
||||
},
|
||||
getPlaceholderTop: function (total) {
|
||||
var Tag = this.props.placeholderTagName || "tr";
|
||||
// When a large trunk of elements is removed from the button, start may be far off the viewport.
|
||||
// To make this issue less severe, limit the top placeholder to the total number of rows.
|
||||
var style = {
|
||||
height: Math.min(this.state.start, total) * this.props.rowHeight
|
||||
};
|
||||
var spacer = <Tag key="placeholder-top" style={style}></Tag>;
|
||||
|
||||
if (this.state.start % 2 === 1) {
|
||||
// fix even/odd rows
|
||||
return [spacer, <Tag key="placeholder-top-2"></Tag>];
|
||||
} else {
|
||||
return spacer;
|
||||
}
|
||||
},
|
||||
getPlaceholderBottom: function (total) {
|
||||
var Tag = this.props.placeholderTagName || "tr";
|
||||
var style = {
|
||||
height: Math.max(0, total - this.state.stop) * this.props.rowHeight
|
||||
};
|
||||
return <Tag key="placeholder-bottom" style={style}></Tag>;
|
||||
},
|
||||
componentDidMount: function () {
|
||||
this.onScroll();
|
||||
window.addEventListener('resize', this.onScroll);
|
||||
},
|
||||
componentWillUnmount: function(){
|
||||
window.removeEventListener('resize', this.onScroll);
|
||||
},
|
||||
onScroll: function () {
|
||||
var viewport = ReactDOM.findDOMNode(this);
|
||||
var top = viewport.scrollTop;
|
||||
var height = viewport.offsetHeight;
|
||||
var start = Math.floor(top / this.props.rowHeight);
|
||||
var stop = start + Math.ceil(height / (this.props.rowHeightMin || this.props.rowHeight));
|
||||
|
||||
this.setState({
|
||||
start: start,
|
||||
stop: stop
|
||||
});
|
||||
},
|
||||
renderRows: function (elems) {
|
||||
var rows = [];
|
||||
var max = Math.min(elems.length, this.state.stop);
|
||||
|
||||
for (var i = this.state.start; i < max; i++) {
|
||||
var elem = elems[i];
|
||||
rows.push(this.renderRow(elem));
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
scrollRowIntoView: function (index, head_height) {
|
||||
|
||||
var row_top = (index * this.props.rowHeight) + head_height;
|
||||
var row_bottom = row_top + this.props.rowHeight;
|
||||
|
||||
var viewport = ReactDOM.findDOMNode(this);
|
||||
var viewport_top = viewport.scrollTop;
|
||||
var viewport_bottom = viewport_top + viewport.offsetHeight;
|
||||
|
||||
// Account for pinned thead
|
||||
if (row_top - head_height < viewport_top) {
|
||||
viewport.scrollTop = row_top - head_height;
|
||||
} else if (row_bottom > viewport_bottom) {
|
||||
viewport.scrollTop = row_bottom - viewport.offsetHeight;
|
||||
}
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user