UNPKG

outsystems-ui

Version:
724 lines (620 loc) 21.5 kB
// eslint-disable-next-line @typescript-eslint/no-unused-vars namespace OutSystems.OSUI.Utils.Menu { // OrientationChange callback to be stored and removed on Destroy let _onOrientationChangeCallback: OSFramework.OSUI.GlobalCallbacks.Generic; // App Properties const _appProp = { device: { isLandscape: false, type: undefined as OSFramework.OSUI.GlobalEnum.DeviceType, }, layout: { element: undefined as HTMLElement, isAsideExpandable: false, isAsideMenu: false, isAsideMenuOverlay: false, isBlank: false, isTopMenu: false, }, menu: { element: undefined as HTMLElement, hasEventListeners: false, isOpen: false, }, }; // Focusable elements on the menu context since it's different from the default constant that excludes disabled elements with tabindex=-1 const _menuFocusableElems = 'a[href]:not([disabled]), [tabindex], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled])'; // Add Menu Event Listeners const _addMenuEventListeners = (hasTriggeredByAPI = false): void => { // Ensure events will be removed when SetMenuListeners "API" has been triggered and before readding them again if (hasTriggeredByAPI) { _removeMenuEventListeners(); } // Check if the keydown event should be added const shouldKeyDownBeAdded = OSFramework.OSUI.Helper.DeviceInfo.IsDesktop === false || _appProp.layout.isAsideMenuOverlay || _appProp.layout.isAsideExpandable; if (shouldKeyDownBeAdded && _appProp.menu.hasEventListeners === false) { _appProp.menu.hasEventListeners = true; _appProp.menu.element.addEventListener(OSFramework.OSUI.GlobalEnum.HTMLEvent.keyDown, _menuOnKeypress); } }; // Method that add the OnResize handler const _addMenuOnResize = (): void => { OSFramework.OSUI.Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler( OSFramework.OSUI.Event.DOMEvents.Listeners.Type.WindowResize, _onResizeCallbackHandler ); }; // Method that add the OrientationChange handler, used only for TabletApps (Template_TabletApp > Menu) const _addMenuOnOrientationChange = (callback: OSFramework.OSUI.GlobalCallbacks.Generic): void => { if (callback && OSFramework.OSUI.Helper.DeviceInfo.IsTablet) { _onOrientationChangeCallback = callback; OSFramework.OSUI.Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler( OSFramework.OSUI.Event.DOMEvents.Listeners.Type.OrientationChange, _onOrientationChangeCallbackHandler ); } }; const _hideMenu = (): void => { // Update app properties if (_appProp.menu.element === undefined || _appProp.layout.element === undefined) { _setAppProps(); } const menuIcon = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'menu-icon'); _appProp.layout.element.classList.remove('menu-visible'); _appProp.menu.isOpen = false; menuIcon?.focus(); _updatePropsAndAttrs(); }; // Menu on keypress handler const _menuOnKeypress = function (e: KeyboardEvent) { const isTabPressed = e.key === 'Tab'; const isEscapedPressed = e.key === 'Escape'; const isShiftKey = e.shiftKey; const focusableEls = OSFramework.OSUI.Helper.Dom.TagSelectorAll(_appProp.menu.element, _menuFocusableElems); const firstFocusableEl = focusableEls[0] as HTMLElement; const lastFocusableEl = focusableEls[focusableEls.length - 1] as HTMLElement; if (!isTabPressed && !isEscapedPressed) { return; } //If ESC, Close Menu if (isEscapedPressed && _appProp.menu.isOpen) { e.preventDefault(); e.stopPropagation(); _toggleMenu(); } if (isShiftKey) { if (document.activeElement === firstFocusableEl) { lastFocusableEl.focus(); e.preventDefault(); } } else if (document.activeElement === lastFocusableEl) { firstFocusableEl.focus(); e.preventDefault(); } }; // OrientationChange handler const _onOrientationChangeCallbackHandler = (callback: OSFramework.OSUI.GlobalCallbacks.Generic): void => { if (callback) { OSFramework.OSUI.Helper.ApplySetTimeOut(() => { _onOrientationChangeCallback(); }, 300); } }; // OnResize handler const _onResizeCallbackHandler = (): void => { // Get the current device type const currentDeviceType = OSFramework.OSUI.Helper.DeviceInfo.GetDeviceType(); // Check if the device type is the same as the current device type, if so, return to prevent unnecessary calls if (_appProp.device.type === currentDeviceType) { return; } // Update the device type on the app properties _appProp.device.type = currentDeviceType; // Check if the menu is open if (_appProp.menu.isOpen) { // Close the menu, internally it will update the menu attributes _toggleMenu(); } else { // Update app properties and menu attributes _updatePropsAndAttrs(); } // Remove the menu event listeners since device type changed _removeMenuEventListeners(); // ReAdd the menu event listeners _addMenuEventListeners(); }; // Remove Menu Event Listeners const _removeMenuEventListeners = (): void => { if (_appProp.menu.hasEventListeners) { _appProp.menu.hasEventListeners = false; _appProp.menu.element.removeEventListener('keydown', _menuOnKeypress); } }; // Method that removes the OrientationChange handler, used only for TabletApps (Template_TabletApp > Menu) const _removeMenuOnOrientationChange = (): void => { if (_onOrientationChangeCallback && OSFramework.OSUI.Helper.DeviceInfo.IsTablet) { OSFramework.OSUI.Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler( OSFramework.OSUI.Event.DOMEvents.Listeners.Type.OrientationChange, _onOrientationChangeCallbackHandler ); _onOrientationChangeCallback = undefined; } }; // Method that removes the OnResize handler const _removeMenuOnResize = (): void => { OSFramework.OSUI.Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler( OSFramework.OSUI.Event.DOMEvents.Listeners.Type.WindowResize, _onResizeCallbackHandler ); }; // Set A11Y attributes to the menu and its childrens const _setA11YAttributes = (menu?: HTMLElement): void => { // Check if the given menu is undefined if (menu === undefined) { // If so, get the menu from the appProps menu = _appProp.menu.element; } // Get all focusable elements inside const focusableEls = Array.from(menu.querySelectorAll(_menuFocusableElems)); // Get all menu items const menuItems = Array.from(menu.querySelectorAll('.app-menu-links > a')); // Remove the elements that are inside a submenu to be manged by the submenu itself for (let i = 0; i < focusableEls.length; ++i) { if (focusableEls[i].closest('.osui-submenu__items')) { focusableEls.splice(i, 1); } } // Check if there are any menu items if (menuItems.length > 0) { // Add role menuitem to all menu direct children for (const item of menuItems) { if (!item.hasAttribute('role')) { item.setAttribute('role', 'menuitem'); } } } // Check if the menu is open or if it's a desktop device with the aside menu overlay or expandable const enableA11Y = _appProp.menu.isOpen || (OSFramework.OSUI.Helper.DeviceInfo.IsDesktop && _appProp.layout.isAsideMenuOverlay === false && _appProp.layout.isAsideExpandable === false); // Set the tabindex and aria-expanded attributes based on the enableA11Y flag if (enableA11Y) { OSFramework.OSUI.Helper.A11Y.TabIndexTrue(menu); OSFramework.OSUI.Helper.A11Y.AriaExpandedTrue(menu); for (const item of focusableEls) { OSFramework.OSUI.Helper.A11Y.TabIndexTrue(item as HTMLElement); } } else { OSFramework.OSUI.Helper.A11Y.TabIndexFalse(menu); OSFramework.OSUI.Helper.A11Y.AriaExpandedFalse(menu); for (const item of focusableEls) { OSFramework.OSUI.Helper.A11Y.TabIndexFalse(item as HTMLElement); } } }; // Set the properties used to properly manage several menu behaviours const _setAppProps = (): void => { _appProp.menu.element = OSFramework.OSUI.Helper.Dom.ClassSelector(document.body, 'app-menu-content') || OSFramework.OSUI.Helper.Dom.ClassSelector(document.body, 'app-menu-container'); _appProp.layout.element = OSFramework.OSUI.Helper.Dom.ClassSelector(document.body, 'layout'); _appProp.device.type = OSFramework.OSUI.Helper.DeviceInfo.GetDeviceType(); _appProp.device.isLandscape = OSFramework.OSUI.Helper.DeviceInfo.GetDeviceOrientation() === OSFramework.OSUI.GlobalEnum.DeviceOrientation.landscape; _appProp.menu.isOpen = OSFramework.OSUI.Helper.Dom.Styles.ContainsClass( _appProp.layout.element, 'menu-visible' ); _appProp.layout.isTopMenu = OSFramework.OSUI.Helper.Dom.Styles.ContainsClass( _appProp.layout.element, 'layout-top' ); _appProp.layout.isAsideMenu = OSFramework.OSUI.Helper.Dom.Styles.ContainsClass( _appProp.layout.element, 'aside-visible' ); _appProp.layout.isAsideMenuOverlay = OSFramework.OSUI.Helper.Dom.Styles.ContainsClass( _appProp.layout.element, 'aside-overlay' ); _appProp.layout.isBlank = OSFramework.OSUI.Helper.Dom.Styles.ContainsClass( _appProp.layout.element, 'layout-blank' ); _appProp.layout.isAsideExpandable = OSFramework.OSUI.Helper.Dom.Styles.ContainsClass( _appProp.layout.element, 'aside-expandable' ); }; // Method that will make menu visible const _showMenu = (): void => { // Check if the menu should be shown const shouldShowMenu = OSFramework.OSUI.Helper.DeviceInfo.IsDesktop === false || _appProp.layout.isAsideMenuOverlay || _appProp.layout.isAsideExpandable; // If not, prevent menu to be shown if (shouldShowMenu === false) { return; } // Update app properties if (_appProp.menu.element === undefined || _appProp.layout.element === undefined) { _setAppProps(); } _appProp.layout.element.classList.add('menu-visible'); _appProp.menu.element.focus(); _appProp.menu.isOpen = true; _updatePropsAndAttrs(); }; // Method that toggles the menu visibility const _toggleMenu = (): void => { if (_appProp.menu.isOpen) { _hideMenu(); } else { _showMenu(); } }; // Update the app properties and attributes of the menu const _updatePropsAndAttrs = (): void => { _setAppProps(); _setA11YAttributes(); }; // Expose the_addMenuOnOrientationChange method, used only for TabletApps (Template_TabletApp > Menu) export const addMenuOnOrientationChange = _addMenuOnOrientationChange; // Expose the _removeMenuOnOrientationChange method, used only for TabletApps (Template_TabletApp > Menu) export const removeMenuOnOrientationChange = _removeMenuOnOrientationChange; /** * Checks if the menu can be draggable * * @export * @return {*} {string} */ export function IsMenuDraggable(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailCheckIsMenuDraggable, hasValue: true, callback: () => { // Update appProperties _setAppProps(); const _layoutSideMenuVisible = _appProp.layout.isAsideMenu; let _addDragGestures = false; if (window.cordova !== undefined && DeviceDetection.IsRunningAsPWA() === false) { if ( (_layoutSideMenuVisible && OSFramework.OSUI.Helper.DeviceInfo.IsDesktop) || (_layoutSideMenuVisible && OSFramework.OSUI.Helper.DeviceInfo.IsTablet && _appProp.device.isLandscape) ) { _addDragGestures = false; } else { _addDragGestures = true; } } return _addDragGestures; }, }); return result; } /** * Closes the extended menu content * * @export * @return {*} {string} */ export function MenuHide(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetExtendedMenuHide, callback: () => { /** * Get a deprecated menu element * * This can be present at older versions of the Layouts * * @deprecated */ const deprecatedMenu = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'menu'); if (deprecatedMenu) { const appMenu = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'app-menu-container'); const menuOverlay = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'menu-background'); const onTransitionEnd = () => { OSFramework.OSUI.Helper.Dom.Styles.RemoveClass(deprecatedMenu, 'menu--animatable'); deprecatedMenu.removeEventListener('transitionend', onTransitionEnd); }; OSFramework.OSUI.Helper.Dom.Styles.RemoveClass(deprecatedMenu, 'menu--visible'); if (menuOverlay) { menuOverlay.style.opacity = ''; } appMenu.style.transform = ''; deprecatedMenu.addEventListener('transitionend', onTransitionEnd, false); } else { _hideMenu(); } _updatePropsAndAttrs(); }, }); return result; } /** * Opens the extended menu content * * @export * @return {*} {string} */ export function MenuShow(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetExtendedMenuShow, callback: () => { /** * Get a deprecated menu element * * This can be present at older versions of the Layouts * * @deprecated */ const deprecatedMenu = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'menu'); if (deprecatedMenu) { OSFramework.OSUI.Helper.Dom.Styles.AddClass(deprecatedMenu, 'menu--visible'); OSFramework.OSUI.Helper.Dom.Styles.AddClass(deprecatedMenu, 'menu--animatable'); _updatePropsAndAttrs(); } else { _showMenu(); } }, }); return result; } /** * OnDestroy method that is called when the menu is destroyed * * @export * @return {*} {string} */ export function OnDestroy(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetMenuOnDestroy, callback: () => { _removeMenuOnResize(); // Remove the OrientationChange handler only for TabletApps _removeMenuOnOrientationChange(); }, }); return result; } /** * OnReady method that is called when the menu is ready * * @export * @return {*} {string} */ export function OnReady(callback: OSFramework.OSUI.GlobalCallbacks.Generic): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetMenuOnReady, callback: () => { _updatePropsAndAttrs(); _addMenuOnResize(); // Add the OrientationChange handler only for TabletApps _addMenuOnOrientationChange(callback); }, }); return result; } /** * Adds the selected states to menu items * * @export * @param {string} WidgetId * @param {number} ActiveItem * @param {number} ActiveSubItem * @return {*} {string} */ export function SetActiveMenuItems(WidgetId: string, ActiveItem: number, ActiveSubItem: number): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetActiveMenuItems, callback: () => { let widgetSelector = ''; if (WidgetId !== '') { widgetSelector = '#' + WidgetId + ' '; } const appMenuLinks = OSFramework.OSUI.Helper.Dom.TagSelector(document.body, widgetSelector + '.app-menu-links') || OSFramework.OSUI.Helper.Dom.TagSelector(document.body, widgetSelector + '.app-sidemenu-links'); if (appMenuLinks) { const activeLinkBlock = appMenuLinks.children[ActiveItem] as HTMLElement; if (activeLinkBlock) { OSFramework.OSUI.Helper.Dom.Styles.AddClass(activeLinkBlock, 'active'); const activeSubMenu = OSFramework.OSUI.Helper.Dom.ClassSelector(activeLinkBlock, 'submenu') || OSFramework.OSUI.Helper.Dom.ClassSelector(activeLinkBlock, 'osui-submenu'); if (activeSubMenu) { OSFramework.OSUI.Helper.Dom.Styles.AddClass(activeSubMenu, 'active'); const subMenuItem = OSFramework.OSUI.Helper.Dom.ClassSelector(activeSubMenu, 'submenu-items') || OSFramework.OSUI.Helper.Dom.ClassSelector(activeSubMenu, 'osui-submenu__items'); const activeSubMenuItem = subMenuItem.querySelectorAll( '[' + OSFramework.OSUI.Constants.A11YAttributes.Role.AttrName + "='" + OSFramework.OSUI.Constants.A11YAttributes.Role.MenuItem + "']" )[ActiveSubItem] as HTMLElement; if (activeSubMenuItem) { OSFramework.OSUI.Helper.Dom.Styles.AddClass(activeSubMenuItem, 'active'); } } } } }, }); return result; } /** * Use this action on a BottomBar block to set an active state to a BottomBarItem * Used by default on the BottomBar block inside the OutSystems UI Mobile Templates * * @export * @param {*} [ActiveItem=-1] * @return {*} {string} */ export function SetBottomBarActiveElement(ActiveItem = -1): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetBottomBarActiveElement, callback: () => { const bottomBar = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'bottom-bar'); const bottomBarChild = (bottomBar ? bottomBar.children[ActiveItem] : undefined) as HTMLElement; if (bottomBar && bottomBarChild) { OSFramework.OSUI.Helper.Dom.Styles.AddClass(bottomBarChild, 'active'); } }, }); return result; } /** * Supports the items accessible on the menu * * @export * @return {*} {string} */ export function SetMenuAttributes(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetMenuAttributes, callback: () => { if (_appProp.menu.element === undefined) { _setAppProps(); } _setA11YAttributes(); }, }); return result; } /** * Changes the menu icon automatic behavior * * @export * @param {string} MenuAction * @return {*} {string} */ export function SetMenuIcon(MenuAction: string): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetMenuIcon, callback: () => { if (MenuAction === 'Auto') { const appMenu = Array.prototype.slice.call( OSFramework.OSUI.Helper.Dom.TagSelectorAll(document, '.bottom-bar a') ); const bottomBar = Array.prototype.slice.call( OSFramework.OSUI.Helper.Dom.TagSelectorAll(document, '.app-menu a') ); const links = appMenu.concat(bottomBar); let showMenu = false; for (let i = 0; i < links.length; i++) { /* removing platform timestamps */ const timestampIndex = window.location.href.indexOf('_ts') - 1; const currentPage = timestampIndex > 0 ? window.location.href.substring(0, timestampIndex) : window.location.href; if (links[i].attributes['href']) { if ( currentPage.indexOf(links[i].attributes['href'].value) >= 0 || currentPage[currentPage.length - 1] === '/' ) { showMenu = window.history ? window.history.length > 0 : true; } } } const menuIcon = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'app-menu-icon'); if (showMenu) { OSFramework.OSUI.Helper.Dom.Styles.RemoveClass(menuIcon, 'back'); } else { OSFramework.OSUI.Helper.Dom.Styles.AddClass(menuIcon, 'back'); } } }, }); return result; } /** * Makes the menu accessibility-ready * * @export * @return {*} {string} */ export function SetMenuIconListeners(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetMenuIconListeners, callback: () => { const menuIcon = OSFramework.OSUI.Helper.Dom.ClassSelector(document, 'menu-icon'); if (menuIcon) { const menuIconOnKeypress = function (e) { //If enter or space use the menuIcon to validate if (e.keyCode === 32 || e.keyCode === 13) { e.preventDefault(); e.stopPropagation(); _toggleMenu(); } }; menuIcon.addEventListener(OSFramework.OSUI.GlobalEnum.HTMLEvent.keyDown, menuIconOnKeypress); } }, }); return result; } /** * Makes the menu navigation accessibility-ready * * @export * @param {string} WidgetId * @return {*} {string} */ export function SetMenuListeners(WidgetId: string): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailSetMenuListeners, callback: () => { let widgetSelector = ''; if (WidgetId !== '') { widgetSelector = '#' + WidgetId; } // Update app properties if (_appProp.menu.element === undefined || _appProp.layout.element === undefined) { _setAppProps(); } // Get the menu based on the given widgetId const menu = OSFramework.OSUI.Helper.Dom.TagSelector( document.body, widgetSelector + '.app-menu-content' ); // Update menu element _appProp.menu.element = menu; if (_appProp.layout.element && menu) { // Invoking setTimeout to schedule the callback to be run asynchronously OSFramework.OSUI.Helper.AsyncInvocation(() => { _addMenuEventListeners(true); }); } }, }); return result; } /** * Toggles the side menu visibility * * @export * @return {*} {string} */ export function ToggleSideMenu(): string { const result = OutSystems.OSUI.Utils.CreateApiResponse({ errorCode: ErrorCodes.Utilities.FailToggleSideMenu, callback: () => { _toggleMenu(); }, }); return result; } }