343 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
		
			9.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*!
 | |
|  * @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: '<a href="#">Read More</a>',
 | |
|         lessLink: '<a href="#">Close</a>',
 | |
|         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 = 100 + '%',
 | |
|         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.height() > current.data('collapsedHeight'));
 | |
| 
 | |
|       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');
 | |
|         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));
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
| }));
 | |
| 
 |