All files / src/store positionStoreMixin.js

87.93% Statements 51/58
81.63% Branches 40/49
77.78% Functions 7/9
87.93% Lines 51/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                                                                                                            18x 14x     14x 14x 14x       14x 6x 6x   14x 8x 8x   14x   3x               18x 18x 11x     11x 11x 11x     18x 18x 3x     3x       18x       18x       18x               18x 14x 14x 8x 2x     12x 6x 1x     11x   14x 14x 14x 3x 1x 1x 1x     1x   3x 2x 2x 2x     2x     14x         4x 4x            
/**
* 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 PropTypes from 'prop-types';
import assert from '../assert';
 
/**
 * Mixes in position store functionality in the requiring components.
 * Can't use HoC components as React Tree calculations need to be prevented at the utmost cost.
 *
 * @param {Object} Component - class to inject the position store into
 * @param {Object} Configuration - which parts of the position store to inject
 *
 * Select from:
 * - `withY` (`yPosOffset`, `currentViewSequence`)
 * - `withX` (`xPosOffset`, `currentViewSequencePosition`)
 * - `withPosition` (`xPos`, yPos`)
 *
 * Multiple selections are allowed.
 *
 * It will inject the following functionality:
 *
 * (1) `updateFromPositionStore`
 *   - a method which updates this.position from the PositionStore)
 *   - `setState({position: positionState})` is called when `shouldRerender` returns true
 * (2) `shouldRerender`
 *  - only if not defined
 *  - called to determine if the current viewpoint still has enough nodes)
 *  - checks the respective viewports when `withX` or `withY` have been set
 *  - calls `updateScrollPosition`
 *
 * (3) `updateScrollPosition`
 *  - only if not defined
 *  - the default implementation sets `this.el.current.scroll{Left,Top}`
 *
 * (4) `dispatch` (to access the PositionStore for event dispatching)
 *
 * Additionally, it will:
 * (5) enhance `componentWillMount` to inject a subscription to the positionStore
 * (6) enhance `componentWillUnmount` to unsubscribe from the positionStore
 *
 * However, if a `componentWillMount` or `componentWillUnmount` method did exist,
 * this will be called first.
 */
export function positionStoreMixin(Component, {
  withX = false,
  withY = false,
  withPosition = false,
}) {
  Component.prototype.updateFromPositionStore = function() {
    assert(this.context && this.context.positionMSAStore,
      "MSA PositionStore needs to be injected"
    );
    const state = this.context.positionMSAStore.getState();
    this.position = this.position || {};
    Iif (withPosition) {
      this.position.xPos = state.position.xPos;
      this.position.yPos = state.position.yPos;
    }
    if (withX) {
      this.position.xPosOffset = state.xPosOffset;
      this.position.currentViewSequencePosition = state.currentViewSequencePosition;
    }
    if (withY) {
      this.position.yPosOffset = state.yPosOffset;
      this.position.currentViewSequence = state.currentViewSequence;
    }
    if (this.shouldRerender()) {
      // this will always force a rerender as position is a new object
      this.setState({
        position: {
          ...this.position,
        }
      });
    }
  }
 
  const oldComponentWillMount = Component.prototype.componentWillMount;
  Component.prototype.componentWillMount = function() {
    Iif (oldComponentWillMount) {
      oldComponentWillMount.call(this);
    }
    this.updateFromPositionStore = this.updateFromPositionStore.bind(this);
    this.updateFromPositionStore();
    this.context.positionMSAStore.subscribe(this.updateFromPositionStore);
  }
 
  const oldComponentDidUpdate = Component.prototype.componentDidUpdate;
  Component.prototype.componentDidUpdate = function() {
    Iif (oldComponentDidUpdate) {
      oldComponentDidUpdate.call(this);
    }
    this.updateScrollPosition();
  }
 
  // inject the store via contexts
  Component.contextTypes = {
    positionMSAStore: PropTypes.object,
  }
 
  Component.prototype.dispatch = function(payload) {
    this.context.positionMSAStore.dispatch(payload);
  };
 
  defaultRerender(Component, {withX, withY});
}
 
/**
 * Injects a default shouldRerender and updateScrollPosition implementations.
 * updateScrollPosition is called when the shouldRerender yields false
 */
function defaultRerender(Component, {withX = false, withY = false}) {
  if (Component.prototype.shouldRerender === undefined) {
    Component.prototype.shouldRerender = function() {
        if (withY) {
          if (Math.abs(this.position.currentViewSequence - this.position.lastCurrentViewSequence) >= this.props.cacheElements) {
            return true;
          }
        }
        if (withX) {
          if (Math.abs(this.position.currentViewSequencePosition - this.position.lastCurrentViewSequencePosition) >= this.props.cacheElements) {
            return true;
          }
        }
        return this.updateScrollPosition() || false;
    }
    Eif (Component.prototype.updateScrollPosition === undefined) {
      Component.prototype.updateScrollPosition = function(){
        if (this.el && this.el.current) {
          if (withX) {
            let offsetX = -this.position.xPosOffset;
            offsetX += (this.position.lastCurrentViewSequencePosition - this.position.lastStartXTile) * this.props.tileWidth;
            Iif (this.position.currentViewSequencePosition !== this.position.lastCurrentViewSequencePosition) {
              offsetX += (this.position.currentViewSequencePosition - this.position.lastCurrentViewSequencePosition) * this.props.tileWidth;
            }
            this.el.current.scrollLeft = offsetX;
          }
          if (withY) {
            let offsetY = -this.position.yPosOffset;
            offsetY += (this.position.lastCurrentViewSequence - this.position.lastStartYTile) * this.props.tileHeight;
            Iif (this.position.currentViewSequence !== this.position.lastCurrentViewSequence) {
              offsetY += (this.position.currentViewSequence - this.position.lastCurrentViewSequence) * this.props.tileHeight;
            }
            this.el.current.scrollTop = offsetY;
          }
        }
        return false;
      }
    }
  } else {
    // provide dummy updateScrollPosition
    Eif (Component.prototype.updateScrollPosition === undefined) {
      Component.prototype.updateScrollPosition = function(){
      };
    }
  }
}
export default positionStoreMixin;