From d40b3318414e91b3bff51d900663f2eb056f5d4d Mon Sep 17 00:00:00 2001 From: 2kool2 Date: Sun, 22 Jan 2017 18:34:04 +0000 Subject: [PATCH] Responsively crop copy, restore onclick via sliding drop-down animation --- css/styles.css | 121 ++++++++++++++++ index.html | 100 ++++++++++++++ js/cropCopyRestore.2.0.js | 252 ++++++++++++++++++++++++++++++++++ js/cropCopyRestore.2.0.min.js | 4 + license.md | 9 ++ readme.md | 51 +++++++ 6 files changed, 537 insertions(+) create mode 100644 css/styles.css create mode 100644 index.html create mode 100644 js/cropCopyRestore.2.0.js create mode 100644 js/cropCopyRestore.2.0.min.js create mode 100644 license.md create mode 100644 readme.md diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..8586e32 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,121 @@ + +/* Generic styles */ +body { + font-family: sans-serif; + line-height: 1.5; + color: #fff; + background-color: #3a3a3a; + max-width: 40rem; +} +figure {margin: 1rem 0} +pre, code {display:block;margin:0} +pre {overflow-x:scroll;resize:horizontal;min-width:100%;background-color:#333} +pre > code {text-shadow:0 1px #000;word-wrap:normal;padding-left:.75rem} +::selection {background-color:#000} +::-moz-selection {background-color:#000} +h1, h2, h3 {font-weight: 100; line-height:1.25} +h1 {font-size:1.75rem} +h2 {font-size:1.5rem;margin-top:2em} +a,a:visited {color:#fff} + +/* helper class */ +* { + 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; +} + +.CCR_txt * { + /* Required, polyfill if unsupported */ + /* Note: Prevents embedded links from working */ + pointer-events: none; +} + + +/* 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: 2px 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; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..1e71eca --- /dev/null +++ b/index.html @@ -0,0 +1,100 @@ + + + + + Crop copy, restore on-click + + + + + + + + + + + + + + + + + + +

Responsively crop copy, restore onclick via sliding drop-down animation

+ +

+ Responsively crop content copy down to a user-defined number of lines in an accessible manner.
+ Click to fully restore content via a sliding drop-down animation.

+

+ As used on Tesco Food Love Stories. +

+ + +
+ +

Examples

+ +

Single line:

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

Two lines:

+ +
+
+ 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’. +
+
+ +
+ + +

Usage

+ +

Include a link to the styles:

+ +
+
Language HTML
+

+<link rel="stylesheet" href="css/styles.css">
+    
+
+ +

Add content block(s) and data attribute.
The attribute value is equal to the number of lines to display.

+ +
+
Language HTML
+

+<div class="CCR">
+    <div class="CCR_txt"
+         data-cropCopyRestore="2">
+      Content copy…
+    </div>
+</div>
+    
+
+ + +

Include a link to the script:

+ +
+
Language HTML
+

+<script src="js/cropCopyRestore.2.0.min.js"></script>
+    
+
+ + +

CodePen demo: Responsively crop copy, restore onclick via sliding drop-down animation

+

GitHub repo: crop-copy-slide-restore

+

Mike Foskett @ webSemantics

+ + + + + diff --git a/js/cropCopyRestore.2.0.js b/js/cropCopyRestore.2.0.js new file mode 100644 index 0000000..7576134 --- /dev/null +++ b/js/cropCopyRestore.2.0.js @@ -0,0 +1,252 @@ +// 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_copy"; + var iconSVG = ""; + var readMoreSpan = " [Read more]"; + + // 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); + + // 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("." + textClass).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) { + _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); + clone.querySelector("." + textClass).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("." + textClass); + + 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 _clicked(event) { + + var obj = event.target; + + 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 + "" +obj.innerHTML + ""; + } + + + 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)); + +// Run script on page load +window.addEventListener("load", cropCopyRestore, false); diff --git a/js/cropCopyRestore.2.0.min.js b/js/cropCopyRestore.2.0.min.js new file mode 100644 index 0000000..97e574a --- /dev/null +++ b/js/cropCopyRestore.2.0.min.js @@ -0,0 +1,4 @@ +// 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(e,t){"use strict";function n(e,t){return e.substring(0,e.lastIndexOf(t))}function i(e){return e.replace(/[ .,!?:;"“‘'\-]+$/,"")}function r(e,t){e.querySelector("."+E).textContent=t}function o(e){r(e,e.croppedText+T),e.innerHTML.match("visually-hidden")||(e.innerHTML+=R)}function a(e,t){e.setAttribute("aria-expanded",t)}function s(t){function n(e){r(e,e.fullText),e.removeChild(e.querySelector(".visually-hidden")),a(e,!0)}e.requestAnimationFrame?e.requestAnimationFrame(function(){n(t)}):n(t)}function l(e){o(e),a(e,!1)}function c(e,t){var n=e.cloneNode(!0);return n.classList.add(H),n.querySelector("."+E).textContent=t,e.parentNode.insertBefore(n,e.nextSibling),n.initialHeight=n.clientHeight,n}function u(e){var t=c(e,e.croppedText);return e.parentNode.removeChild(t),t.initialHeight}function d(e){var t=c(e,e.fullText);return e.parentNode.removeChild(t),t.initialHeight}function p(e){var t=e.fullText.split(" "),o=0,a=1,s=c(e,t[o]+" "),l=s.querySelector("."+E);for(o=1;o"+e.innerHTML+""}function y(){for(var e,n=t.querySelectorAll("["+L+"]"),i=n.length;i--;)e=n[i],e.fullText?(e.parentNode.classList.remove(N),x(e)):(C(e),g(e,i)),a(e,!1),e.croppedText=p(e),o(e),m(e)}var L="data-cropCopyRestore",b="CCR_btn-",T="…",H="CCR-clone",N="CCR-expanded",E="CCR_copy",A="",R=" [Read more]",k=function(e,t,n){var i;return function(){var r=this,o=arguments,a=function(){i=null,n||e.apply(r,o)},s=n&&!i;clearTimeout(i),i=setTimeout(a,t||200),s&&e.apply(r,o)}};!function(e){var n={transition:"transitionend",WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"otransitionend"},i=t.createElement("div");for(var r in n)if("undefined"!=typeof i.style[r]){e.transitionEnd=n[r];break}}(e),"classList"in t.documentElement||!Object.defineProperty||"undefined"==typeof HTMLElement||Object.defineProperty(HTMLElement.prototype,"classList",{get:function(){function e(e){return function(n){var i=t.className.split(/\s+/),r=i.indexOf(n);e(i,r,n),t.className=i.join(" ")}}var t=this,n={add:e(function(e,t,n){~t||e.push(n)}),remove:e(function(e,t){~t&&e.splice(t,1)}),toggle:e(function(e,t,n){~t?e.splice(t,1):e.push(n)}),contains:function(e){return!!~t.className.split(/\s+/).indexOf(e)},item:function(e){return t.className.split(/\s+/)[e]||null}};return Object.defineProperty(n,"length",{get:function(){return t.className.split(/\s+/).length}}),n}}),y(),e.addEventListener("resize",k(y,100,!1),!1)}(window,document); + +window.addEventListener("load",cropCopyRestore,!1); \ No newline at end of file diff --git a/license.md b/license.md new file mode 100644 index 0000000..405944c --- /dev/null +++ b/license.md @@ -0,0 +1,9 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to http://unlicense.org \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3b9944c --- /dev/null +++ b/readme.md @@ -0,0 +1,51 @@ +#Responsively crop copy, restore onclick via sliding drop-down animation + + +Responsively crop content copy down to a user-defined number of lines in an accessible manner. +Click to fully restore content via a sliding drop-down animation. +As used on Tesco Food Love Stories. + +Vanilla JavaScript, small script <2kB minified and gzipped. +I believe WCAG 2 level AA accessible but as yet unconfirmed. + +CodePen demo: Responsively crop copy, restore onclick via sliding drop-down animation + + +
+##Basic usage + +Incude a link to the styles: + +```html + +``` + +Add content block(s) and data attribute. +The attribute value is equal to the number of lines to display. + +```html +
+
+ Content copy... +
+
+``` + +Include a link to the script: + +```html + +``` + +
+##Status + +In cross-browser / platform testing. +Accessibility testing to follow. + +CodePen demo: Responsively crop copy, restore onclick via sliding drop-down animation +GitHub repo: crop-copy-slide-restore + + +
+Mike Foskett @ webSemantics