aboutsummaryrefslogblamecommitdiffstats
path: root/library/readmore.js/readmore.js
blob: d4c1cf72341fc4f6fcff2818f41ea27183ac2d9c (plain) (tree)






























































                                                                             
                     
                                              


                                                                                                        
                                            










































































                                                                                   









                                                  


                                                                              
                   
         










































                                                                                                  











































































































                                                                                       
/*!
 * @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,
        expandedHeight = el.outerHeight(true),
        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);