-import {handleDropdown} from "../helpers/dropdowns";
import {EditorContainerUiElement, EditorUiElement} from "../core";
import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
import {el} from "../../../utils/dom";
showOnHover?: boolean;
direction?: 'vertical'|'horizontal';
showAside?: boolean;
+ hideOnAction?: boolean;
button: EditorBasicButtonDefinition|EditorButton;
};
showOnHover: false,
direction: 'horizontal',
showAside: undefined,
+ hideOnAction: true,
button: {label: 'Menu'},
}
},
isActive: () => {
return this.open;
- }
+ },
});
}
class: 'editor-dropdown-menu-container',
}, [button, menu]);
- handleDropdown({toggle: button, menu : menu,
+ this.getContext().manager.dropdowns.handle({toggle: button, menu : menu,
showOnHover: this.options.showOnHover,
showAside: typeof this.options.showAside === 'boolean' ? this.options.showAside : (this.options.direction === 'vertical'),
onOpen : () => {
this.getContext().manager.triggerStateUpdateForElement(this.button);
}});
+ if (this.options.hideOnAction) {
+ this.onEvent('button-action', () => {
+ this.getContext().manager.dropdowns.closeAll();
+ }, wrapper);
+ }
+
return wrapper;
}
}
\ No newline at end of file
}
export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
- action: (context: EditorUiContext, button: EditorButton) => void;
+ /**
+ * The action to perform when the button is used.
+ * This can return false to indicate that the completion of the action should
+ * NOT be communicated to parent UI elements, which is what occurs by default.
+ */
+ action: (context: EditorUiContext, button: EditorButton) => void|false;
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
setup?: (context: EditorUiContext, button: EditorButton) => void;
}
protected onClick() {
- this.definition.action(this.getContext(), this);
+ const result = this.definition.action(this.getContext(), this);
+ if (result !== false) {
+ this.emitEvent('button-action');
+ }
}
protected updateActiveState(selection: BaseSelection|null) {
}
}
-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) => {
+ 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;
- // 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;
+ // Register dropdown
+ this.dropdownOptions.set(menu, options);
- if (!withinToggle) {
- hide();
+ // 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);
+ });
}
- });
+ }
}
\ No newline at end of file