3.1 事件模型概述
3.1.1 事件流机制
Sciter-JS 采用标准的 DOM 事件模型,支持事件捕获和冒泡:
graph TD
A[事件触发] --> B[捕获阶段]
B --> C[目标阶段]
C --> D[冒泡阶段]
E[Document] --> F[HTML]
F --> G[Body]
G --> H[Container]
H --> I[Target Element]
B -.-> E
B -.-> F
B -.-> G
B -.-> H
C -.-> I
D -.-> H
D -.-> G
D -.-> F
D -.-> E
3.1.2 事件对象结构
// 事件对象的基本结构
function handleEvent(event) {
console.log({
type: event.type, // 事件类型
target: event.target, // 事件目标
currentTarget: event.currentTarget, // 当前处理元素
phase: event.eventPhase, // 事件阶段
bubbles: event.bubbles, // 是否冒泡
cancelable: event.cancelable, // 是否可取消
timeStamp: event.timeStamp, // 时间戳
detail: event.detail // 自定义数据
});
}
// Sciter 扩展的事件属性
function handleSciterEvent(event) {
console.log({
x: event.x, // 相对于元素的 X 坐标
y: event.y, // 相对于元素的 Y 坐标
screenX: event.screenX, // 屏幕 X 坐标
screenY: event.screenY, // 屏幕 Y 坐标
buttons: event.buttons, // 鼠标按键状态
keyCode: event.keyCode, // 键盘码
altKey: event.altKey, // Alt 键状态
ctrlKey: event.ctrlKey, // Ctrl 键状态
shiftKey: event.shiftKey, // Shift 键状态
metaKey: event.metaKey // Meta 键状态
});
}
3.1.3 事件监听器管理
// 添加事件监听器
const element = document.querySelector(".interactive-element");
// 标准方式
element.addEventListener("click", handleClick, false);
element.addEventListener("click", handleClick, {
capture: false,
once: true,
passive: true
});
// Sciter 简化语法
element.on("click", handleClick);
element.on("click.namespace", handleClick); // 带命名空间
// 移除事件监听器
element.removeEventListener("click", handleClick);
element.off("click", handleClick);
element.off("click.namespace"); // 移除特定命名空间
element.off(".namespace"); // 移除所有命名空间事件
// 一次性事件
element.once("click", function() {
console.log("只执行一次");
});
// 事件委托
document.on("click", ".button", function(event) {
console.log("委托处理按钮点击", this);
});
3.2 鼠标事件处理
3.2.1 基础鼠标事件
class MouseEventHandler {
constructor(element) {
this.element = element;
this.setupMouseEvents();
}
setupMouseEvents() {
// 点击事件
this.element.on("click", this.handleClick.bind(this));
this.element.on("dblclick", this.handleDoubleClick.bind(this));
this.element.on("contextmenu", this.handleContextMenu.bind(this));
// 鼠标按下/释放
this.element.on("mousedown", this.handleMouseDown.bind(this));
this.element.on("mouseup", this.handleMouseUp.bind(this));
// 鼠标移动
this.element.on("mousemove", this.handleMouseMove.bind(this));
this.element.on("mouseenter", this.handleMouseEnter.bind(this));
this.element.on("mouseleave", this.handleMouseLeave.bind(this));
this.element.on("mouseover", this.handleMouseOver.bind(this));
this.element.on("mouseout", this.handleMouseOut.bind(this));
// 滚轮事件
this.element.on("wheel", this.handleWheel.bind(this));
}
handleClick(event) {
console.log("点击事件", {
button: event.button, // 0: 左键, 1: 中键, 2: 右键
buttons: event.buttons,
position: { x: event.x, y: event.y },
modifiers: {
ctrl: event.ctrlKey,
shift: event.shiftKey,
alt: event.altKey
}
});
// 阻止默认行为
if (event.ctrlKey) {
event.preventDefault();
}
}
handleDoubleClick(event) {
console.log("双击事件");
// 选择文本
if (this.element.textContent) {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(this.element);
selection.removeAllRanges();
selection.addRange(range);
}
}
handleContextMenu(event) {
console.log("右键菜单事件");
// 阻止默认右键菜单
event.preventDefault();
this.showCustomContextMenu(event.x, event.y);
}
handleMouseDown(event) {
console.log("鼠标按下", event.button);
this.element.classList.add("pressed");
// 开始拖拽检测
this.dragStart = { x: event.x, y: event.y };
this.isDragging = false;
}
handleMouseUp(event) {
console.log("鼠标释放", event.button);
this.element.classList.remove("pressed");
if (this.isDragging) {
this.handleDragEnd(event);
}
this.dragStart = null;
this.isDragging = false;
}
handleMouseMove(event) {
if (this.dragStart) {
const deltaX = event.x - this.dragStart.x;
const deltaY = event.y - this.dragStart.y;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (!this.isDragging && distance > 5) {
this.isDragging = true;
this.handleDragStart(event);
}
if (this.isDragging) {
this.handleDrag(event, deltaX, deltaY);
}
}
// 更新鼠标位置显示
this.updateMousePosition(event.x, event.y);
}
handleMouseEnter(event) {
console.log("鼠标进入");
this.element.classList.add("hover");
}
handleMouseLeave(event) {
console.log("鼠标离开");
this.element.classList.remove("hover");
}
handleWheel(event) {
console.log("滚轮事件", {
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaZ: event.deltaZ,
deltaMode: event.deltaMode
});
// 自定义滚动行为
if (event.ctrlKey) {
event.preventDefault();
this.handleZoom(event.deltaY > 0 ? -0.1 : 0.1);
}
}
showCustomContextMenu(x, y) {
// 创建自定义右键菜单
const menu = document.createElement("div");
menu.className = "context-menu";
menu.innerHTML = `
<div class="menu-item" data-action="copy">复制</div>
<div class="menu-item" data-action="paste">粘贴</div>
<div class="menu-separator"></div>
<div class="menu-item" data-action="delete">删除</div>
`;
menu.style.position = "fixed";
menu.style.left = x + "px";
menu.style.top = y + "px";
document.body.appendChild(menu);
// 菜单项点击处理
menu.on("click", ".menu-item", (event) => {
const action = event.target.dataset.action;
this.handleMenuAction(action);
menu.remove();
});
// 点击其他地方关闭菜单
document.once("click", () => {
if (menu.parentElement) {
menu.remove();
}
});
}
handleMenuAction(action) {
console.log("菜单操作:", action);
switch (action) {
case "copy":
this.copyContent();
break;
case "paste":
this.pasteContent();
break;
case "delete":
this.deleteContent();
break;
}
}
}
3.2.2 拖拽功能实现
class DragDropManager {
constructor() {
this.dragData = null;
this.dropZones = new Set();
this.setupGlobalEvents();
}
setupGlobalEvents() {
document.on("dragstart", this.handleDragStart.bind(this));
document.on("dragend", this.handleDragEnd.bind(this));
document.on("dragover", this.handleDragOver.bind(this));
document.on("drop", this.handleDrop.bind(this));
}
makeDraggable(element, data = {}) {
element.draggable = true;
element.dataset.dragData = JSON.stringify(data);
element.on("dragstart", (event) => {
this.dragData = {
element: element,
data: data,
startPosition: { x: event.x, y: event.y }
};
element.classList.add("dragging");
// 设置拖拽效果
event.dataTransfer.effectAllowed = "all";
event.dataTransfer.setData("text/plain", JSON.stringify(data));
// 自定义拖拽图像
const dragImage = this.createDragImage(element);
event.dataTransfer.setDragImage(dragImage, 0, 0);
});
element.on("dragend", (event) => {
element.classList.remove("dragging");
this.dragData = null;
});
}
makeDropZone(element, options = {}) {
this.dropZones.add(element);
const config = {
accept: options.accept || "*",
effect: options.effect || "move",
onDrop: options.onDrop || (() => {}),
onDragEnter: options.onDragEnter || (() => {}),
onDragLeave: options.onDragLeave || (() => {})
};
element.dataset.dropConfig = JSON.stringify(config);
element.on("dragover", (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = config.effect;
});
element.on("dragenter", (event) => {
event.preventDefault();
if (this.canAcceptDrop(element, this.dragData)) {
element.classList.add("drop-target");
config.onDragEnter(event, this.dragData);
}
});
element.on("dragleave", (event) => {
if (!element.contains(event.relatedTarget)) {
element.classList.remove("drop-target");
config.onDragLeave(event, this.dragData);
}
});
element.on("drop", (event) => {
event.preventDefault();
element.classList.remove("drop-target");
if (this.canAcceptDrop(element, this.dragData)) {
const dropData = {
dragData: this.dragData,
dropPosition: { x: event.x, y: event.y },
transferData: event.dataTransfer.getData("text/plain")
};
config.onDrop(event, dropData);
}
});
}
canAcceptDrop(dropZone, dragData) {
if (!dragData) return false;
const config = JSON.parse(dropZone.dataset.dropConfig || "{}");
const accept = config.accept || "*";
if (accept === "*") return true;
const dragType = dragData.data.type || "default";
return accept.includes(dragType);
}
createDragImage(element) {
const clone = element.cloneNode(true);
clone.style.opacity = "0.8";
clone.style.transform = "scale(0.9)";
clone.style.position = "absolute";
clone.style.top = "-1000px";
clone.style.left = "-1000px";
document.body.appendChild(clone);
setTimeout(() => {
if (clone.parentElement) {
clone.remove();
}
}, 100);
return clone;
}
}
// 使用示例
const dragDropManager = new DragDropManager();
// 创建可拖拽元素
const draggableItems = document.querySelectorAll(".draggable-item");
draggableItems.forEach((item, index) => {
dragDropManager.makeDraggable(item, {
type: "item",
id: index,
content: item.textContent
});
});
// 创建放置区域
const dropZones = document.querySelectorAll(".drop-zone");
dropZones.forEach(zone => {
dragDropManager.makeDropZone(zone, {
accept: ["item"],
effect: "move",
onDrop: (event, dropData) => {
console.log("放置成功", dropData);
zone.appendChild(dropData.dragData.element);
},
onDragEnter: () => {
zone.style.backgroundColor = "#e3f2fd";
},
onDragLeave: () => {
zone.style.backgroundColor = "";
}
});
});
3.3 键盘事件处理
3.3.1 键盘事件基础
class KeyboardEventHandler {
constructor() {
this.shortcuts = new Map();
this.setupKeyboardEvents();
}
setupKeyboardEvents() {
document.on("keydown", this.handleKeyDown.bind(this));
document.on("keyup", this.handleKeyUp.bind(this));
document.on("keypress", this.handleKeyPress.bind(this));
}
handleKeyDown(event) {
console.log("按键按下", {
key: event.key,
code: event.code,
keyCode: event.keyCode,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey
});
// 检查快捷键
const shortcutKey = this.getShortcutKey(event);
if (this.shortcuts.has(shortcutKey)) {
event.preventDefault();
this.shortcuts.get(shortcutKey)(event);
}
// 特殊按键处理
switch (event.key) {
case "Escape":
this.handleEscape(event);
break;
case "Enter":
this.handleEnter(event);
break;
case "Tab":
this.handleTab(event);
break;
case "ArrowUp":
case "ArrowDown":
case "ArrowLeft":
case "ArrowRight":
this.handleArrowKeys(event);
break;
}
}
handleKeyUp(event) {
console.log("按键释放", event.key);
}
handleKeyPress(event) {
console.log("按键输入", event.key);
}
getShortcutKey(event) {
const parts = [];
if (event.ctrlKey) parts.push("Ctrl");
if (event.shiftKey) parts.push("Shift");
if (event.altKey) parts.push("Alt");
if (event.metaKey) parts.push("Meta");
parts.push(event.key);
return parts.join("+");
}
addShortcut(keys, callback) {
this.shortcuts.set(keys, callback);
}
removeShortcut(keys) {
this.shortcuts.delete(keys);
}
handleEscape(event) {
// 关闭模态框、取消操作等
const modal = document.querySelector(".modal.active");
if (modal) {
modal.classList.remove("active");
}
}
handleEnter(event) {
// 表单提交、确认操作等
if (event.target.matches("input[type='text'], textarea")) {
if (event.ctrlKey) {
// Ctrl+Enter 提交表单
const form = event.target.closest("form");
if (form) {
form.dispatchEvent(new Event("submit"));
}
}
}
}
handleTab(event) {
// 自定义 Tab 导航
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const currentIndex = Array.from(focusableElements).indexOf(event.target);
let nextIndex;
if (event.shiftKey) {
nextIndex = currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1;
} else {
nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0;
}
if (focusableElements[nextIndex]) {
event.preventDefault();
focusableElements[nextIndex].focus();
}
}
handleArrowKeys(event) {
// 方向键导航
const currentElement = event.target;
const navigableElements = document.querySelectorAll(".navigable");
const currentIndex = Array.from(navigableElements).indexOf(currentElement);
if (currentIndex === -1) return;
let nextIndex = currentIndex;
switch (event.key) {
case "ArrowUp":
nextIndex = Math.max(0, currentIndex - 1);
break;
case "ArrowDown":
nextIndex = Math.min(navigableElements.length - 1, currentIndex + 1);
break;
case "ArrowLeft":
nextIndex = Math.max(0, currentIndex - 1);
break;
case "ArrowRight":
nextIndex = Math.min(navigableElements.length - 1, currentIndex + 1);
break;
}
if (nextIndex !== currentIndex) {
event.preventDefault();
navigableElements[nextIndex].focus();
}
}
}
// 使用示例
const keyboardHandler = new KeyboardEventHandler();
// 添加快捷键
keyboardHandler.addShortcut("Ctrl+S", (event) => {
console.log("保存快捷键");
// 执行保存操作
});
keyboardHandler.addShortcut("Ctrl+Z", (event) => {
console.log("撤销快捷键");
// 执行撤销操作
});
keyboardHandler.addShortcut("Ctrl+Shift+Z", (event) => {
console.log("重做快捷键");
// 执行重做操作
});
3.3.2 输入法和文本输入
class TextInputHandler {
constructor(element) {
this.element = element;
this.composing = false;
this.setupInputEvents();
}
setupInputEvents() {
// 输入法事件
this.element.on("compositionstart", this.handleCompositionStart.bind(this));
this.element.on("compositionupdate", this.handleCompositionUpdate.bind(this));
this.element.on("compositionend", this.handleCompositionEnd.bind(this));
// 输入事件
this.element.on("input", this.handleInput.bind(this));
this.element.on("beforeinput", this.handleBeforeInput.bind(this));
// 选择事件
this.element.on("select", this.handleSelect.bind(this));
this.element.on("selectionchange", this.handleSelectionChange.bind(this));
}
handleCompositionStart(event) {
console.log("输入法开始", event.data);
this.composing = true;
this.element.classList.add("composing");
}
handleCompositionUpdate(event) {
console.log("输入法更新", event.data);
}
handleCompositionEnd(event) {
console.log("输入法结束", event.data);
this.composing = false;
this.element.classList.remove("composing");
// 处理最终输入的文本
this.processComposedText(event.data);
}
handleInput(event) {
if (!this.composing) {
console.log("文本输入", {
inputType: event.inputType,
data: event.data,
value: this.element.value
});
this.processTextInput(event);
}
}
handleBeforeInput(event) {
console.log("输入前事件", {
inputType: event.inputType,
data: event.data
});
// 可以在这里阻止某些输入
if (this.shouldPreventInput(event)) {
event.preventDefault();
}
}
handleSelect(event) {
const selection = this.getSelection();
console.log("文本选择", selection);
}
handleSelectionChange(event) {
const selection = this.getSelection();
console.log("选择变化", selection);
}
getSelection() {
return {
start: this.element.selectionStart,
end: this.element.selectionEnd,
text: this.element.value.substring(
this.element.selectionStart,
this.element.selectionEnd
)
};
}
setSelection(start, end) {
this.element.setSelectionRange(start, end);
}
insertText(text) {
const selection = this.getSelection();
const value = this.element.value;
const newValue = value.substring(0, selection.start) +
text +
value.substring(selection.end);
this.element.value = newValue;
this.setSelection(selection.start + text.length, selection.start + text.length);
}
processComposedText(text) {
// 处理输入法输入的文本
console.log("处理输入法文本:", text);
}
processTextInput(event) {
// 处理普通文本输入
switch (event.inputType) {
case "insertText":
this.handleTextInsert(event.data);
break;
case "deleteContentBackward":
this.handleBackspace();
break;
case "deleteContentForward":
this.handleDelete();
break;
case "insertParagraph":
this.handleEnterKey();
break;
}
}
shouldPreventInput(event) {
// 根据业务逻辑决定是否阻止输入
if (event.inputType === "insertText") {
// 例如:只允许数字输入
if (this.element.dataset.inputType === "number") {
return !/^[0-9]$/.test(event.data);
}
}
return false;
}
handleTextInsert(text) {
console.log("插入文本:", text);
}
handleBackspace() {
console.log("退格键");
}
handleDelete() {
console.log("删除键");
}
handleEnterKey() {
console.log("回车键");
}
}
3.4 触摸事件处理
3.4.1 基础触摸事件
class TouchEventHandler {
constructor(element) {
this.element = element;
this.touches = new Map();
this.setupTouchEvents();
}
setupTouchEvents() {
this.element.on("touchstart", this.handleTouchStart.bind(this));
this.element.on("touchmove", this.handleTouchMove.bind(this));
this.element.on("touchend", this.handleTouchEnd.bind(this));
this.element.on("touchcancel", this.handleTouchCancel.bind(this));
}
handleTouchStart(event) {
console.log("触摸开始");
for (const touch of event.changedTouches) {
this.touches.set(touch.identifier, {
startX: touch.clientX,
startY: touch.clientY,
currentX: touch.clientX,
currentY: touch.clientY,
startTime: Date.now()
});
}
// 阻止默认的滚动行为
if (event.touches.length > 1) {
event.preventDefault();
}
}
handleTouchMove(event) {
console.log("触摸移动");
for (const touch of event.changedTouches) {
const touchData = this.touches.get(touch.identifier);
if (touchData) {
touchData.currentX = touch.clientX;
touchData.currentY = touch.clientY;
const deltaX = touchData.currentX - touchData.startX;
const deltaY = touchData.currentY - touchData.startY;
this.handleTouchDrag(touch.identifier, deltaX, deltaY);
}
}
// 多点触控处理
if (event.touches.length === 2) {
this.handlePinchGesture(event.touches);
}
}
handleTouchEnd(event) {
console.log("触摸结束");
for (const touch of event.changedTouches) {
const touchData = this.touches.get(touch.identifier);
if (touchData) {
const duration = Date.now() - touchData.startTime;
const deltaX = touchData.currentX - touchData.startX;
const deltaY = touchData.currentY - touchData.startY;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 判断手势类型
if (duration < 300 && distance < 10) {
this.handleTap(touch);
} else if (distance > 50) {
this.handleSwipe(deltaX, deltaY, duration);
}
this.touches.delete(touch.identifier);
}
}
}
handleTouchCancel(event) {
console.log("触摸取消");
for (const touch of event.changedTouches) {
this.touches.delete(touch.identifier);
}
}
handleTap(touch) {
console.log("点击手势", { x: touch.clientX, y: touch.clientY });
// 触发自定义点击事件
this.element.dispatchEvent(new CustomEvent("tap", {
detail: {
x: touch.clientX,
y: touch.clientY
}
}));
}
handleSwipe(deltaX, deltaY, duration) {
const velocity = Math.sqrt(deltaX * deltaX + deltaY * deltaY) / duration;
const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
let direction;
if (Math.abs(angle) < 45) {
direction = "right";
} else if (Math.abs(angle) > 135) {
direction = "left";
} else if (angle > 0) {
direction = "down";
} else {
direction = "up";
}
console.log("滑动手势", { direction, velocity, deltaX, deltaY });
this.element.dispatchEvent(new CustomEvent("swipe", {
detail: {
direction,
velocity,
deltaX,
deltaY
}
}));
}
handleTouchDrag(touchId, deltaX, deltaY) {
console.log("拖拽手势", { touchId, deltaX, deltaY });
this.element.dispatchEvent(new CustomEvent("touchdrag", {
detail: {
touchId,
deltaX,
deltaY
}
}));
}
handlePinchGesture(touches) {
const touch1 = touches[0];
const touch2 = touches[1];
const distance = Math.sqrt(
Math.pow(touch2.clientX - touch1.clientX, 2) +
Math.pow(touch2.clientY - touch1.clientY, 2)
);
if (!this.lastPinchDistance) {
this.lastPinchDistance = distance;
return;
}
const scale = distance / this.lastPinchDistance;
console.log("缩放手势", { scale, distance });
this.element.dispatchEvent(new CustomEvent("pinch", {
detail: {
scale,
distance,
centerX: (touch1.clientX + touch2.clientX) / 2,
centerY: (touch1.clientY + touch2.clientY) / 2
}
}));
this.lastPinchDistance = distance;
}
}
3.5 自定义事件系统
3.5.1 事件创建和分发
class CustomEventSystem {
constructor() {
this.eventListeners = new Map();
}
// 创建自定义事件
createEvent(type, detail = {}, options = {}) {
return new CustomEvent(type, {
detail,
bubbles: options.bubbles !== false,
cancelable: options.cancelable !== false,
composed: options.composed !== false
});
}
// 分发事件
dispatchEvent(target, type, detail, options) {
const event = this.createEvent(type, detail, options);
return target.dispatchEvent(event);
}
// 事件总线
on(type, listener) {
if (!this.eventListeners.has(type)) {
this.eventListeners.set(type, new Set());
}
this.eventListeners.get(type).add(listener);
}
off(type, listener) {
if (this.eventListeners.has(type)) {
this.eventListeners.get(type).delete(listener);
}
}
emit(type, data) {
if (this.eventListeners.has(type)) {
this.eventListeners.get(type).forEach(listener => {
try {
listener(data);
} catch (error) {
console.error("事件监听器错误:", error);
}
});
}
}
once(type, listener) {
const onceListener = (data) => {
listener(data);
this.off(type, onceListener);
};
this.on(type, onceListener);
}
}
// 全局事件总线
const eventBus = new CustomEventSystem();
// 组件间通信示例
class ComponentA {
constructor() {
this.setupEvents();
}
setupEvents() {
eventBus.on("data-updated", this.handleDataUpdate.bind(this));
}
handleDataUpdate(data) {
console.log("组件A收到数据更新:", data);
this.updateUI(data);
}
updateUI(data) {
// 更新UI逻辑
}
sendMessage() {
eventBus.emit("message-sent", {
from: "ComponentA",
message: "Hello from A",
timestamp: Date.now()
});
}
}
class ComponentB {
constructor() {
this.setupEvents();
}
setupEvents() {
eventBus.on("message-sent", this.handleMessage.bind(this));
}
handleMessage(data) {
console.log("组件B收到消息:", data);
}
updateData() {
const newData = { id: 1, name: "Updated Data" };
eventBus.emit("data-updated", newData);
}
}
3.5.2 事件委托和性能优化
class EventDelegator {
constructor(container) {
this.container = container;
this.delegates = new Map();
this.setupDelegation();
}
setupDelegation() {
this.container.on("click", this.handleDelegatedClick.bind(this));
this.container.on("change", this.handleDelegatedChange.bind(this));
this.container.on("input", this.handleDelegatedInput.bind(this));
}
delegate(eventType, selector, handler) {
const key = `${eventType}:${selector}`;
if (!this.delegates.has(key)) {
this.delegates.set(key, new Set());
}
this.delegates.get(key).add(handler);
}
undelegate(eventType, selector, handler) {
const key = `${eventType}:${selector}`;
if (this.delegates.has(key)) {
this.delegates.get(key).delete(handler);
}
}
handleDelegatedClick(event) {
this.handleDelegatedEvent("click", event);
}
handleDelegatedChange(event) {
this.handleDelegatedEvent("change", event);
}
handleDelegatedInput(event) {
this.handleDelegatedEvent("input", event);
}
handleDelegatedEvent(eventType, event) {
for (const [key, handlers] of this.delegates) {
const [type, selector] = key.split(":");
if (type === eventType) {
if (event.target.matches(selector)) {
handlers.forEach(handler => {
try {
handler.call(event.target, event);
} catch (error) {
console.error("委托事件处理错误:", error);
}
});
}
}
}
}
}
// 使用示例
const delegator = new EventDelegator(document.body);
// 委托按钮点击事件
delegator.delegate("click", ".btn", function(event) {
console.log("按钮被点击:", this.textContent);
});
// 委托表单输入事件
delegator.delegate("input", "input[type='text']", function(event) {
console.log("文本输入:", this.value);
});
// 委托选择框变化事件
delegator.delegate("change", "select", function(event) {
console.log("选择变化:", this.value);
});
3.6 实践练习
3.6.1 交互式图片查看器
class ImageViewer {
constructor(container) {
this.container = container;
this.images = [];
this.currentIndex = 0;
this.isFullscreen = false;
this.init();
}
init() {
this.createViewer();
this.setupEvents();
this.loadImages();
}
createViewer() {
this.container.innerHTML = `
<div class="image-viewer">
<div class="viewer-header">
<button class="btn-close">×</button>
<button class="btn-fullscreen">⛶</button>
</div>
<div class="viewer-content">
<button class="btn-prev">‹</button>
<div class="image-container">
<img class="main-image" src="" alt="">
<div class="loading-indicator">加载中...</div>
</div>
<button class="btn-next">›</button>
</div>
<div class="viewer-footer">
<div class="image-info">
<span class="image-counter">1 / 1</span>
<span class="image-title"></span>
</div>
<div class="thumbnail-strip"></div>
</div>
</div>
`;
this.elements = {
viewer: this.container.querySelector(".image-viewer"),
mainImage: this.container.querySelector(".main-image"),
loading: this.container.querySelector(".loading-indicator"),
counter: this.container.querySelector(".image-counter"),
title: this.container.querySelector(".image-title"),
thumbnails: this.container.querySelector(".thumbnail-strip"),
btnPrev: this.container.querySelector(".btn-prev"),
btnNext: this.container.querySelector(".btn-next"),
btnClose: this.container.querySelector(".btn-close"),
btnFullscreen: this.container.querySelector(".btn-fullscreen")
};
}
setupEvents() {
// 按钮事件
this.elements.btnPrev.on("click", () => this.showPrevious());
this.elements.btnNext.on("click", () => this.showNext());
this.elements.btnClose.on("click", () => this.close());
this.elements.btnFullscreen.on("click", () => this.toggleFullscreen());
// 键盘事件
document.on("keydown", this.handleKeyboard.bind(this));
// 鼠标事件
this.elements.mainImage.on("click", () => this.showNext());
// 触摸事件
const touchHandler = new TouchEventHandler(this.elements.mainImage);
this.elements.mainImage.on("swipe", this.handleSwipe.bind(this));
// 缩略图点击
this.elements.thumbnails.on("click", ".thumbnail", (event) => {
const index = parseInt(event.target.dataset.index);
this.showImage(index);
});
// 图片加载事件
this.elements.mainImage.on("load", () => {
this.elements.loading.style.display = "none";
});
this.elements.mainImage.on("error", () => {
this.elements.loading.textContent = "加载失败";
});
}
loadImages() {
// 从容器中查找所有图片
const imageElements = this.container.querySelectorAll("img[data-viewer]");
this.images = Array.from(imageElements).map((img, index) => ({
src: img.src,
title: img.alt || img.title || `图片 ${index + 1}`,
thumbnail: img.dataset.thumbnail || img.src
}));
this.createThumbnails();
if (this.images.length > 0) {
this.showImage(0);
}
}
createThumbnails() {
this.elements.thumbnails.innerHTML = "";
this.images.forEach((image, index) => {
const thumbnail = document.createElement("img");
thumbnail.className = "thumbnail";
thumbnail.src = image.thumbnail;
thumbnail.dataset.index = index;
thumbnail.alt = image.title;
this.elements.thumbnails.appendChild(thumbnail);
});
}
showImage(index) {
if (index < 0 || index >= this.images.length) return;
this.currentIndex = index;
const image = this.images[index];
this.elements.loading.style.display = "block";
this.elements.mainImage.src = image.src;
this.elements.title.textContent = image.title;
this.elements.counter.textContent = `${index + 1} / ${this.images.length}`;
// 更新缩略图状态
this.elements.thumbnails.querySelectorAll(".thumbnail").forEach((thumb, i) => {
thumb.classList.toggle("active", i === index);
});
// 更新按钮状态
this.elements.btnPrev.disabled = index === 0;
this.elements.btnNext.disabled = index === this.images.length - 1;
}
showPrevious() {
this.showImage(this.currentIndex - 1);
}
showNext() {
this.showImage(this.currentIndex + 1);
}
handleKeyboard(event) {
if (!this.container.classList.contains("active")) return;
switch (event.key) {
case "ArrowLeft":
event.preventDefault();
this.showPrevious();
break;
case "ArrowRight":
event.preventDefault();
this.showNext();
break;
case "Escape":
event.preventDefault();
this.close();
break;
case "F11":
event.preventDefault();
this.toggleFullscreen();
break;
}
}
handleSwipe(event) {
const { direction } = event.detail;
switch (direction) {
case "left":
this.showNext();
break;
case "right":
this.showPrevious();
break;
}
}
open(index = 0) {
this.container.classList.add("active");
this.showImage(index);
}
close() {
this.container.classList.remove("active");
if (this.isFullscreen) {
this.exitFullscreen();
}
}
toggleFullscreen() {
if (this.isFullscreen) {
this.exitFullscreen();
} else {
this.enterFullscreen();
}
}
enterFullscreen() {
if (this.container.requestFullscreen) {
this.container.requestFullscreen();
this.isFullscreen = true;
this.container.classList.add("fullscreen");
}
}
exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
this.isFullscreen = false;
this.container.classList.remove("fullscreen");
}
}
}
// 使用示例
document.ready = function() {
const viewer = new ImageViewer(document.querySelector(".image-viewer-container"));
// 为页面中的图片添加查看器功能
document.on("click", "img[data-viewer]", function(event) {
event.preventDefault();
const images = document.querySelectorAll("img[data-viewer]");
const index = Array.from(images).indexOf(this);
viewer.open(index);
});
};
3.7 本章小结
3.7.1 核心概念
- 事件模型:捕获、目标、冒泡三个阶段的事件流
- 事件类型:鼠标、键盘、触摸、自定义事件
- 事件委托:利用事件冒泡实现高效的事件管理
- 用户交互:拖拽、手势识别、快捷键等交互模式
3.7.2 技术要点
- 性能优化:使用事件委托减少内存占用
- 用户体验:提供键盘导航和触摸支持
- 错误处理:妥善处理事件监听器中的异常
- 内存管理:及时清理事件监听器避免内存泄漏
3.7.3 最佳实践
- 合理使用事件委托和直接绑定
- 为键盘用户提供完整的导航支持
- 实现响应式的触摸交互
- 使用自定义事件实现组件间通信
3.7.4 下一章预告
下一章将学习 样式与布局系统,包括: - CSS-in-JS 和动态样式 - 响应式布局和媒体查询 - 动画和过渡效果 - 主题系统和样式管理
通过本章的学习,你已经掌握了完整的事件处理技能,能够创建丰富的用户交互体验。