,
})
this.pointers.splice(pointerIndex, 1)
this.pointerIsDown = false
}
/** @internal */
_updateLatestPointer(pointer: PointerType, event: PointerEventType, eventTarget: Node) {
this._latestPointer.pointer = pointer
this._latestPointer.event = event
this._latestPointer.eventTarget = eventTarget
}
destroy() {
this._latestPointer.pointer = null
this._latestPointer.event = null
this._latestPointer.eventTarget = null
}
/** @internal */
_createPreparedEvent(
event: PointerEventType,
phase: P,
preEnd?: boolean,
type?: string,
) {
return new InteractEvent(this, event, this.prepared.name, phase, this.element, preEnd, type)
}
/** @internal */
_fireEvent(iEvent: InteractEvent) {
this.interactable?.fire(iEvent)
if (!this.prevEvent || iEvent.timeStamp >= this.prevEvent.timeStamp) {
this.prevEvent = iEvent
}
}
/** @internal */
_doPhase(
signalArg: Omit, 'iEvent'> & { iEvent?: InteractEvent },
) {
const { event, phase, preEnd, type } = signalArg
const { rect } = this
if (rect && phase === 'move') {
// update the rect changes due to pointer move
rectUtils.addEdges(this.edges, rect, this.coords.delta[this.interactable.options.deltaSource])
rect.width = rect.right - rect.left
rect.height = rect.bottom - rect.top
}
const beforeResult = this._scopeFire(`interactions:before-action-${phase}` as any, signalArg)
if (beforeResult === false) {
return false
}
const iEvent = (signalArg.iEvent = this._createPreparedEvent(event, phase, preEnd, type))
this._scopeFire(`interactions:action-${phase}` as any, signalArg)
if (phase === 'start') {
this.prevEvent = iEvent
}
this._fireEvent(iEvent)
this._scopeFire(`interactions:after-action-${phase}` as any, signalArg)
return true
}
/** @internal */
_now() {
return Date.now()
}
}
export default Interaction
export { PointerInfo }
================================================
FILE: packages/@interactjs/core/NativeTypes.ts
================================================
export const NativePointerEvent = null as unknown as InstanceType
export type NativeEventTarget = EventTarget
export type NativeElement = Element
================================================
FILE: packages/@interactjs/core/PointerInfo.ts
================================================
import type { PointerEventType, PointerType } from '@interactjs/core/types'
export class PointerInfo {
id: number
pointer: PointerType
event: PointerEventType
downTime: number
downTarget: Node
constructor(id: number, pointer: PointerType, event: PointerEventType, downTime: number, downTarget: Node) {
this.id = id
this.pointer = pointer
this.event = event
this.downTime = downTime
this.downTarget = downTarget
}
}
================================================
FILE: packages/@interactjs/core/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/core/events.ts
================================================
import * as arr from '@interactjs/utils/arr'
import * as domUtils from '@interactjs/utils/domUtils'
import is from '@interactjs/utils/is'
import pExtend from '@interactjs/utils/pointerExtend'
import * as pointerUtils from '@interactjs/utils/pointerUtils'
import type { Scope } from '@interactjs/core/scope'
import type { Element } from '@interactjs/core/types'
import type { NativeEventTarget } from './NativeTypes'
declare module '@interactjs/core/scope' {
interface Scope {
events: ReturnType
}
}
interface EventOptions {
capture: boolean
passive: boolean
}
type PartialEventTarget = Partial
type ListenerEntry = { func: (event: Event | FakeEvent) => any; options: EventOptions }
function install(scope: Scope) {
const targets: Array<{
eventTarget: PartialEventTarget
events: { [type: string]: ListenerEntry[] }
}> = []
const delegatedEvents: {
[type: string]: Array<{
selector: string
context: Node
listeners: ListenerEntry[]
}>
} = {}
const documents: Document[] = []
const eventsMethods = {
add,
remove,
addDelegate,
removeDelegate,
delegateListener,
delegateUseCapture,
delegatedEvents,
documents,
targets,
supportsOptions: false,
supportsPassive: false,
}
// check if browser supports passive events and options arg
scope.document?.createElement('div').addEventListener('test', null, {
get capture() {
return (eventsMethods.supportsOptions = true)
},
get passive() {
return (eventsMethods.supportsPassive = true)
},
})
scope.events = eventsMethods
function add(
eventTarget: PartialEventTarget,
type: string,
listener: ListenerEntry['func'],
optionalArg?: boolean | EventOptions,
) {
if (!eventTarget.addEventListener) return
const options = getOptions(optionalArg)
let target = arr.find(targets, (t) => t.eventTarget === eventTarget)
if (!target) {
target = {
eventTarget,
events: {},
}
targets.push(target)
}
if (!target.events[type]) {
target.events[type] = []
}
if (!arr.find(target.events[type], (l) => l.func === listener && optionsMatch(l.options, options))) {
eventTarget.addEventListener(
type,
listener as any,
eventsMethods.supportsOptions ? options : options.capture,
)
target.events[type].push({ func: listener, options })
}
}
function remove(
eventTarget: PartialEventTarget,
type: string,
listener?: 'all' | ListenerEntry['func'],
optionalArg?: boolean | EventOptions,
) {
if (!eventTarget.addEventListener || !eventTarget.removeEventListener) return
const targetIndex = arr.findIndex(targets, (t) => t.eventTarget === eventTarget)
const target = targets[targetIndex]
if (!target || !target.events) {
return
}
if (type === 'all') {
for (type in target.events) {
if (target.events.hasOwnProperty(type)) {
remove(eventTarget, type, 'all')
}
}
return
}
let typeIsEmpty = false
const typeListeners = target.events[type]
if (typeListeners) {
if (listener === 'all') {
for (let i = typeListeners.length - 1; i >= 0; i--) {
const entry = typeListeners[i]
remove(eventTarget, type, entry.func, entry.options)
}
return
} else {
const options = getOptions(optionalArg)
for (let i = 0; i < typeListeners.length; i++) {
const entry = typeListeners[i]
if (entry.func === listener && optionsMatch(entry.options, options)) {
eventTarget.removeEventListener(
type,
listener as any,
eventsMethods.supportsOptions ? options : options.capture,
)
typeListeners.splice(i, 1)
if (typeListeners.length === 0) {
delete target.events[type]
typeIsEmpty = true
}
break
}
}
}
}
if (typeIsEmpty && !Object.keys(target.events).length) {
targets.splice(targetIndex, 1)
}
}
function addDelegate(
selector: string,
context: Node,
type: string,
listener: ListenerEntry['func'],
optionalArg?: any,
) {
const options = getOptions(optionalArg)
if (!delegatedEvents[type]) {
delegatedEvents[type] = []
// add delegate listener functions
for (const doc of documents) {
add(doc, type, delegateListener)
add(doc, type, delegateUseCapture, true)
}
}
const delegates = delegatedEvents[type]
let delegate = arr.find(delegates, (d) => d.selector === selector && d.context === context)
if (!delegate) {
delegate = { selector, context, listeners: [] }
delegates.push(delegate)
}
delegate.listeners.push({ func: listener, options })
}
function removeDelegate(
selector: string,
context: Document | Element,
type: string,
listener?: ListenerEntry['func'],
optionalArg?: any,
) {
const options = getOptions(optionalArg)
const delegates = delegatedEvents[type]
let matchFound = false
let index: number
if (!delegates) return
// count from last index of delegated to 0
for (index = delegates.length - 1; index >= 0; index--) {
const cur = delegates[index]
// look for matching selector and context Node
if (cur.selector === selector && cur.context === context) {
const { listeners } = cur
// each item of the listeners array is an array: [function, capture, passive]
for (let i = listeners.length - 1; i >= 0; i--) {
const entry = listeners[i]
// check if the listener functions and capture and passive flags match
if (entry.func === listener && optionsMatch(entry.options, options)) {
// remove the listener from the array of listeners
listeners.splice(i, 1)
// if all listeners for this target have been removed
// remove the target from the delegates array
if (!listeners.length) {
delegates.splice(index, 1)
// remove delegate function from context
remove(context, type, delegateListener)
remove(context, type, delegateUseCapture, true)
}
// only remove one listener
matchFound = true
break
}
}
if (matchFound) {
break
}
}
}
}
// bound to the interactable context when a DOM event
// listener is added to a selector interactable
function delegateListener(event: Event | FakeEvent, optionalArg?: any) {
const options = getOptions(optionalArg)
const fakeEvent = new FakeEvent(event as Event)
const delegates = delegatedEvents[event.type]
const [eventTarget] = pointerUtils.getEventTargets(event as Event)
let element: Node = eventTarget
// climb up document tree looking for selector matches
while (is.element(element)) {
for (let i = 0; i < delegates.length; i++) {
const cur = delegates[i]
const { selector, context } = cur
if (
domUtils.matchesSelector(element, selector) &&
domUtils.nodeContains(context, eventTarget) &&
domUtils.nodeContains(context, element)
) {
const { listeners } = cur
fakeEvent.currentTarget = element
for (const entry of listeners) {
if (optionsMatch(entry.options, options)) {
entry.func(fakeEvent)
}
}
}
}
element = domUtils.parentNode(element)
}
}
function delegateUseCapture(this: Element, event: Event | FakeEvent) {
return delegateListener.call(this, event, true)
}
// for type inferrence
return eventsMethods
}
class FakeEvent implements Partial {
currentTarget: Node
originalEvent: Event
type: string
constructor(originalEvent: Event) {
this.originalEvent = originalEvent
// duplicate the event so that currentTarget can be changed
pExtend(this, originalEvent)
}
preventOriginalDefault() {
this.originalEvent.preventDefault()
}
stopPropagation() {
this.originalEvent.stopPropagation()
}
stopImmediatePropagation() {
this.originalEvent.stopImmediatePropagation()
}
}
function getOptions(param: { [index: string]: any } | boolean): { capture: boolean; passive: boolean } {
if (!is.object(param)) {
return { capture: !!param, passive: false }
}
return {
capture: !!param.capture,
passive: !!param.passive,
}
}
function optionsMatch(a: Partial | boolean, b: Partial) {
if (a === b) return true
if (typeof a === 'boolean') return !!b.capture === a && !!b.passive === false
return !!a.capture === !!b.capture && !!a.passive === !!b.passive
}
export default {
id: 'events',
install,
}
================================================
FILE: packages/@interactjs/core/interactablePreventDefault.spec.ts
================================================
import drag from '@interactjs/actions/drag/plugin'
import autoStart from '@interactjs/auto-start/base'
import interactablePreventDefault from './interactablePreventDefault'
import * as helpers from './tests/_helpers'
test('core/interactablePreventDefault', () => {
window.PointerEvent = null
const { scope, interactable } = helpers.testEnv({
plugins: [interactablePreventDefault, autoStart, drag],
})
const { MouseEvent, Event } = scope.window as any
interactable.draggable({})
const mouseDown: MouseEvent = new MouseEvent('mousedown', { bubbles: true })
const nativeDragStart: Event = new Event('dragstart', { bubbles: true })
nativeDragStart.preventDefault = jest.fn()
scope.document.body.dispatchEvent(mouseDown)
scope.document.body.dispatchEvent(nativeDragStart)
// native dragstart is prevented on interactable
expect(nativeDragStart.preventDefault).toHaveBeenCalledTimes(1)
})
================================================
FILE: packages/@interactjs/core/interactablePreventDefault.ts
================================================
import { matchesSelector, nodeContains } from '@interactjs/utils/domUtils'
import is from '@interactjs/utils/is'
import { getWindow } from '@interactjs/utils/window'
import type { Interactable } from '@interactjs/core/Interactable'
import type Interaction from '@interactjs/core/Interaction'
import type { Scope } from '@interactjs/core/scope'
import type { PointerEventType } from '@interactjs/core/types'
type PreventDefaultValue = 'always' | 'never' | 'auto'
declare module '@interactjs/core/Interactable' {
interface Interactable {
preventDefault(newValue: PreventDefaultValue): this
preventDefault(): PreventDefaultValue
/**
* Returns or sets whether to prevent the browser's default behaviour in
* response to pointer events. Can be set to:
* - `'always'` to always prevent
* - `'never'` to never prevent
* - `'auto'` to let interact.js try to determine what would be best
*
* @param newValue - `'always'`, `'never'` or `'auto'`
* @returns The current setting or this Interactable
*/
preventDefault(newValue?: PreventDefaultValue): PreventDefaultValue | this
checkAndPreventDefault(event: Event): void
}
}
const preventDefault = function preventDefault(this: Interactable, newValue?: PreventDefaultValue) {
if (/^(always|never|auto)$/.test(newValue)) {
this.options.preventDefault = newValue
return this
}
if (is.bool(newValue)) {
this.options.preventDefault = newValue ? 'always' : 'never'
return this
}
return this.options.preventDefault
} as Interactable['preventDefault']
function checkAndPreventDefault(interactable: Interactable, scope: Scope, event: Event) {
const setting = interactable.options.preventDefault
if (setting === 'never') return
if (setting === 'always') {
event.preventDefault()
return
}
// setting === 'auto'
// if the browser supports passive event listeners and isn't running on iOS,
// don't preventDefault of touch{start,move} events. CSS touch-action and
// user-select should be used instead of calling event.preventDefault().
if (scope.events.supportsPassive && /^touch(start|move)$/.test(event.type)) {
const doc = getWindow(event.target).document
const docOptions = scope.getDocOptions(doc)
if (!(docOptions && docOptions.events) || docOptions.events.passive !== false) {
return
}
}
// don't preventDefault of pointerdown events
if (/^(mouse|pointer|touch)*(down|start)/i.test(event.type)) {
return
}
// don't preventDefault on editable elements
if (
is.element(event.target) &&
matchesSelector(event.target, 'input,select,textarea,[contenteditable=true],[contenteditable=true] *')
) {
return
}
event.preventDefault()
}
function onInteractionEvent({ interaction, event }: { interaction: Interaction; event: PointerEventType }) {
if (interaction.interactable) {
interaction.interactable.checkAndPreventDefault(event as Event)
}
}
export function install(scope: Scope) {
const { Interactable } = scope
Interactable.prototype.preventDefault = preventDefault
Interactable.prototype.checkAndPreventDefault = function (event) {
return checkAndPreventDefault(this, scope, event)
}
// prevent native HTML5 drag on interact.js target elements
scope.interactions.docEvents.push({
type: 'dragstart',
listener(event) {
for (const interaction of scope.interactions.list) {
if (
interaction.element &&
(interaction.element === event.target || nodeContains(interaction.element, event.target))
) {
interaction.interactable.checkAndPreventDefault(event)
return
}
}
},
})
}
export default {
id: 'core/interactablePreventDefault',
install,
listeners: ['down', 'move', 'up', 'cancel'].reduce((acc, eventType) => {
acc[`interactions:${eventType}`] = onInteractionEvent
return acc
}, {} as any),
}
================================================
FILE: packages/@interactjs/core/interactionFinder.spec.ts
================================================
import finder from './interactionFinder'
import * as helpers from './tests/_helpers'
test('modifiers/snap', () => {
const { interactable, event, coords, scope } = helpers.testEnv()
const { body } = scope.document
const { list } = scope.interactions
const details = {
pointer: event,
get pointerId(): number {
return details.pointer.pointerId
},
get pointerType(): string {
return details.pointer.pointerType
},
eventType: null as string,
eventTarget: body,
curEventTarget: scope.document,
scope,
}
scope.interactions.new({ pointerType: 'touch' })
scope.interactions.new({ pointerType: 'mouse' })
coords.pointerType = 'mouse'
list[0].pointerType = 'mouse'
list[2]._interacting = true
// [pointerType: mouse] skips inactive mouse and touch interaction
expect(list.indexOf(finder.search(details))).toBe(2)
list[2]._interacting = false
// [pointerType: mouse] returns first idle mouse interaction
expect(list.indexOf(finder.search(details))).toBe(0)
coords.pointerId = 4
list[1].pointerDown({ ...event } as any, { ...event } as any, body)
coords.pointerType = 'touch'
// [pointerType: touch] gets interaction with pointerId
expect(list.indexOf(finder.search(details))).toBe(1)
coords.pointerId = 5
// `[pointerType: touch] returns idle touch interaction without matching pointerId and existing touch interaction has pointer and no target`
expect(list.indexOf(finder.search(details))).toBe(1)
interactable.options.gesture = { enabled: false }
list[1].interactable = interactable
// `[pointerType: touch] no result without matching pointerId and existing touch interaction has a pointer and target not gesturable`
expect(list.indexOf(finder.search(details))).toBe(-1)
interactable.options.gesture = { enabled: true }
// `[pointerType: touch] returns idle touch interaction with gesturable target and existing pointer`
expect(list.indexOf(finder.search(details))).toBe(1)
})
================================================
FILE: packages/@interactjs/core/interactionFinder.ts
================================================
import * as dom from '@interactjs/utils/domUtils'
import type Interaction from '@interactjs/core/Interaction'
import type { Scope } from '@interactjs/core/scope'
import type { PointerType } from '@interactjs/core/types'
export interface SearchDetails {
pointer: PointerType
pointerId: number
pointerType: string
eventType: string
eventTarget: EventTarget
curEventTarget: EventTarget
scope: Scope
}
const finder = {
methodOrder: ['simulationResume', 'mouseOrPen', 'hasPointer', 'idle'] as const,
search(details: SearchDetails) {
for (const method of finder.methodOrder) {
const interaction = finder[method](details)
if (interaction) {
return interaction
}
}
return null
},
// try to resume simulation with a new pointer
simulationResume({ pointerType, eventType, eventTarget, scope }: SearchDetails) {
if (!/down|start/i.test(eventType)) {
return null
}
for (const interaction of scope.interactions.list) {
let element = eventTarget as Node
if (
interaction.simulation &&
interaction.simulation.allowResume &&
interaction.pointerType === pointerType
) {
while (element) {
// if the element is the interaction element
if (element === interaction.element) {
return interaction
}
element = dom.parentNode(element)
}
}
}
return null
},
// if it's a mouse or pen interaction
mouseOrPen({ pointerId, pointerType, eventType, scope }: SearchDetails) {
if (pointerType !== 'mouse' && pointerType !== 'pen') {
return null
}
let firstNonActive
for (const interaction of scope.interactions.list) {
if (interaction.pointerType === pointerType) {
// if it's a down event, skip interactions with running simulations
if (interaction.simulation && !hasPointerId(interaction, pointerId)) {
continue
}
// if the interaction is active, return it immediately
if (interaction.interacting()) {
return interaction
}
// otherwise save it and look for another active interaction
else if (!firstNonActive) {
firstNonActive = interaction
}
}
}
// if no active mouse interaction was found use the first inactive mouse
// interaction
if (firstNonActive) {
return firstNonActive
}
// find any mouse or pen interaction.
// ignore the interaction if the eventType is a *down, and a simulation
// is active
for (const interaction of scope.interactions.list) {
if (interaction.pointerType === pointerType && !(/down/i.test(eventType) && interaction.simulation)) {
return interaction
}
}
return null
},
// get interaction that has this pointer
hasPointer({ pointerId, scope }: SearchDetails) {
for (const interaction of scope.interactions.list) {
if (hasPointerId(interaction, pointerId)) {
return interaction
}
}
return null
},
// get first idle interaction with a matching pointerType
idle({ pointerType, scope }: SearchDetails) {
for (const interaction of scope.interactions.list) {
// if there's already a pointer held down
if (interaction.pointers.length === 1) {
const target = interaction.interactable
// don't add this pointer if there is a target interactable and it
// isn't gesturable
if (target && !(target.options.gesture && target.options.gesture.enabled)) {
continue
}
}
// maximum of 2 pointers per interaction
else if (interaction.pointers.length >= 2) {
continue
}
if (!interaction.interacting() && pointerType === interaction.pointerType) {
return interaction
}
}
return null
},
}
function hasPointerId(interaction: Interaction, pointerId: number) {
return interaction.pointers.some(({ id }) => id === pointerId)
}
export default finder
================================================
FILE: packages/@interactjs/core/interactions.spec.ts
================================================
import Interaction from './Interaction'
import interactions from './interactions'
import * as helpers from './tests/_helpers'
describe('core/interactions', () => {
test('interactions', () => {
let scope = helpers.mockScope()
const interaction = scope.interactions.new({ pointerType: 'TEST' })
// new Interaction is pushed to scope.interactions
expect(scope.interactions.list[0]).toBe(interaction)
// interactions object added to scope
expect(scope.interactions).toBeInstanceOf(Object)
const listeners = scope.interactions.listeners
expect(interactions.methodNames.every((m: string) => typeof listeners[m] === 'function')).toBe(true)
scope = helpers.mockScope()
const newInteraction = scope.interactions.new({})
expect(typeof scope.interactions).toBe('object')
expect(typeof scope.interactions.new).toBe('function')
expect(newInteraction instanceof Interaction).toBe(true)
expect(typeof newInteraction._scopeFire).toBe('function')
expect(scope.actions).toBeInstanceOf(Object)
expect(scope.actions.map).toEqual({})
expect(scope.actions.methodDict).toEqual({})
})
test('interactions document event options', () => {
const { scope } = helpers.testEnv()
const doc = scope.document
let options = {}
scope.browser = { isIOS: false } as any
scope.fire('scope:add-document', { doc, scope, options } as any)
// no doc options.event.passive is added when not iOS
expect(options).toEqual({})
options = {}
scope.browser.isIOS = true
scope.fire('scope:add-document', { doc, scope, options } as any)
// doc options.event.passive is set to false for iOS
expect(options).toEqual({ events: { passive: false } })
})
test('interactions removes pointers on targeting removed elements', () => {
const { interaction, scope } = helpers.testEnv()
const { PointerEvent } = scope.window as any
const div1 = scope.document.body.appendChild(scope.document.createElement('div'))
const div2 = scope.document.body.appendChild(scope.document.createElement('div'))
const touch1Init = { bubbles: true, pointerType: 'touch', pointerId: 1, target: div1 }
const touch2Init = { bubbles: true, pointerType: 'touch', pointerId: 2, target: div2 }
interaction.pointerType = 'touch'
div1.dispatchEvent(new PointerEvent('pointerdown', touch1Init))
div1.dispatchEvent(new PointerEvent('pointermove', touch1Init))
expect(scope.interactions.list).toHaveLength(1)
// down pointer added to interaction
expect(interaction.pointers).toHaveLength(1)
// _latestPointer target is down target
expect(interaction._latestPointer.eventTarget).toBe(div1)
div1.remove()
div2.dispatchEvent(new TouchEvent('touchstart', touch2Init))
// interaction with removed element is reused for new pointer
expect(scope.interactions.list).toEqual([interaction])
// pointer on removed element is removed from existing interaction and new pointerdown is added
expect(interaction.pointers).toHaveLength(1)
})
})
================================================
FILE: packages/@interactjs/core/interactions.ts
================================================
import browser from '@interactjs/utils/browser'
import domObjects from '@interactjs/utils/domObjects'
import { nodeContains } from '@interactjs/utils/domUtils'
import * as pointerUtils from '@interactjs/utils/pointerUtils'
import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope'
import type { ActionName, Listener } from '@interactjs/core/types'
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import './interactablePreventDefault'
import interactablePreventDefault from './interactablePreventDefault'
import InteractionBase from './Interaction'
/* eslint-enable import/no-duplicates */
import type { SearchDetails } from './interactionFinder'
import finder from './interactionFinder'
declare module '@interactjs/core/scope' {
interface Scope {
Interaction: typeof InteractionBase
interactions: {
new: (options: any) => InteractionBase
list: Array>
listeners: { [type: string]: Listener }
docEvents: Array<{ type: string; listener: Listener }>
pointerMoveTolerance: number
}
prevTouchTime: number
}
interface SignalArgs {
'interactions:find': {
interaction: InteractionBase
searchDetails: SearchDetails
}
}
}
const methodNames = [
'pointerDown',
'pointerMove',
'pointerUp',
'updatePointer',
'removePointer',
'windowBlur',
]
function install(scope: Scope) {
const listeners = {} as any
for (const method of methodNames) {
listeners[method] = doOnInteractions(method, scope)
}
const pEventTypes = browser.pEventTypes
let docEvents: typeof scope.interactions.docEvents
if (domObjects.PointerEvent) {
docEvents = [
{ type: pEventTypes.down, listener: releasePointersOnRemovedEls },
{ type: pEventTypes.down, listener: listeners.pointerDown },
{ type: pEventTypes.move, listener: listeners.pointerMove },
{ type: pEventTypes.up, listener: listeners.pointerUp },
{ type: pEventTypes.cancel, listener: listeners.pointerUp },
]
} else {
docEvents = [
{ type: 'mousedown', listener: listeners.pointerDown },
{ type: 'mousemove', listener: listeners.pointerMove },
{ type: 'mouseup', listener: listeners.pointerUp },
{ type: 'touchstart', listener: releasePointersOnRemovedEls },
{ type: 'touchstart', listener: listeners.pointerDown },
{ type: 'touchmove', listener: listeners.pointerMove },
{ type: 'touchend', listener: listeners.pointerUp },
{ type: 'touchcancel', listener: listeners.pointerUp },
]
}
docEvents.push({
type: 'blur',
listener(event) {
for (const interaction of scope.interactions.list) {
interaction.documentBlur(event)
}
},
})
// for ignoring browser's simulated mouse events
scope.prevTouchTime = 0
scope.Interaction = class extends InteractionBase {
get pointerMoveTolerance() {
return scope.interactions.pointerMoveTolerance
}
set pointerMoveTolerance(value) {
scope.interactions.pointerMoveTolerance = value
}
_now() {
return scope.now()
}
}
scope.interactions = {
// all active and idle interactions
list: [],
new(options: { pointerType?: string; scopeFire?: Scope['fire'] }) {
options.scopeFire = (name, arg) => scope.fire(name, arg)
const interaction = new scope.Interaction(options as Required)
scope.interactions.list.push(interaction)
return interaction
},
listeners,
docEvents,
pointerMoveTolerance: 1,
}
function releasePointersOnRemovedEls() {
// for all inactive touch interactions with pointers down
for (const interaction of scope.interactions.list) {
if (!interaction.pointerIsDown || interaction.pointerType !== 'touch' || interaction._interacting) {
continue
}
// if a pointer is down on an element that is no longer in the DOM tree
for (const pointer of interaction.pointers) {
if (!scope.documents.some(({ doc }) => nodeContains(doc, pointer.downTarget))) {
// remove the pointer from the interaction
interaction.removePointer(pointer.pointer, pointer.event)
}
}
}
}
scope.usePlugin(interactablePreventDefault)
}
function doOnInteractions(method: string, scope: Scope) {
return function (event: Event) {
const interactions = scope.interactions.list
const pointerType = pointerUtils.getPointerType(event)
const [eventTarget, curEventTarget] = pointerUtils.getEventTargets(event)
const matches: any[] = [] // [ [pointer, interaction], ...]
if (/^touch/.test(event.type)) {
scope.prevTouchTime = scope.now()
// @ts-expect-error
for (const changedTouch of event.changedTouches) {
const pointer = changedTouch
const pointerId = pointerUtils.getPointerId(pointer)
const searchDetails: SearchDetails = {
pointer,
pointerId,
pointerType,
eventType: event.type,
eventTarget,
curEventTarget,
scope,
}
const interaction = getInteraction(searchDetails)
matches.push([
searchDetails.pointer,
searchDetails.eventTarget,
searchDetails.curEventTarget,
interaction,
])
}
} else {
let invalidPointer = false
if (!browser.supportsPointerEvent && /mouse/.test(event.type)) {
// ignore mouse events while touch interactions are active
for (let i = 0; i < interactions.length && !invalidPointer; i++) {
invalidPointer = interactions[i].pointerType !== 'mouse' && interactions[i].pointerIsDown
}
// try to ignore mouse events that are simulated by the browser
// after a touch event
invalidPointer =
invalidPointer ||
scope.now() - scope.prevTouchTime < 500 ||
// on iOS and Firefox Mobile, MouseEvent.timeStamp is zero if simulated
event.timeStamp === 0
}
if (!invalidPointer) {
const searchDetails = {
pointer: event as PointerEvent,
pointerId: pointerUtils.getPointerId(event as PointerEvent),
pointerType,
eventType: event.type,
curEventTarget,
eventTarget,
scope,
}
const interaction = getInteraction(searchDetails)
matches.push([
searchDetails.pointer,
searchDetails.eventTarget,
searchDetails.curEventTarget,
interaction,
])
}
}
// eslint-disable-next-line no-shadow
for (const [pointer, eventTarget, curEventTarget, interaction] of matches) {
interaction[method](pointer, event, eventTarget, curEventTarget)
}
}
}
function getInteraction(searchDetails: SearchDetails) {
const { pointerType, scope } = searchDetails
const foundInteraction = finder.search(searchDetails)
const signalArg = { interaction: foundInteraction, searchDetails }
scope.fire('interactions:find', signalArg)
return signalArg.interaction || scope.interactions.new({ pointerType })
}
function onDocSignal(
{ doc, scope, options }: SignalArgs[T],
eventMethodName: 'add' | 'remove',
) {
const {
interactions: { docEvents },
events,
} = scope
const eventMethod = events[eventMethodName]
if (scope.browser.isIOS && !options.events) {
options.events = { passive: false }
}
// delegate event listener
for (const eventType in events.delegatedEvents) {
eventMethod(doc, eventType, events.delegateListener)
eventMethod(doc, eventType, events.delegateUseCapture, true)
}
const eventOptions = options && options.events
for (const { type, listener } of docEvents) {
eventMethod(doc, type, listener, eventOptions)
}
}
const interactions: Plugin = {
id: 'core/interactions',
install,
listeners: {
'scope:add-document': (arg) => onDocSignal(arg, 'add'),
'scope:remove-document': (arg) => onDocSignal(arg, 'remove'),
'interactable:unset': ({ interactable }, scope) => {
// Stop and destroy related interactions when an Interactable is unset
for (let i = scope.interactions.list.length - 1; i >= 0; i--) {
const interaction = scope.interactions.list[i]
if (interaction.interactable !== interactable) {
continue
}
interaction.stop()
scope.fire('interactions:destroy', { interaction })
interaction.destroy()
if (scope.interactions.list.length > 2) {
scope.interactions.list.splice(i, 1)
}
}
},
},
onDocSignal,
doOnInteractions,
methodNames,
}
export default interactions
================================================
FILE: packages/@interactjs/core/options.ts
================================================
import type { Point, Listeners, OrBoolean, Element, Rect } from '@interactjs/core/types'
export interface Defaults {
base: BaseDefaults
perAction: PerActionDefaults
actions: ActionDefaults
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ActionDefaults {}
export interface BaseDefaults {
preventDefault?: 'always' | 'never' | 'auto'
deltaSource?: 'page' | 'client'
context?: Node
getRect?: (element: Element) => Rect
}
export interface PerActionDefaults {
enabled?: boolean
origin?: Point | string | Element
listeners?: Listeners
allowFrom?: string | Element
ignoreFrom?: string | Element
}
export type Options = Partial &
Partial & {
[P in keyof ActionDefaults]?: Partial
}
export interface OptionsArg extends BaseDefaults, OrBoolean> {}
export const defaults: Defaults = {
base: {
preventDefault: 'auto',
deltaSource: 'page',
},
perAction: {
enabled: false,
origin: { x: 0, y: 0 },
},
actions: {} as ActionDefaults,
}
================================================
FILE: packages/@interactjs/core/package.json
================================================
{
"name": "@interactjs/core",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/core"
},
"peerDependencies": {
"@interactjs/utils": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/core/scope.spec.ts
================================================
import type { ActionName } from '@interactjs/core/types'
import * as helpers from './tests/_helpers'
describe('core/scope', () => {
test('usePlugin', () => {
const { scope } = helpers.testEnv()
const plugin1 = { id: '1', listeners: {} }
const plugin2 = { id: '2', listeners: {} }
const plugin3 = { id: '3', listeners: {}, before: ['2'] }
const plugin4 = { id: '4', listeners: {}, before: ['2', '3'] }
const initialListeners = scope.listenerMaps.map((l) => l.id)
scope.usePlugin(plugin1)
scope.usePlugin(plugin2)
scope.usePlugin(plugin3)
scope.usePlugin(plugin4)
expect(scope.listenerMaps.map((l) => l.id)).toEqual([...initialListeners, '1', '4', '3', '2'])
})
test('interactable unset', () => {
const { scope, interactable, interaction, event } = helpers.testEnv()
const interactable2 = scope.interactables.new('x')
expect(scope.interactables.list).toContain(interactable)
expect(scope.interactables.list).toContain(interactable2)
;(interactable.options as any).test = { enabled: true }
interaction.pointerDown(event, event, scope.document.body)
interaction.start({ name: 'test' as ActionName }, interactable, scope.document.body)
const started = interaction._interacting
interactable.unset()
const stopped = !interaction._interacting
expect(scope.interactables.list).not.toContain(interactable)
expect(scope.interactables.list).toContain(interactable2)
// interaction is stopped on interactable.unset()
expect(started && stopped).toBe(true)
// repeated call to unset
interactable.unset()
expect(scope.interactables.list).not.toContain(interactable)
expect(scope.interactables.list).toContain(interactable2)
})
})
================================================
FILE: packages/@interactjs/core/scope.ts
================================================
import browser from '@interactjs/utils/browser'
import clone from '@interactjs/utils/clone'
import domObjects from '@interactjs/utils/domObjects'
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import raf from '@interactjs/utils/raf'
import * as win from '@interactjs/utils/window'
import type Interaction from '@interactjs/core/Interaction'
import { Eventable } from './Eventable'
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import './events'
import './interactions'
import events from './events'
import { Interactable as InteractableBase } from './Interactable'
import { InteractableSet } from './InteractableSet'
import { InteractEvent } from './InteractEvent'
import interactions from './interactions'
/* eslint-enable import/no-duplicates */
import { createInteractStatic } from './InteractStatic'
import type { OptionsArg } from './options'
import { defaults } from './options'
import type { Actions } from './types'
export interface SignalArgs {
'scope:add-document': DocSignalArg
'scope:remove-document': DocSignalArg
'interactable:unset': { interactable: InteractableBase }
'interactable:set': { interactable: InteractableBase; options: OptionsArg }
'interactions:destroy': { interaction: Interaction }
}
export type ListenerName = keyof SignalArgs
export type ListenerMap = {
[P in ListenerName]?: (arg: SignalArgs[P], scope: Scope, signalName: P) => void | boolean
}
interface DocSignalArg {
doc: Document
window: Window
scope: Scope
options: Record
}
export interface Plugin {
[key: string]: any
id?: string
listeners?: ListenerMap
before?: string[]
install?(scope: Scope, options?: any): void
}
/** @internal */
export class Scope {
id = `__interact_scope_${Math.floor(Math.random() * 100)}`
isInitialized = false
listenerMaps: Array<{
map: ListenerMap
id?: string
}> = []
browser = browser
defaults = clone(defaults) as typeof defaults
Eventable = Eventable
actions: Actions = {
map: {},
phases: {
start: true,
move: true,
end: true,
},
methodDict: {} as any,
phaselessTypes: {},
}
interactStatic = createInteractStatic(this)
InteractEvent = InteractEvent
Interactable: typeof InteractableBase
interactables = new InteractableSet(this)
// main window
_win!: Window
// main document
document!: Document
// main window
window!: Window
// all documents being listened to
documents: Array<{ doc: Document; options: any }> = []
_plugins: {
list: Plugin[]
map: { [id: string]: Plugin }
} = {
list: [],
map: {},
}
constructor() {
const scope = this
this.Interactable = class extends InteractableBase {
get _defaults() {
return scope.defaults
}
set(this: T, options: OptionsArg) {
super.set(options)
scope.fire('interactable:set', {
options,
interactable: this,
})
return this
}
unset(this: InteractableBase) {
super.unset()
const index = scope.interactables.list.indexOf(this)
if (index < 0) return
scope.interactables.list.splice(index, 1)
scope.fire('interactable:unset', { interactable: this })
}
}
}
addListeners(map: ListenerMap, id?: string) {
this.listenerMaps.push({ id, map })
}
fire(name: T, arg: SignalArgs[T]): void | false {
for (const {
map: { [name]: listener },
} of this.listenerMaps) {
if (!!listener && listener(arg as any, this, name as never) === false) {
return false
}
}
}
onWindowUnload = (event: BeforeUnloadEvent) => this.removeDocument(event.target as Document)
init(window: Window | typeof globalThis) {
return this.isInitialized ? this : initScope(this, window)
}
pluginIsInstalled(plugin: Plugin) {
const { id } = plugin
return id ? !!this._plugins.map[id] : this._plugins.list.indexOf(plugin) !== -1
}
usePlugin(plugin: Plugin, options?: { [key: string]: any }) {
if (!this.isInitialized) {
return this
}
if (this.pluginIsInstalled(plugin)) {
return this
}
if (plugin.id) {
this._plugins.map[plugin.id] = plugin
}
this._plugins.list.push(plugin)
if (plugin.install) {
plugin.install(this, options)
}
if (plugin.listeners && plugin.before) {
let index = 0
const len = this.listenerMaps.length
const before = plugin.before.reduce((acc, id) => {
acc[id] = true
acc[pluginIdRoot(id)] = true
return acc
}, {})
for (; index < len; index++) {
const otherId = this.listenerMaps[index].id
if (otherId && (before[otherId] || before[pluginIdRoot(otherId)])) {
break
}
}
this.listenerMaps.splice(index, 0, { id: plugin.id, map: plugin.listeners })
} else if (plugin.listeners) {
this.listenerMaps.push({ id: plugin.id, map: plugin.listeners })
}
return this
}
addDocument(doc: Document, options?: any): void | false {
// do nothing if document is already known
if (this.getDocIndex(doc) !== -1) {
return false
}
const window = win.getWindow(doc)
options = options ? extend({}, options) : {}
this.documents.push({ doc, options })
this.events.documents.push(doc)
// don't add an unload event for the main document
// so that the page may be cached in browser history
if (doc !== this.document) {
this.events.add(window, 'unload', this.onWindowUnload)
}
this.fire('scope:add-document', { doc, window, scope: this, options })
}
removeDocument(doc: Document) {
const index = this.getDocIndex(doc)
const window = win.getWindow(doc)
const options = this.documents[index].options
this.events.remove(window, 'unload', this.onWindowUnload)
this.documents.splice(index, 1)
this.events.documents.splice(index, 1)
this.fire('scope:remove-document', { doc, window, scope: this, options })
}
getDocIndex(doc: Document) {
for (let i = 0; i < this.documents.length; i++) {
if (this.documents[i].doc === doc) {
return i
}
}
return -1
}
getDocOptions(doc: Document) {
const docIndex = this.getDocIndex(doc)
return docIndex === -1 ? null : this.documents[docIndex].options
}
now() {
return (((this.window as any).Date as typeof Date) || Date).now()
}
}
// Keep Scope class internal, but expose minimal interface to avoid broken types when Scope is stripped out
export interface Scope {
fire(name: T, arg: SignalArgs[T]): void | false
}
/** @internal */
export function initScope(scope: Scope, window: Window | typeof globalThis) {
scope.isInitialized = true
if (is.window(window)) {
win.init(window)
}
domObjects.init(window)
browser.init(window)
raf.init(window)
// @ts-expect-error
scope.window = window
scope.document = window.document
scope.usePlugin(interactions)
scope.usePlugin(events)
return scope
}
function pluginIdRoot(id: string) {
return id && id.replace(/\/.*$/, '')
}
================================================
FILE: packages/@interactjs/core/tests/_helpers.ts
================================================
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import * as pointerUtils from '@interactjs/utils/pointerUtils'
import type { PointerType, Rect, Target, ActionName, ActionProps } from '@interactjs/core/types'
import type { Plugin } from '../scope'
import { Scope } from '../scope'
let counter = 0
export function unique() {
return counter++
}
export function uniqueProps(obj: any) {
for (const prop in obj) {
if (!obj.hasOwnProperty(prop)) {
continue
}
if (is.object(obj)) {
uniqueProps(obj[prop])
} else {
obj[prop] = counter++
}
}
}
export function newCoordsSet(n = 0) {
return {
start: {
page: { x: n++, y: n++ },
client: { x: n++, y: n++ },
timeStamp: n++,
},
cur: {
page: { x: n++, y: n++ },
client: { x: n++, y: n++ },
timeStamp: n++,
},
prev: {
page: { x: n++, y: n++ },
client: { x: n++, y: n++ },
timeStamp: n++,
},
delta: {
page: { x: n++, y: n++ },
client: { x: n++, y: n++ },
timeStamp: n++,
},
velocity: {
page: { x: n++, y: n++ },
client: { x: n++, y: n++ },
timeStamp: n++,
},
}
}
export function newPointer(n = 50) {
return {
pointerId: n++,
pageX: n++,
pageY: n++,
clientX: n++,
clientY: n++,
} as PointerType
}
export function mockScope({ document = window.document } = {} as any) {
const window = document.defaultView
const scope = new Scope().init(window)
extend(scope.actions.phaselessTypes, { teststart: true, testmove: true, testend: true })
return scope
}
export function getProps(src: T, props: readonly K[]) {
return props.reduce(
(acc, prop) => {
if (prop in src) {
acc[prop] = src[prop]
}
return acc
},
{} as Pick,
)
}
export function testEnv({
plugins = [],
target,
rect,
document = window.document,
}: {
plugins?: Plugin[]
target?: T
rect?: Rect
document?: Document
} = {}) {
const scope = mockScope({ document })
for (const plugin of plugins) {
scope.usePlugin(plugin)
}
if (!target) {
;(target as unknown as HTMLElement) = scope.document.body
}
const interaction = scope.interactions.new({})
const interactable = scope.interactables.new(target)
const coords: pointerUtils.MockCoords = pointerUtils.newCoords()
coords.target = target
const event = pointerUtils.coordsToEvent(coords)
if (rect) {
interactable.rectChecker(() => ({ ...rect }))
}
return {
scope,
interaction,
target: target as T extends undefined ? HTMLElement : T,
interactable,
coords,
event,
interact: scope.interactStatic,
start: (action: ActionProps) =>
interaction.start(action, interactable, target as HTMLElement),
stop: () => interaction.stop(),
down: () => interaction.pointerDown(event, event, target as HTMLElement),
move: (force?: boolean) =>
force ? interaction.move() : interaction.pointerMove(event, event, target as HTMLElement),
up: () => interaction.pointerUp(event, event, target as HTMLElement, target as HTMLElement),
}
}
export function timeout(n: number) {
return new Promise((resolve) => setTimeout(resolve, n))
}
export function ltrbwh(
left: number,
top: number,
right: number,
bottom: number,
width: number,
height: number,
) {
return { left, top, right, bottom, width, height }
}
================================================
FILE: packages/@interactjs/core/types.ts
================================================
import type Interaction from '@interactjs/core/Interaction'
import type { Interactable } from './Interactable'
import type { PhaseMap, InteractEvent } from './InteractEvent'
import type { NativePointerEvent as NativePointerEvent_ } from './NativeTypes'
export type OrBoolean = {
[P in keyof T]: T[P] | boolean
}
export type Element = HTMLElement | SVGElement
export type Context = Document | Element
export type EventTarget = Window | Document | Element
export type Target = EventTarget | string
export interface Point {
x: number
y: number
}
export interface Size {
width: number
height: number
}
export interface Rect {
top: number
left: number
bottom: number
right: number
width?: number
height?: number
}
export type FullRect = Required
export type RectFunction = (...args: T) => Rect | Element
export type RectResolvable = Rect | string | Element | RectFunction
export type Dimensions = Point & Size
export interface CoordsSetMember {
page: Point
client: Point
timeStamp: number
}
export interface CoordsSet {
cur: CoordsSetMember
prev: CoordsSetMember
start: CoordsSetMember
delta: CoordsSetMember
velocity: CoordsSetMember
}
export interface HasGetRect {
getRect(element: Element): Rect
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ActionMap {}
export type ActionName = keyof ActionMap
export interface Actions {
map: ActionMap
phases: PhaseMap
methodDict: Record
phaselessTypes: { [type: string]: true }
}
export interface ActionProps {
name: T
axis?: 'x' | 'y' | 'xy' | null
edges?: EdgeOptions | null
}
export interface InertiaOption {
resistance?: number
minSpeed?: number
endSpeed?: number
allowResume?: boolean
smoothEndDuration?: number
}
export type InertiaOptions = InertiaOption | boolean
export interface EdgeOptions {
top?: boolean | string | Element
left?: boolean | string | Element
bottom?: boolean | string | Element
right?: boolean | string | Element
}
export type CursorChecker = (
action: ActionProps,
interactable: Interactable,
element: Element,
interacting: boolean,
) => string
export interface ActionMethod {
(this: Interactable): T
(this: Interactable, options?: Partial> | boolean): typeof this
}
export interface OptionMethod {
(this: Interactable): T
// eslint-disable-next-line no-undef
(this: Interactable, options: T): typeof this
}
export type ActionChecker = (
pointerEvent: any,
defaultAction: string,
interactable: Interactable,
element: Element,
interaction: Interaction,
) => ActionProps
export type OriginFunction = (target: Element) => Rect
export interface PointerEventsOptions {
holdDuration?: number
allowFrom?: string
ignoreFrom?: string
origin?: Rect | Point | string | Element | OriginFunction
}
export type RectChecker = (element: Element) => Rect
export type NativePointerEventType = typeof NativePointerEvent_
export type PointerEventType = MouseEvent | TouchEvent | Partial | InteractEvent
export type PointerType = MouseEvent | Touch | Partial | InteractEvent
export type EventTypes = string | ListenerMap | Array
export type Listener = (...args: any[]) => any
export type Listeners = ListenerMap | ListenerMap[]
export type ListenersArg = Listener | ListenerMap | Array
export interface ListenerMap {
[index: string]: ListenersArg | ListenersArg[]
}
export type ArrayElementType = T extends Array ? P : never
================================================
FILE: packages/@interactjs/dev-tools/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/dev-tools/babel-plugin-prod.js
================================================
/* global process, __dirname */
const path = require('path')
const PROD_EXT = '.prod'
function fixImportSource ({ node: { source } }, { filename }) {
if (shouldIgnoreImport(source)) return
let resolvedShort = ''
try {
const paths = [filename && path.dirname(filename), __dirname, process.cwd()].filter((p) => !!p)
const resolved = require.resolve(source.value, { paths })
const resolvedWithoutScopePath = resolved.replace(/.*[\\/]@interactjs[\\/]/, '')
resolvedShort = path
.join('@interactjs', resolvedWithoutScopePath)
// windows path to posix
.replace(/\\/g, '/')
source.value = resolvedShort.replace(/(\.js)?$/, PROD_EXT)
} catch (e) {}
}
function babelPluginInteractjsProd () {
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.warn(
"[@interactjs/dev-tools] You're using the production plugin in the development environment. You might lose out on some helpful hints!",
)
}
return {
visitor: {
ImportDeclaration: fixImportSource,
ExportNamedDeclaration: fixImportSource,
ExportAllDeclaration: fixImportSource,
ExportDefaultSpecifier: fixImportSource,
},
}
}
function shouldIgnoreImport (source) {
return (
!source ||
// only change @interactjs scoped imports
!source.value.startsWith('@interactjs/') ||
// ignore imports of prod files
source.value.endsWith(PROD_EXT) ||
source.value.endsWith(PROD_EXT + '.js')
)
}
module.exports = babelPluginInteractjsProd
Object.assign(module.exports, {
default: babelPluginInteractjsProd,
fixImportSource,
})
================================================
FILE: packages/@interactjs/dev-tools/babel-plugin-prod.spec.ts
================================================
/** @jest-environment node */
import * as babel from '@babel/core'
import proposalExportDefaultFrom from '@babel/plugin-proposal-export-default-from'
import babelPluginProd, { fixImportSource } from './babel-plugin-prod'
describe('@dev-tools/prod/babel-plugin-prod', () => {
const filename = require.resolve('@interactjs/_dev/test/fixtures/babelPluginProject/index.js')
const cases = [
{
module: 'x',
expected: 'x',
message: 'non @interact/* package unchanged',
},
{
module: 'interact',
expected: 'interact',
message: 'unscoped interact import unchanged',
},
{
module: '@interactjs/NONEXISTENT_PACKAGE',
expected: '@interactjs/NONEXISTENT_PACKAGE',
message: 'missing package unchanged',
},
{
module: '@interactjs/a/NONEXISTENT_MODULE',
expected: '@interactjs/a/NONEXISTENT_MODULE',
message: 'import of missing module unchanged',
},
{
module: '@interactjs/a',
expected: '@interactjs/a/package-main-file.prod',
message: 'package main module',
},
{
module: '@interactjs/a/a',
expected: '@interactjs/a/a.prod',
message: 'package root-level non index module',
},
{
module: '@interactjs/a/b',
expected: '@interactjs/a/b/index.prod',
message: 'nested index module',
},
{
module: '@interactjs/a/b/b',
expected: '@interactjs/a/b/b.prod',
message: 'package nested non index module',
},
]
for (const { module, expected, message } of cases) {
// eslint-disable-next-line jest/valid-title
test(message, () => {
const source = { value: module }
fixImportSource({ node: { source } }, { filename })
expect(source.value).toBe(expected)
})
}
test('transforms code when used in babel config', () => {
expect(
babel.transform(
[
'import "@interactjs/a/a";',
'import a, { b } from "@interactjs/a/a";',
'export b from "@interactjs/a/a";',
'export * from "@interactjs/a/a";',
].join('\n'),
{
babelrc: false,
configFile: false,
plugins: [babelPluginProd, [proposalExportDefaultFrom, { loose: true }]],
filename,
sourceType: 'module',
},
).code,
).toEqual(
[
'import "@interactjs/a/a.prod";',
'import a, { b } from "@interactjs/a/a.prod";',
'export { default as b } from "@interactjs/a/a.prod";',
'export * from "@interactjs/a/a.prod";',
].join('\n'),
)
})
})
================================================
FILE: packages/@interactjs/dev-tools/devTools.spec.ts
================================================
import drag from '@interactjs/actions/drag/plugin'
import resize from '@interactjs/actions/resize/plugin'
import * as helpers from '@interactjs/core/tests/_helpers'
import type { Check, Logger } from './plugin'
import devTools from './plugin'
const { checks, links, prefix } = devTools
const checkMap = checks.reduce(
(acc, check) => {
acc[check.name] = check
return acc
},
{} as { [name: string]: Check },
)
test('devTools', () => {
const devToolsWithLogger = {
install: (s) =>
s.usePlugin(devTools, {
logger: {
warn(...args: any[]) {
log(args, 'warn')
},
log(...args: any[]) {
log(args, 'log')
},
error(...args: any[]) {
log(args, 'error')
},
},
}),
}
const {
scope,
interaction,
interactable,
target: element,
down,
start,
move,
stop,
} = helpers.testEnv({
plugins: [devToolsWithLogger, drag, resize],
})
const logs: Array<{ args: any[]; type: keyof Logger }> = []
function log(args: any, type: any) {
logs.push({ args, type })
}
scope.usePlugin(drag)
scope.usePlugin(resize)
interactable.draggable(true).resizable({ onmove: () => {} })
down()
start({ name: 'drag' })
// warning about missing touchAction
expect(logs[0]).toEqual({
args: [prefix + checkMap.touchAction.text, element, links.touchAction],
type: 'warn',
})
// warning about missing move listeners
expect(logs[1]).toEqual({ args: [prefix + checkMap.noListeners.text, 'drag', interactable], type: 'warn' })
stop()
// resolve touchAction
element.style.touchAction = 'none'
// resolve missing listeners
interactable.on('dragmove', () => {})
interaction.start({ name: 'resize' }, interactable, element)
move()
stop()
// warning about resizing without "box-sizing: none"
expect(logs[2]).toEqual({
args: [prefix + checkMap.boxSizing.text, element, links.boxSizing],
type: 'warn',
})
logs.splice(0)
interaction.start({ name: 'resize' }, interactable, element)
move()
stop()
interactable.options.devTools.ignore = { boxSizing: true }
interaction.start({ name: 'resize' }, interactable, element)
move()
stop()
// warning removed with options.devTools.ignore
expect(logs).toHaveLength(1)
logs.splice(0)
// resolve boxSizing
interactable.options.devTools.ignore = {}
element.style.boxSizing = 'border-box'
interaction.start({ name: 'resize' }, interactable, element)
move(true)
stop()
interaction.start({ name: 'drag' }, interactable, element)
move()
stop()
// no warnings when issues are resolved
expect(logs).toHaveLength(0)
})
================================================
FILE: packages/@interactjs/dev-tools/package.json
================================================
{
"name": "@interactjs/dev-tools",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/dev-tools"
},
"peerDependencies": {
"@interactjs/modifiers": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27",
"vue": "3"
},
"devDependencies": {
"vue": "next"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/dev-tools/plugin.ts
================================================
import type Interaction from '@interactjs/core/Interaction'
import type { Scope, Plugin } from '@interactjs/core/scope'
import type { Element, OptionMethod } from '@interactjs/core/types'
import domObjects from '@interactjs/utils/domObjects'
import { parentNode } from '@interactjs/utils/domUtils'
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import isNonNativeEvent from '@interactjs/utils/isNonNativeEvent'
import normalizeListeners from '@interactjs/utils/normalizeListeners'
import * as win from '@interactjs/utils/window'
declare module '@interactjs/core/scope' {
interface Scope {
logger: Logger
}
}
declare module '@interactjs/core/options' {
interface BaseDefaults {
devTools?: DevToolsOptions
}
}
declare module '@interactjs/core/Interactable' {
interface Interactable {
devTools: OptionMethod
}
}
export interface DevToolsOptions {
ignore: { [P in keyof typeof CheckName]?: boolean }
}
export interface Logger {
warn: (...args: any[]) => void
error: (...args: any[]) => void
log: (...args: any[]) => void
}
export interface Check {
name: CheckName
text: string
perform: (interaction: Interaction) => boolean
getInfo: (interaction: Interaction) => any[]
}
enum CheckName {
touchAction = 'touchAction',
boxSizing = 'boxSizing',
noListeners = 'noListeners',
}
const prefix = '[interact.js] '
const links = {
touchAction: 'https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action',
boxSizing: 'https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing',
}
// eslint-disable-next-line no-undef
const isProduction = process.env.NODE_ENV === 'production'
function install(scope: Scope, { logger }: { logger?: Logger } = {}) {
const { Interactable, defaults } = scope
scope.logger = logger || console
defaults.base.devTools = {
ignore: {},
}
Interactable.prototype.devTools = function (options?: object) {
if (options) {
extend(this.options.devTools, options)
return this
}
return this.options.devTools
}
// can't set native events on non string targets without `addEventListener` prop
const { _onOff } = Interactable.prototype
Interactable.prototype._onOff = function (method, typeArg, listenerArg, options, filter) {
if (is.string(this.target) || this.target.addEventListener) {
return _onOff.call(this, method, typeArg, listenerArg, options, filter)
}
if (is.object(typeArg) && !is.array(typeArg)) {
options = listenerArg
listenerArg = null
}
const normalizedListeners = normalizeListeners(typeArg, listenerArg, filter)
for (const type in normalizedListeners) {
if (isNonNativeEvent(type, scope.actions)) continue
scope.logger.warn(
prefix +
`Can't add native "${type}" event listener to target without \`addEventListener(type, listener, options)\` prop.`,
)
}
return _onOff.call(this, method, normalizedListeners, options)
}
}
const checks: Check[] = [
{
name: CheckName.touchAction,
perform({ element }) {
return !!element && !parentHasStyle(element, 'touchAction', /pan-|pinch|none/)
},
getInfo({ element }) {
return [element, links.touchAction]
},
text: 'Consider adding CSS "touch-action: none" to this element\n',
},
{
name: CheckName.boxSizing,
perform(interaction) {
const { element } = interaction
return (
interaction.prepared.name === 'resize' &&
element instanceof domObjects.HTMLElement &&
!hasStyle(element, 'boxSizing', /border-box/)
)
},
text: 'Consider adding CSS "box-sizing: border-box" to this resizable element',
getInfo({ element }) {
return [element, links.boxSizing]
},
},
{
name: CheckName.noListeners,
perform(interaction) {
const actionName = interaction.prepared.name
const moveListeners = interaction.interactable?.events.types[`${actionName}move`] || []
return !moveListeners.length
},
getInfo(interaction) {
return [interaction.prepared.name, interaction.interactable]
},
text: 'There are no listeners set for this action',
},
]
function hasStyle(element: HTMLElement, prop: keyof CSSStyleDeclaration, styleRe: RegExp) {
const value = element.style[prop] || win.window.getComputedStyle(element)[prop]
return styleRe.test((value || '').toString())
}
function parentHasStyle(element: Element, prop: keyof CSSStyleDeclaration, styleRe: RegExp) {
let parent = element as HTMLElement
while (is.element(parent)) {
if (hasStyle(parent, prop, styleRe)) {
return true
}
parent = parentNode(parent) as HTMLElement
}
return false
}
const id = 'dev-tools'
const defaultExport: Plugin = isProduction
? { id, install: () => {} }
: {
id,
install,
listeners: {
'interactions:action-start': ({ interaction }, scope) => {
for (const check of checks) {
const options = interaction.interactable && interaction.interactable.options
if (
!(options && options.devTools && options.devTools.ignore[check.name]) &&
check.perform(interaction)
) {
scope.logger.warn(prefix + check.text, ...check.getInfo(interaction))
}
}
},
},
checks,
CheckName,
links,
prefix,
}
export default defaultExport
================================================
FILE: packages/@interactjs/dev-tools/visualizer/plugin.stub.ts
================================================
export default {}
================================================
FILE: packages/@interactjs/dev-tools/visualizer/plugin.ts
================================================
export default {}
================================================
FILE: packages/@interactjs/dev-tools/visualizer/visualizer.spec.stub.ts
================================================
test.skip('visualizer', () => {})
================================================
FILE: packages/@interactjs/dev-tools/visualizer/visualizer.spec.ts
================================================
test.skip('visualizer', () => {})
================================================
FILE: packages/@interactjs/dev-tools/visualizer/vueModules.stub.ts
================================================
export {}
================================================
FILE: packages/@interactjs/dev-tools/visualizer/vueModules.ts
================================================
export {}
================================================
FILE: packages/@interactjs/inertia/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/inertia/inertia.spec.ts
================================================
import drag from '@interactjs/actions/drag/plugin'
import type { EventPhase, InteractEvent } from '@interactjs/core/InteractEvent'
import * as helpers from '@interactjs/core/tests/_helpers'
import extend from '@interactjs/utils/extend'
import inertia from './plugin'
test('inertia', () => {
const { scope, interaction, down, start, move, up, interactable, coords } = helpers.testEnv({
plugins: [inertia, drag],
rect: { left: 0, top: 0, bottom: 100, right: 100 },
})
const state = interaction.inertia
const modifierChange = 5
const changeModifier = {
options: { endOnly: false },
methods: {
set({ coords: modifierCoords, phase }: any) {
modifierCoords.x = modifierChange
modifierCoords.y = modifierChange
modifierCallPhases.push(phase)
},
},
}
let fired: Array> = []
let modifierCallPhases: EventPhase[] = []
coords.client = coords.page
scope.now = () => coords.timeStamp
interactable.draggable({ inertia: true }).on(
Object.keys(scope.actions.phases).map((p) => `drag${p}`),
(e) => fired.push(e),
)
// test inertia without modifiers or throw
downStartMoveUp({ x: 100, y: 0, dt: 1000 })
// { modifiers: [] } && !thrown: inertia is not activated
expect(state.active).toBe(false)
// test inertia without modifiers and with throw
downStartMoveUp({ x: 100, y: 0, dt: 10 })
// thrown: inertia is activated
expect(state.active && state.timeout).toBeTruthy()
interactable.draggable({ modifiers: [changeModifier as any] })
// test inertia with { endOnly: false } modifier and with throw
downStartMoveUp({ x: 100, y: 0, dt: 10 })
// { endOnly: false } && thrown: modifier is called from pointerUp inertia calc and phase
expect(modifierCallPhases).toEqual(['move', 'inertiastart', 'inertiastart'])
// { endOnly: false } && thrown: move, inertiastart, and end InteractEvents are modified
expect(fired.map(({ page, type }) => ({ ...page, type }))).toEqual([
{ x: 0, y: 0, type: 'dragstart' },
{ x: modifierChange, y: modifierChange, type: 'dragmove' },
{ x: modifierChange, y: modifierChange, type: 'draginertiastart' },
])
// test inertia with { endOnly: true } modifier and with throw
changeModifier.options.endOnly = true
downStartMoveUp({ x: 100, y: 0, dt: 10 })
// { endOnly: true } && thrown: modifier is called from pointerUp inertia calc
expect(modifierCallPhases).toEqual(['inertiastart'])
// { endOnly: true } && thrown: inertia target coords are correct
expect(state.modifiedOffset).toEqual({
// modified target minus move coords
x: modifierChange - 100,
y: modifierChange - 0,
})
// test smoothEnd with { endOnly: false } modifier
changeModifier.options.endOnly = false
downStartMoveUp({ x: 1, y: 0, dt: 1000 })
// { endOnly: false } && !thrown: inertia smoothEnd is not activated
expect(state.active).toBe(false)
// { endOnly: false } && !thrown: modifier is called from pointerUp
expect(modifierCallPhases).toEqual(['move', 'inertiastart'])
// test smoothEnd with { endOnly: true } modifier
changeModifier.options.endOnly = true
downStartMoveUp({ x: 1, y: 0, dt: 1000 })
// { endOnly: true } && !thrown: inertia smoothEnd is activated
expect(state.active).toBe(true)
// { endOnly: true } && !thrown: modifier is called from pointerUp smooth end check
expect(modifierCallPhases).toEqual(['inertiastart'])
interactable.draggable({
modifiers: [
{
options: { endOnly: true },
methods: {
set({ coords: modifiedCoords, phase }) {
extend(modifiedCoords, { x: 300, y: 400 })
modifierCallPhases.push(phase)
},
},
enable: null,
disable: null,
},
],
})
downStartMoveUp({ x: 50, y: 70, dt: 1000 })
coords.timeStamp = 100
expect(state.targetOffset).toEqual({ x: 250, y: 330 })
extend(coords.page, { x: 50, y: 100 })
down()
// inertia is stopped on resume
expect(interaction._interacting && !state.active).toBe(true)
// interaction coords are updated to down coords on resume
expect({ coords: interaction.coords.cur.page, rect: interaction.rect }).toEqual({
coords: coords.page,
rect: { left: 50, top: 70, right: 150, bottom: 170, width: 100, height: 100 },
})
// action resume event coords are set correctly
expect(lastEvent().page).toEqual(coords.page)
move()
// interaction coords are correct on duplicate move after resume
expect({ coords: interaction.coords.cur.page, rect: interaction.rect }).toEqual({
coords: coords.page,
rect: { left: 50, top: 70, right: 150, bottom: 170, width: 100, height: 100 },
})
// second release inertia target is not the modified target
// second release inertia target is not the pointer event coords
// action move event coords on duplicate move after resume is correct
expect(lastEvent().page).toEqual(coords.page)
extend(coords.page, { x: 200, y: 250 })
move()
up()
expect(state.targetOffset).not.toEqual(coords.page)
expect(state.targetOffset).not.toEqual({ x: 300, y: 400 })
// inertiastart is fired at non preEnd modified coords
expect(helpers.getProps(lastEvent(), ['type', 'page', 'rect'] as const)).toEqual({
type: 'draginertiastart',
page: coords.page,
rect: { left: 200, top: 220, right: 300, bottom: 320, width: 100, height: 100 },
})
down()
extend(coords.page, { x: 150, y: 400 })
move()
// interaction coords after second resume are correct
expect({ coords: interaction.coords.cur.page, rect: interaction.rect }).toEqual({
coords: coords.page,
rect: { left: 150, top: 370, right: 250, bottom: 470, width: 100, height: 100 },
})
// action move event after second resume is fired at non preEnd modified coords
expect(helpers.getProps(lastEvent(), ['type', 'page', 'rect'] as const)).toEqual({
type: 'dragmove',
page: coords.page,
rect: { left: 150, top: 370, right: 250, bottom: 470, width: 100, height: 100 },
})
interaction.stop()
function downStartMoveUp({ x, y, dt }: any) {
fired = []
modifierCallPhases = []
coords.timeStamp = 0
interaction.stop()
Object.assign(coords.page, { x: 0, y: 0 })
down()
start({ name: 'drag' })
Object.assign(coords.page, { x, y })
coords.timeStamp = dt
move()
up()
}
function lastEvent() {
return fired[fired.length - 1]
}
})
================================================
FILE: packages/@interactjs/inertia/package.json
================================================
{
"name": "@interactjs/inertia",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/inertia"
},
"dependencies": {
"@interactjs/offset": "1.10.27"
},
"peerDependencies": {
"@interactjs/core": "1.10.27",
"@interactjs/modifiers": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/inertia/plugin.ts
================================================
import type { Interaction, DoPhaseArg } from '@interactjs/core/Interaction'
import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope'
import type { ActionName, Point, PointerEventType } from '@interactjs/core/types'
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import '@interactjs/modifiers/base'
import '@interactjs/offset/plugin'
import * as modifiers from '@interactjs/modifiers/base'
import { Modification } from '@interactjs/modifiers/Modification'
import type { ModifierArg } from '@interactjs/modifiers/types'
import offset from '@interactjs/offset/plugin'
/* eslint-enable import/no-duplicates */
import * as dom from '@interactjs/utils/domUtils'
import hypot from '@interactjs/utils/hypot'
import is from '@interactjs/utils/is'
import { copyCoords } from '@interactjs/utils/pointerUtils'
import raf from '@interactjs/utils/raf'
declare module '@interactjs/core/InteractEvent' {
interface PhaseMap {
resume?: true
inertiastart?: true
}
}
declare module '@interactjs/core/Interaction' {
interface Interaction {
inertia?: InertiaState
}
}
declare module '@interactjs/core/options' {
interface PerActionDefaults {
inertia?: {
enabled?: boolean
resistance?: number // the lambda in exponential decay
minSpeed?: number // target speed must be above this for inertia to start
endSpeed?: number // the speed at which inertia is slow enough to stop
allowResume?: true // allow resuming an action in inertia phase
smoothEndDuration?: number // animate to snap/restrict endOnly if there's no inertia
}
}
}
declare module '@interactjs/core/scope' {
interface SignalArgs {
'interactions:before-action-inertiastart': Omit, 'iEvent'>
'interactions:action-inertiastart': DoPhaseArg
'interactions:after-action-inertiastart': DoPhaseArg
'interactions:before-action-resume': Omit, 'iEvent'>
'interactions:action-resume': DoPhaseArg
'interactions:after-action-resume': DoPhaseArg
}
}
function install(scope: Scope) {
const { defaults } = scope
scope.usePlugin(offset)
scope.usePlugin(modifiers.default)
scope.actions.phases.inertiastart = true
scope.actions.phases.resume = true
defaults.perAction.inertia = {
enabled: false,
resistance: 10, // the lambda in exponential decay
minSpeed: 100, // target speed must be above this for inertia to start
endSpeed: 10, // the speed at which inertia is slow enough to stop
allowResume: true, // allow resuming an action in inertia phase
smoothEndDuration: 300, // animate to snap/restrict endOnly if there's no inertia
}
}
export class InertiaState {
active = false
isModified = false
smoothEnd = false
allowResume = false
modification!: Modification
modifierCount = 0
modifierArg!: ModifierArg
startCoords!: Point
t0 = 0
v0 = 0
te = 0
targetOffset!: Point
modifiedOffset!: Point
currentOffset!: Point
lambda_v0? = 0 // eslint-disable-line camelcase
one_ve_v0? = 0 // eslint-disable-line camelcase
timeout!: number
readonly interaction: Interaction
constructor(interaction: Interaction) {
this.interaction = interaction
}
start(event: PointerEventType) {
const { interaction } = this
const options = getOptions(interaction)
if (!options || !options.enabled) {
return false
}
const { client: velocityClient } = interaction.coords.velocity
const pointerSpeed = hypot(velocityClient.x, velocityClient.y)
const modification = this.modification || (this.modification = new Modification(interaction))
modification.copyFrom(interaction.modification)
this.t0 = interaction._now()
this.allowResume = options.allowResume
this.v0 = pointerSpeed
this.currentOffset = { x: 0, y: 0 }
this.startCoords = interaction.coords.cur.page
this.modifierArg = modification.fillArg({
pageCoords: this.startCoords,
preEnd: true,
phase: 'inertiastart',
})
const thrown =
this.t0 - interaction.coords.cur.timeStamp < 50 &&
pointerSpeed > options.minSpeed &&
pointerSpeed > options.endSpeed
if (thrown) {
this.startInertia()
} else {
modification.result = modification.setAll(this.modifierArg)
if (!modification.result.changed) {
return false
}
this.startSmoothEnd()
}
// force modification change
interaction.modification.result.rect = null
// bring inertiastart event to the target coords
interaction.offsetBy(this.targetOffset)
interaction._doPhase({
interaction,
event,
phase: 'inertiastart',
})
interaction.offsetBy({ x: -this.targetOffset.x, y: -this.targetOffset.y })
// force modification change
interaction.modification.result.rect = null
this.active = true
interaction.simulation = this
return true
}
startInertia() {
const startVelocity = this.interaction.coords.velocity.client
const options = getOptions(this.interaction)
const lambda = options.resistance
const inertiaDur = -Math.log(options.endSpeed / this.v0) / lambda
this.targetOffset = {
x: (startVelocity.x - inertiaDur) / lambda,
y: (startVelocity.y - inertiaDur) / lambda,
}
this.te = inertiaDur
this.lambda_v0 = lambda / this.v0
this.one_ve_v0 = 1 - options.endSpeed / this.v0
const { modification, modifierArg } = this
modifierArg.pageCoords = {
x: this.startCoords.x + this.targetOffset.x,
y: this.startCoords.y + this.targetOffset.y,
}
modification.result = modification.setAll(modifierArg)
if (modification.result.changed) {
this.isModified = true
this.modifiedOffset = {
x: this.targetOffset.x + modification.result.delta.x,
y: this.targetOffset.y + modification.result.delta.y,
}
}
this.onNextFrame(() => this.inertiaTick())
}
startSmoothEnd() {
this.smoothEnd = true
this.isModified = true
this.targetOffset = {
x: this.modification.result.delta.x,
y: this.modification.result.delta.y,
}
this.onNextFrame(() => this.smoothEndTick())
}
onNextFrame(tickFn: () => void) {
this.timeout = raf.request(() => {
if (this.active) {
tickFn()
}
})
}
inertiaTick() {
const { interaction } = this
const options = getOptions(interaction)
const lambda = options.resistance
const t = (interaction._now() - this.t0) / 1000
if (t < this.te) {
const progress = 1 - (Math.exp(-lambda * t) - this.lambda_v0) / this.one_ve_v0
let newOffset: Point
if (this.isModified) {
newOffset = getQuadraticCurvePoint(
0,
0,
this.targetOffset.x,
this.targetOffset.y,
this.modifiedOffset.x,
this.modifiedOffset.y,
progress,
)
} else {
newOffset = {
x: this.targetOffset.x * progress,
y: this.targetOffset.y * progress,
}
}
const delta = { x: newOffset.x - this.currentOffset.x, y: newOffset.y - this.currentOffset.y }
this.currentOffset.x += delta.x
this.currentOffset.y += delta.y
interaction.offsetBy(delta)
interaction.move()
this.onNextFrame(() => this.inertiaTick())
} else {
interaction.offsetBy({
x: this.modifiedOffset.x - this.currentOffset.x,
y: this.modifiedOffset.y - this.currentOffset.y,
})
this.end()
}
}
smoothEndTick() {
const { interaction } = this
const t = interaction._now() - this.t0
const { smoothEndDuration: duration } = getOptions(interaction)
if (t < duration) {
const newOffset = {
x: easeOutQuad(t, 0, this.targetOffset.x, duration),
y: easeOutQuad(t, 0, this.targetOffset.y, duration),
}
const delta = {
x: newOffset.x - this.currentOffset.x,
y: newOffset.y - this.currentOffset.y,
}
this.currentOffset.x += delta.x
this.currentOffset.y += delta.y
interaction.offsetBy(delta)
interaction.move({ skipModifiers: this.modifierCount })
this.onNextFrame(() => this.smoothEndTick())
} else {
interaction.offsetBy({
x: this.targetOffset.x - this.currentOffset.x,
y: this.targetOffset.y - this.currentOffset.y,
})
this.end()
}
}
resume({ pointer, event, eventTarget }: SignalArgs['interactions:down']) {
const { interaction } = this
// undo inertia changes to interaction coords
interaction.offsetBy({
x: -this.currentOffset.x,
y: -this.currentOffset.y,
})
// update pointer at pointer down position
interaction.updatePointer(pointer, event, eventTarget, true)
// fire resume signals and event
interaction._doPhase({
interaction,
event,
phase: 'resume',
})
copyCoords(interaction.coords.prev, interaction.coords.cur)
this.stop()
}
end() {
this.interaction.move()
this.interaction.end()
this.stop()
}
stop() {
this.active = this.smoothEnd = false
this.interaction.simulation = null
raf.cancel(this.timeout)
}
}
function start({ interaction, event }: DoPhaseArg) {
if (!interaction._interacting || interaction.simulation) {
return null
}
const started = interaction.inertia.start(event)
// prevent action end if inertia or smoothEnd
return started ? false : null
}
// Check if the down event hits the current inertia target
// control should be return to the user
function resume(arg: SignalArgs['interactions:down']) {
const { interaction, eventTarget } = arg
const state = interaction.inertia
if (!state.active) return
let element = eventTarget as Node
// climb up the DOM tree from the event target
while (is.element(element)) {
// if interaction element is the current inertia target element
if (element === interaction.element) {
state.resume(arg)
break
}
element = dom.parentNode(element)
}
}
function stop({ interaction }: { interaction: Interaction }) {
const state = interaction.inertia
if (state.active) {
state.stop()
}
}
function getOptions({ interactable, prepared }: Interaction) {
return interactable && interactable.options && prepared.name && interactable.options[prepared.name].inertia
}
const inertia: Plugin = {
id: 'inertia',
before: ['modifiers', 'actions'],
install,
listeners: {
'interactions:new': ({ interaction }) => {
interaction.inertia = new InertiaState(interaction)
},
'interactions:before-action-end': start,
'interactions:down': resume,
'interactions:stop': stop,
'interactions:before-action-resume': (arg) => {
const { modification } = arg.interaction
modification.stop(arg)
modification.start(arg, arg.interaction.coords.cur.page)
modification.applyToInteraction(arg)
},
'interactions:before-action-inertiastart': (arg) => arg.interaction.modification.setAndApply(arg),
'interactions:action-resume': modifiers.addEventModifiers,
'interactions:action-inertiastart': modifiers.addEventModifiers,
'interactions:after-action-inertiastart': (arg) =>
arg.interaction.modification.restoreInteractionCoords(arg),
'interactions:after-action-resume': (arg) => arg.interaction.modification.restoreInteractionCoords(arg),
},
}
// http://stackoverflow.com/a/5634528/2280888
function _getQBezierValue(t: number, p1: number, p2: number, p3: number) {
const iT = 1 - t
return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3
}
function getQuadraticCurvePoint(
startX: number,
startY: number,
cpX: number,
cpY: number,
endX: number,
endY: number,
position: number,
) {
return {
x: _getQBezierValue(position, startX, cpX, endX),
y: _getQBezierValue(position, startY, cpY, endY),
}
}
// http://gizma.com/easing/
function easeOutQuad(t: number, b: number, c: number, d: number) {
t /= d
return -c * t * (t - 2) + b
}
export default inertia
================================================
FILE: packages/@interactjs/interact/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/interact/index.ts
================================================
import { Scope } from '@interactjs/core/scope'
const scope = new Scope()
const interact = scope.interactStatic
export default interact
const _global = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this
scope.init(_global)
================================================
FILE: packages/@interactjs/interact/interact.spec.ts
================================================
import { Scope } from '@interactjs/core/scope'
const makeIframeDoc = () => {
const iframe = document.body.appendChild(document.createElement('iframe'))
return iframe.contentWindow.document
}
test('interact export', () => {
const scope = new Scope()
const interact = scope.interactStatic
scope.init(window)
const interactable1 = interact('selector')
// interact function returns Interactable instance
expect(interactable1).toBeInstanceOf(scope.Interactable)
// same interactable is returned with same target and context
expect(interact('selector')).toBe(interactable1)
// new interactables are added to list
expect(scope.interactables.list).toHaveLength(1)
interactable1.unset()
// unset interactables are removed
expect(scope.interactables.list).toHaveLength(0)
// unset interactions are removed
expect(scope.interactions.list).toHaveLength(0)
const doc1 = document
const doc2 = makeIframeDoc()
const results = (
[
['repeat', doc1],
['repeat', doc2],
[doc1, doc1],
[doc2.body, doc2],
] as const
).reduce((acc, [target, context]) => {
const interactable = interact(target, { context })
// unique contexts make unique interactables with identical targets
expect(acc.some((e) => e.interactable === interactable)).toBe(false)
acc.push({ interactable, target, context })
return acc
}, [])
for (const { interactable, target, context } of results) {
// interactions.get returns correct result with identical targets and different contexts
expect(scope.interactables.getExisting(target, { context })).toBe(interactable)
}
const doc3 = makeIframeDoc()
const prevDocCount = scope.documents.length
interact.addDocument(doc3, { events: { passive: false } })
// interact.addDocument() adds to scope with options
expect(scope.documents[prevDocCount]).toEqual({ doc: doc3, options: { events: { passive: false } } })
interact.removeDocument(doc3)
// interact.removeDocument() removes document from scope
expect(scope.documents).toHaveLength(prevDocCount)
scope.interactables.list.forEach((i) => i.unset())
const plugin1 = {
id: 'test-1',
install() {
plugin1.count++
},
count: 0,
}
const plugin2 = {
id: '',
install() {
plugin2.count++
},
count: 0,
}
interact.use(plugin1)
interact.use(plugin2)
// new plugin install methods are called
expect([plugin1.count, plugin2.count]).toEqual([1, 1])
interact.use({ ...plugin1 })
// different plugin object with same id not installed
expect([plugin1.count, plugin2.count]).toEqual([1, 1])
interact.use(plugin2)
// plugin without id not re-installed
expect([plugin1.count, plugin2.count]).toEqual([1, 1])
})
================================================
FILE: packages/@interactjs/interact/package.json
================================================
{
"name": "@interactjs/interact",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/interact"
},
"dependencies": {
"@interactjs/core": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": false,
"license": "MIT"
}
================================================
FILE: packages/@interactjs/interactjs/index.stub.ts
================================================
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import '@interactjs/actions/plugin'
import '@interactjs/auto-scroll/plugin'
import '@interactjs/auto-start/plugin'
import '@interactjs/core/interactablePreventDefault'
import '@interactjs/dev-tools/plugin'
import '@interactjs/inertia/plugin'
import '@interactjs/interact'
import '@interactjs/modifiers/plugin'
import '@interactjs/offset/plugin'
import '@interactjs/pointer-events/plugin'
import '@interactjs/reflow/plugin'
import actions from '@interactjs/actions/plugin'
import autoScroll from '@interactjs/auto-scroll/plugin'
import autoStart from '@interactjs/auto-start/plugin'
import interactablePreventDefault from '@interactjs/core/interactablePreventDefault'
import devTools from '@interactjs/dev-tools/plugin'
import inertia from '@interactjs/inertia/plugin'
import interact from '@interactjs/interact'
import modifiers from '@interactjs/modifiers/plugin'
import offset from '@interactjs/offset/plugin'
import pointerEvents from '@interactjs/pointer-events/plugin'
import reflow from '@interactjs/reflow/plugin'
/* eslint-enable import/no-duplicates */
interact.use(interactablePreventDefault)
interact.use(offset)
// pointerEvents
interact.use(pointerEvents)
// inertia
interact.use(inertia)
// snap, resize, etc.
interact.use(modifiers)
// autoStart, hold
interact.use(autoStart)
// drag and drop, resize, gesture
interact.use(actions)
// autoScroll
interact.use(autoScroll)
// reflow
interact.use(reflow)
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV !== 'production') {
interact.use(devTools)
}
export default interact
;(interact as any).default = interact
================================================
FILE: packages/@interactjs/interactjs/index.ts
================================================
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import '@interactjs/actions/plugin'
import '@interactjs/auto-scroll/plugin'
import '@interactjs/auto-start/plugin'
import '@interactjs/core/interactablePreventDefault'
import '@interactjs/dev-tools/plugin'
import '@interactjs/inertia/plugin'
import '@interactjs/interact'
import '@interactjs/modifiers/plugin'
import '@interactjs/offset/plugin'
import '@interactjs/pointer-events/plugin'
import '@interactjs/reflow/plugin'
import actions from '@interactjs/actions/plugin'
import autoScroll from '@interactjs/auto-scroll/plugin'
import autoStart from '@interactjs/auto-start/plugin'
import interactablePreventDefault from '@interactjs/core/interactablePreventDefault'
import devTools from '@interactjs/dev-tools/plugin'
import inertia from '@interactjs/inertia/plugin'
import interact from '@interactjs/interact'
import modifiers from '@interactjs/modifiers/plugin'
import offset from '@interactjs/offset/plugin'
import pointerEvents from '@interactjs/pointer-events/plugin'
import reflow from '@interactjs/reflow/plugin'
/* eslint-enable import/no-duplicates */
interact.use(interactablePreventDefault)
interact.use(offset)
// pointerEvents
interact.use(pointerEvents)
// inertia
interact.use(inertia)
// snap, resize, etc.
interact.use(modifiers)
// autoStart, hold
interact.use(autoStart)
// drag and drop, resize, gesture
interact.use(actions)
// autoScroll
interact.use(autoScroll)
// reflow
interact.use(reflow)
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV !== 'production') {
interact.use(devTools)
}
export default interact
;(interact as any).default = interact
================================================
FILE: packages/@interactjs/interactjs/package.json
================================================
{
"name": "@interactjs/interactjs",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git"
},
"dependencies": {
"@interactjs/actions": "1.10.27",
"@interactjs/arrange": "1.10.27",
"@interactjs/auto-scroll": "1.10.27",
"@interactjs/auto-start": "1.10.27",
"@interactjs/clone": "1.10.27",
"@interactjs/core": "1.10.27",
"@interactjs/dev-tools": "1.10.27",
"@interactjs/feedback": "1.10.27",
"@interactjs/inertia": "1.10.27",
"@interactjs/interact": "1.10.27",
"@interactjs/modifiers": "1.10.27",
"@interactjs/multi-target": "1.10.27",
"@interactjs/offset": "1.10.27",
"@interactjs/pointer-events": "1.10.27",
"@interactjs/react": "1.10.27",
"@interactjs/rebound": "1.10.27",
"@interactjs/symbol-tree": "1.10.27",
"@interactjs/reflow": "1.10.27",
"@interactjs/utils": "1.10.27",
"@interactjs/vue": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": false,
"license": "MIT"
}
================================================
FILE: packages/@interactjs/modifiers/Modification.ts
================================================
import type { EventPhase } from '@interactjs/core/InteractEvent'
import type { Interaction, DoAnyPhaseArg } from '@interactjs/core/Interaction'
import type { EdgeOptions, FullRect, Point, Rect } from '@interactjs/core/types'
import clone from '@interactjs/utils/clone'
import extend from '@interactjs/utils/extend'
import * as rectUtils from '@interactjs/utils/rect'
import type { Modifier, ModifierArg, ModifierState } from './types'
export interface ModificationResult {
delta: Point
rectDelta: Rect
coords: Point
rect: FullRect
eventProps: any[]
changed: boolean
}
interface MethodArg {
phase: EventPhase
pageCoords: Point
rect: FullRect
coords: Point
preEnd?: boolean
skipModifiers?: number
}
export class Modification {
states: ModifierState[] = []
startOffset: Rect = { left: 0, right: 0, top: 0, bottom: 0 }
startDelta!: Point
result!: ModificationResult
endResult!: Point
startEdges!: EdgeOptions
edges: EdgeOptions
readonly interaction: Readonly
constructor(interaction: Interaction) {
this.interaction = interaction
this.result = createResult()
this.edges = {
left: false,
right: false,
top: false,
bottom: false,
}
}
start({ phase }: { phase: EventPhase }, pageCoords: Point) {
const { interaction } = this
const modifierList = getModifierList(interaction)
this.prepareStates(modifierList)
this.startEdges = extend({}, interaction.edges)
this.edges = extend({}, this.startEdges)
this.startOffset = getRectOffset(interaction.rect, pageCoords)
this.startDelta = { x: 0, y: 0 }
const arg = this.fillArg({
phase,
pageCoords,
preEnd: false,
})
this.result = createResult()
this.startAll(arg)
const result = (this.result = this.setAll(arg))
return result
}
fillArg(arg: Partial) {
const { interaction } = this
arg.interaction = interaction
arg.interactable = interaction.interactable
arg.element = interaction.element
arg.rect ||= interaction.rect
arg.edges ||= this.startEdges
arg.startOffset = this.startOffset
return arg as ModifierArg
}
startAll(arg: MethodArg & Partial) {
for (const state of this.states) {
if (state.methods.start) {
arg.state = state
state.methods.start(arg as ModifierArg)
}
}
}
setAll(arg: MethodArg & Partial): ModificationResult {
const { phase, preEnd, skipModifiers, rect: unmodifiedRect, edges: unmodifiedEdges } = arg
arg.coords = extend({}, arg.pageCoords)
arg.rect = extend({}, unmodifiedRect)
arg.edges = extend({}, unmodifiedEdges)
const states = skipModifiers ? this.states.slice(skipModifiers) : this.states
const newResult = createResult(arg.coords, arg.rect)
for (const state of states) {
const { options } = state
const lastModifierCoords = extend({}, arg.coords)
let returnValue = null
if (state.methods?.set && this.shouldDo(options, preEnd, phase)) {
arg.state = state
returnValue = state.methods.set(arg as ModifierArg)
rectUtils.addEdges(arg.edges, arg.rect, {
x: arg.coords.x - lastModifierCoords.x,
y: arg.coords.y - lastModifierCoords.y,
})
}
newResult.eventProps.push(returnValue)
}
extend(this.edges, arg.edges)
newResult.delta.x = arg.coords.x - arg.pageCoords.x
newResult.delta.y = arg.coords.y - arg.pageCoords.y
newResult.rectDelta.left = arg.rect.left - unmodifiedRect.left
newResult.rectDelta.right = arg.rect.right - unmodifiedRect.right
newResult.rectDelta.top = arg.rect.top - unmodifiedRect.top
newResult.rectDelta.bottom = arg.rect.bottom - unmodifiedRect.bottom
const prevCoords = this.result.coords
const prevRect = this.result.rect
if (prevCoords && prevRect) {
const rectChanged =
newResult.rect.left !== prevRect.left ||
newResult.rect.right !== prevRect.right ||
newResult.rect.top !== prevRect.top ||
newResult.rect.bottom !== prevRect.bottom
newResult.changed =
rectChanged || prevCoords.x !== newResult.coords.x || prevCoords.y !== newResult.coords.y
}
return newResult
}
applyToInteraction(arg: { phase: EventPhase; rect?: Rect }) {
const { interaction } = this
const { phase } = arg
const curCoords = interaction.coords.cur
const startCoords = interaction.coords.start
const { result, startDelta } = this
const curDelta = result.delta
if (phase === 'start') {
extend(this.startDelta, result.delta)
}
for (const [coordsSet, delta] of [
[startCoords, startDelta],
[curCoords, curDelta],
] as const) {
coordsSet.page.x += delta.x
coordsSet.page.y += delta.y
coordsSet.client.x += delta.x
coordsSet.client.y += delta.y
}
const { rectDelta } = this.result
const rect = arg.rect || interaction.rect
rect.left += rectDelta.left
rect.right += rectDelta.right
rect.top += rectDelta.top
rect.bottom += rectDelta.bottom
rect.width = rect.right - rect.left
rect.height = rect.bottom - rect.top
}
setAndApply(
arg: Partial & {
phase: EventPhase
preEnd?: boolean
skipModifiers?: number
modifiedCoords?: Point
},
): void | false {
const { interaction } = this
const { phase, preEnd, skipModifiers } = arg
const result = this.setAll(
this.fillArg({
preEnd,
phase,
pageCoords: arg.modifiedCoords || interaction.coords.cur.page,
}),
)
this.result = result
// don't fire an action move if a modifier would keep the event in the same
// cordinates as before
if (
!result.changed &&
(!skipModifiers || skipModifiers < this.states.length) &&
interaction.interacting()
) {
return false
}
if (arg.modifiedCoords) {
const { page } = interaction.coords.cur
const adjustment = {
x: arg.modifiedCoords.x - page.x,
y: arg.modifiedCoords.y - page.y,
}
result.coords.x += adjustment.x
result.coords.y += adjustment.y
result.delta.x += adjustment.x
result.delta.y += adjustment.y
}
this.applyToInteraction(arg)
}
beforeEnd(arg: Omit & { state?: ModifierState }): void | false {
const { interaction, event } = arg
const states = this.states
if (!states || !states.length) {
return
}
let doPreend = false
for (const state of states) {
arg.state = state
const { options, methods } = state
const endPosition = methods.beforeEnd && methods.beforeEnd(arg as unknown as ModifierArg)
if (endPosition) {
this.endResult = endPosition
return false
}
doPreend = doPreend || (!doPreend && this.shouldDo(options, true, arg.phase, true))
}
if (doPreend) {
// trigger a final modified move before ending
interaction.move({ event, preEnd: true })
}
}
stop(arg: { interaction: Interaction }) {
const { interaction } = arg
if (!this.states || !this.states.length) {
return
}
const modifierArg: Partial = extend(
{
states: this.states,
interactable: interaction.interactable,
element: interaction.element,
rect: null,
},
arg,
)
this.fillArg(modifierArg)
for (const state of this.states) {
modifierArg.state = state
if (state.methods.stop) {
state.methods.stop(modifierArg as ModifierArg)
}
}
this.states = null
this.endResult = null
}
prepareStates(modifierList: Modifier[]) {
this.states = []
for (let index = 0; index < modifierList.length; index++) {
const { options, methods, name } = modifierList[index]
this.states.push({
options,
methods,
index,
name,
})
}
return this.states
}
restoreInteractionCoords({ interaction: { coords, rect, modification } }: { interaction: Interaction }) {
if (!modification.result) return
const { startDelta } = modification
const { delta: curDelta, rectDelta } = modification.result
const coordsAndDeltas = [
[coords.start, startDelta],
[coords.cur, curDelta],
]
for (const [coordsSet, delta] of coordsAndDeltas as any) {
coordsSet.page.x -= delta.x
coordsSet.page.y -= delta.y
coordsSet.client.x -= delta.x
coordsSet.client.y -= delta.y
}
rect.left -= rectDelta.left
rect.right -= rectDelta.right
rect.top -= rectDelta.top
rect.bottom -= rectDelta.bottom
}
shouldDo(options, preEnd?: boolean, phase?: string, requireEndOnly?: boolean) {
if (
// ignore disabled modifiers
!options ||
options.enabled === false ||
// check if we require endOnly option to fire move before end
(requireEndOnly && !options.endOnly) ||
// don't apply endOnly modifiers when not ending
(options.endOnly && !preEnd) ||
// check if modifier should run be applied on start
(phase === 'start' && !options.setStart)
) {
return false
}
return true
}
copyFrom(other: Modification) {
this.startOffset = other.startOffset
this.startDelta = other.startDelta
this.startEdges = other.startEdges
this.edges = other.edges
this.states = other.states.map((s) => clone(s) as ModifierState)
this.result = createResult(extend({}, other.result.coords), extend({}, other.result.rect))
}
destroy() {
for (const prop in this) {
this[prop] = null
}
}
}
function createResult(coords?: Point, rect?: FullRect): ModificationResult {
return {
rect,
coords,
delta: { x: 0, y: 0 },
rectDelta: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
eventProps: [],
changed: true,
}
}
function getModifierList(interaction) {
const actionOptions = interaction.interactable.options[interaction.prepared.name]
const actionModifiers = actionOptions.modifiers
if (actionModifiers && actionModifiers.length) {
return actionModifiers
}
return ['snap', 'snapSize', 'snapEdges', 'restrict', 'restrictEdges', 'restrictSize']
.map((type) => {
const options = actionOptions[type]
return (
options &&
options.enabled && {
options,
methods: options._methods,
}
)
})
.filter((m) => !!m)
}
export function getRectOffset(rect, coords) {
return rect
? {
left: coords.x - rect.left,
top: coords.y - rect.top,
right: rect.right - coords.x,
bottom: rect.bottom - coords.y,
}
: {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
================================================
FILE: packages/@interactjs/modifiers/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/modifiers/all.ts
================================================
/* eslint-disable n/no-extraneous-import, import/no-unresolved */
import aspectRatio from './aspectRatio'
import avoid from './avoid/avoid'
import restrictEdges from './restrict/edges'
import restrict from './restrict/pointer'
import restrictRect from './restrict/rect'
import restrictSize from './restrict/size'
import rubberband from './rubberband/rubberband'
import snapEdges from './snap/edges'
import snap from './snap/pointer'
import snapSize from './snap/size'
import spring from './spring/spring'
import transform from './transform/transform'
export default {
aspectRatio,
restrictEdges,
restrict,
restrictRect,
restrictSize,
snapEdges,
snap,
snapSize,
spring,
avoid,
transform,
rubberband,
}
================================================
FILE: packages/@interactjs/modifiers/aspectRatio.spec.ts
================================================
import resize from '@interactjs/actions/resize/plugin'
import * as helpers from '@interactjs/core/tests/_helpers'
import type { FullRect, EdgeOptions } from '@interactjs/core/types'
import type { AspectRatioOptions } from './aspectRatio'
import aspectRatio from './aspectRatio'
import modifiersBase from './base'
import restrictSize from './restrict/size'
const { ltrbwh } = helpers
test('modifiers/aspectRatio', () => {
const rect = Object.freeze({ left: 0, top: 0, right: 10, bottom: 20, width: 10, height: 20 })
const { interactable, interaction, event, coords, target } = helpers.testEnv({
plugins: [modifiersBase, resize],
rect,
})
coords.client = coords.page
const options: AspectRatioOptions = {}
let lastRect: FullRect = null
interactable.resizable({
edges: { left: true, top: true, right: true, bottom: true },
modifiers: [aspectRatio(options)],
listeners: {
move(e) {
lastRect = e.rect
},
},
})
options.equalDelta = true
downStartMoveUp({ x: 2, y: 4.33, edges: { left: true, top: true } })
// `equalDelta: true, 1 { left: true, top: true }
expect(lastRect).toEqual(ltrbwh(2, 2, 10, 20, 8, 18))
downStartMoveUp({ x: 30, y: 2, edges: { bottom: true } })
// equalDelta: true, 2, edges: { bottom: true }
expect(lastRect).toEqual(ltrbwh(0, 0, 12, 22, 12, 22))
options.equalDelta = false
options.ratio = 2
downStartMoveUp({ x: -5, y: 2, edges: { left: true } })
// equalDelta: false, ratio: 2, edges: left
expect(lastRect).toEqual(ltrbwh(-5, 12.5, 10, 20, 15, 7.5))
// combine with restrictSize
options.modifiers = [
restrictSize({
max: { width: 20, height: 20 },
}),
]
options.equalDelta = false
options.ratio = 2
downStartMoveUp({ x: 20, y: 0, edges: { right: true } })
// restrictSize with critical primary edge
expect(lastRect).toEqual(ltrbwh(0, 0, 20, 10, 20, 10))
downStartMoveUp({ x: 20, y: 20, edges: { bottom: true } })
// restrictSize with critical secondary edge
expect(lastRect).toEqual(ltrbwh(0, 0, 20, 10, 20, 10))
options.ratio = 0.5
downStartMoveUp({ x: 5, y: -5, edges: { left: true, bottom: true } })
// equalDelta: false, ratio: 2, edges: left & bottom
expect(lastRect).toEqual(ltrbwh(5, 0, 10, 10, 5, 10))
downStartMoveUp({ x: -5, y: -5, edges: { right: true, top: true } })
// equalDelta: false, ratio: 2, edges: right & top
expect(lastRect).toEqual(ltrbwh(0, 10, 5, 20, 5, 10))
function downStartMoveUp({ x, y, edges }: { x: number; y: number; edges: EdgeOptions }) {
coords.timeStamp = 0
interaction.stop()
lastRect = null
Object.assign(coords.page, { x: 0, y: 0 })
interaction.pointerDown(event, event, target)
interaction.start({ name: 'resize', edges }, interactable, target)
Object.assign(coords.page, { x, y })
interaction.pointerMove(event, event, target)
interaction.pointerMove(event, event, target)
interaction.pointerUp(event, event, target, target)
}
})
================================================
FILE: packages/@interactjs/modifiers/aspectRatio.ts
================================================
/**
* @module modifiers/aspectRatio
*
* @description
* This modifier forces elements to be resized with a specified dx/dy ratio.
*
* ```js
* interact(target).resizable({
* modifiers: [
* interact.modifiers.snapSize({
* targets: [ interact.snappers.grid({ x: 20, y: 20 }) ],
* }),
* interact.aspectRatio({ ratio: 'preserve' }),
* ],
* });
* ```
*/
import type { Point, Rect, EdgeOptions } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import { addEdges } from '@interactjs/utils/rect'
import { makeModifier } from './base'
import { Modification } from './Modification'
import type { Modifier, ModifierModule, ModifierState } from './types'
export interface AspectRatioOptions {
ratio?: number | 'preserve'
equalDelta?: boolean
modifiers?: Modifier[]
enabled?: boolean
}
export type AspectRatioState = ModifierState<
AspectRatioOptions,
{
startCoords: Point
startRect: Rect
linkedEdges: EdgeOptions
ratio: number
equalDelta: boolean
xIsPrimaryAxis: boolean
edgeSign: {
x: number
y: number
}
subModification: Modification
}
>
const aspectRatio: ModifierModule = {
start(arg) {
const { state, rect, edges, pageCoords: coords } = arg
let { ratio, enabled } = state.options
const { equalDelta, modifiers } = state.options
if (ratio === 'preserve') {
ratio = rect.width / rect.height
}
state.startCoords = extend({}, coords)
state.startRect = extend({}, rect)
state.ratio = ratio
state.equalDelta = equalDelta
const linkedEdges = (state.linkedEdges = {
top: edges.top || (edges.left && !edges.bottom),
left: edges.left || (edges.top && !edges.right),
bottom: edges.bottom || (edges.right && !edges.top),
right: edges.right || (edges.bottom && !edges.left),
})
state.xIsPrimaryAxis = !!(edges.left || edges.right)
if (state.equalDelta) {
const sign = (linkedEdges.left ? 1 : -1) * (linkedEdges.top ? 1 : -1)
state.edgeSign = {
x: sign,
y: sign,
}
} else {
state.edgeSign = {
x: linkedEdges.left ? -1 : 1,
y: linkedEdges.top ? -1 : 1,
}
}
if (enabled !== false) {
extend(edges, linkedEdges)
}
if (!modifiers?.length) return
const subModification = new Modification(arg.interaction)
subModification.copyFrom(arg.interaction.modification)
subModification.prepareStates(modifiers)
state.subModification = subModification
subModification.startAll({ ...arg })
},
set(arg) {
const { state, rect, coords } = arg
const { linkedEdges } = state
const initialCoords = extend({}, coords)
const aspectMethod = state.equalDelta ? setEqualDelta : setRatio
extend(arg.edges, linkedEdges)
aspectMethod(state, state.xIsPrimaryAxis, coords, rect)
if (!state.subModification) {
return null
}
const correctedRect = extend({}, rect)
addEdges(linkedEdges, correctedRect, {
x: coords.x - initialCoords.x,
y: coords.y - initialCoords.y,
})
const result = state.subModification.setAll({
...arg,
rect: correctedRect,
edges: linkedEdges,
pageCoords: coords,
prevCoords: coords,
prevRect: correctedRect,
})
const { delta } = result
if (result.changed) {
const xIsCriticalAxis = Math.abs(delta.x) > Math.abs(delta.y)
// do aspect modification again with critical edge axis as primary
aspectMethod(state, xIsCriticalAxis, result.coords, result.rect)
extend(coords, result.coords)
}
return result.eventProps
},
defaults: {
ratio: 'preserve',
equalDelta: false,
modifiers: [],
enabled: false,
},
}
function setEqualDelta({ startCoords, edgeSign }: AspectRatioState, xIsPrimaryAxis: boolean, coords: Point) {
if (xIsPrimaryAxis) {
coords.y = startCoords.y + (coords.x - startCoords.x) * edgeSign.y
} else {
coords.x = startCoords.x + (coords.y - startCoords.y) * edgeSign.x
}
}
function setRatio(
{ startRect, startCoords, ratio, edgeSign }: AspectRatioState,
xIsPrimaryAxis: boolean,
coords: Point,
rect: Rect,
) {
if (xIsPrimaryAxis) {
const newHeight = rect.width / ratio
coords.y = startCoords.y + (newHeight - startRect.height) * edgeSign.y
} else {
const newWidth = rect.height * ratio
coords.x = startCoords.x + (newWidth - startRect.width) * edgeSign.x
}
}
export default makeModifier(aspectRatio, 'aspectRatio')
export { aspectRatio }
================================================
FILE: packages/@interactjs/modifiers/avoid/avoid.stub.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/avoid/avoid.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/base.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import type { ActionName, Element } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import modifiersBase from './base'
test('modifiers/base', () => {
const { scope, target, interaction, interactable, coords, event } = helpers.testEnv({
plugins: [modifiersBase],
})
// modifiers prop is added new Interaction
expect(is.object(interaction.modification)).toBe(true)
coords.client = coords.page
const testAction = { name: 'test' as ActionName }
const element = target as Element
const startCoords = { x: 100, y: 200 }
const moveCoords = { x: 400, y: 500 }
const options: any = { target: { x: 100, y: 100 }, setStart: true }
let firedEvents: any[] = []
interactable.rectChecker(() => ({ top: 0, left: 0, bottom: 50, right: 50 }))
interactable.on('teststart testmove testend', (e) => firedEvents.push(e))
extend(coords.page, startCoords)
interaction.pointerDown(event, event, element)
;(interactable.options as any).test = {
enabled: true,
modifiers: [
{
options,
methods: targetModifier,
},
],
}
interaction.start(testAction, interactable, element)
// modifier methods.start() was called
expect(options.started).toBe(true)
// modifier methods.set() was called
expect(options.setted).toBe(true)
// start event coords are modified
expect(interaction.prevEvent.page).toEqual(options.target)
// interaction.coords.start are restored after action start phase
expect(interaction.coords.start.page).toEqual(startCoords)
// interaction.coords.cur are restored after action start phase
expect(interaction.coords.cur.page).toEqual(startCoords)
extend(coords.page, moveCoords)
interaction.pointerMove(event, event, element)
// interaction.coords.cur are restored after action move phase
expect(interaction.coords.cur.page).toEqual(moveCoords)
// interaction.coords.start are restored after action move phase
expect(interaction.coords.start.page).toEqual(startCoords)
// move event start coords are modified
expect({ x: interaction.prevEvent.x0, y: interaction.prevEvent.y0 }).toEqual({ x: 100, y: 100 })
firedEvents = []
scope.interactions.pointerMoveTolerance = 0
interaction.pointerMove(event, event, element)
// duplicate result coords are ignored
expect(firedEvents).toHaveLength(0)
interaction.stop()
// modifier methods.stop() was called
expect(options.stopped).toBe(true)
// don't set start
options.setStart = null
// add second modifier
;(interactable.options as any).test.modifiers.push({
options,
methods: doubleModifier,
})
extend(coords.page, startCoords)
interaction.pointerDown(event, event, element)
interaction.start(testAction, interactable, element)
// modifier methods.set() was not called on start phase without options.setStart
expect(options.setted).toBeUndefined()
// start event coords are not modified without options.setStart
expect(interaction.prevEvent.page).toEqual({ x: 100, y: 200 })
// interaction.coords.start are not modified without options.setStart
expect(interaction.coords.start.page).toEqual({ x: 100, y: 200 })
extend(coords.page, moveCoords)
interaction.pointerMove(event, event, element)
// move event coords are modified by all modifiers
expect(interaction.prevEvent.page).toEqual({ x: 200, y: 200 })
interaction.pointerMove(event, event, element)
expect(() => {
interaction._scopeFire('interactions:action-resume', {
interaction,
phase: 'resume',
iEvent: {} as any,
event,
})
}).not.toThrow()
interaction.stop()
interaction.pointerUp(event, event, element, element)
// interaction coords after stopping are as expected
expect(interaction.coords.cur.page).toEqual(moveCoords)
})
const targetModifier = {
start({ state }: any) {
state.options.started = true
},
set({ state, coords }: any) {
const { target } = state.options
coords.x = target.x
coords.y = target.y
state.options.setted = true
},
stop({ state }: any) {
state.options.stopped = true
delete state.options.started
delete state.options.setted
},
}
const doubleModifier = {
start() {},
set({ coords }: any) {
coords.x *= 2
coords.y *= 2
},
}
================================================
FILE: packages/@interactjs/modifiers/base.ts
================================================
import type { InteractEvent } from '@interactjs/core/InteractEvent'
import type Interaction from '@interactjs/core/Interaction'
import type { Plugin } from '@interactjs/core/scope'
import { Modification } from './Modification'
import type { Modifier, ModifierModule, ModifierState } from './types'
declare module '@interactjs/core/Interaction' {
interface Interaction {
modification?: Modification
}
}
declare module '@interactjs/core/InteractEvent' {
interface InteractEvent {
modifiers?: Array<{
name: string
[key: string]: any
}>
}
}
declare module '@interactjs/core/options' {
interface PerActionDefaults {
modifiers?: Modifier[]
}
}
export function makeModifier<
Defaults extends { enabled?: boolean },
State extends ModifierState,
Name extends string,
Result,
>(module: ModifierModule, name?: Name) {
const { defaults } = module
const methods = {
start: module.start,
set: module.set,
beforeEnd: module.beforeEnd,
stop: module.stop,
}
const modifier = (_options?: Partial) => {
const options = (_options || {}) as Defaults
options.enabled = options.enabled !== false
// add missing defaults to options
for (const prop in defaults) {
if (!(prop in options)) {
;(options as any)[prop] = defaults[prop]
}
}
const m: Modifier = {
options,
methods,
name,
enable: () => {
options.enabled = true
return m
},
disable: () => {
options.enabled = false
return m
},
}
return m
}
if (name && typeof name === 'string') {
// for backwrads compatibility
modifier._defaults = defaults
modifier._methods = methods
}
return modifier
}
export function addEventModifiers({
iEvent,
interaction,
}: {
iEvent: InteractEvent
interaction: Interaction
}) {
const result = interaction.modification!.result
if (result) {
iEvent.modifiers = result.eventProps
}
}
const modifiersBase: Plugin = {
id: 'modifiers/base',
before: ['actions'],
install: (scope) => {
scope.defaults.perAction.modifiers = []
},
listeners: {
'interactions:new': ({ interaction }) => {
interaction.modification = new Modification(interaction)
},
'interactions:before-action-start': (arg) => {
const { interaction } = arg
const modification = arg.interaction.modification!
modification.start(arg, interaction.coords.start.page)
interaction.edges = modification.edges
modification.applyToInteraction(arg)
},
'interactions:before-action-move': (arg) => {
const { interaction } = arg
const { modification } = interaction
const ret = modification.setAndApply(arg)
interaction.edges = modification.edges
return ret
},
'interactions:before-action-end': (arg) => {
const { interaction } = arg
const { modification } = interaction
const ret = modification.beforeEnd(arg)
interaction.edges = modification.startEdges
return ret
},
'interactions:action-start': addEventModifiers,
'interactions:action-move': addEventModifiers,
'interactions:action-end': addEventModifiers,
'interactions:after-action-start': (arg) => arg.interaction.modification.restoreInteractionCoords(arg),
'interactions:after-action-move': (arg) => arg.interaction.modification.restoreInteractionCoords(arg),
'interactions:stop': (arg) => arg.interaction.modification.stop(arg),
},
}
export default modifiersBase
================================================
FILE: packages/@interactjs/modifiers/noop.ts
================================================
import type { ModifierFunction } from './types'
const noop = (() => {}) as unknown as ModifierFunction
noop._defaults = {}
export default noop
================================================
FILE: packages/@interactjs/modifiers/package.json
================================================
{
"name": "@interactjs/modifiers",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/modifiers"
},
"dependencies": {
"@interactjs/snappers": "1.10.27"
},
"peerDependencies": {
"@interactjs/core": "1.10.27",
"@interactjs/rebound": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/modifiers/plugin.ts
================================================
import type { Plugin } from '@interactjs/core/scope'
import snappers from '@interactjs/snappers/plugin'
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import './all'
import './base'
import all from './all'
import base from './base'
/* eslint-enable import/no-duplicates */
declare module '@interactjs/core/InteractStatic' {
export interface InteractStatic {
modifiers: typeof all
}
}
const modifiers: Plugin = {
id: 'modifiers',
install(scope) {
const { interactStatic: interact } = scope
scope.usePlugin(base)
scope.usePlugin(snappers)
interact.modifiers = all
// for backwrads compatibility
for (const type in all) {
const { _defaults, _methods } = all[type as keyof typeof all]
;(_defaults as any)._methods = _methods
;(scope.defaults.perAction as any)[type] = _defaults
}
},
}
export default modifiers
================================================
FILE: packages/@interactjs/modifiers/restrict/edges.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import { restrictEdges } from '../restrict/edges'
test('restrictEdges', () => {
const { interaction } = helpers.testEnv()
const edges = { top: true, bottom: true, left: true, right: true }
interaction.prepared = {} as any
interaction.prepared.edges = edges
interaction._rects = {} as any
interaction._rects.corrected = { x: 10, y: 20, width: 300, height: 200 } as any
interaction._interacting = true
const options: any = { enabled: true }
const coords = { x: 40, y: 40 }
const offset = { top: 0, left: 0, bottom: 0, right: 0 }
const state = { options, offset }
const arg = { interaction, edges, state } as any
arg.coords = { ...coords }
// outer restriction
options.outer = { top: 100, left: 100, bottom: 200, right: 200 }
restrictEdges.set(arg)
// outer restriction is applied correctly
expect(arg.coords).toEqual({ x: coords.y + 60, y: coords.y + 60 })
arg.coords = { ...coords }
// inner restriction
options.outer = null
options.inner = { top: 0, left: 0, bottom: 10, right: 10 }
restrictEdges.set(arg)
// inner restriction is applied correctly
expect(arg.coords).toEqual({ x: coords.x - 40, y: coords.y - 40 })
// offset
Object.assign(offset, {
top: 100,
left: 100,
bottom: 200,
right: 200,
})
arg.coords = { ...coords }
options.outer = { top: 100, left: 100, bottom: 200, right: 200 }
options.inner = null
restrictEdges.set(arg)
// outer restriction is applied correctly with offset
expect(arg.coords).toEqual({ x: coords.x + 160, y: coords.x + 160 })
// start
interaction.modification = {} as any
arg.startOffset = { top: 5, left: 10, bottom: -8, right: -16 }
interaction.interactable = {
getRect() {
return { top: 500, left: 900 }
},
} as any
options.offset = 'self'
restrictEdges.start(arg)
// start gets x/y from selector string
expect(arg.state.offset).toEqual({ top: 505, left: 910, bottom: 508, right: 916 })
})
================================================
FILE: packages/@interactjs/modifiers/restrict/edges.ts
================================================
// This modifier adds the options.resize.restrictEdges setting which sets min and
// max for the top, left, bottom and right edges of the target being resized.
//
// interact(target).resize({
// edges: { top: true, left: true },
// restrictEdges: {
// inner: { top: 200, left: 200, right: 400, bottom: 400 },
// outer: { top: 0, left: 0, right: 600, bottom: 600 },
// },
// })
import type { Point, Rect } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import * as rectUtils from '@interactjs/utils/rect'
import { makeModifier } from '../base'
import type { ModifierArg, ModifierState } from '../types'
import type { RestrictOptions } from './pointer'
import { getRestrictionRect } from './pointer'
export interface RestrictEdgesOptions {
inner: RestrictOptions['restriction']
outer: RestrictOptions['restriction']
offset?: RestrictOptions['offset']
endOnly: boolean
enabled?: boolean
}
export type RestrictEdgesState = ModifierState<
RestrictEdgesOptions,
{
inner: Rect
outer: Rect
offset: RestrictEdgesOptions['offset']
}
>
const noInner = { top: +Infinity, left: +Infinity, bottom: -Infinity, right: -Infinity }
const noOuter = { top: -Infinity, left: -Infinity, bottom: +Infinity, right: +Infinity }
function start({ interaction, startOffset, state }: ModifierArg) {
const { options } = state
let offset: Point
if (options) {
const offsetRect = getRestrictionRect(options.offset, interaction, interaction.coords.start.page)
offset = rectUtils.rectToXY(offsetRect)
}
offset = offset || { x: 0, y: 0 }
state.offset = {
top: offset.y + startOffset.top,
left: offset.x + startOffset.left,
bottom: offset.y - startOffset.bottom,
right: offset.x - startOffset.right,
}
}
function set({ coords, edges, interaction, state }: ModifierArg) {
const { offset, options } = state
if (!edges) {
return
}
const page = extend({}, coords)
const inner = getRestrictionRect(options.inner, interaction, page) || ({} as Rect)
const outer = getRestrictionRect(options.outer, interaction, page) || ({} as Rect)
fixRect(inner, noInner)
fixRect(outer, noOuter)
if (edges.top) {
coords.y = Math.min(Math.max(outer.top + offset.top, page.y), inner.top + offset.top)
} else if (edges.bottom) {
coords.y = Math.max(Math.min(outer.bottom + offset.bottom, page.y), inner.bottom + offset.bottom)
}
if (edges.left) {
coords.x = Math.min(Math.max(outer.left + offset.left, page.x), inner.left + offset.left)
} else if (edges.right) {
coords.x = Math.max(Math.min(outer.right + offset.right, page.x), inner.right + offset.right)
}
}
function fixRect(rect: Rect, defaults: Rect) {
for (const edge of ['top', 'left', 'bottom', 'right']) {
if (!(edge in rect)) {
rect[edge] = defaults[edge]
}
}
return rect
}
const defaults: RestrictEdgesOptions = {
inner: null,
outer: null,
offset: null,
endOnly: false,
enabled: false,
}
const restrictEdges = {
noInner,
noOuter,
start,
set,
defaults,
}
export default makeModifier(restrictEdges, 'restrictEdges')
export { restrictEdges }
================================================
FILE: packages/@interactjs/modifiers/restrict/pointer.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import { restrict } from '../restrict/pointer'
test('restrict larger than restriction', () => {
const edges = { left: 0, top: 0, right: 200, bottom: 200 }
const rect = { ...edges, width: 200, height: 200 }
const { interaction } = helpers.testEnv({ rect })
const restriction = { left: 100, top: 50, right: 150, bottom: 150 }
const options = {
...restrict.defaults,
restriction: null as any,
elementRect: { left: 0, top: 0, right: 1, bottom: 1 },
}
const state = { options, offset: null as any }
const arg: any = {
interaction,
state,
rect,
startOffset: rect,
coords: { x: 0, y: 0 },
pageCoords: { x: 0, y: 0 },
}
options.restriction = () => null as any
expect(() => {
restrict.start(arg as any)
restrict.set(arg as any)
}).not.toThrow()
options.restriction = restriction
restrict.start(arg as any)
arg.coords = { x: 0, y: 0 }
restrict.set(arg)
// allows top and left edge values to be lower than the restriction
expect(arg.coords).toEqual({ x: 0, y: 0 })
arg.coords = { x: restriction.left + 10, y: restriction.top + 10 }
restrict.set(arg)
// keeps the top left edge values lower than the restriction
expect(arg.coords).toEqual({ x: restriction.left - rect.left, y: restriction.top - rect.top })
arg.coords = { x: restriction.right - rect.right - 10, y: restriction.bottom - rect.right - 10 }
restrict.set(arg)
// keeps the bottom right edge values higher than the restriction
expect(arg.coords).toEqual({ x: restriction.right - rect.right, y: restriction.bottom - rect.right })
})
================================================
FILE: packages/@interactjs/modifiers/restrict/pointer.ts
================================================
import type Interaction from '@interactjs/core/Interaction'
import type { RectResolvable, Rect, Point } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import * as rectUtils from '@interactjs/utils/rect'
import { makeModifier } from '../base'
import type { ModifierArg, ModifierModule, ModifierState } from '../types'
export interface RestrictOptions {
// where to drag over
restriction: RectResolvable<[number, number, Interaction]>
// what part of self is allowed to drag over
elementRect: Rect
offset: Rect
// restrict just before the end drag
endOnly: boolean
enabled?: boolean
}
export type RestrictState = ModifierState<
RestrictOptions,
{
offset: Rect
}
>
function start({ rect, startOffset, state, interaction, pageCoords }: ModifierArg) {
const { options } = state
const { elementRect } = options
const offset: Rect = extend(
{
left: 0,
top: 0,
right: 0,
bottom: 0,
},
options.offset || {},
)
if (rect && elementRect) {
const restriction = getRestrictionRect(options.restriction, interaction, pageCoords)
if (restriction) {
const widthDiff = restriction.right - restriction.left - rect.width
const heightDiff = restriction.bottom - restriction.top - rect.height
if (widthDiff < 0) {
offset.left += widthDiff
offset.right += widthDiff
}
if (heightDiff < 0) {
offset.top += heightDiff
offset.bottom += heightDiff
}
}
offset.left += startOffset.left - rect.width * elementRect.left
offset.top += startOffset.top - rect.height * elementRect.top
offset.right += startOffset.right - rect.width * (1 - elementRect.right)
offset.bottom += startOffset.bottom - rect.height * (1 - elementRect.bottom)
}
state.offset = offset
}
function set({ coords, interaction, state }: ModifierArg) {
const { options, offset } = state
const restriction = getRestrictionRect(options.restriction, interaction, coords)
if (!restriction) return
const rect = rectUtils.xywhToTlbr(restriction)
coords.x = Math.max(Math.min(rect.right - offset.right, coords.x), rect.left + offset.left)
coords.y = Math.max(Math.min(rect.bottom - offset.bottom, coords.y), rect.top + offset.top)
}
export function getRestrictionRect(
value: RectResolvable<[number, number, Interaction]>,
interaction: Interaction,
coords?: Point,
) {
if (is.func(value)) {
return rectUtils.resolveRectLike(value, interaction.interactable, interaction.element, [
coords.x,
coords.y,
interaction,
])
} else {
return rectUtils.resolveRectLike(value, interaction.interactable, interaction.element)
}
}
const defaults: RestrictOptions = {
restriction: null,
elementRect: null,
offset: null,
endOnly: false,
enabled: false,
}
const restrict: ModifierModule = {
start,
set,
defaults,
}
export default makeModifier(restrict, 'restrict')
export { restrict }
================================================
FILE: packages/@interactjs/modifiers/restrict/rect.ts
================================================
import extend from '@interactjs/utils/extend'
import { makeModifier } from '../base'
import { restrict } from './pointer'
const defaults = extend(
{
get elementRect() {
return { top: 0, left: 0, bottom: 1, right: 1 }
},
set elementRect(_) {},
},
restrict.defaults,
)
const restrictRect = {
start: restrict.start,
set: restrict.set,
defaults,
}
export default makeModifier(restrictRect, 'restrictRect')
export { restrictRect }
================================================
FILE: packages/@interactjs/modifiers/restrict/size.spec.ts
================================================
import type { ResizeEvent } from '@interactjs/actions/resize/plugin'
import resize from '@interactjs/actions/resize/plugin'
import * as helpers from '@interactjs/core/tests/_helpers'
import extend from '@interactjs/utils/extend'
import * as rectUtils from '@interactjs/utils/rect'
import modifiersBase from '../base'
import restrictSize from './size'
test('restrictSize', () => {
const rect = rectUtils.xywhToTlbr({ left: 0, top: 0, right: 200, bottom: 300 })
const { interaction, interactable, coords, down, start, move } = helpers.testEnv({
plugins: [modifiersBase, resize],
rect,
})
const edges = { left: true, top: true }
const action: any = { name: 'resize', edges }
const options = {
min: { width: 60, height: 50 } as any,
max: { width: 300, height: 350 } as any,
}
let latestEvent: ResizeEvent = null
interactable
.resizable({
modifiers: [restrictSize(options)],
})
.on('resizestart resizemove resizeend', (e) => {
latestEvent = e
})
down()
start(action)
extend(coords.page, { x: -50, y: -40 })
move()
// within both min and max
expect(latestEvent.page).toEqual(coords.page)
extend(coords.page, { x: -200, y: -300 })
move()
// outside max
expect(latestEvent.page).toEqual({ x: -100, y: -50 })
extend(coords.page, { x: 250, y: 320 })
move()
// outside min
expect(latestEvent.page).toEqual({ x: 140, y: 250 })
// min and max function restrictions
let minFuncArgs: any[]
let maxFuncArgs: any[]
options.min = (...args: any[]) => {
minFuncArgs = args
}
options.max = (...args: any[]) => {
maxFuncArgs = args
}
move()
// correct args are passed to min function restriction
expect(minFuncArgs).toEqual([coords.page.x, coords.page.y, interaction])
// correct args are passed to max function restriction
expect(maxFuncArgs).toEqual([coords.page.x, coords.page.y, interaction])
})
================================================
FILE: packages/@interactjs/modifiers/restrict/size.ts
================================================
import type { Point, Rect, Size } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import * as rectUtils from '@interactjs/utils/rect'
import { makeModifier } from '../base'
import type { ModifierArg, ModifierState } from '../types'
import type { RestrictEdgesState } from './edges'
import { restrictEdges } from './edges'
import type { RestrictOptions } from './pointer'
import { getRestrictionRect } from './pointer'
const noMin = { width: -Infinity, height: -Infinity }
const noMax = { width: +Infinity, height: +Infinity }
export interface RestrictSizeOptions {
min?: Size | Point | RestrictOptions['restriction']
max?: Size | Point | RestrictOptions['restriction']
endOnly: boolean
enabled?: boolean
}
function start(arg: ModifierArg) {
return restrictEdges.start(arg)
}
export type RestrictSizeState = RestrictEdgesState &
ModifierState<
RestrictSizeOptions & { inner: Rect; outer: Rect },
{
min: Rect
max: Rect
}
>
function set(arg: ModifierArg) {
const { interaction, state, rect, edges } = arg
const { options } = state
if (!edges) {
return
}
const minSize =
rectUtils.tlbrToXywh(getRestrictionRect(options.min as any, interaction, arg.coords)) || noMin
const maxSize =
rectUtils.tlbrToXywh(getRestrictionRect(options.max as any, interaction, arg.coords)) || noMax
state.options = {
endOnly: options.endOnly,
inner: extend({}, restrictEdges.noInner),
outer: extend({}, restrictEdges.noOuter),
}
if (edges.top) {
state.options.inner.top = rect.bottom - minSize.height
state.options.outer.top = rect.bottom - maxSize.height
} else if (edges.bottom) {
state.options.inner.bottom = rect.top + minSize.height
state.options.outer.bottom = rect.top + maxSize.height
}
if (edges.left) {
state.options.inner.left = rect.right - minSize.width
state.options.outer.left = rect.right - maxSize.width
} else if (edges.right) {
state.options.inner.right = rect.left + minSize.width
state.options.outer.right = rect.left + maxSize.width
}
restrictEdges.set(arg)
state.options = options
}
const defaults: RestrictSizeOptions = {
min: null,
max: null,
endOnly: false,
enabled: false,
}
const restrictSize = {
start,
set,
defaults,
}
export default makeModifier(restrictSize, 'restrictSize')
export { restrictSize }
================================================
FILE: packages/@interactjs/modifiers/rubberband/rubberband.stub.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/rubberband/rubberband.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/snap/edges.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import type { EdgeOptions } from '@interactjs/core/types'
import { snapEdges } from '../snap/edges'
test('modifiers/snap/edges', () => {
const rect = { top: 0, left: 0, bottom: 100, right: 100 }
const { interaction, interactable } = helpers.testEnv({ rect })
interaction.interactable = interactable
interaction._interacting = true
const target0 = Object.freeze({
left: 50,
right: 150,
top: 0,
bottom: 100,
})
const options = {
targets: [{ ...target0 }],
range: Infinity,
}
const pageCoords = Object.freeze({ x: 0, y: 0 })
const arg = {
interaction,
// resize from top left
edges: { top: true, left: true } as EdgeOptions,
interactable: interaction.interactable,
state: null as any,
pageCoords,
coords: { ...pageCoords },
offset: [{ x: 0, y: 0 }],
}
arg.state = { options }
snapEdges.start!(arg as any)
snapEdges.set!(arg as any)
// modified coords are correct
expect(arg.coords).toEqual({ x: target0.left, y: target0.top })
// resize from bottom right
arg.edges = { bottom: true, right: true }
arg.state = { options }
snapEdges.start!(arg as any)
snapEdges.set!(arg as any)
// modified coord are correct
expect(arg.coords).toEqual({ x: target0.right, y: target0.bottom })
})
================================================
FILE: packages/@interactjs/modifiers/snap/edges.ts
================================================
/**
* @module modifiers/snapEdges
*
* @description
* This modifier allows snapping of the edges of targets during resize
* interactions.
*
* ```js
* interact(target).resizable({
* snapEdges: {
* targets: [interact.snappers.grid({ x: 100, y: 50 })],
* },
* })
*
* interact(target).resizable({
* snapEdges: {
* targets: [
* interact.snappers.grid({
* top: 50,
* left: 50,
* bottom: 100,
* right: 100,
* }),
* ],
* },
* })
* ```
*/
import clone from '@interactjs/utils/clone'
import extend from '@interactjs/utils/extend'
import { makeModifier } from '../base'
import type { ModifierArg, ModifierModule } from '../types'
import type { SnapOptions, SnapState } from './pointer'
import { snapSize } from './size'
export type SnapEdgesOptions = Pick
function start(arg: ModifierArg) {
const { edges } = arg
if (!edges) {
return null
}
arg.state.targetFields = arg.state.targetFields || [
[edges.left ? 'left' : 'right', edges.top ? 'top' : 'bottom'],
]
return snapSize.start(arg)
}
const snapEdges: ModifierModule> = {
start,
set: snapSize.set,
defaults: extend(clone(snapSize.defaults), {
targets: undefined,
range: undefined,
offset: { x: 0, y: 0 },
} as const),
}
export default makeModifier(snapEdges, 'snapEdges')
export { snapEdges }
================================================
FILE: packages/@interactjs/modifiers/snap/pointer.spec.ts
================================================
import drag from '@interactjs/actions/drag/plugin'
import * as helpers from '@interactjs/core/tests/_helpers'
import type { Point } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import modifiersBase from '../base'
import snap from '../snap/pointer'
test('modifiers/snap', () => {
const rect = helpers.ltrbwh(0, 0, 100, 100, 100, 100)
const { interaction, interactable, coords, down, move, start, stop } = helpers.testEnv({
plugins: [modifiersBase, drag],
rect,
})
coords.client = coords.page
const origin = { x: 120, y: 120 }
let funcArgs!: { x: number; y: number; offset: number; index: number; unexpected: unknown[] }
const target0 = Object.freeze({ x: 50, y: 100 })
const targetFunc = (x, y, _interaction, offset, index, ...unexpected) => {
funcArgs = { x, y, offset, index, unexpected }
return target0
}
const relativePoint = { x: 0, y: 0 }
const options = {
offset: undefined as Point | undefined,
offsetWithOrigin: true,
targets: [target0, targetFunc],
range: Infinity,
relativePoints: [relativePoint],
}
let lastEventModifiers!: any[]
interactable
.draggable({
origin,
modifiers: [snap(options)],
})
.on('dragmove dragstart dragend', (e) => {
lastEventModifiers = e.modifiers
})
down()
start({ name: 'drag' })
extend(coords.page, { x: 50, y: 50 })
move()
// event.modifiers entry has expected props
expect(Object.keys(lastEventModifiers[0]).sort()).toEqual([
'delta',
'distance',
'inRange',
'range',
'target',
])
// snaps to target and adds origin which will be subtracted by InteractEvent
expect(helpers.getProps(lastEventModifiers[0].target, ['x', 'y'])).toEqual({
x: target0.x + origin.x,
y: target0.y + origin.y,
})
options.targets = [targetFunc]
down()
start({ name: 'drag' })
move(true)
stop()
expect(funcArgs).toEqual({
x: coords.page.x - origin.x,
y: coords.page.y - origin.y,
offset: {
x: origin.x,
y: origin.y,
relativePoint,
index: 0,
},
index: 0,
// x, y, interaction, offset, index are passed to target function; origin subtracted from x, y
unexpected: [],
})
options.offset = { x: 300, y: 300 }
options.offsetWithOrigin = false
down()
start({ name: 'drag' })
move(true)
const { startOffset } = interaction.modification!
const relativeOffset = {
x: options.offset.x + startOffset.left,
y: options.offset.y + startOffset.top,
}
// event.modifiers entry has source element of options.targets array, range, and offset
expect(helpers.getProps(lastEventModifiers[0].target, ['source', 'range', 'offset'])).toEqual({
source: targetFunc,
range: Infinity,
offset: { ...relativeOffset, index: 0, relativePoint },
})
// origin not added to target when !options.offsetWithOrigin
expect(helpers.getProps(lastEventModifiers[0].target, ['x', 'y'])).toEqual({
x: target0.x + relativeOffset.x,
y: target0.y + relativeOffset.y,
})
// origin still subtracted from function target x, y args when !options.offsetWithOrigin
expect({ x: funcArgs.x, y: funcArgs.y }).toEqual({
x: coords.page.x - origin.x - relativeOffset.x,
y: coords.page.y - origin.y - relativeOffset.y,
})
})
================================================
FILE: packages/@interactjs/modifiers/snap/pointer.ts
================================================
import type { Interaction, InteractionProxy } from '@interactjs/core/Interaction'
import type { ActionName, Point, RectResolvable, Element } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import getOriginXY from '@interactjs/utils/getOriginXY'
import hypot from '@interactjs/utils/hypot'
import is from '@interactjs/utils/is'
import { resolveRectLike, rectToXY } from '@interactjs/utils/rect'
import { makeModifier } from '../base'
import type { ModifierArg, ModifierState } from '../types'
export interface Offset {
x: number
y: number
index: number
relativePoint?: Point | null
}
export interface SnapPosition {
x?: number
y?: number
range?: number
offset?: Offset
[index: string]: any
}
export type SnapFunction = (
x: number,
y: number,
interaction: InteractionProxy,
offset: Offset,
index: number,
) => SnapPosition
export type SnapTarget = SnapPosition | SnapFunction
export interface SnapOptions {
targets?: SnapTarget[]
// target range
range?: number
// self points for snapping. [0,0] = top left, [1,1] = bottom right
relativePoints?: Point[]
// startCoords = offset snapping from drag start page position
offset?: Point | RectResolvable<[Interaction]> | 'startCoords'
offsetWithOrigin?: boolean
origin?: RectResolvable<[Element]> | Point
endOnly?: boolean
enabled?: boolean
}
export type SnapState = ModifierState<
SnapOptions,
{
offsets?: Offset[]
closest?: any
targetFields?: string[][]
}
>
function start(arg: ModifierArg) {
const { interaction, interactable, element, rect, state, startOffset } = arg
const { options } = state
const origin = options.offsetWithOrigin ? getOrigin(arg) : { x: 0, y: 0 }
let snapOffset: Point
if (options.offset === 'startCoords') {
snapOffset = {
x: interaction.coords.start.page.x,
y: interaction.coords.start.page.y,
}
} else {
const offsetRect = resolveRectLike(options.offset as any, interactable, element, [interaction])
snapOffset = rectToXY(offsetRect) || { x: 0, y: 0 }
snapOffset.x += origin.x
snapOffset.y += origin.y
}
const { relativePoints } = options
state.offsets =
rect && relativePoints && relativePoints.length
? relativePoints.map((relativePoint, index) => ({
index,
relativePoint,
x: startOffset.left - rect.width * relativePoint.x + snapOffset.x,
y: startOffset.top - rect.height * relativePoint.y + snapOffset.y,
}))
: [
{
index: 0,
relativePoint: null,
x: snapOffset.x,
y: snapOffset.y,
},
]
}
function set(arg: ModifierArg) {
const { interaction, coords, state } = arg
const { options, offsets } = state
const origin = getOriginXY(interaction.interactable!, interaction.element!, interaction.prepared.name)
const page = extend({}, coords)
const targets: SnapPosition[] = []
if (!options.offsetWithOrigin) {
page.x -= origin.x
page.y -= origin.y
}
for (const offset of offsets!) {
const relativeX = page.x - offset.x
const relativeY = page.y - offset.y
for (let index = 0, len = options.targets!.length; index < len; index++) {
const snapTarget = options.targets![index]
let target: SnapPosition
if (is.func(snapTarget)) {
target = snapTarget(relativeX, relativeY, interaction._proxy, offset, index)
} else {
target = snapTarget
}
if (!target) {
continue
}
targets.push({
x: (is.number(target.x) ? target.x : relativeX) + offset.x,
y: (is.number(target.y) ? target.y : relativeY) + offset.y,
range: is.number(target.range) ? target.range : options.range,
source: snapTarget,
index,
offset,
})
}
}
const closest = {
target: null,
inRange: false,
distance: 0,
range: 0,
delta: { x: 0, y: 0 },
}
for (const target of targets) {
const range = target.range
const dx = target.x - page.x
const dy = target.y - page.y
const distance = hypot(dx, dy)
let inRange = distance <= range
// Infinite targets count as being out of range
// compared to non infinite ones that are in range
if (range === Infinity && closest.inRange && closest.range !== Infinity) {
inRange = false
}
if (
!closest.target ||
(inRange
? // is the closest target in range?
closest.inRange && range !== Infinity
? // the pointer is relatively deeper in this target
distance / range < closest.distance / closest.range
: // this target has Infinite range and the closest doesn't
(range === Infinity && closest.range !== Infinity) ||
// OR this target is closer that the previous closest
distance < closest.distance
: // The other is not in range and the pointer is closer to this target
!closest.inRange && distance < closest.distance)
) {
closest.target = target
closest.distance = distance
closest.range = range
closest.inRange = inRange
closest.delta.x = dx
closest.delta.y = dy
}
}
if (closest.inRange) {
coords.x = closest.target.x
coords.y = closest.target.y
}
state.closest = closest
return closest
}
function getOrigin(arg: Partial>) {
const { element } = arg.interaction
const optionsOrigin = rectToXY(resolveRectLike(arg.state.options.origin as any, null, null, [element]))
const origin = optionsOrigin || getOriginXY(arg.interactable, element, arg.interaction.prepared.name)
return origin
}
const defaults: SnapOptions = {
range: Infinity,
targets: null,
offset: null,
offsetWithOrigin: true,
origin: null,
relativePoints: null,
endOnly: false,
enabled: false,
}
const snap = {
start,
set,
defaults,
}
export default makeModifier(snap, 'snap')
export { snap }
================================================
FILE: packages/@interactjs/modifiers/snap/size.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import { snapSize } from '../snap/size'
test('modifiers/snapSize', () => {
const { interaction, interactable } = helpers.testEnv()
interaction.interactable = interactable
interactable.getRect = () => ({ top: 0, left: 0, bottom: 100, right: 100 }) as any
interaction._interacting = true
const target0 = Object.freeze({ x: 50, y: 100 })
const options = {
targets: [{ ...target0 }],
range: Infinity,
}
const state = {
options,
delta: { x: 0, y: 0 },
offset: [{ x: 0, y: 0 }],
}
const pageCoords = Object.freeze({ x: 10, y: 20 })
const arg = {
interaction,
interactable: interaction.interactable,
edges: { top: true, left: true },
state,
pageCoords,
coords: { ...pageCoords },
}
snapSize.start(arg as any)
snapSize.set(arg)
// snapSize.set single target, zereo offset
expect(arg.coords).toEqual(target0)
})
================================================
FILE: packages/@interactjs/modifiers/snap/size.ts
================================================
// This modifier allows snapping of the size of targets during resize
// interactions.
import extend from '@interactjs/utils/extend'
import is from '@interactjs/utils/is'
import { makeModifier } from '../base'
import type { ModifierArg } from '../types'
import type { SnapOptions, SnapState } from './pointer'
import { snap } from './pointer'
export type SnapSizeOptions = Pick
function start(arg: ModifierArg) {
const { state, edges } = arg
const { options } = state
if (!edges) {
return null
}
arg.state = {
options: {
targets: null,
relativePoints: [
{
x: edges.left ? 0 : 1,
y: edges.top ? 0 : 1,
},
],
offset: options.offset || 'self',
origin: { x: 0, y: 0 },
range: options.range,
},
}
state.targetFields = state.targetFields || [
['width', 'height'],
['x', 'y'],
]
snap.start(arg)
state.offsets = arg.state.offsets
arg.state = state
}
function set(arg) {
const { interaction, state, coords } = arg
const { options, offsets } = state
const relative = {
x: coords.x - offsets[0].x,
y: coords.y - offsets[0].y,
}
state.options = extend({}, options)
state.options.targets = []
for (const snapTarget of options.targets || []) {
let target
if (is.func(snapTarget)) {
target = snapTarget(relative.x, relative.y, interaction)
} else {
target = snapTarget
}
if (!target) {
continue
}
for (const [xField, yField] of state.targetFields) {
if (xField in target || yField in target) {
target.x = target[xField]
target.y = target[yField]
break
}
}
state.options.targets.push(target)
}
const returnValue = snap.set(arg)
state.options = options
return returnValue
}
const defaults: SnapSizeOptions = {
range: Infinity,
targets: null,
offset: null,
endOnly: false,
enabled: false,
}
const snapSize = {
start,
set,
defaults,
}
export default makeModifier(snapSize, 'snapSize')
export { snapSize }
================================================
FILE: packages/@interactjs/modifiers/spring/spring.stub.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/spring/spring.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/transform/transform.stub.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/transform/transform.ts
================================================
export { default } from '../noop'
================================================
FILE: packages/@interactjs/modifiers/types.ts
================================================
import type { Interactable } from '@interactjs/core/Interactable'
import type { EventPhase } from '@interactjs/core/InteractEvent'
import type Interaction from '@interactjs/core/Interaction'
import type { EdgeOptions, FullRect, Point, Rect } from '@interactjs/core/types'
export interface Modifier<
Defaults = any,
State extends ModifierState = any,
Name extends string = any,
Result = any,
> {
options: Defaults
methods: {
start?: (arg: ModifierArg) => void
set?: (arg: ModifierArg) => Result
beforeEnd?: (arg: ModifierArg) => Point | void
stop?: (arg: ModifierArg) => void
}
name?: Name
enable: () => Modifier
disable: () => Modifier
}
export type ModifierState = {
options: Defaults
methods?: Modifier['methods']
index?: number
name?: Name
} & StateProps
export interface ModifierArg {
interaction: Interaction
interactable: Interactable
phase: EventPhase
rect: FullRect
edges: EdgeOptions
state: State
element: Element
pageCoords: Point
prevCoords: Point
prevRect?: FullRect
coords: Point
startOffset: Rect
preEnd?: boolean
}
export interface ModifierModule<
Defaults extends { enabled?: boolean },
State extends ModifierState,
Result = unknown,
> {
defaults?: Defaults
start?(arg: ModifierArg): void
set?(arg: ModifierArg): Result
beforeEnd?(arg: ModifierArg): Point | void
stop?(arg: ModifierArg): void
}
export interface ModifierFunction<
Defaults extends { enabled?: boolean },
State extends ModifierState,
Name extends string,
> {
(_options?: Partial): Modifier
_defaults: Defaults
_methods: ModifierModule
}
================================================
FILE: packages/@interactjs/offset/offset.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import offset from './plugin'
test('plugins/spring', () => {
const { interaction, event, coords, target } = helpers.testEnv({ plugins: [offset] })
const body = target as HTMLBodyElement
interaction.pointerMove(event, event, body)
interaction.offsetBy({ x: 100, y: 100 })
interaction.pointerMove(event, event, body)
// coords are not updated when pointer is not down
expect(interaction.coords.cur.page).toEqual(coords.page)
interaction.pointerUp(event, event, body, body)
interaction.stop()
interaction.pointerDown(event, event, body)
interaction.offsetBy({ x: 100, y: 50 })
interaction.pointerMove(event, event, body)
// coords are not updated when pointer is not down
expect(interaction.coords.cur.page).toEqual({ x: coords.page.x + 100, y: coords.page.y + 50 })
})
================================================
FILE: packages/@interactjs/offset/package.json
================================================
{
"name": "@interactjs/offset",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/offset"
},
"peerDependencies": {
"@interactjs/core": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/offset/plugin.ts
================================================
import type Interaction from '@interactjs/core/Interaction'
import { _ProxyMethods } from '@interactjs/core/Interaction'
import type { Plugin } from '@interactjs/core/scope'
import type { Point } from '@interactjs/core/types'
import * as rectUtils from '@interactjs/utils/rect'
declare module '@interactjs/core/Interaction' {
interface Interaction {
offsetBy?: typeof offsetBy
offset: {
total: Point
pending: Point
}
}
enum _ProxyMethods {
offsetBy = '',
}
}
;(_ProxyMethods as any).offsetBy = ''
export function addTotal(interaction: Interaction) {
if (!interaction.pointerIsDown) {
return
}
addToCoords(interaction.coords.cur, interaction.offset.total)
interaction.offset.pending.x = 0
interaction.offset.pending.y = 0
}
function beforeAction({ interaction }: { interaction: Interaction }) {
applyPending(interaction)
}
function beforeEnd({ interaction }: { interaction: Interaction }): boolean | void {
const hadPending = applyPending(interaction)
if (!hadPending) return
interaction.move({ offset: true })
interaction.end()
return false
}
function end({ interaction }: { interaction: Interaction }) {
interaction.offset.total.x = 0
interaction.offset.total.y = 0
interaction.offset.pending.x = 0
interaction.offset.pending.y = 0
}
export function applyPending(interaction: Interaction) {
if (!hasPending(interaction)) {
return false
}
const { pending } = interaction.offset
addToCoords(interaction.coords.cur, pending)
addToCoords(interaction.coords.delta, pending)
rectUtils.addEdges(interaction.edges, interaction.rect, pending)
pending.x = 0
pending.y = 0
return true
}
function offsetBy(this: Interaction, { x, y }: Point) {
this.offset.pending.x += x
this.offset.pending.y += y
this.offset.total.x += x
this.offset.total.y += y
}
function addToCoords({ page, client }, { x, y }: Point) {
page.x += x
page.y += y
client.x += x
client.y += y
}
function hasPending(interaction: Interaction) {
return !!(interaction.offset.pending.x || interaction.offset.pending.y)
}
const offset: Plugin = {
id: 'offset',
before: ['modifiers', 'pointer-events', 'actions', 'inertia'],
install(scope) {
scope.Interaction.prototype.offsetBy = offsetBy
},
listeners: {
'interactions:new': ({ interaction }) => {
interaction.offset = {
total: { x: 0, y: 0 },
pending: { x: 0, y: 0 },
}
},
'interactions:update-pointer': ({ interaction }) => addTotal(interaction),
'interactions:before-action-start': beforeAction,
'interactions:before-action-move': beforeAction,
'interactions:before-action-end': beforeEnd,
'interactions:stop': end,
},
}
export default offset
================================================
FILE: packages/@interactjs/pointer-events/PointerEvent.spec.ts
================================================
import * as helpers from '@interactjs/core/tests/_helpers'
import * as pointerUtils from '@interactjs/utils/pointerUtils'
import { PointerEvent } from './PointerEvent'
test('PointerEvent constructor', () => {
const type = 'TEST_EVENT'
const pointerId = -100
const testPointerProp = ['TEST_POINTER_PROP']
const pointer = {
pointerId,
testPointerProp,
pointerType: 'TEST_POINTER_TYPE',
} as any
const testEventProp = ['TEST_EVENT_PROP']
const event = {
testEventProp,
} as any
const { interaction } = helpers.testEnv()
const eventTarget = {} as Element
const pointerEvent = new PointerEvent(type, pointer, event, eventTarget, interaction as any, 0) as any
// pointerEvent is extended form pointer
expect(pointerEvent.testPointerProp).toBe(testPointerProp)
// pointerEvent is extended form Event
expect(pointerEvent.testEventProp).toBe(testEventProp)
// type is set correctly
expect(pointerEvent.type).toBe(type)
// pointerType is set correctly
expect(pointerEvent.pointerType).toBe(pointerUtils.getPointerType(pointer))
// pointerId is set correctly
expect(pointerEvent.pointerId).toBe(pointerId)
// originalEvent is set correctly
expect(pointerEvent.originalEvent).toBe(event)
// interaction is set correctly
expect(pointerEvent.interaction).toBe(interaction._proxy)
// target is set correctly
expect(pointerEvent.target).toBe(eventTarget)
// currentTarget is null
expect(pointerEvent.currentTarget).toBeNull()
})
test('PointerEvent methods', () => {
const methodContexts = {} as any
const event: any = ['preventDefault', 'stopPropagation', 'stopImmediatePropagation'].reduce(
(acc, methodName) => {
acc[methodName] = function () {
methodContexts[methodName] = this
}
return acc
},
helpers.newPointer(),
)
const pointerEvent = new PointerEvent('TEST', {} as any, event, null, {} as any, 0)
pointerEvent.preventDefault()
// PointerEvent.preventDefault() calls preventDefault of originalEvent
expect(methodContexts.preventDefault).toBe(event)
// propagationStopped is false before call to stopPropagation
expect(pointerEvent.propagationStopped).toBe(false)
pointerEvent.stopPropagation()
// stopPropagation sets propagationStopped to true
expect(pointerEvent.propagationStopped).toBe(true)
// PointerEvent.stopPropagation() does not call stopPropagation of originalEvent
// immediatePropagationStopped is false before call to stopImmediatePropagation
expect(methodContexts.stopPropagation).toBeUndefined()
expect(pointerEvent.immediatePropagationStopped).toBe(false)
pointerEvent.stopImmediatePropagation()
// PointerEvent.stopImmediatePropagation() does not call stopImmediatePropagation of originalEvent
expect(methodContexts.stopImmediatePropagation).toBeUndefined()
// stopImmediatePropagation sets immediatePropagationStopped to true
expect(pointerEvent.immediatePropagationStopped).toBe(true)
const origin = { x: 20, y: 30 }
pointerEvent._subtractOrigin(origin)
// subtractOrigin updates pageX correctly
expect(pointerEvent.pageX).toBe(event.pageX - origin.x)
// subtractOrigin updates pageY correctly
expect(pointerEvent.pageY).toBe(event.pageY - origin.y)
// subtractOrigin updates clientX correctly
expect(pointerEvent.clientX).toBe(event.clientX - origin.x)
// subtractOrigin updates clientY correctly
expect(pointerEvent.clientY).toBe(event.clientY - origin.y)
pointerEvent._addOrigin(origin)
// addOrigin with the subtracted origin reverts to original coordinates
expect(['pageX', 'pageY', 'clientX', 'clientY'].every((prop) => pointerEvent[prop] === event[prop])).toBe(
true,
)
})
================================================
FILE: packages/@interactjs/pointer-events/PointerEvent.ts
================================================
import { BaseEvent } from '@interactjs/core/BaseEvent'
import type Interaction from '@interactjs/core/Interaction'
import type { PointerEventType, PointerType, Point } from '@interactjs/core/types'
import * as pointerUtils from '@interactjs/utils/pointerUtils'
export class PointerEvent extends BaseEvent {
declare type: T
declare originalEvent: PointerEventType
declare pointerId: number
declare pointerType: string
declare double: boolean
declare pageX: number
declare pageY: number
declare clientX: number
declare clientY: number
declare dt: number
declare eventable: any;
[key: string]: any
constructor(
type: T,
pointer: PointerType | PointerEvent,
event: PointerEventType,
eventTarget: Node,
interaction: Interaction,
timeStamp: number,
) {
super(interaction)
pointerUtils.pointerExtend(this, event)
if (event !== pointer) {
pointerUtils.pointerExtend(this, pointer)
}
this.timeStamp = timeStamp
this.originalEvent = event
this.type = type
this.pointerId = pointerUtils.getPointerId(pointer)
this.pointerType = pointerUtils.getPointerType(pointer)
this.target = eventTarget
this.currentTarget = null
if (type === 'tap') {
const pointerIndex = interaction.getPointerIndex(pointer)
this.dt = this.timeStamp - interaction.pointers[pointerIndex].downTime
const interval = this.timeStamp - interaction.tapTime
this.double =
!!interaction.prevTap &&
interaction.prevTap.type !== 'doubletap' &&
interaction.prevTap.target === this.target &&
interval < 500
} else if (type === 'doubletap') {
this.dt = (pointer as PointerEvent<'tap'>).timeStamp - interaction.tapTime
this.double = true
}
}
_subtractOrigin({ x: originX, y: originY }: Point) {
this.pageX -= originX
this.pageY -= originY
this.clientX -= originX
this.clientY -= originY
return this
}
_addOrigin({ x: originX, y: originY }: Point) {
this.pageX += originX
this.pageY += originY
this.clientX += originX
this.clientY += originY
return this
}
/**
* Prevent the default behaviour of the original Event
*/
preventDefault() {
this.originalEvent.preventDefault()
}
}
================================================
FILE: packages/@interactjs/pointer-events/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/pointer-events/base.spec.ts
================================================
import { Eventable } from '@interactjs/core/Eventable'
import type { Scope } from '@interactjs/core/scope'
import * as helpers from '@interactjs/core/tests/_helpers'
import type { PointerEventType, PointerType } from '@interactjs/core/types'
import type { EventTargetList } from './base'
import pointerEvents from './base'
import interactableTargets from './interactableTargets'
test('pointerEvents.types', () => {
expect(pointerEvents.types).toEqual({
down: true,
move: true,
up: true,
cancel: true,
tap: true,
doubletap: true,
hold: true,
})
})
test('pointerEvents.fire', () => {
const { scope, interaction, event, coords } = helpers.testEnv({ plugins: [pointerEvents] })
const eventable = new Eventable(pointerEvents.defaults)
const type = 'TEST'
const element = {}
const eventTarget = {}
const TEST_PROP = ['TEST_PROP']
let firedEvent: any
const targets: EventTargetList = [
{
eventable,
node: element as Node,
props: {
TEST_PROP,
},
},
]
eventable.on(type, (e) => {
firedEvent = e
})
pointerEvents.fire(
{
type,
eventTarget,
pointer: {},
event: {},
interaction: {},
targets,
} as any,
scope,
)
// Fired event is an instance of pointerEvents.PointerEvent
expect(firedEvent instanceof pointerEvents.PointerEvent).toBe(true)
// Fired event type is correct
expect(firedEvent.type).toBe(type)
// Fired event currentTarget is correct
expect(firedEvent.currentTarget).toBe(element)
// Fired event target is correct
expect(firedEvent.target).toBe(eventTarget)
// Fired event has props from target.props
expect(firedEvent.TEST_PROP).toBe(TEST_PROP)
scope.now = () => coords.timeStamp
coords.timeStamp = 0
interaction.pointerDown(event, event, scope.document)
coords.timeStamp = 500
interaction.pointerUp(event, event, scope.document, scope.document)
// interaction.tapTime is updated
expect(interaction.tapTime).toBe(500)
// interaction.prevTap is updated
expect(interaction.prevTap.type).toBe('tap')
})
test('pointerEvents.collectEventTargets', () => {
const { scope, interaction } = helpers.testEnv()
const type = 'TEST'
const TEST_PROP = ['TEST_PROP']
const target = {
node: {} as Node,
props: { TEST_PROP },
eventable: new Eventable(pointerEvents.defaults),
}
let collectedTargets: EventTargetList
function onCollect({ targets }: { targets?: EventTargetList }) {
targets.push(target)
collectedTargets = targets
}
scope.addListeners({
'pointerEvents:collect-targets': onCollect,
})
pointerEvents.collectEventTargets(
{
interaction,
pointer: {},
event: {},
eventTarget: {},
type,
} as any,
scope,
)
expect(collectedTargets).toEqual([target])
})
test('pointerEvents Interaction update-pointer signal', () => {
const scope: Scope = helpers.mockScope()
scope.usePlugin(pointerEvents)
const interaction = scope.interactions.new({})
const initialHold = { duration: Infinity, timeout: null as number }
const event = {} as PointerEventType
interaction.updatePointer(helpers.newPointer(0), event, null, false)
// set hold info for move on new pointer
expect(interaction.pointers.map((p) => p.hold)).toEqual([initialHold])
interaction.removePointer(helpers.newPointer(0), event)
interaction.updatePointer(helpers.newPointer(0), event, null, true)
expect(interaction.pointers.map((p) => p.hold)).toEqual([initialHold])
interaction.updatePointer(helpers.newPointer(5), event, null, true)
expect(interaction.pointers.map((p) => p.hold)).toEqual([initialHold, initialHold])
})
test('pointerEvents Interaction remove-pointer signal', () => {
const scope: Scope = helpers.mockScope()
scope.usePlugin(pointerEvents)
const interaction = scope.interactions.new({})
const ids = [0, 1, 2, 3]
const removals = [
{ id: 0, remain: [1, 2, 3], message: 'first of 4' },
{ id: 2, remain: [1, 3], message: 'middle of 3' },
{ id: 3, remain: [1], message: 'last of 2' },
{ id: 1, remain: [], message: 'final' },
]
for (const id of ids) {
const index = interaction.updatePointer(
{ pointerId: id } as PointerType,
{} as PointerEventType,
null,
true,
)
// use the ids as the pointerInfo.hold value for this test
interaction.pointers[index].hold = id as any
}
for (const removal of removals) {
interaction.removePointer({ pointerId: removal.id } as any, null)
// `${removal.message} - remaining interaction.pointers[i].hold are correct`
expect(interaction.pointers.map((p) => p.hold as unknown as number)).toEqual(removal.remain)
}
})
test('pointerEvents down hold up tap', async () => {
const { interaction, event, interactable } = helpers.testEnv({
plugins: [pointerEvents, interactableTargets],
})
const fired: PointerEvent[] = []
for (const type in pointerEvents.types) {
interactable.on(type, (e) => fired.push(e))
}
interaction.pointerDown(event, event, event.target)
interaction.pointerMove(event, event, event.target)
// duplicate move event is not fired
expect(fired.map((e) => e.type)).toEqual(['down'])
const holdTimer = interaction.pointers[0].hold
// hold timeout is set
expect(holdTimer.timeout).toBeTruthy()
await helpers.timeout(holdTimer.duration)
interaction.pointerUp(event, event, event.target, event.target)
// tap event is fired after down, hold and up events
expect(fired.map((e) => e.type)).toEqual(['down', 'hold', 'up', 'tap'])
})
================================================
FILE: packages/@interactjs/pointer-events/base.ts
================================================
import type { Eventable } from '@interactjs/core/Eventable'
import type { Interaction } from '@interactjs/core/Interaction'
import type { PerActionDefaults } from '@interactjs/core/options'
import type { Scope, SignalArgs, Plugin } from '@interactjs/core/scope'
import type { Point, PointerType, PointerEventType, Element } from '@interactjs/core/types'
import * as domUtils from '@interactjs/utils/domUtils'
import extend from '@interactjs/utils/extend'
import getOriginXY from '@interactjs/utils/getOriginXY'
import { PointerEvent } from './PointerEvent'
export type EventTargetList = Array<{
node: Node
eventable: Eventable
props: { [key: string]: any }
}>
export interface PointerEventOptions extends PerActionDefaults {
enabled?: undefined // not used
holdDuration?: number
ignoreFrom?: any
allowFrom?: any
origin?: Point | string | Element
}
declare module '@interactjs/core/scope' {
interface Scope {
pointerEvents: typeof pointerEvents
}
}
declare module '@interactjs/core/Interaction' {
interface Interaction {
prevTap?: PointerEvent
tapTime?: number
}
}
declare module '@interactjs/core/PointerInfo' {
interface PointerInfo {
hold?: {
duration: number
timeout: any
}
}
}
declare module '@interactjs/core/options' {
interface ActionDefaults {
pointerEvents: Options
}
}
declare module '@interactjs/core/scope' {
interface SignalArgs {
'pointerEvents:new': { pointerEvent: PointerEvent }
'pointerEvents:fired': {
interaction: Interaction
pointer: PointerType | PointerEvent
event: PointerEventType | PointerEvent
eventTarget: Node
pointerEvent: PointerEvent
targets?: EventTargetList
type: string
}
'pointerEvents:collect-targets': {
interaction: Interaction
pointer: PointerType | PointerEvent
event: PointerEventType | PointerEvent
eventTarget: Node
targets?: EventTargetList
type: string
path: Node[]
node: null
}
}
}
const defaults: PointerEventOptions = {
holdDuration: 600,
ignoreFrom: null,
allowFrom: null,
origin: { x: 0, y: 0 },
}
const pointerEvents: Plugin = {
id: 'pointer-events/base',
before: ['inertia', 'modifiers', 'auto-start', 'actions'],
install,
listeners: {
'interactions:new': addInteractionProps,
'interactions:update-pointer': addHoldInfo,
'interactions:move': moveAndClearHold,
'interactions:down': (arg, scope) => {
downAndStartHold(arg, scope)
fire(arg, scope)
},
'interactions:up': (arg, scope) => {
clearHold(arg)
fire(arg, scope)
tapAfterUp(arg, scope)
},
'interactions:cancel': (arg, scope) => {
clearHold(arg)
fire(arg, scope)
},
},
PointerEvent,
fire,
collectEventTargets,
defaults,
types: {
down: true,
move: true,
up: true,
cancel: true,
tap: true,
doubletap: true,
hold: true,
} as { [type: string]: true },
}
function fire(
arg: {
pointer: PointerType | PointerEvent
event: PointerEventType | PointerEvent
eventTarget: Node
interaction: Interaction
type: T
targets?: EventTargetList
},
scope: Scope,
) {
const { interaction, pointer, event, eventTarget, type, targets = collectEventTargets(arg, scope) } = arg
const pointerEvent = new PointerEvent(type, pointer, event, eventTarget, interaction, scope.now())
scope.fire('pointerEvents:new', { pointerEvent })
const signalArg = {
interaction,
pointer,
event,
eventTarget,
targets,
type,
pointerEvent,
}
for (let i = 0; i < targets.length; i++) {
const target = targets[i]
for (const prop in target.props || {}) {
;(pointerEvent as any)[prop] = target.props[prop]
}
const origin = getOriginXY(target.eventable, target.node)
pointerEvent._subtractOrigin(origin)
pointerEvent.eventable = target.eventable
pointerEvent.currentTarget = target.node
target.eventable.fire(pointerEvent)
pointerEvent._addOrigin(origin)
if (
pointerEvent.immediatePropagationStopped ||
(pointerEvent.propagationStopped &&
i + 1 < targets.length &&
targets[i + 1].node !== pointerEvent.currentTarget)
) {
break
}
}
scope.fire('pointerEvents:fired', signalArg)
if (type === 'tap') {
// if pointerEvent should make a double tap, create and fire a doubletap
// PointerEvent and use that as the prevTap
const prevTap = pointerEvent.double
? fire(
{
interaction,
pointer,
event,
eventTarget,
type: 'doubletap',
},
scope,
)
: pointerEvent
interaction.prevTap = prevTap
interaction.tapTime = prevTap.timeStamp
}
return pointerEvent
}
function collectEventTargets(
{
interaction,
pointer,
event,
eventTarget,
type,
}: {
interaction: Interaction
pointer: PointerType | PointerEvent
event: PointerEventType | PointerEvent
eventTarget: Node
type: T
},
scope: Scope,
) {
const pointerIndex = interaction.getPointerIndex(pointer)
const pointerInfo = interaction.pointers[pointerIndex]
// do not fire a tap event if the pointer was moved before being lifted
if (
type === 'tap' &&
(interaction.pointerWasMoved ||
// or if the pointerup target is different to the pointerdown target
!(pointerInfo && pointerInfo.downTarget === eventTarget))
) {
return []
}
const path = domUtils.getPath(eventTarget as Element | Document)
const signalArg = {
interaction,
pointer,
event,
eventTarget,
type,
path,
targets: [] as EventTargetList,
node: null,
}
for (const node of path) {
signalArg.node = node
scope.fire('pointerEvents:collect-targets', signalArg)
}
if (type === 'hold') {
signalArg.targets = signalArg.targets.filter(
(target) =>
target.eventable.options.holdDuration === interaction.pointers[pointerIndex]?.hold?.duration,
)
}
return signalArg.targets
}
function addInteractionProps({ interaction }) {
interaction.prevTap = null // the most recent tap event on this interaction
interaction.tapTime = 0 // time of the most recent tap event
}
function addHoldInfo({ down, pointerInfo }: SignalArgs['interactions:update-pointer']) {
if (!down && pointerInfo.hold) {
return
}
pointerInfo.hold = { duration: Infinity, timeout: null }
}
function clearHold({ interaction, pointerIndex }) {
const hold = interaction.pointers[pointerIndex].hold
if (hold && hold.timeout) {
clearTimeout(hold.timeout)
hold.timeout = null
}
}
function moveAndClearHold(arg: SignalArgs['interactions:move'], scope: Scope) {
const { interaction, pointer, event, eventTarget, duplicate } = arg
if (!duplicate && (!interaction.pointerIsDown || interaction.pointerWasMoved)) {
if (interaction.pointerIsDown) {
clearHold(arg)
}
fire(
{
interaction,
pointer,
event,
eventTarget: eventTarget as Element,
type: 'move',
},
scope,
)
}
}
function downAndStartHold(
{ interaction, pointer, event, eventTarget, pointerIndex }: SignalArgs['interactions:down'],
scope: Scope,
) {
const timer = interaction.pointers[pointerIndex].hold!
const path = domUtils.getPath(eventTarget as Element | Document)
const signalArg = {
interaction,
pointer,
event,
eventTarget,
type: 'hold',
targets: [] as EventTargetList,
path,
node: null,
}
for (const node of path) {
signalArg.node = node
scope.fire('pointerEvents:collect-targets', signalArg)
}
if (!signalArg.targets.length) return
let minDuration = Infinity
for (const target of signalArg.targets) {
const holdDuration = target.eventable.options.holdDuration
if (holdDuration < minDuration) {
minDuration = holdDuration
}
}
timer.duration = minDuration
timer.timeout = setTimeout(() => {
fire(
{
interaction,
eventTarget,
pointer,
event,
type: 'hold',
},
scope,
)
}, minDuration)
}
function tapAfterUp(
{ interaction, pointer, event, eventTarget }: SignalArgs['interactions:up'],
scope: Scope,
) {
if (!interaction.pointerWasMoved) {
fire({ interaction, eventTarget, pointer, event, type: 'tap' }, scope)
}
}
function install(scope: Scope) {
scope.pointerEvents = pointerEvents
scope.defaults.actions.pointerEvents = pointerEvents.defaults
extend(scope.actions.phaselessTypes, pointerEvents.types)
}
export default pointerEvents
================================================
FILE: packages/@interactjs/pointer-events/holdRepeat.spec.ts
================================================
import { Eventable } from '@interactjs/core/Eventable'
import * as helpers from '@interactjs/core/tests/_helpers'
import holdRepeat from './holdRepeat'
test('holdRepeat count', () => {
const pointerEvent = {
type: 'hold',
count: 0,
}
const { scope } = helpers.testEnv({ plugins: [holdRepeat] })
scope.fire('pointerEvents:new', { pointerEvent } as any)
// first hold count is 1 with count previously undefined
expect(pointerEvent.count).toBe(1)
const count = 20
pointerEvent.count = count
scope.fire('pointerEvents:new', { pointerEvent } as any)
// existing hold count is incremented
expect(pointerEvent.count).toBe(count + 1)
})
test('holdRepeat onFired', () => {
const { scope, interaction } = helpers.testEnv({ plugins: [holdRepeat] })
const pointerEvent = {
type: 'hold',
}
const eventTarget = {}
const eventable = new Eventable(
Object.assign({}, scope.pointerEvents.defaults, {
holdRepeatInterval: 0,
}),
)
const signalArg = {
interaction,
pointerEvent,
eventTarget,
targets: [
{
eventable,
},
],
}
scope.fire('pointerEvents:fired', signalArg as any)
// interaction interval handle was not saved with 0 holdRepeatInterval
expect('holdIntervalHandle' in interaction).toBe(false)
eventable.options.holdRepeatInterval = 10
scope.fire('pointerEvents:fired', signalArg as any)
// interaction interval handle was saved with interval > 0
expect('holdIntervalHandle' in interaction).toBe(true)
clearInterval(interaction.holdIntervalHandle)
pointerEvent.type = 'NOT_HOLD'
delete interaction.holdIntervalHandle
scope.fire('pointerEvents:fired', signalArg as any)
// interaction interval handle is not saved if pointerEvent.type is not "hold"
expect('holdIntervalHandle' in interaction).toBe(false)
})
================================================
FILE: packages/@interactjs/pointer-events/holdRepeat.ts
================================================
import type Interaction from '@interactjs/core/Interaction'
import type { ListenerMap, Scope, SignalArgs, Plugin } from '@interactjs/core/scope'
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import './base'
import basePlugin from './base'
/* eslint-enable import/no-duplicates */
import { type PointerEvent } from './PointerEvent'
declare module '@interactjs/core/Interaction' {
interface Interaction {
holdIntervalHandle?: any
}
}
declare module '@interactjs/pointer-events/PointerEvent' {
interface PointerEvent {
count?: number
}
}
declare module '@interactjs/pointer-events/base' {
interface PointerEventOptions {
holdRepeatInterval?: number
}
}
function install(scope: Scope) {
scope.usePlugin(basePlugin)
const { pointerEvents } = scope
// don't repeat by default
pointerEvents.defaults.holdRepeatInterval = 0
pointerEvents.types.holdrepeat = scope.actions.phaselessTypes.holdrepeat = true
}
function onNew({ pointerEvent }: { pointerEvent: PointerEvent }) {
if (pointerEvent.type !== 'hold') return
pointerEvent.count = (pointerEvent.count || 0) + 1
}
function onFired(
{ interaction, pointerEvent, eventTarget, targets }: SignalArgs['pointerEvents:fired'],
scope: Scope,
) {
if (pointerEvent.type !== 'hold' || !targets.length) return
// get the repeat interval from the first eventable
const interval = targets[0].eventable.options.holdRepeatInterval
// don't repeat if the interval is 0 or less
if (interval <= 0) return
// set a timeout to fire the holdrepeat event
interaction.holdIntervalHandle = setTimeout(() => {
scope.pointerEvents.fire(
{
interaction,
eventTarget,
type: 'hold',
pointer: pointerEvent,
event: pointerEvent,
},
scope,
)
}, interval)
}
function endHoldRepeat({ interaction }: { interaction: Interaction }) {
// set the interaction's holdStopTime property
// to stop further holdRepeat events
if (interaction.holdIntervalHandle) {
clearInterval(interaction.holdIntervalHandle)
interaction.holdIntervalHandle = null
}
}
const holdRepeat: Plugin = {
id: 'pointer-events/holdRepeat',
install,
listeners: ['move', 'up', 'cancel', 'endall'].reduce(
(acc, enderTypes) => {
;(acc as any)[`pointerEvents:${enderTypes}`] = endHoldRepeat
return acc
},
{
'pointerEvents:new': onNew,
'pointerEvents:fired': onFired,
} as ListenerMap,
),
}
export default holdRepeat
================================================
FILE: packages/@interactjs/pointer-events/interactableTargets.ts
================================================
import type { Interactable } from '@interactjs/core/Interactable'
import type { Scope, Plugin } from '@interactjs/core/scope'
import type { Element } from '@interactjs/core/types'
import extend from '@interactjs/utils/extend'
import type { PointerEventOptions } from '@interactjs/pointer-events/base'
declare module '@interactjs/core/Interactable' {
interface Interactable {
pointerEvents(options: Partial): this
/** @internal */
__backCompatOption: (optionName: string, newValue: any) => any
}
}
function install(scope: Scope) {
const { Interactable } = scope
Interactable.prototype.pointerEvents = function (
this: Interactable,
options: Partial,
) {
extend(this.events.options, options)
return this
}
const __backCompatOption = Interactable.prototype._backCompatOption
Interactable.prototype._backCompatOption = function (optionName, newValue) {
const ret = __backCompatOption.call(this, optionName, newValue)
if (ret === this) {
this.events.options[optionName] = newValue
}
return ret
}
}
const plugin: Plugin = {
id: 'pointer-events/interactableTargets',
install,
listeners: {
'pointerEvents:collect-targets': ({ targets, node, type, eventTarget }, scope) => {
scope.interactables.forEachMatch(node, (interactable: Interactable) => {
const eventable = interactable.events
const options = eventable.options
if (
eventable.types[type] &&
eventable.types[type].length &&
interactable.testIgnoreAllow(options, node, eventTarget)
) {
targets.push({
node,
eventable,
props: { interactable },
})
}
})
},
'interactable:new': ({ interactable }) => {
interactable.events.getRect = function (element: Element) {
return interactable.getRect(element)
}
},
'interactable:set': ({ interactable, options }, scope) => {
extend(interactable.events.options, scope.pointerEvents.defaults)
extend(interactable.events.options, options.pointerEvents || {})
},
},
}
export default plugin
================================================
FILE: packages/@interactjs/pointer-events/package.json
================================================
{
"name": "@interactjs/pointer-events",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/pointer-events"
},
"peerDependencies": {
"@interactjs/core": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/pointer-events/plugin.ts
================================================
import type { Plugin } from '@interactjs/core/scope'
/* eslint-disable import/no-duplicates -- for typescript module augmentations */
import './base'
import './holdRepeat'
import './interactableTargets'
import * as pointerEvents from './base'
import holdRepeat from './holdRepeat'
import interactableTargets from './interactableTargets'
/* eslint-enable import/no-duplicates */
const plugin: Plugin = {
id: 'pointer-events',
install(scope) {
scope.usePlugin(pointerEvents)
scope.usePlugin(holdRepeat)
scope.usePlugin(interactableTargets)
},
}
export default plugin
================================================
FILE: packages/@interactjs/reflow/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/reflow/package.json
================================================
{
"name": "@interactjs/reflow",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/reflow"
},
"peerDependencies": {
"@interactjs/core": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/reflow/plugin.ts
================================================
import type { Interactable } from '@interactjs/core/Interactable'
import type { DoAnyPhaseArg, Interaction } from '@interactjs/core/Interaction'
import type { Scope, Plugin } from '@interactjs/core/scope'
import type { ActionName, ActionProps, Element } from '@interactjs/core/types'
import * as arr from '@interactjs/utils/arr'
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
'interactions:action-reflow': DoAnyPhaseArg
'interactions:after-action-reflow': DoAnyPhaseArg
}
}
declare module '@interactjs/core/Interactable' {
interface Interactable {
/**
* ```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
*/
reflow(action: ActionProps): ReturnType
}
}
declare module '@interactjs/core/Interaction' {
interface Interaction {
_reflowPromise: Promise
_reflowResolve: (...args: unknown[]) => void
}
}
declare module '@interactjs/core/InteractEvent' {
interface PhaseMap {
reflow?: true
}
}
function install(scope: Scope) {
const { Interactable } = scope
scope.actions.phases.reflow = true
Interactable.prototype.reflow = function (action: ActionProps) {
return doReflow(this, action, scope)
}
}
function doReflow(
interactable: Interactable,
action: ActionProps,
scope: Scope,
): Promise {
const elements = interactable.getAllElements()
// tslint:disable-next-line variable-name
const Promise = (scope.window as any).Promise
const promises: Array> | 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
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(scope, interactable, element, action, event)
}
if (promises) {
promises.push(reflowPromise)
}
}
return promises && Promise.all(promises).then(() => interactable)
}
function startReflow(
scope: Scope,
interactable: Interactable,
element: Element,
action: ActionProps,
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((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
================================================
FILE: packages/@interactjs/reflow/reflow.spec.ts
================================================
import type { Interactable } from '@interactjs/core/Interactable'
import type { InteractEvent } from '@interactjs/core/InteractEvent'
import * as helpers from '@interactjs/core/tests/_helpers'
import type { ActionName, Point } from '@interactjs/core/types'
import PromisePolyfill from 'promise-polyfill'
import reflow from './plugin'
const testAction = { name: 'TEST' as ActionName }
const Promise_ = Promise
describe('reflow', () => {
test('sync', () => {
const rect = Object.freeze({ top: 100, left: 200, bottom: 300, right: 400 })
const { scope, interactable } = helpers.testEnv({ plugins: [reflow], rect })
Object.assign(scope.actions, { TEST: {}, names: ['TEST'] })
// reflow method is added to Interactable.prototype
expect(scope.Interactable.prototype.reflow instanceof Function).toBe(true)
const fired: InteractEvent[] = []
let beforeReflowDelta: Point
interactable.fire = ((iEvent: any) => {
fired.push(iEvent)
}) as any
;(interactable.options as any).TEST = { enabled: true }
interactable.rectChecker(() => ({ ...rect }))
// modify move coords
scope.addListeners({
'interactions:before-action-move': ({ interaction }) => {
interaction.coords.cur.page = {
x: rect.left + 100,
y: rect.top - 50,
}
},
'interactions:before-action-reflow': ({ interaction }) => {
beforeReflowDelta = { ...interaction.coords.delta.page }
},
})
interactable.reflow(testAction)
const phases = ['reflow', 'start', 'move', 'end']
expect(phases.map((_phase, index) => fired[index]?.type)).toEqual(phases.map((phase) => `TEST${phase}`))
for (const index in phases) {
const phase = phases[index]
// `event #${index} is ${phase}`
expect(fired[index].type).toBe(`TEST${phase}`)
}
const interaction = fired[0]._interaction
// uses element top left for event coords
expect(interaction.coords.start.page).toEqual({
x: rect.left,
y: rect.top,
})
const reflowMove = fired[2]
// interaction delta is zero before-action-reflow
expect(beforeReflowDelta!).toEqual({ x: 0, y: 0 })
// move delta is correct with modified interaction coords
expect(reflowMove.delta).toEqual({ x: 100, y: -50 })
// reflow pointer was lifted
expect(interaction.pointerIsDown).toBe(false)
// reflow pointer was removed from interaction
expect(interaction.pointers).toHaveLength(0)
// interaction is removed from list
expect(scope.interactions.list).not.toContain(interaction)
})
test('async', async () => {
const { scope } = helpers.testEnv({ plugins: [reflow] })
Object.assign(scope.actions, { TEST: {}, names: ['TEST'] })
let reflowEvent: any
let promise: Promise
const interactable = scope.interactables.new(scope.document.documentElement)
const rect = Object.freeze({ top: 100, left: 200, bottom: 300, right: 400 })
interactable.rectChecker(() => ({ ...rect }))
interactable.fire = ((iEvent: any) => {
reflowEvent = iEvent
}) as any
;(interactable.options as any).TEST = { enabled: true }
// test with Promise implementation
;(scope.window as any).Promise = PromisePolyfill
promise = interactable.reflow(testAction)
// method returns a Promise if available
expect(promise instanceof (scope.window as any).Promise).toBe(true)
// reflow may end synchronously
expect(reflowEvent.interaction.interacting()).toBe(false)
// returned Promise resolves to interactable
expect(await promise).toBe(interactable)
let stoppedFromTimeout: boolean
// block the end of the reflow interaction and stop it after a timeout
scope.addListeners({
'interactions:before-action-end': ({ interaction }) => {
setTimeout(() => {
interaction.stop()
stoppedFromTimeout = true
}, 0)
return false
},
})
stoppedFromTimeout = false
promise = interactable.reflow(testAction)
// interaction continues if end is blocked
expect(reflowEvent.interaction.interacting() && !stoppedFromTimeout).toBe(true)
await promise
// interaction is stopped after promise is resolved
expect(reflowEvent.interaction.interacting() && stoppedFromTimeout).toBe(false)
// test without Promise implementation
stoppedFromTimeout = false
;(scope.window as any).Promise = undefined
promise = interactable.reflow(testAction)
// method returns null if no Proise is avilable
expect(promise).toBeNull()
// interaction continues if end is blocked without Promise
expect(reflowEvent.interaction.interacting() && !stoppedFromTimeout).toBe(true)
await new Promise_((resolve) =>
setTimeout(() => {
// interaction is stopped after timeout without Promised
expect(reflowEvent.interaction.interacting() || !stoppedFromTimeout).toBe(false)
resolve()
}, 0),
)
})
})
================================================
FILE: packages/@interactjs/snappers/all.ts
================================================
/* eslint-disable import/no-named-as-default, import/no-unresolved */
export { default as edgeTarget } from './edgeTarget'
export { default as elements } from './elements'
export { default as grid } from './grid'
================================================
FILE: packages/@interactjs/snappers/edgeTarget.stub.ts
================================================
export default () => {}
================================================
FILE: packages/@interactjs/snappers/edgeTarget.ts
================================================
export default () => {}
================================================
FILE: packages/@interactjs/snappers/elements.stub.ts
================================================
export default () => {}
================================================
FILE: packages/@interactjs/snappers/elements.ts
================================================
export default () => {}
================================================
FILE: packages/@interactjs/snappers/grid.ts
================================================
import type { Rect, Point } from '@interactjs/core/types'
import type { SnapFunction, SnapTarget } from '@interactjs/modifiers/snap/pointer'
export interface GridOptionsBase {
range?: number
limits?: Rect
offset?: Point
}
export interface GridOptionsXY extends GridOptionsBase {
x: number
y: number
}
export interface GridOptionsTopLeft extends GridOptionsBase {
top?: number
left?: number
}
export interface GridOptionsBottomRight extends GridOptionsBase {
bottom?: number
right?: number
}
export interface GridOptionsWidthHeight extends GridOptionsBase {
width?: number
height?: number
}
export type GridOptions = GridOptionsXY | GridOptionsTopLeft | GridOptionsBottomRight | GridOptionsWidthHeight
export default (grid: GridOptions) => {
const coordFields = (
[
['x', 'y'],
['left', 'top'],
['right', 'bottom'],
['width', 'height'],
] as const
).filter(([xField, yField]) => xField in grid || yField in grid)
const gridFunc: SnapFunction & {
grid: typeof grid
coordFields: typeof coordFields
} = (x, y) => {
const {
range,
limits = {
left: -Infinity,
right: Infinity,
top: -Infinity,
bottom: Infinity,
},
offset = { x: 0, y: 0 },
} = grid
const result: SnapTarget & {
grid: typeof grid
} = { range, grid, x: null as number, y: null as number }
for (const [xField, yField] of coordFields) {
const gridx = Math.round((x - offset.x) / (grid as any)[xField])
const gridy = Math.round((y - offset.y) / (grid as any)[yField])
result[xField] = Math.max(limits.left, Math.min(limits.right, gridx * (grid as any)[xField] + offset.x))
result[yField] = Math.max(limits.top, Math.min(limits.bottom, gridy * (grid as any)[yField] + offset.y))
}
return result
}
gridFunc.grid = grid
gridFunc.coordFields = coordFields
return gridFunc
}
================================================
FILE: packages/@interactjs/snappers/package.json
================================================
{
"name": "@interactjs/snappers",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/snappers"
},
"peerDependencies": {
"@interactjs/utils": "1.10.27"
},
"optionalDependencies": {
"@interactjs/interact": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/snappers/plugin.ts
================================================
import type { Plugin } from '@interactjs/core/scope'
import extend from '@interactjs/utils/extend'
import * as allSnappers from './all'
declare module '@interactjs/core/InteractStatic' {
export interface InteractStatic {
snappers: typeof allSnappers
createSnapGrid: typeof allSnappers.grid
}
}
const snappersPlugin: Plugin = {
id: 'snappers',
install(scope) {
const { interactStatic: interact } = scope
interact.snappers = extend(interact.snappers || {}, allSnappers)
interact.createSnapGrid = interact.snappers.grid
},
}
export default snappersPlugin
================================================
FILE: packages/@interactjs/types/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/types/index.ts
================================================
/* eslint-disable import/no-extraneous-dependencies */
import type { InteractEvent as _InteractEvent, EventPhase } from '@interactjs/core/InteractEvent'
import type * as interaction from '@interactjs/core/Interaction'
import type { ActionName, ActionProps as _ActionProps } from '@interactjs/core/types'
// import module augmentations
import '@interactjs/interactjs'
export * from '@interactjs/core/types'
export type { Plugin } from '@interactjs/core/scope'
export type { EventPhase } from '@interactjs/core/InteractEvent'
export type { Options } from '@interactjs/core/options'
export type { PointerEvent } from '@interactjs/pointer-events/PointerEvent'
export type { Interactable } from '@interactjs/core/Interactable'
export type { DragEvent } from '@interactjs/actions/drag/plugin'
export type { DropEvent } from '@interactjs/actions/drop/DropEvent'
export type { GestureEvent } from '@interactjs/actions/gesture/plugin'
export type { ResizeEvent } from '@interactjs/actions/resize/plugin'
export type { SnapFunction, SnapTarget } from '@interactjs/modifiers/snap/pointer'
export type ActionProps = _ActionProps
export type Interaction = interaction.Interaction
export type InteractionProxy = interaction.InteractionProxy
export type PointerArgProps = interaction.PointerArgProps
export type InteractEvent = _InteractEvent<
T,
P
>
================================================
FILE: packages/@interactjs/types/package.json
================================================
{
"name": "@interactjs/types",
"version": "1.10.27",
"main": "index",
"module": "index",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/taye/interact.js.git",
"directory": "packages/@interactjs/types"
},
"typings": "typings.d.ts",
"devDependencies": {
"@interactjs/actions": "1.10.27",
"@interactjs/arrange": "1.10.27",
"@interactjs/auto-scroll": "1.10.27",
"@interactjs/auto-start": "1.10.27",
"@interactjs/core": "1.10.27",
"@interactjs/dev-tools": "1.10.27",
"@interactjs/inertia": "1.10.27",
"@interactjs/interact": "1.10.27",
"@interactjs/interactjs": "1.10.27",
"@interactjs/modifiers": "1.10.27",
"@interactjs/pointer-events": "1.10.27",
"@interactjs/reflow": "1.10.27",
"@interactjs/snappers": "1.10.27",
"@interactjs/utils": "1.10.27"
},
"publishConfig": {
"access": "public"
},
"sideEffects": [
"**/index.js",
"**/index.prod.js"
],
"license": "MIT"
}
================================================
FILE: packages/@interactjs/types/types.spec.ts
================================================
/** @jest-environment node */
import path from 'path'
import * as execTypes from '@interactjs/_dev/scripts/execTypes'
import { mkdirp } from 'mkdirp'
import * as shell from 'shelljs'
import temp from 'temp'
jest.setTimeout(15000)
test('typings', async () => {
shell.config.fatal = true
const tempDir = temp.track().mkdirSync('testProject')
const modulesDir = path.join(tempDir, 'node_modules')
const tempTypesDir = path.join(modulesDir, '@interactjs', 'types')
const interactDir = path.join(modulesDir, 'interactjs')
await mkdirp(interactDir)
// run .d.ts generation script with output to temp dir node_modules
await execTypes.combined(tempTypesDir)
// copy .d.ts and package.json files of deps to temp dir
shell.cp(path.join('packages', 'interactjs', '{*.d.ts,package.json}'), interactDir)
shell.cp(path.join('packages', '@interactjs', 'types', '{*.d.ts,package.json}'), tempTypesDir)
shell.cp('-R', path.join(process.cwd(), 'test', 'fixtures', 'dependentTsProject', '*'), tempDir)
expect(() => {
shell.exec(`${getBin('tsc')} -b`, { cwd: tempDir })
}).not.toThrow()
shell.config.reset()
})
const nodeBins = path.join(process.cwd(), 'node_modules', '.bin')
const getBin = (name: string) => path.join(nodeBins, name)
================================================
FILE: packages/@interactjs/utils/ElementState.stub.ts
================================================
export default {}
================================================
FILE: packages/@interactjs/utils/ElementState.ts
================================================
export default {}
================================================
FILE: packages/@interactjs/utils/README.md
================================================
This package is an internal part of interactjs and is not meant
to be used independently as each update may introduce breaking changes
================================================
FILE: packages/@interactjs/utils/arr.ts
================================================
type Filter = (element: T, index: number, array: T[]) => boolean
export const contains = (array: T[], target: T) => array.indexOf(target) !== -1
export const remove = (array: T[], target: T) => array.splice(array.indexOf(target), 1)
export const merge = (target: Array, source: U[]) => {
for (const item of source) {
target.push(item)
}
return target
}
export const from = (source: ArrayLike) => merge([] as T[], source as T[])
export const findIndex = (array: T[], func: Filter) => {
for (let i = 0; i < array.length; i++) {
if (func(array[i], i, array)) {
return i
}
}
return -1
}
export const find = (array: T[], func: Filter): T | undefined => array[findIndex(array, func)]
================================================
FILE: packages/@interactjs/utils/browser.ts
================================================
import domObjects from './domObjects'
import is from './is'
const browser = {
init,
supportsTouch: null as boolean,
supportsPointerEvent: null as boolean,
isIOS7: null as boolean,
isIOS: null as boolean,
isIe9: null as boolean,
isOperaMobile: null as boolean,
prefixedMatchesSelector: null as 'matches',
pEventTypes: null as {
up: string
down: string
over: string
out: string
move: string
cancel: string
},
wheelEvent: null as string,
}
function init(window: any) {
const Element = domObjects.Element
const navigator: Partial = window.navigator || {}
// Does the browser support touch input?
browser.supportsTouch =
'ontouchstart' in window ||
(is.func(window.DocumentTouch) && domObjects.document instanceof window.DocumentTouch)
// Does the browser support PointerEvents
// https://github.com/taye/interact.js/issues/703#issuecomment-471570492
browser.supportsPointerEvent = (navigator as any).pointerEnabled !== false && !!domObjects.PointerEvent
browser.isIOS = /iP(hone|od|ad)/.test(navigator.platform)
// scrolling doesn't change the result of getClientRects on iOS 7
browser.isIOS7 = /iP(hone|od|ad)/.test(navigator.platform) && /OS 7[^\d]/.test(navigator.appVersion)
browser.isIe9 = /MSIE 9/.test(navigator.userAgent)
// Opera Mobile must be handled differently
browser.isOperaMobile =
navigator.appName === 'Opera' && browser.supportsTouch && /Presto/.test(navigator.userAgent)
// prefix matchesSelector
browser.prefixedMatchesSelector = (
'matches' in Element.prototype
? 'matches'
: 'webkitMatchesSelector' in Element.prototype
? 'webkitMatchesSelector'
: 'mozMatchesSelector' in Element.prototype
? 'mozMatchesSelector'
: 'oMatchesSelector' in Element.prototype
? 'oMatchesSelector'
: 'msMatchesSelector'
) as 'matches'
browser.pEventTypes = browser.supportsPointerEvent
? domObjects.PointerEvent === window.MSPointerEvent
? {
up: 'MSPointerUp',
down: 'MSPointerDown',
over: 'mouseover',
out: 'mouseout',
move: 'MSPointerMove',
cancel: 'MSPointerCancel',
}
: {
up: 'pointerup',
down: 'pointerdown',
over: 'pointerover',
out: 'pointerout',
move: 'pointermove',
cancel: 'pointercancel',
}
: null
// because Webkit and Opera still use 'mousewheel' event type
browser.wheelEvent = domObjects.document && 'onmousewheel' in domObjects.document ? 'mousewheel' : 'wheel'
}
export default browser
================================================
FILE: packages/@interactjs/utils/center.ts
================================================
import type { Rect } from '@interactjs/core/types'
export default (rect: Rect) => ({
x: rect.left + (rect.right - rect.left) / 2,
y: rect.top + (rect.bottom - rect.top) / 2,
})
================================================
FILE: packages/@interactjs/utils/clone.ts
================================================
import * as arr from './arr'
import is from './is'
// tslint:disable-next-line ban-types
export default function clone