/*! * @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: 'Read More', lessLink: 'Close', 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, expandedHeight = el.outerHeight(), cssMaxHeight = parseInt(el.css({maxHeight: ''}).css('max-height').replace(/[^-\d\.]/g, ''), 10), defaultHeight = element.data('defaultHeight'); console.log("el height: " + expandedHeight); 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; // Waiting for the page to load doesn't work when there is dynamic content // But usually we already have the content, so no need to wait //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 }); } } }); }, 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);