Skip to main content

DOM Events

  • event.preventDefault().
  • event.stopPropagation().
  • By default, event handlers are executed in the bubbling phase (unless set useCapture to true).
  • element.dispatchEvent(event) to trigger events.

Events Object

Property/MethodType
typeString被触发的事件类型
trustedBoolean浏览器生成/JavaScript 创建
ViewAbstractView事件所发生的 window 对象
currentTargetElementEvent handler binding
targetElementEvent trigger
bubblesBoolean事件是否冒泡
cancelableBoolean是否可以取消事件的默认行为
eventPhaseNumber捕获阶段/到达目标/冒泡阶段
defaultPreventedBooleanpreventDefault() called
preventDefault()Function用于取消事件的默认行为
stopPropagation()Function用于取消所有后续事件捕获或冒泡
stopImmediatePropagation()Function用于取消所有后续事件捕获或冒泡

Events Checking

function handleEvent(event) {
node.matches(event.target) // return false or true
node.contains(event.target) // return false or true
}

Global UI Events

DOMContentLoaded event:

  • 当文档中没有脚本时, 浏览器解析完 HTML 文档便能触发 DOMContentLoaded 事件.
  • 如果文档中包含脚本, 则脚本会阻塞文档的解析, 脚本需要等 CSSOM 构建完成才能执行:
    • DOM/CSSOM 构建完毕, async 脚本执行完成之后, DOMContentLoaded 事件触发.
    • HTML 文档构建不受 defer 脚本影响, 不需要等待 defer 脚本执行与样式表加载, HTML 解析完毕后, DOMContentLoaded 立即触发.
  • 在任何情况下, DOMContentLoaded 的触发不需要等待图片等其他资源加载完成.
  • HTML 文档解析完成就会触发 DOMContentLoaded, 所有资源加载完成之后, load 事件才会被触发.
function ready(fn) {
if (document.readyState !== 'loading')
fn()
else
document.addEventListener('DOMContentLoaded', fn)
}

document.addEventListener('DOMContentLoaded', (event) => {
console.log('DOM fully loaded and parsed.')
})

readystatechange event:

document.addEventListener('readystatechange', (event) => {
// HTML5 readyState
if (
document.readyState === 'interactive'
|| document.readyState === 'complete'
) {
console.log('Content loaded')
} else if (document.readyState === 'loading') {
console.log('Loading')
}
})

load event (加载完成):

window.addEventListener('load', () => {
const image = document.createElement('img')
image.addEventListener('load', (event) => {
console.log(event.target.src)
})
document.body.appendChild(image)
image.src = 'smile.gif'

const script = document.createElement('script')
script.addEventListener('load', (event) => {
console.log('Loaded')
})
script.src = 'example.js'
script.async = true
document.body.appendChild(script)

const link = document.createElement('link')
link.type = 'text/css'
link.rel = 'stylesheet'
link.addEventListener('load', (event) => {
console.log('css loaded')
})
link.href = 'example.css'
document.getElementsByTagName('head')[0].appendChild(link)
})

visibilitychange event, 切换标签页时改变网页标题/声音/视频:

window.addEventListener('visibilitychange', () => {
switch (document.visibilityState) {
case 'hidden':
console.log('Tab隐藏')
break
case 'visible':
console.log('Tab被聚焦')
break
default:
throw new Error('Unsupported visibility!')
}
})
const videoElement = document.getElementById('videoElement')

// AutoPlay the video if application is visible
if (document.visibilityState === 'visible')
videoElement.play()

// Handle page visibility change events
function handleVisibilityChange() {
if (document.visibilityState === 'hidden')
videoElement.pause()
else
videoElement.play()
}

document.addEventListener('visibilitychange', handleVisibilityChange, false)

pageshow event (e.g. BFCache compatible):

window.addEventListener('pageshow', (event) => {
if (event.persisted)
console.log('Page was loaded from cache.')
})
  • beforeunload event.
  • unload event: 卸载完成.
  • abort event: 提前终止.
  • error event.
  • select event: 在文本框 (<input>textarea) 上选择字符.
  • resize event: 缩放.
  • scroll event: 滚动.

