You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

保留内部span标签的视差横幅文本多行拆分需求

视差横幅含内部标签的文本换行拆分方案

核心思路

通过解析富文本为完整原子片段 + 模拟排版判断换行的方式,确保拆分后的每行都包含合法闭合的HTML结构,同时适配窗口尺寸变化。核心是避免直接截断HTML标签,而是以完整的文本块或元素为单位进行行拆分。

具体实现步骤

  1. 解析原始内容:将横幅内的文本节点和完整span元素拆分为独立的原子片段,确保每个片段都是可独立渲染的合法单元(不会出现标签截断)。
  2. 模拟排版容器:创建一个与目标横幅样式完全一致的隐藏容器,用于实时判断当前行内容的宽度是否超出父容器。
  3. 逐片段填充拆分:依次将原子片段加入模拟容器,若宽度超出父容器则新建一行,保证每行的内容都是完整的文本或元素。
  4. 生成最终行结构:将每个合法行用span包裹,替换原始横幅内容,实现每行独立动画的基础结构。
  5. 窗口 resize 适配:监听窗口尺寸变化,防抖触发重新拆分逻辑,保证响应式适配。

代码实现

HTML结构

<div class="parallax-banner" id="banner">
  这是一段<span class="highlight">带高亮的文本</span>,需要拆分成多行,<span class="italic">支持内部标签</span>的完整保留。
</div>

CSS样式

.parallax-banner {
  width: 100%;
  font-size: 24px;
  line-height: 1.5;
  padding: 20px;
  box-sizing: border-box;
  /* 视差相关样式可自行添加 */
}

.parallax-banner .line {
  display: block;
  margin: 4px 0;
  /* 示例动画:鼠标悬停时平移 */
  transition: transform 0.3s ease;
}

.parallax-banner .line:hover {
  transform: translateX(10px);
}

.parallax-banner .highlight {
  color: #ff4444;
  font-weight: bold;
}

.parallax-banner .italic {
  font-style: italic;
}

/* 隐藏的模拟排版容器,样式与目标横幅完全对齐 */
.line-simulator {
  position: absolute;
  top: -9999px;
  left: -9999px;
  white-space: nowrap;
  font-size: 24px;
  line-height: 1.5;
  padding: 20px;
  box-sizing: border-box;
  width: 100%;
}

JavaScript逻辑

const banner = document.getElementById('banner');
const simulator = document.createElement('div');
simulator.className = 'line-simulator';
document.body.appendChild(simulator);

// 解析横幅内容为原子片段(文本节点/完整span元素)
function parseContent(element) {
  const fragments = [];
  Array.from(element.childNodes).forEach(node => {
    if (node.nodeType === Node.TEXT_NODE) {
      const text = node.textContent.trim();
      if (text) {
        // 按空格拆分单词(英文场景),中文可改为按字符拆分或保留整段
        text.split(' ').forEach(word => {
          if (word) fragments.push({ type: 'text', content: word + ' ' });
        });
      }
    } else if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SPAN') {
      fragments.push({ type: 'element', content: node.outerHTML });
    }
  });
  return fragments;
}

// 执行文本拆分逻辑
function splitIntoLines() {
  const parentWidth = banner.offsetWidth;
  const fragments = parseContent(banner);
  banner.innerHTML = '';
  simulator.innerHTML = '';

  let currentLine = [];

  fragments.forEach(frag => {
    // 将当前片段加入模拟容器
    if (frag.type === 'text') {
      simulator.appendChild(document.createTextNode(frag.content));
    } else {
      const temp = document.createElement('div');
      temp.innerHTML = frag.content;
      simulator.appendChild(temp.firstChild);
    }

    // 检查宽度是否超出
    if (simulator.offsetWidth > parentWidth) {
      // 生成当前行
      if (currentLine.length > 0) {
        const lineSpan = document.createElement('span');
        lineSpan.className = 'line';
        currentLine.forEach(item => {
          if (item.type === 'text') {
            lineSpan.appendChild(document.createTextNode(item.content));
          } else {
            const temp = document.createElement('div');
            temp.innerHTML = item.content;
            lineSpan.appendChild(temp.firstChild);
          }
        });
        banner.appendChild(lineSpan);
        currentLine = [];
      }
      // 重置模拟容器,放入当前片段作为新行开头
      simulator.innerHTML = '';
      if (frag.type === 'text') {
        simulator.appendChild(document.createTextNode(frag.content));
      } else {
        const temp = document.createElement('div');
        temp.innerHTML = frag.content;
        simulator.appendChild(temp.firstChild);
      }
    }
    currentLine.push(frag);
  });

  // 处理最后一行
  if (currentLine.length > 0) {
    const lineSpan = document.createElement('span');
    lineSpan.className = 'line';
    currentLine.forEach(item => {
      if (item.type === 'text') {
        lineSpan.appendChild(document.createTextNode(item.content));
      } else {
        const temp = document.createElement('div');
        temp.innerHTML = item.content;
        lineSpan.appendChild(temp.firstChild);
      }
    });
    banner.appendChild(lineSpan);
  }
}

// 初始化执行
splitIntoLines();

// 防抖处理窗口resize事件
let resizeDebounce;
window.addEventListener('resize', () => {
  clearTimeout(resizeDebounce);
  resizeDebounce = setTimeout(splitIntoLines, 200);
});

关键注意事项

  • 样式一致性:模拟容器的样式必须与目标横幅完全匹配(字体、字号、内边距等),否则宽度计算会出现偏差。
  • 嵌套标签扩展:如果需要支持更深层次的嵌套标签(如span内嵌套span),可修改parseContent函数为递归解析,确保所有元素都作为完整片段处理。
  • 性能优化:resize事件添加防抖,避免短时间内频繁触发拆分逻辑;若横幅内容复杂,可考虑缓存解析后的片段,减少重复解析开销。
  • 中英文适配:英文场景按单词拆分避免截断,中文场景可改为按字符拆分或保留连续文本块,根据需求调整parseContent内的文本拆分逻辑。

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

火山引擎 最新活动