diff options
Diffstat (limited to 'library/readmore.js')
-rw-r--r-- | library/readmore.js/README.md | 171 | ||||
-rw-r--r-- | library/readmore.js/readmore.js | 319 |
2 files changed, 490 insertions, 0 deletions
diff --git a/library/readmore.js/README.md b/library/readmore.js/README.md new file mode 100644 index 000000000..0116fbe8b --- /dev/null +++ b/library/readmore.js/README.md @@ -0,0 +1,171 @@ +# Readmore.js + +A smooth, responsive jQuery plugin for collapsing and expanding long blocks of text with "Read more" and "Close" links. + +The markup Readmore.js requires is so simple, you can probably use it with your existing HTML—there's no need for complicated sets of `div`'s or hardcoded classes, just call `.readmore()` on the element containing your block of text and Readmore.js takes care of the rest. Readmore.js plays well in a responsive environment, too. + +Readmore.js is tested with—and supported on—all versions of jQuery greater than 1.9.1. All the "good" browsers are supported, as well as IE10+; IE8 & 9 _should_ work, but are not supported and the experience will not be ideal. + + +## Install + +Install Readmore.js with Bower: + +``` +$ bower install readmore +``` + +Then include it in your HTML: + +```html +<script src="/bower_components/readmore/readmore.min.js"></script> +``` + + +## Use + +```javascript +$('article').readmore(); +``` + +It's that simple. You can change the speed of the animation, the height of the collapsed block, and the open and close elements. + +```javascript +$('article').readmore({ + speed: 75, + lessLink: '<a href="#">Read less</a>' +}); +``` + +### The options: + +* `speed: 100` in milliseconds +* `collapsedHeight: 200` in pixels +* `heightMargin: 16` in pixels, avoids collapsing blocks that are only slightly larger than `collapsedHeight` +* `moreLink: '<a href="#">Read more</a>'` +* `lessLink: '<a href="#">Close</a>'` +* `embedCSS: true` insert required CSS dynamically, set this to `false` if you include the necessary CSS in a stylesheet +* `blockCSS: 'display: block; width: 100%;'` sets the styling of the blocks, ignored if `embedCSS` is `false` +* `startOpen: false` do not immediately truncate, start in the fully opened position +* `beforeToggle: function() {}` called after a more or less link is clicked, but *before* the block is collapsed or expanded +* `afterToggle: function() {}` called *after* the block is collapsed or expanded + +If the element has a `max-height` CSS property, Readmore.js will use that value rather than the value of the `collapsedHeight` option. + +### The callbacks: + +The callback functions, `beforeToggle` and `afterToggle`, both receive the same arguments: `trigger`, `element`, and `expanded`. + +* `trigger`: the "Read more" or "Close" element that was clicked +* `element`: the block that is being collapsed or expanded +* `expanded`: Boolean; `true` means the block is expanded + +#### Callback example: + +Here's an example of how you could use the `afterToggle` callback to scroll back to the top of a block when the "Close" link is clicked. + +```javascript +$('article').readmore({ + afterToggle: function(trigger, element, expanded) { + if(! expanded) { // The "Close" link was clicked + $('html, body').animate( { scrollTop: element.offset().top }, {duration: 100 } ); + } + } +}); +``` + +### Removing Readmore: + +You can remove the Readmore.js functionality like so: + +```javascript +$('article').readmore('destroy'); +``` + +Or, you can be more surgical by specifying a particular element: + +```javascript +$('article:first').readmore('destroy'); +``` + +### Toggling blocks programmatically: + +You can toggle a block from code: + +```javascript +$('article:nth-of-type(3)').readmore('toggle'); +``` + + +## CSS: + +Readmore.js is designed to use CSS for as much functionality as possible: collapsed height can be set in CSS with the `max-height` property; "collapsing" is achieved by setting `overflow: hidden` on the containing block and changing the `height` property; and, finally, the expanding/collapsing animation is done with CSS3 transitions. + +By default, Readmore.js inserts the following CSS, in addition to some transition-related rules: + +```css +selector + [data-readmore-toggle], selector[data-readmore] { + display: block; + width: 100%; +} +``` + +_`selector` would be the element you invoked `readmore()` on, e.g.: `$('selector').readmore()`_ + +You can override the base rules when you set up Readmore.js like so: + +```javascript +$('article').readmore({blockCSS: 'display: inline-block; width: 50%;'}); +``` + +If you want to include the necessary styling in your site's stylesheet, you can disable the dynamic embedding by setting `embedCSS` to `false`: + +```javascript +$('article').readmore({embedCSS: false}); +``` + +### Media queries and other CSS tricks: + +If you wanted to set a `maxHeight` based on lines, you could do so in CSS with something like: + +```css +body { + font: 16px/1.5 sans-serif; +} + +/* Show only 4 lines in smaller screens */ +article { + max-height: 6em; /* (4 * 1.5 = 6) */ +} +``` + +Then, with a media query you could change the number of lines shown, like so: + +```css +/* Show 8 lines on larger screens */ +@media screen and (min-width: 640px) { + article { + max-height: 12em; + } +} +``` + + +## Contributing + +Pull requests are always welcome, but not all suggested features will get merged. Feel free to contact me if you have an idea for a feature. + +Pull requests should include the minified script and this readme and the demo HTML should be updated with descriptions of your new feature. + +You'll need NPM: + +``` +$ npm install +``` + +Which will install the necessary development dependencies. Then, to build the minified script: + +``` +$ gulp compress +``` + diff --git a/library/readmore.js/readmore.js b/library/readmore.js/readmore.js new file mode 100644 index 000000000..81cfb3cea --- /dev/null +++ b/library/readmore.js/readmore.js @@ -0,0 +1,319 @@ +/*! + * @preserve + * + * Readmore.js jQuery plugin + * Author: @jed_foster + * Project home: http://jedfoster.github.io/Readmore.js + * Licensed under the MIT license + * + * Debounce function from http://davidwalsh.name/javascript-debounce-function + */ + +/* global jQuery */ + +(function($) { + 'use strict'; + + var readmore = 'readmore', + defaults = { + speed: 100, + collapsedHeight: 200, + heightMargin: 16, + moreLink: '<a href="#">Read More</a>', + lessLink: '<a href="#">Close</a>', + embedCSS: true, + blockCSS: 'display: block; width: 100%;', + startOpen: false, + + // callbacks + beforeToggle: function(){}, + afterToggle: function(){} + }, + cssEmbedded = {}, + uniqueIdCounter = 0; + + function debounce(func, wait, immediate) { + var timeout; + + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (! immediate) { + func.apply(context, args); + } + }; + var callNow = immediate && !timeout; + + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) { + func.apply(context, args); + } + }; + } + + function uniqueId(prefix) { + var id = ++uniqueIdCounter; + + return String(prefix == null ? 'rmjs-' : prefix) + id; + } + + function setBoxHeights(element) { + var el = element.clone().css({ + height: 'auto', + width: element.width(), + maxHeight: 'none', + overflow: 'hidden' + }).insertAfter(element), + expandedHeight = el.outerHeight(), + cssMaxHeight = parseInt(el.css({maxHeight: ''}).css('max-height').replace(/[^-\d\.]/g, ''), 10), + defaultHeight = element.data('defaultHeight'); + + el.remove(); + + var collapsedHeight = element.data('collapsedHeight') || defaultHeight; + + if (!cssMaxHeight) { + collapsedHeight = defaultHeight; + } + else if (cssMaxHeight > collapsedHeight) { + collapsedHeight = cssMaxHeight; + } + + // Store our measurements. + element.data({ + expandedHeight: expandedHeight, + maxHeight: cssMaxHeight, + collapsedHeight: collapsedHeight + }) + // and disable any `max-height` property set in CSS + .css({ + maxHeight: 'none' + }); + } + + var resizeBoxes = debounce(function() { + $('[data-readmore]').each(function() { + var current = $(this), + isExpanded = (current.attr('aria-expanded') === 'true'); + + setBoxHeights(current); + + current.css({ + height: current.data( (isExpanded ? 'expandedHeight' : 'collapsedHeight') ) + }); + }); + }, 100); + + function embedCSS(options) { + if (! cssEmbedded[options.selector]) { + var styles = ' '; + + if (options.embedCSS && options.blockCSS !== '') { + styles += options.selector + ' + [data-readmore-toggle], ' + + options.selector + '[data-readmore]{' + + options.blockCSS + + '}'; + } + + // Include the transition CSS even if embedCSS is false + styles += options.selector + '[data-readmore]{' + + 'transition: height ' + options.speed + 'ms;' + + 'overflow: hidden;' + + '}'; + + (function(d, u) { + var css = d.createElement('style'); + css.type = 'text/css'; + + if (css.styleSheet) { + css.styleSheet.cssText = u; + } + else { + css.appendChild(d.createTextNode(u)); + } + + d.getElementsByTagName('head')[0].appendChild(css); + }(document, styles)); + + cssEmbedded[options.selector] = true; + } + } + + function Readmore(element, options) { + var $this = this; + + this.element = element; + + this.options = $.extend({}, defaults, options); + + $(this.element).data({ + defaultHeight: this.options.collapsedHeight, + heightMargin: this.options.heightMargin + }); + + embedCSS(this.options); + + this._defaults = defaults; + this._name = readmore; + + window.addEventListener('load', function() { + $this.init(); + }); + } + + + Readmore.prototype = { + init: function() { + var $this = this; + + $(this.element).each(function() { + var current = $(this); + + setBoxHeights(current); + + var collapsedHeight = current.data('collapsedHeight'), + heightMargin = current.data('heightMargin'); + + if (current.outerHeight(true) <= collapsedHeight + heightMargin) { + // The block is shorter than the limit, so there's no need to truncate it. + return true; + } + else { + var id = current.attr('id') || uniqueId(), + useLink = $this.options.startOpen ? $this.options.lessLink : $this.options.moreLink; + + current.attr({ + 'data-readmore': '', + 'aria-expanded': false, + 'id': id + }); + + current.after($(useLink) + .on('click', function(event) { $this.toggle(this, current[0], event); }) + .attr({ + 'data-readmore-toggle': '', + 'aria-controls': id + })); + + if (! $this.options.startOpen) { + current.css({ + height: collapsedHeight + }); + } + } + }); + + window.addEventListener('resize', function() { + resizeBoxes(); + }); + }, + + toggle: function(trigger, element, event) { + if (event) { + event.preventDefault(); + } + + if (! trigger) { + trigger = $('[aria-controls="' + this.element.id + '"]')[0]; + } + + if (! element) { + element = this.element; + } + + var $this = this, + $element = $(element), + newHeight = '', + newLink = '', + expanded = false, + collapsedHeight = $element.data('collapsedHeight'); + + if ($element.height() <= collapsedHeight) { + newHeight = $element.data('expandedHeight') + 'px'; + newLink = 'lessLink'; + expanded = true; + } + else { + newHeight = collapsedHeight; + newLink = 'moreLink'; + } + + // Fire beforeToggle callback + // Since we determined the new "expanded" state above we're now out of sync + // with our true current state, so we need to flip the value of `expanded` + $this.options.beforeToggle(trigger, element, ! expanded); + + $element.css({'height': newHeight}); + + // Fire afterToggle callback + $element.on('transitionend', function() { + $this.options.afterToggle(trigger, element, expanded); + + $(this).attr({ + 'aria-expanded': expanded + }).off('transitionend'); + }); + + $(trigger).replaceWith($($this.options[newLink]) + .on('click', function(event) { $this.toggle(this, element, event); }) + .attr({ + 'data-readmore-toggle': '', + 'aria-controls': $element.attr('id') + })); + }, + + destroy: function() { + $(this.element).each(function() { + var current = $(this); + + current.attr({ + 'data-readmore': null, + 'aria-expanded': null + }) + .css({ + maxHeight: '', + height: '' + }) + .next('[data-readmore-toggle]') + .remove(); + + current.removeData(); + }); + } + }; + + + $.fn.readmore = function(options) { + var args = arguments, + selector = this.selector; + + options = options || {}; + + if (typeof options === 'object') { + return this.each(function() { + if ($.data(this, 'plugin_' + readmore)) { + var instance = $.data(this, 'plugin_' + readmore); + instance.destroy.apply(instance); + } + + options.selector = selector; + + $.data(this, 'plugin_' + readmore, new Readmore(this, options)); + }); + } + else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { + return this.each(function () { + var instance = $.data(this, 'plugin_' + readmore); + if (instance instanceof Readmore && typeof instance[options] === 'function') { + instance[options].apply(instance, Array.prototype.slice.call(args, 1)); + } + }); + } + }; + +})(jQuery); + + |