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 技术要点

  1. 性能优化:使用事件委托减少内存占用
  2. 用户体验:提供键盘导航和触摸支持
  3. 错误处理:妥善处理事件监听器中的异常
  4. 内存管理:及时清理事件监听器避免内存泄漏

3.7.3 最佳实践

  • 合理使用事件委托和直接绑定
  • 为键盘用户提供完整的导航支持
  • 实现响应式的触摸交互
  • 使用自定义事件实现组件间通信

3.7.4 下一章预告

下一章将学习 样式与布局系统,包括: - CSS-in-JS 和动态样式 - 响应式布局和媒体查询 - 动画和过渡效果 - 主题系统和样式管理

通过本章的学习,你已经掌握了完整的事件处理技能,能够创建丰富的用户交互体验。