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..dc7978e
--- /dev/null
+++ b/src/fiber/ScreenEventTypes.js
@@ -0,0 +1,14 @@
+// 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_FOCUS,
+ SCREEN_BLUR
+];
+
+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/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/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/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({
diff --git a/src/fiber/events.js b/src/fiber/events.js
index 5f2273f..43179e2 100644
--- a/src/fiber/events.js
+++ b/src/fiber/events.js
@@ -1,39 +1,183 @@
-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';
+import SyntheticFocusEvent from './SyntheticFocusEvent';
+
+const eventSubscriptionMappings = {};
+const eventRegistrationNameToConfig = {};
+const syntheticEventDispatchConfigs = {};
+
+const screenEventTransforms = {
+ [ScreenEventTypes.SCREEN_KEYPRESS]: (targetInst, eventTarget, ...args) => {
+ const [ch, key] = args;
+
+ const keyPressEvent = {
+ type: 'keypress', // native event type
+ target: targetInst,
+ bubbles: true,
+ cancelable: true,
+
+ altKey: false,
+ metaKey: key.meta,
+ ctrlKey: key.ctrl,
+ shiftKey: key.shift,
+ repeat: false,
+ key: ch,
+ charCode: ch.charCodeAt(0)
+ };
+
+ 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]: (targetInst, eventTarget, ...args) => {
+ const [old] = args;
+ const eventData = {
+ type: 'focus',
+ target: targetInst,
+ bubbles: false,
+ cancelable: false,
+
+ relatedTarget: old
+ };
+ return [SyntheticFocusEvent.getPooled(
+ syntheticEventDispatchConfigs[eventData.type],
+ targetInst,
+ eventData,
+ eventTarget
+ )];
+ },
+ [ScreenEventTypes.SCREEN_BLUR]: (targetInst, eventTarget, ...args) => {
+ const [next] = args;
+ const eventData = {
+ type: 'blur',
+ target: targetInst,
+ bubbles: false,
+ cancelable: false,
+
+ relatedTarget: next
+ };
+ return [SyntheticFocusEvent.getPooled(
+ syntheticEventDispatchConfigs[eventData.type],
+ targetInst,
+ eventData,
+ eventTarget
+ )];
+ }
+};
+
+// 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]
];
-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;
+ syntheticEventDispatchConfigs[eventName.toLowerCase()] = 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, false));
+ root.screen.on('element ' + screenEventName, dispatchScreenEvent.bind(null, root, screenEventType, true));
}
- */
+}
- if (typeof handler === 'function') {
- if (event === 'focus' || event === 'blur') {
- args[0] = node;
- }
- handler(...args);
+function dispatchScreenEvent(root, screenEventType, firstArgIsTarget, ...args) {
+ const targetInst = firstArgIsTarget ? args.shift() : root.screen.focused;
+ const eventTransform = screenEventTransforms[screenEventType];
+
+ if (!eventTransform) {
+ throw new Error('unhandled event: ' + screenEventType);
}
-};
+ const syntheticEvents = eventTransform(
+ targetInst,
+ targetInst,
+ ...args
+ );
+
+ // Gather list of ancestors that have listeners
+ 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);
+ }
+ }
+ node = node.parent;
+ } while (node != root);
+
+ // Capture phase
+ captureListeners.forEach(element => {
+ if (syntheticEvent.isPropagationStopped()) {
+ return;
+ }
+ element.props[dispatchConfig.phasedRegistrationNames.captured].call(element, syntheticEvent);
+ });
-export default eventListener;
+ 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 0cc43ff..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'
@@ -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,
@@ -46,10 +46,9 @@ 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 @@ 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 @@ const createBlessedRenderer = function(blessed) {
child : Instance | TextInstance
) : void {
parentInstance.remove(child);
- child.off('event', child._eventListener);
child.destroy();
},
@@ -197,7 +196,6 @@ const createBlessedRenderer = function(blessed) {
child : Instance | TextInstance
) : void {
parentInstance.remove(child);
- child.off('event', child._eventListener);
child.destroy();
},
@@ -238,11 +236,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);
+}
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"