Hi everybody!
I wanted to share a neat little JavaScript snippet I worked on during my first YouTube live stream. The goal was to clip a block of text to a specific number of lines and add a "Read More" link (or anything else, as the current project requires) at the end if the text exceeds that limit—in the same block, not as a separate button.
The History
The project, which I can’t name just yet, used a previous version of this function. How did it work?
- It split the text into separate characters.
- Placed each and every single one into a
<span>
. - Used
forEach
on the spans and checked if theoffsetTop
of that span changed—if it did, it’s a new line.
I don’t know where my head was when coding this—not on my shoulders, that’s for sure.
Amazing New Solution!
Frankly, it’s nothing that hasn’t been done before.
But it works.
And it’s Elegant!
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<script type="text/javascript" src="LinesLimit.js" defer></script> | |
<style> | |
body {margin: 0; padding: 20px; display: flex; gap: 20px; background-color: #1d1d1d; color: white; flex-wrap: wrap; font-family: sans-serif; line-height: 150%;} | |
.Item {min-width: 300px; max-width: 350px; flex: 1; border-radius: 12px; border: 1px solid #aaaaaa; padding: 20px;} | |
.Item * {margin-top: 0; margin-bottom: 0;} | |
.Item h3 {margin-bottom: 20px;} | |
</style> | |
</head> | |
<body> | |
<div class="Item"> | |
<h3>1. The Wreck of Salvation</h3> | |
<p data-rows="3">The vastness of space was a silent graveyard, punctuated by the dim glow of dead stars. Amid this void floated the Salvation, a derelict colony ship long forgotten by those who launched it. Once a beacon of humanity’s hope, it now stood as a tomb for its crew and passengers. Its hull bore the scars of micro-meteor impacts and centuries of decay.<br>Inside, the emergency lights flickered intermittently, casting eerie shadows down the empty corridors. What remained of the ship's AI barely sustained itself, looping old broadcasts from Earth. Faint echoes of laughter and music from a world long gone reverberated through the metallic skeleton, a cruel reminder of what had been lost.</p> | |
</div> | |
<div class="Item"> | |
<h3>2. The Ghost Signal</h3> | |
<p data-rows="3">It was the ghost signal that first drew him in. Rion Thane, a scavenger eking out a living in the outer belts, intercepted it on a battered receiver. The transmission was garbled, broken by static, but unmistakably human. Against his better judgment, Rion charted a course toward the coordinates.<br>As he approached, the Salvation loomed like a giant carcass against the backdrop of the cosmos. Docking his small craft to the derelict, he felt the weight of history and something darker pressing against his chest. The ghost signal cut off abruptly as soon as his boots touched the ship’s floor, replaced by an oppressive silence.</p> | |
</div> | |
<div class="Item"> | |
<h3>3. Echoes of the Forgotten</h3> | |
<p data-rows="3">The interior of the Salvation was a labyrinth of broken corridors and frost-coated chambers. Personal belongings lay scattered—a child's toy, a diary with pages stuck together, a wedding ring. Rion resisted the urge to explore further; his goal was the bridge, where the black box might offer answers.<br>The air was stale, a mix of metal and decay. Strange markings, like claw marks, marred the walls. As Rion moved deeper, the ship seemed to breathe, the groans of its aged structure mimicking a heartbeat. And always, there was the faint sound of whispers, just out of earshot.</p> | |
</div> | |
<div class="Item"> | |
<h3>4. The Shadows Walk</h3> | |
<p data-rows="3">Rion reached the cryo chamber, where rows of frost-covered pods lined the walls. Many were shattered, their occupants reduced to bone and dust. A few pods still functioned, the occupants frozen in unnatural poses, their faces twisted in terror.<br>It was then he saw it—a shadow moving across the glass. Whirling around, Rion saw nothing but the faint outline of his own reflection. Yet he felt it, a presence watching him, something alive within the cold expanse of the ship. He quickened his pace, his breath fogging in the frigid air.</p> | |
</div> | |
<div class="Item"> | |
<h3>5. The Captain’s Last Log</h3> | |
<p data-rows="3">The bridge was a shattered command center, screens cracked and chairs overturned. Among the wreckage, Rion found the captain's log embedded in the console, miraculously intact. He plugged it into his handheld device, watching as fragmented recordings came to life.<br>“We encountered… something,” the captain’s voice trembled. “Not human. It came from the void. It got into the systems first. Then the crew.” The recordings grew erratic, the captain’s voice dissolving into screams. “We locked it in the lower decks. God help anyone who sets it free.”</p> | |
</div> | |
<div class="Item"> | |
<h3>6. The Thing Below</h3> | |
<p data-rows="3">Rion’s curiosity burned brighter than his fear. The mention of the lower decks and the possibility of alien life was too enticing. With trembling hands, he overrode the locks on the access shaft and descended into the depths of the Salvation.<br>The lower decks were a hellscape. Organic growths pulsed on the walls, oozing a viscous black fluid. The air was suffused with a foul stench that clung to Rion’s lungs. And then he heard it—a low growl, primal and hungry. He froze as a pair of glowing eyes pierced the darkness.</p> | |
</div> | |
<div class="Item"> | |
<h3>7. The Predator Awakes</h3> | |
<p data-rows="3">The creature emerged, a grotesque fusion of organic and mechanical parts. Its body shimmered with liquid metal, and tendrils writhed where limbs should have been. Rion raised his weapon, but the thing moved faster than thought, pinning him against the wall with a single motion.<br>It didn’t kill him. Instead, it studied him, its glowing eyes boring into his soul. Images flashed through Rion’s mind—stars collapsing, civilizations erased, and the endless hunger of the void. The creature spoke without words, its voice a cacophony of despair.</p> | |
</div> | |
<div class="Item"> | |
<h3>8. Assimilation</h3> | |
<p data-rows="3">Rion realized too late that he was being invaded. The creature's tendrils sank into his flesh, spreading cold fire through his veins. His screams echoed through the empty halls, blending with the wails of those who had come before him.<br>The Salvation was alive, its systems fused with the alien entity. Rion’s thoughts blurred, his memories consumed and replaced by the collective consciousness of the ship’s victims. He was no longer himself but a part of something vast and unknowable.</p> | |
</div> | |
<div class="Item"> | |
<h3>9. The New Signal</h3> | |
<p data-rows="3">Days, or perhaps weeks later, a new signal radiated from the Salvation. It was cleaner than the ghost signal Rion had intercepted but equally haunting. It spoke of riches beyond imagination, urging scavengers and explorers to come.<br>The ship waited patiently, its trap reset. The entity, now more powerful with Rion’s addition, coiled in the depths, ready to consume its next victim. The Salvation was no longer a tomb; it was a predator, luring the greedy and desperate with promises of salvation.</p> | |
</div> | |
<div class="Item"> | |
<h3>10. The Endless Void</h3> | |
<p data-rows="3">Far from the Salvation, the signal reached another scavenger, another lonely soul in the void. They plotted their course toward the derelict, their mind filled with visions of wealth and discovery.<br>The cycle would continue. The entity would grow. The void would claim more lives. And in the vast emptiness of space, no one would hear them scream.</p> | |
</div> | |
</body> | |
</html> |
let eol_dot = document.querySelectorAll('[data-rows]'); | |
let eol_intersection = new IntersectionObserver(entries=> { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
eol_intersection.unobserve(entry.target); | |
LineLimitProcess(entry.target); | |
} | |
}); | |
}); | |
eol_dot.forEach(ed => { | |
if (ed.dataset.rowsProcess === 'now') { | |
LineLimitProcess(ed); | |
} else { | |
eol_intersection.observe(ed); | |
} | |
}); | |
function LineLimitProcess(el) { | |
let lines = 3; | |
if (/mobile/i.test(navigator.userAgent) && el.dataset.rowsMobile) { | |
lines = parseInt(el.dataset.rowsMobile); | |
} else if (!/mobile/i.test(navigator.userAgent) && el.dataset.rowsDesktop) { | |
lines = parseInt(el.dataset.rowsDesktop); | |
} else if (el.dataset.rows) { | |
lines = parseInt(el.dataset.rows); | |
} | |
let add_content = ' ... <a href="">Read more</a>'; | |
let full_text = el.textContent.trim(); | |
let current_character = 0; | |
let last_character = full_text.length - 1; | |
let current_check_text = ''; | |
el.innerHTML = ''; | |
let height = el.clientHeight; | |
let current_lines = 0; | |
let limited = false; | |
while (current_character < last_character) { | |
current_check_text += full_text[current_character]; | |
el.innerHTML = current_check_text + add_content; | |
if (height < el.clientHeight) { // new line | |
current_lines++; | |
height = el.clientHeight; | |
if (current_lines > lines) { | |
limited = true; | |
break; | |
} | |
} | |
current_character++; | |
} | |
if (limited) { | |
el.innerHTML = current_check_text.slice(0, -1) + add_content; | |
} else { | |
el.innerHTML = current_check_text; | |
} | |
} |
How It Works
In short, it checks added characters in a loop, starting with a single character plus the appendage, and if the block’s height is higher than before, it’s a new line.
As I said, elegant! I love this new solution.
And I kept that Observer, because why not, right?
Fun Fact
I asked ChatGPT to write a dark sci-fi story. Haven't read it, yet. Honestly - I can't wait to get to that!
Come Hang Out Next Time
The YouTube live stream was an interesting experience. I liked it a lot!
Check the stream here: https://youtube.com/live/FBpU7P3o5ME
Thank you for reading!
Top comments (0)