1(function(module, $, undefined) { 2 3 var BLOCK_TAGS = ['table', 'thead', 'tbody', 'tfoot', 'tr', 'col', 'colgroup', 4 'object', 'embed', 'param', 'ol', 'ul', 'dl', 'blockquote', 'select', 5 'optgroup', 'option', 'textarea', 'script', 'style']; 6 7 /* 8 * Trim function. Trim only end of string whitespaces 9 * 10 * text - String to trim 11 * 12 * Returns text without end whitespaces 13 */ 14 function trimRight(text) { 15 return text.replace(/\s*$/, ""); 16 } 17 18 function setText(element, text) { 19 if (element.innerText) { 20 element.innerText = text; 21 } else if (element.nodeValue) { 22 element.nodeValue = text; 23 } else if (element.textContent) { 24 element.textContent = text; 25 } else { 26 return false; 27 } 28 } 29 30 /* 31 * Truncate the nearest sibling node. If no valid immediate sibling is found, 32 * traverse one level up to a cousin node. 33 * 34 * $element - The jQuery node to truncate. $rootNode - The jQuery root node to 35 * measure the truncated height. $clipNode - The jQuery node to insert right 36 * after the truncation point. options - An object containing: ellipsis - The 37 * ellipsis string to append at the end of the truncation. maxHeight - The 38 * maximum height for the root node (in px). position - The position of the 39 * truncation ("start", "middle", "end"). 40 * 41 * Returns true if truncation happened, false otherwise. 42 */ 43 function truncateNearestSibling($element, $rootNode, $clipNode, options) { 44 var $parent = $element.parent(); 45 var $prevSibling; 46 47 $element.remove(); 48 49 // Take into account length of $clipNode element previous inserted. 50 var clipLength = $clipNode ? $clipNode.length : 0; 51 52 if ($parent.contents().length > clipLength) { 53 54 // Valid previous sibling element (sharing same parent node) exists, 55 // so attempt to truncate it. 56 $prevSibling = $parent.contents().eq(-1 - clipLength); 57 return truncateTextContent($prevSibling, $rootNode, $clipNode, options); 58 59 } else { 60 61 // No previous sibling element (sharing same parent node) exists. 62 // Therefore, search parent's sibling. 63 64 var $parentSibling = $parent.prev(); 65 $prevSibling = $parentSibling.contents().eq(-1); 66 67 if ($prevSibling.length) { 68 69 // Because traversal is in-order so the algorithm already checked that 70 // this point meets the height requirement. As such, it's safe to truncate 71 // here. 72 setText($prevSibling[0], $prevSibling.text() + options.ellipsis); 73 $parent.remove(); 74 75 if ($clipNode.length) { 76 $parentSibling.append($clipNode); 77 } 78 return true; 79 } 80 } 81 82 return false; 83 } 84 85 /* Truncates, at the beginning, the text content of a node using binary search */ 86 function truncateTextStart($element, $rootNode, $clipNode, options) { 87 var element = $element[0]; 88 var original = $element.text(); 89 90 var maxChunk = ''; 91 var mid, chunk; 92 var low = 0; 93 var high = original.length; 94 95 while (low <= high) { 96 mid = low + ((high - low) >> 1); // Integer division 97 98 chunk = options.ellipsis 99 + trimRight(original.substr(mid - 1, original.length)); 100 setText(element, chunk); 101 102 if ($rootNode.height() > options.maxHeight) { 103 // too big, reduce the chunk 104 low = mid + 1; 105 } else { 106 // chunk valid, try to get a bigger chunk 107 high = mid - 1; 108 maxChunk = maxChunk.length > chunk.length ? maxChunk : chunk; 109 } 110 } 111 112 if (maxChunk.length > 0) { 113 setText(element, maxChunk); 114 return true; 115 } else { 116 return truncateNearestSibling($element, $rootNode, $clipNode, options); 117 } 118 } 119 120 /* Truncates, at the end, the text content of a node using binary search */ 121 function truncateTextEnd($element, $rootNode, $clipNode, options) { 122 var element = $element[0]; 123 var original = $element.text(); 124 125 var maxChunk = ''; 126 var mid, chunk; 127 var low = 0; 128 var high = original.length; 129 130 // Binary Search 131 while (low <= high) { 132 mid = low + ((high - low) >> 1); // Integer division 133 134 chunk = trimRight(original.substr(0, mid + 1)) + options.ellipsis; 135 setText(element, chunk); 136 137 if ($rootNode.height() > options.maxHeight) { 138 // too big, reduce the chunk 139 high = mid - 1; 140 } else { 141 // chunk valid, try to get a bigger chunk 142 low = mid + 1; 143 maxChunk = maxChunk.length > chunk.length ? maxChunk : chunk; 144 } 145 } 146 147 if (maxChunk.length > 0) { 148 setText(element, maxChunk); 149 return true; 150 } else { 151 return truncateNearestSibling($element, $rootNode, $clipNode, options); 152 } 153 } 154 155 /* Truncates, at the middle, the text content of a node using binary search */ 156 function truncateTextMiddle($element, $rootNode, $clipNode, options) { 157 var element = $element[0]; 158 var original = $element.text(); 159 160 var maxChunk = ''; 161 var low = 0; 162 var len = original.length; 163 var high = len >> 1; 164 var mid, chunk; 165 166 while (low <= high) { 167 mid = low + ((high - low) >> 1); // Integer division 168 169 chunk = trimRight(original.substr(0, mid)) + options.ellipsis 170 + original.substr(len - mid, len - mid); 171 setText(element, chunk); 172 173 if ($rootNode.height() > options.maxHeight) { 174 // too big, reduce the chunk 175 high = mid - 1; 176 } else { 177 // chunk valid, try to get a bigger chunk 178 low = mid + 1; 179 180 maxChunk = maxChunk.length > chunk.length ? maxChunk : chunk; 181 } 182 } 183 184 if (maxChunk.length > 0) { 185 setText(element, maxChunk); 186 return true; 187 } else { 188 return truncateNearestSibling($element, $rootNode, $clipNode, options); 189 } 190 } 191 192 /* 193 * Truncates the text content of a node using binary search. If no valid 194 * truncation point is found, attempt to truncate its nearest sibling. 195 * 196 * $textNode - The jQuery node to truncate. $rootNode - The jQuery root node to 197 * measure the truncated height. $clipNode - The jQuery node to insert right
198 * after the truncation point. options - An object containing: ellipsis - The 199 * ellipsis string to append at the end of the truncation. maxHeight - The 200 * maximum height for the root node (in px). position - The position of the 201 * truncation ("start", "middle", "end"). 202 * 203 * Returns true if truncation happened, false otherwise. 204 */ 205 function truncateTextContent($element, $rootNode, $clipNode, options) { // jshint 206 // ignore:line 207 if (options.position === "end") { 208 return truncateTextEnd($element, $rootNode, $clipNode, options); 209 } else if (options.position === "start") { 210 return truncateTextStart($element, $rootNode, $clipNode, options); 211 } else { 212 return truncateTextMiddle($element, $rootNode, $clipNode, options); 213 } 214 } 215 216 /* 217 * Recursively truncates a nested node. Traverses the children node tree in 218 * reverse order. 219 */ 220 function truncateNestedNodeStart($element, $rootNode, $clipNode, options) { 221 var element = $element[0]; 222 223 var $children = $element.contents(); 224 var $child, child; 225 226 var length = $children.length; 227 var index = length - 1; 228 var truncated = false; 229 230 $element.empty(); 231 232 for (; index >= 0 && !truncated; index--) { 233 234 $child = $children.eq(index); 235 child = $child[0]; 236 237 if (child.nodeType === 8) { // comment node 238 continue; 239 } 240 241 element.insertBefore(child, element.firstChild); 242 243 if ($clipNode.length) { 244 if ($.inArray(element.tagName.toLowerCase(), BLOCK_TAGS) >= 0) { 245 // Certain elements like <li> should not be appended to. 246 $element.after($clipNode); 247 } else { 248 $element.append($clipNode); 249 } 250 } 251 252 if ($rootNode.height() > options.maxHeight) { 253 if (child.nodeType === 3) { // text node 254 truncated = truncateTextContent($child, $rootNode, $clipNode, options); 255 } else { 256 truncated = truncateNestedNode($child, $rootNode, $clipNode, options); 257 } 258 } 259 260 if (!truncated && $clipNode.length) { 261 $clipNode.remove(); 262 } 263 264 } 265 266 return truncated; 267 } 268 269 /* 270 * Recursively truncates a nested node. Traverses the children node tree 271 * in-order. 272 */ 273 function truncateNestedNodeEnd($element, $rootNode, $clipNode, options) { 274 var element = $element[0]; 275 276 var $children = $element.contents(); 277 var $child, child; 278 279 var index = 0; 280 var length = $children.length; 281 var truncated = false; 282 283 $element.empty(); 284 285 for (; index < length && !truncated; index++) { 286 287 $child = $children.eq(index); 288 child = $child[0]; 289 290 if (child.nodeType === 8) { // comment node 291 continue; 292 } 293 294 element.appendChild(child); 295 296 if ($clipNode.length) { 297 if ($.inArray(element.tagName.toLowerCase(), BLOCK_TAGS) >= 0) { 298 // Certain elements like <li> should not be appended to. 299 $element.after($clipNode); 300 } else { 301 $element.append($clipNode); 302 } 303 } 304 305 if ($rootNode.height() > options.maxHeight) { 306 if (child.nodeType === 3) { // text node 307 truncated = truncateTextContent($child, $rootNode, $clipNode, options); 308 } else { 309 truncated = truncateNestedNode($child, $rootNode, $clipNode, options); 310 } 311 } 312 313 if (!truncated && $clipNode.length) { 314 $clipNode.remove(); 315 } 316 317 } 318 319 return truncated; 320 } 321 322 /* 323 * Recursively truncates a nested node. 324 * 325 * $element - The jQuery nested node to truncate. $rootNode - The jQuery root 326 * node to measure the truncated height. $clipNode - The jQuery node to insert 327 * right after the truncation point. options - An object containing: ellipsis - 328 * The ellipsis string to append at the end of the truncation. maxHeight - The 329 * maximum height for the root node (in px). position - The position of the 330 * truncation ("start", "middle", "end"). 331 * 332 * Returns true if truncation happened, false otherwise. 333 */ 334 function truncateNestedNode($element, $rootNode, $clipNode, options) { // jshint 335 // ignore:line 336 if (options.position === "end") { 337 return truncateNestedNodeEnd($element, $rootNode, $clipNode, options); 338 } else if (options.position === "start") { 339 return truncateNestedNodeStart($element, $rootNode, $clipNode, options); 340 } else { 341 // TODO: Truncate middle for nested = HARDCORE 342 return truncateNestedNodeEnd($element, $rootNode, $clipNode, options); 343 } 344 } 345 346 /* 347 * Public: Creates an instance of Truncate. 348 * 349 * element - A DOM element to be truncated. options - An Object literal 350 * containing setup options. 351 * 352 * Examples: 353 * 354 * var element = document.createElement('span'); element.innerHTML = 'This is<br>odd.'; 355 * var truncated = new Truncate(element, { lines: 1, lineHeight: 16, ellipsis: '⦠', 356 * showMore: '<a class="show-more">Show More</a>', showLess: '<a 357 * class="show-less">Show Less</a>', position: "start" }); // Update HTML 358 * truncated.update('This is not very odd.'); // Undo truncation
359 * truncated.expand(); // Redo truncation truncated.collapse(); 360 */ 361 function Truncate(element, options) { 362 this.element = element; 363 this.$element = $(element); 364 365 this._name = 'truncate'; 366 this._defaults = { 367 lines: 1, 368 ellipsis: 'â¦', 369 showMore: '', 370 showLess: '', 371 position: 'end', 372 lineHeight: 'auto' 373 }; 374 375 this.options = $.extend({}, this._defaults, options); 376 377 if (this.options.lineHeight === 'auto') { 378 var lineHeightCss = this.$element.css('line-height'), lineHeight = 18; // for 379 // Normal 380 // we 381 // return 382 // the 383 // default 384 // value: 385 // 18px 386 387 if (lineHeightCss !== "normal") { 388 lineHeight = parseInt(lineHeightCss, 10); 389 } 390 391 this.options.lineHeight = lineHeight; 392 } 393 394 if (this.options.maxHeight === undefined) { 395 this.options.maxHeight = parseInt(this.options.lines, 10) 396 * parseInt(this.options.lineHeight, 10); 397 } 398 399 if (this.options.position !== 'start' && this.options.position !== 'middle' 400 && this.options.position !== 'end') { 401 this.options.position = 'end'; 402 } 403 404 this.$clipNode = $($.parseHTML(this.options.showMore), this.$element); 405 406 this.original = this.cached = element.innerHTML; 407 408 this.isTruncated = false; // True if the original content overflows the 409 // container. 410 this.isCollapsed = true; // True if the container is currently collapsed. 411 412 this.update(); 413 } 414 415 Truncate.prototype = { 416 417 /* 418 * Public: Updates the inner HTML of the element and re-truncates. Will not 419 * perform an updade if the container is currently expanded, instead it will 420 * wait until the next time .collapse() is called. 421 * 422 * html - The new HTML. 423 * 424 * Returns nothing. 425 */ 426 update: function(html) { 427 var wasExpanded = !this.isCollapsed; 428 429 // Update HTML if provided, otherwise use the current html and restore 430 // the truncated content to the original if it's currently present. 431 if (typeof html !== 'undefined') { 432 this.original = this.element.innerHTML = html; 433 } else if (this.isCollapsed && this.element.innerHTML === this.cached) { 434 this.element.innerHTML = this.original; 435 } 436 437 // Wrap the contents in order to ignore container's margin/padding. 438 var $wrap = this.$element.wrapInner('<div/>').children(); 439 $wrap.css({ 440 border: 'none', 441 margin: 0, 442 padding: 0, 443 width: 'auto', 444 height: 'auto', 445 'word-wrap': 'break-word' 446 }); 447 448 this.isTruncated = false; 449 // Check if already meets height requirement 450 if ($wrap.height() > this.options.maxHeight) { 451 this.isTruncated = truncateNestedNode($wrap, $wrap, this.$clipNode, 452 this.options); 453 454 if (this.isExplicitlyCollapsed) { 455 this.isCollapsed = true; 456 wasExpanded = false; 457 } 458 } else { 459 this.isCollapsed = false; 460 } 461 462 // Restore the wrapped contents 463 $wrap.replaceWith($wrap.contents()); 464 465 // Cache the truncated content 466 this.cached = this.element.innerHTML; 467 468 // If the container was expanded when .update() was called then restore 469 // it to it's previous state. 470 if (wasExpanded) { 471 this.element.innerHTML = this.original; 472 } 473 }, 474 475 /* 476 * Public: Expands the element to show content in full. 477 * 478 * Returns nothing. 479 */ 480 expand: function() { 481 var includeShowLess = true; 482 483 if (this.isExplicitlyCollapsed) { 484 this.isExplicitlyCollapsed = false; 485 includeShowLess = false; 486 } 487 488 if (!this.isCollapsed) { 489 return; 490 } 491 492 this.isCollapsed = false; 493 494 this.element.innerHTML = this.isTruncated ? this.original 495 + (includeShowLess ? this.options.showLess : "") : this.original; 496 }, 497 498 /* 499 * Public: Collapses the element to the truncated state. Uses the cached HTML 500 * from .update() by default. 501 * 502 * retruncate - True to retruncate original HTML, otherwise use cached HTML. 503 * 504 * Returns nothing. 505 */ 506 collapse: function(retruncate) { 507 this.isExplicitlyCollapsed = true; 508 509 if (this.isCollapsed) { 510 return; 511 } 512 513 this.isCollapsed = true; 514 515 retruncate = retruncate || false; 516 if (retruncate) { 517 this.update(); 518 } else { 519 this.element.innerHTML = this.cached; 520 } 521 } 522 }; 523 524 // Lightweight plugin wrapper preventing multiple instantiations 525 $.fn.truncate = function(options) { 526 var args = $.makeArray(arguments).slice(1); 527 return this.each(function() { 528 var truncate = $.data(this, 'jquery-truncate'); 529 if (!truncate) { 530 $.data(this, 'jquery-truncate', new Truncate(this, options)); 531 } else if (typeof truncate[options] === 'function') {
532 truncate[options].apply(truncate, args); 533 } 534 }); 535 }; 536 537 module.Truncate = Truncate; 538 539})(this, 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.