From 8b549ef425d28a087921fcf5564f394424dadab0 Mon Sep 17 00:00:00 2001 From: Shaheen Gandhi Date: Sat, 22 Jun 2019 15:19:15 -0700 Subject: [PATCH 1/5] [es] Make react-blessed ES compatible --- src/fiber/fiber.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/fiber/fiber.js b/src/fiber/fiber.js index 0cc43ff..7f91633 100644 --- a/src/fiber/fiber.js +++ b/src/fiber/fiber.js @@ -10,7 +10,7 @@ import injectIntoDevToolsConfig from './devtools' const emptyObject = {}; let runningEffects = []; -const createBlessedRenderer = function(blessed) { +export const createBlessedRenderer = function(blessed) { type Instance = { type: string, props: Object, @@ -238,11 +238,8 @@ const createBlessedRenderer = function(blessed) { } } -module.exports = { - render: function render(element, screen, callback) { - const blessed = require('blessed'); - const renderer = createBlessedRenderer(blessed); - return renderer(element, screen, callback); - }, - createBlessedRenderer: createBlessedRenderer -}; +export function render(element, screen, callback) { + const blessed = require('blessed'); + const renderer = createBlessedRenderer(blessed); + return renderer(element, screen, callback); +} From bfbf17f967b9a46ab2625b2009c2db39add20c5b Mon Sep 17 00:00:00 2001 From: Shaheen Gandhi Date: Mon, 1 Jul 2019 22:08:30 -0400 Subject: [PATCH 2/5] Generate and dispatch SyntheticEvents --- examples/hotkeys.jsx | 50 ++++ package.json | 1 + run.js | 1 + src/fiber/ScreenEventTypes.js | 10 + src/fiber/SyntheticEvent.js | 339 ++++++++++++++++++++++++++++ src/fiber/SyntheticKeyboardEvent.js | 65 ++++++ src/fiber/SyntheticUIEvent.js | 15 ++ src/fiber/events.js | 152 ++++++++++--- src/fiber/fiber.js | 8 +- src/fiber/getEventCharCode.js | 49 ++++ src/fiber/getEventKey.js | 108 +++++++++ src/fiber/getEventModifierState.js | 41 ++++ src/shared/invariant.js | 54 +++++ src/shared/warningWithoutStack.js | 54 +++++ yarn.lock | 8 +- 15 files changed, 919 insertions(+), 36 deletions(-) create mode 100644 examples/hotkeys.jsx create mode 100644 src/fiber/ScreenEventTypes.js create mode 100644 src/fiber/SyntheticEvent.js create mode 100644 src/fiber/SyntheticKeyboardEvent.js create mode 100644 src/fiber/SyntheticUIEvent.js create mode 100644 src/fiber/getEventCharCode.js create mode 100644 src/fiber/getEventKey.js create mode 100644 src/fiber/getEventModifierState.js create mode 100644 src/shared/invariant.js create mode 100644 src/shared/warningWithoutStack.js diff --git a/examples/hotkeys.jsx b/examples/hotkeys.jsx new file mode 100644 index 0000000..47cb4dc --- /dev/null +++ b/examples/hotkeys.jsx @@ -0,0 +1,50 @@ +import React, {Component} from 'react'; +import blessed from 'neo-blessed'; +import {HotKeys, configure} from 'react-hotkeys'; +import {createBlessedRenderer} from '../src'; + +const render = createBlessedRenderer(blessed); + +configure({ + defaultComponent: 'element', + defaultKeyEvent: 'keypress' +}); + +const keyMap = { + 'quit': ['q'] +}; + +class App extends Component { + componentDidMount() { + this._hotkeysContainer && this._hotkeysContainer.focus(); + } + + render() { + const handlers = { + 'quit': () => { + process.exit(); + } + }; + return ( + this._hotkeysContainer = c}> + + This example uses neo-blessed fork of blessed library. + + + ); + } +} + +const screen = blessed.screen({ + autoPadding: true, + smartCSR: true, + title: 'press q to quit' +}); + +screen.enableInput(); +screen.key(['C-c'], (ch, key) => {process.exit();}); + +global.window = screen; +const component = render(, screen); diff --git a/package.json b/package.json index 45563a0..63bbbf4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "lodash": "^4.x.x", "mocha": "^4.0.1", "neo-blessed": "^0.2.0", + "react-hotkeys": "git+https://github.com/visigoth/react-hotkeys#0bf84cc4b7cb264c663a349b6b02575a1a577b49", "react-motion": "^0.5.2", "rollup": "^0.65.0", "rollup-plugin-babel": "^4.0.2", diff --git a/run.js b/run.js index 64677de..c7fe5ad 100644 --- a/run.js +++ b/run.js @@ -19,6 +19,7 @@ const examples = [ 'neo-blessed', 'progressbar', 'remove', + 'hotkeys' ]; if (examples.indexOf(example) === -1) { diff --git a/src/fiber/ScreenEventTypes.js b/src/fiber/ScreenEventTypes.js new file mode 100644 index 0000000..604b21e --- /dev/null +++ b/src/fiber/ScreenEventTypes.js @@ -0,0 +1,10 @@ +// Constants for native events emitted by screen +export const SCREEN_KEYPRESS = 'keypress'; + +export const all = [ + SCREEN_KEYPRESS +]; + +export function screenEventName(eventType) { + return eventType; +}; \ No newline at end of file diff --git a/src/fiber/SyntheticEvent.js b/src/fiber/SyntheticEvent.js new file mode 100644 index 0000000..3de25ea --- /dev/null +++ b/src/fiber/SyntheticEvent.js @@ -0,0 +1,339 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint valid-typeof: 0 */ + +import invariant from '../shared/invariant'; +import warningWithoutStack from '../shared/warningWithoutStack'; + +const EVENT_POOL_SIZE = 10; + +/** + * @interface Event + * @see http://www.w3.org/TR/DOM-Level-3-Events/ + */ +const EventInterface = { + type: null, + target: null, + // currentTarget is set when dispatching; no use in copying it here + currentTarget: function() { + return null; + }, + eventPhase: null, + bubbles: null, + cancelable: null, + timeStamp: function(event) { + return event.timeStamp || Date.now(); + }, + defaultPrevented: null, + isTrusted: null, +}; + +function functionThatReturnsTrue() { + return true; +} + +function functionThatReturnsFalse() { + return false; +} + +/** + * Synthetic events are dispatched by event plugins, typically in response to a + * top-level event delegation handler. + * + * These systems should generally use pooling to reduce the frequency of garbage + * collection. The system should check `isPersistent` to determine whether the + * event should be released into the pool after being dispatched. Users that + * need a persisted event should invoke `persist`. + * + * Synthetic events (and subclasses) implement the DOM Level 3 Events API by + * normalizing browser quirks. Subclasses do not necessarily have to implement a + * DOM interface; custom application-specific events can also subclass this. + * + * @param {object} dispatchConfig Configuration used to dispatch this event. + * @param {*} targetInst Marker identifying the event target. + * @param {object} nativeEvent Native browser event. + * @param {DOMEventTarget} nativeEventTarget Target node. + */ +function SyntheticEvent( + dispatchConfig, + targetInst, + nativeEvent, + nativeEventTarget, +) { + if (process.env.NODE_ENV == 'development') { + // these have a getter/setter for warnings + delete this.nativeEvent; + delete this.preventDefault; + delete this.stopPropagation; + delete this.isDefaultPrevented; + delete this.isPropagationStopped; + } + + this.dispatchConfig = dispatchConfig; + this._targetInst = targetInst; + this.nativeEvent = nativeEvent; + + const Interface = this.constructor.Interface; + for (const propName in Interface) { + if (!Interface.hasOwnProperty(propName)) { + continue; + } + if (process.env.NODE_ENV == 'development') { + delete this[propName]; // this has a getter/setter for warnings + } + const normalize = Interface[propName]; + if (normalize) { + this[propName] = normalize(nativeEvent); + } else { + if (propName === 'target') { + this.target = nativeEventTarget; + } else { + this[propName] = nativeEvent[propName]; + } + } + } + + const defaultPrevented = + nativeEvent.defaultPrevented != null + ? nativeEvent.defaultPrevented + : nativeEvent.returnValue === false; + if (defaultPrevented) { + this.isDefaultPrevented = functionThatReturnsTrue; + } else { + this.isDefaultPrevented = functionThatReturnsFalse; + } + this.isPropagationStopped = functionThatReturnsFalse; + return this; +} + +Object.assign(SyntheticEvent.prototype, { + preventDefault: function() { + this.defaultPrevented = true; + const event = this.nativeEvent; + if (!event) { + return; + } + + if (event.preventDefault) { + event.preventDefault(); + } else if (typeof event.returnValue !== 'unknown') { + event.returnValue = false; + } + this.isDefaultPrevented = functionThatReturnsTrue; + }, + + stopPropagation: function() { + const event = this.nativeEvent; + if (!event) { + return; + } + + if (event.stopPropagation) { + event.stopPropagation(); + } else if (typeof event.cancelBubble !== 'unknown') { + // The ChangeEventPlugin registers a "propertychange" event for + // IE. This event does not support bubbling or cancelling, and + // any references to cancelBubble throw "Member not found". A + // typeof check of "unknown" circumvents this issue (and is also + // IE specific). + event.cancelBubble = true; + } + + this.isPropagationStopped = functionThatReturnsTrue; + }, + + /** + * We release all dispatched `SyntheticEvent`s after each event loop, adding + * them back into the pool. This allows a way to hold onto a reference that + * won't be added back into the pool. + */ + persist: function() { + this.isPersistent = functionThatReturnsTrue; + }, + + /** + * Checks if this event should be released back into the pool. + * + * @return {boolean} True if this should not be released, false otherwise. + */ + isPersistent: functionThatReturnsFalse, + + /** + * `PooledClass` looks for `destructor` on each instance it releases. + */ + destructor: function() { + const Interface = this.constructor.Interface; + for (const propName in Interface) { + if (__DEV__) { + Object.defineProperty( + this, + propName, + getPooledWarningPropertyDefinition(propName, Interface[propName]), + ); + } else { + this[propName] = null; + } + } + this.dispatchConfig = null; + this._targetInst = null; + this.nativeEvent = null; + this.isDefaultPrevented = functionThatReturnsFalse; + this.isPropagationStopped = functionThatReturnsFalse; + this._dispatchListeners = null; + this._dispatchInstances = null; + if (__DEV__) { + Object.defineProperty( + this, + 'nativeEvent', + getPooledWarningPropertyDefinition('nativeEvent', null), + ); + Object.defineProperty( + this, + 'isDefaultPrevented', + getPooledWarningPropertyDefinition( + 'isDefaultPrevented', + functionThatReturnsFalse, + ), + ); + Object.defineProperty( + this, + 'isPropagationStopped', + getPooledWarningPropertyDefinition( + 'isPropagationStopped', + functionThatReturnsFalse, + ), + ); + Object.defineProperty( + this, + 'preventDefault', + getPooledWarningPropertyDefinition('preventDefault', () => {}), + ); + Object.defineProperty( + this, + 'stopPropagation', + getPooledWarningPropertyDefinition('stopPropagation', () => {}), + ); + } + }, +}); + +SyntheticEvent.Interface = EventInterface; + +/** + * Helper to reduce boilerplate when creating subclasses. + */ +SyntheticEvent.extend = function(Interface) { + const Super = this; + + const E = function() {}; + E.prototype = Super.prototype; + const prototype = new E(); + + function Class() { + return Super.apply(this, arguments); + } + Object.assign(prototype, Class.prototype); + Class.prototype = prototype; + Class.prototype.constructor = Class; + + Class.Interface = Object.assign({}, Super.Interface, Interface); + Class.extend = Super.extend; + addEventPoolingTo(Class); + + return Class; +}; + +addEventPoolingTo(SyntheticEvent); + +/** + * Helper to nullify syntheticEvent instance properties when destructing + * + * @param {String} propName + * @param {?object} getVal + * @return {object} defineProperty object + */ +function getPooledWarningPropertyDefinition(propName, getVal) { + const isFunction = typeof getVal === 'function'; + return { + configurable: true, + set: set, + get: get, + }; + + function set(val) { + const action = isFunction ? 'setting the method' : 'setting the property'; + warn(action, 'This is effectively a no-op'); + return val; + } + + function get() { + const action = isFunction + ? 'accessing the method' + : 'accessing the property'; + const result = isFunction + ? 'This is a no-op function' + : 'This is set to null'; + warn(action, result); + return getVal; + } + + function warn(action, result) { + const warningCondition = false; + warningWithoutStack( + warningCondition, + "This synthetic event is reused for performance reasons. If you're seeing this, " + + "you're %s `%s` on a released/nullified synthetic event. %s. " + + 'If you must keep the original synthetic event around, use event.persist(). ' + + 'See https://fb.me/react-event-pooling for more information.', + action, + propName, + result, + ); + } +} + +function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) { + const EventConstructor = this; + if (EventConstructor.eventPool.length) { + const instance = EventConstructor.eventPool.pop(); + EventConstructor.call( + instance, + dispatchConfig, + targetInst, + nativeEvent, + nativeInst, + ); + return instance; + } + return new EventConstructor( + dispatchConfig, + targetInst, + nativeEvent, + nativeInst, + ); +} + +function releasePooledEvent(event) { + const EventConstructor = this; + invariant( + event instanceof EventConstructor, + 'Trying to release an event instance into a pool of a different type.', + ); + event.destructor(); + if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) { + EventConstructor.eventPool.push(event); + } +} + +function addEventPoolingTo(EventConstructor) { + EventConstructor.eventPool = []; + EventConstructor.getPooled = getPooledEvent; + EventConstructor.release = releasePooledEvent; +} + +export default SyntheticEvent; diff --git a/src/fiber/SyntheticKeyboardEvent.js b/src/fiber/SyntheticKeyboardEvent.js new file mode 100644 index 0000000..ef1ceed --- /dev/null +++ b/src/fiber/SyntheticKeyboardEvent.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import SyntheticUIEvent from './SyntheticUIEvent'; +import getEventCharCode from './getEventCharCode'; +import getEventKey from './getEventKey'; +import getEventModifierState from './getEventModifierState'; + +/** + * @interface KeyboardEvent + * @see http://www.w3.org/TR/DOM-Level-3-Events/ + */ +const SyntheticKeyboardEvent = SyntheticUIEvent.extend({ + key: getEventKey, + location: null, + ctrlKey: null, + shiftKey: null, + altKey: null, + metaKey: null, + repeat: null, + locale: null, + getModifierState: getEventModifierState, + // Legacy Interface + charCode: function(event) { + // `charCode` is the result of a KeyPress event and represents the value of + // the actual printable character. + + // KeyPress is deprecated, but its replacement is not yet final and not + // implemented in any major browser. Only KeyPress has charCode. + if (event.type === 'keypress') { + return getEventCharCode(event); + } + return 0; + }, + keyCode: function(event) { + // `keyCode` is the result of a KeyDown/Up event and represents the value of + // physical keyboard key. + + // The actual meaning of the value depends on the users' keyboard layout + // which cannot be detected. Assuming that it is a US keyboard layout + // provides a surprisingly accurate mapping for US and European users. + // Due to this, it is left to the user to implement at this time. + if (event.type === 'keydown' || event.type === 'keyup') { + return event.keyCode; + } + return 0; + }, + which: function(event) { + // `which` is an alias for either `keyCode` or `charCode` depending on the + // type of the event. + if (event.type === 'keypress') { + return getEventCharCode(event); + } + if (event.type === 'keydown' || event.type === 'keyup') { + return event.keyCode; + } + return 0; + }, +}); + +export default SyntheticKeyboardEvent; diff --git a/src/fiber/SyntheticUIEvent.js b/src/fiber/SyntheticUIEvent.js new file mode 100644 index 0000000..8e65381 --- /dev/null +++ b/src/fiber/SyntheticUIEvent.js @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import SyntheticEvent from './SyntheticEvent'; + +const SyntheticUIEvent = SyntheticEvent.extend({ + view: null, + detail: null, +}); + +export default SyntheticUIEvent; diff --git a/src/fiber/events.js b/src/fiber/events.js index 5f2273f..867f5a2 100644 --- a/src/fiber/events.js +++ b/src/fiber/events.js @@ -1,39 +1,131 @@ -const startCase = (string) => ( - string.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match) { - return (+match === 0) ? "" : match.toUpperCase(); - }).replace(/[^A-Za-z0-9 ]+/, "") -) - -const blacklist = [ - 'adopt', - 'attach', - 'destroy', - 'reparent', - 'parsed content', - 'set content', +import * as ScreenEventTypes from './ScreenEventTypes'; +import SyntheticKeyboardEvent from './SyntheticKeyboardEvent'; + +const eventSubscriptionMappings = {}; +const eventDispatchConfigs = {}; +const eventRegistrationNameToConfig = {} + +const screenEventTransforms = { + [ScreenEventTypes.SCREEN_KEYPRESS]: (dispatchConfig, targetInst, eventTarget, ...args) => { + const [ch, key] = args; + console.log(ch); + console.log(key); + + const eventData = { + type: ScreenEventTypes.SCREEN_KEYPRESS, + target: targetInst, + bubbles: true, + cancelable: true, + + altKey: false, + metaKey: key.meta, + ctrlKey: key.ctrl, + shiftKey: key.shift, + repeat: false, + key: ch + }; + return SyntheticKeyboardEvent.getPooled( + dispatchConfig, + targetInst, + eventData, + eventTarget + ); + } +}; + +// Pairs of react events and screen native events +const eventTuples = [ + ['keyPress', ScreenEventTypes.SCREEN_KEYPRESS] ]; -const eventName = event => `on${startCase(event)}`; -const eventListener = (node, event, ...args) => { - if (node._updating) return; +function addEventConfiguration(eventName, screenEventType) { + const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1); + const onEvent = 'on' + capitalizedEvent; + const config = { + phasedRegistrationNames: { + bubbled: onEvent, + captured: onEvent + 'Capture', + }, + screenEvents: [screenEventType] + }; + eventSubscriptionMappings[eventName] = config.screenEvents; + eventDispatchConfigs[screenEventType] = config; - const handler = node.props[eventName(event)]; + eventRegistrationNameToConfig[onEvent] = config; + eventRegistrationNameToConfig[onEvent + 'Capture'] = config; +} - /* - if (blacklist.indexOf(event) === -1) { - if (handler) { - console.log(event, ': ', startCase(event).replace(/ /g, '')); - } +eventTuples.forEach(tuple => { + const [event, screenEventType] = tuple; + addEventConfiguration(event, screenEventType); +}); + +const isListening = new Set(); + +function ensureListeningTo(root, screenEventType) { + const screenEventName = ScreenEventTypes.screenEventName(screenEventType); + + if (!isListening.has(screenEventType)) { + isListening.add(screenEventType); + root.screen.on(screenEventName, dispatchScreenEvent.bind(null, root, screenEventType)); } - */ +} - if (typeof handler === 'function') { - if (event === 'focus' || event === 'blur') { - args[0] = node; - } - handler(...args); +function dispatchScreenEvent(root, screenEventType, ...args) { + if (screenEventType != ScreenEventTypes.SCREEN_KEYPRESS) { + return; } -}; -export default eventListener; + const dispatchConfig = eventDispatchConfigs[screenEventType]; + const targetInst = root.screen.focused; + const eventTransform = screenEventTransforms[screenEventType]; + const syntheticEvent = eventTransform( + dispatchConfig, + targetInst, + targetInst, + ...args + ); + + // Gather list of ancestors that have listeners + const captureListeners = []; + const bubbleListeners = []; + var node = targetInst; + do { + if (dispatchConfig.phasedRegistrationNames.captured in node.props) { + captureListeners.unshift(node); + } + if (dispatchConfig.phasedRegistrationNames.bubbled in node.props) { + bubbleListeners.push(node); + } + node = node.parent; + } while (node != root); + + // Capture phase + captureListeners.forEach(element => { + if (syntheticEvent.isPropagationStopped()) { + return; + } + element.props[dispatchConfig.phasedRegistrationNames.captured].call(element, syntheticEvent); + }); + + bubbleListeners.forEach(element => { + if (syntheticEvent.isPropagationStopped()) { + return; + } + element.props[dispatchConfig.phasedRegistrationNames.bubbled].call(element, syntheticEvent); + }); +} + +function updateEventRegistrations(root, instance, props) { + // TODO(shaheen) remove current registrations not appearing in props + for (var propKey in props) { + const config = eventRegistrationNameToConfig[propKey]; + if (config) { + config.screenEvents.forEach(screenEventType => { + ensureListeningTo(root, screenEventType); + }); + } + } +} +export default updateEventRegistrations; diff --git a/src/fiber/fiber.js b/src/fiber/fiber.js index 7f91633..5e42707 100644 --- a/src/fiber/fiber.js +++ b/src/fiber/fiber.js @@ -1,7 +1,7 @@ /* @flow */ import type { HostConfig, Reconciler } from 'react-fiber-types'; import ReactFiberReconciler from 'react-reconciler' -import eventListener from './events' +import updateEventRegistrations from './events' import update from '../shared/update' import solveClass from '../shared/solveClass' import debounce from 'lodash/debounce' @@ -46,10 +46,9 @@ export const createBlessedRenderer = function(blessed) { if (type.startsWith(blessedTypePrefix)) { type = type.slice(blessedTypePrefix.length); } + const instance = blessed[type](appliedProps); instance.props = props; - instance._eventListener = (...args) => eventListener(instance, ...args); - instance.on('event', instance._eventListener); return instance; }, @@ -69,6 +68,7 @@ export const createBlessedRenderer = function(blessed) { ) : boolean { const {children, ...appliedProps} = solveClass(props); update(instance, appliedProps); + updateEventRegistrations(rootContainerInstance, instance, appliedProps); instance.props = props; return false; }, @@ -188,7 +188,6 @@ export const createBlessedRenderer = function(blessed) { child : Instance | TextInstance ) : void { parentInstance.remove(child); - child.off('event', child._eventListener); child.destroy(); }, @@ -197,7 +196,6 @@ export const createBlessedRenderer = function(blessed) { child : Instance | TextInstance ) : void { parentInstance.remove(child); - child.off('event', child._eventListener); child.destroy(); }, diff --git a/src/fiber/getEventCharCode.js b/src/fiber/getEventCharCode.js new file mode 100644 index 0000000..7e6c6bf --- /dev/null +++ b/src/fiber/getEventCharCode.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `charCode` represents the actual "character code" and is safe to use with + * `String.fromCharCode`. As such, only keys that correspond to printable + * characters produce a valid `charCode`, the only exception to this is Enter. + * The Tab-key is considered non-printable and does not have a `charCode`, + * presumably because it does not produce a tab-character in browsers. + * + * @param {object} nativeEvent Native browser event. + * @return {number} Normalized `charCode` property. + */ +function getEventCharCode(nativeEvent) { + let charCode; + const keyCode = nativeEvent.keyCode; + + if ('charCode' in nativeEvent) { + charCode = nativeEvent.charCode; + + // FF does not set `charCode` for the Enter-key, check against `keyCode`. + if (charCode === 0 && keyCode === 13) { + charCode = 13; + } + } else { + // IE8 does not implement `charCode`, but `keyCode` has the correct value. + charCode = keyCode; + } + + // IE and Edge (on Windows) and Chrome / Safari (on Windows and Linux) + // report Enter as charCode 10 when ctrl is pressed. + if (charCode === 10) { + charCode = 13; + } + + // Some non-printable keys are reported in `charCode`/`keyCode`, discard them. + // Must not discard the (non-)printable Enter-key. + if (charCode >= 32 || charCode === 13) { + return charCode; + } + + return 0; +} + +export default getEventCharCode; diff --git a/src/fiber/getEventKey.js b/src/fiber/getEventKey.js new file mode 100644 index 0000000..89834bd --- /dev/null +++ b/src/fiber/getEventKey.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import getEventCharCode from './getEventCharCode'; + +/** + * Normalization of deprecated HTML5 `key` values + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names + */ +const normalizeKey = { + Esc: 'Escape', + Spacebar: ' ', + Left: 'ArrowLeft', + Up: 'ArrowUp', + Right: 'ArrowRight', + Down: 'ArrowDown', + Del: 'Delete', + Win: 'OS', + Menu: 'ContextMenu', + Apps: 'ContextMenu', + Scroll: 'ScrollLock', + MozPrintableKey: 'Unidentified', +}; + +/** + * Translation from legacy `keyCode` to HTML5 `key` + * Only special keys supported, all others depend on keyboard layout or browser + * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names + */ +const translateToKey = { + '8': 'Backspace', + '9': 'Tab', + '12': 'Clear', + '13': 'Enter', + '16': 'Shift', + '17': 'Control', + '18': 'Alt', + '19': 'Pause', + '20': 'CapsLock', + '27': 'Escape', + '32': ' ', + '33': 'PageUp', + '34': 'PageDown', + '35': 'End', + '36': 'Home', + '37': 'ArrowLeft', + '38': 'ArrowUp', + '39': 'ArrowRight', + '40': 'ArrowDown', + '45': 'Insert', + '46': 'Delete', + '112': 'F1', + '113': 'F2', + '114': 'F3', + '115': 'F4', + '116': 'F5', + '117': 'F6', + '118': 'F7', + '119': 'F8', + '120': 'F9', + '121': 'F10', + '122': 'F11', + '123': 'F12', + '144': 'NumLock', + '145': 'ScrollLock', + '224': 'Meta', +}; + +/** + * @param {object} nativeEvent Native browser event. + * @return {string} Normalized `key` property. + */ +function getEventKey(nativeEvent: KeyboardEvent): string { + if (nativeEvent.key) { + // Normalize inconsistent values reported by browsers due to + // implementations of a working draft specification. + + // FireFox implements `key` but returns `MozPrintableKey` for all + // printable characters (normalized to `Unidentified`), ignore it. + const key = normalizeKey[nativeEvent.key] || nativeEvent.key; + if (key !== 'Unidentified') { + return key; + } + } + + // Browser does not implement `key`, polyfill as much of it as we can. + if (nativeEvent.type === 'keypress') { + const charCode = getEventCharCode(nativeEvent); + + // The enter-key is technically both printable and non-printable and can + // thus be captured by `keypress`, no other non-printable key should. + return charCode === 13 ? 'Enter' : String.fromCharCode(charCode); + } + if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') { + // While user keyboard layout determines the actual meaning of each + // `keyCode` value, almost all function keys have a universal value. + return translateToKey[nativeEvent.keyCode] || 'Unidentified'; + } + return ''; +} + +export default getEventKey; diff --git a/src/fiber/getEventModifierState.js b/src/fiber/getEventModifierState.js new file mode 100644 index 0000000..cb72afd --- /dev/null +++ b/src/fiber/getEventModifierState.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +/** + * Translation from modifier key to the associated property in the event. + * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers + */ + +const modifierKeyToProp = { + Alt: 'altKey', + Control: 'ctrlKey', + Meta: 'metaKey', + Shift: 'shiftKey', +}; + +// Older browsers (Safari <= 10, iOS Safari <= 10.2) do not support +// getModifierState. If getModifierState is not supported, we map it to a set of +// modifier keys exposed by the event. In this case, Lock-keys are not supported. +function modifierStateGetter(keyArg) { + const syntheticEvent = this; + const nativeEvent = syntheticEvent.nativeEvent; + if (nativeEvent.getModifierState) { + return nativeEvent.getModifierState(keyArg); + } + const keyProp = modifierKeyToProp[keyArg]; + return keyProp ? !!nativeEvent[keyProp] : false; +} + +function getEventModifierState( + nativeEvent, +) { + return modifierStateGetter; +} + +export default getEventModifierState; diff --git a/src/shared/invariant.js b/src/shared/invariant.js new file mode 100644 index 0000000..8e0583b --- /dev/null +++ b/src/shared/invariant.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * Use invariant() to assert state which your program assumes to be true. + * + * Provide sprintf-style format (only %s is supported) and arguments + * to provide information about what broke and what you were + * expecting. + * + * The invariant message will be stripped in production, but the invariant + * will remain to ensure logic does not differ in production. + */ + +let validateFormat = () => {}; + +if (process.env.NODE_ENV == 'development') { + validateFormat = function(format) { + if (format === undefined) { + throw new Error('invariant requires an error message argument'); + } + }; +} + +export default function invariant(condition, format, a, b, c, d, e, f) { + validateFormat(format); + + if (!condition) { + let error; + if (format === undefined) { + error = new Error( + 'Minified exception occurred; use the non-minified dev environment ' + + 'for the full error message and additional helpful warnings.', + ); + } else { + const args = [a, b, c, d, e, f]; + let argIndex = 0; + error = new Error( + format.replace(/%s/g, function() { + return args[argIndex++]; + }), + ); + error.name = 'Invariant Violation'; + } + + error.framesToPop = 1; // we don't care about invariant's own frame + throw error; + } +} diff --git a/src/shared/warningWithoutStack.js b/src/shared/warningWithoutStack.js new file mode 100644 index 0000000..98a1905 --- /dev/null +++ b/src/shared/warningWithoutStack.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Similar to invariant but only logs a warning if the condition is not met. + * This can be used to log issues in development environments in critical + * paths. Removing the logging code for production environments will keep the + * same logic and follow the same code paths. + */ + +let warningWithoutStack = () => {}; + +if (process.env.NODE_ENV == 'development') { + warningWithoutStack = function(condition, format, ...args) { + if (format === undefined) { + throw new Error( + '`warningWithoutStack(condition, format, ...args)` requires a warning ' + + 'message argument', + ); + } + if (args.length > 8) { + // Check before the condition to catch violations early. + throw new Error( + 'warningWithoutStack() currently supports at most 8 arguments.', + ); + } + if (condition) { + return; + } + if (typeof console !== 'undefined') { + const argsWithFormat = args.map(item => '' + item); + argsWithFormat.unshift('Warning: ' + format); + + // We intentionally don't use spread (or .apply) directly because it + // breaks IE9: https://github.com/facebook/react/issues/13610 + Function.prototype.apply.call(console.error, console, argsWithFormat); + } + try { + // --- Welcome to debugging React --- + // This error was thrown as a convenience so that you can use this stack + // to find the callsite that caused this warning to fire. + let argIndex = 0; + const message = + 'Warning: ' + format.replace(/%s/g, () => args[argIndex++]); + throw new Error(message); + } catch (x) {} + }; +} + +export default warningWithoutStack; diff --git a/yarn.lock b/yarn.lock index fb8700d..0bcd845 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2090,7 +2090,7 @@ prop-types@^15.5.8: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.6.2: +prop-types@^15.6.1, prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -2123,6 +2123,12 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +"react-hotkeys@git+https://github.com/visigoth/react-hotkeys#0bf84cc4b7cb264c663a349b6b02575a1a577b49": + version "2.0.0-pre8" + resolved "git+https://github.com/visigoth/react-hotkeys#0bf84cc4b7cb264c663a349b6b02575a1a577b49" + dependencies: + prop-types "^15.6.1" + react-is@^16.8.1: version "16.8.6" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" From c7418dc095c65f269db8c36938cc660c0a7be225 Mon Sep 17 00:00:00 2001 From: Shaheen Gandhi Date: Wed, 3 Jul 2019 03:17:09 -0400 Subject: [PATCH 3/5] Get focus and blur working --- src/fiber/ScreenEventTypes.js | 6 ++- src/fiber/SyntheticFocusEvent.js | 18 ++++++++ src/fiber/events.js | 71 +++++++++++++++++++++++++------- 3 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 src/fiber/SyntheticFocusEvent.js diff --git a/src/fiber/ScreenEventTypes.js b/src/fiber/ScreenEventTypes.js index 604b21e..dc7978e 100644 --- a/src/fiber/ScreenEventTypes.js +++ b/src/fiber/ScreenEventTypes.js @@ -1,8 +1,12 @@ // Constants for native events emitted by screen export const SCREEN_KEYPRESS = 'keypress'; +export const SCREEN_FOCUS = 'focus'; +export const SCREEN_BLUR = 'blur'; export const all = [ - SCREEN_KEYPRESS + SCREEN_KEYPRESS, + SCREEN_FOCUS, + SCREEN_BLUR ]; export function screenEventName(eventType) { diff --git a/src/fiber/SyntheticFocusEvent.js b/src/fiber/SyntheticFocusEvent.js new file mode 100644 index 0000000..8a2d14f --- /dev/null +++ b/src/fiber/SyntheticFocusEvent.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import SyntheticUIEvent from './SyntheticUIEvent'; + +/** + * @interface FocusEvent + * @see http://www.w3.org/TR/DOM-Level-3-Events/ + */ +const SyntheticFocusEvent = SyntheticUIEvent.extend({ + relatedTarget: null, +}); + +export default SyntheticFocusEvent; diff --git a/src/fiber/events.js b/src/fiber/events.js index 867f5a2..ed0a90c 100644 --- a/src/fiber/events.js +++ b/src/fiber/events.js @@ -1,5 +1,6 @@ import * as ScreenEventTypes from './ScreenEventTypes'; import SyntheticKeyboardEvent from './SyntheticKeyboardEvent'; +import SyntheticFocusEvent from './SyntheticFocusEvent'; const eventSubscriptionMappings = {}; const eventDispatchConfigs = {}; @@ -8,8 +9,6 @@ const eventRegistrationNameToConfig = {} const screenEventTransforms = { [ScreenEventTypes.SCREEN_KEYPRESS]: (dispatchConfig, targetInst, eventTarget, ...args) => { const [ch, key] = args; - console.log(ch); - console.log(key); const eventData = { type: ScreenEventTypes.SCREEN_KEYPRESS, @@ -22,7 +21,8 @@ const screenEventTransforms = { ctrlKey: key.ctrl, shiftKey: key.shift, repeat: false, - key: ch + key: ch, + charCode: ch.charCodeAt(0) }; return SyntheticKeyboardEvent.getPooled( dispatchConfig, @@ -30,12 +30,48 @@ const screenEventTransforms = { eventData, eventTarget ); + }, + [ScreenEventTypes.SCREEN_FOCUS]: (dispatchConfig, targetInst, eventTarget, ...args) => { + const [old] = args; + const eventData = { + type: ScreenEventTypes.SCREEN_FOCUS, + target: targetInst, + bubbles: false, + cancelable: false, + + relatedTarget: old + }; + return SyntheticFocusEvent.getPooled( + dispatchConfig, + targetInst, + eventData, + eventTarget + ) + }, + [ScreenEventTypes.SCREEN_BLUR]: (dispatchConfig, targetInst, eventTarget, ...args) => { + const [next] = args; + const eventData = { + type: ScreenEventTypes.SCREEN_BLUR, + target: targetInst, + bubbles: false, + cancelable: false, + + relatedTarget: next + }; + return SyntheticFocusEvent.getPooled( + dispatchConfig, + targetInst, + eventData, + eventTarget + ) } }; // Pairs of react events and screen native events const eventTuples = [ - ['keyPress', ScreenEventTypes.SCREEN_KEYPRESS] + ['keyPress', ScreenEventTypes.SCREEN_KEYPRESS], + ['focus', ScreenEventTypes.SCREEN_FOCUS], + ['blur', ScreenEventTypes.SCREEN_BLUR] ]; function addEventConfiguration(eventName, screenEventType) { @@ -67,18 +103,19 @@ function ensureListeningTo(root, screenEventType) { if (!isListening.has(screenEventType)) { isListening.add(screenEventType); - root.screen.on(screenEventName, dispatchScreenEvent.bind(null, root, screenEventType)); + root.screen.on(screenEventName, dispatchScreenEvent.bind(null, root, screenEventType, false)); + root.screen.on('element ' + screenEventName, dispatchScreenEvent.bind(null, root, screenEventType, true)); } } -function dispatchScreenEvent(root, screenEventType, ...args) { - if (screenEventType != ScreenEventTypes.SCREEN_KEYPRESS) { - return; - } - +function dispatchScreenEvent(root, screenEventType, firstArgIsTarget, ...args) { const dispatchConfig = eventDispatchConfigs[screenEventType]; - const targetInst = root.screen.focused; + const targetInst = firstArgIsTarget ? args.shift() : root.screen.focused; const eventTransform = screenEventTransforms[screenEventType]; + + if (!eventTransform) { + throw new Error('unhandled event: ' + screenEventType); + } const syntheticEvent = eventTransform( dispatchConfig, targetInst, @@ -91,11 +128,13 @@ function dispatchScreenEvent(root, screenEventType, ...args) { const bubbleListeners = []; var node = targetInst; do { - if (dispatchConfig.phasedRegistrationNames.captured in node.props) { - captureListeners.unshift(node); - } - if (dispatchConfig.phasedRegistrationNames.bubbled in node.props) { - bubbleListeners.push(node); + if (node.props) { + if (dispatchConfig.phasedRegistrationNames.captured in node.props) { + captureListeners.unshift(node); + } + if (dispatchConfig.phasedRegistrationNames.bubbled in node.props) { + bubbleListeners.push(node); + } } node = node.parent; } while (node != root); From 9ae5e74c937cb63569522660db932cb3f63dd0c5 Mon Sep 17 00:00:00 2001 From: Shaheen Gandhi Date: Thu, 4 Jul 2019 00:23:01 -0400 Subject: [PATCH 4/5] Bookend keypress with keydown and keyup --- src/fiber/events.js | 113 ++++++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 50 deletions(-) diff --git a/src/fiber/events.js b/src/fiber/events.js index ed0a90c..43179e2 100644 --- a/src/fiber/events.js +++ b/src/fiber/events.js @@ -3,15 +3,15 @@ import SyntheticKeyboardEvent from './SyntheticKeyboardEvent'; import SyntheticFocusEvent from './SyntheticFocusEvent'; const eventSubscriptionMappings = {}; -const eventDispatchConfigs = {}; -const eventRegistrationNameToConfig = {} +const eventRegistrationNameToConfig = {}; +const syntheticEventDispatchConfigs = {}; const screenEventTransforms = { - [ScreenEventTypes.SCREEN_KEYPRESS]: (dispatchConfig, targetInst, eventTarget, ...args) => { + [ScreenEventTypes.SCREEN_KEYPRESS]: (targetInst, eventTarget, ...args) => { const [ch, key] = args; - const eventData = { - type: ScreenEventTypes.SCREEN_KEYPRESS, + const keyPressEvent = { + type: 'keypress', // native event type target: targetInst, bubbles: true, cancelable: true, @@ -24,52 +24,63 @@ const screenEventTransforms = { key: ch, charCode: ch.charCodeAt(0) }; - return SyntheticKeyboardEvent.getPooled( - dispatchConfig, - targetInst, - eventData, - eventTarget + + const keyDownEvent = Object.assign({}, keyPressEvent); + keyDownEvent.type = 'keydown'; + + const keyUpEvent = Object.assign({}, keyPressEvent); + keyUpEvent.type = 'keyup'; + + return [keyDownEvent, keyPressEvent, keyUpEvent].map(eventData => + SyntheticKeyboardEvent.getPooled( + syntheticEventDispatchConfigs[eventData.type], + targetInst, + eventData, + eventTarget + ) ); }, - [ScreenEventTypes.SCREEN_FOCUS]: (dispatchConfig, targetInst, eventTarget, ...args) => { + [ScreenEventTypes.SCREEN_FOCUS]: (targetInst, eventTarget, ...args) => { const [old] = args; const eventData = { - type: ScreenEventTypes.SCREEN_FOCUS, + type: 'focus', target: targetInst, bubbles: false, cancelable: false, relatedTarget: old }; - return SyntheticFocusEvent.getPooled( - dispatchConfig, + return [SyntheticFocusEvent.getPooled( + syntheticEventDispatchConfigs[eventData.type], targetInst, eventData, eventTarget - ) + )]; }, - [ScreenEventTypes.SCREEN_BLUR]: (dispatchConfig, targetInst, eventTarget, ...args) => { + [ScreenEventTypes.SCREEN_BLUR]: (targetInst, eventTarget, ...args) => { const [next] = args; const eventData = { - type: ScreenEventTypes.SCREEN_BLUR, + type: 'blur', target: targetInst, bubbles: false, cancelable: false, relatedTarget: next }; - return SyntheticFocusEvent.getPooled( - dispatchConfig, + return [SyntheticFocusEvent.getPooled( + syntheticEventDispatchConfigs[eventData.type], targetInst, eventData, eventTarget - ) + )]; } }; -// Pairs of react events and screen native events +// Pairs of react event types and screen native events const eventTuples = [ + ['keyDown', ScreenEventTypes.SCREEN_KEYPRESS], ['keyPress', ScreenEventTypes.SCREEN_KEYPRESS], + ['keyUp', ScreenEventTypes.SCREEN_KEYPRESS], ['focus', ScreenEventTypes.SCREEN_FOCUS], ['blur', ScreenEventTypes.SCREEN_BLUR] ]; @@ -85,7 +96,7 @@ function addEventConfiguration(eventName, screenEventType) { screenEvents: [screenEventType] }; eventSubscriptionMappings[eventName] = config.screenEvents; - eventDispatchConfigs[screenEventType] = config; + syntheticEventDispatchConfigs[eventName.toLowerCase()] = config; eventRegistrationNameToConfig[onEvent] = config; eventRegistrationNameToConfig[onEvent + 'Capture'] = config; @@ -109,49 +120,51 @@ function ensureListeningTo(root, screenEventType) { } function dispatchScreenEvent(root, screenEventType, firstArgIsTarget, ...args) { - const dispatchConfig = eventDispatchConfigs[screenEventType]; const targetInst = firstArgIsTarget ? args.shift() : root.screen.focused; const eventTransform = screenEventTransforms[screenEventType]; if (!eventTransform) { throw new Error('unhandled event: ' + screenEventType); } - const syntheticEvent = eventTransform( - dispatchConfig, + const syntheticEvents = eventTransform( targetInst, targetInst, ...args ); // Gather list of ancestors that have listeners - const captureListeners = []; - const bubbleListeners = []; - var node = targetInst; - do { - if (node.props) { - if (dispatchConfig.phasedRegistrationNames.captured in node.props) { - captureListeners.unshift(node); + syntheticEvents.forEach(syntheticEvent => { + const dispatchConfig = syntheticEvent.dispatchConfig; + const captureListeners = []; + const bubbleListeners = []; + + var node = targetInst; + do { + if (node.props) { + if (dispatchConfig.phasedRegistrationNames.captured in node.props) { + captureListeners.unshift(node); + } + if (dispatchConfig.phasedRegistrationNames.bubbled in node.props) { + bubbleListeners.push(node); + } } - if (dispatchConfig.phasedRegistrationNames.bubbled in node.props) { - bubbleListeners.push(node); - } - } - node = node.parent; - } while (node != root); + node = node.parent; + } while (node != root); - // Capture phase - captureListeners.forEach(element => { - if (syntheticEvent.isPropagationStopped()) { - return; - } - element.props[dispatchConfig.phasedRegistrationNames.captured].call(element, syntheticEvent); - }); + // Capture phase + captureListeners.forEach(element => { + if (syntheticEvent.isPropagationStopped()) { + return; + } + element.props[dispatchConfig.phasedRegistrationNames.captured].call(element, syntheticEvent); + }); - bubbleListeners.forEach(element => { - if (syntheticEvent.isPropagationStopped()) { - return; - } - element.props[dispatchConfig.phasedRegistrationNames.bubbled].call(element, syntheticEvent); + bubbleListeners.forEach(element => { + if (syntheticEvent.isPropagationStopped()) { + return; + } + element.props[dispatchConfig.phasedRegistrationNames.bubbled].call(element, syntheticEvent); + }); }); } From 4ab9307b030f96275ae7f734d9bd92f5095eb3cd Mon Sep 17 00:00:00 2001 From: Shaheen Gandhi Date: Thu, 4 Jul 2019 18:47:19 -0400 Subject: [PATCH 5/5] Do not map global.window to global --- src/fiber/devtools.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/fiber/devtools.js b/src/fiber/devtools.js index 9176042..b3319b6 100644 --- a/src/fiber/devtools.js +++ b/src/fiber/devtools.js @@ -5,9 +5,6 @@ try { defineProperty(global, 'WebSocket', { value: require('ws') }); - defineProperty(global, 'window', { - value: global - }); const {connectToDevTools} = require('react-devtools-core'); connectToDevTools({