├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── _config.yml ├── demo ├── dist │ ├── index.dist.js │ ├── index.html │ └── style.css ├── index.js └── package.json ├── package.json └── src ├── clamp.js ├── index.d.ts └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | package-lock.json 29 | yarn.lock 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | .git/ 3 | node_modules/ 4 | package-lock.json 5 | .gitignore 6 | .editorconfig 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cezary Nowak 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | React-dotdotdot 2 | ================ 3 | Cross-browser multiline text ellipsis for react 4 | 5 | 6 | 7 | 8 | Inspired by: 9 | https://github.com/BeSite/jQuery.dotdotdot 10 | 11 | Internally uses: 12 | https://www.npmjs.com/package/clamp-js 13 | 14 | Installation 15 | ---------------- 16 | ``` 17 | npm install --save react-dotdotdot 18 | ``` 19 | 20 | Sample usage 21 | ---------------- 22 | ``` 23 | import React from 'react' 24 | import Dotdotdot from 'react-dotdotdot' 25 | 26 | ... 27 | 28 | render() { 29 | return ( 30 |
31 | 32 |

33 | Long, long
34 | content,
35 | 3 lines
36 | will be shown. 37 |

38 |
39 |
40 | ) 41 | } 42 | 43 | ``` 44 | 45 | 46 | Dotdotdot props: 47 | ---------------- 48 | **clamp** (Number | String | 'auto'). This controls where and when to clamp the text of an element. Submitting a number controls the number of lines that should be displayed. Second, you can submit a CSS value (in px or em) that controls the height of the element as a String. Finally, you can submit the word 'auto' as a string. Auto will try to fill up the available space with the content and then automatically clamp once content no longer fits. This last option should only be set if a static height is being set on the element elsewhere (such as through CSS) otherwise no clamping will be done. 49 | 50 | **useNativeClamp**: [default: `false`] Use -webkit-line-clamp available in Webkit (Chrome, Safari) only. 51 | 52 | **splitOnChars**: [default: `['.', '-', '–', '—', ' ']`] Split on sentences (periods), hypens, en-dashes, em-dashes, and words (spaces). 53 | 54 | **animate**: [default: false] animated clamp 55 | 56 | **truncationChar**: The character to insert at the end of the HTML element after truncation is performed. This defaults to an ellipsis (…). 57 | `useNativeClamp` overrides it to default. 58 | 59 | **truncationHTML**: String of HTML to use instead of truncationChar 60 | 61 | **tagName**: [default: `div`] (String). The type of HTML tag which will wrap the component's content. 62 | 63 | Notes 64 | ----------------- 65 | React-dotdotdot is simple plugin, if you need more functionality, consider using react-truncate 66 | https://www.npmjs.com/package/react-truncate 67 | 68 | Known issues: 69 | ----------------- 70 | - react-dotdotdot does not work with text containers with nested markup. 71 | - `padding-bottom` CSS rule breaks clamp 72 | - `line-height` units might be important for React-dotdotdot. We recommend `px` over `em` 73 | 74 | Changelog 75 | ----------------- 76 | 1.3.1 77 | - Update TypeScript definition to add missing props (thanks @tuxracer) 78 | - round line-height value from computed float value - IE11 fix (thanks @YoonjiJang) 79 | 80 | 1.3.0 81 | - `useNativeClamp` prop is set to false by default, it was causing some issues. 82 | - Comments are not counted as a text anymore 83 | - Remove Github's `potential security vulnerability ` with `react-dom` 84 | 85 | 1.2.4 86 | - Added TypeScript typings (thanks @vojty and @feimosi) 87 | 88 | 1.2.3 89 | - Add the option to choose a tag other than `div` (thanks @Kalita-Roman) 90 | - Fix demo on Firefox 91 | - Added `.npmignore` to limit package size 92 | 93 | 1.2.2 94 | - Revert: Fix break word for long text 95 | - Update documentation 96 | 97 | 1.2.1 98 | - Update documentation 99 | - Re-trigger clamp on window.load 100 | - Allow for all params to passed to clamp-js (splitOnChars, animate, etc) 101 | 102 | 1.2.0 103 | - Fix word breaking for long text (issues #21 and #15; Thanks @krzysztofczernek). 104 | - calculate correct height for many childs + clamp: 'auto' (thanks @rurquia) 105 | - Update dependencies to support react 16 (thanks @emersonbroga) 106 | 107 | 1.0.17 108 | - Support for IE11, Edge and Firefox (thanks, @kkwiatkowski) 109 | 110 | 1.0.16 111 | - Remove clamp-js from package.json dependencies, as it's not maintained anymore. 112 | - Bugfix for `TypeError: elem.lastChild is null` in Firefox. 113 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /demo/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/dist/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Thanks to Carolyn McNeillie 3 | Source: https://codepen.io/carolynmcneillie/pen/Lewxrm' 4 | */ 5 | 6 | /*Let's play!*/ 7 | 8 | /*Font-weight*/ 9 | .font-weight.small { 10 | font-weight: 300; 11 | } 12 | 13 | .font-weight.medium { 14 | font-weight: 400; 15 | } 16 | 17 | .font-weight.large { 18 | font-weight: 600; 19 | } 20 | 21 | /*Letter-spacing*/ 22 | .letter-spacing.small, 23 | .letter-spacing.small span { 24 | letter-spacing: -0.05em; 25 | } 26 | 27 | .letter-spacing.medium, 28 | .letter-spacing.medium span { 29 | letter-spacing: 0; 30 | } 31 | 32 | .letter-spacing.large, 33 | .letter-spacing.large span { 34 | letter-spacing: 0.3em; 35 | } 36 | 37 | /*Word-spacing*/ 38 | .word-spacing.small { 39 | word-spacing: -0.2em; 40 | } 41 | 42 | .word-spacing.medium { 43 | word-spacing: 0em; 44 | } 45 | 46 | .word-spacing.large { 47 | word-spacing: 0.5em; 48 | } 49 | 50 | /*Line-height */ 51 | .line-height.small { 52 | line-height: 0.9; 53 | } 54 | 55 | .line-height.medium { 56 | line-height: 1.3; 57 | } 58 | 59 | .line-height.large { 60 | line-height: 2; 61 | } 62 | 63 | /* Justification */ 64 | .left { 65 | text-align: left; 66 | } 67 | 68 | .justified { 69 | text-align: justify; 70 | } 71 | 72 | .justified-hyphen { 73 | text-align: justify; 74 | hyphens: auto; 75 | } 76 | 77 | /*BONUS - play with letter-spacing on the all-caps ORANGE MARMALADE*/ 78 | 79 | span { 80 | letter-spacing: 0.025em; 81 | } 82 | 83 | /*Styles*/ 84 | 85 | * { 86 | box-model: border-box; 87 | } 88 | 89 | body { 90 | font-size: 16px; 91 | font-family: 'Cormorant Garamond', serif; 92 | line-height: 21px; 93 | font-weight: 400; 94 | letter-spacing: 0; 95 | } 96 | 97 | .wrapper { 98 | width: 1080px; 99 | border-top: 2px solid #efefef; 100 | display: block; 101 | margin: 0 auto; 102 | } 103 | 104 | .wrapper:after { 105 | content: ""; 106 | display: table; 107 | clear: both; 108 | } 109 | 110 | .wrapper > div { 111 | width: 300px; 112 | padding-right: 60px; 113 | padding-top: 30px; 114 | padding-bottom: 100px; 115 | float: left; 116 | } 117 | 118 | h1 { 119 | display: block; 120 | width: 1080px; 121 | margin: 0 auto; 122 | padding: 15px; 123 | text-transform: uppercase; 124 | letter-spacing: 0.025em; 125 | font-variant-ligatures: none; 126 | font-weight: 700; 127 | font-size: 32px; 128 | } 129 | 130 | h2 { 131 | display: block; 132 | padding-bottom: 15px; 133 | letter-spacing: 0.05em; 134 | font-variant-ligatures: none; 135 | font-weight: 700; 136 | font-size: 16px; 137 | margin: 0; 138 | } 139 | 140 | p { 141 | padding: 0; 142 | margin: 0; 143 | } 144 | 145 | p.intro { 146 | max-width: 1080px; 147 | margin: 0 auto; 148 | padding-bottom: 12px; 149 | font-size: 24px; 150 | } 151 | 152 | p.intro:first-of-type { 153 | padding-top: 100px; 154 | } 155 | 156 | p.intro:last-of-type { 157 | padding-bottom: 100px; 158 | } 159 | 160 | @media screen and (max-width: 1100px) { 161 | 162 | .intro, 163 | h1, 164 | .wrapper { 165 | width: 600px; 166 | padding-bottom: 50px; 167 | } 168 | 169 | .intro:last-of-type, 170 | .wrapper div, 171 | h1{ 172 | padding-bottom: 15px; 173 | } 174 | 175 | .wrapper div { 176 | display: block; 177 | float: none; 178 | width: 100%; 179 | margin: 0 auto; 180 | } 181 | 182 | } 183 | 184 | @media screen and (max-width: 680px) { 185 | 186 | body { 187 | padding: 35px; 188 | } 189 | 190 | .intro, 191 | h1, 192 | .wrapper { 193 | width: 100%; 194 | } 195 | 196 | h1 { 197 | padding: 10px 0; 198 | } 199 | 200 | } 201 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Dotdotdot from 'react-dotdotdot' 4 | 5 | class App extends React.Component { 6 | render() { 7 | return ( 8 |
9 |
10 | This page is clamped version of: Codepen 11 |
12 | Thanks to Carolyn McNeillie 13 |
14 |
15 |

16 | What CSS property do you use to set the colour of a text block? If you said color you were … wrong! 17 |

18 |

19 | Don’t take it too hard, though. It was a trick question. In typographic parlance, “color” refers to the visual density of a block of text. Here's a little sandbox where you can play with some of the properties that impact typographic color. 20 |

21 |

Font Weight

22 |
23 |
24 |

Light / clamp=3 useNativeClamp=false

25 | 29 |

30 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 31 |

32 |
33 |
34 |
35 |

Normal / clamp=7 useNativeClamp=true

36 | 37 |

38 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 39 |

40 |
41 |
42 |
43 |

Heavy / clamp=7

44 | 45 |

46 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 47 |

48 |
49 |
50 |
51 |

Tracking

52 |
53 |
54 |

Tight / clamp=5 useNativeClamp=false truncationChar="»"

55 | 56 |

57 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 58 |

59 |
60 |
61 |
62 |

Normal / clamp=5 useNativeClamp=false truncationHTML="<br /><marquee>…</marquee>" truncationChar=""

63 | 64 |

65 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 66 |

67 |
68 |
69 |
70 |

Wide / clamp=3

71 | 72 |

73 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 74 |

75 |
76 |
77 |
78 |

Word Spacing / clamp=3

79 |
80 |
81 |

Tight

82 | 83 |

84 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 85 |

86 |
87 |
88 |
89 |

Normal / clamp=3

90 | 91 |

92 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 93 |

94 |
95 |
96 |
97 |

Wide / clamp=3

98 | 99 |

100 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 101 |

102 |
103 |
104 |
105 |

Ledding

106 |
107 |
108 |

Tight / clamp=3

109 | 110 |

111 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 112 |

113 |
114 |
115 |
116 |

Normal / clamp=3

117 | 118 |

119 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 120 |

121 |
122 |
123 |
124 |

Double / clamp=3

125 | 126 |

127 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 128 |

129 |
130 |
131 |
132 |

Justification / clamp=3

133 |
134 |
135 |

Ragged Right

136 | 137 |

138 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 139 |

140 |
141 |
142 |
143 |

Justified / clamp=3

144 | 145 |

146 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 147 |

148 |
149 |
150 |
151 |

Justified with Hyphenation / clamp=3

152 | 153 |

154 | Either the well was very deep, or she fell very slowly, for she had plenty of time as she went down to look about her and to wonder what was going to happen next. First, she tried to look down and make out what she was coming to, but it was too dark to see anything; then she looked at the sides of the well, and noticed that they were filled with cupboards and book-shelves; here and there she saw maps and pictures hung upon pegs. She took down a jar from one of the shelves as she passed; it was labelled `ORANGE MARMALADE', but to her great disappointment it was empty: she did not like to drop the jar for fear of killing somebody, so managed to put it into one of the cupboards as she fell past it. 155 |

