diff --git a/packages/core/primitives/event-dispatch/src/action_resolver.ts b/packages/core/primitives/event-dispatch/src/action_resolver.ts
new file mode 100644
index 00000000000000..3cc1c9b81b22fb
--- /dev/null
+++ b/packages/core/primitives/event-dispatch/src/action_resolver.ts
@@ -0,0 +1,365 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Attribute} from './attribute';
+import {Char} from './char';
+import {EventInfo} from './event_info';
+import {EventType} from './event_type';
+import {Property} from './property';
+import * as a11yClick from './a11y_click';
+import * as cache from './cache';
+import * as eventInfoLib from './event_info';
+import * as eventLib from './event';
+
+/**
+ * Since maps from event to action are immutable we can use a single map
+ * to represent the empty map.
+ */
+const EMPTY_ACTION_MAP: {[key: string]: string} = {};
+
+/**
+ * This regular expression matches a semicolon.
+ */
+const REGEXP_SEMICOLON = /\s*;\s*/;
+
+/** If no event type is defined, defaults to `click`. */
+const DEFAULT_EVENT_TYPE: string = EventType.CLICK;
+
+/** Resolves actions for Events. */
+export class ActionResolver {
+ private a11yClickSupport: boolean = false;
+ private readonly customEventSupport: boolean;
+ private readonly jsnamespaceSupport: boolean;
+ private readonly syntheticMouseEventSupport: boolean;
+
+ private updateEventInfoForA11yClick?: (eventInfo: eventInfoLib.EventInfo) => void = undefined;
+
+ private preventDefaultForA11yClick?: (eventInfo: eventInfoLib.EventInfo) => void = undefined;
+
+ private populateClickOnlyAction?: (
+ actionElement: Element,
+ eventInfo: eventInfoLib.EventInfo,
+ actionMap: {[key: string]: string},
+ ) => void = undefined;
+
+ constructor({
+ customEventSupport = false,
+ jsnamespaceSupport = false,
+ syntheticMouseEventSupport = false,
+ }: {
+ customEventSupport?: boolean;
+ jsnamespaceSupport?: boolean;
+ syntheticMouseEventSupport?: boolean;
+ } = {}) {
+ this.customEventSupport = customEventSupport;
+ this.jsnamespaceSupport = jsnamespaceSupport;
+ this.syntheticMouseEventSupport = syntheticMouseEventSupport;
+ }
+
+ resolve(eventInfo: EventInfo) {
+ if (this.customEventSupport) {
+ if (eventInfoLib.getEventType(eventInfo) === EventType.CUSTOM) {
+ const detail = (eventInfoLib.getEvent(eventInfo) as CustomEvent).detail;
+ // For custom events, use a secondary dispatch based on the internal
+ // custom type of the event.
+ if (!detail || !detail['_type']) {
+ // This should never happen.
+ return;
+ }
+ eventInfoLib.setEventType(eventInfo, detail['_type']);
+ }
+ }
+
+ this.populateAction(eventInfo);
+ }
+
+ /**
+ * 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`.
+ */
+ private populateAction(eventInfo: eventInfoLib.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:
+ //
+ //
+ //
+ //
+ // 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:
+ //
+ //
+ //
+ // 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 (
+ eventInfoLib.getEventType(eventInfo) === EventType.CLICK &&
+ eventLib.isModifiedClickEvent(eventInfoLib.getEvent(eventInfo))
+ ) {
+ eventInfoLib.setEventType(eventInfo, EventType.CLICKMOD);
+ } else if (this.a11yClickSupport) {
+ this.updateEventInfoForA11yClick!(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.
+ let actionElement: Element | null = eventInfoLib.getTargetElement(eventInfo);
+ while (actionElement && actionElement !== eventInfoLib.getContainer(eventInfo)) {
+ if (actionElement.nodeType === Node.ELEMENT_NODE) {
+ // There are cases where the `target` is not an `Element`, so explicitly check
+ // before checking whether there is an action on the `Element`.
+ this.populateActionOnElement(actionElement, eventInfo);
+ }
+
+ if (eventInfoLib.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;
+ }
+ if (actionElement[Property.OWNER]) {
+ actionElement = actionElement[Property.OWNER] as Element;
+ continue;
+ }
+ if (actionElement.parentNode?.nodeName !== '#document-fragment') {
+ actionElement = actionElement.parentNode as Element | null;
+ } else {
+ actionElement = (actionElement.parentNode as ShadowRoot | null)?.host ?? null;
+ }
+ }
+
+ const action = eventInfoLib.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 (
+ eventInfoLib.getEventType(eventInfo) === EventType.MOUSEENTER ||
+ eventInfoLib.getEventType(eventInfo) === EventType.MOUSELEAVE ||
+ eventInfoLib.getEventType(eventInfo) === EventType.POINTERENTER ||
+ eventInfoLib.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 (
+ eventLib.isMouseSpecialEvent(
+ eventInfoLib.getEvent(eventInfo),
+ eventInfoLib.getEventType(eventInfo),
+ eventInfoLib.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 = eventLib.createMouseSpecialEvent(
+ eventInfoLib.getEvent(eventInfo),
+ eventInfoLib.getActionElement(action),
+ );
+ eventInfoLib.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)
+ eventInfoLib.setTargetElement(eventInfo, eventInfoLib.getActionElement(action));
+ } else {
+ eventInfoLib.unsetAction(eventInfo);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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. In order to fully qualify jsaction names using a
+ * namespace, the DOM is searched starting at the current node and
+ * going through ancestor nodes until a jsnamespace attribute is
+ * found.
+ *
+ * @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`.
+ */
+ private populateActionOnElement(actionElement: Element, eventInfo: eventInfoLib.EventInfo) {
+ const actionMap = this.parseActions(actionElement, eventInfoLib.getContainer(eventInfo));
+
+ const actionName = actionMap[eventInfoLib.getEventType(eventInfo)];
+ if (actionName !== undefined) {
+ eventInfoLib.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.
+ * @param container The node which limits the namespace lookup for a jsaction
+ * name. The container node itself will not be searched.
+ * @return Map from event to qualified name of the jsaction bound to it.
+ */
+ private parseActions(actionElement: Element, container: Node): {[key: string]: string} {
+ let actionMap: {[key: string]: string} | undefined = cache.get(actionElement);
+ if (!actionMap) {
+ const jsactionAttribute = actionElement.getAttribute(Attribute.JSACTION);
+ if (!jsactionAttribute) {
+ actionMap = EMPTY_ACTION_MAP;
+ cache.set(actionElement, actionMap);
+ } else {
+ actionMap = cache.getParsed(jsactionAttribute);
+ if (!actionMap) {
+ actionMap = {};
+ const values = jsactionAttribute.split(REGEXP_SEMICOLON);
+ for (let idx = 0; idx < values.length; idx++) {
+ const value = values[idx];
+ if (!value) {
+ continue;
+ }
+ const colon = value.indexOf(Char.EVENT_ACTION_SEPARATOR);
+ const hasColon = colon !== -1;
+ const type = hasColon ? value.substr(0, colon).trim() : DEFAULT_EVENT_TYPE;
+ const action = hasColon ? value.substr(colon + 1).trim() : value;
+ actionMap[type] = action;
+ }
+ cache.setParsed(jsactionAttribute, actionMap);
+ }
+ // If namespace support is active we need to augment the (potentially
+ // cached) jsaction mapping with the namespace.
+ if (this.jsnamespaceSupport) {
+ const noNs = actionMap;
+ actionMap = {};
+ for (const type in noNs) {
+ actionMap[type] = this.getFullyQualifiedAction(noNs[type], actionElement, container);
+ }
+ }
+ cache.set(actionElement, actionMap);
+ }
+ }
+ return actionMap;
+ }
+
+ /**
+ * Returns the fully qualified jsaction action. If the given jsaction
+ * name doesn't already contain the namespace, the function iterates
+ * over ancestor nodes until a jsnamespace attribute is found, and
+ * uses the value of that attribute as the namespace.
+ *
+ * @param action The jsaction action to resolve.
+ * @param start The node from which to start searching for a jsnamespace
+ * attribute.
+ * @param container The node which limits the search for a jsnamespace
+ * attribute. This node will be searched.
+ * @return The fully qualified name of the jsaction. If no namespace is found,
+ * returns the unqualified name in case it exists in the global namespace.
+ */
+ private getFullyQualifiedAction(action: string, start: Element, container: Node): string {
+ if (isNamespacedAction(action)) {
+ return action;
+ }
+
+ let node: Node | null = start;
+ while (node) {
+ const namespace = getNamespaceFromElement(node as Element);
+ if (namespace) {
+ return namespace + Char.NAMESPACE_ACTION_SEPARATOR + action;
+ }
+
+ // If this node is the container, stop.
+ if (node === container) {
+ break;
+ }
+
+ node = node.parentNode;
+ }
+
+ return action;
+ }
+
+ addA11yClickSupport(
+ updateEventInfoForA11yClick: typeof a11yClick.updateEventInfoForA11yClick,
+ preventDefaultForA11yClick: typeof a11yClick.preventDefaultForA11yClick,
+ populateClickOnlyAction: typeof a11yClick.populateClickOnlyAction,
+ ) {
+ this.a11yClickSupport = true;
+ this.updateEventInfoForA11yClick = updateEventInfoForA11yClick;
+ this.preventDefaultForA11yClick = preventDefaultForA11yClick;
+ this.populateClickOnlyAction = populateClickOnlyAction;
+ }
+}
+
+/**
+ * Checks if a jsaction action contains a namespace part.
+ */
+function isNamespacedAction(action: string): boolean {
+ return action.indexOf(Char.NAMESPACE_ACTION_SEPARATOR) >= 0;
+}
+
+/**
+ * Returns the value of the jsnamespace attribute of the given node.
+ * Also caches the value for subsequent lookups.
+ * @param element The node whose jsnamespace attribute is being asked for.
+ * @return The value of the jsnamespace attribute, or null if not found.
+ */
+function getNamespaceFromElement(element: Element): string | null {
+ let namespace = cache.getNamespace(element);
+ // Only query for the attribute if it has not been queried for
+ // before. getAttribute() returns null if an attribute is not present. Thus,
+ // namespace is string|null if the query took place in the past, or
+ // undefined if the query did not take place.
+ if (namespace === undefined) {
+ namespace = element.getAttribute(Attribute.JSNAMESPACE);
+ cache.setNamespace(element, namespace);
+ }
+ return namespace;
+}
diff --git a/packages/core/primitives/event-dispatch/src/eventcontract.ts b/packages/core/primitives/event-dispatch/src/eventcontract.ts
index fbd2a86f1ef84f..d363e99b2ebd3a 100644
--- a/packages/core/primitives/event-dispatch/src/eventcontract.ts
+++ b/packages/core/primitives/event-dispatch/src/eventcontract.ts
@@ -31,9 +31,7 @@
*/
import * as a11yClickLib from './a11y_click';
-import {Attribute} from './attribute';
-import * as cache from './cache';
-import {Char} from './char';
+import {ActionResolver} from './action_resolver';
import {EarlyJsactionData} from './earlyeventcontract';
import * as eventLib from './event';
import {EventContractContainerManager} from './event_contract_container';
@@ -77,19 +75,6 @@ export type Dispatcher = (eventInfo: eventInfoLib.EventInfo, globalDispatch?: bo
*/
type EventHandler = (eventType: string, event: Event, container: Element) => void;
-const DEFAULT_EVENT_TYPE: string = EventType.CLICK;
-
-/**
- * Since maps from event to action are immutable we can use a single map
- * to represent the empty map.
- */
-const EMPTY_ACTION_MAP: {[key: string]: string} = {};
-
-/**
- * This regular expression matches a semicolon.
- */
-const REGEXP_SEMICOLON = /\s*;\s*/;
-
/**
* EventContract intercepts events in the bubbling phase at the
* boundary of a container element, and maps them to generic actions
@@ -113,6 +98,12 @@ export class EventContract implements UnrenamedEventContract {
private containerManager: EventContractContainerManager | null;
+ private readonly actionResolver = new ActionResolver({
+ customEventSupport: EventContract.CUSTOM_EVENT_SUPPORT,
+ jsnamespaceSupport: EventContract.JSNAMESPACE_SUPPORT,
+ syntheticMouseEventSupport: EventContract.MOUSE_SPECIAL_SUPPORT,
+ });
+
/**
* The DOM events which this contract covers. Used to prevent double
* registration of event types. The value of the map is the
@@ -139,21 +130,9 @@ export class EventContract implements UnrenamedEventContract {
*/
private queuedEventInfos: eventInfoLib.EventInfo[] | null = [];
- /** Whether a11y click support has been loaded or not. */
- private hasA11yClickSupport = false;
/** Whether to add an a11y click listener. */
private addA11yClickListener = false;
- private updateEventInfoForA11yClick?: (eventInfo: eventInfoLib.EventInfo) => void = undefined;
-
- private preventDefaultForA11yClick?: (eventInfo: eventInfoLib.EventInfo) => void = undefined;
-
- private populateClickOnlyAction?: (
- actionElement: Element,
- eventInfo: eventInfoLib.EventInfo,
- actionMap: {[key: string]: string},
- ) => void = undefined;
-
ecaacs?: (
updateEventInfoForA11yClick: typeof a11yClickLib.updateEventInfoForA11yClick,
preventDefaultForA11yClick: typeof a11yClickLib.preventDefaultForA11yClick,
@@ -191,21 +170,7 @@ export class EventContract implements UnrenamedEventContract {
eventInfoLib.setIsReplay(eventInfo, true);
this.queuedEventInfos?.push(eventInfo);
}
- if (
- EventContract.CUSTOM_EVENT_SUPPORT &&
- eventInfoLib.getEventType(eventInfo) === EventType.CUSTOM
- ) {
- const detail = (eventInfoLib.getEvent(eventInfo) as CustomEvent).detail;
- // For custom events, use a secondary dispatch based on the internal
- // custom type of the event.
- if (!detail || !detail['_type']) {
- // This should never happen.
- return;
- }
- eventInfoLib.setEventType(eventInfo, detail['_type']);
- }
-
- this.populateAction(eventInfo);
+ this.actionResolver.resolve(eventInfo);
if (!this.dispatcher) {
return;
@@ -232,162 +197,6 @@ export class EventContract implements UnrenamedEventContract {
this.dispatcher(eventInfo);
}
- /**
- * 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`.
- */
- private populateAction(eventInfo: eventInfoLib.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:
- //
- //
- //
- //
- // 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:
- //
- //
- //
- // 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 (
- eventInfoLib.getEventType(eventInfo) === EventType.CLICK &&
- eventLib.isModifiedClickEvent(eventInfoLib.getEvent(eventInfo))
- ) {
- eventInfoLib.setEventType(eventInfo, EventType.CLICKMOD);
- } else if (this.hasA11yClickSupport) {
- this.updateEventInfoForA11yClick!(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.
- let actionElement: Element | null = eventInfoLib.getTargetElement(eventInfo);
- while (actionElement && actionElement !== eventInfoLib.getContainer(eventInfo)) {
- this.populateActionOnElement(actionElement, eventInfo);
-
- if (eventInfoLib.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;
- }
- if (actionElement[Property.OWNER]) {
- actionElement = actionElement[Property.OWNER] as Element;
- continue;
- }
- if (actionElement.parentNode?.nodeName !== '#document-fragment') {
- actionElement = actionElement.parentNode as Element | null;
- } else {
- actionElement = (actionElement.parentNode as ShadowRoot | null)?.host ?? null;
- }
- }
-
- const action = eventInfoLib.getAction(eventInfo);
- if (!action) {
- // No action found.
- return;
- }
-
- if (this.hasA11yClickSupport) {
- 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 (
- EventContract.MOUSE_SPECIAL_SUPPORT &&
- (eventInfoLib.getEventType(eventInfo) === EventType.MOUSEENTER ||
- eventInfoLib.getEventType(eventInfo) === EventType.MOUSELEAVE ||
- eventInfoLib.getEventType(eventInfo) === EventType.POINTERENTER ||
- eventInfoLib.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 (
- eventLib.isMouseSpecialEvent(
- eventInfoLib.getEvent(eventInfo),
- eventInfoLib.getEventType(eventInfo),
- eventInfoLib.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 = eventLib.createMouseSpecialEvent(
- eventInfoLib.getEvent(eventInfo),
- eventInfoLib.getActionElement(action),
- );
- eventInfoLib.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)
- eventInfoLib.setTargetElement(eventInfo, eventInfoLib.getActionElement(action));
- } else {
- eventInfoLib.unsetAction(eventInfo);
- }
- }
- }
-
- /**
- * 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. In order to fully qualify jsaction names using a
- * namespace, the DOM is searched starting at the current node and
- * going through ancestor nodes until a jsnamespace attribute is
- * found.
- *
- * @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`.
- */
- private populateActionOnElement(actionElement: Element, eventInfo: eventInfoLib.EventInfo) {
- const actionMap = parseActions(actionElement, eventInfoLib.getContainer(eventInfo));
-
- const actionName = actionMap[eventInfoLib.getEventType(eventInfo)];
- if (actionName !== undefined) {
- eventInfoLib.setAction(eventInfo, actionName, actionElement);
- }
-
- if (this.hasA11yClickSupport) {
- this.populateClickOnlyAction!(actionElement, eventInfo, actionMap);
- }
- }
-
/**
* Enables jsaction handlers to be called for the event type given by
* name.
@@ -576,10 +385,11 @@ export class EventContract implements UnrenamedEventContract {
populateClickOnlyAction: typeof a11yClickLib.populateClickOnlyAction,
) {
this.addA11yClickListener = true;
- this.hasA11yClickSupport = true;
- this.updateEventInfoForA11yClick = updateEventInfoForA11yClick;
- this.preventDefaultForA11yClick = preventDefaultForA11yClick;
- this.populateClickOnlyAction = populateClickOnlyAction;
+ this.actionResolver.addA11yClickSupport(
+ updateEventInfoForA11yClick,
+ preventDefaultForA11yClick,
+ populateClickOnlyAction,
+ );
}
}
@@ -615,158 +425,3 @@ function shouldPreventDefaultBeforeDispatching(
eventInfoLib.getEventType(eventInfo) === EventType.CLICKMOD)
);
}
-
-/**
- * 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.
- * @param container The node which limits the namespace lookup for a jsaction
- * name. The container node itself will not be searched.
- * @return Map from event to qualified name of the jsaction bound to it.
- */
-export function parseActions(actionElement: Element, container: Node): {[key: string]: string} {
- let actionMap: {[key: string]: string} | undefined = cache.get(actionElement);
- if (!actionMap) {
- const jsactionAttribute = getAttr(actionElement, Attribute.JSACTION);
- if (!jsactionAttribute) {
- actionMap = EMPTY_ACTION_MAP;
- cache.set(actionElement, actionMap);
- } else {
- actionMap = cache.getParsed(jsactionAttribute);
- if (!actionMap) {
- actionMap = {};
- const values = jsactionAttribute.split(REGEXP_SEMICOLON);
- for (let idx = 0; idx < values.length; idx++) {
- const value = values[idx];
- if (!value) {
- continue;
- }
- const colon = value.indexOf(Char.EVENT_ACTION_SEPARATOR);
- const hasColon = colon !== -1;
- const type = hasColon ? stringTrim(value.substr(0, colon)) : DEFAULT_EVENT_TYPE;
- const action = hasColon ? stringTrim(value.substr(colon + 1)) : value;
- actionMap[type] = action;
- }
- cache.setParsed(jsactionAttribute, actionMap);
- }
- // If namespace support is active we need to augment the (potentially
- // cached) jsaction mapping with the namespace.
- if (EventContract.JSNAMESPACE_SUPPORT) {
- const noNs = actionMap;
- actionMap = {};
- for (const type in noNs) {
- actionMap[type] = getFullyQualifiedAction(noNs[type], actionElement, container);
- }
- }
- cache.set(actionElement, actionMap);
- }
- }
- return actionMap;
-}
-
-/**
- * Returns the fully qualified jsaction action. If the given jsaction
- * name doesn't already contain the namespace, the function iterates
- * over ancestor nodes until a jsnamespace attribute is found, and
- * uses the value of that attribute as the namespace.
- *
- * @param action The jsaction action to resolve.
- * @param start The node from which to start searching for a jsnamespace
- * attribute.
- * @param container The node which limits the search for a jsnamespace
- * attribute. This node will be searched.
- * @return The fully qualified name of the jsaction. If no namespace is found,
- * returns the unqualified name in case it exists in the global namespace.
- */
-function getFullyQualifiedAction(action: string, start: Element, container: Node): string {
- if (EventContract.JSNAMESPACE_SUPPORT) {
- if (isNamespacedAction(action)) {
- return action;
- }
-
- let node: Node | null = start;
- while (node) {
- const namespace = getNamespaceFromElement(node as Element);
- if (namespace) {
- return namespace + Char.NAMESPACE_ACTION_SEPARATOR + action;
- }
-
- // If this node is the container, stop.
- if (node === container) {
- break;
- }
-
- node = node.parentNode;
- }
- }
-
- return action;
-}
-
-/**
- * Checks if a jsaction action contains a namespace part.
- */
-function isNamespacedAction(action: string): boolean {
- return action.indexOf(Char.NAMESPACE_ACTION_SEPARATOR) >= 0;
-}
-
-/**
- * Returns the value of the jsnamespace attribute of the given node.
- * Also caches the value for subsequent lookups.
- * @param element The node whose jsnamespace attribute is being asked for.
- * @return The value of the jsnamespace attribute, or null if not found.
- */
-function getNamespaceFromElement(element: Element): string | null {
- let namespace = cache.getNamespace(element);
- // Only query for the attribute if it has not been queried for
- // before. getAttr() returns null if an attribute is not present. Thus,
- // namespace is string|null if the query took place in the past, or
- // undefined if the query did not take place.
- if (namespace === undefined) {
- namespace = getAttr(element, Attribute.JSNAMESPACE);
- cache.setNamespace(element, namespace);
- }
- return namespace;
-}
-
-/**
- * Accesses the event handler attribute value of a DOM node. It guards
- * against weird situations (described in the body) that occur in
- * connection with nodes that are removed from their document.
- * @param element The DOM element.
- * @param attribute The name of the attribute to access.
- * @return The attribute value if it was found, null otherwise.
- */
-function getAttr(element: Element, attribute: string): string | null {
- let value = null;
- // NOTE: Nodes in IE do not always have a getAttribute
- // method defined. This is the case where sourceElement has in
- // fact been removed from the DOM before eventContract begins
- // handling - where a parentNode does not have getAttribute
- // defined.
- // NOTE: We must use the 'in' operator instead of the regular dot
- // notation, since the latter fails in IE8 if the getAttribute method is not
- // defined. See b/7139109.
- if ('getAttribute' in element) {
- value = element.getAttribute(attribute);
- }
- return value;
-}
-
-/**
- * Helper function to trim whitespace from the beginning and the end
- * of the string. This deliberately doesn't use the closure equivalent
- * to keep dependencies small.
- * @param str Input string.
- * @return Trimmed string.
- */
-function stringTrim(str: string): string {
- if (typeof String.prototype.trim === 'function') {
- return str.trim();
- }
-
- const trimmedLeft = str.replace(/^\s+/, '');
- return trimmedLeft.replace(/\s+$/, '');
-}
diff --git a/packages/core/test/bundling/defer/bundle.golden_symbols.json b/packages/core/test/bundling/defer/bundle.golden_symbols.json
index 932ef3b764402c..19624a9ff9b4f7 100644
--- a/packages/core/test/bundling/defer/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/defer/bundle.golden_symbols.json
@@ -1037,6 +1037,9 @@
{
"name": "init_a11y_click"
},
+ {
+ "name": "init_action_resolver"
+ },
{
"name": "init_advance"
},