+
-
-
- { this.getPlaceholderTop(flows.length) }
- {rows}
- { this.getPlaceholderBottom(flows.length) }
+
+
+
+
+
+ {flows.map(flow => (
+
+ ))}
+
);
}
-});
+}
-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 000000000..d37b9f374
--- /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 000000000..5d4cf796e
--- /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} [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 f462fdccf..000000000
--- 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 =
;
-
- if (this.state.start % 2 === 1) {
- // fix even/odd rows
- return [spacer,
];
- } 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
;
- },
- 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;
- }
- },
-};