All files / store withPositionStore.js

94.83% Statements 55/58
93.48% Branches 43/46
100% Functions 11/11
94.83% Lines 55/58
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192                                                                                                  45x 45x         45x 45x       9x       10x                 60x     60x 60x     60x   60x 56x 56x       60x   3x     3x         57x 330x   57x 12x                       15x 15x     15x 15x 10x 2x     13x 11x 1x     12x                 66x   66x 30x 30x   36x 32x 17x 17x 17x 17x     17x   32x 15x 15x 15x 15x     15x           2x       54x 45x 45x   54x               22x 22x       22x      
/**
* Copyright 2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
 
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import createRef from 'create-react-ref/lib/createRef';
import {
  forOwn,
  pick,
} from 'lodash-es';
 
import assert from '../assert';
 
/**
 * Injects the position store functionality in the requiring components.
 * This won't trigger state updates to prevent React Tree calculcuation at the utmost cost.
 *
 * @param {Object} Component - class to inject the position store into
 * @param {Object} Configuration - which parts of the position store to check for smart rerendering
 *
 * Select from:
 * - `withY` (`yPosOffset`, `currentViewSequence`)
 * - `withX` (`xPosOffset`, `currentViewSequencePosition`)
 *
 * Multiple selections are allowed.
 *
 * It will pass the following functionality properties:
 *
 * (a) `position` (current state of the position store)
 * WARNING: this gets updated in-place to avoid react rerenders
 *
 * (b) `positionDispatch` (dispatch method for the position store)
 *
 * Furthermore,
 *
 * (1) ff a component implements `updateScrollPosition`, it will be called after
 * every store update. Otherwise a default implementation will be used.
 *
 * (2) If a component implements `shouldRerender(newPosition)`, it will be called after
 * every store update. Otherwise a default implementation will be used.
 */
function withPositionConsumer(Component, {withX = false, withY = false} = {}) {
  class MSAPositionConsumer extends PureComponent {
    constructor(props) {
      super(props);
      this.el = createRef();
    }
 
    componentDidMount() {
      // update to all updates from the position store
      this.unsubscribe = this.context.positionMSAStore.subscribe(this.updateFromPositionStore);
      this.updateScrollPosition(true);
    }
 
    componentDidUpdate(){
      this.updateScrollPosition();
    }
 
    componentWillUnmount() {
      this.unsubscribe();
    }
 
    /**
     * a method which updates this.position from the PositionStore
     * when `shouldRerender` returns true, calls `setState({position: positionState})` is called
     * always calls `updateScrollPosition`
     */
    updateFromPositionStore = () => {
      assert(this.context && this.context.positionMSAStore,
        "MSA PositionStore needs to be injected"
      );
      const state = this.context.positionMSAStore.getState();
      this.position = this.position || {};
 
      // create new position object to compare it with the previous
      const newPosition = pick(state, ["currentViewSequence",
        "currentViewSequencePosition", "xPosOffset", "yPosOffset"]);
      if (state.position) {
        newPosition.xPos = state.position.xPos;
        newPosition.yPos = state.position.yPos;
      }
 
      // not called on the first render
      if (this.el.current && this.shouldRerender(newPosition)) {
        // this will always force a rerender as position is a new object
        this.position = newPosition;
        // it doesn't matter what state we set here, this is just to force
        // React to rerender
        this.setState({
          position: this.position,
        });
      } else {
        // copy over new position
        forOwn(newPosition, (v, k) => {
          this.position[k] = v;
        });
        if (this.el.current) {
          this.updateScrollPosition();
        }
      }
    }
 
    /**
     * If the child defines this method, it will be called.
     * Otherwiese
     * - determine if the current viewpoint still has enough nodes
     * - checks the respective viewports when `withX` or `withY` have been set
     */
    shouldRerender = (newPosition) => {
      const it = this.el.current;
      Iif (it.shouldRerender !== undefined) {
        return it.shouldRerender(newPosition);
      }
      const cacheElements = it.props.cacheElements;
      if (withY) {
        if (Math.abs(newPosition.currentViewSequence - this.position.lastCurrentViewSequence) >= cacheElements) {
          return true;
        }
      }
      if (withX) {
        if (Math.abs(newPosition.currentViewSequencePosition - this.position.lastCurrentViewSequencePosition) >= cacheElements) {
          return true;
        }
      }
      return false;
    }
 
 
    /**
     * If the child defines this method, it will be called.
     * Otherwise the default implementation will be used which sets `this.el.current.scroll{Left,Top}` (depending on with{X,Y})
     */
    updateScrollPosition = () => {
      const it = this.el.current;
      // be careful - might be a shallow render
      if (it && it.updateScrollPosition !== undefined) {
        it.updateScrollPosition();
        return;
      }
      if (it && it.el && it.el.current) {
        if (withX) {
          const tileWidth = it.props.tileWidth;
          let offsetX = -this.position.xPosOffset;
          offsetX += (this.position.lastCurrentViewSequencePosition - this.position.lastStartXTile) * tileWidth;
          Iif (this.position.currentViewSequencePosition !== this.position.lastCurrentViewSequencePosition) {
            offsetX += (this.position.currentViewSequencePosition - this.position.lastCurrentViewSequencePosition) * tileWidth;
          }
          it.el.current.scrollLeft = offsetX;
        }
        if (withY) {
          const tileHeight = it.props.tileHeight;
          let offsetY = -this.position.yPosOffset;
          offsetY += (this.position.lastCurrentViewSequence - this.position.lastStartYTile) * tileHeight;
          Iif (this.position.currentViewSequence !== this.position.lastCurrentViewSequence) {
            offsetY += (this.position.currentViewSequence - this.position.lastCurrentViewSequence) * tileHeight;
          }
          it.el.current.scrollTop = offsetY;
        }
      }
    }
 
    dispatch = (payload) => {
      this.context.positionMSAStore.dispatch(payload);
    }
 
    render() {
      if (!this.hasBeenInitialized) {
        this.updateFromPositionStore();
        this.hasBeenInitialized = true;
      }
      return React.createElement(Component, {
        ref:this.el,
        position: this.position,
        positionDispatch: this.dispatch,
        ...this.props,
      });
    }
  }
  MSAPositionConsumer.displayName = `withPosition(${Component.displayName || Component.name})`;
  MSAPositionConsumer.contextTypes = {
    positionMSAStore: PropTypes.object,
  }
 
  return MSAPositionConsumer;
}
export default withPositionConsumer;