Form Events

// <form className='validated-form' noValidate onSubmit={onSubmit}>

function onSubmit(event) {
event.preventDefault()

const form = event.target
const isValid = form.checkValidity() // returns true or false
const formData = new FormData(form)

const validationMessages = Array.from(formData.keys()).reduce((acc, key) => {
acc[key] = form.elements[key].validationMessage
return acc
}, {})

setErrors(validationMessages)

console.log({
validationMessages,
data,
isValid,
})

if (isValid) {
// here you do what you need to do if is valid
const data = Array.from(formData.keys()).reduce((acc, key) => {
acc[key] = formData.get(key)
return acc
}, {})
} else {
// apply invalid class
Array.from(form.elements).forEach((i) => {
if (i.checkValidity()) {
// field is valid
i.parentElement.classList.remove('invalid')
} else {
// field is invalid
i.parentElement.classList.add('invalid')
console.log(i.validity)
}
})
}
}
document.querySelector('form').addEventListener('submit', (event) => {
const form = event.target
const url = new URL(form.action || window.location.href)
const formData = new FormData(form)
const searchParameters = new URLSearchParams(formData)

const options = {
method: form.method,
}

if (options.method === 'post') {
// Modify request body to include form data
options.body
= form.enctype === 'multipart/form-data' ? formData : searchParameters
} else {
// Modify URL to include form data
url.search = searchParameters
}

fetch(url, options)
event.preventDefault()
})
import { useRouter } from 'next/navigation'

export default function Page() {
const router = useRouter()

const handleSubmit = (event) => {
event.preventDefault()

const formData = new FormData(event.target)
const data = Object.fromEntries(formData)

// Do something with data. Most likely, send it to the server using fetch

// Redirect the user to the new page
router.push('/thank-you')
}

return <form onSubmit={handleSubmit}>...</form>
}

Input Events

  • blur/focus/focusin/focusout event.
  • input/change event.
  • select event: 在文本框 (<input>textarea) 上选择字符.
  • composition event: 中文输入事件.

Input Focus Event

HTML5 focus management:

  • 在页面完全加载之前, document.activeElement 为 null.
  • 默认情况下, document.activeElement 在页面刚加载完之后会设置为 document.body.
document.getElementById('myButton').focus()
console.log(document.activeElement === button) // true
console.log(document.hasFocus()) // true
Focus Events

当焦点从页面中的一个元素移到另一个元素上时, 会依次发生如下事件:

  1. focusout: 在失去焦点的元素上触发.
  2. focusin: 在获得焦点的元素上触发
  3. blur: 在失去焦点的元素上触发
  4. DOMFocusOut: 在失去焦点的元素上触发
  5. focus: 在获得焦点的元素上触发
  6. DOMFocusIn: 在获得焦点的元素上触发.

Input Change Event

  • input event:
    • <input type="text" />.
    • <input type="password"/>.
    • <textarea />.
  • change event:
    • <input type="checkbox" />.
    • <input type="radio" />.
    • <input type="file" />.
    • <input type="file" multiple />.
    • <select />.
const input = document.querySelector('input')

input.addEventListener('change', () => {
for (const file of Array.from(input.files)) {
const reader = new FileReader()
reader.addEventListener('load', () => {
console.log('File', file.name, 'starts with', reader.result.slice(0, 20))
})
reader.readAsText(file)
}
})

Input Select Event

const input = document.querySelector('input')

input.addEventListener('select', (event) => {
const log = document.getElementById('log')
const selection = event.target.value.substring(
event.target.selectionStart,
event.target.selectionEnd
)
log.textContent = `You selected: ${selection}`
})

Clipboard Events

Clipboard API (modern alternative for document.execCommand(command)):

  • copy event.
  • cut event.
  • paste event.
const source = document.querySelector('div.source')

source.addEventListener('copy', (event) => {
const selection = document.getSelection()
event.clipboardData.setData(
'text/plain',
selection.toString().concat('copyright information')
)
event.preventDefault()
})

