summaryrefslogtreecommitdiffstats
path: root/readmore.js
diff options
context:
space:
mode:
Diffstat (limited to 'readmore.js')
-rw-r--r--readmore.js306
1 files changed, 216 insertions, 90 deletions
diff --git a/readmore.js b/readmore.js
index 1229fb4..3a993de 100644
--- a/readmore.js
+++ b/readmore.js
@@ -1,192 +1,318 @@
/*!
+ * @preserve
+ *
* Readmore.js jQuery plugin
* Author: @jed_foster
- * Project home: jedfoster.github.io/Readmore.js
+ * Project home: http://jedfoster.github.io/Readmore.js
* Licensed under the MIT license
+ *
+ * Debounce function from http://davidwalsh.name/javascript-debounce-function
*/
-;(function($) {
+/* global jQuery */
+
+(function($) {
+ 'use strict';
var readmore = 'readmore',
defaults = {
speed: 100,
- maxHeight: 200,
+ collapsedHeight: 200,
heightMargin: 16,
moreLink: '<a href="#">Read More</a>',
lessLink: '<a href="#">Close</a>',
embedCSS: true,
- sectionCSS: 'display: block; width: 100%;',
+ blockCSS: 'display: block; width: 100%;',
startOpen: false,
- expandedClass: 'readmore-js-expanded',
- collapsedClass: 'readmore-js-collapsed',
// callbacks
beforeToggle: function(){},
afterToggle: function(){}
},
- cssEmbedded = false;
+ 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;
- function Readmore( element, options ) {
- this.element = element;
+ 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(true),
+ 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'
+ });
+ }
- this.options = $.extend( {}, defaults, options);
+ var resizeBoxes = debounce(function() {
+ $('[data-readmore]').each(function() {
+ var current = $(this),
+ isExpanded = (current.attr('aria-expanded') === 'true');
- $(this.element).data('max-height', this.options.maxHeight);
- $(this.element).data('height-margin', this.options.heightMargin);
+ setBoxHeights(current);
- delete(this.options.maxHeight);
+ 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 +
+ '}';
+ }
- if(this.options.embedCSS && ! cssEmbedded) {
- var styles = '.readmore-js-toggle, .readmore-js-section { ' + this.options.sectionCSS + ' } .readmore-js-section { overflow: hidden; }';
+ // 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');
+ (function(d, u) {
+ var css = d.createElement('style');
css.type = 'text/css';
- if(css.styleSheet) {
- css.styleSheet.cssText = u;
+
+ if (css.styleSheet) {
+ css.styleSheet.cssText = u;
}
else {
- css.appendChild(d.createTextNode(u));
+ css.appendChild(d.createTextNode(u));
}
+
d.getElementsByTagName('head')[0].appendChild(css);
}(document, styles));
- cssEmbedded = true;
+ 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;
- this.init();
+ window.addEventListener('load', function() {
+ $this.init();
+ });
}
- Readmore.prototype = {
+ Readmore.prototype = {
init: function() {
var $this = this;
$(this.element).each(function() {
- var current = $(this),
- maxHeight = (parseInt(current.css('max-height').replace(/[^-\d\.]/g, ''), 10) > current.data('max-height')) ? parseInt(current.css('max-height').replace(/[^-\d\.]/g, ''), 10) : current.data('max-height'),
- heightMargin = current.data('height-margin');
+ var current = $(this);
- if(current.css('max-height') != 'none') {
- current.css('max-height', 'none');
- }
+ setBoxHeights(current);
- $this.setBoxHeight(current);
+ var collapsedHeight = current.data('collapsedHeight'),
+ heightMargin = current.data('heightMargin');
- if(current.outerHeight(true) <= maxHeight + 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 {
- current.addClass('readmore-js-section ' + $this.options.collapsedClass).data('collapsedHeight', maxHeight);
-
- var useLink = $this.options.startOpen ? $this.options.lessLink : $this.options.moreLink;
- current.after($(useLink).on('click', function(event) { $this.toggleSlider(this, current, event) }).addClass('readmore-js-toggle'));
-
- if(!$this.options.startOpen) {
- current.css({height: maxHeight});
+ 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).on('resize', function(event) {
- $this.resizeBoxes();
+ window.addEventListener('resize', function() {
+ resizeBoxes();
});
},
- toggleSlider: function(trigger, element, event)
- {
- event.preventDefault();
+ 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,
- newHeight = newLink = sectionClass = '',
+ $element = $(element),
+ newHeight = '',
+ newLink = '',
expanded = false,
- collapsedHeight = $(element).data('collapsedHeight');
+ collapsedHeight = $element.data('collapsedHeight');
- if ($(element).height() <= collapsedHeight) {
- newHeight = $(element).data('expandedHeight') + 'px';
+ if ($element.height() <= collapsedHeight) {
+ newHeight = $element.data('expandedHeight') + 'px';
newLink = 'lessLink';
expanded = true;
- sectionClass = $this.options.expandedClass;
}
-
else {
newHeight = collapsedHeight;
newLink = 'moreLink';
- sectionClass = $this.options.collapsedClass;
}
// Fire beforeToggle callback
- $this.options.beforeToggle(trigger, element, expanded);
+ // 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).animate({'height': newHeight}, {duration: $this.options.speed, complete: function() {
- // Fire afterToggle callback
- $this.options.afterToggle(trigger, element, expanded);
+ $element.css({'height': newHeight});
- $(trigger).replaceWith($($this.options[newLink]).on('click', function(event) { $this.toggleSlider(this, element, event) }).addClass('readmore-js-toggle'));
+ // Fire afterToggle callback
+ $element.on('transitionend', function() {
+ $this.options.afterToggle(trigger, element, expanded);
- $(this).removeClass($this.options.collapsedClass + ' ' + $this.options.expandedClass).addClass(sectionClass);
- }
+ $(this).attr({
+ 'aria-expanded': expanded
+ }).off('transitionend');
});
- },
-
- setBoxHeight: function(element) {
- var el = element.clone().css({'height': 'auto', 'width': element.width(), 'overflow': 'hidden'}).insertAfter(element),
- height = el.outerHeight(true);
-
- el.remove();
- element.data('expandedHeight', height);
- },
-
- resizeBoxes: function() {
- var $this = this;
-
- $('.readmore-js-section').each(function() {
- var current = $(this);
-
- $this.setBoxHeight(current);
-
- if(current.height() > current.data('expandedHeight') || (current.hasClass($this.options.expandedClass) && current.height() < current.data('expandedHeight')) ) {
- current.css('height', current.data('expandedHeight'));
- }
- });
+ $(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() {
- var $this = this;
-
$(this.element).each(function() {
var current = $(this);
- current.removeClass('readmore-js-section ' + $this.options.collapsedClass + ' ' + $this.options.expandedClass).css({'max-height': '', 'height': 'auto'}).next('.readmore-js-toggle').remove();
+ 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;
- if (options === undefined || typeof options === 'object') {
- return this.each(function () {
+
+ $.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);
+ instance.destroy.apply(instance);
}
- $.data(this, 'plugin_' + readmore, new Readmore( this, options ));
+ options.selector = selector;
+
+ $.data(this, 'plugin_' + readmore, new Readmore(this, options));
});
- } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
+ }
+ 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 ) );
+ instance[options].apply(instance, Array.prototype.slice.call(args, 1));
}
});
}
};
+
})(jQuery);
+