diffhtml-middleware-inline-transitions
Version:
Monitors inline attributes and assigns transition hooks
132 lines (107 loc) • 4.01 kB
JavaScript
const { assign, keys } = Object;
const eventsToTransitionName = {
attributechanged: 'attributeChanged',
textchanged: 'textChanged',
};
// Store maps of elements to handlers that are associated to transitions.
const transitionsMap = {
attached: new Map(),
detached: new Map(),
replaced: new Map(),
attributechanged: new Map(),
textchanged: new Map(),
};
// Internal global transition state handlers, allows us to bind once and match.
const boundHandlers = {};
/**
* Binds inline transitions to the parent element and triggers for any matching
* nested children.
*/
export default function inlineTransitions() {
// Monitors whenever an element changes an attribute, if the attribute is a
// valid state name, add this element into the related Set.
const attributeChanged = function(domNode, name, _oldVal, newVal) {
const prefix = name.toLowerCase().slice(0, 2);
// Don't bother with non-events.
if (prefix !== 'on') {
return;
}
// Normalize the event name to lowercase and without `on`.
name = name.toLowerCase().slice(2);
const map = transitionsMap[name];
const isFunction = typeof newVal === 'function';
// Abort early if not a valid transition or if the new value exists, but
// isn't a function.
if (!map || (newVal && !isFunction)) {
return;
}
// Add or remove based on the value existence and type.
map[isFunction ? 'set' : 'delete'](domNode, newVal);
};
let Internals = null;
const subscribe = (_Internals) => {
Internals = _Internals;
Internals.TransitionCache.get('attributeChanged').add(attributeChanged);
// Add a transition for every type.
keys(transitionsMap).forEach(name => {
const map = transitionsMap[name];
const transitionName = eventsToTransitionName[name] || name;
// This handler is bound for every possible transition to help limit the
// amount of transitions bound.
const handler = (childNode, ...rest) => {
// Abort early if no childNode was present.
if (!childNode) {
return;
}
// If the child element triggered in the transition is the root
// element, this is an easy lookup for the handler.
if (map.has(childNode)) {
return map.get(childNode)(childNode, childNode, ...rest);
}
// Text nodes are looked up on the parent.
else if (transitionName === 'textChanged') {
const { parentNode } = childNode;
if (map.has(parentNode)) {
return map.get(parentNode)(parentNode, childNode, ...rest);
}
}
// Search if a parent is bound to this.
else {
let promises = [];
map.forEach((_, parentNode) => {
if (parentNode.contains(childNode)) {
const ret = map.get(parentNode)(parentNode, childNode, ...rest);
ret && promises.push(ret);
}
});
if (promises.length) {
return Promise.all(promises);
}
}
};
// Save the handler for later unbinding.
boundHandlers[transitionName] = handler;
// Add the state handler.
Internals.TransitionCache.get(transitionName).add(handler);
});
};
// This will unbind any internally bound transition states.
const unsubscribe = Internals => {
// Unbind all the transition states.
Internals.TransitionCache.get('attributeChanged').delete(attributeChanged);
// Remove all elements from the internal cache.
keys(transitionsMap).forEach(name => {
const transitionName = eventsToTransitionName[name] || name;
// Unbind the associated global handler.
const handler = boundHandlers[transitionName];
Internals.TransitionCache.get(transitionName).delete(handler);
});
// Empty the bound handlers.
boundHandlers.length = 0;
};
return assign(() => {}, {
displayName: 'inlineTransitionsTask',
subscribe,
unsubscribe,
});
}