PageRenderTime 27ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/x2engine/js/jquery-expander/jquery.expander.js

https://gitlab.com/e0/X2CRM
JavaScript | 400 lines | 267 code | 70 blank | 63 comment | 46 complexity | a97189cde59a8eed28d86f273bcaf68a MD5 | raw file
  1. /*!
  2. * Expander - v1.4.7 - 2013-08-30
  3. * http://plugins.learningjquery.com/expander/
  4. * Copyright (c) 2013 Karl Swedberg
  5. * Licensed MIT (http://www.opensource.org/licenses/mit-license.php)
  6. */
  7. (function($) {
  8. $.expander = {
  9. version: '1.4.7',
  10. defaults: {
  11. // the number of characters at which the contents will be sliced into two parts.
  12. slicePoint: 100,
  13. // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false)
  14. preserveWords: true,
  15. // a threshold of sorts for whether to initially hide/collapse part of the element's contents.
  16. // If after slicing the contents in two there are fewer words in the second part than
  17. // the value set by widow, we won't bother hiding/collapsing anything.
  18. widow: 4,
  19. // text displayed in a link instead of the hidden part of the element.
  20. // clicking this will expand/show the hidden/collapsed text
  21. expandText: 'read more',
  22. expandPrefix: '… ',
  23. expandAfterSummary: false,
  24. // class names for summary element and detail element
  25. summaryClass: 'summary',
  26. detailClass: 'details',
  27. // class names for <span> around "read-more" link and "read-less" link
  28. moreClass: 'read-more',
  29. lessClass: 'read-less',
  30. // number of milliseconds after text has been expanded at which to collapse the text again.
  31. // when 0, no auto-collapsing
  32. collapseTimer: 0,
  33. // effects for expanding and collapsing
  34. expandEffect: 'slideDown',
  35. expandSpeed: 250,
  36. collapseEffect: 'slideUp',
  37. collapseSpeed: 200,
  38. // allow the user to re-collapse the expanded text.
  39. userCollapse: true,
  40. // text to use for the link to re-collapse the text
  41. userCollapseText: 'read less',
  42. userCollapsePrefix: ' ',
  43. // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called
  44. onSlice: null, // function() {}
  45. beforeExpand: null, // function() {},
  46. afterExpand: null, // function() {},
  47. onCollapse: null, // function(byUser) {}
  48. afterCollapse: null // function() {}
  49. }
  50. };
  51. $.fn.expander = function(options) {
  52. var meth = 'init';
  53. if (typeof options === 'string') {
  54. meth = options;
  55. options = {};
  56. }
  57. var opts = $.extend({}, $.expander.defaults, options),
  58. rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i,
  59. rAmpWordEnd = opts.wordEnd || /(&(?:[^;]+;)?|[a-zA-Z\u00C0-\u0100]+)$/,
  60. rOpenCloseTag = /<\/?(\w+)[^>]*>/g,
  61. rOpenTag = /<(\w+)[^>]*>/g,
  62. rCloseTag = /<\/(\w+)>/g,
  63. rLastCloseTag = /(<\/[^>]+>)\s*$/,
  64. rTagPlus = /^(<[^>]+>)+.?/,
  65. delayedCollapse;
  66. var methods = {
  67. init: function() {
  68. this.each(function() {
  69. var i, l, tmp, newChar, summTagless, summOpens, summCloses,
  70. lastCloseTag, detailText, detailTagless, html, expand,
  71. $thisDetails, $readMore,
  72. openTagsForDetails = [],
  73. closeTagsForsummaryText = [],
  74. defined = {},
  75. thisEl = this,
  76. $this = $(this),
  77. $summEl = $([]),
  78. o = $.extend({}, opts, $this.data('expander') || $.meta && $this.data() || {}),
  79. hasDetails = !!$this.find('.' + o.detailClass).length,
  80. hasBlocks = !!$this.find('*').filter(function() {
  81. var display = $(this).css('display');
  82. return (/^block|table|list/).test(display);
  83. }).length,
  84. el = hasBlocks ? 'div' : 'span',
  85. detailSelector = el + '.' + o.detailClass,
  86. moreClass = o.moreClass + '',
  87. lessClass = o.lessClass + '',
  88. expandSpeed = o.expandSpeed || 0,
  89. allHtml = $.trim( $this.html() ),
  90. allText = $.trim( $this.text() ),
  91. summaryText = allHtml.slice(0, o.slicePoint);
  92. // allow multiple classes for more/less links
  93. o.moreSelector = 'span.' + moreClass.split(' ').join('.');
  94. o.lessSelector = 'span.' + lessClass.split(' ').join('.');
  95. // bail out if we've already set up the expander on this element
  96. if ( $.data(this, 'expanderInit') ) {
  97. return;
  98. }
  99. $.data(this, 'expanderInit', true);
  100. $.data(this, 'expander', o);
  101. // determine which callback functions are defined
  102. $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse', 'afterCollapse'], function(index, val) {
  103. defined[val] = $.isFunction(o[val]);
  104. });
  105. // back up if we're in the middle of a tag or word
  106. summaryText = backup(summaryText);
  107. // summary text sans tags length
  108. summTagless = summaryText.replace(rOpenCloseTag, '').length;
  109. // add more characters to the summary, one for each character in the tags
  110. while (summTagless < o.slicePoint) {
  111. newChar = allHtml.charAt(summaryText.length);
  112. if (newChar === '<') {
  113. newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0];
  114. }
  115. summaryText += newChar;
  116. summTagless++;
  117. }
  118. summaryText = backup(summaryText, o.preserveWords);
  119. // separate open tags from close tags and clean up the lists
  120. summOpens = summaryText.match(rOpenTag) || [];
  121. summCloses = summaryText.match(rCloseTag) || [];
  122. // filter out self-closing tags
  123. tmp = [];
  124. $.each(summOpens, function(index, val) {
  125. if ( !rSelfClose.test(val) ) {
  126. tmp.push(val);
  127. }
  128. });
  129. summOpens = tmp;
  130. // strip close tags to just the tag name
  131. l = summCloses.length;
  132. for (i = 0; i < l; i++) {
  133. summCloses[i] = summCloses[i].replace(rCloseTag, '$1');
  134. }
  135. // tags that start in summary and end in detail need:
  136. // a). close tag at end of summary
  137. // b). open tag at beginning of detail
  138. $.each(summOpens, function(index, val) {
  139. var thisTagName = val.replace(rOpenTag, '$1');
  140. var closePosition = $.inArray(thisTagName, summCloses);
  141. if (closePosition === -1) {
  142. openTagsForDetails.push(val);
  143. closeTagsForsummaryText.push('</' + thisTagName + '>');
  144. } else {
  145. summCloses.splice(closePosition, 1);
  146. }
  147. });
  148. // reverse the order of the close tags for the summary so they line up right
  149. closeTagsForsummaryText.reverse();
  150. // create necessary summary and detail elements if they don't already exist
  151. if ( !hasDetails ) {
  152. // end script if there is no detail text or if detail has fewer words than widow option
  153. detailText = allHtml.slice(summaryText.length);
  154. detailTagless = $.trim( detailText.replace(rOpenCloseTag, '') );
  155. if ( detailTagless === '' || detailTagless.split(/\s+/).length < o.widow ) {
  156. return;
  157. }
  158. // otherwise, continue...
  159. lastCloseTag = closeTagsForsummaryText.pop() || '';
  160. summaryText += closeTagsForsummaryText.join('');
  161. detailText = openTagsForDetails.join('') + detailText;
  162. } else {
  163. // assume that even if there are details, we still need readMore/readLess/summary elements
  164. // (we already bailed out earlier when readMore el was found)
  165. // but we need to create els differently
  166. // remove the detail from the rest of the content
  167. detailText = $this.find(detailSelector).remove().html();
  168. // The summary is what's left
  169. summaryText = $this.html();
  170. // allHtml is the summary and detail combined (this is needed when content has block-level elements)
  171. allHtml = summaryText + detailText;
  172. lastCloseTag = '';
  173. }
  174. o.moreLabel = $this.find(o.moreSelector).length ? '' : buildMoreLabel(o);
  175. if (hasBlocks) {
  176. detailText = allHtml;
  177. }
  178. summaryText += lastCloseTag;
  179. // onSlice callback
  180. o.summary = summaryText;
  181. o.details = detailText;
  182. o.lastCloseTag = lastCloseTag;
  183. if (defined.onSlice) {
  184. // user can choose to return a modified options object
  185. // one last chance for user to change the options. sneaky, huh?
  186. // but could be tricky so use at your own risk.
  187. tmp = o.onSlice.call(thisEl, o);
  188. // so, if the returned value from the onSlice function is an object with a details property, we'll use that!
  189. o = tmp && tmp.details ? tmp : o;
  190. }
  191. // build the html with summary and detail and use it to replace old contents
  192. html = buildHTML(o, hasBlocks);
  193. $this.html( html );
  194. // set up details and summary for expanding/collapsing
  195. $thisDetails = $this.find(detailSelector);
  196. $readMore = $this.find(o.moreSelector);
  197. // Hide details span using collapseEffect unless
  198. // expandEffect is NOT slideDown and collapseEffect IS slideUp.
  199. // The slideUp effect sets span's "default" display to
  200. // inline-block. This is necessary for slideDown, but
  201. // problematic for other "showing" animations.
  202. // Fixes #46
  203. if (o.collapseEffect === 'slideUp' && o.expandEffect !== 'slideDown' || $this.is(':hidden')) {
  204. $thisDetails.css({display: 'none'});
  205. } else {
  206. $thisDetails[o.collapseEffect](0);
  207. }
  208. $summEl = $this.find('div.' + o.summaryClass);
  209. expand = function(event) {
  210. event.preventDefault();
  211. $readMore.hide();
  212. $summEl.hide();
  213. if (defined.beforeExpand) {
  214. o.beforeExpand.call(thisEl);
  215. }
  216. $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() {
  217. $thisDetails.css({zoom: ''});
  218. if (defined.afterExpand) {o.afterExpand.call(thisEl);}
  219. delayCollapse(o, $thisDetails, thisEl);
  220. });
  221. };
  222. $readMore.find('a').unbind('click.expander').bind('click.expander', expand);
  223. if ( o.userCollapse && !$this.find(o.lessSelector).length ) {
  224. $this
  225. .find(detailSelector)
  226. .append('<span class="' + o.lessClass + '">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>');
  227. }
  228. $this
  229. .find(o.lessSelector + ' a')
  230. .unbind('click.expander')
  231. .bind('click.expander', function(event) {
  232. event.preventDefault();
  233. clearTimeout(delayedCollapse);
  234. var $detailsCollapsed = $(this).closest(detailSelector);
  235. reCollapse(o, $detailsCollapsed);
  236. if (defined.onCollapse) {
  237. o.onCollapse.call(thisEl, true);
  238. }
  239. });
  240. }); // this.each
  241. },
  242. destroy: function() {
  243. this.each(function() {
  244. var o, details,
  245. $this = $(this);
  246. if ( !$this.data('expanderInit') ) {
  247. return;
  248. }
  249. o = $.extend({}, $this.data('expander') || {}, opts);
  250. details = $this.find('.' + o.detailClass).contents();
  251. $this.removeData('expanderInit');
  252. $this.removeData('expander');
  253. $this.find(o.moreSelector).remove();
  254. $this.find('.' + o.summaryClass).remove();
  255. $this.find('.' + o.detailClass).after(details).remove();
  256. $this.find(o.lessSelector).remove();
  257. });
  258. }
  259. };
  260. // run the methods (almost always "init")
  261. if ( methods[meth] ) {
  262. methods[ meth ].call(this);
  263. }
  264. // utility functions
  265. function buildHTML(o, blocks) {
  266. var el = 'span',
  267. summary = o.summary;
  268. if ( blocks ) {
  269. el = 'div';
  270. // if summary ends with a close tag, tuck the moreLabel inside it
  271. if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) {
  272. summary = summary.replace(rLastCloseTag, o.moreLabel + '$1');
  273. } else {
  274. // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary
  275. // fixes #19
  276. summary += o.moreLabel;
  277. }
  278. // and wrap it in a div
  279. summary = '<div class="' + o.summaryClass + '">' + summary + '</div>';
  280. } else {
  281. summary += o.moreLabel;
  282. }
  283. return [
  284. summary,
  285. ' <',
  286. el + ' class="' + o.detailClass + '"',
  287. '>',
  288. o.details,
  289. '</' + el + '>'
  290. ].join('');
  291. }
  292. function buildMoreLabel(o) {
  293. var ret = '<span class="' + o.moreClass + '">' + o.expandPrefix;
  294. ret += '<a href="#">' + o.expandText + '</a></span>';
  295. return ret;
  296. }
  297. function backup(txt, preserveWords) {
  298. if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) {
  299. txt = txt.slice( 0, txt.lastIndexOf('<') );
  300. }
  301. if (preserveWords) {
  302. txt = txt.replace(rAmpWordEnd,'');
  303. }
  304. return $.trim(txt);
  305. }
  306. function reCollapse(o, el) {
  307. el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function() {
  308. var prevMore = el.prev('span.' + o.moreClass).show();
  309. if (!prevMore.length) {
  310. el.parent().children('div.' + o.summaryClass).show()
  311. .find('span.' + o.moreClass).show();
  312. }
  313. if (o.afterCollapse) {o.afterCollapse.call(el);}
  314. });
  315. }
  316. function delayCollapse(option, $collapseEl, thisEl) {
  317. if (option.collapseTimer) {
  318. delayedCollapse = setTimeout(function() {
  319. reCollapse(option, $collapseEl);
  320. if ( $.isFunction(option.onCollapse) ) {
  321. option.onCollapse.call(thisEl, false);
  322. }
  323. }, option.collapseTimer);
  324. }
  325. }
  326. return this;
  327. };
  328. // plugin defaults
  329. $.fn.expander.defaults = $.expander.defaults;
  330. })(jQuery);