Giannis Paraskevopoulos
Giannis Paraskevopoulos

Reputation: 18411

Show more/less if text does not fit

I am creating some cards with dynamic data using jquery. There is a description property that either fits on the card or not. When it does not fit, i would like to keep the card the same size as the others but add a "show more/less" button that would expand the card to show the rest of the description.

I have created a fiddle showing the card creation as well as what i have tried so far.

At this point i have a function checkTruncation that will check if the text would fit in its container or not. The issue seems to rely on the fact that the elements are not yet rendered so their width is 0 which makes the function return always true.

The function is as this:

let checkTruncation = function (jqueryElement) {
    var $element = jqueryElement;
    if ($element) {
        var $c = $element
            .clone()
            .css({ display: 'inline', width: 'auto', visibility: 'hidden' })
            .appendTo('body');

        let truncated = $c.width() > $element.width()
        console.log($c.width(), $element.width(), $c.width() > $element.width())
        $c.remove();

        return truncated;
    }
    return false;
}

Also, instead of me reinventing the wheel, is there any suggested library that could achieve this functionality? I know i have seen such behavior, but cannot remember where.

UPDATE

In the fiddle above, i am just calling the getCard function, which is responsible to provide the template for the card, twice just to show some sample. In the real life situation i am getting data through ajax and populating the cards using the following when a user clicks on a button:

$('.fetchButton').on('click', function(){
    $.get(url, filter)
        .done(function (data) {
            jQuery.each(data, function (index, item) {
                $('.container').append(getCard(item));
            })
        }
})

UPDATE 2

With the help of @LGSon i have come to this scenario which is mostly using CSS to achieve what i need. Still the answer given is valid and did solve my issue so i will keep it as an answer.

Upvotes: 3

Views: 1494

Answers (2)

Asons
Asons

Reputation: 87191

In addition to given answer, which explained why the original code didn't work, here is a different approach, showing how one can use CSS to show/hide a "Read more..." button

With absolute: position and overflow: hidden we can hide a "button" at the bottom of a "text" element, and when that element reach the "wrapper's" height, the hidden "button" becomes visible.

I also made use of a pseudo element and data-* attribute to keep "button text" in the markup, and the CSS attr() to toggle it when showing/hiding the text.

Stack snippet

document.querySelectorAll('.readmore').forEach( function(link) {
  link.addEventListener('click', function() {
    this.closest('.wrapper').classList.toggle('show');
  });
});
.wrapper {
  height: 70px;
  border: 1px solid gray;
  overflow: hidden;
}
.wrapper.show {
  height: auto;
}

.text {
  position: relative;
  overflow: hidden;
}

.readmore {
  position: absolute;
  width: 100%;
  top: 52px;
  left: 0;
  background: white;
  color: red;
  cursor: pointer;
}
  .readmore::before {
    content: attr(data-more)
  }
  .wrapper.show .readmore {
    position: relative;
    top: auto;
    display: block;
  }
  .wrapper.show .readmore::before {
    content: attr(data-less)
  }
<div class="wrapper">
  <div class="text">
    Some smaller dummy text here

    <span class="readmore" data-more="Show more..." data-less="Show less..."></span>
  </div>
</div>

<div class="wrapper">
  <div class="text">
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    
    <span class="readmore" data-more="Show more..." data-less="Show less..."></span>
  </div>
</div>


If possible, the "Show more/less..." text can of course be set in the CSS, to simplify maintenance and have it in one place.

document.querySelectorAll('.readmore').forEach( function(link) {
  link.addEventListener('click', function() {
    this.closest('.wrapper').classList.toggle('show');
  });
});
.wrapper {
  height: 70px;
  border: 1px solid gray;
  overflow: hidden;
}
.wrapper.show {
  height: auto;
}

.text {
  position: relative;
  overflow: hidden;
}

.readmore {
  position: absolute;
  width: 100%;
  top: 52px;
  left: 0;
  background: white;
  color: red;
  cursor: pointer;
}
  .readmore::before {
    content: 'Show more...'
  }
  .wrapper.show .readmore {
    position: relative;
    top: auto;
    display: block;
  }
  .wrapper.show .readmore::before {
    content: 'Show less...'
  }
<div class="wrapper">
  <div class="text">
    Some smaller dummy text here

    <span class="readmore"></span>
  </div>
</div>

<div class="wrapper">
  <div class="text">
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    Some bigger dummy text here <br>
    
    <span class="readmore"></span>
  </div>
</div>


Updated based on a comment

Here is an updated Stack snippet version of your Fiddle/Codepen sample's

let getCard = function(options) {
  let item = options.data;
  let $parent = options.parent;

  let outerDiv = $('<div>').addClass('shops-content-right');
  let boxDiv = $('<div>').addClass('boxes-right row');
  let shopDiv = $('<div>').addClass('one-shop-box col-md-6 offset-md-3');
  let cubeDiv = $('<div>').addClass('cube text-center');

  let wrapperDiv = $('<div>').addClass('wrapper');
  let textDiv = $('<div>').addClass('text');
  let descriptionP = $('<p>').addClass('text-disc').text(item.description);

  let loadMoreP = $('<span>More >').addClass('readmore');

  textDiv.append([descriptionP, loadMoreP]);
  wrapperDiv.append(textDiv);
  cubeDiv
    .append(wrapperDiv);

  shopDiv.append(cubeDiv);
  boxDiv.append(shopDiv);
  outerDiv.append(boxDiv);

  $parent.append(outerDiv);

  loadMoreP.on('click', function() {
    if (wrapperDiv.hasClass('collapsed')) {
      wrapperDiv.removeClass('collapsed');
      loadMoreP.text("More >");
    } else {
      wrapperDiv.addClass('collapsed');
      loadMoreP.text("Less <");
    }
  })

  return outerDiv;
}


$('.fetchButton').on('click', function() {
  getCard({
    data: {
      description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
    },
    parent: $('.container'),
    lines: 3
  })
  getCard({
    data: {
      description: 'Lorem ipsum dolor sit amet, consectetur'
    },
    parent: $('.container'),
    lines: 3
  })
})
.row {
  background: #f8f9fa;
  margin-top: 20px;
}

.col {
  border: solid 1px #6c757d;
  padding: 10px;
}

.one-shop-box {
  border: 1px solid;
}

.shops-content-right .one-shop-box .cube p.text-disc {
  font-size: 16px;
  /*height: 50px;*/
  overflow: hidden;
}

.wrapper {
  height: 90px;
  overflow: hidden;
}

.wrapper.collapsed {
  height: auto;
}

.text {
  position: relative;
  overflow: hidden;
}

.readmore {
  position: absolute;
  width: 100%;
  top: 72px;
  left: 0;
  background: white;
  color: red;
}

.wrapper.collapsed .readmore {
  position: relative;
  top: auto;
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<!-- 
  Bootstrap docs: https://getbootstrap.com/docs
-->

<div class="container">
  <button class="fetchButton">
Fetch Data
</button>

</div>

Upvotes: 2

Chayim Friedman
Chayim Friedman

Reputation: 70989

This happens beacuse you doesn't append your element to document, so its width is 0. So, update your code as the following:

let checkTruncation = function($element) {
  if ($element) {
    var $c = $element
      .clone()
      .css({ display: 'inline', width: 'auto', visibility: 'hidden' })
      .appendTo('body');

    let truncated = $c.width() > $element.width();
    // console.log($c.width(), $element.width(), $c.width() > $element.width());
    $c.remove();

    return truncated;
  }
    
  return false;
};

let baseUrl ='';

let getCard = function(item, $parent) {
  let outerDiv = $('<div>').addClass('shops-content-right');
  let boxDiv = $('<div>').addClass('boxes-right row');
  let shopDiv = $('<div>').addClass('one-shop-box col-md-6 offset-md-3');
  let cubeDiv = $('<div>').addClass('cube text-center');
  let descriptionP = $('<p>').addClass('text-disc').text(item.description).addClass('collapsed');
  let loadMoreP = $('<p>').css({ 'font-size': '16px', 'cursor': 'pointer' }).text("More >");

  cubeDiv.append(descriptionP).append(loadMoreP);
  shopDiv.append(cubeDiv);
  boxDiv.append(shopDiv);
  outerDiv.append(boxDiv);
  $parent.append(outerDiv);

  loadMoreP.on('click', function () {
    if (descriptionP.hasClass('collapsed')) {
      descriptionP.removeClass('collapsed');
      loadMoreP.text("Less <");
    } else {
      descriptionP.addClass('collapsed');
      loadMoreP.text("More >");
    }
  });

  if (checkTruncation(descriptionP)) {
    loadMoreP.show();
  } else {
    loadMoreP.hide();
  }
};

const $container = $('.container');

getCard({
  description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
}, $container);

getCard({
  description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
}, $container);
.row {
  background: #f8f9fa;
  margin-top: 20px;
}

.col {
  border: solid 1px #6c757d;
  padding: 10px;
}

.one-shop-box {
  border:1px solid;
}

.shops-content-right .one-shop-box .cube p.text-disc {
  font-size: 16px;
  height: 50px;
  overflow: hidden;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<div class="container"></div>

I pass to the function getCard() the container element, and it append the new element to it before it calls to the function checkTruncation().

And update your code that calls getCard() as follows:

const $container = $('.container');
$('.fetchButton').on('click', function() {
  $.get(url, filter).done(function(data) {
    $.each(data, function(index, item) {
      getCard(item, $container);
    });
  });
});

Upvotes: 1

Related Questions