Mouse Events

  • mousedown event.
  • mouseup event.
  • click event:
    • mousedownmouseup 都触发后, 触发此事件.
    • event.clientX/event.clientY.
    • event.pageX/event.pageY.
    • event.screenX/event.screenY.
    • event.shiftKey/event.ctrlKey/event.altKey/event.metaKey.
  • dbclick event: click 两次触发后, 触发此事件.
  • mousemove event.
  • mouseenter event.
  • mouseleave event: pointer has exited the element and all of its descendants.
  • mouseout event: pointer leaves the element or leaves one of the element's descendants.
  • mouseover event.
  • wheel event (replace deprecated mousewheel event).

For click event, no need for X/Y to judge internal/outside state. Use element.contains to check is a better way.

window.addEventListener('click', (event) => {
if (document.getElementById('main').contains(event.target))
process()
})

Drag Event:

  • dragstart: start point.
  • dragend
  • dragenter: call event.preventDefault() in drop zone.
  • dragover: call event.preventDefault() in drop zone.
  • dragleave
  • drop: end point.

Key point for implementing DnD widget is DataTransfer:

  • Bindings between Drag Zone and Drop Zone.
  • DataTransfer.dropEffect and DataTransfer.effectAllowed to define DnD UI type.
  • DataTransfer.getData and DataTransfer.setData to transfer data.
  • DataTransfer.files and DataTransfer.items to transfer data.

Context Menu Event:

const noContext = document.getElementById('noContextMenu')

noContext.addEventListener('contextmenu', (e) => {
e.preventDefault()
})

Keyboard Events

keydown/keypress/keyup event:

const textbox = document.getElementById('myText')

textbox.addEventListener('keyup', (event) => {
console.log(event.charCode || event.keyCode)
})

event.key (replace deprecated event.keyCode):

'Alt'
'CapsLock'
'Control'
'Fn'
'Numlock'
'Shift'
'Enter'
'Tab'
' ' // space bar

'ArrowDown'
'ArrowLeft'
'ArrowRight'
'ArrowUp'
'Home'
'End'
'PageDOwn'
'PageUp'

'Backspace'
'Delete'
'Redo'
'Undo'

Device Events

  • deviceorientation event.
  • devicemotion event.
  • touchstart event.
  • touchmove event.
  • touchend event.
  • touchcancel event.

