I have a component which will, currently, render something out that looks as follows:
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
I always want the first two items and last two items to be visible, however, I want everything in the middle to truncate into ...
if possible. That is to say, if the string above was to overflow the containing div, it should have the following result
Corvid / Games / ... / Night Elf / Malfurion
I have tried making a structure like as follows:
<div className={styles.container}>
<div className={styles.first}>
{/** Contains first two items */}
</div>
<div className={styles.truncate}>
{/** N arbitrary path items */}
</div>
<div className={styles.last}>
{/** Last two items */}
</div>
</div>
Could this be achieved with CSS?
Interesting problem - I can't see a reliable CSS-only solution unfortunately. That is, unless the HTML structure can be edited, and even then there will be some hardcoding, I don't believe there is a reliable CSS-only solution.
However, here are 3 potential solutions:
In the example below I've created a function truncateBreadcrumbs()
which accepts 3 parameters:
selector
- a CSS selector matching the elements you want to truncateseparator
- the character used to separate the elementssegments
- the number of segments you want to truncate the string toIt can be used like:
truncateBreadcrumbs(".js-truncate", "/", 4);
which would find all elements with a class of .js-truncate
and truncate the contents to 4 elements, with the ...
separator in the middle, like:
Corvid / Games / ... / Night Elf / Malfurion
Odd-numbers of segments can also be used, for example 5
would generate:
Corvid / Games / World of Warcraft / ... / Night Elf / Malfurion
If the segment
argument is equal to or greater than the number of elements, no truncation occurs.
And here's the full working example:
function truncateBreadcrumbs(selector, separator, segments) {
const els = Array.from(document.querySelectorAll(selector));
els.forEach(el => {
const split = Math.ceil(segments / 2);
const elContent = el.innerHTML.split(separator);
if (elContent.length <= segments) {
return;
}
el.innerHTML = [].concat(
elContent.slice(0, split),
["..."],
elContent.slice(-(segments-split))
).join(` ${separator} `);
});
}
truncateBreadcrumbs(".js-truncate--2", "/", 2);
truncateBreadcrumbs(".js-truncate--4", "/", 4);
truncateBreadcrumbs(".js-truncate--5", "/", 5);
<div class="js-truncate--2">Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion</div>
<div class="js-truncate--4">Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion</div>
<div class="js-truncate--5">Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion</div>
<div class="js-truncate--4">Corvid / Games / Night Elf / Malfurion</div>
It appears (based on the className
attribute) you're using React. If that's the case we can create a simple functional component to truncate the text. I've taken the code above and made this into a functional component <Truncate />
which does the same thing:
const Truncate = function(props) {
const { segments, separator } = props;
const split = Math.ceil(segments / 2);
const elContent = props.children.split(separator);
if (elContent.length <= segments) {
return (<div>{props.children}</div>);
}
const newContent = [].concat(
elContent.slice(0, split),
["..."],
elContent.slice(-(segments-split))
).join(` ${separator} `);
return (
<div>{newContent}</div>
)
}
It can be used as:
<Truncate segments="4" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
And here's the full working example:
const Truncate = function(props) {
const { segments, separator } = props;
const split = Math.ceil(segments / 2);
const elContent = props.children.split(separator);
if (elContent.length <= segments) {
return (<div>{props.children}</div>);
}
const newContent = [].concat(
elContent.slice(0, split),
["..."],
elContent.slice(-(segments-split))
).join(` ${separator} `);
return (
<div>{newContent}</div>
)
}
class App extends React.Component {
render() {
return (
<div>
<Truncate segments="2" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
<Truncate segments="4" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
<Truncate segments="5" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
<Truncate segments="4" separator="/">
Corvid / Games / Night Elf / Malfurion
</Truncate>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
We can also make a stateful component to respond to the width of the screen/element. This is a really rough take on the idea - a component that tests the width of the element and truncates it if necessary. Ideally, the component would only truncate as much as needed, instead of to a fixed number of segments.
The usage is the same as above:
<Truncate segments="4" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
but the difference is the component tests the container width to see if the segments can fit, and if not the text is truncated. Click the 'Expand snippet' button to view the demo in full-screen so you can resize the window.
class Truncate extends React.Component {
constructor(props) {
super(props);
this.segments = props.segments;
this.separator = props.separator;
this.split = Math.ceil(this.segments / 2);
this.state = {
content: props.children
}
}
componentDidMount = () => {
this.truncate();
window.addEventListener("resize", this.truncate);
}
componentWillUnmount = () => {
window.removeEventListener("resize", this.truncate);
}
truncate = () => {
if (this.div.scrollWidth > this.div.offsetWidth) {
const elContentArr = this.state.content.split(this.separator);
this.setState({
content: [].concat(
elContentArr.slice(0, this.split),
["..."],
elContentArr.slice(-(this.segments - this.split))
).join(` ${this.separator} `)
})
}
}
render() {
return (
<div className="truncate" ref={(el) => { this.div = el; }}>
{this.state.content}
</div>
)
}
}
class App extends React.Component {
render() {
return (
<div>
<Truncate segments="2" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
<Truncate segments="4" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
<Truncate segments="5" separator="/">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</Truncate>
<Truncate segments="4" separator="/">
Corvid / Games / Night Elf / Malfurion
</Truncate>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById("app"));
.truncate {
display: block;
overflow: visible;
white-space: nowrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
This is one way to to it, well only works if there always more than 4 items.
ul { list-style-type: none; }
ul li { display: none; }
ul li:nth-last-child(n+2):after { content: " / "; }
ul li:nth-child(2):after { content: " / ... /"; }
ul li:nth-child(-n+2), ul li:nth-last-of-type(-n+2) { display: inline-block; }
<ul>
<li><a href="#">Corvid</a></li>
<li><a href="#">Games</a></li>
<li><a href="#">World of Warcraft</a></li>
<li><a href="#">Assets</a></li>
<li><a href="#">Character Models</a></li>
<li><a href="#">Alliance</a></li>
<li><a href="#">Night Elf</a></li>
<li><a href="#">Malfurion</a></li>
</ul>
a work around that could be to add a class in front of the selectors, and only truncate if there is more than 4 items. ideally that would be added on render, but alternatively you could add it by javascript as in this example
document.querySelectorAll("ul").forEach( el => el.childElementCount > 4 && el.classList.add("truncate") );
ul { list-style-type: none; }
ul li { display: inline-block;}
ul li:nth-last-child(n+2):after { content: " / "; }
ul.truncate li { display: none; }
ul.truncate li:nth-child(2):after { content: " / ... /"; }
ul.truncate li:nth-child(-n+2), ul li:nth-last-of-type(-n+2) { display: inline-block; }
<ul>
<li><a href="#">Corvid</a></li>
<li><a href="#">Games</a></li>
<li><a href="#">World of Warcraft</a></li>
<li><a href="#">Assets</a></li>
<li><a href="#">Character Models</a></li>
<li><a href="#">Alliance</a></li>
<li><a href="#">Night Elf</a></li>
<li><a href="#">Malfurion</a></li>
</ul>
Dont know if there is something im missing in the question, but if you do not want to change your structure your could do it like this as well:
.truncate div { display: inline-block; }
.truncate .mid { display: none; }
.truncate .first:after { content: "... /"; }
<div class="truncate">
<div class="first">
Corvid / Games /
</div>
<div class="mid">
World of Warcraft / Assets / Character Models / Alliance /
</div>
<div class="last">
Night Elf / Malfurion
</div>
</div>
or if you want a simple pure js function for it
document.querySelectorAll(".turncate").forEach( el => {
const parts = el.innerText.split(" / ");
if(parts.length > 4){
parts.splice(2, parts.length-4); //ensure we only have two first and last items
parts.splice(2, 0, "..."); // add ... after 2 first items.
el.innerText = parts.join(" / ");
}
});
<div class="turncate">
Corvid / Games / World of Warcraft / Assets / Character Models / Alliance / Night Elf / Malfurion
</div>
.enumeration > div{display:inline-block;font-size:150%}
.enumeration > div:after{content: " / ";color:blue;font-weight:bold;}
.enumeration > div:nth-child(n+3) {display:none;}
.enumeration > div:nth-child(n+2):after {content: " / ... / ";color: red;}
.enumeration > div:nth-last-child(-n+2) {display:inline-block;}
.enumeration > div:nth-last-child(-n+3):after{content:" / ";color:green;}
.enumeration > div:nth-last-child(-n+2):after{content:" / ";color:orange;}
.enumeration > div:last-child:after{content:""}
A bit tricky, but it is only css and works on any list size. :) (The colors and bold are just to make easier to see where is each selector applied.)
.enumeration > div{display:inline-block;font-size:150%}
.enumeration > div:after{content: " / ";color:blue;font-weight:bold;} /*1*/
.enumeration > div:nth-child(n+3) {display:none;}/*2*/
.enumeration > div:nth-child(n+2):after {content: " / ... / ";color: red;}/*3*/
.enumeration > div:nth-last-child(-n+2) {display:inline-block;}/*4*/
.enumeration > div:nth-last-child(-n+3):after{content:" / ";color:green;}/*5*/
.enumeration > div:nth-last-child(-n+2):after{content:" / ";color:orange;}/*6*/
.enumeration > div:last-child:after{content:""}/*7*/
<h2>
More than 4 items list
</h2>
<div class="enumeration">
<div>item 1</div>
<div>item 2</div>
<div>item 3</div>
<div>item 4</div>
<div>item 5</div>
<div>item 6</div>
<div>item 7</div>
<div>item 8</div>
<div>item 9</div>
</div>
<h2>
4 items list
</h2>
<div class="enumeration">
<div>item 1</div>
<div>item 2</div>
<div>item 3</div>
<div>item 4</div>
</div>
<h2>
3 items list
</h2>
<div class="enumeration">
<div>item 1</div>
<div>item 2</div>
<div>item 3</div>
</div>
<h2>
2 items list
</h2>
<div class="enumeration">
<div>item 1</div>
<div>item 2</div>
</div>
<h2>
1 item list
</h2>
<div class="enumeration">
<div>item 1</div>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With