├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── composer.json ├── dist ├── dotdotdot.d.ts ├── dotdotdot.es6.js ├── dotdotdot.esm.js ├── dotdotdot.js └── dotdotdot.umd.js ├── gulpfile.js ├── index.html ├── package.json ├── src └── dotdotdot.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Mac system files. 2 | ._* 3 | 4 | # Ignore sass-cache files. 5 | *.sass-cache* 6 | *.scssc 7 | 8 | # Ignore Gulp modules 9 | node_modules -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | 7 | ## Using the issue tracker 8 | 9 | The issue tracker is the preferred channel for [bug reports](#bugs) and 10 | [features requests](#features), but please respect the following restrictions: 11 | 12 | * Please **do not** use the issue tracker for personal support requests. 13 | 14 | * Please keep the discussion **on topic** and respect the opinions of others. 15 | 16 | 17 | 18 | ## Bug reports 19 | 20 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 21 | Good bug reports are extremely helpful - thank you! 22 | 23 | Guidelines for bug reports: 24 | 25 | 1. **Use the GitHub issue search** — check if the issue has already been 26 | reported. 27 | 28 | 2. **Check if the issue has been fixed** — try to reproduce it using the 29 | latest branch in the repository. 30 | 31 | 3. **Isolate the problem** — create a [reduced test 32 | case](http://css-tricks.com/reduced-test-cases/) and a live example. 33 | 34 | A good bug report shouldn't leave others needing to chase you up for more 35 | information. Please try to be as detailed as possible in your report. What is 36 | your environment? What steps will reproduce the issue? What browser(s) and OS 37 | experience the problem? What would you expect to be the outcome? All these 38 | details will help people to fix any potential bugs. 39 | 40 | 41 | 42 | ## Feature requests 43 | 44 | Feature requests are welcome. But take a moment to find out whether your idea 45 | fits with the scope and aims of the project. It's up to *you* to make a strong 46 | case to convince the project's developers of the merits of this feature. Please 47 | provide as much detail and context as possible. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. 2 | To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dotdotdot 2 | ================ 3 | 4 | Dotdotdot is a javascript plugin for truncating multiple line content on a webpage. 5 | It uses an ellipsis to indicate that there is more text than currently visible. 6 | Optionally, the plugin can keep a "read more" anchor visible at the end of the content, after the ellipsis. 7 | 8 | [Demo](http://dotdotdot.frebsite.nl) 9 | 10 | When using dotdotdot to truncate HTML, you don't need to worry about your HTML markup, the plugin knows its way around most elements. 11 | It's responsive, so when resizing the browser, the ellipsis will update on the fly. 12 | 13 | Need help? Have a look at [the documentation](http://dotdotdot.frebsite.nl). 14 | 15 | 16 | 17 | ### Licence 18 | The dotdotdot javascript plugin is licensed under the [CC-BY-NC-4.0 license](http://creativecommons.org/licenses/by-nc/4.0/).
19 | You can [purchase a license](http://dotdotdot.frebsite.nl#download) if you want to use it in a commercial project. 20 | 21 | ### Browser support 22 | The dotdotdot javascript plugin targets modern browsers that support ES5, meaning Internet Explorer 10 and earlier are not supported. 23 | For Internet Explorer 11, you''ll need [some polyfills](https://polyfill.io/v3/polyfill.min.js?features=default%2CElement.prototype.append%2CElement.prototype.prepend%2CElement.prototype.remove%2CElement.prototype.replaceWith%2CElement.prototype.classList%2CElement.prototype.cloneNode%2CElement.prototype.matches). 24 | If you need support for Internet Explorer 9 or 10, use the legacy (jQuery) version: [version 3.2.3](https://github.com/FrDH/dotdotdot-JS/releases/tag/v3.2.3). 25 | 26 | ### Development 27 | This project uses [Gulp(4)](http://gulpjs.com/) to minify the JS file. 28 | If you are unfamiliar with Gulp, check [this tutorial](https://travismaynard.com/writing/getting-started-with-gulp) on how to get started.
29 | Run `gulp watch` in the command-line to put a watch on the files and run all scripts immediately after saving your changes. 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotdotdot-js", 3 | "version": "4.1.0", 4 | "authors": "Fred Heusschen ", 5 | "license": "CC-BY-NC-4.0", 6 | "description": "Dotdotdot is a javascript plugin for truncating multiple line content with an ellipsis.", 7 | "keywords": [ 8 | "truncate", 9 | "truncating", 10 | "ellipsis", 11 | "dotdotdot", 12 | "multiline", 13 | "text", 14 | "text-overflow", 15 | "overflow", 16 | "dots" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /dist/dotdotdot.d.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * dotdotdot JS 4.1.0 3 | * 4 | * dotdotdot.frebsite.nl 5 | * 6 | * Copyright (c) Fred Heusschen 7 | * www.frebsite.nl 8 | * 9 | * License: CC-BY-NC-4.0 10 | * http://creativecommons.org/licenses/by-nc/4.0/ 11 | */ 12 | /** An object with function values. */ 13 | interface dddFunctionObject { 14 | [key: string]: Function; 15 | } 16 | /** Default options for the class. */ 17 | interface dddOptions { 18 | /** The ellipsis to place after the truncated text. */ 19 | ellipsis?: string; 20 | /** Function to invoke after the truncate process. */ 21 | callback?: Function; 22 | /** How to truncate: 'node', 'word' (default) or 'letter'. */ 23 | truncate?: string; 24 | /** Optional tolerance for the container height. */ 25 | tolerance?: number; 26 | /** Selector for elements not to remove from the DOM. */ 27 | keep?: string; 28 | /** Whether and when to update the ellipsis: null, true or 'window' (default) */ 29 | watch?: string; 30 | /** The height for the container. If null, the max-height will be read from the CSS properties. */ 31 | height?: number; 32 | } 33 | /** 34 | * Class for a multiline ellipsis. 35 | */ 36 | export default class Dotdotdot { 37 | /** Plugin version. */ 38 | static version: string; 39 | /** Default options. */ 40 | static options: dddOptions; 41 | /** Element to truncate */ 42 | container: HTMLElement; 43 | /** Inner element, added for measuring. */ 44 | innerContainer: HTMLElement; 45 | /** Options. */ 46 | options: dddOptions; 47 | /** The max-height for the element. */ 48 | maxHeight: number; 49 | /** The ellipsis to use for truncating. */ 50 | ellipsis: Text; 51 | /** The API */ 52 | API: dddFunctionObject; 53 | /** Storage for the watch timeout, oddly it has a number type. */ 54 | watchTimeout: number; 55 | /** Storage for the watch interval, oddly it has a number type. */ 56 | watchInterval: number; 57 | /** Storage for the original style attribute. */ 58 | originalStyle: string; 59 | /** Storage for the original HTML. */ 60 | originalContent: Node[]; 61 | /** Function to invoke on window resize. Needs to be stored so it can be removed later on. */ 62 | resizeEvent: EventListener; 63 | /** 64 | * Truncate a multiline element with an ellipsis. 65 | * 66 | * @param {HTMLElement} container The element to truncate. 67 | * @param {object} [options=Dotdotdot.options] Options for the menu. 68 | */ 69 | constructor(container: HTMLElement, options?: dddOptions); 70 | /** 71 | * Restore the container to a pre-init state. 72 | */ 73 | restore(): void; 74 | /** 75 | * Fully destroy the plugin. 76 | */ 77 | destroy(): void; 78 | /** 79 | * Start a watch for the truncate process. 80 | */ 81 | watch(): void; 82 | /** 83 | * Stop the watch. 84 | */ 85 | unwatch(): void; 86 | /** 87 | * Start the truncate process. 88 | */ 89 | truncate(): boolean; 90 | /** 91 | * Truncate an element by removing elements from the end. 92 | * 93 | * @param {HTMLElement} element The element to truncate. 94 | */ 95 | _truncateToNode(element: HTMLElement): void; 96 | /** 97 | * Truncate a sentence by removing words from the end. 98 | * 99 | * @param {HTMLElement} element The element to truncate. 100 | */ 101 | _truncateToWord(element: HTMLElement): void; 102 | /** 103 | * Truncate a word by removing letters from the end. 104 | * 105 | * @param {HTMLElement} element The element to truncate. 106 | */ 107 | _truncateToLetter(element: HTMLElement): void; 108 | /** 109 | * Test if the content fits in the container. 110 | * 111 | * @return {boolean} Whether or not the content fits in the container. 112 | */ 113 | _fits(): boolean; 114 | /** 115 | * Add the ellipsis to a text. 116 | * 117 | * @param {string} text The text to add the ellipsis to. 118 | * @return {string} The text with the added ellipsis. 119 | */ 120 | _addEllipsis(text: string): string; 121 | /** 122 | * Sanitize and collect the original contents. 123 | * 124 | * @return {array} The sanitizes HTML elements. 125 | */ 126 | _getOriginalContent(): HTMLElement[]; 127 | /** 128 | * Find the max-height for the container. 129 | * 130 | * @return {number} The max-height for the container. 131 | */ 132 | _getMaxHeight(): number; 133 | /** DOM traversing functions to uniform datatypes. */ 134 | static $: { 135 | /** 136 | * Find elements by a query selector in an element. 137 | * 138 | * @param {string} selector The selector to search for. 139 | * @param {HTMLElement} [element=document] The element to search in. 140 | * @return {array} The found elements. 141 | */ 142 | find: (selector: string, element?: HTMLElement | Document) => HTMLElement[]; 143 | /** 144 | * Collect child nodes (HTML elements and TextNodes) in an element. 145 | * 146 | * @param {HTMLElement} [element=document] The element to search in. 147 | * @return {array} The found nodes. 148 | */ 149 | contents: (element?: HTMLElement | Document) => Node[]; 150 | }; 151 | } 152 | export {}; 153 | -------------------------------------------------------------------------------- /dist/dotdotdot.es6.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * dotdotdot JS 4.1.0 3 | * 4 | * dotdotdot.frebsite.nl 5 | * 6 | * Copyright (c) Fred Heusschen 7 | * www.frebsite.nl 8 | * 9 | * License: CC-BY-NC-4.0 10 | * http://creativecommons.org/licenses/by-nc/4.0/ 11 | */ 12 | export default class Dotdotdot{constructor(t,e=Dotdotdot.options){this.container=t,this.options=e||{},this.watchTimeout=null,this.watchInterval=null,this.resizeEvent=null;for(let t in Dotdotdot.options)Dotdotdot.options.hasOwnProperty(t)&&void 0===this.options[t]&&(this.options[t]=Dotdotdot.options[t]);var i=this.container.dotdotdot;i&&i.destroy(),this.API={},["truncate","restore","destroy","watch","unwatch"].forEach(t=>{this.API[t]=(()=>this[t].call(this))}),this.container.dotdotdot=this.API,this.originalStyle=this.container.getAttribute("style")||"",this.originalContent=this._getOriginalContent(),this.ellipsis=document.createTextNode(this.options.ellipsis);var o=window.getComputedStyle(this.container);"break-word"!==o["word-wrap"]&&(this.container.style["word-wrap"]="break-word"),"pre"===o["white-space"]?this.container.style["white-space"]="pre-wrap":"nowrap"===o["white-space"]&&(this.container.style["white-space"]="normal"),null===this.options.height&&(this.options.height=this._getMaxHeight()),this.truncate(),this.options.watch&&this.watch()}restore(){this.unwatch(),this.container.setAttribute("style",this.originalStyle),this.container.classList.remove("ddd-truncated"),this.container.innerHTML="",this.originalContent.forEach(t=>{this.container.append(t)})}destroy(){this.restore(),this.container.dotdotdot=null}watch(){this.unwatch();var t={width:null,height:null},e=(e,i,o)=>{if(this.container.offsetWidth||this.container.offsetHeight||this.container.getClientRects().length){let n={width:e[i],height:e[o]};return t.width==n.width&&t.height==n.height||this.truncate(),n}return t};"window"===this.options.watch?(this.resizeEvent=(i=>{this.watchTimeout&&clearTimeout(this.watchTimeout),this.watchTimeout=setTimeout(()=>{t=e(window,"innerWidth","innerHeight")},100)}),window.addEventListener("resize",this.resizeEvent)):this.watchInterval=setInterval(()=>{t=e(this.container,"clientWidth","clientHeight")},1e3)}unwatch(){this.resizeEvent&&(window.removeEventListener("resize",this.resizeEvent),this.resizeEvent=null),this.watchInterval&&clearInterval(this.watchInterval),this.watchTimeout&&clearTimeout(this.watchTimeout)}truncate(){var t=!1;return this.container.innerHTML="",this.originalContent.forEach(t=>{this.container.append(t.cloneNode(!0))}),this.maxHeight=this._getMaxHeight(),this._fits()||(t=!0,this._truncateToNode(this.container)),this.container.classList[t?"add":"remove"]("ddd-truncated"),this.options.callback.call(this.container,t),t}_truncateToNode(t){var e=[],i=[];if(Dotdotdot.$.contents(t).forEach(t=>{if(1!=t.nodeType||!t.matches(".ddd-keep")){let o=document.createComment("");t.replaceWith(o),i.push(t),e.push(o)}}),i.length){for(var o=0;o1)return void i[o-2].remove();break}}for(var n=o;n=0;n--)if(t.textContent=this._addEllipsis(o.slice(0,n).join(i)),this._fits()){"letter"==this.options.truncate&&(t.textContent=o.slice(0,n+1).join(i),this._truncateToLetter(t));break}}_truncateToLetter(t){for(var e=t.textContent.split(""),i="",o=e.length;o>=0&&(!(i=e.slice(0,o).join("")).length||(t.textContent=this._addEllipsis(i),!this._fits()));o--);}_fits(){return this.container.scrollHeight<=this.maxHeight+this.options.tolerance}_addEllipsis(t){for(var e=[" "," ",",",";",".","!","?"];e.indexOf(t.slice(-1))>-1;)t=t.slice(0,-1);return t+=this.ellipsis.textContent}_getOriginalContent(){let t="script, style";this.options.keep&&(t+=", "+this.options.keep),Dotdotdot.$.find(t,this.container).forEach(t=>{t.classList.add("ddd-keep")});let e="div, section, article, header, footer, p, h1, h2, h3, h4, h5, h6, table, td, td, dt, dd, li";[this.container,...Dotdotdot.$.find("*",this.container)].forEach(t=>{t.normalize(),Dotdotdot.$.contents(t).forEach(e=>{8==e.nodeType&&t.removeChild(e)}),Dotdotdot.$.contents(t).forEach(i=>{if(3==i.nodeType&&""==i.textContent.trim()){let o=i.previousSibling,n=i.nextSibling;(i.parentElement.matches("table, thead, tbody, tfoot, tr, dl, ul, ol, video")||!o||1==o.nodeType&&o.matches(e)||!n||1==n.nodeType&&n.matches(e))&&t.removeChild(i)}})});let i=[];return Dotdotdot.$.contents(this.container).forEach(t=>{i.push(t.cloneNode(!0))}),i}_getMaxHeight(){if("number"==typeof this.options.height)return this.options.height;for(var t=window.getComputedStyle(this.container),e=["maxHeight","height"],i=0,o=0;o(e=e||document,Array.prototype.slice.call(e.querySelectorAll(t))),contents:t=>(t=t||document,Array.prototype.slice.call(t.childNodes))},function(t){void 0!==t&&(t.fn.dotdotdot=function(t){return this.each((e,i)=>{let o=new Dotdotdot(i,t);i.dotdotdot=o.API})})}(window.Zepto||window.jQuery); -------------------------------------------------------------------------------- /dist/dotdotdot.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * dotdotdot JS 4.1.0 3 | * 4 | * dotdotdot.frebsite.nl 5 | * 6 | * Copyright (c) Fred Heusschen 7 | * www.frebsite.nl 8 | * 9 | * License: CC-BY-NC-4.0 10 | * http://creativecommons.org/licenses/by-nc/4.0/ 11 | */ 12 | var Dotdotdot=function(){function t(e,i){void 0===i&&(i=t.options);var n=this;for(var o in this.container=e,this.options=i||{},this.watchTimeout=null,this.watchInterval=null,this.resizeEvent=null,t.options)t.options.hasOwnProperty(o)&&void 0===this.options[o]&&(this.options[o]=t.options[o]);var r=this.container.dotdotdot;r&&r.destroy(),this.API={},["truncate","restore","destroy","watch","unwatch"].forEach(function(t){n.API[t]=function(){return n[t].call(n)}}),this.container.dotdotdot=this.API,this.originalStyle=this.container.getAttribute("style")||"",this.originalContent=this._getOriginalContent(),this.ellipsis=document.createTextNode(this.options.ellipsis);var s=window.getComputedStyle(this.container);"break-word"!==s["word-wrap"]&&(this.container.style["word-wrap"]="break-word"),"pre"===s["white-space"]?this.container.style["white-space"]="pre-wrap":"nowrap"===s["white-space"]&&(this.container.style["white-space"]="normal"),null===this.options.height&&(this.options.height=this._getMaxHeight()),this.truncate(),this.options.watch&&this.watch()}return t.prototype.restore=function(){var t=this;this.unwatch(),this.container.setAttribute("style",this.originalStyle),this.container.classList.remove("ddd-truncated"),this.container.innerHTML="",this.originalContent.forEach(function(e){t.container.append(e)})},t.prototype.destroy=function(){this.restore(),this.container.dotdotdot=null},t.prototype.watch=function(){var t=this;this.unwatch();var e={width:null,height:null},i=function(i,n,o){if(t.container.offsetWidth||t.container.offsetHeight||t.container.getClientRects().length){var r={width:i[n],height:i[o]};return e.width==r.width&&e.height==r.height||t.truncate(),r}return e};"window"===this.options.watch?(this.resizeEvent=function(n){t.watchTimeout&&clearTimeout(t.watchTimeout),t.watchTimeout=setTimeout(function(){e=i(window,"innerWidth","innerHeight")},100)},window.addEventListener("resize",this.resizeEvent)):this.watchInterval=setInterval(function(){e=i(t.container,"clientWidth","clientHeight")},1e3)},t.prototype.unwatch=function(){this.resizeEvent&&(window.removeEventListener("resize",this.resizeEvent),this.resizeEvent=null),this.watchInterval&&clearInterval(this.watchInterval),this.watchTimeout&&clearTimeout(this.watchTimeout)},t.prototype.truncate=function(){var t=this,e=!1;return this.container.innerHTML="",this.originalContent.forEach(function(e){t.container.append(e.cloneNode(!0))}),this.maxHeight=this._getMaxHeight(),this._fits()||(e=!0,this._truncateToNode(this.container)),this.container.classList[e?"add":"remove"]("ddd-truncated"),this.options.callback.call(this.container,e),e},t.prototype._truncateToNode=function(e){var i=[],n=[];if(t.$.contents(e).forEach(function(t){if(1!=t.nodeType||!t.matches(".ddd-keep")){var e=document.createComment("");t.replaceWith(e),n.push(t),i.push(e)}}),n.length){for(var o=0;o1)return void n[o-2].remove();break}}for(var a=o;a=0;o--)if(t.textContent=this._addEllipsis(n.slice(0,o).join(i)),this._fits()){"letter"==this.options.truncate&&(t.textContent=n.slice(0,o+1).join(i),this._truncateToLetter(t));break}},t.prototype._truncateToLetter=function(t){for(var e=t.textContent.split(""),i="",n=e.length;n>=0&&(!(i=e.slice(0,n).join("")).length||(t.textContent=this._addEllipsis(i),!this._fits()));n--);},t.prototype._fits=function(){return this.container.scrollHeight<=this.maxHeight+this.options.tolerance},t.prototype._addEllipsis=function(t){for(var e=[" "," ",",",";",".","!","?"];e.indexOf(t.slice(-1))>-1;)t=t.slice(0,-1);return t+=this.ellipsis.textContent},t.prototype._getOriginalContent=function(){var e="script, style";this.options.keep&&(e+=", "+this.options.keep),t.$.find(e,this.container).forEach(function(t){t.classList.add("ddd-keep")});var i="div, section, article, header, footer, p, h1, h2, h3, h4, h5, h6, table, td, td, dt, dd, li";[this.container].concat(t.$.find("*",this.container)).forEach(function(e){e.normalize(),t.$.contents(e).forEach(function(t){8==t.nodeType&&e.removeChild(t)}),t.$.contents(e).forEach(function(t){if(3==t.nodeType&&""==t.textContent.trim()){var n=t.previousSibling,o=t.nextSibling;(t.parentElement.matches("table, thead, tbody, tfoot, tr, dl, ul, ol, video")||!n||1==n.nodeType&&n.matches(i)||!o||1==o.nodeType&&o.matches(i))&&e.removeChild(t)}})});var n=[];return t.$.contents(this.container).forEach(function(t){n.push(t.cloneNode(!0))}),n},t.prototype._getMaxHeight=function(){if("number"==typeof this.options.height)return this.options.height;for(var t=window.getComputedStyle(this.container),e=["maxHeight","height"],i=0,n=0;n1)return void n[o-2].remove();break}}for(var a=o;a=0;o--)if(t.textContent=this._addEllipsis(n.slice(0,o).join(i)),this._fits()){"letter"==this.options.truncate&&(t.textContent=n.slice(0,o+1).join(i),this._truncateToLetter(t));break}},t.prototype._truncateToLetter=function(t){for(var e=t.textContent.split(""),i="",n=e.length;n>=0&&(!(i=e.slice(0,n).join("")).length||(t.textContent=this._addEllipsis(i),!this._fits()));n--);},t.prototype._fits=function(){return this.container.scrollHeight<=this.maxHeight+this.options.tolerance},t.prototype._addEllipsis=function(t){for(var e=[" "," ",",",";",".","!","?"];e.indexOf(t.slice(-1))>-1;)t=t.slice(0,-1);return t+=this.ellipsis.textContent},t.prototype._getOriginalContent=function(){var e="script, style";this.options.keep&&(e+=", "+this.options.keep),t.$.find(e,this.container).forEach(function(t){t.classList.add("ddd-keep")});var i="div, section, article, header, footer, p, h1, h2, h3, h4, h5, h6, table, td, td, dt, dd, li";[this.container].concat(t.$.find("*",this.container)).forEach(function(e){e.normalize(),t.$.contents(e).forEach(function(t){8==t.nodeType&&e.removeChild(t)}),t.$.contents(e).forEach(function(t){if(3==t.nodeType&&""==t.textContent.trim()){var n=t.previousSibling,o=t.nextSibling;(t.parentElement.matches("table, thead, tbody, tfoot, tr, dl, ul, ol, video")||!n||1==n.nodeType&&n.matches(i)||!o||1==o.nodeType&&o.matches(i))&&e.removeChild(t)}})});var n=[];return t.$.contents(this.container).forEach(function(t){n.push(t.cloneNode(!0))}),n},t.prototype._getMaxHeight=function(){if("number"==typeof this.options.height)return this.options.height;for(var t=window.getComputedStyle(this.container),e=["maxHeight","height"],i=0,n=0;n1)return void n[o-2].remove();break}}for(var a=o;a=0;o--)if(t.textContent=this._addEllipsis(n.slice(0,o).join(i)),this._fits()){"letter"==this.options.truncate&&(t.textContent=n.slice(0,o+1).join(i),this._truncateToLetter(t));break}},t.prototype._truncateToLetter=function(t){for(var e=t.textContent.split(""),i="",n=e.length;n>=0&&(!(i=e.slice(0,n).join("")).length||(t.textContent=this._addEllipsis(i),!this._fits()));n--);},t.prototype._fits=function(){return this.container.scrollHeight<=this.maxHeight+this.options.tolerance},t.prototype._addEllipsis=function(t){for(var e=[" "," ",",",";",".","!","?"];e.indexOf(t.slice(-1))>-1;)t=t.slice(0,-1);return t+=this.ellipsis.textContent},t.prototype._getOriginalContent=function(){var e="script, style";this.options.keep&&(e+=", "+this.options.keep),t.$.find(e,this.container).forEach(function(t){t.classList.add("ddd-keep")});var i="div, section, article, header, footer, p, h1, h2, h3, h4, h5, h6, table, td, td, dt, dd, li";[this.container].concat(t.$.find("*",this.container)).forEach(function(e){e.normalize(),t.$.contents(e).forEach(function(t){8==t.nodeType&&e.removeChild(t)}),t.$.contents(e).forEach(function(t){if(3==t.nodeType&&""==t.textContent.trim()){var n=t.previousSibling,o=t.nextSibling;(t.parentElement.matches("table, thead, tbody, tfoot, tr, dl, ul, ol, video")||!n||1==n.nodeType&&n.matches(i)||!o||1==o.nodeType&&o.matches(i))&&e.removeChild(t)}})});var n=[];return t.$.contents(this.container).forEach(function(t){n.push(t.cloneNode(!0))}),n},t.prototype._getMaxHeight=function(){if("number"==typeof this.options.height)return this.options.height;for(var t=window.getComputedStyle(this.container),e=["maxHeight","height"],i=0,n=0;n { 8 | return ( 9 | gulp 10 | .src('src/*.ts') 11 | 12 | // First, we transpile back to JS. 13 | .pipe( 14 | typescript({ 15 | target, 16 | module, 17 | }) 18 | ) 19 | 20 | // Next, uglify it. 21 | .pipe( 22 | terser({ 23 | output: { 24 | comments: '/^!/', 25 | }, 26 | }) 27 | ) 28 | ); 29 | }; 30 | 31 | /** Save plugin to be used with UMD pattern. */ 32 | const jsUMD = (cb) => { 33 | return transpile('es5', 'umd') 34 | .pipe(rename('dotdotdot.umd.js')) 35 | .pipe(gulp.dest('dist')); 36 | }; 37 | 38 | /** Save plugin to be used as an ES6 module. */ 39 | const jsES6 = (cb) => { 40 | return transpile('es6', 'es6') 41 | .pipe(rename('dotdotdot.es6.js')) 42 | .pipe(gulp.dest('dist')); 43 | }; 44 | 45 | /** Save plugin to be used with bundlers that support the pkg.module definition. */ 46 | const jsESM = (cb) => { 47 | return transpile('es5', 'es6') 48 | .pipe(rename('dotdotdot.esm.js')) 49 | .pipe(gulp.dest('dist')); 50 | }; 51 | 52 | /** Save plugin to be used without UMD pattern or ES6 module. */ 53 | const js = (cb) => { 54 | return gulp 55 | .src('dist/dotdotdot.esm.js') 56 | .pipe(rename('dotdotdot.js')) 57 | .pipe(replace('export default Dotdotdot;', '')) 58 | .pipe(gulp.dest('dist')); 59 | }; 60 | 61 | const types = (cb) => { 62 | return gulp 63 | .src('src/*.ts') 64 | .pipe(typescript({ declaration: true })) 65 | .dts.pipe(gulp.dest('dist')); 66 | }; 67 | 68 | const defaultTask = gulp.parallel(jsUMD, gulp.series(jsESM, js), jsES6, types); 69 | exports.default = defaultTask; 70 | 71 | // Watch task 'gulp watch': Starts a watch on JS tasks 72 | const watch = (cb) => { 73 | gulp.watch( 74 | 'src/*.ts', 75 | gulp.parallel(jsUMD, gulp.series(jsESM, js), jsES6, types) 76 | ); 77 | cb(); 78 | }; 79 | exports.watch = watch; 80 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | dotdotdot, javascript plugin for multiple line content ellipsis. 9 | 114 | 115 | 116 | 117 |
118 |
119 |
120 | 121 |
122 | Oh no!
123 | You're using an old browser. 124 | Although the Dotdotdot plugin will work in older browsers (although you might need some polyfills), 125 | these examples use modern JS your browser probably doesn't understand. 126 |
127 | 128 |
129 |

dotdotdot

130 |

Javascript plugin for truncating multiple line content on a webpage.
131 | Resize your browser to see the examples below in action.

132 |

Documentation: dotdotdot.frebsite.nl

133 |
134 | 135 |
136 |
137 |

Truncate multiple line content

138 |

Lorem Ipsum is simply dummy text of the printing and typesetting industry. 139 | It has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. 140 | Lorem Ipsum has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 141 | Read more »

142 |
143 |
144 | 145 |
146 |
147 |

Truncate to readable pathnames

148 |
    149 |
  1. file:///users/your-name/desktop/project/website/htdocs/index.html
  2. 150 |
  3. file:///users/your-name/desktop/project/website/htdocs/css/style.css
  4. 151 |
  5. file:///users/your-name/desktop/project/website/htdocs/css/layout.css
  6. 152 |
153 |
154 |
155 | 156 |
157 |
158 |

Toggle full story

159 |

Lorem Ipsum is simply dummy text of the printing and typesetting industry. 160 | It has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. 161 | Lorem Ipsum has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.

162 | 163 |
164 |
165 | 166 |
167 | 168 | 169 | 170 | 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dotdotdot-js", 3 | "version": "4.1.0", 4 | "main": "dist/dotdotdot.js", 5 | "module": "dist/dotdotdot.esm.js", 6 | "types": "dist/dotdotdot.d.ts", 7 | "author": "Fred Heusschen ", 8 | "license": "CC-BY-NC-4.0", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/FrDH/dotdotdot-js.git" 12 | }, 13 | "description": "Dotdotdot is a javascript plugin for truncating multiple line content with an ellipsis.", 14 | "keywords": [ 15 | "truncate", 16 | "truncating", 17 | "ellipsis", 18 | "dotdotdot", 19 | "multiline", 20 | "text", 21 | "text-overflow", 22 | "overflow", 23 | "dots" 24 | ], 25 | "scripts": { 26 | "build": "gulp default" 27 | }, 28 | "devDependencies": { 29 | "gulp": "^4.0.0", 30 | "gulp-rename": "^1.4.0", 31 | "gulp-replace": "^1.0.0", 32 | "gulp-terser": "^1.1.7", 33 | "gulp-typescript": "^5.0.0", 34 | "typescript": "^3.3.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/dotdotdot.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * dotdotdot JS 4.1.0 3 | * 4 | * dotdotdot.frebsite.nl 5 | * 6 | * Copyright (c) Fred Heusschen 7 | * www.frebsite.nl 8 | * 9 | * License: CC-BY-NC-4.0 10 | * http://creativecommons.org/licenses/by-nc/4.0/ 11 | */ 12 | 13 | /** An object with any value. */ 14 | interface dddLooseObject { 15 | [key: string]: any; 16 | } 17 | 18 | /** An object with function values. */ 19 | interface dddFunctionObject { 20 | [key: string]: Function; 21 | } 22 | 23 | /** Default options for the class. */ 24 | interface dddOptions { 25 | /** The ellipsis to place after the truncated text. */ 26 | ellipsis?: string; 27 | 28 | /** Function to invoke after the truncate process. */ 29 | callback?: Function; 30 | 31 | /** How to truncate: 'node', 'word' (default) or 'letter'. */ 32 | truncate?: string; 33 | 34 | /** Optional tolerance for the container height. */ 35 | tolerance?: number; 36 | 37 | /** Selector for elements not to remove from the DOM. */ 38 | keep?: string; 39 | 40 | /** Whether and when to update the ellipsis: null, true or 'window' (default) */ 41 | watch?: string; 42 | 43 | /** The height for the container. If null, the max-height will be read from the CSS properties. */ 44 | height?: number; 45 | } 46 | 47 | /** 48 | * Class for a multiline ellipsis. 49 | */ 50 | export default class Dotdotdot { 51 | /** Plugin version. */ 52 | static version: string = '4.1.0'; 53 | 54 | /** Default options. */ 55 | static options: dddOptions = { 56 | ellipsis: '\u2026 ', 57 | callback: function (isTruncated) {}, 58 | truncate: 'word', 59 | tolerance: 0, 60 | keep: null, 61 | watch: 'window', 62 | height: null, 63 | }; 64 | 65 | /** Element to truncate */ 66 | container: HTMLElement; 67 | 68 | /** Inner element, added for measuring. */ 69 | innerContainer: HTMLElement; 70 | 71 | /** Options. */ 72 | options: dddOptions; 73 | 74 | /** The max-height for the element. */ 75 | maxHeight: number; 76 | 77 | /** The ellipsis to use for truncating. */ 78 | ellipsis: Text; 79 | 80 | /** The API */ 81 | API: dddFunctionObject; 82 | 83 | /** Storage for the watch timeout, oddly it has a number type. */ 84 | watchTimeout: number; 85 | 86 | /** Storage for the watch interval, oddly it has a number type. */ 87 | watchInterval: number; 88 | 89 | /** Storage for the original style attribute. */ 90 | originalStyle: string; 91 | 92 | /** Storage for the original HTML. */ 93 | originalContent: Node[]; 94 | 95 | /** Function to invoke on window resize. Needs to be stored so it can be removed later on. */ 96 | resizeEvent: EventListener; 97 | 98 | /** 99 | * Truncate a multiline element with an ellipsis. 100 | * 101 | * @param {HTMLElement} container The element to truncate. 102 | * @param {object} [options=Dotdotdot.options] Options for the menu. 103 | */ 104 | constructor( 105 | container: HTMLElement, 106 | options: dddOptions = Dotdotdot.options 107 | ) { 108 | this.container = container; 109 | this.options = options || {}; 110 | 111 | // Set the watch timeout and -interval; 112 | this.watchTimeout = null; 113 | this.watchInterval = null; 114 | 115 | // Set the resize event handler. 116 | this.resizeEvent = null; 117 | 118 | // Extend the specified options with the default options. 119 | for (let option in Dotdotdot.options) { 120 | if (!Dotdotdot.options.hasOwnProperty(option)) { 121 | continue; 122 | } 123 | 124 | if (typeof this.options[option] == 'undefined') { 125 | this.options[option] = Dotdotdot.options[option]; 126 | } 127 | } 128 | 129 | // If the element allready is a dotdotdot instance. 130 | // -> Destroy the previous instance. 131 | var oldAPI = this.container['dotdotdot']; 132 | if (oldAPI) { 133 | oldAPI.destroy(); 134 | } 135 | 136 | // Create the API. 137 | this.API = {}; 138 | ['truncate', 'restore', 'destroy', 'watch', 'unwatch'].forEach((fn) => { 139 | this.API[fn] = () => { 140 | return this[fn].call(this); 141 | }; 142 | }); 143 | 144 | // Store the API. 145 | this.container['dotdotdot'] = this.API; 146 | 147 | // Store the original style attribute; 148 | this.originalStyle = this.container.getAttribute('style') || ''; 149 | 150 | // Collect the original contents. 151 | this.originalContent = this._getOriginalContent(); 152 | 153 | // Create the ellipsis Text node. 154 | this.ellipsis = document.createTextNode(this.options.ellipsis); 155 | 156 | // Set CSS properties for the container. 157 | var computedStyle = window.getComputedStyle(this.container); 158 | if (computedStyle['word-wrap'] !== 'break-word') { 159 | this.container.style['word-wrap'] = 'break-word'; 160 | } 161 | if (computedStyle['white-space'] === 'pre') { 162 | this.container.style['white-space'] = 'pre-wrap'; 163 | } else if (computedStyle['white-space'] === 'nowrap') { 164 | this.container.style['white-space'] = 'normal'; 165 | } 166 | 167 | // Set the max-height for the container. 168 | if (this.options.height === null) { 169 | this.options.height = this._getMaxHeight(); 170 | } 171 | 172 | // Truncate the text. 173 | this.truncate(); 174 | 175 | // Set the watch. 176 | if (this.options.watch) { 177 | this.watch(); 178 | } 179 | } 180 | 181 | /** 182 | * Restore the container to a pre-init state. 183 | */ 184 | restore() { 185 | // Stop the watch. 186 | this.unwatch(); 187 | 188 | // Restore the original style. 189 | this.container.setAttribute('style', this.originalStyle); 190 | 191 | // Restore the original classname. 192 | this.container.classList.remove('ddd-truncated'); 193 | 194 | // Restore the original contents. 195 | this.container.innerHTML = ''; 196 | this.originalContent.forEach((element) => { 197 | this.container.append(element); 198 | }); 199 | } 200 | 201 | /** 202 | * Fully destroy the plugin. 203 | */ 204 | destroy() { 205 | this.restore(); 206 | this.container['dotdotdot'] = null; 207 | } 208 | 209 | /** 210 | * Start a watch for the truncate process. 211 | */ 212 | watch() { 213 | // Stop any previous watch. 214 | this.unwatch(); 215 | 216 | /** The previously measure sizes. */ 217 | var oldSizes = { 218 | width: null, 219 | height: null, 220 | }; 221 | 222 | /** 223 | * Measure the sizes and start the truncate proces. 224 | */ 225 | var watchSizes = ( 226 | element: Window | HTMLElement, 227 | width: string, 228 | height: string 229 | ) => { 230 | // Only if the container is visible. 231 | if ( 232 | this.container.offsetWidth || 233 | this.container.offsetHeight || 234 | this.container.getClientRects().length 235 | ) { 236 | let newSizes = { 237 | width: element[width], 238 | height: element[height], 239 | }; 240 | 241 | if ( 242 | oldSizes.width != newSizes.width || 243 | oldSizes.height != newSizes.height 244 | ) { 245 | this.truncate(); 246 | } 247 | 248 | return newSizes; 249 | } 250 | return oldSizes; 251 | }; 252 | 253 | // Update onWindowResize. 254 | if (this.options.watch === 'window') { 255 | this.resizeEvent = (evnt) => { 256 | // Debounce the resize event to prevent it from being called very often. 257 | if (this.watchTimeout) { 258 | clearTimeout(this.watchTimeout); 259 | } 260 | 261 | this.watchTimeout = setTimeout(() => { 262 | oldSizes = watchSizes(window, 'innerWidth', 'innerHeight'); 263 | }, 100); 264 | }; 265 | 266 | window.addEventListener('resize', this.resizeEvent); 267 | 268 | // Update in an interval. 269 | } else { 270 | this.watchInterval = setInterval(() => { 271 | oldSizes = watchSizes( 272 | this.container, 273 | 'clientWidth', 274 | 'clientHeight' 275 | ); 276 | }, 1000); 277 | } 278 | } 279 | 280 | /** 281 | * Stop the watch. 282 | */ 283 | unwatch() { 284 | // Stop the windowResize handler. 285 | if (this.resizeEvent) { 286 | window.removeEventListener('resize', this.resizeEvent); 287 | this.resizeEvent = null; 288 | } 289 | 290 | // Stop the watch interval. 291 | if (this.watchInterval) { 292 | clearInterval(this.watchInterval); 293 | } 294 | 295 | // Stop the watch timeout. 296 | if (this.watchTimeout) { 297 | clearTimeout(this.watchTimeout); 298 | } 299 | } 300 | 301 | /** 302 | * Start the truncate process. 303 | */ 304 | truncate() { 305 | var isTruncated = false; 306 | 307 | // Fill the container with all the original content. 308 | this.container.innerHTML = ''; 309 | this.originalContent.forEach((element) => { 310 | this.container.append(element.cloneNode(true)); 311 | }); 312 | 313 | // Get the max height. 314 | this.maxHeight = this._getMaxHeight(); 315 | 316 | // Truncate the text. 317 | if (!this._fits()) { 318 | isTruncated = true; 319 | this._truncateToNode(this.container); 320 | } 321 | 322 | // Add a class to the container to indicate whether or not it is truncated. 323 | this.container.classList[isTruncated ? 'add' : 'remove']( 324 | 'ddd-truncated' 325 | ); 326 | 327 | // Invoke the callback. 328 | this.options.callback.call(this.container, isTruncated); 329 | 330 | return isTruncated; 331 | } 332 | 333 | /** 334 | * Truncate an element by removing elements from the end. 335 | * 336 | * @param {HTMLElement} element The element to truncate. 337 | */ 338 | _truncateToNode(element: HTMLElement) { 339 | var _coms = [], 340 | _elms = []; 341 | 342 | // Empty the element 343 | // -> replace all contents with comments 344 | Dotdotdot.$.contents(element).forEach((element) => { 345 | if ( 346 | element.nodeType != 1 || 347 | !(element as HTMLElement).matches('.ddd-keep') 348 | ) { 349 | let comment = document.createComment(''); 350 | (element as HTMLElement).replaceWith(comment); 351 | 352 | _elms.push(element); 353 | _coms.push(comment); 354 | } 355 | }); 356 | 357 | if (!_elms.length) { 358 | return; 359 | } 360 | 361 | // Re-fill the element 362 | // -> replace comments with contents until it doesn't fit anymore. 363 | for (var e = 0; e < _elms.length; e++) { 364 | _coms[e].replaceWith(_elms[e]); 365 | 366 | let ellipsis = this.ellipsis.cloneNode(true); 367 | 368 | switch (_elms[e].nodeType) { 369 | case 1: 370 | _elms[e].append(ellipsis); 371 | break; 372 | 373 | case 3: 374 | _elms[e].after(ellipsis); 375 | break; 376 | } 377 | 378 | let fits = this._fits(); 379 | ellipsis.parentElement.removeChild(ellipsis); 380 | 381 | if (!fits) { 382 | if (this.options.truncate == 'node' && e > 1) { 383 | _elms[e - 2].remove(); 384 | return; 385 | } 386 | break; 387 | } 388 | } 389 | 390 | // Remove left over comments. 391 | for (var c = e; c < _coms.length; c++) { 392 | _coms[c].remove(); 393 | } 394 | 395 | // Get last element 396 | // -> the element that overflows. 397 | 398 | var _last = _elms[Math.max(0, Math.min(e, _elms.length - 1))]; 399 | 400 | // Border case 401 | // -> the last node with only an ellipsis in it... 402 | if (_last.nodeType == 1) { 403 | let element = document.createElement(_last.nodeName); 404 | element.append(this.ellipsis); 405 | 406 | _last.replaceWith(element); 407 | 408 | // ... fits 409 | // -> Restore the full last element. 410 | if (this._fits()) { 411 | element.replaceWith(_last); 412 | 413 | // ... doesn't fit 414 | // -> remove it and go back one element. 415 | } else { 416 | element.remove(); 417 | _last = _elms[Math.max(0, e - 1)]; 418 | } 419 | } 420 | 421 | // Proceed inside last element. 422 | if (_last.nodeType == 1) { 423 | this._truncateToNode(_last); 424 | } else { 425 | this._truncateToWord(_last); 426 | } 427 | } 428 | 429 | /** 430 | * Truncate a sentence by removing words from the end. 431 | * 432 | * @param {HTMLElement} element The element to truncate. 433 | */ 434 | _truncateToWord(element: HTMLElement) { 435 | var text = element.textContent, 436 | seporator = text.indexOf(' ') !== -1 ? ' ' : '\u3000', 437 | words = text.split(seporator); 438 | 439 | for (var a = words.length; a >= 0; a--) { 440 | element.textContent = this._addEllipsis( 441 | words.slice(0, a).join(seporator) 442 | ); 443 | 444 | if (this._fits()) { 445 | if (this.options.truncate == 'letter') { 446 | element.textContent = words.slice(0, a + 1).join(seporator); 447 | this._truncateToLetter(element); 448 | } 449 | break; 450 | } 451 | } 452 | } 453 | 454 | /** 455 | * Truncate a word by removing letters from the end. 456 | * 457 | * @param {HTMLElement} element The element to truncate. 458 | */ 459 | _truncateToLetter(element: HTMLElement) { 460 | var letters = element.textContent.split(''), 461 | text = ''; 462 | 463 | for (var a = letters.length; a >= 0; a--) { 464 | text = letters.slice(0, a).join(''); 465 | 466 | if (!text.length) { 467 | continue; 468 | } 469 | 470 | element.textContent = this._addEllipsis(text); 471 | 472 | if (this._fits()) { 473 | break; 474 | } 475 | } 476 | } 477 | 478 | /** 479 | * Test if the content fits in the container. 480 | * 481 | * @return {boolean} Whether or not the content fits in the container. 482 | */ 483 | _fits(): boolean { 484 | return ( 485 | this.container.scrollHeight <= 486 | this.maxHeight + this.options.tolerance 487 | ); 488 | } 489 | 490 | /** 491 | * Add the ellipsis to a text. 492 | * 493 | * @param {string} text The text to add the ellipsis to. 494 | * @return {string} The text with the added ellipsis. 495 | */ 496 | _addEllipsis(text: string): string { 497 | var remove = [' ', '\u3000', ',', ';', '.', '!', '?']; 498 | 499 | while (remove.indexOf(text.slice(-1)) > -1) { 500 | text = text.slice(0, -1); 501 | } 502 | text += this.ellipsis.textContent; 503 | 504 | return text; 505 | } 506 | 507 | /** 508 | * Sanitize and collect the original contents. 509 | * 510 | * @return {array} The sanitizes HTML elements. 511 | */ 512 | _getOriginalContent(): HTMLElement[] { 513 | let keep = 'script, style'; 514 | if (this.options.keep) { 515 | keep += ', ' + this.options.keep; 516 | } 517 | 518 | // Add "keep" class to nodes to keep. 519 | Dotdotdot.$.find(keep, this.container).forEach((elem) => { 520 | elem.classList.add('ddd-keep'); 521 | }); 522 | 523 | /** Block level HTML tags. */ 524 | let _block_tags_ = 525 | 'div, section, article, header, footer, p, h1, h2, h3, h4, h5, h6, table, td, td, dt, dd, li'; 526 | 527 | /** HTML tags that only have block level children. */ 528 | let _block_parents_ = 529 | 'table, thead, tbody, tfoot, tr, dl, ul, ol, video'; 530 | 531 | [this.container, ...Dotdotdot.$.find('*', this.container)].forEach( 532 | (element) => { 533 | // Removes empty Text nodes and joins adjacent Text nodes. 534 | element.normalize(); 535 | 536 | // Remove comments first 537 | Dotdotdot.$.contents(element).forEach((text) => { 538 | if (text.nodeType == 8) { 539 | element.removeChild(text); 540 | } 541 | }); 542 | 543 | // Loop over all contents and remove nodes that can be removed. 544 | Dotdotdot.$.contents(element).forEach((text) => { 545 | // Remove Text nodes that do not take up space in the DOM. 546 | // This kinda asumes a default display property for the elements in the container. 547 | if (text.nodeType == 3) { 548 | if (text.textContent.trim() == '') { 549 | let prev = text.previousSibling as HTMLElement, 550 | next = text.nextSibling as HTMLElement; 551 | 552 | if ( 553 | text.parentElement.matches(_block_parents_) || 554 | !prev || 555 | (prev.nodeType == 1 && 556 | prev.matches(_block_tags_)) || 557 | !next || 558 | (next.nodeType == 1 && 559 | next.matches(_block_tags_)) 560 | ) { 561 | element.removeChild(text); 562 | } 563 | } 564 | } 565 | }); 566 | } 567 | ); 568 | 569 | // Create a clone of all contents. 570 | let content = []; 571 | Dotdotdot.$.contents(this.container).forEach((element) => { 572 | content.push(element.cloneNode(true)); 573 | }); 574 | 575 | return content; 576 | } 577 | 578 | /** 579 | * Find the max-height for the container. 580 | * 581 | * @return {number} The max-height for the container. 582 | */ 583 | _getMaxHeight(): number { 584 | if (typeof this.options.height == 'number') { 585 | return this.options.height; 586 | } 587 | 588 | var style = window.getComputedStyle(this.container); 589 | 590 | // Find smallest CSS height 591 | var properties = ['maxHeight', 'height'], 592 | height = 0; 593 | 594 | for (var a = 0; a < properties.length; a++) { 595 | let property = style[properties[a]]; 596 | if (property.slice(-2) == 'px') { 597 | height = parseFloat(property); 598 | break; 599 | } 600 | } 601 | 602 | // Remove padding-top/bottom when needed. 603 | if (style.boxSizing == 'border-box') { 604 | properties = [ 605 | 'borderTopWidth', 606 | 'borderBottomWidth', 607 | 'paddingTop', 608 | 'paddingBottom', 609 | ]; 610 | 611 | for (var a = 0; a < properties.length; a++) { 612 | let property = style[properties[a]]; 613 | if (property.slice(-2) == 'px') { 614 | height -= parseFloat(property); 615 | } 616 | } 617 | } 618 | 619 | // Sanitize 620 | return Math.max(height, 0); 621 | } 622 | 623 | /** DOM traversing functions to uniform datatypes. */ 624 | static $ = { 625 | /** 626 | * Find elements by a query selector in an element. 627 | * 628 | * @param {string} selector The selector to search for. 629 | * @param {HTMLElement} [element=document] The element to search in. 630 | * @return {array} The found elements. 631 | */ 632 | find: ( 633 | selector: string, 634 | element?: HTMLElement | Document 635 | ): HTMLElement[] => { 636 | element = element || document; 637 | return Array.prototype.slice.call( 638 | element.querySelectorAll(selector) 639 | ); 640 | }, 641 | 642 | /** 643 | * Collect child nodes (HTML elements and TextNodes) in an element. 644 | * 645 | * @param {HTMLElement} [element=document] The element to search in. 646 | * @return {array} The found nodes. 647 | */ 648 | contents: (element?: HTMLElement | Document): Node[] => { 649 | element = element || document; 650 | return Array.prototype.slice.call(element.childNodes); 651 | }, 652 | }; 653 | } 654 | 655 | // The jQuery plugin. 656 | (function ($) { 657 | if (typeof $ != 'undefined') { 658 | $.fn.dotdotdot = function (options) { 659 | return this.each((e, element) => { 660 | let dot = new Dotdotdot(element, options); 661 | element['dotdotdot'] = dot.API; 662 | }); 663 | }; 664 | } 665 | })(window['Zepto'] || window['jQuery']); 666 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/*.ts" 4 | ], 5 | "compilerOptions": { 6 | "target": "es6", 7 | "module": "es6" 8 | } 9 | } 10 | --------------------------------------------------------------------------------