UNPKG

@eclipse-scout/core

Version:
183 lines (170 loc) 8.53 kB
/* * Copyright (c) 2010, 2024 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import $ from 'jquery'; /** * Utility methods for focus. */ export const focusUtils = { /** * @returns whether the given element is focusable by mouse. */ isFocusableByMouse(element: HTMLElement | JQuery): boolean { return $.ensure(element).closest('.unfocusable').length === 0; }, /** * @returns whether the element must not gain the focus, even if it has a tabindex. This is only true for elements with tabindex="-2". */ isFocusPrevented(element: HTMLElement | JQuery): boolean { return Number($.ensure(element).attr('tabindex')) === -2; }, /** * @param $entryPoint the entry point of the current {@link Session} * @param nativeFocusable whether to include elements that we consider to be unfocusable but would gain the focus by the browser if we did not prevent it (elements with tabindex="-2"). Default is false. * @returns all parents that are focusable by mouse inside a focus boundary (marked by elements having the class .focus-boundary) */ getParentsFocusableByMouse(element: HTMLElement | JQuery, $entryPoint: JQuery, nativeFocusable = false): JQuery { return $.ensure(element) .parentsUntil('.focus-boundary', nativeFocusable ? ':focusable-native' : ':focusable') // Stay inside focus boundaries (e.g. search forms should not consider parent table) .not($entryPoint) // Exclude $entryPoint as all elements are its descendants. However, the $entryPoint is only focusable to provide Portlet support. .filter((index, elem) => focusUtils.isFocusableByMouse(elem)); }, /** * @param $entryPoint the entry point of the current {@link Session} * @param nativeFocusable whether to include elements that we consider to be unfocusable but would gain the focus by the browser if we did not prevent it (elements with tabindex="-2"). Default is false. * @returns the given element if it is focusable by mouse, or the first parent that is focusable by mouse. */ closestFocusableByMouse(element: HTMLElement | JQuery, $entryPoint: JQuery, nativeFocusable = false): JQuery { let $element = $.ensure(element); if ($element.is(nativeFocusable ? ':focusable-native' : ':focusable') && focusUtils.isFocusableByMouse($element)) { return $element; } return focusUtils.getParentsFocusableByMouse($element, $entryPoint, nativeFocusable).first(); }, /** * @returns whether the given element has a parent which is focusable by mouse. */ containsParentFocusableByMouse(element: HTMLElement | JQuery, $entryPoint: JQuery): boolean { return focusUtils.getParentsFocusableByMouse(element, $entryPoint).length > 0; }, /** * @returns whether the given element contains content which is selectable to the user, e.g. to be copied into clipboard. * It also returns true for disabled text-fields, because the user must be able to select and copy text from these text-fields. */ isSelectableText(element: HTMLElement | JQuery): boolean { let $element = $(element); // Find the closest element which has a 'user-select' with a value other than 'auto'. If that value // is 'none', the text is not selectable. This code mimics the "inheritance behavior" of the CSS // property "-moz-user-select: -moz-none" as described in [1]. This does not seem to work in some // cases in Firefox, even with bug [2] fixed. As a workaround, we implement the desired behavior here. // // Note: Some additional CSS rules are required for events other than 'mousedown', see main.css. // // [1] https://developer.mozilla.org/en-US/docs/Web/CSS/user-select // [2] https://bugzilla.mozilla.org/show_bug.cgi?id=648624 let $el = $element; while ($el.css('user-select') === 'auto') { $el = $el.parent(); // Fix for Firefox: parent of BODY element is HtmlDocument. When calling $el.css on the HtmlDocument // Firefox throws an error that ownerDocument is undefined. Thus, we don't go higher than BODY element // and assume body is never selectable. if ($el.is('body')) { return false; } } if ($el.css('user-select') === 'none') { return false; } if ($element.is('input[disabled][type=text], textarea[disabled]')) { return true; } // When element or its children have text, it should be selectable. // The old implementation only looked at the text of the element itself // but not at the text of its children. With the old approach it was not // possible to select something inside a TD, for instance: // <td><span>Foo</span></td> // Because TD itself has no text at all. // When an element has no text we return false, because if we could select // empty elements, we'd lose focus more often. return $element.text().trim().length > 0; }, /** * @returns true if the element or one of its parents is draggable. */ isDraggable(element: HTMLElement | JQuery): boolean { return $.ensure(element).closest('[draggable="true"]').length > 0; }, /** * @returns true if the given HTML element is the active element in its own document, false otherwise. */ isActiveElement(element: HTMLElement | JQuery): boolean { if (!element) { return false; } let activeElement: Element; if (element instanceof $) { activeElement = (element as JQuery).activeElement(true); element = element[0]; } else { let htmlElement = element as HTMLElement; let ownerDocument = htmlElement instanceof Document ? htmlElement : htmlElement.ownerDocument; activeElement = ownerDocument.activeElement; } return activeElement === element; }, /** * Stores the currently focused element and focuses this element again in the next animation frame if the focus changed to the entry point element. * This is useful if the current task would focus the entry point element which cannot be prevented. * * @param $entryPoint the entry point of the current {@link Session} * @param options options to be passed to the {@link HTMLElement.focus} call */ restoreFocusLater($entryPoint: JQuery, options?: FocusOptions) { // queueMicrotask does not work, it looks like the microtask will be executed before the focus change. // requestAnimationFrame also prevents flickering (compared to setTimeout) $entryPoint = $.ensure($entryPoint); if (!$entryPoint.length) { return; // nothing to do } let doc = $entryPoint.document(true); let prevFocusedElement = doc.activeElement as HTMLElement; requestAnimationFrame(() => { let focusedElement = doc.activeElement as HTMLElement; // Restore previous focus if the current active element is an element we don't want to be focused (the $entryPoint or tabindex="-2") if (focusedElement === $entryPoint[0] || focusUtils.isFocusPrevented(focusedElement)) { prevFocusedElement.focus(options); } }); }, /** * Sets the focus to the given target element just before the next repaint (using requestAnimationFrame). * This allows other event handlers to be fired before the focus is actually changed. * * @param target the element to be focused * @param options options to be passed to the {@link HTMLElement.focus} call */ focusLater(target: HTMLElement | JQuery, options?: FocusOptions) { let $target = $.ensure(target); if (!$target.length) { return; // nothing to do } let doc = $target.document(true); let prevFocusedElement = doc.activeElement; requestAnimationFrame(() => { // Check if the active element is the same as before. If not, someone has changed the focus in the meantime and the // scheduled focusLater() request is obsolete. If the active element is considered to be unfocusable via tabindex="-2", // we always change the focus to the target element. (This can happen if the global mouse down handler did not suppress // the default behavior to avoid cancelling other events, e.g. 'dragstart'). let focusedElement = doc.activeElement as HTMLElement; if (focusedElement === prevFocusedElement || focusUtils.isFocusPrevented(focusedElement)) { $target[0].focus(options); } }); } };