UNPKG

@angular/core

Version:

Angular - the core framework

1,471 lines (1,462 loc) • 76.5 kB
/** * @license Angular v18.1.0 * (c) 2010-2024 Google LLC. https://angular.io/ * License: MIT */ const Attribute = { /** * The jsaction attribute defines a mapping of a DOM event to a * generic event (aka jsaction), to which the actual event handlers * that implement the behavior of the application are bound. The * value is a semicolon separated list of colon separated pairs of * an optional DOM event name and a jsaction name. If the optional * DOM event name is omitted, 'click' is assumed. The jsaction names * are dot separated pairs of a namespace and a simple jsaction * name. * * See grammar in README.md for expected syntax in the attribute value. */ JSACTION: 'jsaction', }; const Char = { /** * The separator between the namespace and the action name in the * jsaction attribute value. */ NAMESPACE_ACTION_SEPARATOR: '.', /** * The separator between the event name and action in the jsaction * attribute value. */ EVENT_ACTION_SEPARATOR: ':', }; /* * Names of events that are special to jsaction. These are not all * event types that are legal to use in either HTML or the addEvent() * API, but these are the ones that are treated specially. All other * DOM events can be used in either addEvent() or in the value of the * jsaction attribute. Beware of browser specific events or events * that don't bubble though: If they are not mentioned here, then * event contract doesn't work around their peculiarities. */ const EventType = { /** * Mouse middle click, introduced in Chrome 55 and not yet supported on * other browsers. */ AUXCLICK: 'auxclick', /** * The change event fired by browsers when the `value` attribute of input, * select, and textarea elements are changed. */ CHANGE: 'change', /** * The click event. In addEvent() refers to all click events, in the * jsaction attribute it refers to the unmodified click and Enter/Space * keypress events. In the latter case, a jsaction click will be triggered, * for accessibility reasons. See clickmod and clickonly, below. */ CLICK: 'click', /** * Specifies the jsaction for a modified click event (i.e. a mouse * click with the modifier key Cmd/Ctrl pressed). This event isn't * separately enabled in addEvent(), because in the DOM, it's just a * click event. */ CLICKMOD: 'clickmod', /** * Specifies the jsaction for a click-only event. Click-only doesn't take * into account the case where an element with focus receives an Enter/Space * keypress. This event isn't separately enabled in addEvent(). */ CLICKONLY: 'clickonly', /** * The dblclick event. */ DBLCLICK: 'dblclick', /** * Focus doesn't bubble, but you can use it in addEvent() and * jsaction anyway. EventContract does the right thing under the * hood. */ FOCUS: 'focus', /** * This event only exists in IE. For addEvent() and jsaction, use * focus instead; EventContract does the right thing even though * focus doesn't bubble. */ FOCUSIN: 'focusin', /** * Analog to focus. */ BLUR: 'blur', /** * Analog to focusin. */ FOCUSOUT: 'focusout', /** * Submit doesn't bubble, so it cannot be used with event * contract. However, the browser helpfully fires a click event on * the submit button of a form (even if the form is not submitted by * a click on the submit button). So you should handle click on the * submit button instead. */ SUBMIT: 'submit', /** * The keydown event. In addEvent() and non-click jsaction it represents the * regular DOM keydown event. It represents click actions in non-Gecko * browsers. */ KEYDOWN: 'keydown', /** * The keypress event. In addEvent() and non-click jsaction it represents the * regular DOM keypress event. It represents click actions in Gecko browsers. */ KEYPRESS: 'keypress', /** * The keyup event. In addEvent() and non-click jsaction it represents the * regular DOM keyup event. It represents click actions in non-Gecko * browsers. */ KEYUP: 'keyup', /** * The mouseup event. Can either be used directly or used implicitly to * capture mouseup events. In addEvent(), it represents a regular DOM * mouseup event. */ MOUSEUP: 'mouseup', /** * The mousedown event. Can either be used directly or used implicitly to * capture mouseenter events. In addEvent(), it represents a regular DOM * mouseover event. */ MOUSEDOWN: 'mousedown', /** * The mouseover event. Can either be used directly or used implicitly to * capture mouseenter events. In addEvent(), it represents a regular DOM * mouseover event. */ MOUSEOVER: 'mouseover', /** * The mouseout event. Can either be used directly or used implicitly to * capture mouseover events. In addEvent(), it represents a regular DOM * mouseout event. */ MOUSEOUT: 'mouseout', /** * The mouseenter event. Does not bubble and fires individually on each * element being entered within a DOM tree. */ MOUSEENTER: 'mouseenter', /** * The mouseleave event. Does not bubble and fires individually on each * element being entered within a DOM tree. */ MOUSELEAVE: 'mouseleave', /** * The mousemove event. */ MOUSEMOVE: 'mousemove', /** * The pointerup event. Can either be used directly or used implicitly to * capture pointerup events. In addEvent(), it represents a regular DOM * pointerup event. */ POINTERUP: 'pointerup', /** * The pointerdown event. Can either be used directly or used implicitly to * capture pointerenter events. In addEvent(), it represents a regular DOM * mouseover event. */ POINTERDOWN: 'pointerdown', /** * The pointerover event. Can either be used directly or used implicitly to * capture pointerenter events. In addEvent(), it represents a regular DOM * pointerover event. */ POINTEROVER: 'pointerover', /** * The pointerout event. Can either be used directly or used implicitly to * capture pointerover events. In addEvent(), it represents a regular DOM * pointerout event. */ POINTEROUT: 'pointerout', /** * The pointerenter event. Does not bubble and fires individually on each * element being entered within a DOM tree. */ POINTERENTER: 'pointerenter', /** * The pointerleave event. Does not bubble and fires individually on each * element being entered within a DOM tree. */ POINTERLEAVE: 'pointerleave', /** * The pointermove event. */ POINTERMOVE: 'pointermove', /** * The pointercancel event. */ POINTERCANCEL: 'pointercancel', /** * The gotpointercapture event is fired when * Element.setPointerCapture(pointerId) is called on a mouse input, or * implicitly when a touch input begins. */ GOTPOINTERCAPTURE: 'gotpointercapture', /** * The lostpointercapture event is fired when * Element.releasePointerCapture(pointerId) is called, or implicitly after a * touch input ends. */ LOSTPOINTERCAPTURE: 'lostpointercapture', /** * The error event. The error event doesn't bubble, but you can use it in * addEvent() and jsaction anyway. EventContract does the right thing under * the hood (except in IE8 which does not use error events). */ ERROR: 'error', /** * The load event. The load event doesn't bubble, but you can use it in * addEvent() and jsaction anyway. EventContract does the right thing * under the hood. */ LOAD: 'load', /** * The unload event. */ UNLOAD: 'unload', /** * The touchstart event. Bubbles, will only ever fire in browsers with * touch support. */ TOUCHSTART: 'touchstart', /** * The touchend event. Bubbles, will only ever fire in browsers with * touch support. */ TOUCHEND: 'touchend', /** * The touchmove event. Bubbles, will only ever fire in browsers with * touch support. */ TOUCHMOVE: 'touchmove', /** * The input event. */ INPUT: 'input', /** * The scroll event. */ SCROLL: 'scroll', /** * The toggle event. The toggle event doesn't bubble, but you can use it in * addEvent() and jsaction anyway. EventContract does the right thing * under the hood. */ TOGGLE: 'toggle', /** * A custom event. The actual custom event type is declared as the 'type' * field in the event details. Supported in Firefox 6+, IE 9+, and all Chrome * versions. * * This is an internal name. Users should use jsaction's fireCustomEvent to * fire custom events instead of relying on this type to create them. */ CUSTOM: '_custom', }; const NON_BUBBLING_MOUSE_EVENTS = [ EventType.MOUSEENTER, EventType.MOUSELEAVE, 'pointerenter', 'pointerleave', ]; /** * Detects whether a given event type is supported by JSAction. */ const isSupportedEvent = (eventType) => SUPPORTED_EVENTS.indexOf(eventType) >= 0; const SUPPORTED_EVENTS = [ EventType.CLICK, EventType.DBLCLICK, EventType.FOCUS, EventType.FOCUSIN, EventType.BLUR, EventType.ERROR, EventType.FOCUSOUT, EventType.KEYDOWN, EventType.KEYUP, EventType.KEYPRESS, EventType.LOAD, EventType.MOUSEOVER, EventType.MOUSEOUT, EventType.SUBMIT, EventType.TOGGLE, EventType.TOUCHSTART, EventType.TOUCHEND, EventType.TOUCHMOVE, 'touchcancel', 'auxclick', 'change', 'compositionstart', 'compositionupdate', 'compositionend', 'beforeinput', 'input', 'select', 'copy', 'cut', 'paste', 'mousedown', 'mouseup', 'wheel', 'contextmenu', 'dragover', 'dragenter', 'dragleave', 'drop', 'dragstart', 'dragend', 'pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerover', 'pointerout', 'gotpointercapture', 'lostpointercapture', // Video events. 'ended', 'loadedmetadata', // Page visibility events. 'pagehide', 'pageshow', 'visibilitychange', // Content visibility events. 'beforematch', ]; /** * * Decides whether or not an event type is an event that only has a capture phase. * * @param eventType * @returns bool */ const isCaptureEvent = (eventType) => CAPTURE_EVENTS.indexOf(eventType) >= 0; const CAPTURE_EVENTS = [ EventType.FOCUS, EventType.BLUR, EventType.ERROR, EventType.LOAD, EventType.TOGGLE, ]; /** All properties that are used by jsaction. */ const Property = { /** * The parsed value of the jsaction attribute is stored in this * property on the DOM node. The parsed value is an Object. The * property names of the object are the events; the values are the * names of the actions. This property is attached even on nodes * that don't have a jsaction attribute as an optimization, because * property lookup is faster than attribute access. */ JSACTION: '__jsaction', /** * The owner property references an a logical owner for a DOM node. JSAction * will follow this reference instead of parentNode when traversing the DOM * to find jsaction attributes. This allows overlaying a logical structure * over a document where the DOM structure can't reflect that structure. */ OWNER: '__owner', }; /** * Map from jsaction annotation to a parsed map from event name to action name. */ const parseCache = {}; function registerEventType(element, eventType, action) { const cache = get(element) || {}; cache[eventType] = action; set(element, cache); } function unregisterEventType(element, eventType) { const cache = get(element); if (cache) { cache[eventType] = undefined; } } /** * Reads the jsaction parser cache from the given DOM Element. * * @param element . * @return Map from event to qualified name of the jsaction bound to it. */ function get(element) { // @ts-ignore return element[Property.JSACTION]; } /** * Writes the jsaction parser cache to the given DOM Element. * * @param element . * @param actionMap Map from event to qualified name of the jsaction bound to * it. */ function set(element, actionMap) { // @ts-ignore element[Property.JSACTION] = actionMap; } /** * Looks up the parsed action map from the source jsaction attribute value. * * @param text Unparsed jsaction attribute value. * @return Parsed jsaction attribute value, if already present in the cache. */ function getParsed(text) { return parseCache[text]; } /** * Inserts the parse result for the given source jsaction value into the cache. * * @param text Unparsed jsaction attribute value. * @param parsed Attribute value parsed into the action map. */ function setParsed(text, parsed) { parseCache[text] = parsed; } /** * Clears the jsaction parser cache from the given DOM Element. * * @param element . */ function clear(element) { if (Property.JSACTION in element) { delete element[Property.JSACTION]; } } /** Added for readability when accessing stable property names. */ function getEventType(eventInfo) { return eventInfo.eventType; } /** Added for readability when accessing stable property names. */ function setEventType(eventInfo, eventType) { eventInfo.eventType = eventType; } /** Added for readability when accessing stable property names. */ function getEvent(eventInfo) { return eventInfo.event; } /** Added for readability when accessing stable property names. */ function setEvent(eventInfo, event) { eventInfo.event = event; } /** Added for readability when accessing stable property names. */ function getTargetElement(eventInfo) { return eventInfo.targetElement; } /** Added for readability when accessing stable property names. */ function setTargetElement(eventInfo, targetElement) { eventInfo.targetElement = targetElement; } /** Added for readability when accessing stable property names. */ function getContainer(eventInfo) { return eventInfo.eic; } /** Added for readability when accessing stable property names. */ function setContainer(eventInfo, container) { eventInfo.eic = container; } /** Added for readability when accessing stable property names. */ function getTimestamp(eventInfo) { return eventInfo.timeStamp; } /** Added for readability when accessing stable property names. */ function setTimestamp(eventInfo, timestamp) { eventInfo.timeStamp = timestamp; } /** Added for readability when accessing stable property names. */ function getAction(eventInfo) { return eventInfo.eia; } /** Added for readability when accessing stable property names. */ function setAction(eventInfo, actionName, actionElement) { eventInfo.eia = [actionName, actionElement]; } /** Added for readability when accessing stable property names. */ function unsetAction(eventInfo) { eventInfo.eia = undefined; } /** Added for readability when accessing stable property names. */ function getActionName(actionInfo) { return actionInfo[0]; } /** Added for readability when accessing stable property names. */ function getActionElement(actionInfo) { return actionInfo[1]; } /** Added for readability when accessing stable property names. */ function getIsReplay(eventInfo) { return eventInfo.eirp; } /** Added for readability when accessing stable property names. */ function setIsReplay(eventInfo, replay) { eventInfo.eirp = replay; } /** Added for readability when accessing stable property names. */ function getA11yClickKey(eventInfo) { return eventInfo.eiack; } /** Added for readability when accessing stable property names. */ function setA11yClickKey(eventInfo, a11yClickKey) { eventInfo.eiack = a11yClickKey; } /** Added for readability when accessing stable property names. */ function getResolved(eventInfo) { return eventInfo.eir; } /** Added for readability when accessing stable property names. */ function setResolved(eventInfo, resolved) { eventInfo.eir = resolved; } /** Clones an `EventInfo` */ function cloneEventInfo(eventInfo) { return { eventType: eventInfo.eventType, event: eventInfo.event, targetElement: eventInfo.targetElement, eic: eventInfo.eic, eia: eventInfo.eia, timeStamp: eventInfo.timeStamp, eirp: eventInfo.eirp, eiack: eventInfo.eiack, eir: eventInfo.eir, }; } /** * Utility function for creating an `EventInfo`. * * This can be used from code-size sensitive compilation units, as taking * parameters vs. an `Object` literal reduces code size. */ function createEventInfoFromParameters(eventType, event, targetElement, container, timestamp, action, isReplay, a11yClickKey) { return { eventType, event, targetElement, eic: container, timeStamp: timestamp, eia: action, eirp: isReplay, eiack: a11yClickKey, }; } /** * Utility function for creating an `EventInfo`. * * This should be used in compilation units that are less sensitive to code * size. */ function createEventInfo({ eventType, event, targetElement, container, timestamp, action, isReplay, a11yClickKey, }) { return { eventType, event, targetElement, eic: container, timeStamp: timestamp, eia: action ? [action.name, action.element] : undefined, eirp: isReplay, eiack: a11yClickKey, }; } /** * Utility class around an `EventInfo`. * * This should be used in compilation units that are less sensitive to code * size. */ class EventInfoWrapper { constructor(eventInfo) { this.eventInfo = eventInfo; } getEventType() { return getEventType(this.eventInfo); } setEventType(eventType) { setEventType(this.eventInfo, eventType); } getEvent() { return getEvent(this.eventInfo); } setEvent(event) { setEvent(this.eventInfo, event); } getTargetElement() { return getTargetElement(this.eventInfo); } setTargetElement(targetElement) { setTargetElement(this.eventInfo, targetElement); } getContainer() { return getContainer(this.eventInfo); } setContainer(container) { setContainer(this.eventInfo, container); } getTimestamp() { return getTimestamp(this.eventInfo); } setTimestamp(timestamp) { setTimestamp(this.eventInfo, timestamp); } getAction() { const action = getAction(this.eventInfo); if (!action) return undefined; return { name: action[0], element: action[1], }; } setAction(action) { if (!action) { unsetAction(this.eventInfo); return; } setAction(this.eventInfo, action.name, action.element); } getIsReplay() { return getIsReplay(this.eventInfo); } setIsReplay(replay) { setIsReplay(this.eventInfo, replay); } getResolved() { return getResolved(this.eventInfo); } setResolved(resolved) { setResolved(this.eventInfo, resolved); } clone() { return new EventInfoWrapper(cloneEventInfo(this.eventInfo)); } } /** * If on a Macintosh with an extended keyboard, the Enter key located in the * numeric pad has a different ASCII code. */ const MAC_ENTER = 3; /** The Enter key. */ const ENTER = 13; /** The Space key. */ const SPACE = 32; /** Special keycodes used by jsaction for the generic click action. */ const KeyCode = { MAC_ENTER, ENTER, SPACE }; /** * Gets a browser event type, if it would differ from the JSAction event type. */ function getBrowserEventType(eventType) { // Mouseenter and mouseleave events are not handled directly because they // are not available everywhere. In browsers where they are available, they // don't bubble and aren't visible at the container boundary. Instead, we // synthesize the mouseenter and mouseleave events from mouseover and // mouseout events, respectively. Cf. eventcontract.js. if (eventType === EventType.MOUSEENTER) { return EventType.MOUSEOVER; } else if (eventType === EventType.MOUSELEAVE) { return EventType.MOUSEOUT; } else if (eventType === EventType.POINTERENTER) { return EventType.POINTEROVER; } else if (eventType === EventType.POINTERLEAVE) { return EventType.POINTEROUT; } return eventType; } /** * Registers the event handler function with the given DOM element for * the given event type. * * @param element The element. * @param eventType The event type. * @param handler The handler function to install. * @return Information needed to uninstall the event handler eventually. */ function addEventListener(element, eventType, handler) { // All event handlers are registered in the bubbling // phase. // // All browsers support focus and blur, but these events only are propagated // in the capture phase. Very legacy browsers do not support focusin or // focusout. // // It would be a bad idea to register all event handlers in the // capture phase because then regular onclick handlers would not be // executed at all on events that trigger a jsaction. That's not // entirely what we want, at least for now. // // Error and load events (i.e. on images) do not bubble so they are also // handled in the capture phase. let capture = false; if (isCaptureEvent(eventType)) { capture = true; } element.addEventListener(eventType, handler, capture); return { eventType, handler, capture }; } /** * Removes the event handler for the given event from the element. * the given event type. * * @param element The element. * @param info The information needed to deregister the handler, as returned by * addEventListener(), above. */ function removeEventListener(element, info) { if (element.removeEventListener) { element.removeEventListener(info.eventType, info.handler, info.capture); // `detachEvent` is an old DOM API. // tslint:disable-next-line:no-any } else if (element.detachEvent) { // `detachEvent` is an old DOM API. // tslint:disable-next-line:no-any element.detachEvent(`on${info.eventType}`, info.handler); } } /** * Cancels propagation of an event. * @param e The event to cancel propagation for. */ function stopPropagation(e) { e.stopPropagation ? e.stopPropagation() : (e.cancelBubble = true); } /** * Prevents the default action of an event. * @param e The event to prevent the default action for. */ function preventDefault(e) { e.preventDefault ? e.preventDefault() : (e.returnValue = false); } /** * Gets the target Element of the event. In Firefox, a text node may appear as * the target of the event, in which case we return the parent element of the * text node. * @param e The event to get the target of. * @return The target element. */ function getTarget(e) { let el = e.target; // In Firefox, the event may have a text node as its target. We always // want the parent Element the text node belongs to, however. if (!el.getAttribute && el.parentNode) { el = el.parentNode; } return el; } /** * Whether we are on a Mac. Not pulling in useragent just for this. */ let isMac = typeof navigator !== 'undefined' && /Macintosh/.test(navigator.userAgent); /** * Determines and returns whether the given event (which is assumed to be a * click event) is a middle click. * NOTE: There is not a consistent way to identify middle click * http://www.unixpapa.com/js/mouse.html */ function isMiddleClick(e) { return ( // `which` is an old DOM API. // tslint:disable-next-line:no-any e.which === 2 || // `which` is an old DOM API. // tslint:disable-next-line:no-any (e.which == null && // `button` is an old DOM API. // tslint:disable-next-line:no-any e.button === 4) // middle click for IE ); } /** * Determines and returns whether the given event (which is assumed * to be a click event) is modified. A middle click is considered a modified * click to retain the default browser action, which opens a link in a new tab. * @param e The event. * @return Whether the given event is modified. */ function isModifiedClickEvent(e) { return ( // `metaKey` is an old DOM API. // tslint:disable-next-line:no-any (isMac && e.metaKey) || // `ctrlKey` is an old DOM API. // tslint:disable-next-line:no-any (!isMac && e.ctrlKey) || isMiddleClick(e) || // `shiftKey` is an old DOM API. // tslint:disable-next-line:no-any e.shiftKey); } /** Whether we are on WebKit (e.g., Chrome). */ const isWebKit = typeof navigator !== 'undefined' && !/Opera/.test(navigator.userAgent) && /WebKit/.test(navigator.userAgent); /** Whether we are on IE. */ const isIe = typeof navigator !== 'undefined' && (/MSIE/.test(navigator.userAgent) || /Trident/.test(navigator.userAgent)); /** Whether we are on Gecko (e.g., Firefox). */ const isGecko = typeof navigator !== 'undefined' && !/Opera|WebKit/.test(navigator.userAgent) && /Gecko/.test(navigator.product); /** * Determines and returns whether the given element is a valid target for * keypress/keydown DOM events that act like regular DOM clicks. * @param el The element. * @return Whether the given element is a valid action key target. */ function isValidActionKeyTarget(el) { if (!('getAttribute' in el)) { return false; } if (isTextControl(el)) { return false; } if (isNativelyActivatable(el)) { return false; } // `isContentEditable` is an old DOM API. // tslint:disable-next-line:no-any if (el.isContentEditable) { return false; } return true; } /** * Whether an event has a modifier key activated. * @param e The event. * @return True, if a modifier key is activated. */ function hasModifierKey(e) { return ( // `ctrlKey` is an old DOM API. // tslint:disable-next-line:no-any e.ctrlKey || // `shiftKey` is an old DOM API. // tslint:disable-next-line:no-any e.shiftKey || // `altKey` is an old DOM API. // tslint:disable-next-line:no-any e.altKey || // `metaKey` is an old DOM API. // tslint:disable-next-line:no-any e.metaKey); } /** * Determines and returns whether the given event has a target that already * has event handlers attached because it is a native HTML control. Used to * determine if preventDefault should be called when isActionKeyEvent is true. * @param e The event. * @return If preventDefault should be called. */ function shouldCallPreventDefaultOnNativeHtmlControl(e) { const el = getTarget(e); const tagName = el.tagName.toUpperCase(); const role = (el.getAttribute('role') || '').toUpperCase(); if (tagName === 'BUTTON' || role === 'BUTTON') { return true; } if (!isNativeHTMLControl(el)) { return false; } if (tagName === 'A') { return false; } /** * Fix for physical d-pads on feature phone platforms; the native event * (ie. isTrusted: true) needs to fire to show the OPTION list. See * b/135288469 for more info. */ if (tagName === 'SELECT') { return false; } if (processSpace(el)) { return false; } if (isTextControl(el)) { return false; } return true; } /** * Determines and returns whether the given event acts like a regular DOM click, * and should be handled instead of the click. If this returns true, the caller * will call preventDefault() to prevent a possible duplicate event. * This is represented by a keypress (keydown on Gecko browsers) on Enter or * Space key. * @param e The event. * @return True, if the event emulates a DOM click. */ function isActionKeyEvent(e) { let key = // `which` is an old DOM API. // tslint:disable-next-line:no-any e.which || // `keyCode` is an old DOM API. // tslint:disable-next-line:no-any e.keyCode; if (!key && e.key) { key = ACTION_KEY_TO_KEYCODE[e.key]; } if (isWebKit && key === KeyCode.MAC_ENTER) { key = KeyCode.ENTER; } if (key !== KeyCode.ENTER && key !== KeyCode.SPACE) { return false; } const el = getTarget(e); if (e.type !== EventType.KEYDOWN || !isValidActionKeyTarget(el) || hasModifierKey(e)) { return false; } // For <input type="checkbox">, we must only handle the browser's native click // event, so that the browser can toggle the checkbox. if (processSpace(el) && key === KeyCode.SPACE) { return false; } // If this element is non-focusable, ignore stray keystrokes (b/18337209) // Sscreen readers can move without tab focus, so any tabIndex is focusable. // See B/21809604 if (!isFocusable(el)) { return false; } const type = (el.getAttribute('role') || el.type || el.tagName).toUpperCase(); const isSpecificTriggerKey = IDENTIFIER_TO_KEY_TRIGGER_MAPPING[type] % key === 0; const isDefaultTriggerKey = !(type in IDENTIFIER_TO_KEY_TRIGGER_MAPPING) && key === KeyCode.ENTER; const hasType = el.tagName.toUpperCase() !== 'INPUT' || !!el.type; return (isSpecificTriggerKey || isDefaultTriggerKey) && hasType; } /** * Checks whether a DOM element can receive keyboard focus. * This code is based on goog.dom.isFocusable, but simplified since we shouldn't * care about visibility if we're already handling a keyboard event. */ function isFocusable(el) { return ((el.tagName in NATIVELY_FOCUSABLE_ELEMENTS || hasSpecifiedTabIndex(el)) && !el.disabled); } /** * @param element Element to check. * @return Whether the element has a specified tab index. */ function hasSpecifiedTabIndex(element) { // IE returns 0 for an unset tabIndex, so we must use getAttributeNode(), // which returns an object with a 'specified' property if tabIndex is // specified. This works on other browsers, too. const attrNode = element.getAttributeNode('tabindex'); // Must be lowercase! return attrNode != null && attrNode.specified; } /** Element tagnames that are focusable by default. */ const NATIVELY_FOCUSABLE_ELEMENTS = { 'A': 1, 'INPUT': 1, 'TEXTAREA': 1, 'SELECT': 1, 'BUTTON': 1, }; /** @return True, if the Space key was pressed. */ function isSpaceKeyEvent(e) { const key = // `which` is an old DOM API. // tslint:disable-next-line:no-any e.which || // `keyCode` is an old DOM API. // tslint:disable-next-line:no-any e.keyCode; const el = getTarget(e); const elementName = (el.type || el.tagName).toUpperCase(); return key === KeyCode.SPACE && elementName !== 'CHECKBOX'; } /** * Determines whether the event corresponds to a non-bubbling mouse * event type (mouseenter, mouseleave, pointerenter, and pointerleave). * * During mouseover (mouseenter) and pointerover (pointerenter), the * relatedTarget is the element being entered from. During mouseout (mouseleave) * and pointerout (pointerleave), the relatedTarget is the element being exited * to. * * In both cases, if relatedTarget is outside target, then the corresponding * special event has occurred, otherwise it hasn't. * * @param e The mouseover/mouseout event. * @param type The type of the mouse special event. * @param element The element on which the jsaction for the * mouseenter/mouseleave event is defined. * @return True if the event is a mouseenter/mouseleave event. */ function isMouseSpecialEvent(e, type, element) { // `relatedTarget` is an old DOM API. // tslint:disable-next-line:no-any const related = e.relatedTarget; return (((e.type === EventType.MOUSEOVER && type === EventType.MOUSEENTER) || (e.type === EventType.MOUSEOUT && type === EventType.MOUSELEAVE) || (e.type === EventType.POINTEROVER && type === EventType.POINTERENTER) || (e.type === EventType.POINTEROUT && type === EventType.POINTERLEAVE)) && (!related || (related !== element && !element.contains(related)))); } /** * Creates a new EventLike object for a mouseenter/mouseleave event that's * derived from the original corresponding mouseover/mouseout event. * @param e The event. * @param target The element on which the jsaction for the mouseenter/mouseleave * event is defined. * @return A modified event-like object copied from the event object passed into * this function. */ function createMouseSpecialEvent(e, target) { // We have to create a copy of the event object because we need to mutate // its fields. We do this for the special mouse events because the event // target needs to be retargeted to the action element rather than the real // element (since we are simulating the special mouse events with mouseover/ // mouseout). // // Since we're making a copy anyways, we might as well attempt to convert // this event into a pseudo-real mouseenter/mouseleave event by adjusting // its type. // // tslint:disable-next-line:no-any const copy = {}; for (const property in e) { if (property === 'srcElement' || property === 'target') { continue; } const key = property; // Making a copy requires iterating through all properties of `Event`. // tslint:disable-next-line:no-dict-access-on-struct-type const value = e[key]; if (typeof value === 'function') { continue; } // Value should be the expected type, but the value of `key` is not known // statically. // tslint:disable-next-line:no-any copy[key] = value; } if (e.type === EventType.MOUSEOVER) { copy['type'] = EventType.MOUSEENTER; } else if (e.type === EventType.MOUSEOUT) { copy['type'] = EventType.MOUSELEAVE; } else if (e.type === EventType.POINTEROVER) { copy['type'] = EventType.POINTERENTER; } else { copy['type'] = EventType.POINTERLEAVE; } copy['target'] = copy['srcElement'] = target; copy['bubbles'] = false; return copy; } /** * Returns touch data extracted from the touch event: clientX, clientY, screenX * and screenY. If the event has no touch information at all, the returned * value is null. * * The fields of this Object are unquoted. * * @param event A touch event. */ function getTouchData(event) { const touch = (event.changedTouches && event.changedTouches[0]) || (event.touches && event.touches[0]); if (!touch) { return null; } return { clientX: touch.clientX, clientY: touch.clientY, screenX: touch.screenX, screenY: touch.screenY, }; } /** * Creates a new EventLike object for a "click" event that's derived from the * original corresponding "touchend" event for a fast-click implementation. * * It takes a touch event, adds common fields found in a click event and * changes the type to 'click', so that the resulting event looks more like * a real click event. * * @param event A touch event. * @return A modified event-like object copied from the event object passed into * this function. */ function recreateTouchEventAsClick(event) { const click = {}; click['originalEventType'] = event.type; click['type'] = EventType.CLICK; for (const property in event) { if (property === 'type' || property === 'srcElement') { continue; } const key = property; // Making a copy requires iterating through all properties of `TouchEvent`. // tslint:disable-next-line:no-dict-access-on-struct-type const value = event[key]; if (typeof value === 'function') { continue; } // Value should be the expected type, but the value of `key` is not known // statically. // tslint:disable-next-line:no-any click[key] = value; } // Ensure that the event has the most recent timestamp. This timestamp // may be used in the future to validate or cancel subsequent click events. click['timeStamp'] = Date.now(); // Emulate preventDefault and stopPropagation behavior click['defaultPrevented'] = false; click['preventDefault'] = syntheticPreventDefault; click['_propagationStopped'] = false; click['stopPropagation'] = syntheticStopPropagation; // Emulate click coordinates using touch info const touch = getTouchData(event); if (touch) { click['clientX'] = touch.clientX; click['clientY'] = touch.clientY; click['screenX'] = touch.screenX; click['screenY'] = touch.screenY; } return click; } /** * An implementation of "preventDefault" for a synthesized event. Simply * sets "defaultPrevented" property to true. */ function syntheticPreventDefault() { this.defaultPrevented = true; } /** * An implementation of "stopPropagation" for a synthesized event. It simply * sets a synthetic non-standard "_propagationStopped" property to true. */ function syntheticStopPropagation() { this._propagationStopped = true; } /** * Mapping of KeyboardEvent.key values to * KeyCode values. */ const ACTION_KEY_TO_KEYCODE = { 'Enter': KeyCode.ENTER, ' ': KeyCode.SPACE, }; /** * Mapping of HTML element identifiers (ARIA role, type, or tagName) to the * keys (enter and/or space) that should activate them. A value of zero means * that both should activate them. */ const IDENTIFIER_TO_KEY_TRIGGER_MAPPING = { 'A': KeyCode.ENTER, 'BUTTON': 0, 'CHECKBOX': KeyCode.SPACE, 'COMBOBOX': KeyCode.ENTER, 'FILE': 0, 'GRIDCELL': KeyCode.ENTER, 'LINK': KeyCode.ENTER, 'LISTBOX': KeyCode.ENTER, 'MENU': 0, 'MENUBAR': 0, 'MENUITEM': 0, 'MENUITEMCHECKBOX': 0, 'MENUITEMRADIO': 0, 'OPTION': 0, 'RADIO': KeyCode.SPACE, 'RADIOGROUP': KeyCode.SPACE, 'RESET': 0, 'SUBMIT': 0, 'SWITCH': KeyCode.SPACE, 'TAB': 0, 'TREE': KeyCode.ENTER, 'TREEITEM': KeyCode.ENTER, }; /** * Returns whether or not to process space based on the type of the element; * checks to make sure that type is not null. * @param element The element. * @return Whether or not to process space based on type. */ function processSpace(element) { const type = (element.getAttribute('type') || element.tagName).toUpperCase(); return type in PROCESS_SPACE; } /** * Returns whether or not the given element is a text control. * @param el The element. * @return Whether or not the given element is a text control. */ function isTextControl(el) { const type = (el.getAttribute('type') || el.tagName).toUpperCase(); return type in TEXT_CONTROLS; } /** * Returns if the given element is a native HTML control. * @param el The element. * @return If the given element is a native HTML control. */ function isNativeHTMLControl(el) { return el.tagName.toUpperCase() in NATIVE_HTML_CONTROLS; } /** * Returns if the given element is natively activatable. Browsers emit click * events for natively activatable elements, even when activated via keyboard. * For these elements, we don't need to raise a11y click events. * @param el The element. * @return If the given element is a native HTML control. */ function isNativelyActivatable(el) { return (el.tagName.toUpperCase() === 'BUTTON' || (!!el.type && el.type.toUpperCase() === 'FILE')); } /** * HTML <input> types (not ARIA roles) which will auto-trigger a click event for * the Space key, with side-effects. We will not call preventDefault if space is * pressed, nor will we raise a11y click events. For all other elements, we can * suppress the default event (which has no desired side-effects) and handle the * keydown ourselves. */ const PROCESS_SPACE = { 'CHECKBOX': true, 'FILE': true, 'OPTION': true, 'RADIO': true, }; /** TagNames and Input types for which to not process enter/space as click. */ const TEXT_CONTROLS = { 'COLOR': true, 'DATE': true, 'DATETIME': true, 'DATETIME-LOCAL': true, 'EMAIL': true, 'MONTH': true, 'NUMBER': true, 'PASSWORD': true, 'RANGE': true, 'SEARCH': true, 'TEL': true, 'TEXT': true, 'TEXTAREA': true, 'TIME': true, 'URL': true, 'WEEK': true, }; /** TagNames that are native HTML controls. */ const NATIVE_HTML_CONTROLS = { 'A': true, 'AREA': true, 'BUTTON': true, 'DIALOG': true, 'IMG': true, 'INPUT': true, 'LINK': true, 'MENU': true, 'OPTGROUP': true, 'OPTION': true, 'PROGRESS': true, 'SELECT': true, 'TEXTAREA': true, }; /** Exported for testing. */ const testing = { setIsMac(value) { isMac = value; }, }; /** * Since maps from event to action are immutable we can use a single map * to represent the empty map. */ const EMPTY_ACTION_MAP = {}; /** * This regular expression matches a semicolon. */ const REGEXP_SEMICOLON = /\s*;\s*/; /** If no event type is defined, defaults to `click`. */ const DEFAULT_EVENT_TYPE = EventType.CLICK; /** Resolves actions for Events. */ class ActionResolver { constructor({ syntheticMouseEventSupport = false, } = {}) { this.a11yClickSupport = false; this.updateEventInfoForA11yClick = undefined; this.preventDefaultForA11yClick = undefined; this.populateClickOnlyAction = undefined; this.syntheticMouseEventSupport = syntheticMouseEventSupport; } resolveEventType(eventInfo) { // We distinguish modified and plain clicks in order to support the // default browser behavior of modified clicks on links; usually to // open the URL of the link in new tab or new window on ctrl/cmd // click. A DOM 'click' event is mapped to the jsaction 'click' // event iff there is no modifier present on the event. If there is // a modifier, it's mapped to 'clickmod' instead. // // It's allowed to omit the event in the jsaction attribute. In that // case, 'click' is assumed. Thus the following two are equivalent: // // <a href="someurl" jsaction="gna.fu"> // <a href="someurl" jsaction="click:gna.fu"> // // For unmodified clicks, EventContract invokes the jsaction // 'gna.fu'. For modified clicks, EventContract won't find a // suitable action and leave the event to be handled by the // browser. // // In order to also invoke a jsaction handler for a modifier click, // 'clickmod' needs to be used: // // <a href="someurl" jsaction="clickmod:gna.fu"> // // EventContract invokes the jsaction 'gna.fu' for modified // clicks. Unmodified clicks are left to the browser. // // In order to set up the event contract to handle both clickonly and // clickmod, only addEvent(EventType.CLICK) is necessary. // // In order to set up the event contract to handle click, // addEvent() is necessary for CLICK, KEYDOWN, and KEYPRESS event types. If // a11y click support is enabled, addEvent() will set up the appropriate key // event handler automatically. if (getEventType(eventInfo) === EventType.CLICK && isModifiedClickEvent(getEvent(eventInfo))) { setEventType(eventInfo, EventType.CLICKMOD); } else if (this.a11yClickSupport) { this.updateEventInfoForA11yClick(eventInfo); } } resolveAction(eventInfo) { if (getResolved(eventInfo)) { return; } this.populateAction(eventInfo, getTargetElement(eventInfo)); setResolved(eventInfo, true); } resolveParentAction(eventInfo) { const action = getAction(eventInfo); const actionElement = action && getActionElement(action); unsetAction(eventInfo); const parentNode = actionElement && this.getParentNode(actionElement); if (!parentNode) { return; } this.populateAction(eventInfo, parentNode); } /** * Searches for a jsaction that the DOM event maps to and creates an * object containing event information used for dispatching by * jsaction.Dispatcher. This method populates the `action` and `actionElement` * fields of the EventInfo object passed in by finding the first * jsaction attribute above the target Node of the event, and below * the container Node, that specifies a jsaction for the event * type. If no such jsaction is found, then action is undefined. * * @param eventInfo `EventInfo` to set `action` and `actionElement` if an * action is found on any `Element` in the path of the `Event`. */ populateAction(eventInfo, currentTarget) { let actionElement = currentTarget; while (actionElement && actionElement !== getContainer(eventInfo)) { if (actionElement.nodeType === Node.ELEMENT_NODE) { this.populateActionOnElement(actionElement, eventInfo); } if (getAction(eventInfo)) { // An event is handled by at most one jsaction. Thus we stop at the // first matching jsaction specified in a jsaction attribute up the // ancestor chain of the event target node. break; } actionElement = this.getParentNode(actionElement); } const action = getAction(eventInfo); if (!action) { // No action found. return; } if (this.a11yClickSupport) { this.preventDefaultForA11yClick(eventInfo); } // We attempt to handle the mouseenter/mouseleave events here by // detecting whether the mouseover/mouseout events correspond to // entering/leaving an element. if (this.syntheticMouseEventSupport) { if (getEventType(eventInfo) === EventType.MOUSEENTER || getEventType(eventInfo) === EventType.MOUSELEAVE || getEventType(eventInfo) === EventType.POINTERENTER || getEventType(eventInfo) === EventType.POINTERLEAVE) { // We attempt to handle the mouseenter/mouseleave events here by // detecting whether the mouseover/mouseout events correspond to // entering/leaving an element. if (isMouseSpecialEvent(getEvent(eventInfo), getEventType(eventInfo), getActionElement(action))) { // If both mouseover/mouseout and mouseenter/mouseleave events are // enabled, two separate handlers for mouseover/mouseout are // registered. Both handlers will see the same event instance // so we create a copy to avoid interfering with the dispatching of // the mouseover/mouseout event. const copiedEvent = createMouseSpecialEvent(getEvent(eventInfo), getActionElement(action)); setEvent(eventInfo, copiedEvent); // Since the mouseenter/mouseleave events do not bubble, the target // of the event is technically the `actionElement` (the node with the // `jsaction` attribute) setTargetElement(eventInfo, getActionElement(action)); } else { unsetAction(eventInfo); } } } } /** * Walk to the parent node, unless the node has a different owner in * which case we walk to the owner. Attempt to walk to host of a * shadow root if needed. */ getParentNode(element) { const owner = element[Property.OWNER]; if (owner) { return owner; } const parentNode = element.parentNode; if (parentNode?.nodeName === '#document-fragment') { return parentNode?.host ?? null; } return parentNode; } /** * Accesses the jsaction map on a node and retrieves the name of the * action the given event is mapped to, if any. It parses the * attribute value and stores it in a property on the node for * subsequent retrieval without re-parsing and re-accessing the * attribute. * * @param actionElement The DOM node to retrieve the jsaction map from. * @param eventInfo `EventInfo` to set `action` and `actionElement` if an * action is found on the `actionElement`. */ populateActionOnElement(actionElement, eventInfo) { const actionMap = this.parseActions(actionElement); const actionName = actionMap[getEventType(eventInfo)]; if (actionName !== undefined) { setAction(eventInfo, actionName, actionElement); } if (this.a11yClickSupport) { this.populateClickOnlyAction(actionElement, eventInfo, actionMap); } } /** * Parses and caches an element's jsaction element into a map. * * This is primarily for internal use. * * @param actionElement The DOM node to retrieve the jsaction map from. * @retu