refactor: ♻️ remove useless hooks and add test specs
This commit is contained in:
@ -3,7 +3,6 @@ import { Global } from '../../global'
|
||||
import { Util } from './util'
|
||||
import { Hook } from './hook'
|
||||
import { Store } from './store'
|
||||
import { EventRaw } from './alias'
|
||||
import { EventObject } from './object'
|
||||
import { EventHandler } from './types'
|
||||
|
||||
@ -27,10 +26,12 @@ export namespace Core {
|
||||
}
|
||||
|
||||
// Caller can pass in an object of custom data in lieu of the handler
|
||||
let handlerData: any
|
||||
if (typeof handler !== 'function') {
|
||||
const temp = handler
|
||||
handler = temp.handler // eslint-disable-line
|
||||
selector = temp.selector // eslint-disable-line
|
||||
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
|
||||
@ -70,7 +71,6 @@ export namespace Core {
|
||||
hook = Hook.get(type)
|
||||
|
||||
// handleObj is passed to all event handlers
|
||||
const others = typeof handler === 'function' ? undefined : handler
|
||||
const handleObj: Store.HandlerObject = {
|
||||
type,
|
||||
originType,
|
||||
@ -79,8 +79,7 @@ export namespace Core {
|
||||
guid,
|
||||
handler: handler as EventHandler<any, any>,
|
||||
namespace: namespaces.join('.'),
|
||||
needsContext: Util.needsContext(selector),
|
||||
...others,
|
||||
...handlerData,
|
||||
}
|
||||
|
||||
// Init the event handler queue if we're the first
|
||||
@ -163,8 +162,9 @@ export namespace Core {
|
||||
if (
|
||||
(mappedTypes || originType === handleObj.originType) &&
|
||||
(!handler || Util.getHandlerId(handler) === handleObj.guid) &&
|
||||
(!rns || !handleObj.namespace || rns.test(handleObj.namespace)) &&
|
||||
(!selector ||
|
||||
(rns == null ||
|
||||
(handleObj.namespace && rns.test(handleObj.namespace))) &&
|
||||
(selector == null ||
|
||||
selector === handleObj.selector ||
|
||||
(selector === '**' && handleObj.selector))
|
||||
) {
|
||||
@ -212,7 +212,7 @@ export namespace Core {
|
||||
|
||||
const hook = Hook.get(event.type)
|
||||
if (hook.preDispatch && hook.preDispatch(elem, event) === false) {
|
||||
return undefined
|
||||
return
|
||||
}
|
||||
|
||||
const handlerQueue = Util.getHandlerQueue(elem, event)
|
||||
@ -236,8 +236,7 @@ export namespace Core {
|
||||
// specially universal or its namespaces are a superset of the event's.
|
||||
if (
|
||||
event.rnamespace == null ||
|
||||
!handleObj.namespace ||
|
||||
event.rnamespace.test(handleObj.namespace)
|
||||
(handleObj.namespace && event.rnamespace.test(handleObj.namespace))
|
||||
) {
|
||||
event.handleObj = handleObj
|
||||
event.data = handleObj.data
|
||||
@ -267,9 +266,12 @@ export namespace Core {
|
||||
}
|
||||
|
||||
export function trigger(
|
||||
event: EventObject.Event | EventObject | EventRaw | string,
|
||||
data: any,
|
||||
elem?: Store.EventTarget,
|
||||
event:
|
||||
| (Partial<EventObject.Event> & { type: string })
|
||||
| EventObject
|
||||
| string,
|
||||
eventArgs: any,
|
||||
elem: Store.EventTarget,
|
||||
onlyHandlers?: boolean,
|
||||
) {
|
||||
let eventObj = event as EventObject
|
||||
@ -279,17 +281,11 @@ export namespace Core {
|
||||
? []
|
||||
: eventObj.namespace.split('.')
|
||||
|
||||
const node = (elem || Global.document) as HTMLElement
|
||||
const node = elem as HTMLElement
|
||||
|
||||
// Don't do events on text and comment nodes
|
||||
if (node.nodeType === 3 || node.nodeType === 8) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// focus/blur morphs to focusin/out; ensure we're not firing them right now
|
||||
const rfocusMorph = /^(?:focusinfocus|focusoutblur)$/
|
||||
if (rfocusMorph.test(type + triggered)) {
|
||||
return undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (type.indexOf('.') > -1) {
|
||||
@ -306,8 +302,6 @@ export namespace Core {
|
||||
? event
|
||||
: new EventObject(type, typeof event === 'object' ? event : null)
|
||||
|
||||
// Trigger bitmask: & 1 for native handlers; & 2 for custom (always true)
|
||||
eventObj.isTrigger = onlyHandlers ? 2 : 3
|
||||
eventObj.namespace = namespaces.join('.')
|
||||
eventObj.rnamespace = eventObj.namespace
|
||||
? new RegExp(`(^|\\.)${namespaces.join('\\.(?:.*\\.|)')}(\\.|$)`)
|
||||
@ -320,19 +314,19 @@ export namespace Core {
|
||||
}
|
||||
|
||||
const args: [EventObject, ...any[]] = [eventObj]
|
||||
if (Array.isArray(data)) {
|
||||
args.push(...data)
|
||||
if (Array.isArray(eventArgs)) {
|
||||
args.push(...eventArgs)
|
||||
} else {
|
||||
args.push(data)
|
||||
args.push(eventArgs)
|
||||
}
|
||||
|
||||
const hook = Hook.get(type)
|
||||
if (
|
||||
!onlyHandlers &&
|
||||
hook.trigger &&
|
||||
hook.trigger(node, eventObj, data) === false
|
||||
hook.trigger(node, eventObj, eventArgs) === false
|
||||
) {
|
||||
return undefined
|
||||
return
|
||||
}
|
||||
|
||||
let bubbleType
|
||||
@ -343,19 +337,16 @@ export namespace Core {
|
||||
if (!onlyHandlers && !hook.noBubble && !isWindow(node)) {
|
||||
bubbleType = hook.delegateType || type
|
||||
|
||||
let curr = node
|
||||
let last = node
|
||||
let last: Document | HTMLElement = node
|
||||
let curr = node.parentNode as HTMLElement
|
||||
|
||||
if (!rfocusMorph.test(bubbleType + type)) {
|
||||
while (curr != null) {
|
||||
eventPath.push(curr)
|
||||
last = curr
|
||||
curr = curr.parentNode as HTMLElement
|
||||
}
|
||||
|
||||
for (; curr != null; curr = curr.parentNode as HTMLElement) {
|
||||
eventPath.push(curr)
|
||||
last = curr
|
||||
}
|
||||
|
||||
// Only add window if we got to document (e.g., not plain obj or detached DOM)
|
||||
// Only add window if we got to document
|
||||
const doc = node.ownerDocument || Global.document
|
||||
if ((last as any) === doc) {
|
||||
const win =
|
||||
@ -373,23 +364,23 @@ export namespace Core {
|
||||
i < l && !eventObj.isPropagationStopped();
|
||||
i += 1
|
||||
) {
|
||||
const curr = eventPath[i]
|
||||
lastElement = curr
|
||||
const currElement = eventPath[i]
|
||||
lastElement = currElement
|
||||
|
||||
eventObj.type = i > 1 ? (bubbleType as string) : hook.bindType || type
|
||||
|
||||
// custom handler
|
||||
const store = Store.get(curr as Element)
|
||||
// Custom handler
|
||||
const store = Store.get(currElement as Element)
|
||||
if (store) {
|
||||
if (store.events[eventObj.type] && store.handler) {
|
||||
store.handler.call(curr, ...args)
|
||||
store.handler.call(currElement, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
// Native handler
|
||||
const handle = ontype ? curr[ontype] : null
|
||||
if (handle && handle.apply && Util.isValidTarget(curr)) {
|
||||
eventObj.result = handle.call(curr, ...args)
|
||||
const handle = (ontype && currElement[ontype]) || null
|
||||
if (handle && handle.apply && Util.isValidTarget(currElement)) {
|
||||
eventObj.result = handle.call(currElement, ...args)
|
||||
if (eventObj.result === false) {
|
||||
eventObj.preventDefault()
|
||||
}
|
||||
@ -400,13 +391,14 @@ export namespace Core {
|
||||
|
||||
// If nobody prevented the default action, do it now
|
||||
if (!onlyHandlers && !eventObj.isDefaultPrevented()) {
|
||||
const preventDefault = hook.preventDefault
|
||||
if (
|
||||
(!hook.default ||
|
||||
hook.default(eventPath.pop()!, eventObj, data) === false) &&
|
||||
(preventDefault == null ||
|
||||
preventDefault(eventPath.pop()!, eventObj, eventArgs) === false) &&
|
||||
Util.isValidTarget(node)
|
||||
) {
|
||||
// Call a native DOM method on the target with the same name as the event.
|
||||
// Don't do default actions on window, that's where global variables be (#6170)
|
||||
// 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' &&
|
||||
|
644
packages/x6-vector/src/dom/events/emitter.test.ts
Normal file
644
packages/x6-vector/src/dom/events/emitter.test.ts
Normal file
@ -0,0 +1,644 @@
|
||||
import sinon from 'sinon'
|
||||
import { Adopter } from '../common/adopter'
|
||||
import { Dom } from '../dom'
|
||||
import { Core } from './core'
|
||||
import { Hook } from './hook'
|
||||
import { EventObject } from './object'
|
||||
|
||||
describe('Dom', () => {
|
||||
describe('events', () => {
|
||||
const tree = `
|
||||
<div>
|
||||
<div class="one common"></div>
|
||||
<div class="two common"></div>
|
||||
<div class="three">
|
||||
<div class="four"></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
describe('on()', () => {
|
||||
it('should bind single event', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.on('click', spy)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('should bind events with event-handler object', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
div.on({
|
||||
click: spy1,
|
||||
dblclick: spy2,
|
||||
})
|
||||
div.trigger('click')
|
||||
div.trigger('dblclick')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
div.trigger('click')
|
||||
div.trigger('dblclick')
|
||||
expect(spy1.callCount).toEqual(2)
|
||||
expect(spy2.callCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('should bind event with handler object', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.on('click', { handler: spy })
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('should not bind event on invalid target', () => {
|
||||
const text = new Dom(document.createTextNode('foo') as any)
|
||||
const spy = sinon.spy()
|
||||
text.on('click', spy)
|
||||
text.trigger('click')
|
||||
expect(spy.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should delegate event', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy = sinon.spy()
|
||||
container.on('click', '.one', spy)
|
||||
const child = container.findOne('.one')!
|
||||
|
||||
child.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
|
||||
child.trigger('click')
|
||||
expect(spy.callCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('should throw an error when delegating with invalid selector', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy = sinon.spy()
|
||||
expect(() => container.on('click', '.unknown', spy)).toThrowError()
|
||||
})
|
||||
|
||||
it('should support data', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.on('click', { foo: 'bar' }, spy)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(2)
|
||||
|
||||
const e1 = spy.args[0][0]
|
||||
const e2 = spy.args[1][0]
|
||||
expect(e1.data).toEqual({ foo: 'bar' })
|
||||
expect(e2.data).toEqual({ foo: 'bar' })
|
||||
})
|
||||
|
||||
it('should bind false as event handler', () => {
|
||||
const div = new Dom()
|
||||
expect(() => div.on('click', false)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should ignore invalid handler', () => {
|
||||
const div = new Dom()
|
||||
expect(() => div.on('click', null as any)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should not no attaching namespace-only handlers', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
expect(() => div.on('.ns', spy)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('once()', () => {
|
||||
it('should bind single event', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.once('click', spy)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should bind event with namespace', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.once('click.ns', spy)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('off()', () => {
|
||||
it('should unbind single event', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
div.on('click', spy1)
|
||||
div.on('click', spy2)
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
div.off('click', spy1)
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('should unbind events by the given event type', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
div.on('click', spy1)
|
||||
div.on('click', spy2)
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
div.off('click')
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should unbind events by the given namespace', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
div.on('click.ns', spy1)
|
||||
div.on('click.ns', spy2)
|
||||
div.on('click', spy3)
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
div.off('.ns')
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(2)
|
||||
})
|
||||
|
||||
it('should unbind events with event-handler object', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
div.on('click.ns', spy1)
|
||||
div.on('click.ns', spy2)
|
||||
div.on('click', spy3)
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
div.off({ 'click.ns': spy1, click: spy3 })
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(2)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should unbind delegated events', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy = sinon.spy()
|
||||
container.on('click', '.one', spy)
|
||||
const child = container.findOne('.one')!
|
||||
|
||||
child.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
|
||||
container.off('click', '.one')
|
||||
child.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should unbind delegated events with "**" selector', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy = sinon.spy()
|
||||
container.on('click', '.one', spy)
|
||||
const child = container.findOne('.one')!
|
||||
|
||||
child.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
|
||||
container.off('click', '**')
|
||||
child.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should unbind "false" event handler', () => {
|
||||
const div = new Dom()
|
||||
expect(() => div.off('click', false)).not.toThrowError()
|
||||
})
|
||||
|
||||
it('should do noting for elem which do not bind any events', () => {
|
||||
const div = new Dom()
|
||||
expect(() => div.off()).not.toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
describe('trigger()', () => {
|
||||
it('should trigger event with namespace', () => {
|
||||
const div = new Dom().appendTo(document.body)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
div.on('click.ns', spy1)
|
||||
div.on('click.ns', spy2)
|
||||
div.on('click', spy3)
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
div.trigger('click.ns')
|
||||
expect(spy1.callCount).toEqual(2)
|
||||
expect(spy2.callCount).toEqual(2)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
div.remove()
|
||||
})
|
||||
|
||||
it('should also trigger inline binded event', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy(() => false)
|
||||
div.on('click', spy1)
|
||||
const node = div.node as HTMLDivElement
|
||||
node.onclick = spy2
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should trigger event with EventObject', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
div.on('click.ns', spy1)
|
||||
div.on('click.ns', spy2)
|
||||
div.on('click', spy3)
|
||||
|
||||
div.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
|
||||
div.trigger(new EventObject('click', { namespace: 'ns' }))
|
||||
expect(spy1.callCount).toEqual(2)
|
||||
expect(spy2.callCount).toEqual(2)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should trigger event with EventObject created with native event', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
div.on('click.ns', spy1)
|
||||
div.on('click.ns', spy2)
|
||||
div.on('click', spy3)
|
||||
|
||||
const evt = document.createEvent('MouseEvents')
|
||||
evt.initEvent('click', true, true)
|
||||
evt.preventDefault()
|
||||
div.trigger(new EventObject(evt))
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should trigger event with EventLikeObject', () => {
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
div.on('click.ns', spy1)
|
||||
div.on('click.ns', spy2)
|
||||
div.on('click', spy3)
|
||||
|
||||
div.trigger({ type: 'click' })
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should trigger custom event', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.on('foo', spy)
|
||||
div.trigger('foo')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should bind and trigger event on any object', () => {
|
||||
const obj = {}
|
||||
const spy = sinon.spy()
|
||||
Core.on(obj, 'foo', spy)
|
||||
Core.trigger('foo', [], obj)
|
||||
expect(spy.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should trggier event with the given args', () => {
|
||||
const div = new Dom()
|
||||
const spy = sinon.spy()
|
||||
div.on('click', spy)
|
||||
|
||||
div.trigger('click')
|
||||
expect(spy.callCount).toEqual(1)
|
||||
|
||||
div.trigger('click', 1)
|
||||
expect(spy.callCount).toEqual(2)
|
||||
expect(spy.args[1][1]).toEqual(1)
|
||||
|
||||
div.trigger('click', [1, { foo: 'bar' }])
|
||||
expect(spy.callCount).toEqual(3)
|
||||
expect(spy.args[2][1]).toEqual(1)
|
||||
expect(spy.args[2][2]).toEqual({ foo: 'bar' })
|
||||
})
|
||||
|
||||
it('should stop propagation when handler return `false`', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy(() => false)
|
||||
container.on('click', spy1)
|
||||
container.on('click', '.one', spy2)
|
||||
const child = container.findOne('.one')!
|
||||
|
||||
child.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(0)
|
||||
|
||||
container.off('click', '.one', spy2)
|
||||
container.on('click', '.one', spy3)
|
||||
child.trigger('click')
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should stopImmediatePropagation 1', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy((e: EventObject) => {
|
||||
e.stopImmediatePropagation()
|
||||
})
|
||||
const spy3 = sinon.spy()
|
||||
const spy4 = sinon.spy()
|
||||
container.on('click', spy1)
|
||||
container.on('click', '.one', spy2)
|
||||
container.on('click', '.two', spy3)
|
||||
container.on('click', '.three', spy4)
|
||||
|
||||
container.findOne('.one')!.trigger('click')
|
||||
|
||||
expect(spy1.callCount).toEqual(0)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(0)
|
||||
expect(spy4.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should stopImmediatePropagation 2', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy((e: EventObject) => {
|
||||
e.stopImmediatePropagation()
|
||||
})
|
||||
const spy3 = sinon.spy()
|
||||
const spy4 = sinon.spy()
|
||||
container.on('click', spy1)
|
||||
container.on('click', '.one', spy2)
|
||||
container.on('click', '.two', spy3)
|
||||
container.on('click', '.three', spy4)
|
||||
|
||||
const evt = document.createEvent('MouseEvents')
|
||||
evt.initEvent('click', true, true)
|
||||
const node = container.findOne('.one')!.node as HTMLDivElement
|
||||
node.dispatchEvent(evt)
|
||||
|
||||
expect(spy1.callCount).toEqual(0)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(0)
|
||||
expect(spy4.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should prevent default action', (done) => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
container
|
||||
.on('click', (e) => {
|
||||
expect(e.isDefaultPrevented()).toBeTrue()
|
||||
done()
|
||||
})
|
||||
.findOne('.three')!
|
||||
.on('click', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
container.findOne('.four')?.trigger('click')
|
||||
})
|
||||
|
||||
it('should do the default action', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
const spy4 = sinon.spy((e: EventObject) => {
|
||||
e.stopPropagation()
|
||||
})
|
||||
|
||||
container.on('click', spy1)
|
||||
container.on('click', '.one', spy2)
|
||||
const child = container.findOne('.one')!
|
||||
const node = child.node as HTMLDivElement
|
||||
node.onclick = spy3
|
||||
child.on('click', spy4)
|
||||
|
||||
node.dispatchEvent(new Event('click'))
|
||||
expect(spy1.callCount).toEqual(0)
|
||||
expect(spy2.callCount).toEqual(0)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
expect(spy4.callCount).toEqual(1)
|
||||
})
|
||||
|
||||
it('should not propagation when `onlyHandlers` is `true`', () => {
|
||||
const container = Adopter.makeInstance(tree, true)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
container.on('click', spy1)
|
||||
container.on('click', '.one', spy2)
|
||||
const child = container.findOne('.one')!
|
||||
child.on('click', spy3)
|
||||
|
||||
child.trigger('click', [], true)
|
||||
expect(spy1.callCount).toEqual(0)
|
||||
expect(spy2.callCount).toEqual(0)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('hooks', () => {
|
||||
it('should get event properties on natively-triggered event', (done) => {
|
||||
const a = document.createElement('a')
|
||||
const lk = new Dom(a).appendTo(document.body).on('click', function (e) {
|
||||
expect('detail' in e).toBeTrue()
|
||||
expect('cancelable' in e).toBeTrue()
|
||||
expect('bubbles' in e).toBeTrue()
|
||||
expect(e.clientX).toEqual(10)
|
||||
done()
|
||||
})
|
||||
const evt = document.createEvent('MouseEvents')
|
||||
evt.initEvent('click', true, true)
|
||||
lk.trigger(new EventObject(evt, { clientX: 10 }))
|
||||
lk.remove()
|
||||
})
|
||||
|
||||
it('should get event properties added by `addProperty`', (done) => {
|
||||
const div = new Dom().on('click', (e) => {
|
||||
expect(typeof e.clientX === 'number').toBeTrue()
|
||||
done()
|
||||
})
|
||||
const node = div.node as HTMLDivElement
|
||||
const evt = document.createEvent('MouseEvents')
|
||||
evt.initEvent('click')
|
||||
node.dispatchEvent(evt)
|
||||
})
|
||||
|
||||
it('shoud add custom event property with `addProperty`', (done) => {
|
||||
EventObject.addProperty('testProperty', () => 42)
|
||||
|
||||
const div = new Dom().on('click', (e: any) => {
|
||||
expect(e.testProperty).toEqual(42)
|
||||
done()
|
||||
})
|
||||
const node = div.node as HTMLDivElement
|
||||
const evt = document.createEvent('MouseEvents')
|
||||
evt.initEvent('click')
|
||||
node.dispatchEvent(evt)
|
||||
})
|
||||
|
||||
it('should apply hook to prevent triggered `image.load` events from bubbling to `window.load`', () => {
|
||||
const div = new Dom()
|
||||
const win = new Dom(window as any)
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
div.on('load', spy1)
|
||||
win.on('load', spy2)
|
||||
|
||||
const node = div.node as HTMLElement
|
||||
node.dispatchEvent(new Event('load'))
|
||||
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should apply hook to prevent window to unload', () => {
|
||||
const win = new Dom(window as any)
|
||||
const spy1 = sinon.spy(() => {
|
||||
return false
|
||||
})
|
||||
const spy2 = sinon.spy()
|
||||
win.on('beforeunload', spy1)
|
||||
win.on('unload', spy2)
|
||||
const node = win.node as HTMLElement
|
||||
node.dispatchEvent(new Event('beforeunload'))
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should call hooks', () => {
|
||||
const addHook = sinon.spy()
|
||||
const removeHook = sinon.spy()
|
||||
const setupHook = sinon.spy()
|
||||
const teardownHook = sinon.spy()
|
||||
const handleHook = sinon.spy()
|
||||
const triggerHook = sinon.spy()
|
||||
const preDispatchHook = sinon.spy()
|
||||
const postDispatchHook = sinon.spy()
|
||||
|
||||
Hook.register('dblclick', {
|
||||
add: addHook,
|
||||
remove: removeHook,
|
||||
setup: setupHook,
|
||||
teardown: teardownHook,
|
||||
handle: handleHook,
|
||||
trigger: triggerHook,
|
||||
preDispatch: preDispatchHook,
|
||||
postDispatch: postDispatchHook,
|
||||
})
|
||||
const div = new Dom()
|
||||
const spyHandler = sinon.spy()
|
||||
div.on('dblclick', spyHandler)
|
||||
div.trigger('dblclick')
|
||||
div.off('dblclick')
|
||||
|
||||
expect(addHook.callCount).toEqual(1)
|
||||
expect(removeHook.callCount).toEqual(1)
|
||||
expect(setupHook.callCount).toEqual(1)
|
||||
expect(teardownHook.callCount).toEqual(1)
|
||||
expect(handleHook.callCount).toEqual(1)
|
||||
expect(triggerHook.callCount).toEqual(1)
|
||||
expect(preDispatchHook.callCount).toEqual(1)
|
||||
expect(postDispatchHook.callCount).toEqual(1)
|
||||
Hook.unregister('dblclick')
|
||||
})
|
||||
|
||||
it('should not trigger event when `preDispatch` hook return `false`', () => {
|
||||
const preDispatchHook = sinon.spy(() => false)
|
||||
Hook.register('dblclick', {
|
||||
preDispatch: preDispatchHook as any,
|
||||
})
|
||||
const div = new Dom()
|
||||
const spyHandler = sinon.spy()
|
||||
div.on('dblclick', spyHandler)
|
||||
div.trigger('dblclick')
|
||||
Hook.unregister('dblclick')
|
||||
expect(spyHandler.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should not trigger event when `trigger` hook return `false`', () => {
|
||||
const hook = sinon.spy(() => false)
|
||||
Hook.register('dblclick', {
|
||||
trigger: hook as any,
|
||||
})
|
||||
const div = new Dom()
|
||||
const spyHandler = sinon.spy()
|
||||
div.on('dblclick', spyHandler)
|
||||
div.trigger('dblclick')
|
||||
Hook.unregister('dblclick')
|
||||
expect(spyHandler.callCount).toEqual(0)
|
||||
})
|
||||
|
||||
it('should not prevent default when `preventDefault` hook return `false`', () => {
|
||||
const hook = sinon.spy(() => false)
|
||||
Hook.register('click', {
|
||||
preventDefault: hook as any,
|
||||
})
|
||||
const div = new Dom()
|
||||
const spy1 = sinon.spy()
|
||||
const spy2 = sinon.spy()
|
||||
const spy3 = sinon.spy()
|
||||
const node = div.node as HTMLDivElement
|
||||
|
||||
node.click = spy1
|
||||
node.onclick = spy2
|
||||
div.on('click', spy3)
|
||||
div.trigger('click')
|
||||
Hook.unregister('click')
|
||||
|
||||
expect(spy1.callCount).toEqual(1)
|
||||
expect(spy2.callCount).toEqual(1)
|
||||
expect(spy3.callCount).toEqual(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -2,7 +2,6 @@
|
||||
|
||||
import { Core } from './core'
|
||||
import { Util } from './util'
|
||||
import { EventRaw } from './alias'
|
||||
import { EventObject } from './object'
|
||||
import { TypeEventHandler, TypeEventHandlers } from './types'
|
||||
import { Base } from '../common/base'
|
||||
@ -28,12 +27,29 @@ export class EventEmitter<TElement extends Element> extends Base<TElement> {
|
||||
| TypeEventHandler<TElement, TData, TElement, TElement, TType>
|
||||
| false,
|
||||
): this
|
||||
on<TType extends string, TData>(
|
||||
events: TType,
|
||||
data: TData,
|
||||
handlerObject: {
|
||||
handler: TypeEventHandler<TElement, TData, TElement, TElement, TType>
|
||||
selector?: string
|
||||
[key: string]: any
|
||||
},
|
||||
): this
|
||||
on<TType extends string>(
|
||||
events: TType,
|
||||
handler:
|
||||
| TypeEventHandler<TElement, undefined, TElement, TElement, TType>
|
||||
| false,
|
||||
): this
|
||||
on<TType extends string>(
|
||||
events: TType,
|
||||
handlerObject: {
|
||||
handler: TypeEventHandler<TElement, undefined, TElement, TElement, TType>
|
||||
selector?: string
|
||||
[key: string]: any
|
||||
},
|
||||
): this
|
||||
on<TData>(
|
||||
events: TypeEventHandlers<TElement, TData, any, any>,
|
||||
selector: string | null | undefined,
|
||||
@ -73,12 +89,29 @@ export class EventEmitter<TElement extends Element> extends Base<TElement> {
|
||||
| TypeEventHandler<TElement, TData, TElement, TElement, TType>
|
||||
| false,
|
||||
): this
|
||||
once<TType extends string, TData>(
|
||||
events: TType,
|
||||
data: TData,
|
||||
handlerObject: {
|
||||
handler: TypeEventHandler<TElement, TData, TElement, TElement, TType>
|
||||
selector?: string
|
||||
[key: string]: any
|
||||
},
|
||||
): this
|
||||
once<TType extends string>(
|
||||
events: TType,
|
||||
handler:
|
||||
| TypeEventHandler<TElement, undefined, TElement, TElement, TType>
|
||||
| false,
|
||||
): this
|
||||
once<TType extends string>(
|
||||
events: TType,
|
||||
handlerObject: {
|
||||
handler: TypeEventHandler<TElement, undefined, TElement, TElement, TType>
|
||||
selector?: string
|
||||
[key: string]: any
|
||||
},
|
||||
): this
|
||||
once<TData>(
|
||||
events: TypeEventHandlers<TElement, TData, any, any>,
|
||||
selector: string | null | undefined,
|
||||
@ -103,7 +136,6 @@ export class EventEmitter<TElement extends Element> extends Base<TElement> {
|
||||
selector: string,
|
||||
handler: TypeEventHandler<TElement, any, any, any, TType> | false,
|
||||
): this
|
||||
|
||||
off<TType extends string>(
|
||||
events: TType,
|
||||
handler: TypeEventHandler<TElement, any, any, any, TType> | false,
|
||||
@ -136,11 +168,22 @@ export class EventEmitter<TElement extends Element> extends Base<TElement> {
|
||||
}
|
||||
|
||||
trigger(
|
||||
event: string | EventObject | EventRaw | EventObject.Event,
|
||||
data?: any[] | Record<string, any> | string | number | boolean,
|
||||
event:
|
||||
| string
|
||||
| EventObject
|
||||
| (Partial<EventObject.Event> & { type: string }),
|
||||
args?: any[] | Record<string, any> | string | number | boolean,
|
||||
/**
|
||||
* When onlyHandlers is `true`
|
||||
* - Will not call `.event()` on the element it is triggered on. This means
|
||||
* `.trigger('submit', [], true)` on a form will not call `.submit()` on
|
||||
* the form.
|
||||
* - Events will not bubble up the DOM hierarchy; if they are not handled
|
||||
* by the target element directly, they do nothing.
|
||||
*/
|
||||
onlyHandlers?: boolean,
|
||||
) {
|
||||
Core.trigger(event, data, this.node as any, onlyHandlers)
|
||||
Core.trigger(event, args, this.node as any, onlyHandlers)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,10 @@ export namespace Hook {
|
||||
export function register(type: string, hook: Hook) {
|
||||
cache[type] = hook
|
||||
}
|
||||
|
||||
export function unregister(type: string) {
|
||||
delete cache[type]
|
||||
}
|
||||
}
|
||||
|
||||
export interface Hook {
|
||||
@ -146,12 +150,12 @@ export interface Hook {
|
||||
* an event, it also looks for and runs any method on the target object by
|
||||
* the same name unless of the handlers called `event.preventDefault()`. So,
|
||||
* `.trigger("submit")` will execute the `submit()` method on the element if
|
||||
* one exists. When a `default` hook is specified, the hook is called just
|
||||
* prior to checking for and executing the element's default method. If this
|
||||
* hook returns the value `false` the element's default method will be called;
|
||||
* otherwise it is not.
|
||||
* one exists. When a `preventDefault` hook is specified, the hook is called
|
||||
* just prior to checking for and executing the element's default method. If
|
||||
* this hook returns the value `false` the element's default method will be
|
||||
* called; otherwise it is not.
|
||||
*/
|
||||
default?: (
|
||||
preventDefault?: (
|
||||
elem: Store.EventTarget,
|
||||
event: EventObject,
|
||||
data: any,
|
||||
|
@ -9,76 +9,6 @@ export interface EventListener<TElement extends Element>
|
||||
extends EventListener.Methods<TElement> {}
|
||||
|
||||
export namespace EventListener {
|
||||
// Generate interface
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (false) {
|
||||
const events = [
|
||||
'blur',
|
||||
'focus',
|
||||
'focusin',
|
||||
'focusout',
|
||||
'resize',
|
||||
'scroll',
|
||||
'click',
|
||||
'dblclick',
|
||||
'mousedown',
|
||||
'mouseup',
|
||||
'mousemove',
|
||||
'mouseover',
|
||||
'mouseout',
|
||||
'mouseenter',
|
||||
'mouseleave',
|
||||
'change',
|
||||
'select',
|
||||
'submit',
|
||||
'keydown',
|
||||
'keypress',
|
||||
'keyup',
|
||||
'contextmenu',
|
||||
'touchstart',
|
||||
'touchmove',
|
||||
'touchleave',
|
||||
'touchend',
|
||||
'touchcancel',
|
||||
] as const
|
||||
|
||||
events.forEach((event) => {
|
||||
EventListener.prototype[event] = function <TData>(
|
||||
this: EventListener<Element>,
|
||||
eventData?: TData | TypeEventHandler<any, any, any, any, any> | false,
|
||||
handler?: TypeEventHandler<any, any, any, any, any> | false,
|
||||
) {
|
||||
if (eventData == null) {
|
||||
this.trigger(event)
|
||||
} else {
|
||||
this.on(event, null, eventData, handler!)
|
||||
}
|
||||
return this
|
||||
}
|
||||
})
|
||||
|
||||
const methods = events.map(
|
||||
(event) =>
|
||||
`
|
||||
${event}(): this
|
||||
${event}(
|
||||
handler:
|
||||
| TypeEventHandler<TElement, null, TElement, TElement, '${event}'>
|
||||
| false,
|
||||
): this
|
||||
${event}<TData>(
|
||||
eventData: TData,
|
||||
handler:
|
||||
| TypeEventHandler<TElement, TData, TElement, TElement, '${event}'>
|
||||
| false,
|
||||
): this
|
||||
`,
|
||||
)
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(methods.join('\n'))
|
||||
}
|
||||
|
||||
export interface Methods<TElement extends Node> {
|
||||
blur(): this
|
||||
blur(
|
||||
@ -432,3 +362,72 @@ export namespace EventListener {
|
||||
): this
|
||||
}
|
||||
}
|
||||
|
||||
export namespace EventListener {
|
||||
// Generate interface
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
// if (false) {
|
||||
// const events = [
|
||||
// 'blur',
|
||||
// 'focus',
|
||||
// 'focusin',
|
||||
// 'focusout',
|
||||
// 'resize',
|
||||
// 'scroll',
|
||||
// 'click',
|
||||
// 'dblclick',
|
||||
// 'mousedown',
|
||||
// 'mouseup',
|
||||
// 'mousemove',
|
||||
// 'mouseover',
|
||||
// 'mouseout',
|
||||
// 'mouseenter',
|
||||
// 'mouseleave',
|
||||
// 'change',
|
||||
// 'select',
|
||||
// 'submit',
|
||||
// 'keydown',
|
||||
// 'keypress',
|
||||
// 'keyup',
|
||||
// 'contextmenu',
|
||||
// 'touchstart',
|
||||
// 'touchmove',
|
||||
// 'touchleave',
|
||||
// 'touchend',
|
||||
// 'touchcancel',
|
||||
// ] as const
|
||||
// events.forEach((event) => {
|
||||
// EventListener.prototype[event] = function <TData>(
|
||||
// this: EventListener<Element>,
|
||||
// eventData?: TData | TypeEventHandler<any, any, any, any, any> | false,
|
||||
// handler?: TypeEventHandler<any, any, any, any, any> | false,
|
||||
// ) {
|
||||
// if (eventData == null) {
|
||||
// this.trigger(event)
|
||||
// } else {
|
||||
// this.on(event, null, eventData, handler!)
|
||||
// }
|
||||
// return this
|
||||
// }
|
||||
// })
|
||||
// const methods = events.map(
|
||||
// (event) =>
|
||||
// `
|
||||
// ${event}(): this
|
||||
// ${event}(
|
||||
// handler:
|
||||
// | TypeEventHandler<TElement, null, TElement, TElement, '${event}'>
|
||||
// | false,
|
||||
// ): this
|
||||
// ${event}<TData>(
|
||||
// eventData: TData,
|
||||
// handler:
|
||||
// | TypeEventHandler<TElement, TData, TElement, TElement, '${event}'>
|
||||
// | false,
|
||||
// ): this
|
||||
// `,
|
||||
// )
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log(methods.join('\n'))
|
||||
// }
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export class EventObject<
|
||||
TTarget = any,
|
||||
TEvent extends Event = Event
|
||||
> implements EventObject.Event {
|
||||
isDefaultPrevented: () => boolean
|
||||
isDefaultPrevented: () => boolean = Util.returnFalse
|
||||
isPropagationStopped: () => boolean = Util.returnFalse
|
||||
isImmediatePropagationStopped: () => boolean = Util.returnFalse
|
||||
|
||||
@ -23,7 +23,6 @@ export class EventObject<
|
||||
|
||||
data: TData
|
||||
result: any
|
||||
isTrigger: number
|
||||
|
||||
timeStamp: number
|
||||
handleObj: Store.HandlerObject
|
||||
@ -106,7 +105,10 @@ export namespace EventObject {
|
||||
}
|
||||
|
||||
export namespace EventObject {
|
||||
export function addProp(name: string, hook?: any | ((e: EventRaw) => any)) {
|
||||
export function addProperty(
|
||||
name: string,
|
||||
hook?: any | ((e: EventRaw) => any),
|
||||
) {
|
||||
Object.defineProperty(EventObject.prototype, name, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
@ -179,7 +181,7 @@ export namespace EventObject {
|
||||
}
|
||||
|
||||
Object.keys(commonProps).forEach((name: keyof typeof commonProps) =>
|
||||
EventObject.addProp(name, commonProps[name]),
|
||||
EventObject.addProperty(name, commonProps[name]),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,262 +1,21 @@
|
||||
import { isAncestorOf } from '../../util'
|
||||
import { Util } from './util'
|
||||
import { Hook } from './hook'
|
||||
import { Core } from './core'
|
||||
import { Store } from './store'
|
||||
|
||||
// Prevent triggered image.load events from bubbling to window.load
|
||||
export namespace Special {
|
||||
// Prevent triggered image.load events from bubbling to window.load
|
||||
Hook.register('load', {
|
||||
noBubble: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Support: Chrome <=73+
|
||||
// Chrome doesn't alert on `event.preventDefault()`
|
||||
// as the standard mandates.
|
||||
export namespace Special {
|
||||
Hook.register('beforeunload', {
|
||||
postDispatch(elem, event) {
|
||||
// Support: Chrome <=73+
|
||||
// Chrome doesn't alert on `event.preventDefault()`
|
||||
// as the standard mandates.
|
||||
if (event.result !== undefined && event.originalEvent) {
|
||||
event.originalEvent.returnValue = event.result
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
const events = {
|
||||
mouseenter: 'mouseover',
|
||||
mouseleave: 'mouseout',
|
||||
pointerenter: 'pointerover',
|
||||
pointerleave: 'pointerout',
|
||||
}
|
||||
Object.keys(events).forEach((type: keyof typeof events) => {
|
||||
const delegateType = events[type]
|
||||
Hook.register(type, {
|
||||
delegateType,
|
||||
bindType: delegateType,
|
||||
handle(target, event, ...args) {
|
||||
let ret
|
||||
const related = event.relatedTarget
|
||||
const handleObj = event.handleObj
|
||||
|
||||
// For mouseenter/leave call the handler if related is outside the target.
|
||||
// NB: No relatedTarget if the mouse left/entered the browser window
|
||||
if (
|
||||
!related ||
|
||||
(related !== target &&
|
||||
!isAncestorOf(target as Element, related as Element))
|
||||
) {
|
||||
event.type = handleObj.originType
|
||||
ret = handleObj.handler.call(this, event, ...args)
|
||||
event.type = delegateType
|
||||
}
|
||||
return ret
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export namespace Special {
|
||||
namespace State {
|
||||
type Item = boolean | any[] | { value: any }
|
||||
|
||||
const cache: WeakMap<
|
||||
Store.EventTarget,
|
||||
Record<string, Item>
|
||||
> = new WeakMap()
|
||||
|
||||
export function has(elem: Store.EventTarget, type: string) {
|
||||
return cache.has(elem) && cache.get(elem)![type] != null
|
||||
}
|
||||
|
||||
export function get(elem: Store.EventTarget, type: string) {
|
||||
const item = cache.get(elem)
|
||||
if (item) {
|
||||
return item[type]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function set(elem: Store.EventTarget, type: string, val: Item) {
|
||||
if (!cache.has(elem)) {
|
||||
cache.set(elem, {})
|
||||
}
|
||||
const bag = cache.get(elem)!
|
||||
bag[type] = val
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function leverageNative(
|
||||
elem: Store.EventTarget,
|
||||
type: string,
|
||||
isSync?: (taget: Store.EventTarget, t: string) => boolean,
|
||||
) {
|
||||
if (!isSync) {
|
||||
if (!State.has(elem, type)) {
|
||||
Core.on(elem, type, Util.returnTrue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Register the controller as a special universal handler for all event namespaces
|
||||
State.set(elem, type, false)
|
||||
|
||||
Core.on(elem, type, {
|
||||
namespace: false,
|
||||
handler(event, ...args) {
|
||||
const node = this as HTMLElement
|
||||
const nativeHandle = node[type as 'click']
|
||||
let saved = State.get(this, type)!
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if (event.isTrigger & 1 && nativeHandle) {
|
||||
// Interrupt processing of the outer synthetic .trigger()ed event
|
||||
// Saved data should be false in such cases, but might be a leftover
|
||||
// capture object from an async native handler
|
||||
if (!Array.isArray(saved)) {
|
||||
// Store arguments for use when handling the inner native event
|
||||
// There will always be at least one argument (an event object),
|
||||
// so this array will not be confused with a leftover capture object.
|
||||
saved = [event, ...args]
|
||||
State.set(node, type, saved)
|
||||
|
||||
// Trigger the native event and capture its result
|
||||
// Support: IE <=9 - 11+
|
||||
// focus() and blur() are asynchronous
|
||||
const notAsync = isSync(this, type)
|
||||
nativeHandle()
|
||||
let result = State.get(this, type)
|
||||
if (saved !== result || notAsync) {
|
||||
State.set(this, type, false)
|
||||
} else {
|
||||
result = { value: undefined }
|
||||
}
|
||||
|
||||
if (saved !== result) {
|
||||
// Cancel the outer synthetic event
|
||||
event.stopImmediatePropagation()
|
||||
event.preventDefault()
|
||||
|
||||
// Support: Chrome 86+
|
||||
// In Chrome, if an element having a focusout handler is blurred
|
||||
// by clicking outside of it, it invokes the handler synchronously.
|
||||
// If that handler calls `.remove()` on the element, the data is
|
||||
// cleared, leaving `result` undefined. We need to guard against
|
||||
// this.
|
||||
return result && (result as any).value
|
||||
}
|
||||
|
||||
// If this is an inner synthetic event for an event with a bubbling
|
||||
// surrogate (focus or blur), assume that the surrogate already
|
||||
// propagated from triggering the native event and prevent that
|
||||
// from happening again here. This technically gets the ordering
|
||||
// wrong w.r.t. to `.trigger()` (in which the bubbling surrogate
|
||||
// propagates *after* the non-bubbling base), but that seems less
|
||||
// bad than duplication.
|
||||
} else if (Hook.get(type).delegateType) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
// If this is a native event triggered above, everything is now in order
|
||||
// Fire an inner synthetic event with the original arguments
|
||||
} else if (saved && Array.isArray(saved)) {
|
||||
// ...and capture the result
|
||||
State.set(this, type, {
|
||||
value: Core.trigger(
|
||||
// Support: IE <=9 - 11+
|
||||
// Extend with the prototype to reset the above stopImmediatePropagation()
|
||||
jQuery.extend(saved[0], jQuery.Event.prototype),
|
||||
saved.slice(1),
|
||||
this,
|
||||
),
|
||||
})
|
||||
|
||||
// Abort handling of the native event
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Utilize native event to ensure correct state for checkable inputs
|
||||
Hook.register('click', {
|
||||
setup(elem) {
|
||||
if (Util.isCheckableInput(elem)) {
|
||||
leverageNative(elem, 'click', Util.returnTrue)
|
||||
}
|
||||
|
||||
// Return false to allow normal processing in the caller
|
||||
return false
|
||||
},
|
||||
trigger(elem) {
|
||||
// Force setup before triggering a click
|
||||
if (Util.isCheckableInput(elem)) {
|
||||
leverageNative(elem, 'click')
|
||||
}
|
||||
|
||||
// Return non-false to allow normal event-path propagation
|
||||
return true
|
||||
},
|
||||
|
||||
// For cross-browser consistency, suppress native .click() on links
|
||||
// Also prevent it if we're currently inside a leveraged native-event stack
|
||||
default(elem, event) {
|
||||
const target = event.target
|
||||
return (
|
||||
(Util.isCheckableInput(elem) && State.get(target, 'click')) ||
|
||||
(target &&
|
||||
(target as Node).nodeName &&
|
||||
(target as Node).nodeName.toLowerCase() === 'a')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// focus/blur
|
||||
// ----------
|
||||
|
||||
// Support: IE <=9 - 11+
|
||||
// focus() and blur() are asynchronous, except when they are no-op.
|
||||
// So expect focus to be synchronous when the element is already active,
|
||||
// and blur to be synchronous when the element is not already active.
|
||||
// (focus and blur are always synchronous in other supported browsers,
|
||||
// this just defines when we can count on it).
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function expectSync(elem: Element, type: string) {
|
||||
return (elem === document.activeElement) === (type === 'focus')
|
||||
}
|
||||
|
||||
const events = { focus: 'focusin', blur: 'focusout' }
|
||||
Object.keys(events).forEach((type: keyof typeof events) => {
|
||||
const delegateType = events[type]
|
||||
|
||||
// Utilize native event if possible so blur/focus sequence is correct
|
||||
Hook.register(type, {
|
||||
delegateType,
|
||||
setup(elem) {
|
||||
// Claim the first handler
|
||||
// cache.set( elem, "focus", ... )
|
||||
// cache.set( elem, "blur", ... )
|
||||
leverageNative(elem, type, expectSync)
|
||||
|
||||
// Return false to allow normal processing in the caller
|
||||
return false
|
||||
},
|
||||
trigger(elem) {
|
||||
// Force setup before trigger
|
||||
leverageNative(elem, type)
|
||||
|
||||
// Return non-false to allow normal event-path propagation
|
||||
return true
|
||||
},
|
||||
|
||||
// Suppress native focus or blur as it's already being fired
|
||||
// in leverageNative.
|
||||
default() {
|
||||
return true
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -4,14 +4,13 @@ export namespace Store {
|
||||
export type EventTarget = Element | Record<string, unknown>
|
||||
|
||||
export interface HandlerObject {
|
||||
guid: number
|
||||
type: string
|
||||
originType: string
|
||||
data?: any
|
||||
handler: EventHandler<any, any>
|
||||
guid: number
|
||||
data?: any
|
||||
selector?: string
|
||||
namespace: string | false
|
||||
needsContext: boolean
|
||||
namespace?: string
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
|
@ -30,36 +30,26 @@ export namespace Util {
|
||||
}
|
||||
|
||||
export namespace Util {
|
||||
const rnothtmlwhite = /[^\x20\t\r\n\f]+/g
|
||||
const rtypenamespace = /^([^.]*)(?:\.(.+)|)/
|
||||
const rcheckableInput = /^(?:checkbox|radio)$/i
|
||||
const whitespace = '[\\x20\\t\\r\\n\\f]'
|
||||
const rneedsContext = new RegExp(
|
||||
`^${whitespace}*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(${whitespace}*((?:-\\d)?\\d*)${whitespace}*\\)|)(?=[^-]|$)`,
|
||||
'i',
|
||||
)
|
||||
const rNotHTMLWhite = /[^\x20\t\r\n\f]+/g
|
||||
const rNamespace = /^([^.]*)(?:\.(.+)|)/
|
||||
|
||||
export function splitType(types: string) {
|
||||
return (types || '').match(rnothtmlwhite) || ['']
|
||||
return (types || '').match(rNotHTMLWhite) || ['']
|
||||
}
|
||||
|
||||
export function normalizeType(type: string) {
|
||||
const parts = rtypenamespace.exec(type) || []
|
||||
const parts = rNamespace.exec(type) || []
|
||||
return {
|
||||
originType: parts[1],
|
||||
namespaces: parts[2] ? parts[2].split('.').sort() : [],
|
||||
originType: parts[1] ? parts[1].trim() : parts[1],
|
||||
namespaces: parts[2]
|
||||
? parts[2]
|
||||
.split('.')
|
||||
.map((ns) => ns.trim())
|
||||
.sort()
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function isCheckableInput(elem: Store.EventTarget) {
|
||||
const node = elem as HTMLInputElement
|
||||
return (
|
||||
node.click != null &&
|
||||
node.nodeName.toLowerCase() === 'input' &&
|
||||
rcheckableInput.test(node.type)
|
||||
)
|
||||
}
|
||||
|
||||
export function isValidTarget(target: Element | Record<string, any>) {
|
||||
// Accepts only:
|
||||
// - Node
|
||||
@ -72,16 +62,11 @@ export namespace Util {
|
||||
|
||||
export function isValidSelector(elem: Store.EventTarget, selector?: string) {
|
||||
if (selector) {
|
||||
const doce = document.documentElement
|
||||
const matches = doce.matches || (doce as any).msMatchesSelector
|
||||
return matches.call(elem, selector) as boolean
|
||||
const node = elem as Element
|
||||
return node.querySelector != null && node.querySelector(selector) != null
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function needsContext(selector?: string) {
|
||||
return selector != null && rneedsContext.test(selector)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Util {
|
||||
|
Reference in New Issue
Block a user