Created
January 9, 2015 08:25
-
-
Save ziazek/c2012fc48f441db63656 to your computer and use it in GitHub Desktop.
readmore.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* @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.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 = 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; | |
window.addEventListener('load', function() { | |
$this.init(); | |
}); | |
$(document).ready(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 | |
}); | |
} | |
} | |
}); | |
window.addEventListener('resize', function() { | |
resizeBoxes(); | |
}); | |
}, | |
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); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment