You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript LightBox故障求助:媒体切换按钮仅单次可用

问题:LightBox切换媒体仅能生效一次且跳过初始媒体

问题背景

学习JavaScript开发LightBox功能时遇到异常:从本地JSON生成包含图片、视频的媒体列表后,点击媒体打开LightBox,切换上一个/下一个按钮仅能生效一次,且切换后会跳过最初打开的媒体。

已完成工作

  • 创建LightBox类
  • 实现LightBox显示、打开(隐藏页面主内容/头部)、关闭方法
  • 实现上一个/下一个媒体切换的基础方法

index.js

  • 实例化LightBox并传入目标媒体数据
  • 调用类中所有核心方法

现有代码

class LightBox {
  constructor(data) {
    this._data = data;
  }

  createLightBox() {
    const $wrapper = document.querySelector(".lightbox-container");

    if (this._data.classList.contains("post-img")) { // 仅图片媒体拥有的类
      const lightBox = `
        <div class="lightbox">
            <i class="fa-solid chevron fa-chevron-left prev-media"></i>
            <div class="lightbox-text">
                <img alt="${this._data.getAttribute(
                  "alt"
                )}" class="lightbox-media lightbox-img lightbox-active" src="${this._data.getAttribute(
        "src"
      )}"/>
                <p>${this._data.getAttribute("alt")}</p>
            </div>
      <i class="fa-solid chevron fa-chevron-right next-media"></i>
      <i class="close-lightbox fa-solid croix fa-x"></i>
        </div>
    `;
      $wrapper.innerHTML = lightBox;
    } else if (this._data.classList.contains("post-video")) { // 仅视频媒体拥有的类
      const lightBox = `
        <div class="lightbox">
            <i class="fa-solid chevron fa-chevron-left prev-media"></i>
            <div class="lightbox-text">
                <video title="${this._data.getAttribute(
                  "title"
                )}" class="lightbox-media lightbox-video lightbox-active" autoplay loop src="${this._data.getAttribute(
        "src"
      )}"></video>
                <p>${this._data.getAttribute("title")}</p>
            </div>
            <i class="fa-solid chevron fa-chevron-right next-media"></i>
            <i class="close-lightbox fa-solid croix fa-x"></i>
        </div>
`;
      $wrapper.innerHTML = lightBox;
    }
    return $wrapper;
  }

  openLightBox() {
    const $wrapper = document.querySelector(".lightbox-container");
    $wrapper.style.display = "block";
    main.style.display = "none";
    header.style.display = "none";
  }

  closeLightBox() {
    const $wrapper = document.querySelector(".lightbox-container");
    const closeLightBox = document.querySelector(".close-lightbox");
    closeLightBox.addEventListener("click", function () {
      $wrapper.style.display = "none";
      main.style.display = "block";
      header.style.display = "block";
    });
    document.addEventListener("keydown", function (e) {
      if (e.code == "Escape") {
        $wrapper.style.display = "none";
        main.style.display = "block";
        header.style.display = "block";
      }
    });
  }

  prev(mediaCollectionItem) {
    const mediaContent = document.querySelector(".lightbox-text");
    const prev = document.querySelector(".prev-media");
    prev.addEventListener("click", () => {
      mediaContent.innerHTML = "";
      const prevContent = mediaCollectionItem.cloneNode(true);
      const prevTitle = document.createElement("p");
      prevTitle.innerHTML = !prevContent.getAttribute("alt")
        ? prevContent.getAttribute("title")
        : prevContent.getAttribute("alt");
      prevContent.classList.replace("post-media", "lightbox-media");
      mediaContent.append(prevContent, prevTitle);
    });
  }

  next(mediaCollectionItem) {
    const mediaContent = document.querySelector(".lightbox-text");
    const next = document.querySelector(".next-media");
    next.addEventListener("click", () => {
      mediaContent.innerHTML = "";
      const nextContent = mediaCollectionItem.cloneNode(true);
      const nextTitle = document.createElement("p");
      nextTitle.innerHTML = !nextContent.getAttribute("alt")
        ? nextContent.getAttribute("title")
        : nextContent.getAttribute("alt");
      nextContent.classList.replace("post-media", "lightbox-media");
      mediaContent.append(nextContent, nextTitle);
    });
  }
}

index.js

let mediaCollection = document.getElementsByClassName("post-media"); // 所有媒体共有的类
mediaCollection = [...mediaCollection];
for (let i = 0; i < mediaCollection.length; i++) {
  mediaCollection[i].addEventListener("click", () => {
    const userLightBox = new LightBox(mediaCollection[i]);
    const renderLightBox = userLightBox.createLightBox();
    userLightBox.openLightBox();
    userLightBox.closeLightBox();
    userLightBox.prev(mediaCollection[i - 1]);
    userLightBox.next(mediaCollection[i + 1]);
    this.$body.appendChild(renderLightBox);
  });
}

问题原因

  1. 切换逻辑绑定固定媒体:index.js中调用prev()/next()时,传入的是固定的mediaCollection[i-1]/mediaCollection[i+1],点击切换按钮只会跳转到这一个固定媒体,无法连续切换。
  2. 无当前媒体索引跟踪:LightBox类未记录当前显示媒体在集合中的位置,无法动态计算下一个/上一个目标媒体。
  3. 事件重复绑定风险:每次打开LightBox都会重新绑定关闭、切换事件,可能导致事件叠加干扰正常逻辑。

