@uppy/informer
Version:
A notification and error pop-up bar for Uppy.
330 lines (276 loc) • 8.53 kB
text/typescript
// INFO: not typing copy pasted libarary code
// @ts-nocheck
/**
* @source https://github.com/developit/preact-transition-group
*/
import { Component, cloneElement, h, toChildArray } from 'preact'
function assign(obj, props) {
return Object.assign(obj, props)
}
function getKey(vnode, fallback) {
return vnode?.key ?? fallback
}
function linkRef(component, name) {
// biome-ignore lint/suspicious/noAssignInExpressions: ...
const cache = component._ptgLinkedRefs || (component._ptgLinkedRefs = {})
return (
cache[name] ||
// biome-ignore lint/suspicious/noAssignInExpressions: ...
(cache[name] = (c) => {
component.refs[name] = c
})
)
}
function getChildMapping(children) {
const out = {}
for (let i = 0; i < children.length; i++) {
if (children[i] != null) {
const key = getKey(children[i], i.toString(36))
out[key] = children[i]
}
}
return out
}
function mergeChildMappings(prev, next) {
prev = prev || {}
next = next || {}
const getValueForKey = (key) =>
Object.hasOwn(next, key) ? next[key] : prev[key]
// For each key of `next`, the list of keys to insert before that key in
// the combined list
const nextKeysPending = {}
let pendingKeys = []
for (const prevKey in prev) {
if (Object.hasOwn(next, prevKey)) {
if (pendingKeys.length) {
nextKeysPending[prevKey] = pendingKeys
pendingKeys = []
}
} else {
pendingKeys.push(prevKey)
}
}
const childMapping = {}
for (const nextKey in next) {
if (Object.hasOwn(nextKeysPending, nextKey)) {
for (let i = 0; i < nextKeysPending[nextKey].length; i++) {
const pendingNextKey = nextKeysPending[nextKey][i]
childMapping[nextKeysPending[nextKey][i]] =
getValueForKey(pendingNextKey)
}
}
childMapping[nextKey] = getValueForKey(nextKey)
}
// Finally, add the keys which didn't appear before any key in `next`
for (let i = 0; i < pendingKeys.length; i++) {
childMapping[pendingKeys[i]] = getValueForKey(pendingKeys[i])
}
return childMapping
}
const identity = (i) => i
class TransitionGroup extends Component {
constructor(props, context) {
super(props, context)
this.refs = {}
this.state = {
children: getChildMapping(
toChildArray(toChildArray(this.props.children)) || [],
),
}
this.performAppear = this.performAppear.bind(this)
this.performEnter = this.performEnter.bind(this)
this.performLeave = this.performLeave.bind(this)
}
componentWillMount() {
this.currentlyTransitioningKeys = {}
this.keysToAbortLeave = []
this.keysToEnter = []
this.keysToLeave = []
}
componentDidMount() {
const initialChildMapping = this.state.children
for (const key in initialChildMapping) {
if (initialChildMapping[key]) {
// this.performAppear(getKey(initialChildMapping[key], key));
this.performAppear(key)
}
}
}
componentWillReceiveProps(nextProps) {
const nextChildMapping = getChildMapping(
toChildArray(nextProps.children) || [],
)
const prevChildMapping = this.state.children
this.setState((prevState) => ({
children: mergeChildMappings(prevState.children, nextChildMapping),
}))
let key: string
for (key in nextChildMapping) {
if (Object.hasOwn(nextChildMapping, key)) {
const hasPrev = prevChildMapping && Object.hasOwn(prevChildMapping, key)
// We should re-enter the component and abort its leave function
if (
nextChildMapping[key] &&
hasPrev &&
this.currentlyTransitioningKeys[key]
) {
this.keysToEnter.push(key)
this.keysToAbortLeave.push(key)
} else if (
nextChildMapping[key] &&
!hasPrev &&
!this.currentlyTransitioningKeys[key]
) {
this.keysToEnter.push(key)
}
}
}
for (key in prevChildMapping) {
if (Object.hasOwn(prevChildMapping, key)) {
const hasNext = nextChildMapping && Object.hasOwn(nextChildMapping, key)
if (
prevChildMapping[key] &&
!hasNext &&
!this.currentlyTransitioningKeys[key]
) {
this.keysToLeave.push(key)
}
}
}
}
componentDidUpdate() {
const { keysToEnter } = this
this.keysToEnter = []
keysToEnter.forEach(this.performEnter)
const { keysToLeave } = this
this.keysToLeave = []
keysToLeave.forEach(this.performLeave)
}
_finishAbort(key) {
const idx = this.keysToAbortLeave.indexOf(key)
if (idx !== -1) {
this.keysToAbortLeave.splice(idx, 1)
}
}
performAppear(key) {
this.currentlyTransitioningKeys[key] = true
const component = this.refs[key]
if (component?.componentWillAppear) {
component.componentWillAppear(this._handleDoneAppearing.bind(this, key))
} else {
this._handleDoneAppearing(key)
}
}
_handleDoneAppearing(key) {
const component = this.refs[key]
if (component?.componentDidAppear) {
component.componentDidAppear()
}
delete this.currentlyTransitioningKeys[key]
this._finishAbort(key)
const currentChildMapping = getChildMapping(
toChildArray(this.props.children) || [],
)
if (!currentChildMapping || !Object.hasOwn(currentChildMapping, key)) {
// This was removed before it had fully appeared. Remove it.
this.performLeave(key)
}
}
performEnter(key) {
this.currentlyTransitioningKeys[key] = true
const component = this.refs[key]
if (component?.componentWillEnter) {
component.componentWillEnter(this._handleDoneEntering.bind(this, key))
} else {
this._handleDoneEntering(key)
}
}
_handleDoneEntering(key) {
const component = this.refs[key]
if (component?.componentDidEnter) {
component.componentDidEnter()
}
delete this.currentlyTransitioningKeys[key]
this._finishAbort(key)
const currentChildMapping = getChildMapping(
toChildArray(this.props.children) || [],
)
if (!currentChildMapping || !Object.hasOwn(currentChildMapping, key)) {
// This was removed before it had fully entered. Remove it.
this.performLeave(key)
}
}
performLeave(key) {
// If we should immediately abort this leave function,
// don't run the leave transition at all.
const idx = this.keysToAbortLeave.indexOf(key)
if (idx !== -1) {
return
}
this.currentlyTransitioningKeys[key] = true
const component = this.refs[key]
if (component?.componentWillLeave) {
component.componentWillLeave(this._handleDoneLeaving.bind(this, key))
} else {
// Note that this is somewhat dangerous b/c it calls setState()
// again, effectively mutating the component before all the work
// is done.
this._handleDoneLeaving(key)
}
}
_handleDoneLeaving(key) {
// If we should immediately abort the leave,
// then skip this altogether
const idx = this.keysToAbortLeave.indexOf(key)
if (idx !== -1) {
return
}
const component = this.refs[key]
if (component?.componentDidLeave) {
component.componentDidLeave()
}
delete this.currentlyTransitioningKeys[key]
const currentChildMapping = getChildMapping(
toChildArray(this.props.children) || [],
)
if (currentChildMapping && Object.hasOwn(currentChildMapping, key)) {
// This entered again before it fully left. Add it again.
this.performEnter(key)
} else {
const children = assign({}, this.state.children)
delete children[key]
this.setState({ children })
}
}
render(
{
childFactory,
transitionLeave,
transitionName,
transitionAppear,
transitionEnter,
transitionLeaveTimeout,
transitionEnterTimeout,
transitionAppearTimeout,
component,
...props
},
{ children },
) {
// TODO: we could get rid of the need for the wrapper node
// by cloning a single child
const childrenToRender = Object.entries(children)
.map(([key, child]) => {
if (!child) return undefined
const ref = linkRef(this, key)
return cloneElement(childFactory(child), { ref, key })
})
.filter(Boolean)
return h(component, props, childrenToRender)
}
}
TransitionGroup.defaultProps = {
component: 'span',
childFactory: identity,
}
export default TransitionGroup