Source: js/app_modal_dialog.js

/* global BaseUI */
'use strict';

(function(exports) {
  var _ = navigator.mozL10n.get;
  var _id = 0;

  /**
   * The ModalDialog of the AppWindow.
   *
   * Including **alert**, **prompt**, **confirm**, and
   * **single select** dialogs.
   *
   * @class AppModalDialog
   * @param {AppWindow} app The app window instance
   *                        where this dialog should popup.
   * @extends BaseUI
   */
  var AppModalDialog = function AppModalDialog(app) {
    this.app = app;
    this.containerElement = app.element;
    this.events = [];
    // One to one mapping.
    this.instanceID = _id++;
    this._injected = false;
    this._visible = false;
    app.element.addEventListener('mozbrowsershowmodalprompt', this);
    return this;
  };

  AppModalDialog.prototype = Object.create(BaseUI.prototype);

  AppModalDialog.prototype.CLASS_NAME = 'AppModalDialog';

  AppModalDialog.prototype.ELEMENT_PREFIX = 'modal-dialog-';

  AppModalDialog.prototype.customID = function amd_customID() {
    if (this.app) {
      return '[' + this.app.origin + ']';
    } else {
      return '';
    }
  };

  AppModalDialog.prototype.handleEvent = function amd_handleEvent(evt) {
    this.app.debug('handling ' + evt.type);
    evt.preventDefault();
    evt.stopPropagation();
    this.events.push(evt);
    this.menuHeight = 0;
    if (!this._injected) {
      this.render();
    }
    this.show();
    this._injected = true;
  };

  AppModalDialog.prototype.isVisible = function amd_isVisible() {
    return this._visible;
  };

  AppModalDialog.prototype._fetchElements = function amd__fetchElements() {
    this.element = document.getElementById(this.CLASS_NAME + this.instanceID);
    this.elements = {};

    var toCamelCase = function toCamelCase(str) {
      return str.replace(/\-(.)/g, function replacer(str, p1) {
        return p1.toUpperCase();
      });
    };

    this.elementClasses = ['alert', 'alert-ok', 'alert-message',
      'prompt', 'prompt-ok', 'prompt-cancel', 'prompt-input', 'prompt-message',
      'confirm', 'confirm-ok', 'confirm-cancel', 'confirm-message',
      'select-one', 'select-one-cancel', 'select-one-menu', 'select-one-title',
      'alert-title', 'confirm-title', 'prompt-title',
      'custom-prompt', 'custom-prompt-message', 'custom-prompt-buttons',
      'custom-prompt-checkbox'];


    // Loop and add element with camel style name to Modal Dialog attribute.
    this.elementClasses.forEach(function createElementRef(name) {
      this.elements[toCamelCase(name)] =
        this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
    }, this);

    this.elements.menu = this.element.querySelector('menu');
  };

  AppModalDialog.prototype._registerEvents = function amd__registerEvents() {
    var elements = this.elements;
    for (var id in elements) {
      var tagName = elements[id].tagName.toLowerCase();
      if (tagName == 'button' || tagName == 'ul') {
        if (elements[id].classList.contains('confirm')) {
          elements[id].addEventListener('click',
            this.confirmHandler.bind(this));
        } else if (elements[id].classList.contains('cancel')) {
          elements[id].addEventListener('click', this.cancelHandler.bind(this));
        }
      }
    }
  };

  AppModalDialog.prototype.view = function amd_view() {
    var id = this.CLASS_NAME + this.instanceID;
    return `<div class="modal-dialog" id="${id}">
            <form class="modal-dialog-alert generic-dialog"
            role="dialog" tabindex="-1">
            <div class="modal-dialog-message-container inner">
              <h3 class="modal-dialog-alert-title"></h3>
              <p>
                <span class="modal-dialog-alert-message"></span>
              </p>
            </div>
            <menu>
              <button class="modal-dialog-alert-ok confirm affirmative"
              data-l10n-id="ok"></button>
            </menu>
          </form>
          <form class="modal-dialog-confirm generic-dialog"
          role="dialog" tabindex="-1">
            <div class="modal-dialog-message-container inner">
              <h3 class="modal-dialog-confirm-title"></h3>
              <p>
                <span class="modal-dialog-confirm-message"></span>
              </p>
            </div>
            <menu data-items="2">
              <button class="modal-dialog-confirm-cancel cancel"
              data-l10n-id="cancel"></button>
              <button class="modal-dialog-confirm-ok confirm affirmative"
              data-l10n-id="ok"></button>
            </menu>
          </form>
          <form class="modal-dialog-prompt generic-dialog"
            role="dialog" tabindex="-1">
            <div class="modal-dialog-message-container inner">
              <h3 class="modal-dialog-prompt-title"></h3>
              <p>
                <span class="modal-dialog-prompt-message"></span>
                <input class="modal-dialog-prompt-input" />
              </p>
            </div>
            <menu data-items="2">
              <button class="modal-dialog-prompt-cancel cancel"
               data-l10n-id="cancel"></button>
              <button class="modal-dialog-prompt-ok confirm affirmative"
              data-l10n-id="ok"></button>
            </menu>
          </form>
          <form class="modal-dialog-select-one generic-dialog"
            role="dialog" tabindex="-1">
            <div class="modal-dialog-message-container inner">
              <h3 class="modal-dialog-select-one-title"></h3>
              <ul class="modal-dialog-select-one-menu"></ul>
            </div>
            <menu>
              <button class="modal-dialog-select-one-cancel cancel"
              data-l10n-id="cancel"></button>
            </menu>
          </form>
          <form class="modal-dialog-custom-prompt generic-dialog"
            role="dialog" tabindex="-1">
            <div class="modal-dialog-message-container inner">
              <h3 class="modal-dialog-custom-prompt-title"></h3>
              <p class="modal-dialog-custom-prompt-message"></p>
              <label class="pack-checkbox">
                <input class="modal-dialog-custom-prompt-checkbox"
                type="checkbox"/>
                <span></span>
              </label>
            </div>
            <menu class="modal-dialog-custom-prompt-buttons"></menu>
          </form>
        </div>`;
  };

  AppModalDialog.prototype.processNextEvent = function amd_processNextEvent() {
    this.events.splice(0, 1);
    if (this.events.length) {
      this.show();
    } else {
      this.hide();
    }
  };

  AppModalDialog.prototype.kill = function amd_kill() {
    this._visible = false;
    this.containerElement.removeChild(this.element);
  };

  // Show relative dialog and set message/input value well
  AppModalDialog.prototype.show = function amd_show() {
    if (!this.events.length) {
      return;
    }

    this._visible = true;
    var evt = this.events[0];

    var message = evt.detail.message || '';
    var title = this._getTitle(evt.detail.title);
    var elements = this.elements;

    var type = evt.detail.promptType || evt.detail.type;

    switch (type) {
      case 'alert':
        elements.alertTitle.textContent = title;
        elements.alertMessage.textContent = message;
        elements.alert.classList.add('visible');
        elements.alertOk.textContent = evt.yesText ? evt.yesText : _('ok');
        elements.alert.focus();

        this.updateMaxHeight();
        break;

      case 'prompt':
        elements.prompt.classList.add('visible');
        elements.promptInput.value = evt.detail.initialValue;
        elements.promptTitle.textContent = title;
        elements.promptMessage.textContent = message;
        elements.promptOk.textContent = evt.yesText ? evt.yesText : _('ok');
        elements.promptCancel.textContent = evt.noText ?
          evt.noText : _('cancel');
        elements.prompt.focus();
        break;

      case 'confirm':
        elements.confirm.classList.add('visible');
        elements.confirmTitle.textContent = title;
        elements.confirmMessage.textContent = message;
        elements.confirmOk.textContent = evt.yesText ? evt.yesText : _('ok');
        elements.confirmCancel.textContent = evt.noText ?
          evt.noText : _('cancel');
        elements.confirm.focus();
        break;

      case 'selectone':
        this.buildSelectOneDialog(message);
        elements.selectOne.classList.add('visible');
        elements.selectOne.focus();
        break;

      case 'custom-prompt':
        var customPrompt = evt.detail;
        elements.customPrompt.classList.add('visible');
        elements.customPromptMessage.textContent = customPrompt.message;
        // Display custom list of buttons
        elements.customPromptButtons.innerHTML = '';
        elements.customPromptButtons.setAttribute('data-items',
                                                  customPrompt.buttons.length);
        var domElement = null;
        for (var i = customPrompt.buttons.length - 1; i >= 0; i--) {
          var button = customPrompt.buttons[i];
          domElement = document.createElement('button');
          domElement.dataset.buttonIndex = i;
          if (button.messageType === 'builtin') {
            domElement.setAttribute('data-l10n-id', button.message);
          } else if (button.messageType === 'custom') {
            // For custom button, we assume that the text is already translated
            domElement.textContent = button.message;
          } else {
            console.error('Unexpected button type : ' + button.messageType);
            continue;
          }
          domElement.addEventListener('click', this.confirmHandler.bind(this));
          elements.customPromptButtons.appendChild(domElement);
        }
        domElement.classList.add('affirmative');

        // Eventualy display a checkbox:
        var checkbox = elements.customPromptCheckbox;
        if (customPrompt.showCheckbox) {
          if (customPrompt.checkboxCheckedByDefault) {
            checkbox.setAttribute('checked', 'true');
          } else {
            checkbox.removeAttribute('checked');
          }
          // We assume that checkbox custom message is already translated
          checkbox.nextElementSibling.textContent =
            customPrompt.checkboxMessage;
        } else {
          checkbox.parentNode.classList.add('hidden');
        }

        elements.customPrompt.focus();
        break;
    }

    this.app.browser.element.setAttribute('aria-hidden', true);
    this.element.classList.add('visible');
  };

  AppModalDialog.prototype.hide = function amd_hide() {
    this._visible = false;
    this.element.blur();
    this.app.browser.element.removeAttribute('aria-hidden');
    this.element.classList.remove('visible');
    if (this.app) {
      this.app.focus();
    }
    if (!this.events.length) {
      return;
    }

    var evt = this.events[0];
    var type = evt.detail.promptType || evt.detail.type;
    if (type === 'prompt') {
      this.elements.promptInput.blur();
    }
    this.elements[type].classList.remove('visible');
  };

  // When user clicks OK button on alert/confirm/prompt
  AppModalDialog.prototype.confirmHandler =
    function amd_confirmHandler(clickEvt) {
      if (!this.events.length) {
        return;
      }

      clickEvt.preventDefault();

      var elements = this.elements;

      var evt = this.events[0];

      var type = evt.detail.promptType || evt.detail.type;
      switch (type) {
        case 'alert':
          elements.alert.classList.remove('visible');
          break;

        case 'prompt':
          evt.detail.returnValue = elements.promptInput.value;
          elements.prompt.classList.remove('visible');
          break;

        case 'confirm':
          evt.detail.returnValue = true;
          elements.confirm.classList.remove('visible');
          break;

        case 'custom-prompt':
          var returnValue = {
            selectedButton: clickEvt.target.dataset.buttonIndex
          };
          if (evt.detail.showCheckbox) {
            returnValue.checked = elements.customPromptCheckbox.checked;
          }
          evt.detail.returnValue = returnValue;
          elements.customPrompt.classList.remove('visible');
          break;
      }

      if (evt.detail.unblock) {
        evt.detail.unblock();
      }

      this.processNextEvent();
    };

  // When user clicks cancel button on confirm/prompt or
  // when the user try to escape the dialog with the escape key
  AppModalDialog.prototype.cancelHandler =
    function amd_cancelHandler(clickEvt) {
      if (!this.events.length) {
        return;
      }

      clickEvt.preventDefault();
      var evt = this.events[0];
      var elements = this.elements;

      var type = evt.detail.promptType || evt.detail.type;
      switch (type) {
        case 'alert':
          elements.alert.classList.remove('visible');
          break;

        case 'prompt':
          /* return null when click cancel */
          evt.detail.returnValue = null;
          elements.prompt.classList.remove('visible');
          break;

        case 'confirm':
          /* return false when click cancel */
          evt.detail.returnValue = false;
          elements.confirm.classList.remove('visible');
          break;

        case 'selectone':
          /* return null when click cancel */
          evt.detail.returnValue = null;
          elements.selectOne.classList.remove('visible');
          break;
      }

      if (evt.detail.unblock) {
        evt.detail.unblock();
      }

      this.processNextEvent();
    };

  // When user selects an option on selectone dialog
  AppModalDialog.prototype.selectOneHandler =
    function amd_selectOneHandler(target) {
      if (!this.events.length) {
        return;
      }

      var elements = this.elements;

      var evt = this.events[0];

      evt.detail.returnValue = target.id;
      elements.selectOne.classList.remove('visible');

      if (evt.detail.unblock) {
        evt.detail.unblock();
      }

      this.processNextEvent();
    };

  AppModalDialog.prototype.updateMaxHeight = function() {
    // Setting maxHeight for being able to scroll long
    // texts: formHeight - menuHeight - titleHeight - margin
    // We should fix this in a common way for all the dialogs
    // in the building blocks layer: Bug 1096902
    this.menuHeight = this.menuHeight || this.elements.menu.offsetHeight;
    var messageHeight = this.element.offsetHeight - this.menuHeight;
    messageHeight -= this.elements.alertTitle.offsetHeight;
    var margin = window.getComputedStyle(this.elements.alertTitle).marginBottom;
    var messageContainer = this.elements.alert.querySelector('.inner p');
    var calc = 'calc(' + messageHeight + 'px - ' + margin + ')';
    messageContainer.style.maxHeight = calc;
  };

  AppModalDialog.prototype._getTitle =
    function amd__getTitle(title) {
      //
      // XXX Bug 982006, subsystems like uriloader still report errors with
      // titles which are important to the user for context in diagnosing
      // issues.
      //
      // However, we will ignore all titles containing a URL using the app
      // protocol. These types of titles simply indicate that the active
      // application is prompting and are more confusing to the user than
      // useful. Instead we will return the application name if there is one
      // or an empty string.
      //
      if (!title ||
          title.includes('app://')) {
        return this.app.name || '';
      }

      return title;
    };
  exports.AppModalDialog = AppModalDialog;
}(window));