Use touch events:

  • Dispatch custom tap/press/swipe/pinch/drag/drop/rotate event.
  • Dispatch standard click/dbclick/mousedown/mouseup/mousemove` event.
interface Pointer {
startTouch: Touch
startTime: number
status: string
element: TouchEventTarget
lastTouch?: Touch
lastTime?: number
deltaX?: number
deltaY?: number
duration?: number
distance?: number
isVertical?: boolean
}

type TouchEventTarget = HTMLDivElement | EventTarget
type TouchEventHandler = (pointer: Pointer, touch: Touch) => void

class Recognizer {
pointers: Map<Touch['identifier'], Pointer>

constructor() {
this.pointers = new Map()
}

start(event: TouchEvent, callback?: TouchEventHandler) {
// touches: 当前屏幕上所有触摸点的列表.
// targetTouches: 当前对象上所有触摸点的列表.
// changedTouches: 涉及当前事件的触摸点的列表.
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i]
const pointer: Pointer = {
startTouch: touch,
startTime: Date.now(),
status: 'tapping',
element: event.target,
}
this.pointers.set(touch.identifier, pointer)
if (callback)
callback(pointer, touch)
}
}

move(event: TouchEvent, callback?: TouchEventHandler) {
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i]
const pointer = this.pointers.get(touch.identifier)

if (!pointer)
return

if (!pointer.lastTouch) {
pointer.lastTouch = pointer.startTouch
pointer.lastTime = pointer.startTime
pointer.deltaX = 0
pointer.deltaY = 0
pointer.duration = 0
pointer.distance = 0
}

let time = Date.now() - pointer.lastTime

if (time > 0) {
const RECORD_DURATION = 70

if (time > RECORD_DURATION)
time = RECORD_DURATION

if (pointer.duration + time > RECORD_DURATION)
pointer.duration = RECORD_DURATION - time

pointer.duration += time
pointer.lastTouch = touch
pointer.lastTime = Date.now()
pointer.deltaX = touch.clientX - pointer.startTouch.clientX
pointer.deltaY = touch.clientY - pointer.startTouch.clientY
const x = pointer.deltaX * pointer.deltaX
const y = pointer.deltaY * pointer.deltaY
pointer.distance = Math.sqrt(x + y)
pointer.isVertical = x < y

if (callback)
callback(pointer, touch)
}
}
}

end(event: TouchEvent, callback?: TouchEventHandler) {
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i]
const id = touch.identifier
const pointer = this.pointers.get(id)

if (!pointer)
continue
if (callback)
callback(pointer, touch)

this.pointers.delete(id)
}
}

cancel(event: TouchEvent, callback?: TouchEventHandler) {
this.end(event, callback)
}

fire(elem: TouchEventTarget, type: string, props: EventInit) {
if (elem) {
const event = new Event(type, {
bubbles: true,
cancelable: true,
...props,
})
elem.dispatchEvent(event)
}
}

static bind(el: TouchEventTarget, recognizer: Recognizer) {
function move(event: TouchEvent) {
recognizer.move(event)
}

function end(event: TouchEvent) {
recognizer.end(event)
document.removeEventListener('touchmove', move)
document.removeEventListener('touchend', end)
document.removeEventListener('touchcancel', cancel)
}

function cancel(event: TouchEvent) {
recognizer.cancel(event)
document.removeEventListener('touchmove', move)
document.removeEventListener('touchend', end)
document.removeEventListener('touchcancel', cancel)
}

el.addEventListener('touchstart', (event: TouchEvent) => {
recognizer.start(event)
document.addEventListener('touchmove', move)
document.addEventListener('touchend', end)
document.addEventListener('touchcancel', cancel)
})
}
}

export default Recognizer

Dispatch Events

Dispatch MouseEvent:

const btn = document.getElementById('myBtn')

// 创建 event 对象
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: document.defaultView,
})

// 触发事件
btn.dispatchEvent(event)

Dispatch KeyboardEvent:

const textbox = document.getElementById('myTextbox')

// 按照 DOM3 的方式创建 event 对象
if (document.implementation.hasFeature('KeyboardEvents', '3.0')) {
// 初始化 event 对象
const event = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
view: document.defaultView,
key: 'a',
location: 0,
shiftKey: true,
})

// 触发事件
textbox.dispatchEvent(event)
}

Dispatch CustomEvent:

const div = document.getElementById('myDiv')
div.addEventListener('myEvent', (event) => {
console.log(`DIV: ${event.detail}`)
})
document.addEventListener('myEvent', (event) => {
console.log(`DOCUMENT: ${event.detail}`)
})

if (document.implementation.hasFeature('CustomEvents', '3.0')) {
const event = new CustomEvent('myEvent', {
bubbles: true,
cancelable: true,
detail: 'Hello world!',
})
div.dispatchEvent(event)
}

Events Util

class EventUtil {
static getEvent(event) {
return event || window.event
}

static getTarget(event) {
return event.target || event.srcElement
}

static getRelatedTarget(event) {
// For `mouseover` and `mouseout` event:
if (event.relatedTarget)
return event.relatedTarget
else if (event.toElement)
return event.toElement
else if (event.fromElement)
return event.fromElement
else
return null
}

static preventDefault(event) {
if (event.preventDefault)
event.preventDefault()
else
event.returnValue = false
}

static stopPropagation(event) {
if (event.stopPropagation)
event.stopPropagation()
else
event.cancelBubble = true
}

static addHandler(element, type, handler) {
if (element.addEventListener)
element.addEventListener(type, handler, false)
else if (element.attachEvent)
element.attachEvent(`on${type}`, handler)
else
element[`on${type}`] = handler
}

static removeHandler(element, type, handler) {
if (element.removeEventListener)
element.removeEventListener(type, handler, false)
else if (element.detachEvent)
element.detachEvent(`on${type}`, handler)
else
element[`on${type}`] = null
}
}