index.js 8.0 KB
Newer Older
1 2 3 4
import Overlay from './core/overlay';
import Element from './core/element';
import Popover from './core/popover';
import './common/polyfill';
K
Kamran Ahmed 已提交
5 6 7 8 9
import {
  CLASS_CLOSE_BTN,
  CLASS_NEXT_STEP_BTN,
  CLASS_PREV_STEP_BTN,
  ESC_KEY_CODE,
10 11
  ID_POPOVER,
  LEFT_KEY_CODE,
K
Kamran Ahmed 已提交
12 13
  OVERLAY_ANIMATE,
  OVERLAY_OPACITY,
14 15
  OVERLAY_PADDING,
  RIGHT_KEY_CODE,
16
} from './common/constants';
17

K
Kamran Ahmed 已提交
18 19 20
/**
 * Plugin class that drives the plugin
 */
K
Kamran Ahmed 已提交
21
export default class Driver {
K
Kamran Ahmed 已提交
22
  /**
K
Kamran Ahmed 已提交
23
   * @param {Object} options
K
Kamran Ahmed 已提交
24
   */
K
Kamran Ahmed 已提交
25 26
  constructor(options = {}) {
    this.options = Object.assign({
K
Kamran Ahmed 已提交
27 28 29
      animate: OVERLAY_ANIMATE,     // Whether to animate or not
      opacity: OVERLAY_OPACITY,     // Overlay opacity
      padding: OVERLAY_PADDING,     // Spacing around the element from the overlay
K
Kamran Ahmed 已提交
30 31 32 33 34 35
      onHighlightStarted: () => {   // When element is about to be highlighted
      },
      onHighlighted: () => {        // When element has been highlighted
      },
      onDeselected: () => {         // When the element has been deselected
      },
K
Kamran Ahmed 已提交
36 37
    }, options);

K
Kamran Ahmed 已提交
38 39 40
    this.document = document;
    this.window = window;

41
    this.isActivated = false;
K
Kamran Ahmed 已提交
42
    this.overlay = new Overlay(this.options, this.window, this.document);
43

K
Kamran Ahmed 已提交
44 45 46
    this.steps = [];            // steps to be presented if any
    this.currentStep = 0;       // index for the currently highlighted step

K
Kamran Ahmed 已提交
47 48
    this.onScroll = this.onScroll.bind(this);
    this.onResize = this.onResize.bind(this);
K
Kamran Ahmed 已提交
49
    this.onKeyUp = this.onKeyUp.bind(this);
K
Kamran Ahmed 已提交
50
    this.onClick = this.onClick.bind(this);
K
Kamran Ahmed 已提交
51 52 53 54 55

    // Event bindings
    this.bind();
  }

K
Kamran Ahmed 已提交
56 57 58 59
  /**
   * Binds any DOM events listeners
   * @todo: add throttling in all the listeners
   */
K
Kamran Ahmed 已提交
60 61 62 63
  bind() {
    this.document.addEventListener('scroll', this.onScroll, false);
    this.document.addEventListener('DOMMouseScroll', this.onScroll, false);
    this.window.addEventListener('resize', this.onResize, false);
K
Kamran Ahmed 已提交
64
    this.window.addEventListener('keyup', this.onKeyUp, false);
K
Kamran Ahmed 已提交
65
    this.window.addEventListener('click', this.onClick, false);
66 67
  }

K
Kamran Ahmed 已提交
68 69 70 71 72
  /**
   * Removes the popover if clicked outside the highlighted element
   * or outside the
   * @param e
   */
K
Kamran Ahmed 已提交
73
  onClick(e) {
74
    if (!this.hasHighlightedElement() || !this.isActivated) {
K
Kamran Ahmed 已提交
75
      // Has no highlighted element so ignore the click
76 77 78
      return;
    }

K
Kamran Ahmed 已提交
79
    const highlightedElement = this.overlay.getHighlightedElement();
K
Kamran Ahmed 已提交
80
    const popover = this.document.getElementById(ID_POPOVER);
K
Kamran Ahmed 已提交
81

K
Kamran Ahmed 已提交
82 83 84
    const clickedHighlightedElement = highlightedElement.node.contains(e.target);
    const clickedPopover = popover && popover.contains(e.target);

85
    // Remove the overlay If clicked outside the highlighted element
K
Kamran Ahmed 已提交
86
    if (!clickedHighlightedElement && !clickedPopover) {
87
      this.reset();
K
Kamran Ahmed 已提交
88 89 90
      return;
    }

K
Kamran Ahmed 已提交
91 92 93
    const nextClicked = e.target.classList.contains(CLASS_NEXT_STEP_BTN);
    const prevClicked = e.target.classList.contains(CLASS_PREV_STEP_BTN);
    const closeClicked = e.target.classList.contains(CLASS_CLOSE_BTN);
K
Kamran Ahmed 已提交
94

95 96 97 98 99
    if (closeClicked) {
      this.reset();
      return;
    }

K
Kamran Ahmed 已提交
100
    if (nextClicked) {
K
Kamran Ahmed 已提交
101 102 103
      this.moveNext();
    } else if (prevClicked) {
      this.movePrevious();
104
    }
K
Kamran Ahmed 已提交
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
  }

