tor-browser

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

Popover.js (8146B)


      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
      3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
      4 
      5 import React, { Component } from "devtools/client/shared/vendor/react";
      6 import { div } from "devtools/client/shared/vendor/react-dom-factories";
      7 import PropTypes from "devtools/client/shared/vendor/react-prop-types";
      8 import BracketArrow from "./BracketArrow";
      9 
     10 const classnames = require("resource://devtools/client/shared/classnames.js");
     11 
     12 class Popover extends Component {
     13  state = {
     14    coords: {
     15      left: 0,
     16      top: 0,
     17      orientation: "down",
     18      targetMid: { x: 0, y: 0 },
     19    },
     20  };
     21  firstRender = true;
     22 
     23  static defaultProps = {
     24    type: "popover",
     25  };
     26 
     27  static get propTypes() {
     28    return {
     29      children: PropTypes.node.isRequired,
     30      editorRef: PropTypes.object.isRequired,
     31      mouseout: PropTypes.func.isRequired,
     32      target: PropTypes.object.isRequired,
     33      targetPosition: PropTypes.object.isRequired,
     34      type: PropTypes.string.isRequired,
     35    };
     36  }
     37 
     38  componentDidMount() {
     39    const { type } = this.props;
     40    this.gapHeight = this.$gap.getBoundingClientRect().height;
     41    const coords =
     42      type == "popover" ? this.getPopoverCoords() : this.getTooltipCoords();
     43 
     44    if (coords) {
     45      this.setState({ coords });
     46    }
     47 
     48    this.firstRender = false;
     49    this.startTimer();
     50  }
     51 
     52  componentDidUpdate(prevProps) {
     53    // We have to update `coords` when the Popover type changes
     54    if (
     55      prevProps.type != this.props.type ||
     56      prevProps.target !== this.props.target
     57    ) {
     58      const coords =
     59        this.props.type == "popover"
     60          ? this.getPopoverCoords()
     61          : this.getTooltipCoords();
     62 
     63      if (coords) {
     64        this.setState({ coords });
     65      }
     66    }
     67  }
     68 
     69  componentWillUnmount() {
     70    if (this.timerId) {
     71      clearTimeout(this.timerId);
     72    }
     73  }
     74 
     75  startTimer() {
     76    this.timerId = setTimeout(this.onTimeout, 0);
     77  }
     78 
     79  onTimeout = () => {
     80    const isHoveredOnGap = this.$gap && this.$gap.matches(":hover");
     81    const isHoveredOnPopover = this.$popover && this.$popover.matches(":hover");
     82    const isHoveredOnTooltip = this.$tooltip && this.$tooltip.matches(":hover");
     83    const isHoveredOnTarget = this.props.target.matches(":hover");
     84 
     85    // Don't clear the current preview if mouse is hovered on:
     86    // - popover or tooltip (depending on the preview type we either have a PopOver or a Tooltip)
     87    // - target, which is the highlighted token in CodeMirror
     88    if (isHoveredOnPopover || isHoveredOnTooltip || isHoveredOnTarget) {
     89      this.timerId = setTimeout(this.onTimeout, 0);
     90      return;
     91    }
     92 
     93    // If we are only hovering the "gap", i.e. the extra space where the arrow pointing
     94    // to the highlighted token is, hide the popup with an extra timeout
     95    if (isHoveredOnGap) {
     96      this.timerId = setTimeout(this.onTimeout, 200);
     97      return;
     98    }
     99 
    100    this.props.mouseout();
    101  };
    102 
    103  calculateLeft(target, editor, popover, orientation) {
    104    const estimatedLeft = target.left;
    105    const estimatedRight = estimatedLeft + popover.width;
    106    const isOverflowingRight = estimatedRight > editor.right;
    107    if (orientation === "right") {
    108      return target.left + target.width;
    109    }
    110    if (isOverflowingRight) {
    111      const adjustedLeft = editor.right - popover.width - 8;
    112      return adjustedLeft;
    113    }
    114    return estimatedLeft;
    115  }
    116 
    117  calculateTopForRightOrientation = (target, editor, popover) => {
    118    if (popover.height <= editor.height) {
    119      const rightOrientationTop = target.top - popover.height / 2;
    120      if (rightOrientationTop < editor.top) {
    121        return editor.top - target.height;
    122      }
    123      const rightOrientationBottom = rightOrientationTop + popover.height;
    124      if (rightOrientationBottom > editor.bottom) {
    125        return editor.bottom + target.height - popover.height + this.gapHeight;
    126      }
    127      return rightOrientationTop;
    128    }
    129    return editor.top - target.height;
    130  };
    131 
    132  calculateOrientation(target, editor, popover) {
    133    const estimatedBottom = target.bottom + popover.height;
    134    if (editor.bottom > estimatedBottom) {
    135      return "down";
    136    }
    137    const upOrientationTop = target.top - popover.height;
    138    if (upOrientationTop > editor.top) {
    139      return "up";
    140    }
    141 
    142    return "right";
    143  }
    144 
    145  calculateTop = (target, editor, popover, orientation) => {
    146    if (orientation === "down") {
    147      return target.bottom;
    148    }
    149    if (orientation === "up") {
    150      return target.top - popover.height;
    151    }
    152 
    153    return this.calculateTopForRightOrientation(target, editor, popover);
    154  };
    155 
    156  getPopoverCoords() {
    157    if (!this.$popover || !this.props.editorRef) {
    158      return null;
    159    }
    160 
    161    const popover = this.$popover;
    162    const editor = this.props.editorRef;
    163    const popoverRect = popover.getBoundingClientRect();
    164    const editorRect = editor.getBoundingClientRect();
    165    const targetRect = this.props.targetPosition;
    166    const orientation = this.calculateOrientation(
    167      targetRect,
    168      editorRect,
    169      popoverRect
    170    );
    171    const top = this.calculateTop(
    172      targetRect,
    173      editorRect,
    174      popoverRect,
    175      orientation
    176    );
    177    const popoverLeft = this.calculateLeft(
    178      targetRect,
    179      editorRect,
    180      popoverRect,
    181      orientation
    182    );
    183    let targetMid;
    184    if (orientation === "right") {
    185      targetMid = {
    186        x: -14,
    187        y: targetRect.top - top - 2,
    188      };
    189    } else {
    190      targetMid = {
    191        x: targetRect.left - popoverLeft + targetRect.width / 2 - 8,
    192        y: 0,
    193      };
    194    }
    195 
    196    return {
    197      left: popoverLeft,
    198      top,
    199      orientation,
    200      targetMid,
    201    };
    202  }
    203 
    204  getTooltipCoords() {
    205    if (!this.$tooltip || !this.props.editorRef) {
    206      return null;
    207    }
    208    const tooltip = this.$tooltip;
    209    const editor = this.props.editorRef;
    210    const tooltipRect = tooltip.getBoundingClientRect();
    211    const editorRect = editor.getBoundingClientRect();
    212    const targetRect = this.props.targetPosition;
    213    const left = this.calculateLeft(targetRect, editorRect, tooltipRect);
    214    const enoughRoomForTooltipAbove =
    215      targetRect.top - editorRect.top > tooltipRect.height;
    216    const top = enoughRoomForTooltipAbove
    217      ? targetRect.top - tooltipRect.height
    218      : targetRect.bottom;
    219 
    220    return {
    221      left,
    222      top,
    223      orientation: enoughRoomForTooltipAbove ? "up" : "down",
    224      targetMid: { x: 0, y: 0 },
    225    };
    226  }
    227 
    228  getChildren() {
    229    const { children } = this.props;
    230    const { coords } = this.state;
    231    const gap = this.getGap();
    232 
    233    return coords.orientation === "up" ? [children, gap] : [gap, children];
    234  }
    235 
    236  getGap() {
    237    return div({
    238      className: "gap",
    239      key: "gap",
    240      ref: a => (this.$gap = a),
    241    });
    242  }
    243 
    244  getPopoverArrow(orientation, left, top) {
    245    let arrowProps = {};
    246 
    247    if (orientation === "up") {
    248      arrowProps = { orientation: "down", bottom: 10, left };
    249    } else if (orientation === "down") {
    250      arrowProps = { orientation: "up", top: -2, left };
    251    } else {
    252      arrowProps = { orientation: "left", top, left: -4 };
    253    }
    254    return React.createElement(BracketArrow, arrowProps);
    255  }
    256 
    257  renderPopover() {
    258    const { top, left, orientation, targetMid } = this.state.coords;
    259    const arrow = this.getPopoverArrow(orientation, targetMid.x, targetMid.y);
    260    return div(
    261      {
    262        className: classnames("popover", `orientation-${orientation}`, {
    263          up: orientation === "up",
    264        }),
    265        style: {
    266          top,
    267          left,
    268        },
    269        ref: c => (this.$popover = c),
    270      },
    271      arrow,
    272      this.getChildren()
    273    );
    274  }
    275 
    276  renderTooltip() {
    277    const { top, left, orientation } = this.state.coords;
    278    return div(
    279      {
    280        className: `tooltip orientation-${orientation}`,
    281        style: {
    282          top,
    283          left,
    284        },
    285        ref: c => (this.$tooltip = c),
    286      },
    287      this.getChildren()
    288    );
    289  }
    290 
    291  render() {
    292    const { type } = this.props;
    293 
    294    if (type === "tooltip") {
    295      return this.renderTooltip();
    296    }
    297 
    298    return this.renderPopover();
    299  }
    300 }
    301 
    302 export default Popover;