BookStack/resources/js/components/dropdown.js
Dan Brown fa6ac211b6
Dropdowns: Fixed bad direction logic, added dynmaic height
Changes since adding notifications would cause direction to be assessed
upon max height of 80vh, which caused large dropdowns like the audit log
dropdown to drop up and/or go offscreen.
This restores the default assessment of 500px, and adds dynamic
max-height adjustment to provide more room for large dropdowns.

For #4652
2023-11-07 15:07:11 +00:00

150 lines
4.9 KiB
JavaScript

import {onSelect} from '../services/dom';
import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
import {Component} from './component';
/**
* Dropdown
* Provides some simple logic to create simple dropdown menus.
*/
export class Dropdown extends Component {
setup() {
this.container = this.$el;
this.menu = this.$refs.menu;
this.toggle = this.$refs.toggle;
this.moveMenu = this.$opts.moveMenu;
this.bubbleEscapes = this.$opts.bubbleEscapes === 'true';
this.direction = (document.dir === 'rtl') ? 'right' : 'left';
this.body = document.body;
this.showing = false;
this.hide = this.hide.bind(this);
this.setupListeners();
}
show(event = null) {
this.hideAll();
this.menu.style.display = 'block';
this.menu.classList.add('anim', 'menuIn');
this.toggle.setAttribute('aria-expanded', 'true');
const menuOriginalRect = this.menu.getBoundingClientRect();
let heightOffset = 0;
const toggleHeight = this.toggle.getBoundingClientRect().height;
const dropUpwards = menuOriginalRect.bottom > window.innerHeight;
const containerRect = this.container.getBoundingClientRect();
// If enabled, Move to body to prevent being trapped within scrollable sections
if (this.moveMenu) {
this.body.appendChild(this.menu);
this.menu.style.position = 'fixed';
this.menu.style.width = `${menuOriginalRect.width}px`;
this.menu.style.left = `${menuOriginalRect.left}px`;
if (dropUpwards) {
heightOffset = (window.innerHeight - menuOriginalRect.top - toggleHeight / 2);
} else {
heightOffset = menuOriginalRect.top;
}
}
// Adjust menu to display upwards if near the bottom of the screen
if (dropUpwards) {
this.menu.style.top = 'initial';
this.menu.style.bottom = `${heightOffset}px`;
const maxHeight = (window.innerHeight - 40) - (window.innerHeight - containerRect.bottom);
this.menu.style.maxHeight = `${Math.floor(maxHeight)}px`;
} else {
this.menu.style.top = `${heightOffset}px`;
this.menu.style.bottom = 'initial';
const maxHeight = (window.innerHeight - 40) - containerRect.top;
this.menu.style.maxHeight = `${Math.floor(maxHeight)}px`;
}
// Set listener to hide on mouse leave or window click
this.menu.addEventListener('mouseleave', this.hide);
window.addEventListener('click', clickEvent => {
if (!this.menu.contains(clickEvent.target)) {
this.hide();
}
});
// Focus on first input if existing
const input = this.menu.querySelector('input');
if (input !== null) input.focus();
this.showing = true;
const showEvent = new Event('show');
this.container.dispatchEvent(showEvent);
if (event) {
event.stopPropagation();
}
}
hideAll() {
for (const dropdown of window.$components.get('dropdown')) {
dropdown.hide();
}
}
hide() {
this.menu.style.display = 'none';
this.menu.classList.remove('anim', 'menuIn');
this.toggle.setAttribute('aria-expanded', 'false');
this.menu.style.top = '';
this.menu.style.bottom = '';
this.menu.style.maxHeight = '';
if (this.moveMenu) {
this.menu.style.position = '';
this.menu.style[this.direction] = '';
this.menu.style.width = '';
this.menu.style.left = '';
this.container.appendChild(this.menu);
}
this.showing = false;
}
setupListeners() {
const keyboardNavHandler = new KeyboardNavigationHandler(this.container, event => {
this.hide();
this.toggle.focus();
if (!this.bubbleEscapes) {
event.stopPropagation();
}
}, event => {
if (event.target.nodeName === 'INPUT') {
event.preventDefault();
event.stopPropagation();
}
this.hide();
});
if (this.moveMenu) {
keyboardNavHandler.shareHandlingToEl(this.menu);
}
// Hide menu on option click
this.container.addEventListener('click', event => {
const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.includes(event.target)) {
this.hide();
}
});
onSelect(this.toggle, event => {
event.stopPropagation();
event.preventDefault();
this.show(event);
if (event instanceof KeyboardEvent) {
keyboardNavHandler.focusNext();
}
});
}
}