Bug 1260552 - Implement Splitter React component. r=jlongster, r=pbro
☠☠ backed out by 8d211b6a94f6 ☠ ☠
authorJan Odvarko <odvarko@gmail.com>
Thu, 15 Sep 2016 09:54:01 +0200
changeset 314162 e7da6eac37d3e0c3b2d7c07a3b91ff6f3bcb9bda
parent 314161 7d70ac7d03de8f0d2097b8681e811d38545287fa
child 314163 e8de82ab570d878051233aef77271e693d362c78
push id30709
push usercbook@mozilla.com
push dateFri, 16 Sep 2016 10:53:07 +0000
treeherdermozilla-central@23cd4d6ba00c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjlongster, pbro
bugs1260552
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1260552 - Implement Splitter React component. r=jlongster, r=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;