refactor: ♻️ remove useless hooks and add test specs

This commit is contained in:
bubkoo
2021-04-15 23:14:32 +08:00
parent a2da80e9a5
commit acceaa5275
9 changed files with 841 additions and 414 deletions

View File

@ -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' &&

View 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)
})
})
})
})

View File

@ -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
}
}

View File

@ -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,

View File

@ -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'))
// }
}

View File

@ -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]),
)
}

View File

@ -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
},
})
})
}

View File

@ -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 {

View File

@ -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 {