├── .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 |
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 |
--------------------------------------------------------------------------------