ripcrypt/module/utils/PopoverEventManager.mjs

137 lines
3.4 KiB
JavaScript

import { getTooltipDelay } from "../consts.mjs";
import { Logger } from "./Logger.mjs";
export class PopoverEventManager {
#options;
/**
* @param {HTMLElement} element The element to attach the listeners to.
* @param {GenericPopoverMixin} popoverClass The class reference that represents the popover app
*/
constructor(element, popoverClass, options = {}) {
options.locked ??= false;
options.lockable ??= true;
this.#options = options;
this.#element = element;
this.#class = popoverClass;
element.addEventListener(`pointerenter`, this.#pointerEnterHandler.bind(this));
element.addEventListener(`pointerout`, this.#pointerOutHandler.bind(this));
element.addEventListener(`click`, this.#clickHandler.bind(this));
if (options.lockable) {
element.addEventListener(`pointerup`, this.#pointerUpHandler.bind(this));
};
};
destroy() {
this.close();
this.#element.removeEventListener(`pointerenter`, this.#pointerEnterHandler);
this.#element.removeEventListener(`pointerout`, this.#pointerOutHandler);
this.#element.removeEventListener(`click`, this.#clickHandler);
if (this.#options.lockable) {
this.#element.removeEventListener(`pointerup`, this.#pointerUpHandler);
};
this.stopOpen();
this.stopClose();
};
close() {
this.#frameless?.close({ force: true });
this.#framed?.close({ force: true });
};
stopOpen() {
if (this.#openTimeout != null) {
clearTimeout(this.#openTimeout);
this.#openTimeout = null;
};
};
stopClose() {
if (this.#closeTimeout != null) {
clearTimeout(this.#closeTimeout);
this.#closeTimeout = null;
}
};
#element;
#class;
#openTimeout = null;
#closeTimeout = null;
#frameless;
#framed;
#clickHandler() {
// Cleanup for the frameless lifecycle
this.stopOpen();
this.#frameless?.close({ force: true });
if (!this.#framed) {
const app = new this.#class({ popover: { ...this.#options, framed: true } });
this.#framed = app;
}
this.#framed.render({ force: true });
};
#pointerEnterHandler(event) {
this.stopClose();
const pos = event.target.getBoundingClientRect();
const x = pos.x + Math.floor(pos.width / 2);
const y = pos.y;
this.#openTimeout = setTimeout(
() => {
this.#openTimeout = null;
// When we have the framed version rendered, we might as well just focus
// it instead of rendering a new application
if (this.#framed?.rendered) {
this.#framed.bringToFront();
return;
};
// When the frameless is already rendered, we should just move it to the
// new location instead of spawning a new one
if (this.#frameless?.rendered) {
const { width, height } = this.#frameless.element.getBoundingClientRect();
const top = y - height;
const left = x - Math.floor(width / 2);
this.#frameless.setPosition({ left, top });
return;
}
this.#frameless = new this.#class({
popover: {
...this.#options,
framed: false,
x, y,
},
});
this.#frameless.render({ force: true });
},
getTooltipDelay(),
);
};
#pointerOutHandler() {
this.stopOpen();
this.#closeTimeout = setTimeout(
() => {
this.#closeTimeout = null;
this.#frameless?.close();
},
getTooltipDelay(),
);
};
#pointerUpHandler(event) {
if (event.button !== 1 || !this.#frameless?.rendered || Tour.tourInProgress) { return };
event.preventDefault();
this.#frameless.toggleLock();
};
};