Home Reference Source

src/html/Shield.js

import $ from 'jquery'
import BaseObject from 'ol/Object'

import { cssClasses, keyCodes } from '../globals'
import { getInFront } from './html'

import '../../less/shield.less'

/**
 * @typedef {object} ShieldOptions
 * @property {G4UMap} map
 * @property {string} [className='g4u-shield']
 */

/**
 * @typedef {object} ElementPosition
 * @property {jQuery} $actualElement
 * @property {jQuery} $oldParent
 * @property {number} oldIndex
 */

/**
 * @typedef {object} OnTopOptions
 * @property {boolean} [findParentWindow=false]
 */

/**
 * A shield that sets itself in front of all other elements in a context if activated, hides itself if deactivated.
 * It can get another element in front of it (Attention: it gets removed from its context temporarly)
 */
export class Shield extends BaseObject {
  /**
   * @param {ShieldOptions} options
   */
  constructor (options) {
    super()

    /**
     * @type {string}
     * @private
     */
    this.className_ = options.className || 'g4u-shield'

    /**
     * @type {G4UMap}
     * @private
     */
    this.map_ = options.map

    /**
     * @type {jQuery}
     * @private
     */
    this.$context_ = $(this.map_.getTarget())

    /**
     * @type {jQuery}
     * @private
     */
    this.$element_ = $('<div>')
      .addClass(this.className_)

    this.$context_.append(this.$element_)

    this.setActive(options.hasOwnProperty('active') ? options.active : false)

    this.$element_.on('keydown', e => {
      if (e.which === keyCodes.ESCAPE) {
        if (this.getActive()) {
          this.setActive(false)
        }
      }
    })

    /**
     * @type {Map<jQuery, ElementPosition>}
     * @private
     */
    this.elementsOnTop_ = new Map()
  }

  /**
   * @param {boolean} active
   */
  setActive (active) {
    const oldValue = this.active_
    if (oldValue !== active) {
      if (active) {
        this.$element_.removeClass(cssClasses.hidden)
        getInFront(this.$element_, this.$context_)
      } else {
        this.$element_.addClass(cssClasses.hidden)
      }
      this.active_ = active
      this.dispatchEvent({
        type: 'change:active',
        oldValue: oldValue,
        key: 'active'
      })
    }
  }

  /**
   * @returns {boolean}
   */
  getActive () {
    return this.active_
  }

  /**
   * Gets the given element in front of the shield. The element is removed from its context temporarily
   * @param {jQuery} $element
   * @param {OnTopOptions} [options]
   */
  add$OnTop ($element, options = {}) {
    let $actualElement = $element

    if (!options.hasOwnProperty('findParentWindow') || options.findParentWindow) {
      const $window = $element.parents().filter('.g4u-window')
      if ($window.length > 0) {
        $actualElement = $window
      }
    }

    const $oldParent = $actualElement.parent()

    this.elementsOnTop_.set($element[0], {
      $actualElement,
      $oldParent,
      oldIndex: $oldParent.children().index($actualElement)
    })

    this.$element_.append($actualElement)
    getInFront($actualElement, this.$element_)

    for (const className of Array.from($oldParent[0].classList)) {
      this.$element_.addClass(className)
    }
  }

  /**
   * Returns the given element in front of the shield to the previous context
   * @param {jQuery} $element
   * @returns {Promise}
   */
  remove$OnTop ($element) {
    const element = $element[0]

    const { $actualElement, $oldParent, oldIndex } = this.elementsOnTop_.get(element)

    if (oldIndex === 0) {
      $oldParent.prepend($actualElement)
    } else {
      $oldParent.children().eq(oldIndex - 1).after($actualElement)
    }

    for (const className of Array.from($oldParent[0].classList)) {
      this.$element_.removeClass(className)
    }

    this.elementsOnTop_.delete(element)
  }

  /**
   * Returns all children in front of the shield
   * @returns {jQuery}
   */
  get$ElementsInFront () {
    return this.$element_.children()
  }
}