import type { Interactable } from '@interactjs/core/Interactable'
import type { ActionProps, DoAnyPhaseArg, Interaction } from '@interactjs/core/Interaction'
import type { ActionName, Scope, Plugin } from '@interactjs/core/scope'
import type { Element } from '@interactjs/types/index'
import * as arr from '@interactjs/utils/arr'
import is from '@interactjs/utils/is'
import { copyAction } from '@interactjs/utils/misc'
import * as pointerUtils from '@interactjs/utils/pointerUtils'
import { tlbrToXywh } from '@interactjs/utils/rect'
declare module '@interactjs/core/scope' {
interface SignalArgs {
'interactions:before-action-reflow': Omit<DoAnyPhaseArg, 'iEvent'>
'interactions:action-reflow': DoAnyPhaseArg
'interactions:after-action-reflow': DoAnyPhaseArg
}
}
declare module '@interactjs/core/Interactable' {
interface Interactable {
reflow: <T extends ActionName>(action: ActionProps<T>) => ReturnType<typeof doReflow>
}
}
declare module '@interactjs/core/Interaction' {
interface Interaction {
_reflowPromise: Promise<void>
_reflowResolve: (...args: unknown[]) => void
}
}
declare module '@interactjs/core/InteractEvent' {
interface PhaseMap {
reflow?: true
}
}
export function install (scope: Scope) {
const {
/** @lends Interactable */
Interactable,
} = scope
scope.actions.phases.reflow = true
/**
* ```js
* const interactable = interact(target)
* const drag = { name: drag, axis: 'x' }
* const resize = { name: resize, edges: { left: true, bottom: true }
*
* interactable.reflow(drag)
* interactable.reflow(resize)
* ```
*
* Start an action sequence to re-apply modifiers, check drops, etc.
*
* @param { Object } action The action to begin
* @param { string } action.name The name of the action
* @returns { Promise } A promise that resolves to the `Interactable` when actions on all targets have ended
*/
Interactable.prototype.reflow = function (action: ActionProps) {
return doReflow(this, action, scope)
}
}
function doReflow<T extends ActionName> (
interactable: Interactable,
action: ActionProps<T>,
scope: Scope,
): Promise<Interactable> {
const elements = (is.string(interactable.target)
? arr.from(interactable._context.querySelectorAll(interactable.target))
: [interactable.target]) as Element[]
// tslint:disable-next-line variable-name
const Promise = (scope.window as any).Promise
const promises: Array<Promise<null>> | null = Promise ? [] : null
for (const element of elements) {
const rect = interactable.getRect(element as HTMLElement | SVGElement)
if (!rect) {
break
}
const runningInteraction = arr.find(scope.interactions.list, (interaction: Interaction) => {
return (
interaction.interacting() &&
interaction.interactable === interactable &&
interaction.element === element &&
interaction.prepared.name === action.name
)
})
let reflowPromise: Promise<null>
if (runningInteraction) {
runningInteraction.move()
if (promises) {
reflowPromise =
runningInteraction._reflowPromise ||
new Promise((resolve: any) => {
runningInteraction._reflowResolve = resolve
})
}
} else {
const xywh = tlbrToXywh(rect)
const coords = {
page: { x: xywh.x, y: xywh.y },
client: { x: xywh.x, y: xywh.y },
timeStamp: scope.now(),
}
const event = pointerUtils.coordsToEvent(coords)
reflowPromise = startReflow<T>(scope, interactable, element, action, event)
}
if (promises) {
promises.push(reflowPromise)
}
}
return promises && Promise.all(promises).then(() => interactable)
}
function startReflow<T extends ActionName> (
scope: Scope,
interactable: Interactable,
element: Element,
action: ActionProps<T>,
event: any,
) {
const interaction = scope.interactions.new({ pointerType: 'reflow' })
const signalArg = {
interaction,
event,
pointer: event,
eventTarget: element,
phase: 'reflow',
} as const
interaction.interactable = interactable
interaction.element = element
interaction.prevEvent = event
interaction.updatePointer(event, event, element, true)
pointerUtils.setZeroCoords(interaction.coords.delta)
copyAction(interaction.prepared, action)
interaction._doPhase(signalArg)
const { Promise } = (scope.window as unknown) as { Promise: PromiseConstructor }
const reflowPromise = Promise
? new Promise<undefined>(resolve => {
interaction._reflowResolve = resolve
})
: undefined
interaction._reflowPromise = reflowPromise
interaction.start(action, interactable, element)
if (interaction._interacting) {
interaction.move(signalArg)
interaction.end(event)
} else {
interaction.stop()
interaction._reflowResolve()
}
interaction.removePointer(event, event)
return reflowPromise
}
const reflow: Plugin = {
id: 'reflow',
install,
listeners: {
// remove completed reflow interactions
'interactions:stop': ({ interaction }, scope) => {
if (interaction.pointerType === 'reflow') {
if (interaction._reflowResolve) {
interaction._reflowResolve()
}
arr.remove(scope.interactions.list, interaction)
}
},
},
}
export default reflow