  /**
   * Moves to the previous step if possible
   * otherwise resets the overlay
   */
  movePrevious() {
    this.currentStep -= 1;
    if (this.steps[this.currentStep]) {
      this.overlay.highlight(this.steps[this.currentStep]);
    } else {
      this.reset();
    }
  }

  /**
   * Moves to the next step if possible
   * otherwise resets the overlay
   */
  moveNext() {
    this.currentStep += 1;
    if (this.steps[this.currentStep]) {
K
Kamran Ahmed 已提交
127
      this.overlay.highlight(this.steps[this.currentStep]);
K
Kamran Ahmed 已提交
128 129
    } else {
      this.reset();
130
    }
K
Kamran Ahmed 已提交
131 132
  }

133 134 135 136 137 138 139 140 141 142 143 144 145 146
  /**
   * @returns {boolean}
   */
  hasNextStep() {
    return !!this.steps[this.currentStep + 1];
  }

  /**
   * @returns {boolean}
   */
  hasPreviousStep() {
    return !!this.steps[this.currentStep - 1];
  }

K
Kamran Ahmed 已提交
147 148 149 150 151
  /**
   * Resets the steps if any and clears the overlay
   */
  reset() {
    this.currentStep = 0;
152
    this.isActivated = false;
K
Kamran Ahmed 已提交
153 154 155 156 157 158 159
    this.overlay.clear();
  }

  /**
   * Checks if there is any highlighted element or not
   * @returns {boolean}
   */
K
Kamran Ahmed 已提交
160 161
  hasHighlightedElement() {
    const highlightedElement = this.overlay.getHighlightedElement();
K
Kamran Ahmed 已提交
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    return highlightedElement && highlightedElement.node && highlightedElement.highlightFinished;
  }

  /**
   * Gets the currently highlighted element in overlay
   * @returns {Element}
   */
  getHighlightedElement() {
    return this.overlay.getHighlightedElement();
  }

