import { omit, pick, pickBy, forEach, compact } from './fp.js'
import { isDomElement, isComponentNode } from './dynamictypes.js'
import { isPlaceholder, RESOLVE } from './placeholder.js'
import { pragma } from './reactpragma.js'
import resolveShorthandComponent from './resolveShorthandComponent.js'
import getPathLens from './getPathLens.js'
import CONFIG from './CONFIG.js'
// Support dot-separated deep scopes - not sure how much of a real world usecase
// We choose a careful strategy here, ie. if there's no dot, we stay with the
// string version
export function resolvePathScope (scope) {
return typeof scope !== 'string'
? scope
: {
state: getPathLens(scope),
'*': scope
}
}
import wrapVdom from './wrapVdom.js'
// Copied from https://reactjs.org/docs/events.html
const EVENT_PROPS = (
'Copy|Cut|Paste|CompositionEnd|CompositionStart|CompositionUpdate|KeyDown|' +
'KeyPress|KeyUp|Focus|Blur|Change|Input|Invalid|Submit|Click|ContextMenu|' +
'DoubleClick|Drag|DragEnd|DragEnter|DragExit|DragLeave|DragOver|DragStart|' +
'Drop|MouseDown|MouseEnter|MouseLeave|MouseMove|MouseOut|MouseOver|MouseUp|' +
'PointerDown|PointerMove|PointerUp|PointerCancel|GotPointerCapture|' +
'LostPointerCapture|PointerEnter|PointerLeave|PointerOver|PointerOut|' +
'TouchCancel|TouchEnd|TouchMove|TouchStart|Scroll|Wheel|Abort|CanPlay|' +
'CanPlayThrough|DurationChange|Emptied|Encrypted|Ended|Error|LoadedData|' +
'LoadedMetadata|LoadStart|Pause|Play|Playing|Progress|RateChange|Seeked|' +
'Seeking|Stalled|Suspend|TimeUpdate|VolumeChange|Waiting|Load|Error|' +
'AnimationStart|AnimationEnd|AnimationIteration|TransitionEnd|Toggle'
).split('|').map(ev => 'on' + ev)
// Makes these shortcuts available for the following:
// {src => [, { state: src.el.click.mapTo(prev => prev + 1) }]}
//
// 1. A special sink definition where we define sink keys and event$-to-sink$
// mappers:
//
// 2. A callback which maps from event to state:
//
function resolveDomEventProps (vdom) {
const eventProps = pick(EVENT_PROPS)(vdom.props)
if (Object.keys(eventProps).length === 0) {
return false
}
// We have to generate a unique sel, because we can't scope down the
// generated inline component. See the comment at the bottom.
const sel = vdom.props.sel || Symbol('eventprop-autosel')
const getInlineCmp = (type, props, children) => sources => {
const sinks = {}
forEach(eventProps, (_handlers, propKey) => {
const handlers = typeof _handlers === 'function'
? { state: stream => stream.map(_handlers) }
: _handlers
const eventNameDom = propKey.replace(/^on/, '').toLowerCase()
forEach(handlers, (handler, channel) => {
const stream = isPlaceholder(handler)
? handler[RESOLVE](sources.sel[sel][eventNameDom])
: handler(sources.sel[sel][eventNameDom])
sinks[channel] = !sinks[channel]
? stream
: CONFIG.mergeFn([sinks[channel], stream])
})
})
return [
pragma(type, { ...props, sel }, children),
sinks
]
}
wrapVdom(
vdom,
getInlineCmp,
Object.keys(eventProps),
// We must prevent scoping here! Otherwise in this case the HTTP sink will
// not get the click stream:
// {src => [
// ,
// { HTTP: src.el.click. ... }
// ]}
{ noscope: true }
)
return true
}
// This adds a filter on the component's state sink which listens
// on the filtered event coming from inside the component in the
// form of a ['eventName', payload] tuple. The event prop's value
// maps the payload to another tuple which also ends up in its sink.
function resolveComponentEventProps (vdom, powercycle) {
const eventProps =
pickBy((...[, prop]) => /^on(?:$|-|[A-Z])/.test(prop))(vdom.props)
const getTriplets = cfg =>
Object.keys(cfg).reduce(
(...[, channel]) => Object.keys(cfg[channel]).reduce(
(cum, event) => [...cum, [channel, event, cfg[channel][event]]], []
), []
)
// channel, filter key, translation (payload-to-action fn)
const triplets = Object.keys(eventProps).reduce(
(cum, next) => cum.concat(getTriplets(
next === 'on'
? vdom.props['on']
: /^on-/.test(next)
? { [next.replace(/^on-/, '')]: vdom.props[next] }
: { state: { [next.replace(/^on/, '').toLowerCase()]: vdom.props[next] }}
)),
[]
)
if (triplets.length > 0) {
const cmp = vdom.type
vdom.props = omit(Object.keys(eventProps))(vdom.props)
vdom.type = sources => {
const sinks = resolveShorthandComponent(powercycle)(cmp)(sources)
sinks.state = CONFIG.mergeFn(compact(
triplets.map(([channel, event, payloadToAction]) =>
sinks[channel] && sinks[channel]
.filter(([cmpEvent]) => cmpEvent === event)
.map(([, payload]) => payloadToAction(payload))
)
))
return sinks
}
}
}
export default function resolveEventProps (vdom, powercycle) {
if (isDomElement(vdom)) {
return resolveDomEventProps(vdom)
} else if (isComponentNode(vdom)) {
return resolveComponentEventProps(vdom, powercycle)
}
}