/** * Determine whether the menu should be opened in dialog or regular layout. * @type {[type]} */ function determineBreakpoint(element) { return $(element).closest('.ui-dialog-content').length > 0 ? '.ui-dialog-content' : '#content'; } /** * Obtain all parents of the given element that have scrollable content. */ function getScrollableParents(element, menu_width, menu_height) { const offset = $(element).offset(); const breakpoint = determineBreakpoint(element); let elements = []; $(element).parents().each(function () { // Stop at breakpoint if ($(this).is(breakpoint)) { return false; } // Exit early if overflow is visible const overflow = $(this).css('overflow'); if (overflow === 'visible' || overflow === 'inherit') { return; } // Check whether element is overflown const overflown = this.scrollHeight > this.clientHeight || this.scrollWidth > this.clientWidth; if (overflow === 'hidden' && overflown) { elements.push(this); return; } // Check if menu fits inside element const offs = $(this).offset(); const w = $(this).width(); const h = $(this).height(); if (offset.left + menu_width > offs.left + w) { elements.push(this); } else if (offset.top + menu_height > offs.top + h) { elements.push(this); } }); return elements; } class ActionMenu { static stash = new Map(); static openMenus = []; static #secret = Symbol(); static scrollHandlerState = false; /** * Create menu using a singleton pattern for each element. */ static create(element, position = true) { const id = $(element).uniqueId().attr('id'); const breakpoint = determineBreakpoint(element); if (!ActionMenu.stash.has(id)) { const menu_offset = $(element).offset().top + $('.action-menu-content', element).height(); const max_offset = $(breakpoint).offset().top + $(breakpoint).height(); const reversed = menu_offset > max_offset; ActionMenu.stash.set(id, new ActionMenu(ActionMenu.#secret, element, reversed, position)); } return ActionMenu.stash.get(id); } /** * Closes all menus. * @return {[type]} [description] */ static closeAll() { this.stash.forEach((menu) => menu.close()); } /** * Private constructor by implementing the secret/passed_secret mechanism. */ constructor(passed_secret, element, reversed, position) { // Enforce use of create (would use a private constructor if I could) if (ActionMenu.#secret !== passed_secret) { throw new Error('Cannot create ActionMenu. Use ActionMenu.create()!'); } this.element = $(element); this.menu = this.element; this.content = $('.action-menu-content', element); this.is_reversed = reversed; this.is_open = false; this.position = position; const additionalClasses = Object.values({ ...this.element[0].classList }).filter((item) => item != 'action-menu'); const menu_width = this.content.width(); const menu_height = this.content.height(); // Reposition the menu? if (position) { let parents = getScrollableParents(this.element, menu_width, menu_height); if (parents.length > 0) { const form = this.element.closest('form'); if (form) { const id = form.uniqueId().attr('id'); $('.action-menu-item input[type="image"]:not([form])', this.element).attr('form', id); $('.action-menu-item button:not([form])', this.element).attr('form', id); } this.menu = $('