readmore.js 9.0 KB

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