<h1>Responsively crop copy, restore onclick via sliding drop-down animation</h1>

<p>
  Responsively crop content copy down to a user-defined number of lines.<br>
  Click to fully restore the content via a sliding drop-down animation.<br>
  All delivered in an accessible manner.<br>
</p>

<p>As used on Tesco's <a href="http://www.tesco.com/food-love-stories/">Food Love Stories</a></p>

<p>GitHub repo available: <a href="https://github.com/2kool2/crop-copy-slide-restore" target=_blank title="[new window]">crop-copy-slide-restore</a></p>


<section>

  <h2>Single line crop example</h2>

  <div class=CCR>
    <div class=CCR_txt 
         data-cropCopyRestore>
      If you’ve seen our recent TV ad, you’ll be in on our Food Love Story about ‘David’s’ shameful secret: when he met his wife, he fibbed about sharing her love of spicy food. 15 years later, he hasn’t come clean, but he’s still making his wife her favourite curry – with a sneaky dollop of cooling yogurt for him.
    </div>
  </div>


  <h2>Two line crop example</h2>

  <div class=CCR>
    <div class=CCR_txt 
         data-cropCopyRestore=2>
      Sometimes it’s the undemandingly easy, everyday recipes that deliver the most joy. For ‘Iain’ and his dad, from our Food Love Story, it’s croque monsieur. They first had it on a joint trip to France and, since then, it’s become their favourite weekend lunch. ‘Iain’s’ made a few changes to it along the way (bonjour, wafer-thin roast turkey) – but for him and his dad, it’s most definitely ‘proper’.
    </div>
  </div>

</section>

<svg style="display:none">
  <defs>
    <symbol viewBox="0 0 38 38" id="icon-vert">
      <path d="M19 10.5l0 17"></path>
    </symbol>
    <symbol viewBox="0 0 38 38" id="icon-hori">
      <path d="M10.5 19l17 0"></path>
    </symbol>
  </defs>
</svg>

<h2>Features</h2>
<ul>
  <li>User-defined number of lines initially displayed, defined in the HTML.</li>
  <li>JavaScript automagically writes an accurate inline max-height property which is animated via CSS.</li>
  <li>Resizing the viewport recalculates the length of text displayed and adjusts the max-height values.</li>
  <li>Utilises <abbr title="Accessible Rich Internet Applications">ARIA</abbr> roles and live region to help meet <abbr title="Web Content Accessibility Guidelines">WCAG</abbr> 2 (accessibility).</li>
  <li>Vanilla JavaScript and less than 2kB minified &amp; gzipped.</li>
  <li>Support down to IE9.</li>
</ul>


<h2>Status</h2>
<p>
  Cross-browser tested:<br>
  Mac: Firefox Dev, Chrome, Safari, Opera Dev.<br>
  PC: Firefox Dev, Chrome, IE9 - Edge.
</p>
<p>
  In Live testing.<br>
  To be followed by full accessibility testing.
</p>




