Ghost/ghost/admin/app/components/gh-context-menu.js

209 lines
5.4 KiB
JavaScript
Raw Normal View History

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');
}
}