Skip to content

Instantly share code, notes, and snippets.

@ziazek
Created January 9, 2015 08:25
Show Gist options
  • Save ziazek/c2012fc48f441db63656 to your computer and use it in GitHub Desktop.
Save ziazek/c2012fc48f441db63656 to your computer and use it in GitHub Desktop.
readmore.js
/*!
* @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();
});
$(document).ready(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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment