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": {
|
"parser": "babel-eslint"
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -17,16 +17,20 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^3.3.6",
|
"bootstrap": "^3.3.6",
|
||||||
|
"classnames": "^2.2.3",
|
||||||
"flux": "^2.1.1",
|
"flux": "^2.1.1",
|
||||||
"jquery": "^2.2.1",
|
"jquery": "^2.2.1",
|
||||||
"lodash": "^4.5.1",
|
"lodash": "^4.5.1",
|
||||||
"react": "^0.14.7",
|
"react": "^0.14.7",
|
||||||
"react-dom": "^0.14.7",
|
"react-dom": "^0.14.7",
|
||||||
"react-router": "^2.0.0"
|
"react-router": "^2.0.0",
|
||||||
|
"shallowequal": "^0.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.5.2",
|
"babel-core": "^6.5.2",
|
||||||
|
"babel-eslint": "^5.0.0",
|
||||||
"babel-jest": "^6.0.1",
|
"babel-jest": "^6.0.1",
|
||||||
|
"babel-plugin-transform-class-properties": "^6.6.0",
|
||||||
"babel-preset-es2015": "^6.5.0",
|
"babel-preset-es2015": "^6.5.0",
|
||||||
"babel-preset-react": "^6.5.0",
|
"babel-preset-react": "^6.5.0",
|
||||||
"babelify": "^7.2.0",
|
"babelify": "^7.2.0",
|
||||||
|
@ -2,34 +2,6 @@ import React from "react"
|
|||||||
import ReactDOM from "react-dom"
|
import ReactDOM from "react-dom"
|
||||||
import _ from "lodash"
|
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 = {
|
export var Router = {
|
||||||
contextTypes: {
|
contextTypes: {
|
||||||
location: React.PropTypes.object,
|
location: React.PropTypes.object,
|
||||||
|
@ -1,116 +1,151 @@
|
|||||||
import React from "react"
|
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 {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 {StoreView} from "../store/view.js"
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
|
|
||||||
var LogMessage = React.createClass({
|
class EventLogContents extends React.Component {
|
||||||
render: function () {
|
|
||||||
var entry = this.props.entry;
|
static contextTypes = {
|
||||||
var indicator;
|
eventStore: React.PropTypes.object.isRequired,
|
||||||
switch (entry.level) {
|
};
|
||||||
case "web":
|
|
||||||
indicator = <i className="fa fa-fw fa-html5"></i>;
|
static defaultProps = {
|
||||||
break;
|
rowHeight: 18,
|
||||||
case "debug":
|
};
|
||||||
indicator = <i className="fa fa-fw fa-bug"></i>;
|
|
||||||
break;
|
constructor(props, context) {
|
||||||
default:
|
super(props, context);
|
||||||
indicator = <i className="fa fa-fw fa-info"></i>;
|
|
||||||
}
|
this.view = new StoreView(
|
||||||
return (
|
this.context.eventStore,
|
||||||
<div>
|
entry => this.props.filter[entry.level]
|
||||||
{ indicator } {entry.message}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
|
||||||
shouldComponentUpdate: function () {
|
this.heights = {};
|
||||||
return false; // log entries are immutable.
|
this.state = { entries: this.view.list, vScroll: calcVScroll() };
|
||||||
|
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.onViewportUpdate = this.onViewportUpdate.bind(this);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
var EventLogContents = React.createClass({
|
componentDidMount() {
|
||||||
contextTypes: {
|
window.addEventListener("resize", this.onViewportUpdate);
|
||||||
eventStore: React.PropTypes.object.isRequired
|
this.view.addListener("add", this.onChange);
|
||||||
},
|
this.view.addListener("recalculate", this.onChange);
|
||||||
mixins: [AutoScrollMixin, VirtualScrollMixin],
|
this.onViewportUpdate();
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
componentWillUnmount() {
|
||||||
view: view
|
window.removeEventListener("resize", this.onViewportUpdate);
|
||||||
};
|
this.view.removeListener("add", this.onChange);
|
||||||
},
|
this.view.removeListener("recalculate", this.onChange);
|
||||||
componentWillUnmount: function () {
|
this.view.close();
|
||||||
this.state.view.close();
|
}
|
||||||
},
|
|
||||||
filter: function (entry) {
|
componentDidUpdate() {
|
||||||
return this.props.filter[entry.level];
|
this.onViewportUpdate();
|
||||||
},
|
}
|
||||||
onEventLogChange: function () {
|
|
||||||
this.forceUpdate();
|
componentWillReceiveProps(nextProps) {
|
||||||
},
|
|
||||||
componentWillReceiveProps: function (nextProps) {
|
|
||||||
if (nextProps.filter !== this.props.filter) {
|
if (nextProps.filter !== this.props.filter) {
|
||||||
this.state.view.recalculate(entry =>
|
this.view.recalculate(
|
||||||
nextProps.filter[entry.level]
|
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({
|
onViewportUpdate() {
|
||||||
toggle: function (e) {
|
const viewport = ReactDOM.findDOMNode(this);
|
||||||
e.preventDefault();
|
|
||||||
return this.props.toggleLevel(this.props.name);
|
const vScroll = calcVScroll({
|
||||||
},
|
itemCount: this.state.entries.length,
|
||||||
render: function () {
|
rowHeight: this.props.rowHeight,
|
||||||
var className = "label ";
|
viewportTop: viewport.scrollTop,
|
||||||
if (this.props.active) {
|
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 (
|
||||||
|
<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";
|
className += "label-primary";
|
||||||
} else {
|
} else {
|
||||||
className += "label-default";
|
className += "label-default";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleLevel(name);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
className={className}
|
className={className}
|
||||||
onClick={this.toggle}>
|
onClick={onClick}>
|
||||||
{this.props.name}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const AutoScrollEventLog = AutoScroll(EventLogContents);
|
||||||
|
|
||||||
var EventLog = React.createClass({
|
var EventLog = React.createClass({
|
||||||
mixins: [Router],
|
mixins: [Router],
|
||||||
getInitialState: function () {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
filter: {
|
filter: {
|
||||||
"debug": false,
|
"debug": false,
|
||||||
@ -119,18 +154,17 @@ var EventLog = React.createClass({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
close: function () {
|
close() {
|
||||||
var d = {};
|
var d = {};
|
||||||
d[Query.SHOW_EVENTLOG] = undefined;
|
d[Query.SHOW_EVENTLOG] = undefined;
|
||||||
|
|
||||||
this.updateLocation(undefined, d);
|
this.updateLocation(undefined, d);
|
||||||
},
|
},
|
||||||
toggleLevel: function (level) {
|
toggleLevel(level) {
|
||||||
var filter = _.extend({}, this.state.filter);
|
var filter = _.extend({}, this.state.filter);
|
||||||
filter[level] = !filter[level];
|
filter[level] = !filter[level];
|
||||||
this.setState({filter: filter});
|
this.setState({filter: filter});
|
||||||
},
|
},
|
||||||
render: function () {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="eventlog">
|
<div className="eventlog">
|
||||||
<div>
|
<div>
|
||||||
@ -143,7 +177,7 @@ var EventLog = React.createClass({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<EventLogContents filter={this.state.filter}/>
|
<AutoScrollEventLog filter={this.state.filter}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,188 +1,216 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import {StickyHeadMixin, AutoScrollMixin} from "./common.js";
|
import classNames from "classnames";
|
||||||
import {reverseString} from "../utils.js";
|
import {reverseString} from "../utils.js";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import shallowEqual from "shallowequal";
|
||||||
import { VirtualScrollMixin } from "./virtualscroll.js"
|
import AutoScroll from "./helpers/AutoScroll";
|
||||||
|
import {calcVScroll} from "./helpers/VirtualScroll";
|
||||||
import flowtable_columns from "./flowtable-columns.js";
|
import flowtable_columns from "./flowtable-columns.js";
|
||||||
|
|
||||||
var FlowRow = React.createClass({
|
FlowRow.propTypes = {
|
||||||
render: function () {
|
selectFlow: React.PropTypes.func.isRequired,
|
||||||
var flow = this.props.flow;
|
columns: React.PropTypes.array.isRequired,
|
||||||
var columns = this.props.columns.map(function (Column) {
|
flow: React.PropTypes.object.isRequired,
|
||||||
return <Column key={Column.displayName} flow={flow}/>;
|
highlighted: React.PropTypes.bool,
|
||||||
}.bind(this));
|
selected: React.PropTypes.bool,
|
||||||
var className = "";
|
};
|
||||||
if (this.props.selected) {
|
|
||||||
className += " selected";
|
function FlowRow(props) {
|
||||||
}
|
const flow = props.flow;
|
||||||
if (this.props.highlighted) {
|
|
||||||
className += " highlighted";
|
const className = classNames({
|
||||||
}
|
"selected": props.selected,
|
||||||
if (flow.intercepted) {
|
"highlighted": props.highlighted,
|
||||||
className += " intercepted";
|
"intercepted": flow.intercepted,
|
||||||
}
|
"has-request": flow.request,
|
||||||
if (flow.request) {
|
"has-response": flow.response,
|
||||||
className += " has-request";
|
});
|
||||||
}
|
|
||||||
if (flow.response) {
|
|
||||||
className += " has-response";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={className} onClick={this.props.selectFlow.bind(null, flow)}>
|
<tr className={className} onClick={() => props.selectFlow(flow)}>
|
||||||
{columns}
|
{props.columns.map(Column => (
|
||||||
</tr>);
|
<Column key={Column.displayName} flow={flow}/>
|
||||||
},
|
))}
|
||||||
shouldComponentUpdate: function (nextProps) {
|
</tr>
|
||||||
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)
|
|
||||||
//);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var FlowTableHead = React.createClass({
|
class FlowTableHead extends React.Component {
|
||||||
getInitialState: function(){
|
|
||||||
return {
|
static propTypes = {
|
||||||
sortColumn: undefined,
|
setSortKeyFun: React.PropTypes.func.isRequired,
|
||||||
sortDesc: false
|
columns: React.PropTypes.array.isRequired,
|
||||||
};
|
};
|
||||||
},
|
|
||||||
onClick: function(Column){
|
constructor(props, context) {
|
||||||
var sortDesc = this.state.sortDesc;
|
super(props, context);
|
||||||
var hasSort = Column.sortKeyFun;
|
this.state = { sortColumn: undefined, sortDesc: false };
|
||||||
if(Column === this.state.sortColumn){
|
}
|
||||||
|
|
||||||
|
onClick(Column) {
|
||||||
|
const hasSort = Column.sortKeyFun;
|
||||||
|
|
||||||
|
let sortDesc = this.state.sortDesc;
|
||||||
|
|
||||||
|
if (Column === this.state.sortColumn) {
|
||||||
sortDesc = !sortDesc;
|
sortDesc = !sortDesc;
|
||||||
this.setState({
|
this.setState({ sortDesc });
|
||||||
sortDesc: sortDesc
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({ sortColumn: hasSort && Column, sortDesc: false });
|
||||||
sortColumn: hasSort && Column,
|
}
|
||||||
sortDesc: false
|
|
||||||
})
|
let sortKeyFun = Column.sortKeyFun;
|
||||||
|
if (sortDesc) {
|
||||||
|
sortKeyFun = hasSort && function() {
|
||||||
|
const k = Column.sortKeyFun.apply(this, arguments);
|
||||||
|
if (_.isString(k)) {
|
||||||
|
return reverseString("" + k);
|
||||||
}
|
}
|
||||||
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;
|
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>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var ROW_HEIGHT = 32;
|
|
||||||
|
|
||||||
var FlowTable = React.createClass({
|
|
||||||
mixins: [StickyHeadMixin, AutoScrollMixin, VirtualScrollMixin],
|
|
||||||
contextTypes: {
|
|
||||||
view: React.PropTypes.object.isRequired
|
|
||||||
},
|
|
||||||
getInitialState: function () {
|
|
||||||
return {
|
|
||||||
columns: flowtable_columns
|
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
componentWillMount: function () {
|
|
||||||
|
this.props.setSortKeyFun(sortKeyFun);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FlowTable extends React.Component {
|
||||||
|
|
||||||
|
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("add", this.onChange);
|
||||||
this.context.view.addListener("update", this.onChange);
|
this.context.view.addListener("update", this.onChange);
|
||||||
this.context.view.addListener("remove", this.onChange);
|
this.context.view.addListener("remove", this.onChange);
|
||||||
this.context.view.addListener("recalculate", 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("add", this.onChange);
|
||||||
this.context.view.removeListener("update", this.onChange);
|
this.context.view.removeListener("update", this.onChange);
|
||||||
this.context.view.removeListener("remove", this.onChange);
|
this.context.view.removeListener("remove", this.onChange);
|
||||||
this.context.view.removeListener("recalculate", 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}
|
componentDidUpdate() {
|
||||||
ref={flow.id}
|
this.onViewportUpdate();
|
||||||
flow={flow}
|
}
|
||||||
columns={this.state.columns}
|
|
||||||
selected={selected}
|
onViewportUpdate() {
|
||||||
highlighted={highlighted}
|
const viewport = ReactDOM.findDOMNode(this);
|
||||||
selectFlow={this.props.selectFlow}
|
const viewportTop = viewport.scrollTop;
|
||||||
/>;
|
|
||||||
},
|
const vScroll = calcVScroll({
|
||||||
render: function () {
|
viewportTop,
|
||||||
var flows = this.context.view.list;
|
viewportHeight: viewport.offsetHeight,
|
||||||
var rows = this.renderRows(flows);
|
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 (
|
return (
|
||||||
<div className="flow-table" onScroll={this.onScrollFlowTable}>
|
<div className="flow-table" onScroll={this.onViewportUpdate}>
|
||||||
<table>
|
<table>
|
||||||
<FlowTableHead ref="head"
|
<thead ref="head" style={{ transform }}>
|
||||||
columns={this.state.columns}
|
<FlowTableHead
|
||||||
setSortKeyFun={this.props.setSortKeyFun}/>
|
columns={flowtable_columns}
|
||||||
<tbody ref="body">
|
setSortKeyFun={this.props.setSortKeyFun}
|
||||||
{ this.getPlaceholderTop(flows.length) }
|
/>
|
||||||
{rows}
|
</thead>
|
||||||
{ this.getPlaceholderBottom(flows.length) }
|
<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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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