UNPKG

@enact/core

Version:

Enact is an open source JavaScript framework containing everything you need to create a fast, scalable mobile or web application.

860 lines (831 loc) 29.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.stopImmediate = exports.stop = exports.returnsTrue = exports.preventDefault = exports.oneOf = exports.not = exports.log = exports.handle = exports.forwardWithPrevent = exports.forwardCustomWithPrevent = exports.forwardCustom = exports.forward = exports.forProp = exports.forKeyCode = exports.forKey = exports.forEventProp = exports["default"] = exports.callOnEvent = exports.call = exports.adaptEvent = void 0; var _cond = _interopRequireDefault(require("ramda/src/cond")); var _curry = _interopRequireDefault(require("ramda/src/curry")); var _keymap = require("../keymap"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; } /** * `core/handle` provides a set of utilities to support handling events for `kind()`s and * `React.Component`s. The default export, `handle()`, generates an event handler function from a * set of input functions. The input functions either process or filter the event. If an input * function returns `true`, `handle()` will continue processing the event by calling the next input * function in the chain. If it returns `false` (or any falsy value like `null` or `undefined`), * the event handling chain stops at that input function. * * Example: * ``` * import {forKey, forward, handle, preventDefault} from '@enact/core/handle'; * * // logEnter will contain a function that accepts an event, a props object, and a context object * const logEnter = handle( * forward('onKeyDown'), // forwards the event to the function passed in the onKeyDown prop * forKey('enter'), // if the event.keyCode maps to the enter key, allows event processing to continue * preventDefault, // calls event.preventDefault() to prevent the `keypress` event * (ev, props) => { // custom event handler -- in this case, logging some text * // since it doesn't return `true`, no further input functions would be called after this one * console.log('The Enter key was pressed down'); * } * ).finally(() => { * console.log('This will log at the end no matter what happens within the handler above') * }); * ``` * * `handle()` can also be bound to a component instance which allows it to access the instance * `props` and `context`. This allows you to write consistent event handlers for components created * either with `kind()` or ES6 classes without worrying about from where the props are sourced. * * Handlers can either be bound directly using the native `bind()` method or using the `bindAs()` * utility method that is appended to the handler. * * Example: * ``` * import {forKey, forward, handle, preventDefault} from '@enact/core/handle'; * import {Component} from 'react'; * * class MyComponent extends Component { * // bind handle() to the instance * constructor () { * super(); * * // logEnter will be bound to `this` and set as this.handleKeyDown * // * // Equivalent to the following with the advantage of set the function name to be displayed in * // development tool call stacks * // * // this.handleKeyDown = logEnter.bind(this) * logEnter.bindAs(this, 'handleKeyDown'); * } * * render () { * return ( * <div onKeyDown={this.handleKeyDown} /> * ); * } * } * ``` * * @module core/handle * @exports adaptEvent * @exports call * @exports callOnEvent * @exports forward * @exports forwardWithPrevent * @exports forEventProp * @exports forKey * @exports forKeyCode * @exports forProp * @exports handle * @exports log * @exports oneOf * @exports preventDefault * @exports returnsTrue * @exports stop * @exports stopImmediate */ /** * The signature for event handlers * * @callback EventHandler * @memberof core/handle * @param {any} event */ /** * The signature for event handling functions supported by `handle` and related functions * * @callback HandlerFunction * @memberof core/handle * @param {any} event * @param {Object<string, any>} props * @param {Object<string, any>} context */ /** * The signature for {@link core/handle.adaptEvent} parameter `adapter` * * @callback EventAdapter * @memberof core/handle * @param {any} event * @param {Object<string, any>} props * @param {Object<string, any>} context * @returns {any} */ // Accepts an array of handlers, sanitizes them, and returns a handler function // compose(allPass, map(makeSafeHandler)); var makeHandler = function makeHandler(handlers) { // allowing shadowing here to provide a meaningful function name in dev tools // eslint-disable-next-line no-shadow return function handle() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } for (var i = 0; i < handlers.length; i++) { var fn = handlers[i]; if (typeof fn !== 'function' || fn.apply(this, args)) { continue; } return false; } return true; }; }; // Loose check to determine if obj is component-ish if it has both props and context members var hasPropsAndContext = function hasPropsAndContext(obj) { return obj && Object.prototype.hasOwnProperty.call(obj, 'props') && Object.prototype.hasOwnProperty.call(obj, 'context'); }; var named = function named(fn, name) { if (process.env.NODE_ENV !== "production") { try { Object.defineProperty(fn, 'name', { value: name, writeable: false, enumerable: false }); } catch (err) { // unable to set name of function } } return fn; }; var bindAs = function bindAs(fn, obj, name) { var namedFunction = name ? named(fn, name) : fn; var bound = namedFunction.bind(obj); if (name) { obj[name] = bound; } return bound; }; var decorateHandleFunction = function decorateHandleFunction(fn) { fn.named = function (name) { return named(fn, name); }; fn.bindAs = function (obj, name) { return bindAs(fn, obj, name); }; return fn; }; /** * Allows generating event handlers by chaining input functions to filter or short-circuit the * handling flow. Any input function that returns a falsy value will stop the chain. * * The returned handler function has a `finally()` member that accepts a function and returns a new * handler function. The accepted function is called once the original handler completes regardless * of the returned value. * * @method handle * @param {...HandlerFunction} handlers List of handlers to process the event. * * @returns {EventHandler} A function that accepts an event which is dispatched to each of the * provided handlers. * @memberof core/handle * @public */ var handle = exports.handle = function handle() { for (var _len2 = arguments.length, handlers = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { handlers[_key2] = arguments[_key2]; } var h = makeHandler(handlers); // In order to support binding either handle (handle.bind(this)) or a handler // (a = handle(), a.bind(this)), we cache `this` here and use it as the fallback for props and // context if fn() doesn't have its own `this`. var _outer = this; var fn = function prepareHandleArgs(ev, props, context) { var caller = null; // if handle() was bound to a class, use its props and context. otherwise, we accept // incoming props/context as would be provided by computed/handlers from kind() if (hasPropsAndContext(this)) { caller = this; props = this.props; context = this.context; } else if (hasPropsAndContext(_outer)) { caller = _outer; props = _outer.props; context = _outer.context; } return h.call(caller, ev, props, context); }; fn["finally"] = function (cleanup) { return decorateHandleFunction(function handleWithFinally(ev, props, context) { var result = false; if (hasPropsAndContext(this)) { props = this.props; context = this.context; } try { result = fn.call(this, ev, props, context); } finally { cleanup.call(this, ev, props, context); } return result; }); }; return decorateHandleFunction(fn); }; /** * Calls the first handler whose condition passes. Each branch must be passed as an array with the * first element being the condition function and the second being the handler function. The same * arguments are passed to both the condition function and the handler function. The value returned * from the handler is returned. * * Example: * ``` * const handler = oneOf( * [forKey('enter'), handleEnter], * [forKey('left'), handleLeft], * [forKey('right'), handleRight] * ); * ``` * * @method oneOf * @param {...[HandlerFunction, HandlerFunction]} handlers List of conditions and handlers to process the event * * @returns {HandlerFunction} A function that accepts an event which is dispatched to each of the * conditions and, if it passes, onto the provided handler. * @memberof core/handle * @public */ var oneOf = exports.oneOf = handle.oneOf = function () { for (var _len3 = arguments.length, handlers = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { handlers[_key3] = arguments[_key3]; } return handle.call(this, (0, _cond["default"])(handlers)); }; /** * A function that always returns `true`. Optionally accepts a `handler` function which is called * before returning `true`. * * Example: * ``` * // Used to coerce an existing function into a handler * const coercedHandler = handle( * returnsTrue(doesSomething), * willAlwaysBeCalled * ); * * // Used to emulate if/else blocks with `oneOf` * const ifElseHandler = oneOf( * [forKey('enter'), handleEnter], * [returnsTrue, handleOtherwise] * ); * ``` * * @method returnsTrue * @param {Function} [handler] Handler function called before returning `true`. * * @returns {HandlerFunction} A function that returns `true` * @memberof core/handle * @public */ var returnsTrue = exports.returnsTrue = handle.returnsTrue = function (handler) { if (handler && typeof handler === 'function') { return named(function () { for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } handler.apply(this, args); return true; }, 'returnsTrue'); } return true; }; /** * Calls a named function on the event and returns `true`. * * Example: * ``` * import {callOnEvent, handle} from '@enact/core/handle'; * * const callsCustomMethod = handle( * callOnEvent('customMethod'), * (ev) => console.log('ev.customMethod() was called', ev) * ); * ``` * * @method callOnEvent * @param {String} methodName Name of the method to call on the event * @param {Object} ev Event payload * * @returns {true} Always returns `true` * @curried * @memberof core/handle * @private */ var callOnEvent = exports.callOnEvent = handle.callOnEvent = (0, _curry["default"])(function (methodName, ev) { if (ev[methodName]) { ev[methodName](); } else if (ev.nativeEvent && ev.nativeEvent[methodName]) { // In some cases (notably stopImmediatePropagation), React doesn't include a desired method // on its proxy so we check the native event as well. ev.nativeEvent[methodName](); } return true; }); /** * Allows handling to continue if the value of `prop` on the event strictly equals `value` * * Example: * ``` * import {forEventProp, handle} from '@enact/core/handle'; * * const logWhenXEqualsZero = handle( * forEventProp('x', 0), * (ev) => console.log('ev.x was equal to zero', ev) * ); * ``` * * @method forEventProp * @param {String} prop Name of property on event * @param {*} value Value of property * @param {Object} ev Event payload * * @returns {Boolean} Returns `true` if `prop` on `event` strictly equals `value` * @curried * @memberof core/handle * @public */ var forEventProp = exports.forEventProp = handle.forEventProp = (0, _curry["default"])(function (prop, value, ev) { return ev[prop] === value; }); /** * Forwards the event to a function at `name` on `props`. If the specified prop is `undefined` or * is not a function, it is ignored. The return value of the forwarded function is ignored and * `true` is always returned instead. * * Example: * ``` * import {forward, handle} from '@enact/core/handle'; * * const forwardAndLog = handle( * forward('onClick'), * (ev) => console.log('event forwarded to onClick from props', ev) * ); * ``` * * @method forward * @param {String} name Name of method on the `props` * @param {Object} ev Event payload * @param {Object} props Props object * * @returns {true} Always returns `true` * @curried * @memberof core/handle * @public */ var forward = exports.forward = handle.forward = (0, _curry["default"])(named(function (name, ev, props) { var fn = props && props[name]; if (typeof fn === 'function') { fn(ev); } return true; }, 'forward')); /** * Calls `event.preventDefault()` and returns `true`. * * Example: * ``` * import {handle, preventDefault} from '@enact/core/handle'; * * const preventAndLog = handle( * preventDefault, * (ev) => console.log('preventDefault called', ev) * ); * ``` * * @method preventDefault * @param {Object} ev Event payload * * @returns {true} Always returns `true` * @memberof core/handle * @public */ var _preventDefault = exports.preventDefault = handle.preventDefault = callOnEvent('preventDefault'); /** * Forwards the event to a function at `name` on `props` with capability to prevent default * behavior. If the specified prop is `undefined` or is not a function, it is ignored. Returns * `false` when `event.preventDefault()` has been called in a handler. * * Example: * ``` * import {forwardWithPrevent, handle} from '@enact/core/handle'; * * const forwardPreventDefault = handle( * forwardWithPrevent('onClick'), * (ev) => console.log('default action', ev) * ); * ``` * * @method forwardWithPrevent * @param {String} name Name of method on the `props` * @param {Object} ev Event payload * @param {Object} props Props object * * @returns {Boolean} Returns `false` if default action is prevented * @curried * @memberof core/handle * @private */ var forwardWithPrevent = exports.forwardWithPrevent = handle.forwardWithPrevent = (0, _curry["default"])(named(function (name, ev, props) { var prevented = false; var wrappedEvent = Object.assign({}, ev, { preventDefault: function preventDefault() { prevented = true; _preventDefault(ev); } }); forward(name, wrappedEvent, props); return !prevented; }, 'forwardWithPrevent')); /** * Calls `event.stopPropagation()` and returns `true` * * Example: * ``` * import {handle, stop} from '@enact/core/handle'; * * const stopAndLog = handle( * stop, * (ev) => console.log('stopPropagation called', ev) * ); * ``` * * @method stop * @param {Object} ev Event payload * * @returns {true} Always returns `true` * @curried * @memberof core/handle * @public */ var stop = exports.stop = handle.stop = named(callOnEvent('stopPropagation'), 'stop'); /** * Calls `event.stopImmediatePropagation()` and returns `true` * * Example: * ``` * import {handle, stopImmediate} from '@enact/core/handle'; * * const stopImmediateAndLog = handle( * stopImmediate, * (ev) => console.log('stopImmediatePropagation called', ev) * ); * ``` * * @method stopImmediate * @param {Object} ev Event payload * * @returns {true} Always returns `true` * @curried * @memberof core/handle * @public */ var stopImmediate = exports.stopImmediate = handle.stopImmediate = callOnEvent('stopImmediatePropagation'); /** * Allows event handling to continue if `event.keyCode === value`. * * Example: * ``` * import {forKeyCode, handle} from '@enact/core/handle'; * * const logForEscapeKey = handle( * forKeyCode(27), * (ev) => console.log('Escape key pressed down', ev) * ); * ``` * * @method forKeyCode * @param {Number} value `keyCode` to test * @param {Object} ev Event payload * * @returns {Boolean} Returns `true` if `event.keyCode` strictly equals `value` * @curried * @memberof core/handle * @public */ var forKeyCode = exports.forKeyCode = handle.forKeyCode = forEventProp('keyCode'); /** * Allows handling to continue if the event's keyCode is mapped to `name` within * {@link core/keymap}. * * Example: * ``` * import {forKey, handle} from '@enact/core/handle'; * * const logForEnterKey = handle( * forKey('enter'), * (ev) => console.log('Enter key pressed down', ev) * ); * ``` * * @see {@link core/keymap} * @method forKey * @param {String} name Name from {@link core/keymap} * @param {Object} ev Event payload * * @returns {Boolean} Returns `true` if `event.keyCode` is mapped to `name` * @curried * @memberof core/handle * @public */ var forKey = exports.forKey = handle.forKey = (0, _curry["default"])(function (name, ev) { return (0, _keymap.is)(name, ev.keyCode); }); /** * Allows handling to continue if the value of `prop` on the props strictly equals `value`. * * Example: * ``` * import {forProp, handle} from '@enact/core/handle'; * * const logWhenChecked = handle( * forProp('checked', true), * (ev) => console.log('checked prop is true', ev) * ); * ``` * * @method forProp * @param {String} prop Name of property on props object * @param {*} value Value of property * @param {Object} ev Event payload * @param {Object} props Props object * * @returns {Boolean} Returns `true` if the value of `props[prop]` strictly equals `value` * @curried * @memberof core/handle * @public */ var forProp = exports.forProp = handle.forProp = (0, _curry["default"])(function (prop, value, ev, props) { return props[prop] === value; }); /** * Logs the event, props, and context optionally preceded by a custom message. Will only log in * development mode. * * Example: * ``` * import {forProp, handle, log} from '@enact/core/handle'; * * const logWhenChecked = handle( * forProp('checked', true), * log('checked props is true') * ); * ``` * * @method log * @param {String} message Custom message * @param {Object} ev Event payload * @param {...*} [args] Any args passed are logged * * @returns {true} Always returns `true` * @curried * @memberof core/handle * @public */ var log = exports.log = handle.log = (0, _curry["default"])(function (message, ev) { if (process.env.NODE_ENV !== "production") { var _console; for (var _len5 = arguments.length, args = new Array(_len5 > 2 ? _len5 - 2 : 0), _key5 = 2; _key5 < _len5; _key5++) { args[_key5 - 2] = arguments[_key5]; } // eslint-disable-next-line no-console (_console = console).log.apply(_console, [message, ev].concat(args)); } return true; }); /** * Invokes a method by name on the component to which {@link core/handle.handle} is bound. * * If the methods exist on the object, it is called with the event, props, and context and its * return value is returned. * * If the method does not exist or handle isn't bound to an instance, it returns `false`. * * Example: * ``` * import {call, handle, forProp} from '@enact/core/handle'; * * const incrementIfEnabled = handle( * forProp('disabled', false), * call('increment') * ); * * class Counter extends React.Component { * constructor () { * super(); * * this.handleIncrement = incrementIfEnabled.bind(this); * } * * render () { * // ... * } * } * ``` * * @method call * @param {String} method Name of method * * @returns {HandlerFunction} Returns the value returned by `method`, or `false` if the method * does not exist * @memberof core/handle * @public */ var call = exports.call = function call(method) { return named(function () { if (this && this[method]) { return this[method].apply(this, arguments); } return false; }, 'call'); }; /** * Adapts an event with `adapter` before calling `handler`. * * The `adapter` function receives the same arguments as any handler. The value returned from * `adapter` is passed as the first argument to `handler` with the remaining arguments kept the * same. This is often useful to generate a custom event payload before forwarding on to a callback. * * Example: * ``` * import {adaptEvent, forward} from '@enact/core/handle'; * * // calls the onChange callback with an event payload containing a type and value member * const incrementAndChange = adaptEvent( * (ev, props) => ({ * type: 'onChange', * value: props.value + 1 * }), * forward('onChange') * ) * ``` * * @method adaptEvent * @param {EventAdapter} adapter Function to adapt the event payload * @param {HandlerFunction} handler Handler to call with the handler function * * @returns {HandlerFunction} Returns an {@link core/handle.HandlerFunction|event handler} (suitable for passing to handle) that returns the result of `handler` * @curried * @memberof core/handle * @public */ var adaptEvent = exports.adaptEvent = handle.adaptEvent = (0, _curry["default"])(function (adapter, handler) { return named(function (ev) { for (var _len6 = arguments.length, args = new Array(_len6 > 1 ? _len6 - 1 : 0), _key6 = 1; _key6 < _len6; _key6++) { args[_key6 - 1] = arguments[_key6]; } return handler.call.apply(handler, [this, adapter.call.apply(adapter, [this, ev].concat(args))].concat(args)); }, 'adaptEvent'); }); /** * Creates a handler that will forward the event to a function at `name` on `props`. * * If `adapter` is not specified, a new event payload will be generated with a `type` member with * the `name` of the custom event. If `adapter` is specified, the `type` member is added to the * value returned by `adapter`. * * The `adapter` function receives the same arguments as any handler. The value returned from * `adapter` is passed as the first argument to `handler` with the remaining arguments kept the * same. This is often useful to generate a custom event payload before forwarding on to a callback. * * Example: * ``` * import {forwardCustom} from '@enact/core/handle'; * * // calls the onChange callback with the event: {type: 'onChange'} * const forwardChange = forwardCustom('onChange'); * * // calls the onChange callback with the event: {type: 'onChange', index} * const forwardChangeWithIndex = forwardCustom('onChange', (ev, {index}) => ({index})); * ``` * * @method forwardCustom * @param {String} name Name of method on the `props` * @param {EventAdapter} [adapter] Function to adapt the event payload * * @returns {HandlerFunction} Returns an {@link core/handle.EventHandler|event handler} * (suitable for passing to handle or used directly within * `handlers` in {@link core/kind|kind}) that will forward the * custom event. * @memberof core/handle * @public */ var forwardCustom = exports.forwardCustom = handle.forwardCustom = function (name, adapter) { return named(adaptEvent(function (ev) { for (var _len7 = arguments.length, args = new Array(_len7 > 1 ? _len7 - 1 : 0), _key7 = 1; _key7 < _len7; _key7++) { args[_key7 - 1] = arguments[_key7]; } var customEventPayload = adapter ? adapter.call.apply(adapter, [this, ev].concat(args)) : null; // Handle either no adapter or a non-object return from the adapter if (!customEventPayload || typeof customEventPayload !== 'object') { customEventPayload = {}; } customEventPayload.type = name; if (typeof customEventPayload.preventDefault !== 'function' && typeof (ev === null || ev === void 0 ? void 0 : ev.preventDefault) === 'function') { customEventPayload.preventDefault = ev.preventDefault.bind(ev); } if (typeof customEventPayload.stopPropagation !== 'function' && typeof (ev === null || ev === void 0 ? void 0 : ev.stopPropagation) === 'function') { customEventPayload.stopPropagation = ev.stopPropagation.bind(ev); } return customEventPayload; }, forward(name)), 'forwardCustom'); }; /** * Creates a handler that will forward the event to a function at `name` on `props` with capability * to prevent default behavior. If the specified prop is `undefined` or is not a function, it is * ignored. The created handler returns `false` when `event.preventDefault()` has been called in a handler. * * If `adapter` is not specified, a new event payload will be generated with a `type` member with * the `name` of the custom event. If `adapter` is specified, the `type` member is added to the * value returned by `adapter`. * * The `adapter` function receives the same arguments as any handler. The value returned from * `adapter` is passed as the first argument to `handler` with the remaining arguments kept the * same. This is often useful to generate a custom event payload before forwarding on to a callback. * * Example: * ``` * import {forwardCustomWithPrevent, handle} from '@enact/core/handle'; * * // calls the onChange callback with the event: {type: 'onChange'} * const forwardChangePreventDefault = handle( * forwardCustomWithPrevent('onChange'), * (ev) => console.log('default action', ev) * ); * * // calls the onChange callback with the event: {type: 'onChange', index} * const forwardChangeWithIndexPreventDefault = handle( * forwardCustomWithPrevent('onChange', (ev, {index}) => ({index})), * (ev) => console.log('default action', ev) * ); * ``` * * @method forwardCustomWithPrevent * @param {String} name Name of method on the `props` * @param {EventAdapter} [adapter] Function to adapt the event payload * * @returns {HandlerFunction} Returns an {@link core/handle.EventHandler|event handler} * (suitable for passing to handle or used directly within * `handlers` in {@link core/kind|kind}) that will forward the * custom event and will return `false` if default action is prevented * @memberof core/handle * @private */ var forwardCustomWithPrevent = exports.forwardCustomWithPrevent = handle.forwardCustomWithPrevent = function (name, adapter) { return named(function (ev) { for (var _len8 = arguments.length, args = new Array(_len8 > 1 ? _len8 - 1 : 0), _key8 = 1; _key8 < _len8; _key8++) { args[_key8 - 1] = arguments[_key8]; } var prevented = false; function adapterWithPrevent() { var customEventPayload = adapter ? adapter.call.apply(adapter, [this, ev].concat(args)) : null; var existingPreventDefault = null; // Handle either no adapter or a non-object return from the adapter if (!customEventPayload || typeof customEventPayload !== 'object') { customEventPayload = {}; } if (typeof customEventPayload.preventDefault === 'function') { existingPreventDefault = customEventPayload.preventDefault; } else if (typeof (ev === null || ev === void 0 ? void 0 : ev.preventDefault) === 'function') { existingPreventDefault = ev.preventDefault.bind(ev); } customEventPayload.preventDefault = function () { prevented = true; if (typeof existingPreventDefault === 'function') { existingPreventDefault(ev); } }; return customEventPayload; } return forwardCustom.call(this, name, adapterWithPrevent.bind(this)).apply(void 0, [ev].concat(args)) && !prevented; }, 'forwardCustomWithPrevent'); }; /** * Accepts a handler and returns the logical complement of the value returned from the handler. * * Example: * ``` * import {forProp, forward, not, handle} from '@enact/core/handle'; * * // calls the onChange callback when disabled is not true * const handleChange = handle( * not(forProp('disabled', true)), * forward('onChange') * ) * ``` * * @method not * @param {HandlerFunction} handler Handler to complement * * @returns {HandlerFunction} Returns an {@link core/handle.HandlerFunction|event handler} * (suitable for passing to handle) that returns the complement of the * return value of `handler` * @curried * @memberof core/handle * @public */ var not = exports.not = handle.not = function (handler) { return function () { return !handler.apply(void 0, arguments); }; }; var _default = exports["default"] = handle;