Source: src/js/views/maps/LayerOpacityView.js


'use strict';

define(
  [
    'jquery',
    'underscore',
    'backbone',
    'models/maps/assets/MapAsset',
    'text!templates/maps/layer-opacity.html'
  ],
  function (
    $,
    _,
    Backbone,
    MapAsset,
    Template
  ) {

    /**
    * @class LayerOpacityView
    * @classdesc A number slider that shows and updates the opacity in a MapAsset model.
    * Changing the opacity of a layer will also make it visible, if it was not visible
    * before (i.e. this view also updates the MapAsset's visible attribute.)
    * @classcategory Views/Maps
    * @name LayerOpacityView
    * @extends Backbone.View
    * @screenshot views/maps/LayerOpacityView.png
    * @since 2.18.0
    * @constructs
    */
    var LayerOpacityView = Backbone.View.extend(
      /** @lends LayerOpacityView.prototype */{

        /**
        * The type of View this is
        * @type {string}
        */
        type: 'LayerOpacityView',

        /**
        * The HTML classes to use for this view's element
        * @type {string}
        */
        className: 'layer-opacity',

        /**
        * The model that this view uses
        * @type {MapAsset}
        */
        model: undefined,

        /**
         * The primary HTML template for this view
         * @type {Underscore.template}
         */
        template: _.template(Template),

        /**
         * CSS classes assigned to the HTML elements that make up this view
         * @type {Object}
         * @property {string} sliderContainer The element that will be converted by this
         * view into a number slider widget. An element with this class must exist in the
         * template.
         * @property {string} handle The class given to the element that acts as a
         * handle for the slider UI. The handle is created during render and the class is
         * set by the jquery slider widget.
         * @property {string} range The class given to the element that shades the
         * slider from 0 to the current opacity. The range is created during render and
         * the class is set by the jquery slider widget.
         * @property {string} label The element that displays the current opacity
         * value as a percentage. This element is created during render.
         */
        classes: {
          sliderContainer: 'layer-opacity__slider',
          handle: 'layer-opacity__handle',
          range: 'layer-opacity__range',
          label: 'layer-opacity__label'
        },

        /**
        * The events this view will listen to and the associated function to call.
        * @type {Object}
        */
        events: {
          // 'event selector': 'function',
        },

        /**
        * Executed when a new LayerOpacityView is created
        * @param {Object} [options] - A literal object with options to pass to the view
        */
        initialize: function (options) {

          try {
            // Get all the options and apply them to this view
            if (typeof options == 'object') {
              for (const [key, value] of Object.entries(options)) {
                this[key] = value;
              }
            }
          } catch (e) {
            console.log('A LayerOpacityView failed to initialize. Error message: ' + e);
          }

        },

        /**
        * Renders this view
        * @return {LayerOpacityView} Returns the rendered view element
        */
        render: function () {

          try {

            // Save a reference to this view
            var view = this;

            // Ensure the view's main element has the given class name
            this.el.classList.add(this.className);

            // Insert the template into the view
            this.$el.html(this.template({}));

            var startOpacity = this.model ? this.model.get('opacity') || 1 : 1;

            // Find the element that will contain the slider
            view.sliderContainer = this.$el.find('.' + this.classes.sliderContainer).first()

            // The model opacity may be updated by this or other views or models. Make
            // sure that the UI reflects any of these changes.
            view.stopListening(view.model, 'change:opacity')
            view.listenTo(view.model, 'change:opacity', view.updateSlider)

            // Create the jQuery slider widget. See https://api.jqueryui.com/slider/
            view.sliderContainer.slider({
              min: 0,
              max: 1,
              range: 'min',
              value: startOpacity,
              step: 0.01,
              // classes to add to the slider elements
              classes: {
                'ui-slider': '',
                'ui-slider-handle': view.classes.handle,
                'ui-slider-range': view.classes.range
              },
              // event handling
              slide: handleSliderEvent, // when the slider is moved by the user
              change: handleSliderEvent // when the slider is changed programmatically
            })

            // What to do when the opacity slider is changed. The event handler needs the
            // view context to call other functions that update the model and the label.
            function handleSliderEvent (e, ui) {
              const newOpacity = ui.value
              const currentVisibility = view.model.get('visible')
              // Update the model. This will trigger other UI updates in this view.
              view.updateModel(newOpacity)
              // If the opacity changes to anything but zero, then make sure the asset is
              // also visible. (Why would a user change the opacity and not also want the
              // layer visible?)
              if (newOpacity > 0 && !currentVisibility) {
                view.model.set('visible', true)
              // If the opacity is changed to zero, also set visibility to false. This
              // triggers the layer list to grey-out the layer item.
              } else if (newOpacity === 0 && currentVisibility) {
                view.model.set('visible', false)
              }
            }

            // Create the element that will display the current opacity value as a
            // percentage. Insert it into the slider handle so that it can be easily
            // positioned just below the handle, even as the handle moves.
            this.opacityLabel = document.createElement('div')
            this.opacityLabel.className = view.classes.label
            view.sliderContainer.slider('instance').handle.append(this.opacityLabel)
            // Show the initial opacity value
            view.updateLabel(startOpacity)

            return this

          }
          catch (error) {
            console.log(
              'There was an error rendering a LayerOpacityView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Get the new opacity value from the model and update the slider handle position
         * and label. This function is called whenever the model opacity is updated.
         */
        updateSlider: function () {
          try {
            const newOpacity = this.model.get('opacity')
            // Only update if the value has actually changed
            if (newOpacity !== this.displayedOpacity) {
              this.updateLabel(newOpacity)
              // If this function was triggered by any event other than a user sliding the
              // handle, then the slider handle position will need to be updated
              this.sliderContainer.slider('value', newOpacity)
              this.displayedOpacity = newOpacity
            }
          }
          catch (error) {
            console.log(
              'There was an error handling a slider event in a LayerOpacityView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Update the MapAsset model's opacity attribute with a new value.
         * @param {Number} newOpacity A number between 0 and 1 indicating the new opacity
         * value for the MapAsset model
         */
        updateModel: function (newOpacity) {
          try {
            if (!this.model || typeof newOpacity !== 'number') {
              return
            }
            this.model.set('opacity', newOpacity)
          }
          catch (error) {
            console.log(
              'There was an error updating the model in a LayerOpacityView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Update the label with the newOpacity displayed as a percentage
         * @param {Number} newOpacity A number between 0 and 1 indicating the new opacity
         * value for the MapAsset model
         */
        updateLabel: function (newOpacity) {

          try {
            if (!this.opacityLabel || (typeof newOpacity === 'undefined') || typeof newOpacity !== 'number') {
              return
            }
            var opacityPercent = Math.round(newOpacity * 100);
            this.opacityLabel.innerText = opacityPercent + '%'
          }
          catch (error) {
            console.log(
              'There was an error updating the opacity label in a LayerOpacityView' +
              '. Error details: ' + error
            );
          }
        },

        /**
         * Perform clean-up functions when this view is about to be removed from the page
         * or navigated away from.
         */
        onClose: function () {
          try {
            this.stopListening(this.model, 'change:opacity')
          }
          catch (error) {
            console.log(
              'There was an error performing clean up functions in a LayerOpacityView' +
              '. Error details: ' + error
            );
          }
        }

      }
    );

    return LayerOpacityView;

  }
);