Home Reference Source

src/dialog.js

/*
 * OS.js - JavaScript Cloud/Web Desktop Platform
 *
 * Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */

import {h} from 'hyperapp';
import merge from 'deepmerge';
import plain from 'is-plain-object';
import {Box, Button, Toolbar} from '@osjs/gui';

let dialogCount = 0;

/*
 * Default button attributes
 */
const defaultButtons = (_) => ({
  ok: {label: _('LBL_OK'), positive: true},
  close: {label: _('LBL_CLOSE')},
  cancel: {label: _('LBL_CANCEL')},
  yes: {label: _('LBL_YES'), positive: true},
  no: {label: _('LBL_NO')}
});

/*
 * Creates a button from name
 */
const defaultButton = (n, _) => {
  const defs = defaultButtons(_);
  if (defs[n]) {
    return Object.assign({}, {
      name: n
    }, defs[n]);
  }

  return {label: n, name: n};
};

/*
 * Creates options
 */
const createOptions = (options, args) =>
  merge({
    id: null,
    className: 'unknown',
    defaultValue: null,
    buttons: [],
    sound: null,
    window: {
      id: options.id || 'Dialog_' + String(dialogCount),
      title: 'Dialog',
      attributes: {
        gravity: 'center',
        resizable: false,
        maximizable: false,
        minimizable: false,
        sessionable: false,
        classNames: [
          'osjs-dialog',
          `osjs-${options.className || 'unknown'}-dialog`
        ],
        minDimension: {
          width: 300,
          height: 100
        },
      }
    }
  }, options, {
    isMergeableObject: plain
  });

/**
 * OS.js default Dialog implementation
 *
 * Creates a Window with predefined content and actions(s)
 */
export default class Dialog {

  /**
   * Constructor
   * @param {Core} core OS.js Core reference
   * @param {Object} args Arguments given from service creation
   * @param {Object} options Dialog options (including Window)
   * @param {Object} [options.defaultValue] Default callback value
   * @param {Function} callback The callback function
   */
  constructor(core, args, options, callback) {
    this.core = core;
    this.args = args;
    this.callback = callback || function() {};
    this.options = createOptions(options, args);
    this.win = null;
    this.value = undefined;
    this.calledBack = false;

    const _ = core.make('osjs/locale').translate;

    this.buttons = this.options.buttons.map(n =>
      typeof n === 'string'
        ? defaultButton(n, _)
        : {
          label: n.label || 'button',
          name: n.name || 'unknown'
        });

    dialogCount++;
  }

  /**
   * Destroys the dialog
   */
  destroy() {
    if (this.win) {
      this.win.destroy();
    }

    this.win = null;
    this.callback = null;
  }

  /**
   * Renders the dialog
   * @param {Function} cb Callback from window
   */
  render(options, cb) {
    const opts = merge(this.options.window || {}, options, {
      isMergeableObject: plain
    });

    this.win = this.core.make('osjs/window', opts);

    this.win.on('keydown', (ev, win) => {
      if (ev.keyCode === 27) {
        this.emitCallback(this.getNegativeButton(), null, true);
      }
    });

    this.win.on('dialog:button', (name, ev) => {
      this.emitCallback(name, ev, true);
    });

    this.win.on('destroy', () => {
      this.emitCallback('destroy');
    });

    this.win.on('close', () => {
      this.emitCallback('cancel', undefined, true);
    });

    this.win.on('render', () => {
      // this.win.resizeFit();
      this.win.focus();

      const focusButton = this.getNegativeButton();
      const btn = focusButton ? this.win.$content.querySelector(`button[name=${focusButton}]`) : null;
      if (btn) {
        btn.focus();
      }

      this.playSound();
    });

    this.win.init();
    this.win.render(cb);
    this.win.focus();

    return this;
  }

  /**
   * Creates the default view
   * @param {Object[]} children Child nodes
   * @param {Object} [state] Pass on application state (mainly used for buttons)
   * @return {Object} Virtual dom node
   */
  createView(children, state = {}) {
    return h(Box, {grow: 1, shrink: 1}, [
      ...children,
      h(Toolbar, {class: 'osjs-dialog-buttons'}, [
        ...this.createButtons(state.buttons || {})
      ])
    ]);
  }

  /**
   * Gets the button (virtual) DOM elements
   * @param {Object} [states] Button states
   * @return {Object[]} Virtual dom node children list
   */
  createButtons(states = {}) {
    const onclick = (n, ev) => {
      this.win.emit('dialog:button', n, ev);
    };

    return this.buttons.map(b => h(Button, Object.assign({}, {
      disabled: states[b.name] === false,
      onclick: ev => onclick(b.name, ev)
    }, b)));
  }

  /**
   * Emits the callback
   * @param {String} name Button or action name
   * @param {Event} [ev] Browser event reference
   * @param {Boolean} [close=false] Close dialog
   */
  emitCallback(name, ev, close = false) {
    if (this.calledBack) {
      return;
    }
    this.calledBack = true;

    console.debug('Callback in dialog', name, ev, close);

    this.callback(name, this.getValue(), ev);

    if (close) {
      this.destroy();
    }
  }

  /**
   */
  playSound() {
    if (this.core.has('osjs/sounds')) {
      const snd = this.options.sound;
      if (snd) {
        this.core.make('osjs/sounds').play(snd);

        return true;
      }
    }

    return false;
  }

  /**
   * Gets the first positive button
   * @return {String|undefined}
   */
  getPositiveButton() {
    const found = this.buttons.find(b => b.positive === true);
    return found ? found.name : null;
  }

  /**
   * Gets the first negative button
   * @return {String|undefined}
   */
  getNegativeButton() {
    const found = this.buttons.find(b => !b.positive);
    return found ? found.name : null;
  }

  /**
   * Gets the dialog result value
   * @return {*}
   */
  getValue() {
    return typeof this.value === 'undefined'
      ? this.options.defaultValue
      : this.value;
  }

}