DOM Events
event.preventDefault().event.stopPropagation().- By default, event handlers are executed in the bubbling phase
(unless set
useCapturetotrue). element.dispatchEvent(event)to trigger events.
Events Object
| Property/Method | Type | |
|---|---|---|
| type | String | 被触发的事件类型 |
| trusted | Boolean | 浏览器生成/JavaScript 创建 |
| View | AbstractView | 事件所发生的 window 对象 |
| currentTarget | Element | Event handler binding |
| target | Element | Event trigger |
| bubbles | Boolean | 事件是否冒泡 |
| cancelable | Boolean | 是否可以取消事件的默认行为 |
| eventPhase | Number | 捕获阶段/到达目标/冒泡阶段 |
| defaultPrevented | Boolean | preventDefault() 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.')
})
beforeunloadevent.unloadevent: 卸载完成.abortevent: 提前终止.errorevent.selectevent: 在文本框 (<input>或textarea) 上选择字符.resizeevent: 缩放.scrollevent: 滚动.
Form Events
submit/resetevent.- FromData API
- CheckValidity API
// <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/focusoutevent.input/changeevent.selectevent: 在文本框 (<input>或textarea) 上选择字符.compositionevent: 中文输入事件.
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
当焦点从页面中的一个元素移到另一个元素上时, 会依次发生如下事件:
focusout: 在失去焦点的元素上触发.focusin: 在获得焦点的元素上触发blur: 在失去焦点的元素上触发DOMFocusOut: 在失去焦点的元素上触发focus: 在获得焦点的元素上触发DOMFocusIn: 在获得焦点的元素上触发.
Input Change Event
inputevent:<input type="text" />.<input type="password"/>.<textarea />.
changeevent:<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)):
copyevent.cutevent.pasteevent.
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
mousedownevent.mouseupevent.clickevent:mousedown与mouseup都触发后, 触发此事件.event.clientX/event.clientY.event.pageX/event.pageY.event.screenX/event.screenY.event.shiftKey/event.ctrlKey/event.altKey/event.metaKey.
dbclickevent:click两次触发后, 触发此事件.mousemoveevent.mouseenterevent.mouseleaveevent: pointer has exited the element and all of its descendants.mouseoutevent: pointer leaves the element or leaves one of the element's descendants.mouseoverevent.wheelevent (replace deprecatedmousewheelevent).
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()
})
- 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.dropEffectandDataTransfer.effectAllowedto define DnD UI type.DataTransfer.getDataandDataTransfer.setDatato transfer data.DataTransfer.filesandDataTransfer.itemsto transfer data.
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
deviceorientationevent.devicemotionevent.touchstartevent.touchmoveevent.touchendevent.touchcancelevent.
Use
touch
events:
- Dispatch custom
tap/press/swipe/pinch/drag/drop/rotateevent. - 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
}
}