/*!
* @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(factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser globals
factory(jQuery);
}
}(function($) {
'use strict';
var readmore = 'readmore',
defaults = {
speed: 100,
collapsedHeight: 200,
heightMargin: 16,
moreLink: 'Read More',
lessLink: 'Close',
embedCSS: true,
blockCSS: 'display: block; width: 100%;',
startOpen: false,
// callbacks
blockProcessed: function() {},
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 = cssMaxHeight || element.data('collapsedHeight') || defaultHeight;
// 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) {
this.element = element;
this.options = $.extend({}, defaults, options);
embedCSS(this.options);
this._defaults = defaults;
this._name = readmore;
this.init();
// IE8 chokes on `window.addEventListener`, so need to test for support.
if (window.addEventListener) {
// Need to resize boxes when the page has fully loaded.
window.addEventListener('load', resizeBoxes);
window.addEventListener('resize', resizeBoxes);
}
else {
window.attachEvent('load', resizeBoxes);
window.attachEvent('resize', resizeBoxes);
}
}
Readmore.prototype = {
init: function() {
var current = $(this.element);
current.data({
defaultHeight: this.options.collapsedHeight,
heightMargin: this.options.heightMargin
});
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.
if (this.options.blockProcessed && typeof this.options.blockProcessed === 'function') {
this.options.blockProcessed(current, false);
}
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': this.options.startOpen,
'id': id
});
current.after($(useLink)
.on('click', (function(_this) {
return function(event) {
_this.toggle(this, current[0], event);
};
})(this))
.attr({
'data-readmore-toggle': id,
'aria-controls': id
}));
if (! this.options.startOpen) {
current.css({
height: collapsedHeight
});
}
if (this.options.blockProcessed && typeof this.options.blockProcessed === 'function') {
this.options.blockProcessed(current, true);
}
}
},
toggle: function(trigger, element, event) {
if (event) {
event.preventDefault();
}
if (! trigger) {
trigger = $('[aria-controls="' + this.element.id + '"]')[0];
}
if (! element) {
element = this.element;
}
var $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`
if (this.options.beforeToggle && typeof this.options.beforeToggle === 'function') {
this.options.beforeToggle(trigger, $element, ! expanded);
}
$element.css({'height': newHeight});
// Fire afterToggle callback
$element.on('transitionend', (function(_this) {
return function() {
if (_this.options.afterToggle && typeof _this.options.afterToggle === 'function') {
_this.options.afterToggle(trigger, $element, expanded);
}
$(this).attr({
'aria-expanded': expanded
}).off('transitionend');
};
})(this));
$(trigger).replaceWith($(this.options[newLink])
.on('click', (function(_this) {
return function(event) {
_this.toggle(this, element, event);
};
})(this))
.attr({
'data-readmore-toggle': $element.attr('id'),
'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));
}
});
}
};
}));