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.