aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorJason <jason.daurus@gmail.com>2016-03-10 21:40:07 +0800
committerJason <jason.daurus@gmail.com>2016-03-10 21:40:07 +0800
commit1c0496e051d8b1af297138732475b1689ada5eb8 (patch)
tree7a2134ac48cce29694313e5a5b3105f2cf7e93ff /web
parentb413a052f96896d387f58e903203cf2e29c6f1f6 (diff)
downloadmitmproxy-1c0496e051d8b1af297138732475b1689ada5eb8.tar.gz
mitmproxy-1c0496e051d8b1af297138732475b1689ada5eb8.tar.bz2
mitmproxy-1c0496e051d8b1af297138732475b1689ada5eb8.zip
[web] VirtualScroll and AutoScroll helper
Diffstat (limited to 'web')
-rw-r--r--web/.babelrc3
-rw-r--r--web/.eslintrc.yml8
-rw-r--r--web/package.json6
-rw-r--r--web/src/js/components/common.js28
-rw-r--r--web/src/js/components/eventlog.js230
-rw-r--r--web/src/js/components/flowtable.js340
-rw-r--r--web/src/js/components/helpers/AutoScroll.js25
-rw-r--r--web/src/js/components/helpers/VirtualScroll.js70
-rw-r--r--web/src/js/components/virtualscroll.js84
9 files changed, 419 insertions, 375 deletions
diff --git a/web/.babelrc b/web/.babelrc
index facd1809..5dd7708c 100644
--- a/web/.babelrc
+++ b/web/.babelrc
@@ -1,3 +1,4 @@
{
- "presets": ["es2015", "react"]
+ "presets": ["es2015", "react"],
+ "plugins": ["transform-class-properties"]
} \ No newline at end of file
diff --git a/web/.eslintrc.yml b/web/.eslintrc.yml
index 319fa67c..60028c97 100644
--- a/web/.eslintrc.yml
+++ b/web/.eslintrc.yml
@@ -1,9 +1,3 @@
{
- "parserOptions": {
- "ecmaVersion": 6,
- "sourceType": "module",
- "ecmaFeatures": {
- "jsx": true
- }
- }
+ "parser": "babel-eslint"
} \ No newline at end of file
diff --git a/web/package.json b/web/package.json
index 63a664ae..e489a0b1 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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",
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
index 23e0a434..ad97ab38 100644
--- a/web/src/js/components/common.js
+++ b/web/src/js/components/common.js
@@ -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,
diff --git a/web/src/js/components/eventlog.js b/web/src/js/components/eventlog.js
index 51f1578e..d1b23ace 100644
--- a/web/src/js/components/eventlog.js
+++ b/web/src/js/components/eventlog.js
@@ -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; \ No newline at end of file
+export default EventLog;
diff --git a/web/src/js/components/flowtable.js b/web/src/js/components/flowtable.js
index 988d1895..f03b8ec0 100644
--- a/web/src/js/components/flowtable.js
+++ b/web/src/js/components/flowtable.js
@@ -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
+ }
+
+ 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={onClick}
- className={className} />;
- }.bind(this));
- return <thead>
- <tr>{columns}</tr>
- </thead>;
+ 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);
}
-});
-
-
-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 () {
+
+ 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);
diff --git a/web/src/js/components/helpers/AutoScroll.js b/web/src/js/components/helpers/AutoScroll.js
new file mode 100644
index 00000000..d37b9f37
--- /dev/null
+++ b/web/src/js/components/helpers/AutoScroll.js
@@ -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);
diff --git a/web/src/js/components/helpers/VirtualScroll.js b/web/src/js/components/helpers/VirtualScroll.js
new file mode 100644
index 00000000..5d4cf796
--- /dev/null
+++ b/web/src/js/components/helpers/VirtualScroll.js
@@ -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 };
+}
diff --git a/web/src/js/components/virtualscroll.js b/web/src/js/components/virtualscroll.js
deleted file mode 100644
index f462fdcc..00000000
--- a/web/src/js/components/virtualscroll.js
+++ /dev/null
@@ -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;
- }
- },
-};