readmore.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /*!
  2. * @preserve
  3. *
  4. * Readmore.js jQuery plugin
  5. * Author: @jed_foster
  6. * Project home: http://jedfoster.github.io/Readmore.js
  7. * Licensed under the MIT license
  8. *
  9. * Debounce function from http://davidwalsh.name/javascript-debounce-function
  10. */
  11. /* global jQuery */
  12. (function(factory) {
  13. if (typeof define === 'function' && define.amd) {
  14. // AMD
  15. define(['jquery'], factory);
  16. } else if (typeof exports === 'object') {
  17. // CommonJS
  18. module.exports = factory(require('jquery'));
  19. } else {
  20. // Browser globals
  21. factory(jQuery);
  22. }
  23. }(function($) {
  24. 'use strict';
  25. var readmore = 'readmore',
  26. defaults = {
  27. speed: 100,
  28. collapsedHeight: 200,
  29. heightMargin: 16,
  30. moreLink: '<a href="#">Read More</a>',
  31. lessLink: '<a href="#">Close</a>',
  32. embedCSS: true,
  33. blockCSS: 'display: block; width: 100%;',
  34. startOpen: false,
  35. // callbacks
  36. beforeToggle: function(){},
  37. afterToggle: function(){}
  38. },
  39. cssEmbedded = {},
  40. uniqueIdCounter = 0;
  41. function debounce(func, wait, immediate) {
  42. var timeout;
  43. return function() {
  44. var context = this, args = arguments;
  45. var later = function() {
  46. timeout = null;
  47. if (! immediate) {
  48. func.apply(context, args);
  49. }
  50. };
  51. var callNow = immediate && !timeout;
  52. clearTimeout(timeout);
  53. timeout = setTimeout(later, wait);
  54. if (callNow) {
  55. func.apply(context, args);
  56. }
  57. };
  58. }
  59. function uniqueId(prefix) {
  60. var id = ++uniqueIdCounter;
  61. return String(prefix == null ? 'rmjs-' : prefix) + id;
  62. }
  63. function setBoxHeights(element) {
  64. var el = element.clone().css({
  65. height: 'auto',
  66. width: element.width(),
  67. maxHeight: 'none',
  68. overflow: 'hidden'
  69. }).insertAfter(element),
  70. expandedHeight = el.outerHeight(),
  71. cssMaxHeight = parseInt(el.css({maxHeight: ''}).css('max-height').replace(/[^-\d\.]/g, ''), 10),
  72. defaultHeight = element.data('defaultHeight');
  73. el.remove();
  74. var collapsedHeight = cssMaxHeight || element.data('collapsedHeight') || defaultHeight;
  75. // Store our measurements.
  76. element.data({
  77. expandedHeight: expandedHeight,
  78. maxHeight: cssMaxHeight,
  79. collapsedHeight: collapsedHeight
  80. })
  81. // and disable any `max-height` property set in CSS
  82. .css({
  83. maxHeight: 'none'
  84. });
  85. }
  86. var resizeBoxes = debounce(function() {
  87. $('[data-readmore]').each(function() {
  88. var current = $(this),
  89. isExpanded = (current.attr('aria-expanded') === 'true');
  90. setBoxHeights(current);
  91. current.css({
  92. height: current.data( (isExpanded ? 'expandedHeight' : 'collapsedHeight') )
  93. });
  94. });
  95. }, 100);
  96. function embedCSS(options) {
  97. if (! cssEmbedded[options.selector]) {
  98. var styles = ' ';
  99. if (options.embedCSS && options.blockCSS !== '') {
  100. styles += options.selector + ' + [data-readmore-toggle], ' +
  101. options.selector + '[data-readmore]{' +
  102. options.blockCSS +
  103. '}';
  104. }
  105. // Include the transition CSS even if embedCSS is false
  106. styles += options.selector + '[data-readmore]{' +
  107. 'transition: height ' + options.speed + 'ms;' +
  108. 'overflow: hidden;' +
  109. '}';
  110. (function(d, u) {
  111. var css = d.createElement('style');
  112. css.type = 'text/css';
  113. if (css.styleSheet) {
  114. css.styleSheet.cssText = u;
  115. }
  116. else {
  117. css.appendChild(d.createTextNode(u));
  118. }
  119. d.getElementsByTagName('head')[0].appendChild(css);
  120. }(document, styles));
  121. cssEmbedded[options.selector] = true;
  122. }
  123. }
  124. function Readmore(element, options) {
  125. this.element = element;
  126. this.options = $.extend({}, defaults, options);
  127. embedCSS(this.options);
  128. this._defaults = defaults;
  129. this._name = readmore;
  130. this.init();
  131. // IE8 chokes on `window.addEventListener`, so need to test for support.
  132. if (window.addEventListener) {
  133. // Need to resize boxes when the page has fully loaded.
  134. window.addEventListener('load', resizeBoxes);
  135. window.addEventListener('resize', resizeBoxes);
  136. }
  137. else {
  138. window.attachEvent('load', resizeBoxes);
  139. window.attachEvent('resize', resizeBoxes);
  140. }
  141. }
  142. Readmore.prototype = {
  143. init: function() {
  144. var current = $(this.element);
  145. current.data({
  146. defaultHeight: this.options.collapsedHeight,
  147. heightMargin: this.options.heightMargin
  148. });
  149. setBoxHeights(current);
  150. var collapsedHeight = current.data('collapsedHeight'),
  151. heightMargin = current.data('heightMargin');
  152. if (current.outerHeight(true) <= collapsedHeight + heightMargin) {
  153. // The block is shorter than the limit, so there's no need to truncate it.
  154. return true;
  155. }
  156. else {
  157. var id = current.attr('id') || uniqueId(),
  158. useLink = this.options.startOpen ? this.options.lessLink : this.options.moreLink;
  159. current.attr({
  160. 'data-readmore': '',
  161. 'aria-expanded': this.options.startOpen,
  162. 'id': id
  163. });
  164. current.after($(useLink)
  165. .on('click', (function(_this) {
  166. return function(event) {
  167. _this.toggle(this, current[0], event);
  168. };
  169. })(this))
  170. .attr({
  171. 'data-readmore-toggle': '',
  172. 'aria-controls': id
  173. }));
  174. if (! this.options.startOpen) {
  175. current.css({
  176. height: collapsedHeight
  177. });
  178. }
  179. }
  180. },
  181. toggle: function(trigger, element, event) {
  182. if (event) {
  183. event.preventDefault();
  184. }
  185. if (! trigger) {
  186. trigger = $('[aria-controls="' + _this.element.id + '"]')[0];
  187. }
  188. if (! element) {
  189. element = _this.element;
  190. }
  191. var $element = $(element),
  192. newHeight = '',
  193. newLink = '',
  194. expanded = false,
  195. collapsedHeight = $element.data('collapsedHeight');
  196. if ($element.height() <= collapsedHeight) {
  197. newHeight = $element.data('expandedHeight') + 'px';
  198. newLink = 'lessLink';
  199. expanded = true;
  200. }
  201. else {
  202. newHeight = collapsedHeight;
  203. newLink = 'moreLink';
  204. }
  205. // Fire beforeToggle callback
  206. // Since we determined the new "expanded" state above we're now out of sync
  207. // with our true current state, so we need to flip the value of `expanded`
  208. this.options.beforeToggle(trigger, $element, ! expanded);
  209. $element.css({'height': newHeight});
  210. // Fire afterToggle callback
  211. $element.on('transitionend', (function(_this) {
  212. return function() {
  213. _this.options.afterToggle(trigger, $element, expanded);
  214. $(this).attr({
  215. 'aria-expanded': expanded
  216. }).off('transitionend');
  217. }
  218. })(this));
  219. $(trigger).replaceWith($(this.options[newLink])
  220. .on('click', (function(_this) {
  221. return function(event) {
  222. _this.toggle(this, element, event);
  223. };
  224. })(this))
  225. .attr({
  226. 'data-readmore-toggle': '',
  227. 'aria-controls': $element.attr('id')
  228. }));
  229. },
  230. destroy: function() {
  231. $(this.element).each(function() {
  232. var current = $(this);
  233. current.attr({
  234. 'data-readmore': null,
  235. 'aria-expanded': null
  236. })
  237. .css({
  238. maxHeight: '',
  239. height: ''
  240. })
  241. .next('[data-readmore-toggle]')
  242. .remove();
  243. current.removeData();
  244. });
  245. }
  246. };
  247. $.fn.readmore = function(options) {
  248. var args = arguments,
  249. selector = this.selector;
  250. options = options || {};
  251. if (typeof options === 'object') {
  252. return this.each(function() {
  253. if ($.data(this, 'plugin_' + readmore)) {
  254. var instance = $.data(this, 'plugin_' + readmore);
  255. instance.destroy.apply(instance);
  256. }
  257. options.selector = selector;
  258. $.data(this, 'plugin_' + readmore, new Readmore(this, options));
  259. });
  260. }
  261. else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') {
  262. return this.each(function () {
  263. var instance = $.data(this, 'plugin_' + readmore);
  264. if (instance instanceof Readmore && typeof instance[options] === 'function') {
  265. instance[options].apply(instance, Array.prototype.slice.call(args, 1));
  266. }
  267. });
  268. }
  269. };
  270. }));