import Component from '@glimmer/component'; import SelectionList from '../utils/selection-list'; import {action} from '@ember/object'; import {inject as service} from '@ember/service'; import {task} from 'ember-concurrency'; import {tracked} from '@glimmer/tracking'; export default class GhContextMenu extends Component { @service dropdown; @service modals; @tracked isOpen = false; @tracked left = 0; @tracked top = 0; @tracked yPlacement = 'bottom'; @tracked xPlacement = 'right'; @tracked selectionList = new SelectionList(); element = null; /** * The current state of the context menu * @type {'default'|'open'|'modal'|'loading'} * default: default state * open: menu open * modal: modal open * loading: performing an action */ state = 'default'; #originalConfirm = null; #modal = null; setState(state) { switch (state) { case this.state: return; case 'default': this.isOpen = false; this.#closeModal(); this.selectionList.unfreeze(); this.state = state; return; case 'open': if (this.state !== 'default') { return; } this.isOpen = true; this.selectionList.freeze(); this.#closeModal(); this.state = state; return; case 'modal': if (this.state !== 'open') { return; } this.isOpen = false; this.selectionList.freeze(); this.state = state; return; case 'loading': if (this.state !== 'open' && this.state !== 'modal') { return; } this.isOpen = false; this.selectionList.freeze(); this.state = state; return; } } get name() { return this.args.name; } get style() { return `left: ${this.left}px; top: ${this.top}px;`; } get class() { return `gh-placement-${this.yPlacement} gh-placement-${this.xPlacement}`; } @action setup(element) { this.element = element; const dropdownService = this.dropdown; dropdownService.on('close', this, this.close); dropdownService.on('toggle', this, this.toggle); } willDestroy() { super.willDestroy(...arguments); this.element = null; const dropdownService = this.dropdown; dropdownService.off('close', this, this.close); dropdownService.off('toggle', this, this.toggle); } @action open() { this.setState('open'); } @action close() { if (this.state === 'open') { this.setState('default'); } } @action onContextMenuOutside(event) { this.close(); event.preventDefault(); event.stopPropagation(); } // Called by the dropdown service when the context menu should open @action toggle(options) { const targetDropdownName = options.target; if (this.name === targetDropdownName) { if (options.left !== undefined) { this.left = options.left; this.top = options.top; } if (options.selectionList) { this.selectionList = options.selectionList; } this.calculatePlacement(); this.open(); } else { this.close(); } } get listElement() { return this.element?.firstElementChild?.firstElementChild; } calculatePlacement() { if (!this.element || !this.listElement) { return; } const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; const menuHeight = this.listElement.offsetHeight; const menuWidth = this.listElement.offsetWidth; const padding = 10; // Do we have enough place to place the menu below? if (this.top + menuHeight + padding < windowHeight) { this.yPlacement = 'bottom'; } else { this.yPlacement = 'top'; } // Do we have enough place to place the menu to the right? if (this.left + menuWidth + padding < windowWidth) { this.xPlacement = 'right'; } else { this.xPlacement = 'left'; } } @action stopClicks(event) { event.stopPropagation(); } @task *confirmWrapperTask(...args) { this.setState('loading'); let result = yield this.#originalConfirm.perform(...args); this.#originalConfirm = null; this.setState('default'); return result; } openModal(Modal, data) { this.#originalConfirm = data.confirm; data.confirm = this.confirmWrapperTask; this.setState('modal'); this.#modal = this.modals.open(Modal, data); this.#modal.then(() => { // We need to delay a little bit for the click event to be processed // Since the click event is bubbling back to window, where it will trigger a list deselect setTimeout(() => { this.setState('default'); }, 10); }); } #closeModal() { this.#modal?.close(); this.#modal = null; } async performTask(taskObj) { this.setState('loading'); await taskObj.perform(); this.setState('default'); } }