'use strict';
import PopupMenuItem from './popup-menu-item.js';
import PopupMenuPosition from './popup-menu-position.js';
let instance = null;
let isConstructorAllowed = false;
/**
* @class PopupMenu
* @implements {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement|HTMLElement}
* @classdesc
* Implements the functionality of a popup up menu to be displayed in your
* application. The popup menu contains of the list of the provided menu items.
* <p>
* PopupMenu closes itself on {@link https://www.w3schools.com/jsref/event_onclick.asp|click} event
* fired by {@link https://www.w3schools.com/js/js_window.asp|window}. To display
* the menu on click, you must mark the 'click' event as
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Event/defaultPrevented|defaultPrevented}
* using {@link https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault|preventDefault()},
* so the PopupMenu will ignore it. See the
* {@link https://github.com/ui-widgets-js/popup-menu/blob/master/demo/index.js|example}.
* </p>
* <p>
* PopupMenu closes itself on {@link https://www.w3schools.com/jsref/event_onresize.asp| resize} and
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/orientationchange_event|orientationchange}
* events fired by {@link https://www.w3schools.com/js/js_window.asp|window}.
* </p>
*
* @listens window#click
* @listens window#resize
* @listens window#orientationchange
*
* @example
* // Display PopupMenu under the button
* const myButton = document.getElementById('myButton');
* const itemList = [
* new PopupMenuItem(1, 'Menu item 1'),
* new PopupMenuItem(2, 'Menu item 2'),
* new PopupMenuItem(3, 'Menu item 3')
* ];
* const position = PopupMenuPosition.alignBottomLeft(myButton);
* const selectedId = await PopupMenu.show(itemList, position);
*/
export default class PopupMenu extends HTMLElement {
/**
* @constructor
* @private
* @param {PopupMenuItem[]} menuItems Menu items to be displayed
*/
constructor(menuItems) {
if (!isConstructorAllowed) {
throw new Error('This is private constructor. Use PopupMenu.show() instead.');
}
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
width: auto;
height: auto;
font-size: small;
font-family: Arial, sans-serif;
max-width: 320px;
overflow-y: auto;
overflow-x: hidden;
background-color: white;
position: absolute;
box-sizing: border-box;
box-shadow: rgba(221, 221, 221, 0.35) 0px 2px 8px;
border: 1px lightgray solid;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
color: #24292E;
overflow: hidden;
padding-top: 5px;
padding-bottom: 5px;
animation: appear .2s ease-in;
-webkit-animation: appear .2s ease-in;
-moz-animation: appear .2s ease-in;
}
.popup-menu-item {
text-align: left;
overflow: hidden;
display: flex;
width: 100%;
max-width: 100%;
height: 32px;
max-height: 32px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.popup-menu-item:hover {
background-color: #D3D3D352;
}
.popup-menu-item .image {
min-width: 30px;
min-height: 30px;
background-repeat: no-repeat;
background-position: center;
}
.popup-menu-item p {
margin-top: 0px;
margin-bottom: 0px;
margin-left: 8px;
margin-right: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: inherit;
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-webkit-keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@-moz-keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style>
`;
const isContainsImages = menuItems.findIndex(item => item.imageUrl) >= 0;
menuItems.forEach(item => {
const menuItem = document.createElement('div');
menuItem.classList.add('popup-menu-item');
menuItem.innerHTML = `<p>${item.itemName}</p>`;
if (item.imageUrl) {
menuItem.innerHTML =
`<div class="image" style="background-image: url('${item.imageUrl}')"></div>`
+ menuItem.innerHTML;
}
else if (isContainsImages) {
// Place holder to align record with the records which contain image
menuItem.innerHTML =
`<div class="image"></div>`
+ menuItem.innerHTML;
}
menuItem.addEventListener('click', () => {
this._onPopupMenuItemClick(item.itemId);
});
this.shadowRoot.appendChild(menuItem);
});
this._onHandleClosingEvent = this._onHandleClosingEvent.bind(this);
}
connectedCallback() {
['click', 'resize', 'orientationchange'].forEach(eventName =>
window.addEventListener(eventName, this._onHandleClosingEvent));
}
disconnectedCallback() {
['click', 'resize', 'orientationchange'].forEach(eventName =>
window.removeEventListener(eventName, this._onHandleClosingEvent));
}
/**
* @private
* @param {DOMEvent} e Closes the existing instance of PopupMenu if specified
* event is not defaultPrevented
*/
_onHandleClosingEvent(e) {
if (!e.defaultPrevented) {
PopupMenu.close();
}
}
/**
* @private
* @param {any} itemId The id of the selected item
*/
_onPopupMenuItemClick(itemId) {
if (this.onItemSelected) {
this.onItemSelected(itemId);
this.onItemSelected = null;
}
}
static get is() {
return 'popup-menu';
}
/**
* Displays the popup menu and returns the id of the selected item
* asynchronously.
*
* Example of usage:
*
* const selectedId = await PopupMenu.show(itemsList, position);
*
* @static
* @async
* @memberof PopupMenu
* @param {PopupMenuItem[]} menuItems The list of items to be displayed
* @param {PopupMenuPosition} position The position of PopupMenu
*/
static async show(menuItems, position) {
// Validate the input
if (!Array.isArray(menuItems) || menuItems.length === 0) {
throw new Error('Invalid parameter: items must be a non empty Array of PopupMenuItem');
}
if (!menuItems.every(item => item instanceof PopupMenuItem)) {
throw new Error(`Invalid parameter: each menu item must be an instance PopupMenuItem`);
}
if (!(position instanceof PopupMenuPosition)) {
throw new Error ('Invalid parameter: position must be an instance of PopupMenuPosition');
}
PopupMenu.close();
isConstructorAllowed = true;
instance = new PopupMenu(menuItems);
isConstructorAllowed = false;
instance.style.top = position.top;
instance.style.left = position.left;
instance.style.right = position.right;
instance.style.bottom = position.bottom;
const itemId = await new Promise(resolve => {
instance.onItemSelected = resolve;
document.body.appendChild(instance);
});
return itemId;
}
/**
* Closes any existing instance of PopupMenu on the screen
* @static
* @memberof PopupMenu
*/
static close() {
if (instance) {
try {
if (instance.onItemSelected) {
instance.onItemSelected(undefined);
}
instance.parentElement.removeChild(instance);
}
catch(error) {
console.error(`PopupMenu: ${error}\n${error.stack}`);
}
instance = null;
}
}
}
if (!window.customElements.get(PopupMenu.is)) {
window.customElements.define(PopupMenu.is, PopupMenu);
}
export { PopupMenuItem, PopupMenuPosition };