  /**
   * Gets the element that was highlighted before currently highlighted element
   * @returns {Element}
   */
  getLastHighlightedElement() {
    return this.overlay.getLastHighlightedElement();
K
Kamran Ahmed 已提交
179 180
  }

K
Kamran Ahmed 已提交
181 182 183 184 185
  /**
   * Handler for the onScroll event on document
   * Refreshes without animation on scroll to make sure
   * that the highlighted part travels with the scroll
   */
K
Kamran Ahmed 已提交
186
  onScroll() {
187 188 189 190
    if (!this.isActivated) {
      return;
    }

K
Kamran Ahmed 已提交
191 192 193
    this.overlay.refresh(false);
  }

K
Kamran Ahmed 已提交
194 195 196 197 198
  /**
   * Handler for the onResize DOM event
   * Refreshes with animation on scroll to make sure that
   * the highlighted part travels with the width change of window
   */
K
Kamran Ahmed 已提交
199
  onResize() {
200 201 202 203
    if (!this.isActivated) {
      return;
    }

K
Kamran Ahmed 已提交
204 205
    // Refresh with animation
    this.overlay.refresh(true);
206 207
  }

K
Kamran Ahmed 已提交
208 209 210 211
  /**
   * Clears the overlay on escape key process
   * @param event
   */
K
Kamran Ahmed 已提交
212
  onKeyUp(event) {
213 214 215 216
    if (!this.isActivated) {
      return;
    }

K
Kamran Ahmed 已提交
217
    if (event.keyCode === ESC_KEY_CODE) {
218
      this.reset();
K
Kamran Ahmed 已提交
219
    } else if (event.keyCode === RIGHT_KEY_CODE) {
220
      this.moveNext();
K
Kamran Ahmed 已提交
221
    } else if (event.keyCode === LEFT_KEY_CODE) {
222
      this.movePrevious();
K
Kamran Ahmed 已提交
223 224 225
    }
  }

226 227 228 229
  /**
   * Defines steps to be highlighted
   * @param {array} steps
   */
K
Kamran Ahmed 已提交
230 231 232 233
  defineSteps(steps) {
    this.steps = [];

    steps.forEach((step, index) => {
234 235
      if (!step.element || typeof step.element !== 'string') {
        throw new Error(`Element (query selector string) missing in step ${index}`);
K
Kamran Ahmed 已提交
236 237
      }

238 239
      const element = this.prepareElementFromStep(step, steps, index);
      if (!element) {
240 241 242
        return;
      }

K
Kamran Ahmed 已提交
243 244 245 246
      this.steps.push(element);
    });
  }

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 286 287 288 289 290 291 292 293 294 295 296
  /**
   * Prepares the step received from the user and returns an instance
   * of Element
   *
   * @param currentStep Step that is being prepared
   * @param allSteps  List of all the steps
   * @param index Index of the current step
   * @returns {null|Element}
   */
  prepareElementFromStep(currentStep, allSteps = [], index = 0) {
    let querySelector = '';
    let elementOptions = {};

    if (typeof currentStep === 'string') {
      querySelector = currentStep;
    } else {
      querySelector = currentStep.element;
      elementOptions = Object.assign({}, this.options, currentStep);
    }

    const domElement = this.document.querySelector(querySelector);
    if (!domElement) {
      console.warn(`Element to highlight ${querySelector} not found`);
      return null;
    }

    let popover = null;
    if (elementOptions.popover && elementOptions.popover.description) {
      const popoverOptions = Object.assign(
        {},
        this.options,
        elementOptions.popover, {
          totalCount: allSteps.length,
          currentIndex: index,
          isFirst: index === 0,
          isLast: index === allSteps.length - 1,
        },
      );

      popover = new Popover(popoverOptions, this.window, this.document);
    }

    return new Element(domElement, elementOptions, popover, this.overlay, this.window, this.document);
  }

  /**
   * Initiates highlighting steps from first step
   * @param {number} index at which highlight is to be started
   */
  start(index = 0) {
K
Kamran Ahmed 已提交
297 298 299 300
    if (!this.steps || this.steps.length === 0) {
      throw new Error('There are no steps defined to iterate');
    }

301 302
    this.isActivated = true;

303 304
    this.currentStep = index;
    this.overlay.highlight(this.steps[index]);
K
Kamran Ahmed 已提交
305 306
  }

K
Kamran Ahmed 已提交
307
  /**
308 309
   * Highlights the given element
   * @param {string|{element: string, popover: {}}} selector Query selector or a step definition
K
Kamran Ahmed 已提交
310
   */
K
Kamran Ahmed 已提交
311
  highlight(selector) {
312 313
    this.isActivated = true;

314 315
    const element = this.prepareElementFromStep(selector);
    if (!element) {
316
      return;
K
Kamran Ahmed 已提交
317
    }
K
Kamran Ahmed 已提交
318

319
    this.overlay.highlight(element);
320 321
  }
}