tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

SplitBox.js (11657B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const {
      8  Component,
      9  createFactory,
     10 } = require("resource://devtools/client/shared/vendor/react.mjs");
     11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     13 
     14 const Draggable = createFactory(
     15  require("resource://devtools/client/shared/components/splitter/Draggable.js")
     16 );
     17 
     18 /**
     19 * This component represents a Splitter. The splitter supports vertical
     20 * as well as horizontal mode.
     21 */
     22 class SplitBox extends Component {
     23  static get propTypes() {
     24    return {
     25      // Custom class name. You can use more names separated by a space.
     26      className: PropTypes.string,
     27      // Initial size of controlled panel.
     28      initialSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
     29      // Initial width of controlled panel.
     30      initialWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
     31      // Initial height of controlled panel.
     32      initialHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
     33      // Left/top panel
     34      startPanel: PropTypes.any,
     35      // Left/top panel collapse state.
     36      startPanelCollapsed: PropTypes.bool,
     37      // Min panel size.
     38      minSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
     39      // Max panel size.
     40      maxSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
     41      // Right/bottom panel
     42      endPanel: PropTypes.any,
     43      // Right/bottom panel collapse state.
     44      endPanelCollapsed: PropTypes.bool,
     45      // True if the right/bottom panel should be controlled.
     46      endPanelControl: PropTypes.bool,
     47      // Size of the splitter handle bar.
     48      splitterSize: PropTypes.number,
     49      // True if the splitter bar is vertical (default is vertical).
     50      vert: PropTypes.bool,
     51      // Style object.
     52      style: PropTypes.object,
     53      // Call when controlled panel was resized.
     54      onControlledPanelResized: PropTypes.func,
     55      // Optional callback when splitbox resize stops
     56      onResizeEnd: PropTypes.func,
     57      // Retrieve DOM reference to the start panel element
     58      onSelectContainerElement: PropTypes.any,
     59    };
     60  }
     61 
     62  static get defaultProps() {
     63    return {
     64      splitterSize: 5,
     65      vert: true,
     66      endPanelControl: false,
     67    };
     68  }
     69 
     70  static getDerivedStateFromProps(props, state) {
     71    if (
     72      props.endPanelControl === state.prevEndPanelControl &&
     73      props.splitterSize === state.prevSplitterSize &&
     74      props.vert === state.prevVert
     75    ) {
     76      return null;
     77    }
     78 
     79    const newState = {};
     80    if (props.endPanelControl !== state.prevEndPanelControl) {
     81      newState.endPanelControl = props.endPanelControl;
     82      newState.prevEndPanelControl = props.endPanelControl;
     83    }
     84    if (props.splitterSize !== state.prevSplitterSize) {
     85      newState.splitterSize = props.splitterSize;
     86      newState.prevSplitterSize = props.splitterSize;
     87    }
     88    if (props.vert !== state.prevVert) {
     89      newState.vert = props.vert;
     90      newState.prevVert = props.vert;
     91    }
     92 
     93    return newState;
     94  }
     95 
     96  constructor(props) {
     97    super(props);
     98 
     99    /**
    100     * The state stores whether or not the end panel should be controlled, the current
    101     * orientation (vertical or horizontal), the splitter size, and the current size
    102     * (width/height). All these values can change during the component's life time.
    103     */
    104    this.state = {
    105      // True if the right/bottom panel should be controlled.
    106      endPanelControl: props.endPanelControl,
    107      // True if the splitter bar is vertical (default is vertical).
    108      vert: props.vert,
    109      // Size of the splitter handle bar.
    110      splitterSize: props.splitterSize,
    111      // The state for above 3 properties are derived from props, but also managed by the component itself.
    112      // SplitBox manages it's own state but sometimes the parent will pass in new props which will
    113      // override the current state of the component. So we need track the prev value of these props so that
    114      // compare them to the props change and derive new state whenever these 3 props change.
    115      prevEndPanelControl: props.endPanelControl,
    116      prevVert: props.vert,
    117      prevSplitterSize: props.splitterSize,
    118      // Width of controlled panel.
    119      width: props.initialWidth || props.initialSize,
    120      // Height of controlled panel.
    121      height: props.initialHeight || props.initialSize,
    122    };
    123 
    124    this.onStartMove = this.onStartMove.bind(this);
    125    this.onStopMove = this.onStopMove.bind(this);
    126    this.onMove = this.onMove.bind(this);
    127  }
    128 
    129  shouldComponentUpdate(nextProps, nextState) {
    130    return (
    131      nextState.width != this.state.width ||
    132      nextState.endPanelControl != this.props.endPanelControl ||
    133      nextState.height != this.state.height ||
    134      nextState.vert != this.state.vert ||
    135      nextState.splitterSize != this.state.splitterSize ||
    136      nextProps.startPanel != this.props.startPanel ||
    137      nextProps.endPanel != this.props.endPanel ||
    138      nextProps.minSize != this.props.minSize ||
    139      nextProps.maxSize != this.props.maxSize
    140    );
    141  }
    142 
    143  componentDidUpdate(prevProps, prevState) {
    144    if (
    145      this.props.onControlledPanelResized &&
    146      (prevState.width !== this.state.width ||
    147        prevState.height !== this.state.height)
    148    ) {
    149      this.props.onControlledPanelResized(this.state.width, this.state.height);
    150    }
    151  }
    152 
    153  // Dragging Events
    154 
    155  /**
    156   * Set 'resizing' cursor on entire document during splitter dragging.
    157   * This avoids cursor-flickering that happens when the mouse leaves
    158   * the splitter bar area (happens frequently).
    159   */
    160  onStartMove() {
    161    const doc = this.splitBox.ownerDocument;
    162    const defaultCursor = doc.documentElement.style.cursor;
    163    doc.documentElement.style.cursor = this.state.vert
    164      ? "ew-resize"
    165      : "ns-resize";
    166 
    167    this.splitBox.classList.add("dragging");
    168 
    169    this.setState({
    170      defaultCursor,
    171    });
    172  }
    173 
    174  onStopMove() {
    175    const doc = this.splitBox.ownerDocument;
    176    doc.documentElement.style.cursor = this.state.defaultCursor;
    177 
    178    this.splitBox.classList.remove("dragging");
    179 
    180    if (this.props.onResizeEnd) {
    181      this.props.onResizeEnd(
    182        this.state.vert ? this.state.width : this.state.height
    183      );
    184    }
    185  }
    186 
    187  /**
    188   * Adjust size of the controlled panel. Depending on the current
    189   * orientation we either remember the width or height of
    190   * the splitter box.
    191   */
    192  onMove(x, y) {
    193    const nodeBounds = this.splitBox.getBoundingClientRect();
    194 
    195    let size;
    196    let { endPanelControl, vert } = this.state;
    197 
    198    if (vert) {
    199      // Use the document owning the SplitBox to detect rtl. The global document might be
    200      // the one bound to the toolbox shared BrowserRequire, which is irrelevant here.
    201      const doc = this.splitBox.ownerDocument;
    202 
    203      // Switch the control flag in case of RTL. Note that RTL
    204      // has impact on vertical splitter only.
    205      if (doc.dir === "rtl") {
    206        endPanelControl = !endPanelControl;
    207      }
    208 
    209      size = endPanelControl
    210        ? nodeBounds.left + nodeBounds.width - x
    211        : x - nodeBounds.left;
    212 
    213      this.setState({
    214        width: this.getConstrainedSizeInPx(size, nodeBounds.width),
    215      });
    216    } else {
    217      size = endPanelControl
    218        ? nodeBounds.top + nodeBounds.height - y
    219        : y - nodeBounds.top;
    220 
    221      this.setState({
    222        height: this.getConstrainedSizeInPx(size, nodeBounds.height),
    223      });
    224    }
    225  }
    226 
    227  /**
    228   * Calculates the constrained size taking into account the minimum width or
    229   * height passed via this.props.minSize.
    230   *
    231   * @param {number} requestedSize
    232   *        The requested size
    233   * @param {number} splitBoxWidthOrHeight
    234   *        The width or height of the splitBox
    235   *
    236   * @return {number}
    237   *         The constrained size
    238   */
    239  getConstrainedSizeInPx(requestedSize, splitBoxWidthOrHeight) {
    240    let minSize = this.props.minSize + "";
    241 
    242    if (minSize.endsWith("%")) {
    243      minSize = (parseFloat(minSize) / 100) * splitBoxWidthOrHeight;
    244    } else if (minSize.endsWith("px")) {
    245      minSize = parseFloat(minSize);
    246    }
    247    return Math.max(requestedSize, minSize);
    248  }
    249 
    250  // Rendering
    251 
    252  // eslint-disable-next-line complexity
    253  render() {
    254    const { endPanelControl, splitterSize, vert } = this.state;
    255    const {
    256      startPanel,
    257      startPanelCollapsed,
    258      endPanel,
    259      endPanelCollapsed,
    260      minSize,
    261      maxSize,
    262      onSelectContainerElement,
    263    } = this.props;
    264 
    265    const style = Object.assign(
    266      {
    267        // Set the size of the controlled panel (height or width depending on the
    268        // current state). This can be used to help with styling of dependent
    269        // panels.
    270        "--split-box-controlled-panel-size": `${
    271          vert ? this.state.width : this.state.height
    272        }`,
    273      },
    274      this.props.style
    275    );
    276 
    277    // Calculate class names list.
    278    let classNames = ["split-box"];
    279    classNames.push(vert ? "vert" : "horz");
    280    if (this.props.className) {
    281      classNames = classNames.concat(this.props.className.split(" "));
    282    }
    283 
    284    let leftPanelStyle;
    285    let rightPanelStyle;
    286 
    287    // Set proper size for panels depending on the current state.
    288    if (vert) {
    289      leftPanelStyle = {
    290        maxWidth: endPanelControl ? null : maxSize,
    291        minWidth: endPanelControl ? null : minSize,
    292        width: endPanelControl ? null : this.state.width,
    293      };
    294      rightPanelStyle = {
    295        maxWidth: endPanelControl ? maxSize : null,
    296        minWidth: endPanelControl ? minSize : null,
    297        width: endPanelControl ? this.state.width : null,
    298      };
    299    } else {
    300      leftPanelStyle = {
    301        maxHeight: endPanelControl ? null : maxSize,
    302        minHeight: endPanelControl ? null : minSize,
    303        height: endPanelControl ? null : this.state.height,
    304      };
    305      rightPanelStyle = {
    306        maxHeight: endPanelControl ? maxSize : null,
    307        minHeight: endPanelControl ? minSize : null,
    308        height: endPanelControl ? this.state.height : null,
    309      };
    310    }
    311 
    312    // Calculate splitter size
    313    const splitterStyle = {
    314      flex: "0 0 " + splitterSize + "px",
    315    };
    316 
    317    return dom.div(
    318      {
    319        className: classNames.join(" "),
    320        ref: div => {
    321          this.splitBox = div;
    322        },
    323        style,
    324      },
    325      startPanel && !startPanelCollapsed
    326        ? dom.div(
    327            {
    328              className: endPanelControl ? "uncontrolled" : "controlled",
    329              style: leftPanelStyle,
    330              role: "presentation",
    331              ref: div => {
    332                this.startPanelContainer = div;
    333                if (onSelectContainerElement) {
    334                  onSelectContainerElement(div);
    335                }
    336              },
    337            },
    338            startPanel
    339          )
    340        : null,
    341      splitterSize > 0
    342        ? Draggable({
    343            className: "splitter",
    344            style: splitterStyle,
    345            onStart: this.onStartMove,
    346            onStop: this.onStopMove,
    347            onMove: this.onMove,
    348          })
    349        : null,
    350      endPanel && !endPanelCollapsed
    351        ? dom.div(
    352            {
    353              className: endPanelControl ? "controlled" : "uncontrolled",
    354              style: rightPanelStyle,
    355              role: "presentation",
    356              ref: div => {
    357                this.endPanelContainer = div;
    358              },
    359            },
    360            endPanel
    361          )
    362        : null
    363    );
    364  }
    365 }
    366 
    367 module.exports = SplitBox;