File size: 9,199 Bytes
22c7264
8a554f1
22c7264
 
 
 
 
 
8755ee2
a47eab3
 
 
8755ee2
5d08a61
 
0aebd1a
 
 
 
 
 
27c03db
bcf9be5
0aebd1a
5829a93
22c7264
37f5ac4
22c7264
97eb6a7
22c7264
 
 
8755ee2
 
 
97eb6a7
 
a63a502
97eb6a7
 
 
 
37f5ac4
22c7264
 
 
 
a63a502
22c7264
 
 
 
 
a63a502
22c7264
 
8755ee2
 
a47eab3
 
8755ee2
fe79342
5aa283f
27c03db
22c7264
 
 
9f78197
6d7e9d4
 
ede8cb9
 
 
 
 
 
 
 
22c7264
8755ee2
bcf9be5
22c7264
 
 
27c03db
bcf9be5
 
fe79342
 
 
5d08a61
fe79342
 
 
22c7264
 
 
22fca24
 
 
 
 
 
 
 
27c03db
 
 
 
 
 
 
 
8755ee2
 
 
37f5ac4
33909e6
5d08a61
 
 
 
f898881
27c03db
33909e6
6b49118
37f5ac4
 
bcf9be5
a47eab3
8755ee2
 
bcf9be5
22c7264
bcf9be5
 
 
 
 
 
 
 
 
b1c5f8c
bcf9be5
b1c5f8c
bcf9be5
8755ee2
bcf9be5
 
 
 
 
 
 
 
22c7264
bcf9be5
8755ee2
bcf9be5
5d08a61
 
 
 
bcf9be5
 
8755ee2
bcf9be5
5d08a61
 
31d8a1a
5d08a61
31d8a1a
bcf9be5
 
 
 
 
 
 
 
 
 
 
 
 
27c03db
bcf9be5
 
f5ebead
b1c5f8c
 
 
5d08a61
f5ebead
b1c5f8c
 
 
f5ebead
6b49118
 
bcf9be5
 
 
 
a47eab3
 
 
 
b1c5f8c
 
5d08a61
b1c5f8c
 
8755ee2
 
 
 
 
a47eab3
 
 
 
8755ee2
bcf9be5
 
 
 
 
 
 
 
 
8755ee2
 
 
 
a47eab3
 
 
 
8755ee2
bcf9be5
 
 
 
 
 
 
 
22c7264
 
8755ee2
 
 
a47eab3
 
8755ee2
22c7264
 
 
 
 
bb8be8c
 
8755ee2
 
 
 
a47eab3
8755ee2
bb8be8c
 
f898881
 
 
 
 
 
bb8be8c
ede8cb9
f898881
22c7264
 
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import Position from './position';
import { ID_OVERLAY, OVERLAY_ZINDEX } from '../common/constants';

/**
 * Responsible for overlay creation and manipulation i.e.
 * cutting out the visible part, animating between the sections etc
 */
export default class Overlay {
  /**
   * @param {Object} options
   * @param {Window} window
   * @param {Document} document
   */
  constructor(options, window, document) {
    this.options = options;

    this.overlayAlpha = 0;                       // Is used to animate the layover
    this.positionToHighlight = new Position({}); // position at which layover is to be patched at
    this.highlightedPosition = new Position({}); // position at which layover is patched currently
    this.redrawAnimation = null;                 // used to cancel the redraw animation
    this.highlightedElement = null;              // currently highlighted dom element (instance of Element)
    this.lastHighlightedElement = null;          // element that was highlighted before current one

    this.draw = this.draw.bind(this);  // To pass the context of class, as it is to be used in redraw animation callback

    this.window = window;
    this.document = document;

    this.resetOverlay();
    this.setSize();
  }

  /**
   * Prepares the overlay
   */
  resetOverlay() {
    // Check and remove the canvas if it already exists
    const canvasOverlay = this.document.getElementById(ID_OVERLAY);
    if (canvasOverlay && canvasOverlay.parentNode) {
      canvasOverlay.parentNode.removeChild(canvasOverlay);
    }

    const overlay = this.document.createElement('canvas');

    this.overlay = overlay;
    this.context = overlay.getContext('2d');

    this.overlay.id = ID_OVERLAY;
    this.overlay.style.pointerEvents = 'none';
    this.overlay.style.background = 'transparent';
    this.overlay.style.position = 'fixed';
    this.overlay.style.top = '0';
    this.overlay.style.left = '0';
    this.overlay.style.zIndex = OVERLAY_ZINDEX;
  }

  /**
   * Highlights the dom element on the screen
   * @param {Element} element
   * @param {boolean} animate
   */
  highlight(element, animate = true) {
    if (!element || !element.node) {
      console.warn('Invalid element to highlight. Must be an instance of `Element`');
      return;
    }

    // @todo put it in the caller after testing
    this.setSize();

    // Trigger the hook for highlight started
    element.onHighlightStarted();

    // Old element has been deselected
    if (this.highlightedElement) {
      this.highlightedElement.onDeselected();
    }

    // get the position of element around which we need to draw
    const position = element.getCalculatedPosition();
    if (!position.canHighlight()) {
      return;
    }

    this.lastHighlightedElement = this.highlightedElement;
    this.highlightedElement = element;
    this.positionToHighlight = position;

    // If animation is not required then set the last path to be same
    // as the current path so that there is no easing towards it
    if (!this.options.animate || !animate) {
      this.highlightedPosition = this.positionToHighlight;
    }

    this.draw();
  }

  /**
   * Returns the currently selected element
   * @returns {null|*}
   */
  getHighlightedElement() {
    return this.highlightedElement;
  }

