UNPKG

d2-ui

Version:
211 lines (185 loc) 7.51 kB
/** * Copyright 2013-2015, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactEventListener * @typechecks static-only */ 'use strict'; var EventListener = require('fbjs/lib/EventListener'); var ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment'); var PooledClass = require('./PooledClass'); var ReactInstanceHandles = require('./ReactInstanceHandles'); var ReactMount = require('./ReactMount'); var ReactUpdates = require('./ReactUpdates'); var assign = require('./Object.assign'); var getEventTarget = require('./getEventTarget'); var getUnboundedScrollPosition = require('fbjs/lib/getUnboundedScrollPosition'); var DOCUMENT_FRAGMENT_NODE_TYPE = 11; /** * Finds the parent React component of `node`. * * @param {*} node * @return {?DOMEventTarget} Parent container, or `null` if the specified node * is not nested. */ function findParent(node) { // TODO: It may be a good idea to cache this to prevent unnecessary DOM // traversal, but caching is difficult to do correctly without using a // mutation observer to listen for all DOM changes. var nodeID = ReactMount.getID(node); var rootID = ReactInstanceHandles.getReactRootIDFromNodeID(nodeID); var container = ReactMount.findReactContainerForID(rootID); var parent = ReactMount.getFirstReactDOM(container); return parent; } // Used to store ancestor hierarchy in top level callback function TopLevelCallbackBookKeeping(topLevelType, nativeEvent) { this.topLevelType = topLevelType; this.nativeEvent = nativeEvent; this.ancestors = []; } assign(TopLevelCallbackBookKeeping.prototype, { destructor: function () { this.topLevelType = null; this.nativeEvent = null; this.ancestors.length = 0; } }); PooledClass.addPoolingTo(TopLevelCallbackBookKeeping, PooledClass.twoArgumentPooler); function handleTopLevelImpl(bookKeeping) { // TODO: Re-enable event.path handling // // if (bookKeeping.nativeEvent.path && bookKeeping.nativeEvent.path.length > 1) { // // New browsers have a path attribute on native events // handleTopLevelWithPath(bookKeeping); // } else { // // Legacy browsers don't have a path attribute on native events // handleTopLevelWithoutPath(bookKeeping); // } void handleTopLevelWithPath; // temporarily unused handleTopLevelWithoutPath(bookKeeping); } // Legacy browsers don't have a path attribute on native events function handleTopLevelWithoutPath(bookKeeping) { var topLevelTarget = ReactMount.getFirstReactDOM(getEventTarget(bookKeeping.nativeEvent)) || window; // Loop through the hierarchy, in case there's any nested components. // It's important that we build the array of ancestors before calling any // event handlers, because event handlers can modify the DOM, leading to // inconsistencies with ReactMount's node cache. See #1105. var ancestor = topLevelTarget; while (ancestor) { bookKeeping.ancestors.push(ancestor); ancestor = findParent(ancestor); } for (var i = 0; i < bookKeeping.ancestors.length; i++) { topLevelTarget = bookKeeping.ancestors[i]; var topLevelTargetID = ReactMount.getID(topLevelTarget) || ''; ReactEventListener._handleTopLevel(bookKeeping.topLevelType, topLevelTarget, topLevelTargetID, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); } } // New browsers have a path attribute on native events function handleTopLevelWithPath(bookKeeping) { var path = bookKeeping.nativeEvent.path; var currentNativeTarget = path[0]; var eventsFired = 0; for (var i = 0; i < path.length; i++) { var currentPathElement = path[i]; if (currentPathElement.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE) { currentNativeTarget = path[i + 1]; } // TODO: slow var reactParent = ReactMount.getFirstReactDOM(currentPathElement); if (reactParent === currentPathElement) { var currentPathElementID = ReactMount.getID(currentPathElement); var newRootID = ReactInstanceHandles.getReactRootIDFromNodeID(currentPathElementID); bookKeeping.ancestors.push(currentPathElement); var topLevelTargetID = ReactMount.getID(currentPathElement) || ''; eventsFired++; ReactEventListener._handleTopLevel(bookKeeping.topLevelType, currentPathElement, topLevelTargetID, bookKeeping.nativeEvent, currentNativeTarget); // Jump to the root of this React render tree while (currentPathElementID !== newRootID) { i++; currentPathElement = path[i]; currentPathElementID = ReactMount.getID(currentPathElement); } } } if (eventsFired === 0) { ReactEventListener._handleTopLevel(bookKeeping.topLevelType, window, '', bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); } } function scrollValueMonitor(cb) { var scrollPosition = getUnboundedScrollPosition(window); cb(scrollPosition); } var ReactEventListener = { _enabled: true, _handleTopLevel: null, WINDOW_HANDLE: ExecutionEnvironment.canUseDOM ? window : null, setHandleTopLevel: function (handleTopLevel) { ReactEventListener._handleTopLevel = handleTopLevel; }, setEnabled: function (enabled) { ReactEventListener._enabled = !!enabled; }, isEnabled: function () { return ReactEventListener._enabled; }, /** * Traps top-level events by using event bubbling. * * @param {string} topLevelType Record from `EventConstants`. * @param {string} handlerBaseName Event name (e.g. "click"). * @param {object} handle Element on which to attach listener. * @return {?object} An object with a remove function which will forcefully * remove the listener. * @internal */ trapBubbledEvent: function (topLevelType, handlerBaseName, handle) { var element = handle; if (!element) { return null; } return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType)); }, /** * Traps a top-level event by using event capturing. * * @param {string} topLevelType Record from `EventConstants`. * @param {string} handlerBaseName Event name (e.g. "click"). * @param {object} handle Element on which to attach listener. * @return {?object} An object with a remove function which will forcefully * remove the listener. * @internal */ trapCapturedEvent: function (topLevelType, handlerBaseName, handle) { var element = handle; if (!element) { return null; } return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType)); }, monitorScrollValue: function (refresh) { var callback = scrollValueMonitor.bind(null, refresh); EventListener.listen(window, 'scroll', callback); }, dispatchEvent: function (topLevelType, nativeEvent) { if (!ReactEventListener._enabled) { return; } var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent); try { // Event queue being processed in the same cycle allows // `preventDefault`. ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } } }; module.exports = ReactEventListener;