PageSourceSearch

https://www.governancenow.com/gn-images/jquery.expander.js

js governancenow.com collected 2026-09-24 20:07:07 UTC 15,063 bytes, 411 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.