| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type {DispatchConfig} from './ReactSyntheticEventType'; |
| 11 | import type { |
| 12 | AnyNativeEvent, |
| 13 | PluginName, |
| 14 | LegacyPluginModule, |
| 15 | } from './PluginModuleType'; |
| 16 | import type {TopLevelType} from './TopLevelEventTypes'; |
| 17 | |
| 18 | type NamesToPlugins = { |
| 19 | [key: PluginName]: LegacyPluginModule<AnyNativeEvent>, |
| 20 | }; |
| 21 | type EventPluginOrder = null | Array<PluginName>; |
| 22 | |
| 23 | /** |
| 24 | * Injectable ordering of event plugins. |
| 25 | */ |
| 26 | let eventPluginOrder: EventPluginOrder = null; |
| 27 | |
| 28 | /** |
| 29 | * Injectable mapping from names to event plugin modules. |
| 30 | */ |
| 31 | const namesToPlugins: NamesToPlugins = {}; |
| 32 | |
| 33 | /** |
| 34 | * Recomputes the plugin list using the injected plugins and plugin ordering. |
| 35 | * |
| 36 | * @private |
| 37 | */ |
| 38 | function recomputePluginOrdering(): void { |
| 39 | if (!eventPluginOrder) { |
| 40 | // Wait until an `eventPluginOrder` is injected. |
| 41 | return; |
| 42 | } |
| 43 | for (const pluginName in namesToPlugins) { |
| 44 | const pluginModule = namesToPlugins[pluginName]; |
| 45 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 46 | const pluginIndex = eventPluginOrder.indexOf(pluginName); |
| 47 | |
| 48 | if (pluginIndex <= -1) { |
| 49 | throw new Error( |
| 50 | 'EventPluginRegistry: Cannot inject event plugins that do not exist in ' + |
| 51 | `the plugin ordering, \`${pluginName}\`.`, |
| 52 | ); |
| 53 | } |
| 54 | |
| 55 | if (plugins[pluginIndex]) { |
| 56 | continue; |
| 57 | } |
| 58 | |
| 59 | if (!pluginModule.extractEvents) { |
| 60 | throw new Error( |
| 61 | 'EventPluginRegistry: Event plugins must implement an `extractEvents` ' + |
| 62 | `method, but \`${pluginName}\` does not.`, |
| 63 | ); |
| 64 | } |
| 65 | |
| 66 | plugins[pluginIndex] = pluginModule; |
| 67 | const publishedEvents = pluginModule.eventTypes; |
| 68 | for (const eventName in publishedEvents) { |
| 69 | if ( |
| 70 | !publishEventForPlugin( |
| 71 | publishedEvents[eventName], |
| 72 | pluginModule, |
| 73 | eventName, |
| 74 | ) |
| 75 | ) { |
| 76 | throw new Error( |
| 77 | `EventPluginRegistry: Failed to publish event \`${eventName}\` for plugin \`${pluginName}\`.`, |
| 78 | ); |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | /** |
| 85 | * Publishes an event so that it can be dispatched by the supplied plugin. |
| 86 | * |
| 87 | * @param {object} dispatchConfig Dispatch configuration for the event. |
| 88 | * @param {object} PluginModule Plugin publishing the event. |
| 89 | * @return {boolean} True if the event was successfully published. |
| 90 | * @private |
| 91 | */ |
| 92 | function publishEventForPlugin( |
| 93 | dispatchConfig: DispatchConfig, |
| 94 | pluginModule: LegacyPluginModule<AnyNativeEvent>, |
| 95 | eventName: string, |
| 96 | ): boolean { |
| 97 | if (eventNameDispatchConfigs.hasOwnProperty(eventName)) { |
| 98 | throw new Error( |
| 99 | 'EventPluginRegistry: More than one plugin attempted to publish the same ' + |
| 100 | `event name, \`${eventName}\`.`, |
| 101 | ); |
| 102 | } |
| 103 | |
| 104 | eventNameDispatchConfigs[eventName] = dispatchConfig; |
| 105 | |
| 106 | const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames; |
| 107 | if (phasedRegistrationNames) { |
| 108 | for (const phaseName in phasedRegistrationNames) { |
| 109 | if (phasedRegistrationNames.hasOwnProperty(phaseName)) { |
| 110 | // $FlowFixMe[invalid-computed-prop] |
| 111 | const phasedRegistrationName = phasedRegistrationNames[phaseName]; |
| 112 | publishRegistrationName( |
| 113 | phasedRegistrationName, |
| 114 | pluginModule, |
| 115 | eventName, |
| 116 | ); |
| 117 | } |
| 118 | } |
| 119 | return true; |
| 120 | } else if (dispatchConfig.registrationName) { |
| 121 | publishRegistrationName( |
| 122 | dispatchConfig.registrationName, |
| 123 | pluginModule, |
| 124 | eventName, |
| 125 | ); |
| 126 | return true; |
| 127 | } |
| 128 | return false; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Publishes a registration name that is used to identify dispatched events. |
| 133 | * |
| 134 | * @param {string} registrationName Registration name to add. |
| 135 | * @param {object} PluginModule Plugin publishing the event. |
| 136 | * @private |
| 137 | */ |
| 138 | function publishRegistrationName( |
| 139 | registrationName: string, |
| 140 | pluginModule: LegacyPluginModule<AnyNativeEvent>, |
| 141 | eventName: string, |
| 142 | ): void { |
| 143 | if (registrationNameModules[registrationName]) { |
| 144 | throw new Error( |
| 145 | 'EventPluginRegistry: More than one plugin attempted to publish the same ' + |
| 146 | `registration name, \`${registrationName}\`.`, |
| 147 | ); |
| 148 | } |
| 149 | |
| 150 | registrationNameModules[registrationName] = pluginModule; |
| 151 | registrationNameDependencies[registrationName] = |
| 152 | pluginModule.eventTypes[eventName].dependencies; |
| 153 | |
| 154 | if (__DEV__) { |
| 155 | const lowerCasedName = registrationName.toLowerCase(); |
| 156 | possibleRegistrationNames[lowerCasedName] = registrationName; |
| 157 | |
| 158 | if (registrationName === 'onDoubleClick') { |
| 159 | possibleRegistrationNames.ondblclick = registrationName; |
| 160 | } |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Registers plugins so that they can extract and dispatch events. |
| 166 | */ |
| 167 | |
| 168 | /** |
| 169 | * Ordered list of injected plugins. |
| 170 | */ |
| 171 | export const plugins: Array<LegacyPluginModule<AnyNativeEvent>> = []; |
| 172 | |
| 173 | /** |
| 174 | * Mapping from event name to dispatch config |
| 175 | */ |
| 176 | export const eventNameDispatchConfigs: { |
| 177 | [eventName: string]: DispatchConfig, |
| 178 | } = {}; |
| 179 | |
| 180 | /** |
| 181 | * Mapping from registration name to plugin module |
| 182 | */ |
| 183 | export const registrationNameModules: { |
| 184 | [registrationName: string]: LegacyPluginModule<AnyNativeEvent>, |
| 185 | } = {}; |
| 186 | |
| 187 | /** |
| 188 | * Mapping from registration name to event name |
| 189 | */ |
| 190 | export const registrationNameDependencies: { |
| 191 | [registrationName: string]: Array<TopLevelType> | void, |
| 192 | } = {}; |
| 193 | |
| 194 | /** |
| 195 | * Mapping from lowercase registration names to the properly cased version, |
| 196 | * used to warn in the case of missing event handlers. Available |
| 197 | * only in __DEV__. |
| 198 | * @type {Object} |
| 199 | */ |
| 200 | export const possibleRegistrationNames: { |
| 201 | [lowerCasedName: string]: string, |
| 202 | } = __DEV__ ? {} : (null as any); |
| 203 | // Trust the developer to only use possibleRegistrationNames in __DEV__ |
| 204 | |
| 205 | /** |
| 206 | * Injects an ordering of plugins (by plugin name). This allows the ordering |
| 207 | * to be decoupled from injection of the actual plugins so that ordering is |
| 208 | * always deterministic regardless of packaging, on-the-fly injection, etc. |
| 209 | * |
| 210 | * @param {array} InjectedEventPluginOrder |
| 211 | * @internal |
| 212 | */ |
| 213 | export function injectEventPluginOrder( |
| 214 | injectedEventPluginOrder: EventPluginOrder, |
| 215 | ): void { |
| 216 | if (eventPluginOrder) { |
| 217 | throw new Error( |
| 218 | 'EventPluginRegistry: Cannot inject event plugin ordering more than ' + |
| 219 | 'once. You are likely trying to load more than one copy of React.', |
| 220 | ); |
| 221 | } |
| 222 | |
| 223 | // Clone the ordering so it cannot be dynamically mutated. |
| 224 | // $FlowFixMe[method-unbinding] found when upgrading Flow |
| 225 | eventPluginOrder = Array.prototype.slice.call(injectedEventPluginOrder); |
| 226 | recomputePluginOrdering(); |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Injects plugins to be used by plugin event system. The plugin names must be |
| 231 | * in the ordering injected by `injectEventPluginOrder`. |
| 232 | * |
| 233 | * Plugins can be injected as part of page initialization or on-the-fly. |
| 234 | * |
| 235 | * @param {object} injectedNamesToPlugins Map from names to plugin modules. |
| 236 | * @internal |
| 237 | */ |
| 238 | export function injectEventPluginsByName( |
| 239 | injectedNamesToPlugins: NamesToPlugins, |
| 240 | ): void { |
| 241 | let isOrderingDirty = false; |
| 242 | for (const pluginName in injectedNamesToPlugins) { |
| 243 | if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) { |
| 244 | continue; |
| 245 | } |
| 246 | const pluginModule = injectedNamesToPlugins[pluginName]; |
| 247 | if ( |
| 248 | !namesToPlugins.hasOwnProperty(pluginName) || |
| 249 | namesToPlugins[pluginName] !== pluginModule |
| 250 | ) { |
| 251 | if (namesToPlugins[pluginName]) { |
| 252 | throw new Error( |
| 253 | 'EventPluginRegistry: Cannot inject two different event plugins ' + |
| 254 | `using the same name, \`${pluginName}\`.`, |
| 255 | ); |
| 256 | } |
| 257 | |
| 258 | namesToPlugins[pluginName] = pluginModule; |
| 259 | isOrderingDirty = true; |
| 260 | } |
| 261 | } |
| 262 | if (isOrderingDirty) { |
| 263 | recomputePluginOrdering(); |
| 264 | } |
| 265 | } |