All files / msa2/src/store positionStoreMixin.js

67.24% Statements 39/58
63.27% Branches 31/49
66.67% Functions 6/9
67.24% Lines 39/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                                                                                                            20x 11x     11x 11x 11x 4x 4x   11x 7x 7x   11x 4x 4x   11x                   20x 20x 11x 2x   11x 11x 11x     20x 20x               20x       20x       20x               20x 16x 11x 4x       11x 7x       11x   16x 16x 7x                                   7x         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 || {};
    if (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;
    }
    Iif (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() {
    if (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() {
    if (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) {
          Iif (Math.abs(this.position.currentViewSequence - this.position.lastCurrentViewSequence) >= this.props.cacheElements) {
            return true;
          }
        }
        if (withX) {
          Iif (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(){
        Iif (this.el && this.el.current) {
          if (withX) {
            let offsetX = -this.position.xPosOffset;
            offsetX += (this.position.lastCurrentViewSequencePosition - this.position.lastStartXTile) * this.props.tileWidth;
            if (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;
            if (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;