text: /**
   * Copyright (c) 2017-present, Facebook, Inc.
   * All rights reserved.
   *
   * This source code is licensed under the BSD-style license found in the
   * LICENSE file in the root directory of this source tree. An additional grant
   * of patent rights can be found in the PATENTS file in the same directory.
   *
   * @flow
   * @format
   */
  
  import type {Datatip, PinnedDatatipPosition} from './types';
  
  type Position = {
    x: number,
    y: number,
  };
  
  import * as React from 'react';
  import ReactDOM from 'react-dom';
  import {Observable} from 'rxjs';
  import invariant from 'assert';
  import classnames from 'classnames';
  import UniversalDisposable from 'nuclide-commons/UniversalDisposable';
  
  import {DatatipComponent, DATATIP_ACTIONS} from './DatatipComponent';
  import isScrollable from './isScrollable';
  
  const LINE_END_MARGIN = 20;
  
  let _mouseMove$;
  function documentMouseMove$(): Observable<MouseEvent> {
    if (_mouseMove$ == null) {
      _mouseMove$ = Observable.fromEvent(document, 'mousemove');
    }
    return _mouseMove$;
  }
  
  let _mouseUp$;
  function documentMouseUp$(): Observable<MouseEvent> {
    if (_mouseUp$ == null) {
      _mouseUp$ = Observable.fromEvent(document, 'mouseup');
    }
    return _mouseUp$;
  }
  
  export type PinnedDatatipParams = {
    onDispose: (pinnedDatatip: PinnedDatatip) => void,
    hideDataTips: () => void,
    // Defaults to 'end-of-line'.
    position?: PinnedDatatipPosition,
    // Defaults to true.
    showRangeHighlight?: boolean,
  };
  
  export class PinnedDatatip {
    _boundDispose: Function;
    _boundHandleMouseDown: Function;
    _boundHandleMouseEnter: Function;
    _boundHandleMouseLeave: Function;
    _boundHandleCapturedClick: Function;
    _mouseUpTimeout: ?TimeoutID;
    _hostElement: HTMLElement;
    _marker: ?atom$Marker;
    _rangeDecoration: ?atom$Decoration;
    _mouseSubscription: ?rxjs$ISubscription;
    _subscriptions: UniversalDisposable;
    _datatip: Datatip;
    _editor: TextEditor;
    _hostElement: HTMLElement;
    _boundDispose: Function;
    _dragOrigin: ?Position;
    _isDragging: boolean;
    _offset: Position;
    _isHovering: boolean;
    _checkedScrollable: boolean;
    _isScrollable: boolean;
    _hideDataTips: () => void;
    _position: PinnedDatatipPosition;
    _showRangeHighlight: boolean;
  
    constructor(
      datatip: Datatip,
      editor: TextEditor,
      params: PinnedDatatipParams,
    ) {
      this._subscriptions = new UniversalDisposable();
      this._subscriptions.add(
        new UniversalDisposable(() => params.onDispose(this)),
      );
      this._datatip = datatip;
      this._editor = editor;
      this._marker = null;
      this._rangeDecoration = null;
      this._hostElement = document.createElement('div');
      this._hostElement.className = 'datatip-overlay';
      this._boundDispose = this.dispose.bind(this);
      this._boundHandleMouseDown = this.handleMouseDown.bind(this);
      this._boundHandleMouseEnter = this.handleMouseEnter.bind(this);
      this._boundHandleMouseLeave = this.handleMouseLeave.bind(this);
      this._boundHandleCapturedClick = this.handleCapturedClick.bind(this);
      this._checkedScrollable = false;
      this._isScrollable = false;
  
      this._subscriptions.add(
        Observable.fromEvent(this._hostElement, 'wheel').subscribe(e => {
          if (!this._checkedScrollable) {
            this._isScrollable = isScrollable(this._hostElement, e);
            this._checkedScrollable = true;
          }
          if (this._isScrollable) {
            e.stopPropagation();
          }
        }),
      );
      this._hostElement.addEventListener(
        'mouseenter',
        this._boundHandleMouseEnter,
      );
      this._hostElement.addEventListener(
        'mouseleave',
        this._boundHandleMouseLeave,
      );
      this._subscriptions.add(
        new UniversalDisposable(() => {
          this._hostElement.removeEventListener(
            'mouseenter',
            this._boundHandleMouseEnter,
          );
          this._hostElement.removeEventListener(
            'mouseleave',
            this._boundHandleMouseLeave,
          );
        }),
      );
      this._mouseUpTimeout = null;
      this._offset = {x: 0, y: 0};
      this._isDragging = false;
      this._dragOrigin = null;
      this._isHovering = false;
      this._hideDataTips = params.hideDataTips;
      this._position = params.position == null ? 'end-of-line' : params.position;
      this._showRangeHighlight =
        params.showRangeHighlight == null ? true : params.showRangeHighlight;
      this.render();
    }
  
    handleMouseEnter(event: MouseEvent): void {
      this._isHovering = true;
      this._hideDataTips();
    }
  
    handleMouseLeave(event: MouseEvent): void {
      this._isHovering = false;
    }
  
    isHovering(): boolean {
      return this._isHovering;
    }
  
    handleGlobalMouseMove(event: Event): void {
      const evt: MouseEvent = (event: any);
      const {_dragOrigin} = this;
      invariant(_dragOrigin);
      this._isDragging = true;
      this._offset = {
        x: evt.clientX - _dragOrigin.x,
        y: evt.clientY - _dragOrigin.y,
      };
      this.render();
    }
  
    handleGlobalMouseUp(): void {
      // If the datatip was moved, push the effects of mouseUp to the next tick,
      // in order to allow cancellation of captured events (e.g. clicks on child components).
      this._mouseUpTimeout = setTimeout(() => {
        this._isDragging = false;
        this._dragOrigin = null;
        this._mouseUpTimeout = null;
        this._ensureMouseSubscriptionDisposed();
        this.render();
      }, 0);
    }
  
    _ensureMouseSubscriptionDisposed(): void {
      if (this._mouseSubscription != null) {
        this._mouseSubscription.unsubscribe();
        this._mouseSubscription = null;
      }
    }
  
    handleMouseDown(event: Event): void {
      const evt: MouseEvent = (event: any);
      this._dragOrigin = {
        x: evt.clientX - this._offset.x,
        y: evt.clientY - this._offset.y,
      };
      this._ensureMouseSubscriptionDisposed();
      this._mouseSubscription = documentMouseMove$()
        .takeUntil(documentMouseUp$())
        .subscribe(
          (e: MouseEvent) => {
            this.handleGlobalMouseMove(e);
          },
          (error: any) => {},
          () => {
            this.handleGlobalMouseUp();
          },
        );
    }
  
    handleCapturedClick(event: SyntheticEvent<>): void {
      if (this._isDragging) {
        event.stopPropagation();
      } else {
        // Have to re-check scrolling because the datatip size may have changed.
        this._checkedScrollable = false;
      }
    }
  
    // Update the position of the pinned datatip.
    _updateHostElementPosition(): void {
      const {_editor, _datatip, _hostElement, _offset, _position} = this;
      const {range} = _datatip;
      _hostElement.style.display = 'block';
      switch (_position) {
        case 'end-of-line':
          const charWidth = _editor.getDefaultCharWidth();
          const lineLength = _editor.getBuffer().getLines()[range.start.row]
            .length;
          _hostElement.style.top =
            -_editor.getLineHeightInPixels() + _offset.y + 'px';
          _hostElement.style.left =
            (lineLength - range.end.column) * charWidth +
            LINE_END_MARGIN +
            _offset.x +
            'px';
          break;
        case 'above-range':
          _hostElement.style.bottom =
            _editor.getLineHeightInPixels() +
            _hostElement.clientHeight -
            _offset.y +
            'px';
          _hostElement.style.left = _offset.x + 'px';
          break;
        default:
          (_position: empty);
          throw new Error(`Unexpected PinnedDatatip position: ${this._position}`);
      }
    }
  
    async render(): Promise<void> {
      const {_editor, _datatip, _hostElement, _isDragging, _isHovering} = this;
  
      let rangeClassname = 'datatip-highlight-region';
      if (_isHovering) {
        rangeClassname += ' datatip-highlight-region-active';
      }
  
      if (this._marker == null) {
        const marker: atom$Marker = _editor.markBufferRange(_datatip.range, {
          invalidate: 'never',
        });
        this._marker = marker;
        _editor.decorateMarker(marker, {
          type: 'overlay',
          position: 'head',
          item: this._hostElement,
          // above-range datatips currently assume that the overlay is below.
          avoidOverflow: this._position !== 'above-range',
        });
        if (this._showRangeHighlight) {
          this._rangeDecoration = _editor.decorateMarker(marker, {
            type: 'highlight',
            class: rangeClassname,
          });
        }
        await _editor.getElement().getNextUpdatePromise();
        // Guard against disposals during the await.
        if (marker.isDestroyed() || _editor.isDestroyed()) {
          return;
        }
      } else if (this._rangeDecoration != null) {
        this._rangeDecoration.setProperties({
          type: 'highlight',
          class: rangeClassname,
        });
      }
  
      ReactDOM.render(
        <DatatipComponent
          action={DATATIP_ACTIONS.CLOSE}
          actionTitle="Close this datatip"
          className={classnames(
            _isDragging ? 'datatip-dragging' : '',
            'datatip-pinned',
          )}
          datatip={_datatip}
          onActionClick={this._boundDispose}
          onMouseDown={this._boundHandleMouseDown}
          onClickCapture={this._boundHandleCapturedClick}
        />,
        _hostElement,
      );
      this._updateHostElementPosition();
    }
  
    dispose(): void {
      if (this._mouseUpTimeout != null) {
        clearTimeout(this._mouseUpTimeout);
      }
      if (this._marker != null) {
        this._marker.destroy();
      }
      if (this._mouseSubscription != null) {
        this._mouseSubscription.unsubscribe();
      }
      ReactDOM.unmountComponentAtNode(this._hostElement);
      this._hostElement.remove();
      this._subscriptions.dispose();
    }
  }
  