Reputation: 11187
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?
Upvotes: 14
Views: 3966
Reputation: 33439
This uses techniques I got from Lea Verou
See https://codepen.io/HerrSerker/pen/JOaqjL for an SCSS example
ul.breadcrumb, ul.breadcrumb > li {
list-style: none;
margin: 0;
padding: 0;
}
ul.breadcrumb > li {
display: inline-block;
}
/* this adds the slash between the `<li>` */
ul.breadcrumb > li:not(:last-child):after {
content: ' /';
}
/* this is the second `<li>`, but only if there are 5 or more `<li>` */
/* 5 = 2 + 4 - 1 */
ul.breadcrumb > li:nth-child(2):nth-last-child(n+4):after {
content: ' / … /';
}
/* these are `<li>` No. 3 up to 3rd to last `<li>`, but only if there are 5 or more `<li>` */
/* 5 = 3 + 3 - 1 */
ul.breadcrumb > li:nth-child(n+3):nth-last-child(n+3) {
display: none;
}
<ul class="breadcrumb">
<li>Corvid</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
<li>World of Warcraft</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
<li>World of Warcraft</li>
<li>Assets</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
<li>World of Warcraft</li>
<li>Assets</li>
<li>Character Models</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
<li>World of Warcraft</li>
<li>Assets</li>
<li>Character Models</li>
<li>Alliance</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
<li>World of Warcraft</li>
<li>Assets</li>
<li>Character Models</li>
<li>Alliance</li>
<li>Night Elf</li>
</ul>
<ul class="breadcrumb">
<li>Corvid</li>
<li>Games</li>
<li>World of Warcraft</li>
<li>Assets</li>
<li>Character Models</li>
<li>Alliance</li>
<li>Night Elf</li>
<li>Malfurion</li>
</ul>
Upvotes: 2
Reputation: 1363
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>
Upvotes: 6
Reputation: 1668
You can use a combination of flex and truncate text:
.grid-container {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
width: 320px;
margin-top: 30vh;
margin-right: auto;
margin-left: auto;
}
.grid-item {
overflow: hidden;
height: 1.5em;
line-height: 1.5em;
}
.grid-item.expand {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
}
<div class="grid-container">
<div class="grid-item" style="background: red">Corvid / Games </div>
<div class="expand grid-item">/ World of Warcraft / Assets / Character Models / Alliance </div>
<div class="grid-item" style="background: blue">/ Night Elf / Malfurion</div>
</div>
Flex will make the center element width dynamic and text-overflow will make it truncate. If is not what you want please explain more.
Upvotes: 2
Reputation: 2176
.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>
Upvotes: 3
Reputation: 62773
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>
Upvotes: 11
Reputation: 10015
I've put up this proof of concept, using pseudo elements and media queries. Please run the snippet on a full page and change width to less than 500px.
I've learned that media queries cannot apply to individual elements' widths, so this may not be so useful, you would have to adjust the max-width
to a real value based on width of the browser.
DIV.truncate::before {
content: 'World of Warcraft / Assets / Character Models / Alliance';
}
@media (max-width: 500px) {
DIV.truncate::before {
content: '...';
}
}
DIV.container {
font-weight: bold;
font-family: Arial, sans-serif;
font-size: 14px;
white-space: nowrap;
overflow-x: auto;
}
DIV.container>DIV {
display: inline-block;
}
<div class="container">
<div class="first">Corvid / Games /</div>
<div class="truncate"></div>
<div class="last">/ Night Elf / Malfurion</div>
</div>
Upvotes: 2