|
|
|
import type { |
|
State, |
|
OptionsGeneric, |
|
Modifier, |
|
Instance, |
|
VirtualElement, |
|
} from './types'; |
|
import getCompositeRect from './dom-utils/getCompositeRect'; |
|
import getLayoutRect from './dom-utils/getLayoutRect'; |
|
import listScrollParents from './dom-utils/listScrollParents'; |
|
import getOffsetParent from './dom-utils/getOffsetParent'; |
|
import orderModifiers from './utils/orderModifiers'; |
|
import debounce from './utils/debounce'; |
|
import mergeByName from './utils/mergeByName'; |
|
import detectOverflow from './utils/detectOverflow'; |
|
import { isElement } from './dom-utils/instanceOf'; |
|
|
|
const DEFAULT_OPTIONS: OptionsGeneric<any> = { |
|
placement: 'bottom', |
|
modifiers: [], |
|
strategy: 'absolute', |
|
}; |
|
|
|
type PopperGeneratorArgs = { |
|
defaultModifiers?: Array<Modifier<any, any>>, |
|
defaultOptions?: $Shape<OptionsGeneric<any>>, |
|
}; |
|
|
|
function areValidElements(...args: Array<any>): boolean { |
|
return !args.some( |
|
(element) => |
|
!(element && typeof element.getBoundingClientRect === 'function') |
|
); |
|
} |
|
|
|
export function popperGenerator(generatorOptions: PopperGeneratorArgs = {}) { |
|
const { defaultModifiers = [], defaultOptions = DEFAULT_OPTIONS } = |
|
generatorOptions; |
|
|
|
return function createPopper<TModifier: $Shape<Modifier<any, any>>>( |
|
reference: Element | VirtualElement, |
|
popper: HTMLElement, |
|
options: $Shape<OptionsGeneric<TModifier>> = defaultOptions |
|
): Instance { |
|
let state: $Shape<State> = { |
|
placement: 'bottom', |
|
orderedModifiers: [], |
|
options: { ...DEFAULT_OPTIONS, ...defaultOptions }, |
|
modifiersData: {}, |
|
elements: { |
|
reference, |
|
popper, |
|
}, |
|
attributes: {}, |
|
styles: {}, |
|
}; |
|
|
|
let effectCleanupFns: Array<() => void> = []; |
|
let isDestroyed = false; |
|
|
|
const instance = { |
|
state, |
|
setOptions(setOptionsAction) { |
|
const options = |
|
typeof setOptionsAction === 'function' |
|
? setOptionsAction(state.options) |
|
: setOptionsAction; |
|
|
|
cleanupModifierEffects(); |
|
|
|
state.options = { |
|
|
|
...defaultOptions, |
|
...state.options, |
|
...options, |
|
}; |
|
|
|
state.scrollParents = { |
|
reference: isElement(reference) |
|
? listScrollParents(reference) |
|
: reference.contextElement |
|
? listScrollParents(reference.contextElement) |
|
: [], |
|
popper: listScrollParents(popper), |
|
}; |
|
|
|
|
|
|
|
const orderedModifiers = orderModifiers( |
|
mergeByName([...defaultModifiers, ...state.options.modifiers]) |
|
); |
|
|
|
|
|
state.orderedModifiers = orderedModifiers.filter((m) => m.enabled); |
|
|
|
runModifierEffects(); |
|
|
|
return instance.update(); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
forceUpdate() { |
|
if (isDestroyed) { |
|
return; |
|
} |
|
|
|
const { reference, popper } = state.elements; |
|
|
|
|
|
|
|
if (!areValidElements(reference, popper)) { |
|
return; |
|
} |
|
|
|
|
|
state.rects = { |
|
reference: getCompositeRect( |
|
reference, |
|
getOffsetParent(popper), |
|
state.options.strategy === 'fixed' |
|
), |
|
popper: getLayoutRect(popper), |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
state.reset = false; |
|
|
|
state.placement = state.options.placement; |
|
|
|
|
|
|
|
|
|
|
|
state.orderedModifiers.forEach( |
|
(modifier) => |
|
(state.modifiersData[modifier.name] = { |
|
...modifier.data, |
|
}) |
|
); |
|
|
|
for (let index = 0; index < state.orderedModifiers.length; index++) { |
|
if (state.reset === true) { |
|
state.reset = false; |
|
index = -1; |
|
continue; |
|
} |
|
|
|
const { fn, options = {}, name } = state.orderedModifiers[index]; |
|
|
|
if (typeof fn === 'function') { |
|
state = fn({ state, options, name, instance }) || state; |
|
} |
|
} |
|
}, |
|
|
|
|
|
|
|
update: debounce<$Shape<State>>( |
|
() => |
|
new Promise<$Shape<State>>((resolve) => { |
|
instance.forceUpdate(); |
|
resolve(state); |
|
}) |
|
), |
|
|
|
destroy() { |
|
cleanupModifierEffects(); |
|
isDestroyed = true; |
|
}, |
|
}; |
|
|
|
if (!areValidElements(reference, popper)) { |
|
return instance; |
|
} |
|
|
|
instance.setOptions(options).then((state) => { |
|
if (!isDestroyed && options.onFirstUpdate) { |
|
options.onFirstUpdate(state); |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
function runModifierEffects() { |
|
state.orderedModifiers.forEach(({ name, options = {}, effect }) => { |
|
if (typeof effect === 'function') { |
|
const cleanupFn = effect({ state, name, instance, options }); |
|
const noopFn = () => {}; |
|
effectCleanupFns.push(cleanupFn || noopFn); |
|
} |
|
}); |
|
} |
|
|
|
function cleanupModifierEffects() { |
|
effectCleanupFns.forEach((fn) => fn()); |
|
effectCleanupFns = []; |
|
} |
|
|
|
return instance; |
|
}; |
|
} |
|
|
|
export const createPopper = popperGenerator(); |
|
|
|
|
|
export { detectOverflow }; |
|
|