I know I'm coming to this post way late but I ran into the same issue where I was losing a class name I had wrapped part of the text I was then splitting into lines. I'm not sure if this is something that has since been supported and I'm also a noob at GSAP as I've only been using it for the past week. But, for any future visitors perhaps wondering if there's a workaround, I've devised a solution that grabs the text within elements of a specified class to then wrap that same text with spans with that same class. The only caveat (that I know of) is if a block of text (single word/character or more with the special class) occurs more than once in the entire split line text block, it will be applied to the first match.
consider the following markup:
<p class="target">this is some <span class="special">special</span> text.</p>
would merit the following js:
const target = document.querySelector('.target')
// target needs to be cloned because splitText mutates the passed element as well as any references
const original = target.cloneNode(true)
const splitTarget = new SplitText(target, {type: 'lines'})
const preservedSplitTarget = preserveClass(splitTarget, original, '.special')
// function to preserve a class on text split by lines.
// ideally imported from a helper file
function preserveClass(splitText, original, className) {
const texts = Array.from(original.querySelectorAll(className)).map(val => val.innerText.split(' '))
// Check if any of the original element contains any children with the className
if (texts.length === 0) return splitText
// regex to match a text preceded by the beginning of the string or a space and has a space or non-word character or end of string after.
const genWordRegex = word => new RegExp(`(?:^|\\s)${word}(?=\\s|\\W|$)`)
// helper for quick splitting text on spaces and filtering out empty strings
const splitInnerHTML = innerHTML => innerHTML.split(' ').filter(s => s.length > 0)
const {lines} = splitText
// template helper for wrapping text with spans using the className argument
const spanTemplate = content => `<span class="${className.replace(/\./, '')}">${content}</span>`
// tracks which occurrence of the class your tracking (the first level of the multidimensional array)
let blockIndex = 0
// the current block of text to wrap
let textToPreserve = texts[blockIndex]
// tracks which word in the block to look for
let textIndex = 0
// recursively checks the target text against the textArray (textToPreserve) and wraps matching text in target
const recursiveSwap = (target, textArray) => {
const textPiece = textArray[textIndex]
const RE = genWordRegex(textPiece)
// ensure that textPiece lives in the target before proceeding
if (!RE.test(target)) return target
const newTarget = target.replace(RE, first => {
// since javascript doesnt support regex lookbehinds (yet), leading spaces are returned in the match so we want to preserve that
const start = first.charAt(0) === ' ' ? ' ' : ''
return `${start}${spanTemplate(textPiece)}`
})
textIndex = textIndex + 1
// if we have reached the end of the current textBlock
if (textIndex >= textArray.length) {
blockIndex = blockIndex + 1
// if we have reached the end of all textBlocks
if (blockIndex >= texts.length) return newTarget
textToPreserve = texts[blockIndex]
textIndex = 0
}
return recursiveSwap(newTarget, textArray)
}
return {
...splitText,
lines: lines.map((l, i, linesArray) => {
let lineText = l.innerHTML
// return the line if the next piece of text to preserve doesn't match
if (!genWordRegex(textToPreserve[textIndex]).test(lineText)) return l
// if we've already started replacing pieces in the current textBlock or there's only one item in the block
if (textIndex > 0 || textToPreserve.length === 1) {
l.innerHTML = recursiveSwap(lineText, textToPreserve)
} else {
let splitLine = splitInnerHTML(lineText)
let currWordIndex = splitLine.indexOf(textToPreserve[textIndex])
let nextWord = textToPreserve[1]
// check if there are more words in the current line
if (currWordIndex < splitLine.length - 1) {
// check if the next words of both the textToPreserve and line match to ensure we start replacing the correct occurrence of a common word
if (nextWord === splitLine[currWordIndex + 1]) {
l.innerHTML = recursiveSwap(lineText, textToPreserve)
}
// else we've reached the end of the current line
} else {
// check if there is a next line and that the next textToPreserve word matches the first word in the next line
if (i < linesArray.length - 1 && nextWord === splitInnerHTML(linesArray[i + 1].innerHTML)[0]) {
l.innerHTML = recursiveSwap(lineText, textToPreserve)
}
}
}
return l
})
}
}
This is the first iteration of a workaround and certainly has room for improvement and I would love to hear any feedback/concerns regarding this approach. Hope this is of some help.