[[[https://codepen.io/2kool2/pen/mKeeGM]]]
/* Generics */
code {display:block;}
pre {margin: 0; overflow-x: scroll;}


/* Helper classes */

* {box-sizing: border-box;}
.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px, 1px, 1px, 1px);
}


/* Main styles */

.CCR {
  /* Animated via JS embedding inline max-height values */
  /* Note: 1ms shorter than SVG rotation duration */
  overflow: hidden;
  transition: max-height .6s ease-out;
}

.CCR_txt {
  /* Optional, adjust to meet individual project */
  color: #fff;
  background-color: #000;
  padding: .75rem 1rem;
}
.CCR_txt[role="button"] {
  cursor: pointer;
}


/* Icon styles */

.CCR_icon {
  /* SVG container (required) */
  /* Fixes Safari's focus/hover state box-shadow */

  /* Override colours here if required: */
  /* color: #fff; */
  background-color: #3a3a3a;

  float: right;
  margin: 0 0 .75rem .75rem;

  /* Today, we look through the round window */
  border-radius: 100%;
  overflow: hidden;
  display: block;
  width: 1.5em;
  height: 1.5em;
  -webkit-transition: box-shadow .3s ease-out;
  transition: box-shadow .3s ease-out;
}
.CCR_svg {
  background-color: transparent;
  color: currentColor;
  border: .125em solid currentColor;
  border: .125em solid #3a3a3a;
  border-radius: 100%;
  display: block;
  width: 100%;
  height: 100%;
  stroke-width: 4;
  stroke-linecap: square;
  stroke: currentColor;

  /* Note: 1ms longer than SVG rotation duration */
  -webkit-transition: transform .7s ease-out;
  transition: transform .7s ease-out;
}


/* Icon animation */

.CCR-expanded .CCR_svg {
  /* 360deg provides a slower rotation */
  transform: rotateZ(180deg);
}
.CCR_use-plus {
  /* Note: same as SVG rotation duration */
  -webkit-transition: opacity .7s ease-out;
  transition: opacity .7s ease-out;
}
.CCR-expanded .CCR_use-plus {
  opacity: 0;
}

/* Acts as focus state indicator for the control */
/* A requirement to meet WCAG 2 */
.CCR_txt:hover > .CCR_icon,
.CCR_txt:focus > .CCR_icon {
  box-shadow: 0 0 0 4px #99BAD9;
}
// Crop copy responsively, to user-defined number of lines, then restore onclick - v2.0 (IE9+) - 22/01/2017 - M.J.Foskett - https://websemantics.uk/
var cropCopyRestore = (function (window, document) {

  "use strict";

  var dataAttr = "data-cropCopyRestore";
  var buttonId = "CCR_btn-";
  var ellipsis = "…"; // "\u2026"
  var clonedClass = "CCR-clone";
  var expandedClass = "CCR-expanded";
  var textClass = "CCR_txt"; // html text content class
  var copyClass = "CCR_copy"; // span added inside textClass to contain just the copy
  var iconSVG = "<span class=CCR_icon><svg class=CCR_svg focussable=false aria-hidden=true><use class=CCR_use-plus xlink:href=#icon-vert></use><use xlink:href=#icon-hori></use></svg></span>";
  var readMoreSpan = "<span class=visually-hidden> [Read more]</span>";

  // Debounce window resize - https://john-dugan.com/javascript-debounce/
  var debounce=function(e,t,n){var a;return function(){var r=this,i=arguments,o=function(){a=null,n||e.apply(r,i)},s=n&&!a;clearTimeout(a),a=setTimeout(o,t||200),s&&e.apply(r,i)}};

  // transitionend event test and prefix - https://gist.github.com/O-Zone/7230245
  !function(n){var i={transition:"transitionend",WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"otransitionend"},t=document.createElement("div");for(var o in i)if("undefined"!=typeof t.style[o]){n.transitionEnd=i[o];break}}(window);

  // Minimal classList polyfill (for IE9) - Devon Govett - https://gist.github.com/devongovett/1381839
  "classList"in document.documentElement||!Object.defineProperty||"undefined"==typeof HTMLElement||Object.defineProperty(HTMLElement.prototype,"classList",{get:function(){function e(e){return function(t){var s=n.className.split(/\s+/),i=s.indexOf(t);e(s,i,t),n.className=s.join(" ")}}var n=this,t={add:e(function(e,n,t){~n||e.push(t)}),remove:e(function(e,n){~n&&e.splice(n,1)}),toggle:e(function(e,n,t){~n?e.splice(n,1):e.push(t)}),contains:function(e){return!!~n.className.split(/\s+/).indexOf(e)},item:function(e){return n.className.split(/\s+/)[e]||null}};return Object.defineProperty(t,"length",{get:function(){return n.className.split(/\s+/).length}}),t}});


// String cropping functions

  function _removeLastOccur(str, removeStr) {
    return str.substring(0, str.lastIndexOf(removeStr));
  }

  function _removeTrailingPunct(str) {
    return str.replace(/[ .,!?:;"“‘'\-]+$/, "");
  }


// Display and animation functions

  function _display(obj, str) {
    obj.querySelector("." + copyClass).textContent = str;
  }

  function _displayCroppedText(obj) {
    _display(obj, obj.croppedText + ellipsis);
    if (!obj.innerHTML.match("visually-hidden")) {
      obj.innerHTML +=  readMoreSpan;
    }
  }

  function _resetAttr(obj, bool) {
    obj.setAttribute("aria-expanded", bool);
  }

  function _addRemainerText(obj) {

    function __add(obj) {
      _display(obj, obj.fullText);
      obj.removeChild(obj.querySelector(".visually-hidden"));
      _resetAttr(obj, true);
    }

    // maybe use a polyfill?
    if (window.requestAnimationFrame) {
      window.requestAnimationFrame(function() {
        __add(obj);
      });
    } else {
      __add(obj);
    }
  }

  function _removeRemainerText(obj) {
		if (obj) { // Repairs an obscure "tap" condition in Chrome
			_displayCroppedText(obj);
			_resetAttr(obj, false);
		}
  }


// Set copy

  function _createClone(obj, str) {
    // create an invisible clone (used to get an objects height)
    var clone = obj.cloneNode(true);
    clone.classList.add(clonedClass);
		if (clone.querySelector("." + copyClass)) {
			clone.querySelector("." + copyClass).textContent = str;
			obj.parentNode.insertBefore(clone, obj.nextSibling);
			clone.initialHeight = clone.clientHeight;
		}
		return clone;
  }


  function _getCroppedHeight(obj) {
    var clone =_createClone(obj, obj.croppedText);
    obj.parentNode.removeChild(clone);
    return clone.initialHeight;
  }


  function _getFullHeight(obj) {
    var clone =_createClone(obj, obj.fullText);
    obj.parentNode.removeChild(clone);
    return clone.initialHeight;
  }


  function _getCroppedText(obj) {
    var txtArr = obj.fullText.split(" ");
    var i = 0;
    var lines = 1;
    var clone = _createClone(obj, txtArr[i] + " ");
    var textObj = clone.querySelector("." + copyClass);

    for (i = 1; i < txtArr.length; i++) {

      textObj.textContent += txtArr[i] + ellipsis;

      if (clone.clientHeight !== clone.initialHeight) {

        if (lines + "" === obj.noOfLines) {

          _display(clone, _removeLastOccur(textObj.textContent, txtArr[i] + ellipsis));

          obj.croppedMaxHeight = clone.clientHeight;
          obj.parentNode.setAttribute("style", "max-height:" + obj.croppedMaxHeight + "px");
          break;
        }
        lines++;
        clone.initialHeight = clone.clientHeight;
      }

      // Bit of an assumption
      _display(clone, textObj.textContent.replace(txtArr[i] + ellipsis, txtArr[i] + " "));

    }

    _display(clone, _removeTrailingPunct(textObj.textContent));
    obj.parentNode.removeChild(clone);
    return textObj.textContent;
  }


// Handle events

  function _removeText(event) {
    var obj = event.target.p;
    delete event.target.p;
    event.target.removeEventListener(window.transitionEnd, _removeText);
    _removeRemainerText(obj);
  }


	function _getButtonObj(obj) {
		if (obj && obj.classList.contains(textClass)) {
			return obj;
		}
		if (obj.parentNode === null) {
			return false;
		}
		return _getButtonObj(obj.parentNode);
	}


  function _clicked(event) {

    var obj = _getButtonObj(event.target);

    if (obj) {
			if (obj.getAttribute("aria-expanded") === "true") {
				obj.parentNode.style.maxHeight = _getCroppedHeight(obj) + "px";
				obj.parentNode.classList.remove(expandedClass);
				obj.parentNode.p = obj;
				if (window.transitionEnd) {
					obj.parentNode.addEventListener(window.transitionEnd, _removeText, false);
				} else {
					_removeRemainerText(obj);
				}
			} else {
				obj.parentNode.style.maxHeight = _getFullHeight(obj) + "px";
				obj.parentNode.classList.add(expandedClass);
				_addRemainerText(obj);
			}
		}

    event.preventDefault();
  }

  function _keyPressed(event) {
    // Enter or space key
    if (event.which === 13 || event.which === 32) {
      _clicked(event);
    }
  }

  function _addEvents(obj) {
    obj.addEventListener("click", _clicked, false);
    obj.addEventListener("keydown", _keyPressed, false);
  }

  function _removeEvents(obj) {
    obj.removeEventListener("click", _clicked);
    obj.removeEventListener("keydown", _keyPressed);
  }

// Initialisation

  function _initialiseAttributes(obj, i) {
    var str = obj.getAttribute(dataAttr);
    obj.noOfLines = (/^([1-9]\d*)$/.test(str)) ? str : "1"; // Returns 1 - 9 only
    obj.fullText = obj.textContent.trim();
    obj.setAttribute("id", obj.id || buttonId + i);
    obj.setAttribute("role", "button");
    obj.setAttribute("tabindex", "0");
    obj.setAttribute("aria-controls", obj.id);
  }

  function _prepareContent(obj) {

    // Quick and dirty - replace if you wish
    obj.innerHTML =  iconSVG + "<span role=alert aria-live=assertive class=" + copyClass + ">" +obj.innerHTML + "</span>";
  }

  function start() {

    var objs = document.querySelectorAll("[" + dataAttr + "]");
    var i = objs.length;
    var obj;

    while (i--) {

      obj = objs[i];

      // In case it's a resize call rather than initialisation
      if (obj.fullText) {
        obj.parentNode.classList.remove(expandedClass);
        _removeEvents(obj);
      } else {
        _prepareContent(obj);
        _initialiseAttributes(obj, i);
      }

      // Reset, or initialise, common attributes
      _resetAttr(obj, false);
      obj.croppedText = _getCroppedText(obj);

      _displayCroppedText(obj);
      _addEvents(obj);
    }
  }

  start();
  window.addEventListener("resize", debounce(start, 100, false), false);

}(window, document));

window.addEventListener("load", cropCopyRestore, false);
Run Pen

External CSS

  1. https://codepen.io/2kool2/pen/kXQoAA.css

External JavaScript

This Pen doesn't use any external JavaScript resources.