Skip to content

Instantly share code, notes, and snippets.

@2kool2
Last active January 23, 2017 18:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 2kool2/ec2ae520ba62078c98fcd648170e1c8f to your computer and use it in GitHub Desktop.
Save 2kool2/ec2ae520ba62078c98fcd648170e1c8f to your computer and use it in GitHub Desktop.
Responsively crop copy, restore onclick via slide animation
<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>
<p class=smaller><a target=_blank title="[new window]" href="https://codepen.io/2kool2/pens/public/?grid_type=list#">Pens by Mike Foskett</a> &mdash; <a target=_blank title="[new window]" href="https://websemantics.uk/">webSemantics</a></p>

Responsively crop copy, restore onclick via slide animation

Responsively crop content copy down to a user-defined number of lines. Click to restore content via a drop-down sliding animation.

A Pen by mike foskett on CodePen.

License.

// 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><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);
/* 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;
}
<link href="https://codepen.io/2kool2/pen/kXQoAA" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment