Home Reference Source

src/items/menu.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 PanelItem from '../panel-item';
import * as languages from '../locales';
import defaultIcon from '../logo-blue-32x32.png';

const sortBy = fn => (a, b) => -(fn(a) < fn(b)) || +(fn(a) > fn(b));
const sortByLabel = iter => String(iter.label).toLowerCase();

const makeTree = (core, icon, __) => {
  const configuredCategories = core.config('application.categories');
  const locale = core.make('osjs/locale');

  const getIcon = (m, fallbackIcon) => m.icon
    ? (m.icon.match(/^(https?:)\//)
      ? m.icon
      : core.url(m.icon, {}, m))
    : fallbackIcon;

  const getTitle = (item) => locale
    .translatableFlat(item.title, item.name);

  const getCategory = (cat) => locale
    .translate(cat);

  const createCategory = c => ({
    icon: c.icon ? {name: c.icon} : icon,
    label: getCategory(c.label),
    items: []
  });

  const createItem = m => ({
    icon: getIcon(m, icon),
    label: getTitle(m),
    data: {
      name: m.name
    }
  });

  const createCategoryTree = (metadata) => {
    const categories = {};

    metadata
      .filter(m => m.hidden !== true)
      .forEach((m) => {
        const cat = Object.keys(configuredCategories).find(c => c === m.category) || 'other';
        const found = configuredCategories[cat];

        if (!categories[cat]) {
          categories[cat] = createCategory(found);
        }

        categories[cat].items.push(createItem(m));
      });

    Object.keys(categories).forEach(k => {
      categories[k].items.sort(sortBy(sortByLabel));
    });

    const result = Object.values(categories);
    result.sort(sortBy(sortByLabel));

    return result;
  };

  const createFlatMenu = (metadata) => {
    const pinned = [
      ...core.config('application.pinned', [])
      // TODO: User configurable pinned items
    ];

    const items = metadata
      .filter(m => pinned.indexOf(m.name) !== -1)
      .map(createItem);

    if (items.length) {
      items.sort(sortBy(sortByLabel));
      return [
        {type: 'separator'},
        ...items
      ];
    }

    return [];
  };

  const createSystemMenu = () => ([{
    type: 'separator'
  }, {
    icon,
    label: __('LBL_SAVE_AND_LOG_OUT'),
    data: {
      action: 'saveAndLogOut'
    }
  }, {
    icon,
    label: __('LBL_LOG_OUT'),
    data: {
      action: 'logOut'
    }
  }]);

  return (metadata) => {
    const categories = createCategoryTree(metadata);
    const flat = createFlatMenu(metadata);
    const system = createSystemMenu();

    return [
      ...categories,
      ...flat,
      ...system
    ];
  };
};

/**
 * Menu
 *
 * @desc Menu Panel Item
 */
export default class MenuPanelItem extends PanelItem {

  render(state, actions) {
    const _ = this.core.make('osjs/locale').translate;
    const __ = this.core.make('osjs/locale').translatable(languages);
    const icon = this.options.icon || defaultIcon;

    const logout = async (save) => {
      if (save) {
        await this.core.make('osjs/session').save();
      }

      this.core.make('osjs/auth').logout();
    };

    const makeMenu = makeTree(this.core, icon, __);

    const onclick = (ev) => {
      const packages = this.core.make('osjs/packages')
        .getPackages(m => (!m.type || m.type === 'application'));

      this.core.make('osjs/contextmenu').show({
        menu: makeMenu([].concat(packages)),
        position: ev.target,
        callback: (item) => {
          const {name, action} = item.data || {};

          if (name) {
            this.core.run(name);
          } else if (action === 'saveAndLogOut') {
            logout(true);
          } else if (action === 'logOut') {
            logout(false);
          }
        },
        toggle: true
      });
    };

    const onmenuopen = () => {
      const els = Array.from(this.panel.$element.querySelectorAll('.osjs-panel-item[data-name="menu"]'));
      els.forEach(el => el.querySelector('.osjs-panel-item--icon').click());
    };

    this.core.on('osjs/desktop:keybinding:open-application-menu', onmenuopen);
    this.on('destroy', () => this.core.off('osjs/desktop:keybinding:open-application-menu', onmenuopen));

    return super.render('menu', [
      h('div', {
        onclick,
        className: 'osjs-panel-item--clickable osjs-panel-item--icon'
      }, [
        h('img', {
          src: icon,
          alt: _('LBL_MENU')
        }),
        h('span', {}, _('LBL_MENU'))
      ])
    ]);
  }

}