  /**
   * Gets the element that was highlighted before current element
   * @returns {null|*}
   */
  getLastHighlightedElement() {
    return this.lastHighlightedElement;
  }

  /**
   * Removes the overlay and cancel any listeners
   */
  clear() {
    this.positionToHighlight = new Position();
    if (this.highlightedElement) {
      this.highlightedElement.onDeselected();
    }

    this.highlightedElement = null;
    this.lastHighlightedElement = null;

    this.draw();
  }

  /**
   * `draw` is called for every frame . Puts back the
   * filled overlay on body (i.e. while removing existing highlight if any) and
   * Slowly eases towards the item to be selected.
   */
  draw() {
    // Cache the response of this for re-use below
    const canHighlight = this.positionToHighlight.canHighlight();

    // Remove the existing cloak from the body
    // it might be torn i.e. have patches from last highlight
    this.removeCloak();
    // Add the overlay on top of the whole body
    this.addCloak();

    const isFadingIn = this.overlayAlpha < 0.1;

    if (canHighlight) {
      if (isFadingIn) {
        // Ignore the animation, just highlight the item at its current position
        this.highlightedPosition = this.positionToHighlight;
      } else {
        // Slowly move towards the position to highlight
        this.highlightedPosition.left += (this.positionToHighlight.left - this.highlightedPosition.left) * 0.18;
        this.highlightedPosition.top += (this.positionToHighlight.top - this.highlightedPosition.top) * 0.18;
        this.highlightedPosition.right += (this.positionToHighlight.right - this.highlightedPosition.right) * 0.18;
        this.highlightedPosition.bottom += (this.positionToHighlight.bottom - this.highlightedPosition.bottom) * 0.18;
      }
    }

    // Cut the chunk of overlay that is over the highlighted item
    this.removeCloak({
      posX: this.highlightedPosition.left - this.window.scrollX - this.options.padding,
      posY: this.highlightedPosition.top - this.window.scrollY - this.options.padding,
      width: (this.highlightedPosition.right - this.highlightedPosition.left) + (this.options.padding * 2),
      height: (this.highlightedPosition.bottom - this.highlightedPosition.top) + (this.options.padding * 2),
    });

    // Fade the overlay in if we can highlight
    if (canHighlight) {
      if (!this.options.animate) {
        this.overlayAlpha = this.options.opacity;
      } else {
        this.overlayAlpha += (this.options.opacity - this.overlayAlpha) * 0.08;
      }
    } else {
      // otherwise fade out
      this.overlayAlpha = Math.max((this.overlayAlpha * 0.85) - 0.02, 0);
    }

    // cancel any existing animation frames
    // to avoid the overlapping of frames
    this.window.cancelAnimationFrame(this.redrawAnimation);

    // Continue drawing while we can highlight or we are still fading out
    if (canHighlight || this.overlayAlpha > 0) {
      // Add the overlay if not already there
      if (!this.overlay.parentNode) {
        this.document.body.appendChild(this.overlay);
      }

      // Stage a new animation frame only if the position has not been reached
      // or the alpha has not yet fully reached fully required opacity
      if (!this.hasPositionHighlighted()) {
        this.redrawAnimation = this.window.requestAnimationFrame(this.draw);
      } else if (!this.options.animate && isFadingIn) {
        this.redrawAnimation = this.window.requestAnimationFrame(this.draw);
      } else {
        // Element has been highlighted
        this.highlightedElement.onHighlighted();
      }
    } else if (this.overlay.parentNode) {
      // Otherwise if the overlay is there, remove it
      this.document.body.removeChild(this.overlay);
    }
  }

  /**
   * Checks if there as any position highlighted
   * @returns {boolean}
   */
  hasPositionHighlighted() {
    return this.positionToHighlight.equals(this.highlightedPosition) &&
      this.overlayAlpha > (this.options.opacity - 0.05);
  }

  /**
   * Removes the cloak from the given position
   * i.e. cuts the chunk of layout which is over the element
   * to be highlighted
   *
   * @param {number} posX
   * @param {number} posY
   * @param {number} width
   * @param {number} height
   */
  removeCloak({
    posX = 0,
    posY = 0,
    width = this.overlay.width,
    height = this.overlay.height,
  } = {}) {
    this.context.clearRect(posX, posY, width, height);
  }

  /**
   * Adds the overlay i.e. to cover the given
   * position with dark overlay
   *
   * @param {number} posX
   * @param {number} posY
   * @param {number} width
   * @param {number} height
   */
  addCloak({
    posX = 0,
    posY = 0,
    width = this.overlay.width,
    height = this.overlay.height,
  } = {}) {
    this.context.fillStyle = `rgba( 0, 0, 0, ${this.overlayAlpha} )`;
    this.context.fillRect(posX, posY, width, height);
  }

  /**
   * Sets the size for the overlay
   *
   * @param {number|null} width
   * @param {number|null} height
   */
  setSize(width = null, height = null) {
    // By default it is going to cover the whole page and then we will
    // cut out a chunk for the element to be visible out of it
    this.overlay.width = width || this.window.innerWidth;
    this.overlay.height = height || this.window.innerHeight;
  }

  /**
   * Refreshes the overlay i.e. sets the size according to current window size
   * And moves the highlight around if necessary
   *
   * @param {boolean} animate
   */
  refresh(animate = true) {
    this.setSize();

    // If the highlighted element was there Cancel the
    // existing animation frame if any and highlight it again
    // as its position might have been changed
    if (this.highlightedElement) {
      this.window.cancelAnimationFrame(this.redrawAnimation);
      this.highlight(this.highlightedElement, animate);
      this.highlightedElement.onHighlighted();
    }
  }
}