@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
448 lines (383 loc) • 11.9 kB
text/typescript
import {
isValidTarget,
removeHandlerId,
addEventListener,
ensureHandlerId,
splitType,
normalizeType,
removeEventListener,
getHandlerId,
isWindow,
setHandlerId,
getHandlerQueue,
stopPropagationCallback,
} from './util'
import { ensure, remove } from './store'
import { get } from './hook'
import { get as getStore } from './store'
import type { EventTarget, HandlerObject } from './store'
import { EventObject, type EventObjectEvent } from './object'
import { EventHandler } from './types'
import './special'
let triggered: string | undefined
export function on(
elem: EventTarget,
types: string,
handler:
| EventHandler<any, any>
| ({
handler: EventHandler<any, any>
selector?: string
} & Partial<HandlerObject>),
data?: any,
selector?: string,
) {
if (!isValidTarget(elem)) {
return
}
// Caller can pass in an object of custom data in lieu of the handler
let handlerData: any
if (typeof handler !== 'function') {
const { handler: h, selector: s, ...others } = handler
handler = h // eslint-disable-line
selector = s // eslint-disable-line
handlerData = others
}
// Ensure that invalid selectors throw exceptions at attach time
// if (!isValidSelector(elem, selector)) {
// throw new Error('Delegate event with invalid selector.')
// }
const store = ensure(elem)
// Ensure the main handle
let mainHandler = store.handler
if (mainHandler == null) {
mainHandler = store.handler = function (e, ...args: any[]) {
return triggered !== e.type ? dispatch(elem, e, ...args) : undefined
}
}
// Make sure that the handler has a unique ID, used to find/remove it later
const guid = ensureHandlerId(handler)
// Handle multiple events separated by a space
splitType(types).forEach((item) => {
const { originType, namespaces } = normalizeType(item)
// There *must* be a type, no attaching namespace-only handlers
if (!originType) {
return
}
let type = originType
let hook = get(type)
// If selector defined, determine special event type, otherwise given type
type = (selector ? hook.delegateType : hook.bindType) || type
// Update hook based on newly reset type
hook = get(type)
// handleObj is passed to all event handlers
const handleObj: HandlerObject = {
type,
originType,
data,
selector,
guid,
handler: handler as EventHandler<any, any>,
namespace: namespaces.join('.'),
...handlerData,
}
// Init the event handler queue if we're the first
const events = store.events
let bag = events[type]
if (!bag) {
bag = events[type] = { handlers: [], delegateCount: 0 }
// Only use addEventListener if the `hook.steup` returns false
if (
!hook.setup ||
hook.setup(elem, data, namespaces, mainHandler!) === false
) {
addEventListener(
elem as Element,
type,
mainHandler as any as EventListener,
)
}
}
if (hook.add) {
removeHandlerId(handleObj.handler)
hook.add(elem, handleObj)
setHandlerId(handleObj.handler, guid)
}
// Add to the element's handler list, delegates in front
if (selector) {
bag.handlers.splice(bag.delegateCount, 0, handleObj)
bag.delegateCount += 1
} else {
bag.handlers.push(handleObj)
}
})
}
export function off(
elem: EventTarget,
types: string,
handler?: EventHandler<any, any>,
selector?: string,
mappedTypes?: boolean,
) {
const store = getStore(elem)
if (!store) {
return
}
const events = store.events
if (!events) {
return
}
// Once for each type.namespace in types; type may be omitted
splitType(types).forEach((item) => {
const { originType, namespaces } = normalizeType(item)
// Unbind all events (on this namespace, if provided) for the element
if (!originType) {
Object.keys(events).forEach((key) => {
off(elem, key + item, handler, selector, true)
})
return
}
let type = originType
const hook = get(type)
type = (selector ? hook.delegateType : hook.bindType) || type
const bag = events[type]
if (!bag) {
return
}
const rns =
namespaces.length > 0
? new RegExp(`(^|\\.)${namespaces.join('\\.(?:.*\\.|)')}(\\.|$)`)
: null
// Remove matching events
const originHandlerCount = bag.handlers.length
for (let i = bag.handlers.length - 1; i >= 0; i -= 1) {
const handleObj = bag.handlers[i]
if (
(mappedTypes || originType === handleObj.originType) &&
(!handler || getHandlerId(handler) === handleObj.guid) &&
(rns == null ||
(handleObj.namespace && rns.test(handleObj.namespace))) &&
(selector == null ||
selector === handleObj.selector ||
(selector === '**' && handleObj.selector))
) {
bag.handlers.splice(i, 1)
if (handleObj.selector) {
bag.delegateCount -= 1
}
if (hook.remove) {
hook.remove(elem, handleObj)
}
}
}
if (originHandlerCount && bag.handlers.length === 0) {
if (
!hook.teardown ||
hook.teardown(elem, namespaces, store.handler!) === false
) {
removeEventListener(
elem as Element,
type,
store.handler as any as EventListener,
)
}
delete events[type]
}
})
// Remove data and the expando if it's no longer used
if (Object.keys(events).length === 0) {
remove(elem)
}
}
export function dispatch(
elem: EventTarget,
evt: Event | EventObject | string,
...args: any[]
) {
const event = EventObject.create(evt)
event.delegateTarget = elem as Element
const hook = get(event.type)
if (hook.preDispatch && hook.preDispatch(elem, event) === false) {
return
}
const handlerQueue = getHandlerQueue(elem, event)
// Run delegates first; they may want to stop propagation beneath us
for (
let i = 0, l = handlerQueue.length;
i < l && !event.isPropagationStopped();
i += 1
) {
const matched = handlerQueue[i]
event.currentTarget = matched.elem
for (
let j = 0, k = matched.handlers.length;
j < k && !event.isImmediatePropagationStopped();
j += 1
) {
const handleObj = matched.handlers[j]
// If event is namespaced, then each handler is only invoked if it is
// specially universal or its namespaces are a superset of the event's.
if (
event.rnamespace == null ||
(handleObj.namespace && event.rnamespace.test(handleObj.namespace))
) {
event.handleObj = handleObj
event.data = handleObj.data
const hookHandle = get(handleObj.originType).handle
const result = hookHandle
? hookHandle(matched.elem as EventTarget, event, ...args)
: handleObj.handler.call(matched.elem, event, ...args)
if (result !== undefined) {
event.result = result
if (result === false) {
event.preventDefault()
event.stopPropagation()
}
}
}
}
}
// Call the postDispatch hook for the mapped type
if (hook.postDispatch) {
hook.postDispatch(elem, event)
}
return event.result
}
export function trigger(
event: (Partial<EventObjectEvent> & { type: string }) | EventObject | string,
eventArgs: any,
elem: EventTarget,
onlyHandlers?: boolean,
) {
let eventObj = event as EventObject
let type = typeof event === 'string' ? event : event.type
let namespaces =
typeof event === 'string' || eventObj.namespace == null
? []
: eventObj.namespace.split('.')
const node = elem as HTMLElement
// Don't do events on text and comment nodes
if (node.nodeType === 3 || node.nodeType === 8) {
return
}
if (type.indexOf('.') > -1) {
// Namespaced trigger; create a regexp to match event type in handle()
namespaces = type.split('.')
type = namespaces.shift()!
namespaces.sort()
}
const ontype = type.indexOf(':') < 0 && (`on${type}` as 'onclick')
// Caller can pass in a EventObject, Object, or just an event type string
eventObj =
event instanceof EventObject
? event
: new EventObject(type, typeof event === 'object' ? event : null)
eventObj.namespace = namespaces.join('.')
eventObj.rnamespace = eventObj.namespace
? new RegExp(`(^|\\.)${namespaces.join('\\.(?:.*\\.|)')}(\\.|$)`)
: null
// Clean up the event in case it is being reused
eventObj.result = undefined
if (!eventObj.target) {
eventObj.target = node
}
const args: [EventObject, ...any[]] = [eventObj]
if (Array.isArray(eventArgs)) {
args.push(...eventArgs)
} else {
args.push(eventArgs)
}
const hook = get(type)
if (
!onlyHandlers &&
hook.trigger &&
hook.trigger(node, eventObj, eventArgs) === false
) {
return
}
let bubbleType
// Determine event propagation path in advance, per W3C events spec.
// Bubble up to document, then to window; watch for a global ownerDocument
const eventPath = [node]
if (!onlyHandlers && !hook.noBubble && !isWindow(node)) {
bubbleType = hook.delegateType || type
let last: Document | HTMLElement = node
let curr = node.parentNode as HTMLElement
while (curr != null) {
eventPath.push(curr)
last = curr
curr = curr.parentNode as HTMLElement
}
// Only add window if we got to document
const doc = node.ownerDocument || document
if ((last as any) === doc) {
const win =
(last as any).defaultView || (last as any).parentWindow || window
eventPath.push(win)
}
}
let lastElement = node
// Fire handlers on the event path
for (
let i = 0, l = eventPath.length;
i < l && !eventObj.isPropagationStopped();
i += 1
) {
const currElement = eventPath[i]
lastElement = currElement
eventObj.type = i > 1 ? (bubbleType as string) : hook.bindType || type
// Custom handler
const store = getStore(currElement as Element)
if (store) {
if (store.events[eventObj.type] && store.handler) {
store.handler.call(currElement, ...args)
}
}
// Native handler
const handle = (ontype && currElement[ontype]) || null
if (handle && isValidTarget(currElement)) {
eventObj.result = handle.call(currElement, ...args)
if (eventObj.result === false) {
eventObj.preventDefault()
}
}
}
eventObj.type = type
// If nobody prevented the default action, do it now
if (!onlyHandlers && !eventObj.isDefaultPrevented()) {
const preventDefault = hook.preventDefault
if (
(preventDefault == null ||
preventDefault(eventPath.pop()!, eventObj, eventArgs) === false) &&
isValidTarget(node)
) {
// Call a native DOM method on the target with the same name as the
// event. Don't do default actions on window.
if (
ontype &&
typeof node[type as 'click'] === 'function' &&
!isWindow(node)
) {
// Don't re-trigger an onFOO event when we call its FOO() method
const tmp = node[ontype]
if (tmp) {
node[ontype] = null
}
// Prevent re-triggering of the same event, since we already bubbled it above
triggered = type
if (eventObj.isPropagationStopped()) {
lastElement.addEventListener(type, stopPropagationCallback)
}
node[type as 'click']()
if (eventObj.isPropagationStopped()) {
lastElement.removeEventListener(type, stopPropagationCallback)
}
triggered = undefined
if (tmp) {
node[ontype] = tmp
}
}
}
}
return eventObj.result
}