解决方案

修改思路

  • 给LightBox类添加当前索引属性,维护当前显示媒体的位置
  • 传入整个媒体集合而非单个媒体,支持全局切换计算
  • 统一媒体渲染逻辑,避免重复代码
  • 重构切换方法,基于当前索引动态获取目标媒体

修改后的代码

LightBox.js

class LightBox {
  constructor(mediaCollection, currentIndex) {
    this._mediaCollection = mediaCollection; // 保存完整媒体集合
    this._currentIndex = currentIndex; // 当前显示媒体的索引
    this._wrapper = document.querySelector(".lightbox-container");
    this._handleEscape = null; // 存储键盘事件处理函数,用于后续移除
    this._prevHandler = null; // 存储上一个按钮事件处理函数
    this._nextHandler = null; // 存储下一个按钮事件处理函数
  }

  // 统一渲染当前显示的媒体
  renderCurrentMedia() {
    const currentMedia = this._mediaCollection[this._currentIndex];
    let mediaHtml = "";
    let mediaTitle = "";

    // 根据媒体类型生成对应HTML
    if (currentMedia.classList.contains("post-img")) {
      mediaTitle = currentMedia.getAttribute("alt");
      mediaHtml = `<img alt="${mediaTitle}" class="lightbox-media lightbox-img" src="${currentMedia.getAttribute("src")}"/>`;
    } else if (currentMedia.classList.contains("post-video")) {
      mediaTitle = currentMedia.getAttribute("title");
      mediaHtml = `<video title="${mediaTitle}" class="lightbox-media lightbox-video" autoplay loop src="${currentMedia.getAttribute("src")}"></video>`;
    }

    // 渲染完整LightBox结构
    this._wrapper.innerHTML = `
      <div class="lightbox">
          <i class="fa-solid chevron fa-chevron-left prev-media"></i>
          <div class="lightbox-text">
              ${mediaHtml}
              <p>${mediaTitle}</p>
          </div>
          <i class="fa-solid chevron fa-chevron-right next-media"></i>
          <i class="close-lightbox fa-solid croix fa-x"></i>
      </div>
    `;

    // 渲染完成后绑定交互事件
    this.bindEvents();
  }

  openLightBox() {
    this._wrapper.style.display = "block";
    document.querySelector("main").style.display = "none";
    document.querySelector("header").style.display = "none";
  }

  closeLightBox() {
    const closeBtn = this._wrapper.querySelector(".close-lightbox");
    closeBtn.addEventListener("click", () => {
      this._wrapper.style.display = "none";
      document.querySelector("main").style.display = "block";
      document.querySelector("header").style.display = "block";
    });

    // 先移除旧的键盘事件,避免重复绑定
    if (this._handleEscape) {
      document.removeEventListener("keydown", this._handleEscape);
    }
    this._handleEscape = (e) => {
      if (e.code === "Escape") {
        this._wrapper.style.display = "none";
        document.querySelector("main").style.display = "block";
        document.querySelector("header").style.display = "block";
      }
    };
    document.addEventListener("keydown", this._handleEscape);
  }

  // 切换到上一个媒体,支持循环
  prev() {
    this._currentIndex = this._currentIndex === 0 
      ? this._mediaCollection.length - 1 
      : this._currentIndex - 1;
    this.renderCurrentMedia();
  }

  // 切换到下一个媒体,支持循环
  next() {
    this._currentIndex = this._currentIndex === this._mediaCollection.length - 1 
      ? 0 
      : this._currentIndex + 1;
    this.renderCurrentMedia();
  }

  // 统一绑定所有交互事件
  bindEvents() {
    const prevBtn = this._wrapper.querySelector(".prev-media");
    const nextBtn = this._wrapper.querySelector(".next-media");

    // 移除旧的按钮事件,避免叠加
    if (this._prevHandler) prevBtn.removeEventListener("click", this._prevHandler);
    if (this._nextHandler) nextBtn.removeEventListener("click", this._nextHandler);

    // 绑定新的事件处理函数
    this._prevHandler = () => this.prev();
    this._nextHandler = () => this.next();
    prevBtn.addEventListener("click", this._prevHandler);
    nextBtn.addEventListener("click", this._nextHandler);

    // 绑定关闭事件
    this.closeLightBox();
  }
}

index.js

const mediaCollection = [...document.getElementsByClassName("post-media")];

mediaCollection.forEach((media, index) => {
  media.addEventListener("click", () => {
    const userLightBox = new LightBox(mediaCollection, index);
    userLightBox.renderCurrentMedia();
    userLightBox.openLightBox();
  });
});

关键修改说明

  1. 构造函数参数优化:传入完整媒体集合和当前点击媒体的索引,让LightBox能全局跟踪媒体位置。
  2. 统一渲染逻辑:renderCurrentMedia()根据当前索引动态生成媒体HTML,避免重复代码。
  3. 动态切换逻辑:prev()/next()方法基于当前索引计算目标位置,支持循环切换(第一个媒体点上一个跳转到最后一个,反之亦然)。
  4. 事件绑定优化:在bindEvents()中统一处理按钮事件,先移除旧事件再绑定新事件,避免事件叠加导致的异常。
  5. 冗余代码移除:index.js不再需要传入固定的上一个/下一个媒体,切换逻辑完全由LightBox内部处理。

内容的提问来源于stack exchange,提问作者Al_Tecaz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 03:05:40