1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-28 17:02:04 +03:00

Lexical: Updated dropdown handling to match tinymce behaviour

Now toolbars stay open on mouse-out, and close on other toolbar open,
outside click or an accepted action.
To support:
- Added new system to track and manage open dropdowns.
- Added way for buttons to optionally emit events upon actions.
- Added way to listen for events.
- Used the above to control when dropdowns should hide on action, since
  some dont (like overflow containers and split dropdown buttons).
This commit is contained in:
Dan Brown
2025-05-25 16:28:42 +01:00
parent 3280919370
commit 1243108e0f
7 changed files with 120 additions and 43 deletions

View File

@ -34,57 +34,97 @@ function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean
}
}
export function handleDropdown(options: HandleDropdownParams) {
const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options;
let clickListener: Function|null = null;
export class DropDownManager {
const hide = () => {
protected dropdownOptions: WeakMap<HTMLElement, HandleDropdownParams> = new WeakMap();
protected openDropdowns: Set<HTMLElement> = new Set();
constructor() {
this.onMenuMouseOver = this.onMenuMouseOver.bind(this);
window.addEventListener('click', (event: MouseEvent) => {
const target = event.target as HTMLElement;
this.closeAllNotContainingElement(target);
});
}
protected closeAllNotContainingElement(element: HTMLElement): void {
for (const menu of this.openDropdowns) {
if (!menu.parentElement?.contains(element)) {
this.closeDropdown(menu);
}
}
}
protected onMenuMouseOver(event: MouseEvent): void {
const target = event.target as HTMLElement;
this.closeAllNotContainingElement(target);
}
/**
* Close all open dropdowns.
*/
public closeAll(): void {
for (const menu of this.openDropdowns) {
this.closeDropdown(menu);
}
}
protected closeDropdown(menu: HTMLElement): void {
menu.hidden = true;
menu.style.removeProperty('position');
menu.style.removeProperty('left');
menu.style.removeProperty('top');
if (clickListener) {
window.removeEventListener('click', clickListener as EventListener);
}
this.openDropdowns.delete(menu);
menu.removeEventListener('mouseover', this.onMenuMouseOver);
const onClose = this.getOptions(menu).onClose;
if (onClose) {
onClose();
}
};
}
const show = () => {
protected openDropdown(menu: HTMLElement): void {
const {toggle, showAside, onOpen} = this.getOptions(menu);
menu.hidden = false
positionMenu(menu, toggle, Boolean(showAside));
clickListener = (event: MouseEvent) => {
if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) {
hide();
}
}
window.addEventListener('click', clickListener as EventListener);
this.openDropdowns.add(menu);
menu.addEventListener('mouseover', this.onMenuMouseOver);
if (onOpen) {
onOpen();
}
};
const toggleShowing = (event: MouseEvent) => {
menu.hasAttribute('hidden') ? show() : hide();
};
toggle.addEventListener('click', toggleShowing);
if (showOnHover) {
toggle.addEventListener('mouseenter', toggleShowing);
}
menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => {
// Prevent mouseleave hiding if withing the same bounds of the toggle.
// Avoids hiding in the event the mouse is interrupted by a high z-index
// item like a browser scrollbar.
const toggleBounds = toggle.getBoundingClientRect();
const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left;
const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top;
const withinToggle = withinX && withinY;
if (!withinToggle) {
hide();
protected getOptions(menu: HTMLElement): HandleDropdownParams {
const options = this.dropdownOptions.get(menu);
if (!options) {
throw new Error(`Can't find options for dropdown menu`);
}
});
return options;
}
/**
* Add handling for a new dropdown.
*/
public handle(options: HandleDropdownParams) {
const {menu, toggle, showOnHover} = options;
// Register dropdown
this.dropdownOptions.set(menu, options);
// Configure default events
const toggleShowing = (event: MouseEvent) => {
menu.hasAttribute('hidden') ? this.openDropdown(menu) : this.closeDropdown(menu);
};
toggle.addEventListener('click', toggleShowing);
if (showOnHover) {
toggle.addEventListener('mouseenter', () => {
this.openDropdown(menu);
});
}
}
}