Bug 1260552 - Implement Splitter React component; r=jlongster, pbro
authorJan Odvarko <odvarko@gmail.com>
Tue, 06 Sep 2016 19:02:23 +0200
changeset 314498 3f1d76375421676232b7fae1b5ff530786238369
parent 314497 bf78a4c31c1c85b17006883f47282bc6c40fed82
child 314499 dd2b2926a163d5b2a79a547df09d01f51b531862
push id20577
push usercbook@mozilla.com
push dateWed, 21 Sep 2016 06:31:06 +0000
treeherderfx-team@36373fbb7811 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlongster, pbro
bugs1260552
milestone52.0a1
Bug 1260552 - Implement Splitter React component; r=jlongster, pbro
devtools/client/shared/components/moz.build
devtools/client/shared/components/splitter/draggable.js
devtools/client/shared/components/splitter/moz.build
devtools/client/shared/components/splitter/split-box.css
devtools/client/shared/components/splitter/split-box.js
--- a/devtools/client/shared/components/moz.build
+++ b/devtools/client/shared/components/moz.build
@@ -1,16 +1,17 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'reps',
+    'splitter',
     'tabs',
     'tree'
 ]
 
 DevToolsModules(
     'frame.js',
     'h-split-box.js',
     'notification-box.css',
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/splitter/draggable.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { DOM: dom, PropTypes } = React;
+
+const Draggable = React.createClass({
+  displayName: "Draggable",
+
+  propTypes: {
+    onMove: PropTypes.func.isRequired,
+    onStart: PropTypes.func,
+    onStop: PropTypes.func,
+    style: PropTypes.object,
+    className: PropTypes.string
+  },
+
+  startDragging(ev) {
+    ev.preventDefault();
+    const doc = ReactDOM.findDOMNode(this).ownerDocument;
+    doc.addEventListener("mousemove", this.onMove);
+    doc.addEventListener("mouseup", this.onUp);
+    this.props.onStart && this.props.onStart();
+  },
+
+  onMove(ev) {
+    ev.preventDefault();
+    // Use screen coordinates so, moving mouse over iframes
+    // doesn't mangle (relative) coordinates.
+    this.props.onMove(ev.screenX, ev.screenY);
+  },
+
+  onUp(ev) {
+    ev.preventDefault();
+    const doc = ReactDOM.findDOMNode(this).ownerDocument;
+    doc.removeEventListener("mousemove", this.onMove);
+    doc.removeEventListener("mouseup", this.onUp);
+    this.props.onStop && this.props.onStop();
+  },
+
+  render() {
+    return dom.div({
+      style: this.props.style,
+      className: this.props.className,
+      onMouseDown: this.startDragging
+    });
+  }
+});
+
+module.exports = Draggable;
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/splitter/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'draggable.js',
+    'split-box.css',
+    'split-box.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.css
@@ -0,0 +1,88 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.split-box {
+  display: flex;
+  flex: 1;
+  min-width: 0;
+  height: 100%;
+  width: 100%;
+}
+
+.split-box.vert {
+  flex-direction: row;
+}
+
+.split-box.horz {
+  flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+  display: flex;
+  flex: 1;
+  min-width: 0;
+  overflow: auto;
+}
+
+.split-box > .controlled {
+  display: flex;
+  overflow: auto;
+}
+
+.split-box > .splitter {
+  background-image: none;
+  border: 0;
+  border-style: solid;
+  border-color: transparent;
+  background-color: var(--theme-splitter-color);
+  background-clip: content-box;
+  position: relative;
+
+  box-sizing: border-box;
+
+  /* Positive z-index positions the splitter on top of its siblings and makes
+     it clickable on both sides. */
+  z-index: 1;
+}
+
+.split-box.vert > .splitter {
+  min-width: calc(var(--devtools-splitter-inline-start-width) +
+    var(--devtools-splitter-inline-end-width) + 1px);
+
+  border-inline-start-width: var(--devtools-splitter-inline-start-width);
+  border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+  margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+  margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+  cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+  min-height: calc(var(--devtools-splitter-top-width) +
+    var(--devtools-splitter-bottom-width) + 1px);
+
+  border-top-width: var(--devtools-splitter-top-width);
+  border-bottom-width: var(--devtools-splitter-bottom-width);
+
+  margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+  margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+  cursor: ns-resize;
+}
+
+.split-box.disabled {
+  pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+  pointer-events: none;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/splitter/split-box.js
@@ -0,0 +1,207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const Draggable = React.createFactory(require("devtools/client/shared/components/splitter/draggable"));
+const { DOM: dom, PropTypes } = React;
+
+/**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+const SplitBox = React.createClass({
+  displayName: "SplitBox",
+
+  propTypes: {
+    // Custom class name. You can use more names separated by a space.
+    className: PropTypes.string,
+    // Initial size of controlled panel.
+    initialSize: PropTypes.number,
+    // Left/top panel
+    startPanel: PropTypes.any,
+    // Min panel size.
+    minSize: PropTypes.number,
+    // Max panel size.
+    maxSize: PropTypes.number,
+    // Right/bottom panel
+    endPanel: PropTypes.any,
+    // True if the right/bottom panel should be controlled.
+    endPanelControl: PropTypes.bool,
+    // Size of the splitter handle bar.
+    splitterSize: PropTypes.number,
+    // True if the splitter bar is vertical (default is vertical).
+    vert: PropTypes.bool
+  },
+
+  getDefaultProps() {
+    return {
+      splitterSize: 5,
+      vert: true,
+      endPanelControl: false
+    };
+  },
+
+  /**
+   * The state stores the current orientation (vertical or horizontal)
+   * and the current size (width/height). All these values can change
+   * during the component's life time.
+   */
+  getInitialState() {
+    return {
+      vert: this.props.vert,
+      width: this.props.initialWidth || this.props.initialSize,
+      height: this.props.initialHeight || this.props.initialSize
+    };
+  },
+
+  // Dragging Events
+
+  /**
+   * Set 'resizing' cursor on entire document during splitter dragging.
+   * This avoids cursor-flickering that happens when the mouse leaves
+   * the splitter bar area (happens frequently).
+   */
+  onStartMove() {
+    const splitBox = ReactDOM.findDOMNode(this);
+    const doc = splitBox.ownerDocument;
+    let defaultCursor = doc.documentElement.style.cursor;
+    doc.documentElement.style.cursor = (this.state.vert ? "ew-resize" : "ns-resize");
+
+    splitBox.classList.add("dragging");
+
+    this.setState({
+      defaultCursor: defaultCursor
+    });
+  },
+
+  onStopMove() {
+    const splitBox = ReactDOM.findDOMNode(this);
+    const doc = splitBox.ownerDocument;
+    doc.documentElement.style.cursor = this.state.defaultCursor;
+
+    splitBox.classList.remove("dragging");
+  },
+
+  /**
+   * Adjust size of the controlled panel. Depending on the current
+   * orientation we either remember the width or height of
+   * the splitter box.
+   */
+  onMove(x, y) {
+    const node = ReactDOM.findDOMNode(this);
+    const doc = node.ownerDocument;
+    const win = doc.defaultView;
+
+    let size;
+    let { endPanelControl } = this.props;
+
+    if (this.state.vert) {
+      // Switch the control flag in case of RTL. Note that RTL
+      // has impact on vertical splitter only.
+      let dir = win.getComputedStyle(doc.documentElement).direction;
+      if (dir == "rtl") {
+        endPanelControl = !endPanelControl;
+      }
+
+      let innerOffset = x - win.mozInnerScreenX;
+      size = endPanelControl ?
+        (node.offsetLeft + node.offsetWidth) - innerOffset :
+        innerOffset - node.offsetLeft;
+
+      this.setState({
+        width: size
+      });
+    } else {
+      let innerOffset = y - win.mozInnerScreenY;
+      size = endPanelControl ?
+        (node.offsetTop + node.offsetHeight) - innerOffset :
+        innerOffset - node.offsetTop;
+
+      this.setState({
+        height: size
+      });
+    }
+  },
+
+  // Rendering
+
+  render() {
+    const vert = this.state.vert;
+    const { startPanel, endPanel, endPanelControl, minSize,
+      maxSize, splitterSize } = this.props;
+
+    let style = Object.assign({}, this.props.style);
+
+    // Calculate class names list.
+    let classNames = ["split-box"];
+    classNames.push(vert ? "vert" : "horz");
+    if (this.props.className) {
+      classNames = classNames.concat(this.props.className.split(" "));
+    }
+
+    let leftPanelStyle;
+    let rightPanelStyle;
+
+    // Set proper size for panels depending on the current state.
+    if (vert) {
+      leftPanelStyle = {
+        maxWidth: endPanelControl ? null : maxSize,
+        minWidth: endPanelControl ? null : minSize,
+        width: endPanelControl ? null : this.state.width
+      };
+      rightPanelStyle = {
+        maxWidth: endPanelControl ? maxSize : null,
+        minWidth: endPanelControl ? minSize : null,
+        width: endPanelControl ? this.state.width : null
+      };
+    } else {
+      leftPanelStyle = {
+        maxHeight: endPanelControl ? null : maxSize,
+        minHeight: endPanelControl ? null : minSize,
+        height: endPanelControl ? null : this.state.height
+      };
+      rightPanelStyle = {
+        maxHeight: endPanelControl ? maxSize : null,
+        minHeight: endPanelControl ? minSize : null,
+        height: endPanelControl ? this.state.height : null
+      };
+    }
+
+    // Calculate splitter size
+    let splitterStyle = {
+      flex: "0 0 " + splitterSize + "px"
+    };
+
+    return (
+      dom.div({
+        className: classNames.join(" "),
+        style: style },
+        startPanel ?
+          dom.div({
+            className: endPanelControl ? "uncontrolled" : "controlled",
+            style: leftPanelStyle},
+            startPanel
+          ) : null,
+        Draggable({
+          className: "splitter",
+          style: splitterStyle,
+          onStart: this.onStartMove,
+          onStop: this.onStopMove,
+          onMove: this.onMove
+        }),
+        endPanel ?
+          dom.div({
+            className: endPanelControl ? "controlled" : "uncontrolled",
+            style: rightPanelStyle},
+            endPanel
+          ) : null
+      )
+    );
+  }
+});
+
+module.exports = SplitBox;