在将文本拆分为多行时,保留内部的span标签
我正在尝试制作一个视差横幅。横幅将包含一个简短的文本句子,会换行显示。但我希望每一行都能单独执行动画。因此我的目标是把文本拆分成适应父容器宽度所需的任意多的行。这个容器的宽度会根据屏幕尺寸而变化。我也需要在屏幕尺寸变化时更新结果——例如平板电脑旋转时。
总之,如果我在视差效果中有一段文本,如下所示:
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque fermentum sem magna, vitae pharetra neque</p>
我希望得到像下面这样的结果,每个span包含一行能容纳的最大单词数。
<p>
<span>Lorem ipsum dolor sit amet, consectetur</span>
<span>adipiscing elit. Quisque fermentum sem</span>
<span>magna, vitae pharetra neque<span>
</p>
其实我已经通过逐个往一个span中添加单词,当某个单词放不下时就创建一个新的span来实现了。
let cmpParallaxTextHighlight = {
component: null,
init: function (el) {
const _ = this;
_.element = el;
_.debugMode = true;
_.paragraphs = document.querySelectorAll('.cmp-parallax-text-highlight__paragraph');
_.splitLines();
_.reRenderOnResize();
},
// logging for debug mode
log: function (message, type) {
const _ = this;
if (_.debugMode === false) return;
type = type || 'log';
switch (type) {
case 'info':
console.info(`%cInfo : ${message}`, 'color: #0099ff')
break;
default:
console.log(`${message}`)
break;
}
},
// re-render the parallax text when the window is resized
reRenderOnResize: function () {
const _ = this;
window.addEventListener('resize', function () {
_.splitLines();
});
},
// break each paragraph into multiple lines depending on the available width
// each line will be wrapped in a span
splitLines: function () {
const _ = this;
_.paragraphs.forEach(function (paragraph) {
// get the paragraph text from the data attribute.
let originalText = paragraph.getAttribute('data-original-text');
// if the data attribute doesn't exist, this must be the first time the component is being initialized.
// let's set the data attribute to the paragraph text.
if (!originalText) {
originalText = paragraph.textContent.replace(/\s+/g, ' ').trim();
paragraph.setAttribute('data-original-text', originalText);
}
// clear the paragraph on the page
paragraph.innerHTML = '';
// get the parent container. We will need this to define our available width
let container = paragraph.parentElement;
let containerWidth = container.scrollWidth;
// break the paragraph text into words
let words = originalText.split(' ');
// create an array to hold the lines
let lineArray = [];
// create a temporary hidden span to get line widths and add it to the page
let tempSpan = document.createElement('span');
tempSpan.textContent = '';
tempSpan.style.display = 'inline-block';
tempSpan.style.whiteSpace = 'nowrap';
tempSpan.style.opacity = '0';
paragraph.appendChild(tempSpan);
// create a new test line
let testLine = '';
// loop through the individual words. place all that fit inside the first span.
words.forEach(function (word) {
testLine = testLine ? testLine + ' ' + word : word;
tempSpan.innerHTML = testLine;
if (tempSpan.scrollWidth > containerWidth) {
testLine = testLine.split(' ').slice(0, -1).join(' ');
// now we have a line of suitable length, add it to the paragraph
_.addNewLineToParagraph(paragraph, testLine);
// clear test elements down ready for the next line
tempSpan.innerHTML += '';
testLine = word;
}
});
// We would have added all but the last line to the paragraph, so add it now
if (testLine) {
_.addNewLineToParagraph(paragraph, testLine);
}
paragraph.removeChild(tempSpan);
});
},
// add passed text to the paragraph as a new line contained in a span
addNewLineToParagraph: function (paragraph, text) {
const _ = this;
let newLine = document.createElement('span');
newLine.classList.add('cmp-parallax-text-highlight__paragraph_line');
newLine.innerHTML = text;
paragraph.appendChild(newLine);
},
}
cmpParallaxTextHighlight.init();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="dist/styles.css">
</head>
<body>
<div style="height: 100px; width: 100%"></div>
<div class="cmp-parallax-text-highlight" data-component="cmpParallaxTextHighlight">
<div class="cmp-parallax-text-highlight__background"></div>
<div class="cmp-parallax-text-highlight__inner_content container">
<div class="row">
<div class="col-8">
<p class="cmp-parallax-text-highlight__paragraph">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque fermentum sem magna, vitae pharetra neque </p>
</div>
</div>
</div>
</div>
<script>
</script>
</body>
现在的问题是,我还在努力解决,如何处理原始文本字符串中包含的任何HTML。比如说,如果我的文本中包含如下的span标签:
<p>Lorem ipsum dolor <span class="my_class">sit amet, consectetur adipiscing elit. </span>Quisque fermentum sem<span class="my_class"> magna, vitae pharetra</span> neque </p>
我需要输出是有效的HTML,因此如果一个span内的文本换到多行,每个span都应包含一个打开标签和一个关闭标签。于是我的结果将会是:
<p>
<span>Lorem ipsum dolor <span class="my_class">sit amet, consectetur</span></span>
<span><span class="my_class">adipiscing elit</span>. Quisque fermentum sem</span>
<span><span class="my_class">magna, vitae pharetra</span> neque<span>
</p>
我尝试过的所有方法都以灾难性的方式失败。根本没有接近目标。我甚至不知道我的思路应该是什么,更不用说怎么编码实现。有人知道我该如何实现吗?我知道市场上有很多视差库,但最终目标如此定制化,我找不到能提供太多帮助的库。
谢谢
解决方案
不要尝试拆分HTML字符串。拆分渲染后的DOM。
我做了以下修改
使用data-original-html代替data-original-text,因为必须保留原始标记。
我找到了一个TreeWalker,用来遍历段落内的真实文本节点,包括嵌套在span内的文本。
我们需要使用Range来测量,并在后续用 range.cloneContents() 克隆渲染后的DOM,即便一行的起始或结束落在现有的内联元素内部,也能创建有效的片段。
我尽量让实现接近你的代码。
请注意空白处的不可换行。
let cmpParallaxTextHighlight = {
lineTolerance: 6,
leftTolerance: 2,
init: function() {
const _ = this;
_.paragraphs = document.querySelectorAll('.cmp-parallax-text-highlight__paragraph');
_.splitLines();
_.reRenderOnResize();
},
reRenderOnResize: function() {
const _ = this;
let resizeFrame;
window.addEventListener('resize', function() {
cancelAnimationFrame(resizeFrame);
resizeFrame = requestAnimationFrame(function() {
_.splitLines();
});
});
},
splitLines: function() {
const _ = this;
_.paragraphs.forEach(function(paragraph) {
paragraph.dataset.originalHtml ||= paragraph.innerHTML.trim();
paragraph.innerHTML = paragraph.dataset.originalHtml;
const words = _.getWordsWithPositions(paragraph);
const lines = _.groupWordsIntoLines(words);
const lineSpans = lines.map(function(line) {
return _.createLineSpan(_.createLineFragment(paragraph, line));
});
paragraph.replaceChildren(...lineSpans);
});
},
getWordsWithPositions: function(paragraph) {
const words = [];
const walker = document.createTreeWalker(paragraph, NodeFilter.SHOW_TEXT);
const regex = /\S+\s*/g;
while (walker.nextNode()) {
const node = walker.currentNode;
const text = node.nodeValue;
let match;
regex.lastIndex = 0;
while ((match = regex.exec(text))) {
const range = document.createRange();
range.setStart(node, match.index);
range.setEnd(node, match.index + match[0].length);
const rect = range.getClientRects()[0];
if (!rect) continue;
words.push({
node: node,
start: match.index,
end: match.index + match[0].length,
top: rect.top,
left: rect.left
});
}
}
return words;
},
groupWordsIntoLines: function(words) {
const _ = this;
return words.reduce(function(lines, word) {
const lastLine = lines[lines.length - 1];
const previousWord = lastLine && lastLine.end;
const isNewLine =
!lastLine ||
Math.abs(lastLine.top - word.top) > _.lineTolerance ||
word.left < previousWord.left - _.leftTolerance;
if (isNewLine) {
lines.push({
top: word.top,
start: word,
end: word
});
} else {
lastLine.end = word;
}
return lines;
}, []);
},
createLineFragment: function(paragraph, line) {
const _ = this;
const range = document.createRange();
range.setStart(line.start.node, line.start.start);
range.setEnd(line.end.node, line.end.end);
return _.wrapFragment(
range.cloneContents(),
_.getSharedWrappers(paragraph, line.start.node, line.end.node)
);
},
getSharedWrappers: function(paragraph, startNode, endNode) {
const wrappers = [];
let node = startNode.parentElement;
while (node && node !== paragraph) {
if (node.contains(endNode)) {
wrappers.push(node);
}
node = node.parentElement;
}
return wrappers;
},
wrapFragment: function(fragment, wrappers) {
return wrappers.reduce(function(currentFragment, wrapper) {
const clone = wrapper.cloneNode(false);
const wrappedFragment = document.createDocumentFragment();
clone.appendChild(currentFragment);
wrappedFragment.appendChild(clone);
return wrappedFragment;
}, fragment);
},
createLineSpan: function(fragment) {
const line = document.createElement('span');
line.className = 'cmp-parallax-text-highlight__paragraph_line';
line.appendChild(fragment);
return line;
}
};
cmpParallaxTextHighlight.init();
.myClass {
color: green
}
.cmp-parallax-text-highlight__paragraph_line {
display: block;
white-space: nowrap;
}
<div style="height: 100px; width: 100%"></div>
<div class="cmp-parallax-text-highlight" data-component="cmpParallaxTextHighlight">
<div class="cmp-parallax-text-highlight__background"></div>
<div class="cmp-parallax-text-highlight__inner_content container">
<div class="row">
<div class="col-8">
<p class="cmp-parallax-text-highlight__paragraph">Lorem ipsum dolor <span class="myClass">sit amet</span>, consectetur adipiscing elit. <span class="myClass">Quisque fermentum sem magna</span>, vitae pharetra neque </p>
</div>
</div>
</div>
</div>
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。