156 |
157 |
158 |
159 |
160 | ); 161 | } 162 | } 163 | 164 | ReactDOM.hydrate(, document.querySelector('#app')); 165 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dotdotdot-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run watch", 8 | "watch": "watchify ./index.js -o ./dist/index.dist.js -t [ babelify --presets [ env react ] ]", 9 | "build": "browserify ./index.js -o ./dist/index.dist.js -t [ babelify --presets [ env react ] ]" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "prop-types": "*", 15 | "react": ">=16.2.1", 16 | "react-dom": ">=16.2.1", 17 | "react-dotdotdot": "file:../" 18 | }, 19 | "devDependencies": { 20 | "babel-cli": "6.24.0", 21 | "babel-core": "6.24.0", 22 | "babel-plugin-transform-react-jsx": "^6.24.1", 23 | "babel-preset-env": "^1.2.2", 24 | "babel-preset-react": "^6.24.1", 25 | "babel-runtime": "6.23.0", 26 | "babelify": "^7.3.0", 27 | "browserify": "^15.2.0", 28 | "watchify": "^3.10.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dotdotdot", 3 | "version": "1.3.1", 4 | "description": "Multiline text ellipsis for react", 5 | "main": "src/index.js", 6 | "typings": "src/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/CezaryDanielNowak/React-dotdotdot.git" 13 | }, 14 | "author": "Cezary Daniel Nowak", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/CezaryDanielNowak/React-dotdotdot/issues" 18 | }, 19 | "homepage": "https://github.com/CezaryDanielNowak/React-dotdotdot#readme", 20 | "peerDependencies": { 21 | "prop-types": "*", 22 | "react": "*", 23 | "react-dom": "*" 24 | }, 25 | "dependencies": { 26 | "object.pick": "^1.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/clamp.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Clamp.js 0.7.0 3 | * Based on: https://github.com/xavi160/Clamp.js/commit/e313818da231b8dd8fd603dd9c9a61a9d725c22f 4 | * Mixins: 5 | * - https://github.com/josephschmitt/Clamp.js/pull/50 6 | * - https://github.com/josephschmitt/Clamp.js/pull/49 7 | * 8 | * Copyright 2011-2013, Joseph Schmitt http://joe.sh 9 | * Released under the WTFPL license 10 | * http://sam.zoy.org/wtfpl/ 11 | */ 12 | 13 | (function(root, factory) { 14 | if (typeof define === 'function' && define.amd) { 15 | // AMD 16 | define([], factory); 17 | } else if (typeof exports === 'object') { 18 | // Node, CommonJS-like 19 | module.exports = factory(); 20 | } else { 21 | // Browser globals 22 | root.$clamp = factory(); 23 | } 24 | }(this, function() { 25 | /** 26 | * Clamps a text node. 27 | * @param {HTMLElement} element. Element containing the text node to clamp. 28 | * @param {Object} options. Options to pass to the clamper. 29 | */ 30 | function clamp(element, options) { 31 | options = options || {}; 32 | 33 | var self = this, 34 | win = window, 35 | opt = { 36 | clamp: options.clamp || 2, 37 | useNativeClamp: typeof(options.useNativeClamp) != 'undefined' ? options.useNativeClamp : true, 38 | splitOnChars: options.splitOnChars || ['.', '-', '–', '—', ' '], //Split on sentences (periods), hypens, en-dashes, em-dashes, and words (spaces). 39 | animate: options.animate || false, 40 | truncationChar: options.truncationChar || '…', 41 | truncationHTML: options.truncationHTML 42 | }, 43 | 44 | sty = element.style, 45 | originalText = element.innerHTML, 46 | 47 | supportsNativeClamp = typeof(element.style.webkitLineClamp) != 'undefined', 48 | clampValue = opt.clamp, 49 | isCSSValue = clampValue.indexOf && (clampValue.indexOf('px') > -1 || clampValue.indexOf('em') > -1), 50 | truncationHTMLContainer; 51 | 52 | if (opt.truncationHTML) { 53 | truncationHTMLContainer = document.createElement('span'); 54 | truncationHTMLContainer.innerHTML = opt.truncationHTML; 55 | } 56 | 57 | 58 | // UTILITY FUNCTIONS __________________________________________________________ 59 | 60 | /** 61 | * Return the current style for an element. 62 | * @param {HTMLElement} elem The element to compute. 63 | * @param {string} prop The style property. 64 | * @returns {number} 65 | */ 66 | function computeStyle(elem, prop) { 67 | if (!win.getComputedStyle) { 68 | win.getComputedStyle = function(el, pseudo) { 69 | this.el = el; 70 | this.getPropertyValue = function(prop) { 71 | var re = /(\-([a-z]){1})/g; 72 | if (prop == 'float') prop = 'styleFloat'; 73 | if (re.test(prop)) { 74 | prop = prop.replace(re, function() { 75 | return arguments[2].toUpperCase(); 76 | }); 77 | } 78 | return el.currentStyle && el.currentStyle[prop] ? el.currentStyle[prop] : null; 79 | }; 80 | return this; 81 | }; 82 | } 83 | 84 | const computedStyle = win.getComputedStyle(elem, null); 85 | return computedStyle ? computedStyle.getPropertyValue(prop) : null; 86 | } 87 | 88 | /** 89 | * Returns the maximum number of lines of text that should be rendered based 90 | * on the current height of the element and the line-height of the text. 91 | */ 92 | function getMaxLines(height) { 93 | var availHeight = height || (element.parentNode.clientHeight-element.offsetTop), 94 | lineHeight = getLineHeight(element); 95 | 96 | return Math.max(Math.floor(availHeight / lineHeight), 0); 97 | } 98 | 99 | /** 100 | * Returns the maximum height a given element should have based on the line- 101 | * height of the text and the given clamp value. 102 | */ 103 | function getMaxHeight(clmp) { 104 | var lineHeight = getLineHeight(element); 105 | return lineHeight * clmp; 106 | } 107 | 108 | /** 109 | * Returns the line-height of an element as an integer. 110 | */ 111 | function getLineHeight(elem) { 112 | var lh = computeStyle(elem, 'line-height'); 113 | if (lh == 'normal') { 114 | // Normal line heights vary from browser to browser. The spec recommends 115 | // a value between 1.0 and 1.2 of the font size. Using 1.1 to split the diff. 116 | lh = parseFloat(computeStyle(elem, 'font-size')) * 1.2; 117 | } 118 | return Math.round(parseFloat(lh)); 119 | } 120 | 121 | 122 | // MEAT AND POTATOES (MMMM, POTATOES...) ______________________________________ 123 | var splitOnChars = opt.splitOnChars.slice(0), 124 | splitChar = splitOnChars[0], 125 | chunks, 126 | lastChunk; 127 | 128 | /** 129 | * Gets an element's last child. That may be another node or a node's contents. 130 | */ 131 | function getLastChild(elem) { 132 | if (!elem.lastChild) { 133 | return; 134 | } 135 | //Current element has children, need to go deeper and get last child as a text node 136 | if (elem.lastChild.children && elem.lastChild.children.length > 0) { 137 | return getLastChild(Array.prototype.slice.call(elem.children).pop()); 138 | } else if ( 139 | !elem.lastChild 140 | || !elem.lastChild.nodeValue 141 | || elem.lastChild.nodeValue == opt.truncationChar 142 | || elem.lastChild.nodeType === Node.COMMENT_NODE 143 | ) { 144 | // Handle scenario where the last child is white-space node 145 | var sibling = elem.lastChild; 146 | do { 147 | if (!sibling) { 148 | return; 149 | } 150 | // TEXT_NODE 151 | if ( 152 | sibling.nodeType === 3 153 | && ['', opt.truncationChar].indexOf(sibling.nodeValue) === -1 154 | && elem.lastChild.nodeType !== Node.COMMENT_NODE 155 | ) { 156 | return sibling; 157 | } 158 | if (sibling.lastChild) { 159 | var lastChild = getLastChild(sibling); 160 | if (lastChild) { 161 | return lastChild; 162 | } 163 | } 164 | //Current sibling is pretty useless 165 | sibling.parentNode.removeChild(sibling); 166 | } while (sibling = sibling.previousSibling); 167 | } 168 | //This is the last child we want, return it 169 | else { 170 | return elem.lastChild; 171 | } 172 | } 173 | 174 | /** 175 | * Removes one character at a time from the text until its width or 176 | * height is beneath the passed-in max param. 177 | */ 178 | function truncate(target, maxHeight) { 179 | if (!target || !maxHeight) { 180 | return; 181 | } 182 | 183 | /** 184 | * Resets global variables. 185 | */ 186 | function reset() { 187 | splitOnChars = opt.splitOnChars.slice(0); 188 | splitChar = splitOnChars[0]; 189 | chunks = null; 190 | lastChunk = null; 191 | } 192 | 193 | var nodeValue = target.nodeValue.replace(opt.truncationChar, ''); 194 | 195 | //Grab the next chunks 196 | if (!chunks) { 197 | //If there are more characters to try, grab the next one 198 | if (splitOnChars.length > 0) { 199 | splitChar = splitOnChars.shift(); 200 | } 201 | //No characters to chunk by. Go character-by-character 202 | else { 203 | splitChar = ''; 204 | } 205 | 206 | chunks = nodeValue.split(splitChar); 207 | } 208 | 209 | //If there are chunks left to remove, remove the last one and see if 210 | // the nodeValue fits. 211 | if (chunks.length > 1) { 212 | // console.log('chunks', chunks); 213 | lastChunk = chunks.pop(); 214 | // console.log('lastChunk', lastChunk); 215 | applyEllipsis(target, chunks.join(splitChar)); 216 | } 217 | //No more chunks can be removed using this character 218 | else { 219 | chunks = null; 220 | } 221 | 222 | //Insert the custom HTML before the truncation character 223 | if (truncationHTMLContainer) { 224 | target.nodeValue = target.nodeValue.replace(opt.truncationChar, ''); 225 | element.innerHTML = target.nodeValue + ' ' + truncationHTMLContainer.innerHTML + opt.truncationChar; 226 | } 227 | 228 | //Search produced valid chunks 229 | if (chunks) { 230 | //It fits 231 | if (element.clientHeight <= maxHeight) { 232 | //There's still more characters to try splitting on, not quite done yet 233 | if (splitOnChars.length >= 0 && splitChar !== '') { 234 | applyEllipsis(target, chunks.join(splitChar) + splitChar + lastChunk); 235 | chunks = null; 236 | } 237 | //Finished! 238 | else { 239 | return element.innerHTML; 240 | } 241 | } 242 | } 243 | //No valid chunks produced 244 | else { 245 | //No valid chunks even when splitting by letter, time to move 246 | //on to the next node 247 | if (splitChar === '') { 248 | applyEllipsis(target, ''); 249 | target = getLastChild(element); 250 | 251 | reset(); 252 | } 253 | } 254 | 255 | //If you get here it means still too big, let's keep truncating 256 | if (opt.animate) { 257 | setTimeout(function() { 258 | truncate(target, maxHeight); 259 | }, opt.animate === true ? 10 : opt.animate); 260 | } else { 261 | return truncate(target, maxHeight); 262 | } 263 | } 264 | 265 | function applyEllipsis(elem, str) { 266 | elem.nodeValue = str + opt.truncationChar; 267 | } 268 | 269 | 270 | // CONSTRUCTOR ________________________________________________________________ 271 | 272 | if (clampValue == 'auto') { 273 | clampValue = getMaxLines(); 274 | } else if (isCSSValue) { 275 | clampValue = getMaxLines(parseInt(clampValue, 10)); 276 | } 277 | 278 | var clampedText; 279 | if (supportsNativeClamp && opt.useNativeClamp) { 280 | sty.overflow = 'hidden'; 281 | sty.textOverflow = 'ellipsis'; 282 | sty.webkitBoxOrient = 'vertical'; 283 | sty.display = '-webkit-box'; 284 | sty.webkitLineClamp = clampValue; 285 | 286 | if (isCSSValue) { 287 | sty.height = opt.clamp + 'px'; 288 | } 289 | } else { 290 | var height = getMaxHeight(clampValue); 291 | if (height < element.clientHeight) { 292 | clampedText = truncate(getLastChild(element), height); 293 | } 294 | } 295 | 296 | return { 297 | 'original': originalText, 298 | 'clamped': clampedText 299 | }; 300 | } 301 | 302 | return clamp; 303 | })); 304 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface DotdotdotProps extends React.HTMLProps { 4 | /** 5 | * The number of lines that should be displayed, a css pixel value for height 6 | * as a string (i.e. "100px"), or "auto" to try and fill the available space 7 | */ 8 | clamp: string | number | 'auto'; 9 | 10 | /** Use -webkit-line-clamp available in WebKit (Chrome, Safari) only */ 11 | useNativeClamp?: boolean; 12 | 13 | /** Split on sentences (periods), hypens, en-dashes, em-dashes, and words */ 14 | splitOnChars?: string[]; 15 | 16 | /** Animate clamp */ 17 | animate?: boolean; 18 | 19 | /** 20 | * The character to insert at the end of the HTML element after trunation is 21 | * performed. 22 | */ 23 | truncationChar?: string; 24 | 25 | /** String of HTML to use instead of truncationChar */ 26 | truncationHTML?: string; 27 | 28 | /** The type of HTML tag which will wrap the component's content */ 29 | tagName?: string; 30 | } 31 | 32 | export default class Dotdotdot extends React.Component { } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var clamp = require('./clamp.js'); 3 | var pick = require('object.pick'); 4 | var PropTypes = require('prop-types'); 5 | var ReactDOM = require('react-dom'); 6 | 7 | /** 8 | * multuline text-overflow: ellipsis 9 | */ 10 | function Dotdotdot() { 11 | if(!(this instanceof Dotdotdot)) { 12 | throw new TypeError("Cannot call a class as a function"); 13 | } 14 | this.update = this.update.bind(this); 15 | this.getContainerRef = function (container) { 16 | this.container = container; 17 | }.bind(this); 18 | } 19 | 20 | Dotdotdot.prototype = Object.create(React.Component.prototype); 21 | Dotdotdot.prototype.componentDidMount = function() { 22 | window.addEventListener('resize', this.update, false); 23 | // NOTE: It's possible, not all fonts are loaded on window.load 24 | window.addEventListener('load', this.update, false); 25 | this.dotdotdot(ReactDOM.findDOMNode(this.container)); 26 | }; 27 | Dotdotdot.prototype.componentWillUnmount = function() { 28 | window.removeEventListener('resize', this.update, false); 29 | window.removeEventListener('load', this.update, false); 30 | }; 31 | Dotdotdot.prototype.componentDidUpdate = function() { 32 | this.dotdotdot(ReactDOM.findDOMNode(this.container)); 33 | }; 34 | 35 | Dotdotdot.prototype.dotdotdot = function(container) { 36 | if (!container) { 37 | return; 38 | } 39 | 40 | if (this.props.clamp) { 41 | if (container.length) { 42 | throw new Error('Please provide exacly one child to dotdotdot'); 43 | } 44 | clamp(container, pick(this.props, [ 45 | 'animate', 46 | 'clamp', 47 | 'splitOnChars', 48 | 'truncationChar', 49 | 'truncationHTML', 50 | 'useNativeClamp' 51 | ])); 52 | }; 53 | }; 54 | Dotdotdot.prototype.update = function() { 55 | this.forceUpdate(); 56 | }; 57 | 58 | Dotdotdot.prototype.render = function() { 59 | return React.createElement( 60 | this.props.tagName, 61 | { 62 | ref: this.getContainerRef, 63 | className: this.props.className 64 | }, 65 | this.props.children 66 | ); 67 | }; 68 | 69 | // Statics: 70 | Dotdotdot.propTypes = { 71 | children: PropTypes.node, 72 | clamp: PropTypes.oneOfType([ 73 | PropTypes.string, 74 | PropTypes.number, 75 | PropTypes.bool 76 | ]).isRequired, 77 | truncationChar: PropTypes.string, 78 | useNativeClamp: PropTypes.bool, 79 | className: PropTypes.string, 80 | tagName: PropTypes.string 81 | }; 82 | 83 | Dotdotdot.defaultProps = { 84 | truncationChar: '\u2026', 85 | useNativeClamp: false, 86 | tagName: 'div' 87 | }; 88 | 89 | module.exports = Dotdotdot; 90 | --------------------------------------------------------------------------------