Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Truncate text in the middle of a series of divs

Tags:

css

web

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?

like image 859
corvid Avatar asked Aug 08 '17 13:08

corvid


3 Answers

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:

  1. A simple JavaScript utility function
  2. A functional (stateless) React component
  3. A stateful React component

1. A JavaScript Function

In the example below I've created a function truncateBreadcrumbs() which accepts 3 parameters:

  • selector - a CSS selector matching the elements you want to truncate
  • separator - the character used to separate the elements
  • segments - the number of segments you want to truncate the string to

It 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>

2. A Functional (Stateless) React Component

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>

3. A Stateful React Component

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>
like image 152
Brett DeWoody Avatar answered Oct 21 '22 08:10

Brett DeWoody


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>

Edit:

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>
like image 6
keja Avatar answered Oct 21 '22 06:10

keja


.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.)

  1. display "/" after every item
  2. don`t display items after the second item
  3. display the "/.../" after the item from the second
  4. display the last two items (they where hidden in 2)
  5. display " / " after the third item from the end
  6. display " / " after the second item from the end
  7. don't display "/" after the last item.

.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>
like image 3
miguel-svq Avatar answered Oct 21 '22 08:10

miguel-svq