1(function($) { 2 3 /** 4 * Hash containing mapping of selectors to settings hashes for target selectors that should be live updated. 5 * 6 * @type {Object.<string, Object>} 7 * @private 8 */ 9 var liveUpdatingTargetSelectors = {}; 10 11 /** 12 * Interval ID for live updater. Contains interval ID when the live updater interval is active, or is undefined 13 * otherwise. 14 * 15 * @type {number} 16 * @private 17 */ 18 var liveUpdaterIntervalId; 19 20 /** 21 * Boolean indicating whether the live updater is running. 22 * 23 * @type {boolean} 24 * @private 25 */ 26 var liveUpdaterRunning = false; 27 28 /** 29 * Set of default settings. 30 * 31 * @type {Object.<string, string>} 32 * @private 33 */ 34 var defaultSettings = { 35 ellipsis: '...', 36 setTitle: 'never', 37 live: false 38 }; 39 40 /** 41 * Perform ellipsis on selected elements. 42 * 43 * @param {string} selector the inner selector of elements that ellipsis may work on. Inner elements not referred to by this 44 * selector are left untouched. 45 * @param {Object.<string, string>=} options optional options to override default settings. 46 * @return {jQuery} the current jQuery object for chaining purposes. 47 * @this {jQuery} the current jQuery object. 48 */ 49 $.fn.ellipsis = function(selector, options) { 50 var subjectElements, settings; 51 52 subjectElements = $(this); 53 54 // Check for options argument only. 55 if (typeof selector !== 'string') { 56 options = selector; 57 selector = undefined; 58 } 59 60 // Create the settings from the given options and the default settings. 61 settings = $.extend({}, defaultSettings, options); 62 63 // If selector is not set, work on immediate children (default behaviour). 64 settings.selector = selector; 65 66 // Do ellipsis on each subject element. 67 subjectElements.each(function() { 68 var elem = $(this); 69 70 // Do ellipsis on subject element. 71 ellipsisOnElement(elem, settings); 72 }); 73 74 // If live option is enabled, add subject elements to live updater. Otherwise remove from live updater. 75 if (settings.live) { 76 addToLiveUpdater(subjectElements.selector, settings); 77 78 } else { 79 removeFromLiveUpdater(subjectElements.selector); 80 } 81 82 // Return jQuery object for chaining. 83 return this; 84 }; 85 86 87 /** 88 * Perform ellipsis on the given container. 89 * 90 * @param {jQuery} containerElement jQuery object containing one DOM element to perform ellipsis on. 91 * @param {Object.<string, string>} settings the settings for this ellipsis operation. 92 * @private 93 */ 94 function ellipsisOnElement(containerElement, settings) { 95 var containerData = containerElement.data('jqae'); 96 if (!containerData) containerData = {}; 97 98 // Check if wrapper div was already created and bound to the container element. 99 var wrapperElement = containerData.wrapperElement; 100 101 // If not, create wrapper element. 102 if (!wrapperElement) { 103 wrapperElement = containerElement.wrapInner('<div/>').find('>div'); 104 105 // Wrapper div should not add extra size. 106 wrapperElement.css({ 107 margin: 0, 108 padding: 0, 109 border: 0 110 }); 111 } 112 113 // Check if the original wrapper element content was already bound to the wrapper element. 114 var wrapperElementData = wrapperElement.data('jqae'); 115 if (!wrapperElementData) wrapperElementData = {}; 116 117 var wrapperOriginalContent = wrapperElementData.originalContent; 118 119 // If so, clone the original content, re-bind the original wrapper content to the clone, and replace the 120 // wrapper with the clone. 121 if (wrapperOriginalContent) { 122 wrapperElement = wrapperElementData.originalContent.clone(true) 123 .data('jqae', {originalContent: wrapperOriginalContent}).replaceAll(wrapperElement); 124 125 } else { 126 // Otherwise, clone the current wrapper element and bind it as original content to the wrapper element. 127 128 wrapperElement.data('jqae', {originalContent: wrapperElement.clone(true)}); 129 } 130 131 // Bind the wrapper element and current container width and height to the container element. Current container 132 // width and height are stored to detect changes to the container size. 133 containerElement.data('jqae', { 134 wrapperElement: wrapperElement, 135 containerWidth: containerElement.width(), 136 containerHeight: containerElement.height() 137 }); 138 139 // Calculate with current container element height. 140 var containerElementHeight = containerElement.height(); 141 142 // Calculate wrapper offset. 143 var wrapperOffset = (parseInt(containerElement.css('padding-top'), 10) || 0) + (parseInt(containerElement.css('border-top-width'), 10) || 0) - (wrapperElement.offset().top - containerElement.offset().top); 144 145 // Normally the ellipsis characters are applied to the last non-empty text-node in the selected element. If the 146 // selected element becomes empty during ellipsis iteration, the ellipsis characters cannot be applied to that 147 // selected element, and must be deferred to the previous selected element. This parameter keeps track of that. 148 var deferAppendEllipsis = false;
149 150 // Loop through all selected elements in reverse order. 151 var selectedElements = wrapperElement; 152 if (settings.selector) selectedElements = $(wrapperElement.find(settings.selector).get().reverse()); 153 154 selectedElements.each(function() { 155 var selectedElement = $(this), 156 originalText = selectedElement.text(), 157 ellipsisApplied = false; 158 159 // Check if we can safely remove the selected element. This saves a lot of unnecessary iterations. 160 if (wrapperElement.innerHeight() - selectedElement.innerHeight() > containerElementHeight + wrapperOffset) { 161 selectedElement.remove(); 162 163 } else { 164 // Reverse recursively remove empty elements, until the element that contains a non-empty text-node. 165 removeLastEmptyElements(selectedElement); 166 167 // If the selected element has not become empty, start ellipsis iterations on the selected element. 168 if (selectedElement.contents().length) { 169 170 // If a deffered ellipsis is still pending, apply it now to the last text-node. 171 if (deferAppendEllipsis) { 172 getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis; 173 deferAppendEllipsis = false; 174 } 175 176 // Iterate until wrapper element height is less than or equal to the original container element 177 // height plus possible wrapperOffset. 178 while (wrapperElement.innerHeight() > containerElementHeight + wrapperOffset) { 179 // Apply ellipsis on last text node, by removing one word. 180 ellipsisApplied = ellipsisOnLastTextNode(selectedElement); 181 182 // If ellipsis was succesfully applied, remove any remaining empty last elements and append the 183 // ellipsis characters. 184 if (ellipsisApplied) { 185 removeLastEmptyElements(selectedElement); 186 187 // If the selected element is not empty, append the ellipsis characters. 188 if (selectedElement.contents().length) { 189 getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis; 190 191 } else { 192 // If the selected element has become empty, defer the appending of the ellipsis characters 193 // to the previous selected element. 194 deferAppendEllipsis = true; 195 selectedElement.remove(); 196 break; 197 } 198 199 } else { 200 // If ellipsis could not be applied, defer the appending of the ellipsis characters to the 201 // previous selected element. 202 deferAppendEllipsis = true; 203 selectedElement.remove(); 204 break; 205 } 206 } 207 208 // If the "setTitle" property is set to "onEllipsis" and the ellipsis has been applied, or if the 209 // property is set to "always", the add the "title" attribute with the original text. Else remove the 210 // "title" attribute. When the "setTitle" property is set to "never" we do not touch the "title" 211 // attribute. 212 if (((settings.setTitle == 'onEllipsis') && ellipsisApplied) || (settings.setTitle == 'always')) { 213 selectedElement.attr('title', originalText); 214 215 } else if (settings.setTitle != 'never') { 216 selectedElement.removeAttr('title'); 217 } 218 } 219 } 220 }); 221 } 222 223 /** 224 * Performs ellipsis on the last text node of the given element. Ellipsis is done by removing a full word. 225 * 226 * @param {jQuery}
226 element jQuery object containing a single DOM element. 227 * @return {boolean} true when ellipsis has been done, false otherwise. 228 * @private 229 */ 230 function ellipsisOnLastTextNode(element) { 231 var lastTextNode = getLastTextNode(element); 232 233 // If the last text node is found, do ellipsis on that node. 234 if (lastTextNode.length) { 235 var text = lastTextNode.get(0).nodeValue; 236 237 // Find last space character, and remove text from there. If no space is found the full remaining text is 238 // removed. 239 var pos = text.lastIndexOf(' '); 240 if (pos > -1) { 241 text = $.trim(text.substring(0, pos)); 242 lastTextNode.get(0).nodeValue = text; 243 244 } else { 245 lastTextNode.get(0).nodeValue = ''; 246 } 247 248 return true; 249 } 250 251 return false; 252 } 253 254 /** 255 * Get last text node of the given element. 256 * 257 * @param {jQuery} element jQuery object containing a single element. 258 * @return {jQuery} jQuery object containing a single text node. 259 * @private 260 */ 261 function getLastTextNode(element) { 262 if (element.contents().length) { 263 264 // Get last child node. 265 var contents = element.contents(); 266 var lastNode = contents.eq(contents.length - 1); 267 268 // If last node is a text node, return it. 269 if (lastNode.filter(textNodeFilter).length) { 270 return lastNode; 271 272 } else { 273 // Else it is an element node, and we recurse into it. 274 275 return getLastTextNode(lastNode); 276 } 277 278 } else { 279 // If there is no last child node, we append an empty text node and return that. Normally this should not 280 // happen, as we test for emptiness before calling getLastTextNode. 281 282 element.append(''); 283 var contents = element.contents(); 284 return contents.eq(contents.length - 1); 285 } 286 } 287 288 /** 289 * Remove last empty elements. This is done recursively until the last element contains a non-empty text node. 290 * 291 * @param {jQuery} element jQuery object containing a single element. 292 * @return {boolean} true when elements have been removed, false otherwise. 293 * @private 294 */ 295 function removeLastEmptyElements(element) { 296 if (element.contents().length) { 297 298 // Get last child node. 299 var contents = element.contents(); 300 var lastNode = contents.eq(contents.length - 1); 301 302 // If last child node is a text node, check for emptiness. 303 if (lastNode.filter(textNodeFilter).length) { 304 var text = lastNode.get(0).nodeValue; 305 text = $.trim(text); 306 307 if (text == '') { 308 // If empty, remove the text node. 309 lastNode.remove(); 310 311 return true; 312 313 } else { 314 return false; 315 } 316 317 } else { 318 // If the last child node is an element node, remove the last empty child nodes on that node. 319 while (removeLastEmptyElements(lastNode)) { 320 } 321
322 // If the last child node contains no more child nodes, remove the last child node. 323 if (lastNode.contents().length) { 324 return false; 325 326 } else { 327 lastNode.remove(); 328 329 return true; 330 } 331 } 332 } 333 334 return false; 335 } 336 337 /** 338 * Filter for testing on text nodes. 339 * 340 * @return {boolean} true when this node is a text node, false otherwise. 341 * @this {Node} 342 * @private 343 */ 344 function textNodeFilter() { 345 return this.nodeType === 3; 346 } 347 348 /** 349 * Add target selector to hash of target selectors. If this is the first target selector added, start the live 350 * updater. 351 * 352 * @param {string} targetSelector the target selector to run the live updater for. 353 * @param {Object.<string, string>} settings the settings to apply on this target selector. 354 * @private 355 */ 356 function addToLiveUpdater(targetSelector, settings) { 357 // Store target selector with its settings. 358 liveUpdatingTargetSelectors[targetSelector] = settings; 359 360 // If the live updater has not yet been started, start it now. 361 if (!liveUpdaterIntervalId) { 362 liveUpdaterIntervalId = window.setInterval(function() { 363 doLiveUpdater(); 364 }, 200); 365 } 366 } 367 368 /** 369 * Remove the target selector from the hash of target selectors. It this is the last remaining target selector 370 * being removed, stop the live updater. 371 * 372 * @param {string} targetSelector the target selector to stop running the live updater for. 373 * @private 374 */ 375 function removeFromLiveUpdater(targetSelector) { 376 // If the hash contains the target selector, remove it. 377 if (liveUpdatingTargetSelectors[targetSelector]) { 378 delete liveUpdatingTargetSelectors[targetSelector]; 379 380 // If no more target selectors are in the hash, stop the live updater. 381 if (!liveUpdatingTargetSelectors.length) { 382 if (liveUpdaterIntervalId) { 383 window.clearInterval(liveUpdaterIntervalId); 384 liveUpdaterIntervalId = undefined; 385 } 386 } 387 } 388 }; 389 390 /** 391 * Run the live updater. The live updater is periodically run to check if its monitored target selectors require 392 * re-applying of the ellipsis. 393 * 394 * @private 395 */ 396 function doLiveUpdater() { 397 // If the live updater is already running, skip this time. We only want one instance running at a time. 398 if (!liveUpdaterRunning) { 399 liveUpdaterRunning = true; 400 401 // Loop through target selectors. 402 for (var targetSelector in liveUpdatingTargetSelectors) { 403 $(targetSelector).each(function() { 404 var containerElement, containerData; 405 406 containerElement = $(this); 407 containerData = containerElement.data('jqae'); 408 409 // If container element dimensions have changed, or the container element is new, run ellipsis on 410 // that container element. 411 if ((containerData.containerWidth != containerElement.width()) || 412 (containerData.containerHeight != containerElement.height())) { 413 ellipsisOnElement(containerElement, liveUpdatingTargetSelectors[targetSelector]); 414 } 415 }); 416 } 417 418 liveUpdaterRunning = false; 419 } 420 }; 421 422})(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.