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