2023-04-04 15:43:43 +03:00
|
|
|
import Component from '@glimmer/component';
|
|
|
|
import SelectionList from '../utils/selection-list';
|
|
|
|
import {action} from '@ember/object';
|
|
|
|
import {inject as service} from '@ember/service';
|
2023-04-13 16:04:06 +03:00
|
|
|
import {task} from 'ember-concurrency';
|
2023-04-04 15:43:43 +03:00
|
|
|
import {tracked} from '@glimmer/tracking';
|
|
|
|
|
|
|
|
export default class GhContextMenu extends Component {
|
|
|
|
@service dropdown;
|
2023-04-13 16:04:06 +03:00
|
|
|
@service modals;
|
2023-04-04 15:43:43 +03:00
|
|
|
|
|
|
|
@tracked isOpen = false;
|
|
|
|
@tracked left = 0;
|
|
|
|
@tracked top = 0;
|
2023-04-14 12:04:11 +03:00
|
|
|
@tracked yPlacement = 'bottom';
|
|
|
|
@tracked xPlacement = 'right';
|
2023-04-04 15:43:43 +03:00
|
|
|
@tracked selectionList = new SelectionList();
|
2023-04-14 12:04:11 +03:00
|
|
|
element = null;
|
2023-04-04 15:43:43 +03:00
|
|
|
|
2023-04-13 16:04:06 +03:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
2023-04-13 16:06:49 +03:00
|
|
|
state = 'default';
|
2023-04-13 16:04:06 +03:00
|
|
|
|
|
|
|
#originalConfirm = null;
|
|
|
|
#modal = null;
|
|
|
|
|
|
|
|
setState(state) {
|
|
|
|
switch (state) {
|
|
|
|
case this.state:
|
|
|
|
return;
|
|
|
|
case 'default':
|
|
|
|
this.isOpen = false;
|
|
|
|
this.#closeModal();
|
2023-04-14 11:05:24 +03:00
|
|
|
this.selectionList.unfreeze();
|
2023-04-13 16:04:06 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-04 15:43:43 +03:00
|
|
|
get name() {
|
|
|
|
return this.args.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
get style() {
|
|
|
|
return `left: ${this.left}px; top: ${this.top}px;`;
|
|
|
|
}
|
|
|
|
|
2023-04-14 12:04:11 +03:00
|
|
|
get class() {
|
|
|
|
return `gh-placement-${this.yPlacement} gh-placement-${this.xPlacement}`;
|
|
|
|
}
|
|
|
|
|
2023-04-04 15:43:43 +03:00
|
|
|
@action
|
2023-04-14 12:04:11 +03:00
|
|
|
setup(element) {
|
|
|
|
this.element = element;
|
2023-04-04 15:43:43 +03:00
|
|
|
const dropdownService = this.dropdown;
|
|
|
|
dropdownService.on('close', this, this.close);
|
|
|
|
dropdownService.on('toggle', this, this.toggle);
|
|
|
|
}
|
|
|
|
|
|
|
|
willDestroy() {
|
|
|
|
super.willDestroy(...arguments);
|
2023-04-14 12:04:11 +03:00
|
|
|
this.element = null;
|
2023-04-04 15:43:43 +03:00
|
|
|
const dropdownService = this.dropdown;
|
|
|
|
dropdownService.off('close', this, this.close);
|
|
|
|
dropdownService.off('toggle', this, this.toggle);
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
open() {
|
2023-04-13 16:04:06 +03:00
|
|
|
this.setState('open');
|
2023-04-04 15:43:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
close() {
|
2023-04-13 16:04:06 +03:00
|
|
|
if (this.state === 'open') {
|
|
|
|
this.setState('default');
|
|
|
|
}
|
2023-04-04 15:43:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@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;
|
|
|
|
}
|
|
|
|
|
2023-04-14 12:04:11 +03:00
|
|
|
this.calculatePlacement();
|
2023-04-04 15:43:43 +03:00
|
|
|
this.open();
|
2023-04-13 16:04:06 +03:00
|
|
|
} else {
|
2023-04-04 15:43:43 +03:00
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-14 12:04:11 +03:00
|
|
|
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';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-04 15:43:43 +03:00
|
|
|
@action
|
|
|
|
stopClicks(event) {
|
|
|
|
event.stopPropagation();
|
|
|
|
}
|
2023-04-13 16:04:06 +03:00
|
|
|
|
|
|
|
@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(() => {
|
2023-04-14 11:05:24 +03:00
|
|
|
// 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);
|
2023-04-13 16:04:06 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#closeModal() {
|
|
|
|
this.#modal?.close();
|
|
|
|
this.#modal = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async performTask(taskObj) {
|
|
|
|
this.setState('loading');
|
|
|
|
await taskObj.perform();
|
|
|
|
this.setState('default');
|
|
|
|
}
|
2023-04-04 15:43:43 +03:00
|
|
|
}
|