Home Reference Source

src/controls/ComposedControl.js

import $ from 'jquery'

import { Control } from './Control'
import { cssClasses } from '../globals'

import '../../less/toolbox.less'
import '../../less/layermenu.less'

/**
 * @typedef {g4uControlOptions} ComposedControlOptions
 * @property {string} [containerClassName]
 * @property {boolean} [squeeze=false]
 * @property {number} [minHeight=50]
 */

/**
 * This is a class ComposedControl which provides some functionality for controls which are composed out of several
 * other controls. It makes use of the composite pattern.
 */

export class ComposedControl extends Control {
  /**
   * @param {ComposedControlOptions} [options={}]
   */
  constructor (options = {}) {
    const $container = $('<div>')

    if (options.hasOwnProperty('element')) {
      $(options.element).append($container)
    } else {
      options.element = $container.get(0)
    }

    options.singleButton = false

    super(options)

    /**
     * @type {Control[]}
     * @private
     */
    this.controls_ = []

    /**
     * @type {jQuery}
     * @private
     */
    this.$container_ = $container

    if (options.hasOwnProperty('containerClassName')) {
      this.$container_.addClass(options.containerClassName)
    }

    /**
     * @type {string}
     * @private
     */
    this.classNameItem_ = this.getClassName() + '-item'
    /**
     * @type {string}
     * @private
     */
    this.classNameItemFirst_ = this.classNameItem_ + '-first'
    /**
     * @type {string}
     * @private
     */
    this.classNameItemLast_ = this.classNameItem_ + '-last'

    this.squeeze_ = options.squeeze === true
    this.minHeight_ = options.minHeight === undefined ? 50 : options.minHeight
  }

  /**
   * @returns {Control[]}
   */
  getControls () {
    return this.controls_
  }

  /**
   * @returns {jQuery}
   */
  get$Container () {
    return this.$container_
  }

  /**
   * This method adds some helping css classes to the items
   * @param {jQuery} $item
   * @returns {jQuery}
   * @private
   */
  addClasses_ ($item) {
    $item.addClass(this.classNameItemLast_)

    if (this.$container_.children().length === 0) {
      $item.addClass(this.classNameItemFirst_)
    } else {
      this.$container_.children(':last-child').removeClass(this.classNameItemLast_)
    }

    return $item
  }

  /**
   *
   * @param {Control} control
   * @param {Object} options
   * @param {Boolean} [options.claim=true] if claim is set to false the control won't add anything to the container
   * @param {Boolean} [options.wrap=true] if wrap is set to true a span will be put arround the element
   *    if set to false the element of the control will be inserted directly
   * @param {Boolean} [options.cssPosition=true] if this is set to true the wrap or the element will get css classes
   *    indicating its position inside the container
   * @param {HTMLElement} [options.element] if this is set it will be put inside the container instead calling
   *    control.set$Target()
   */
  addControl (control, options = {}) {
    const map = this.getMap()
    if (map) {
      map.addControl(control)
    } else {
      throw new Error('composed controls needs to be added to the map before they can get any controls')
    }

    if (!(options.hasOwnProperty('claim')) || !options.claim) {
      if (!(options.hasOwnProperty('wrap')) || options.wrap) {
        const $wrap = $('<div>')

        if (options.hasOwnProperty('element')) {
          $wrap.append($(options.element))
        } else {
          control.set$Target($wrap)
        }

        $wrap.addClass(this.classNameItem_)

        if (!(options.hasOwnProperty('cssPosition')) || options.cssPosition) {
          this.addClasses_($wrap)
        }

        if (!control.getVisible()) {
          $wrap.addClass(cssClasses.hidden)
        }

        control.on('change:visible', () => {
          $wrap.toggleClass(cssClasses.hidden, !control.getVisible())
        })

        this.$container_.append($wrap)
      } else {
        $(options.element).addClass(this.classNameItem_)

        if (!(options.hasOwnProperty('cssPosition')) || options.cssPosition) {
          this.addClasses_($(options.element))
        }

        if (options.hasOwnProperty('element')) {
          this.$container_.append($(options.element))
        } else {
          control.set$Target(this.$container_)
        }
      }
    }

    control.on('change:visible', () => this.updateVisibility())
    control.on('change', e => this.dispatchEvent(e))
    control.on('change:size', e => this.dispatchEvent(e))

    this.controls_.push(control)

    this.changed()
    this.updateVisibility()
  }

  /**
   * @param {Control} control
   * @param {boolean} [propagate=true] propagate to map
   */
  removeControl (control, propagate = true) {
    const index = this.controls_.indexOf(control)
    if (index > -1) {
      control.get$Element()
        .add(control.get$Element().parent())
        .add(control.get$Target())
        .filter('.' + this.classNameItem_)
        .remove()

      this.controls_.splice(index, 1)
      if (propagate) {
        this.getMap().removeControl(control)
      }

      this.changed()
    }
  }

  /**
   * @param {G4UMap} map
   */
  setMap (map) {
    if (this.getMap()) {
      this.controls_.forEach(control => {
        this.getMap().removeControl(control)
      })

      this.controls_ = []
    }

    super.setMap(map)

    if (map) {
      map.getControls().on('remove', e => {
        this.removeControl(e.element, false)
      })
    }
  }

  updateVisibility () {
    this.setVisible(this.controls_.some(c => c.getVisible()))
  }

  /**
   * Returns true if the control is squeezable in the given dimension. Used by Positioning.
   * @param {string} dimension
   * @returns {boolean}
   */
  isSqueezable (dimension) {
    return this.squeeze_ && dimension === 'height'
  }

  /**
   * Squeezes the control in the given dimension by the provided value. Used by Positioning
   * Returns the value the control could get squeezed by.
   * @param {string} dimension
   * @param {number} value
   * @returns {number}
   */
  squeezeBy (dimension, value) {
    if (this.squeeze_ && dimension === 'height') {
      const height = this.$container_.height()
      const newHeight = Math.max(this.minHeight_, height - value)
      if (height > newHeight) {
        this.$container_.css('max-height', newHeight)
        return height - newHeight
      }
    }

    return 0
  }

  beforePositioning () {
    this.scrolled_ = this.$container_.scrollTop()
  }

  /**
   * used by positioning
   */
  afterPositioning () {
    this.$container_.scrollTop(this.scrolled_)
  }

  /**
   * Removes the squeeze. Used by Positioning.
   * @param {string} dimension
   */
  release (dimension) {
    if (dimension === 'height') {
      this.$container_.css('max-height', '')
    }
  }
}