├── .babelrc
├── .npmignore
├── test
├── index.js
└── component
│ ├── auto-ellipsis.spec.css
│ └── auto-ellipsis.spec.js
├── .gitignore
├── .travis.yml
├── src
├── index.js
└── component
│ ├── auto-ellipsis.css
│ └── auto-ellipsis.jsx
├── example
├── src
│ ├── index.css
│ └── index.jsx
└── index.html
├── .editorconfig
├── server.js
├── webpack.config.test.js
├── webpack.config.development.js
├── LICENSE
├── .eslintrc
├── README.md
└── package.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "stage": 0
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | example
3 | temp
4 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import './component/auto-ellipsis.spec'
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 | lib
5 | temp
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "iojs-2"
4 | script:
5 | - npm run lint
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import AutoEllipsis from './component/auto-ellipsis'
2 | export default AutoEllipsis
3 |
--------------------------------------------------------------------------------
/src/component/auto-ellipsis.css:
--------------------------------------------------------------------------------
1 | .root {
2 | display: block;
3 | height: 100%;
4 | overflow: visible;
5 | word-wrap: break-word;
6 | }
7 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 500px;
3 | height: 40px;
4 | padding: 50px;
5 | line-height: 20px;
6 | border: 10px solid #ccc;
7 | composes: root from '../../src/component/auto-ellipsis.css';
8 | }
9 |
--------------------------------------------------------------------------------
/test/component/auto-ellipsis.spec.css:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 500px;
3 | height: 40px;
4 | padding: 50px;
5 | line-height: 20px;
6 | border: 10px solid #ccc;
7 | composes: root from '../../src/component/auto-ellipsis.css';
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = tab
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | max_line_length = 80
10 |
11 | [{*.yml, .eslintrc, .babelrc, package.json}]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/example/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import AutoEllipsis from '../../src/'
4 | import styles from './index.css'
5 |
6 | const props = {
7 | content: 'auto-ellipsis is a React component for truncation when content overlength. It use DOM range to compute the ideal endPoint of content.',
8 | styles,
9 | }
10 |
11 | ReactDOM.render(,
12 | document.getElementById('auto-ellipsis-wrap'))
13 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | auto-ellipsis demo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var webpack = require('webpack')
4 | var WebpackDevServer = require('webpack-dev-server')
5 | var config = require('./webpack.config.development')
6 |
7 | var IP = '127.0.0.1'
8 | var port = 3000
9 |
10 | var args = process.argv.splice(2)
11 |
12 | if (args[0] === 'test') {
13 | config = require('./webpack.config.test')
14 | port = 3001
15 | }
16 |
17 | new WebpackDevServer(webpack(config), {
18 | publicPath: config.output.publicPath,
19 | hot: true,
20 | historyApiFallback: false,
21 | stats: {
22 | colors: true,
23 | },
24 | }).listen(port, IP, function(err) {
25 | if (err) {
26 | console.log(err)
27 | }
28 | console.log('Listening at ' + IP + ':' + port)
29 | })
30 |
--------------------------------------------------------------------------------
/webpack.config.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var path = require('path')
4 | var webpack = require('webpack')
5 | var autoprefixer = require('autoprefixer')
6 |
7 | var host = 'http://127.0.0.1:3001'
8 |
9 | module.exports = {
10 | devtool: '#eval-source-map',
11 | entry: [
12 | 'webpack-dev-server/client?' + host,
13 | 'webpack/hot/dev-server',
14 | 'mocha!./test/index.js',
15 | ],
16 | output: {
17 | path: path.join(__dirname, 'temp'),
18 | filename: 'test.js',
19 | publicPath: host + '/static/',
20 | },
21 | module: {
22 | loaders: [{
23 | test: /\.(js|jsx)$/,
24 | loader: 'babel',
25 | exclude: /node_modules/,
26 | }, {
27 | test: /\.css$/,
28 | loader: 'style!css?modules&localIdentName=[local]-[hash:base64:5]!postcss',
29 | },]
30 | },
31 | resolve: {
32 | extensions: ['', '.js', '.jsx'],
33 | },
34 | postcss: [autoprefixer],
35 | plugins: [
36 | new webpack.HotModuleReplacementPlugin(),
37 | new webpack.DefinePlugin({
38 | 'process.env.NODE_ENV': JSON.stringify('test'),
39 | }),
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/webpack.config.development.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var path = require('path')
4 | var webpack = require('webpack')
5 | var autoprefixer = require('autoprefixer')
6 |
7 | var host = 'http://127.0.0.1:3000'
8 |
9 | module.exports = {
10 | devtool: '#eval-source-map',
11 | entry: [
12 | 'webpack-dev-server/client?' + host,
13 | 'webpack/hot/only-dev-server',
14 | './example/src/index.jsx',
15 | ],
16 | output: {
17 | path: path.join(__dirname, 'temp'),
18 | filename: 'index.js',
19 | publicPath: host + '/static/',
20 | },
21 | module: {
22 | loaders: [{
23 | test: /\.(js|jsx)$/,
24 | loader: 'react-hot!babel',
25 | exclude: /node_modules/,
26 | }, {
27 | test: /\.css$/,
28 | loader: 'style!css?modules&localIdentName=[local]-[hash:base64:5]!postcss',
29 | },]
30 | },
31 | resolve: {
32 | extensions: ['', '.js', '.jsx'],
33 | },
34 | postcss: [autoprefixer],
35 | plugins: [
36 | new webpack.HotModuleReplacementPlugin(),
37 | new webpack.DefinePlugin({
38 | 'process.env.NODE_ENV': JSON.stringify('development'),
39 | }),
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 ideal-react
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 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | env:
4 | es6: true
5 |
6 | ecmaFeatures:
7 | modules: true
8 | jsx: true
9 |
10 | parser: babel-eslint
11 |
12 | plugins: [react]
13 |
14 |
15 | rules:
16 |
17 | # Best Practices
18 |
19 | semi: [2, never]
20 |
21 | curly: [2, multi-line]
22 | comma-dangle: [2, always-multiline]
23 | no-use-before-define: [2, nofunc]
24 |
25 | no-loop-func: 1 # should allow arrow function
26 |
27 |
28 | # Strict Mode
29 |
30 | strict: [2, global]
31 | global-strict: 0
32 |
33 |
34 | # Consistence
35 |
36 | quotes: [2, single, avoid-escape]
37 | new-cap: [2, capIsNew: false]
38 |
39 | no-underscore-dangle: 0
40 | new-parens: 0
41 |
42 |
43 | # ES6+
44 |
45 | no-var: 2
46 | prefer-const: 2
47 |
48 | object-shorthand: 1 # buggy
49 |
50 | constructor-super: 2
51 | no-this-before-super: 2
52 |
53 | generator-star-spacing: 2
54 |
55 |
56 | # Extra
57 |
58 | no-labels: 1
59 | no-proto: 1
60 | no-constant-condition: 1
61 |
62 | # React
63 |
64 | react/jsx-uses-react: 2
65 | react/jsx-uses-vars: 2
66 | react/react-in-jsx-scope: 2
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # auto-ellipsis [](https://travis-ci.org/ideal-react/auto-ellipsis)
2 |
3 | > auto-ellipsis is a React component for truncation when content overlength.
4 |
5 | ## install
6 |
7 | > npm install auto-ellipsis
8 |
9 | ## build
10 |
11 | Auto-ellipsis use [css-modules][1] to resolve `css in react`. So you may use some plugin to deal with `css-modules`. If you use webpack, you just need use css-loader: `css-loader?modules`.
12 |
13 | ## custom UI
14 |
15 | Auto-ellipsis use [react-css-modules][2], it provide a high-order component to make css-modules apply in React component painlessly. We can use `css-loader?modules&localIdentName=[local]-[hash:base64:5]` in dev, then we can base `[local]` to set our own styles.
16 | You set your own styles, pass styles as props to component. More check example.
17 |
18 | ## principle
19 |
20 | Auto-ellipsis use DOM Range to compute the suitable endPoint. Range is a dom element, so we continually compare `dom bottom` with `Range bottom(dom is container)` from the back forward. Finally, we find the position suit: `dom Range bottom <= dom bottom`.
21 |
22 | ## demo
23 |
24 | See [demo][3].
25 |
26 | ## LICENSE
27 |
28 | MIT.
29 |
30 |
31 | [1]: https://github.com/css-modules/css-modules
32 | [2]: https://github.com/gajus/react-css-modules
33 | [3]: http://ideal-react.github.io
34 |
35 |
36 |
--------------------------------------------------------------------------------
/test/component/auto-ellipsis.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import expect from 'expect'
4 | import AutoEllipsis from '../../src/'
5 | import styles from './auto-ellipsis.spec.css'
6 |
7 | describe('React', () => {
8 | const root = document.body.appendChild(document.createElement('div'))
9 | root.style.visibility = 'hidden'
10 |
11 | describe('AutoEllipsis', () => {
12 | const content = 'auto-ellipsis is a React component for truncation when content overlength. It use DOM range to compute the ideal endPoint of content.'
13 | const sliceContent = 'auto-ellipsis is a React component for truncation when content overlength. It use DOM range to compute the ideal endPoint ...'
14 | const props = {content, styles}
15 |
16 | it('should be slice and add title by default', () => {
17 | const component = ReactDOM.render(,
18 | root)
19 | const dom = ReactDOM.findDOMNode(component)
20 | expect(dom.innerHTML).toEqual(sliceContent)
21 | expect(dom.title).toEqual(content)
22 | })
23 |
24 | it('should be slice and no title by set', () => {
25 | const component = ReactDOM.render(, root)
27 | const dom = ReactDOM.findDOMNode(component)
28 | expect(dom.innerHTML).toEqual(sliceContent)
29 | expect(dom.title).toEqual('')
30 | })
31 |
32 | it('should be slice and tag is by set', () => {
33 | const component = ReactDOM.render(, root)
35 | const dom = ReactDOM.findDOMNode(component)
36 | expect(dom.innerHTML).toEqual(sliceContent)
37 | expect(dom.tagName.toUpperCase()).toEqual('P')
38 | })
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "auto-ellipsis",
3 | "version": "1.3.0",
4 | "description": "auto-ellipsis is a React component for truncation when content overlength.",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "start": "node server",
8 | "test": "node server test",
9 | "clean": "rimraf lib temp",
10 | "lint": "eslint src test example --ext .js,.jsx",
11 | "build:lib": "babel src --out-dir lib && cp src/component/*.css lib/component",
12 | "build": "npm run clean && npm run lint && npm run build:lib",
13 | "prepublish": "npm run build"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/ideal-react/auto-ellipsis.git"
18 | },
19 | "keywords": [
20 | "react component",
21 | "ellipsis",
22 | "truncate"
23 | ],
24 | "author": "ustccjw",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/ideal-react/auto-ellipsis/issues"
28 | },
29 | "homepage": "https://github.com/ideal-react/auto-ellipsis#readme",
30 | "devDependencies": {
31 | "autoprefixer": "^6.0.1",
32 | "babel": "^5.8.23",
33 | "babel-eslint": "^4.1.1",
34 | "babel-loader": "^5.3.2",
35 | "css-loader": "^0.16.0",
36 | "eslint": "^1.3.1",
37 | "eslint-plugin-react": "^3.3.1",
38 | "expect": "^1.10.0",
39 | "mocha": "^2.3.2",
40 | "mocha-loader": "^0.7.1",
41 | "postcss-loader": "^0.6.0",
42 | "react-hot-loader": "^1.3.0",
43 | "rimraf": "^2.4.3",
44 | "style-loader": "^0.12.3",
45 | "webpack": "^1.12.0",
46 | "webpack-dev-server": "^1.10.1"
47 | },
48 | "dependencies": {
49 | "react": "^0.14.0-rc1",
50 | "react-css-modules": "^3.1.0",
51 | "react-dom": "^0.14.0-rc1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/component/auto-ellipsis.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import CSSModules from 'react-css-modules'
4 | import styles from './auto-ellipsis.css'
5 |
6 | @CSSModules(styles)
7 | export default class AutoEllipsis extends React.Component {
8 | static propTypes = {
9 | tag: React.PropTypes.string,
10 | content: React.PropTypes.string.isRequired,
11 | addTitle: React.PropTypes.bool,
12 | styles: React.PropTypes.object,
13 | }
14 |
15 | static defaultProps = {
16 | tag: 'div',
17 | addTitle: true,
18 | }
19 |
20 | componentDidMount() {
21 | this.computeContent()
22 | }
23 |
24 | shouldComponentUpdate(nextProps, nextState) {
25 | return JSON.stringify(this.props) !== JSON.stringify(nextProps)
26 | }
27 |
28 | componentDidUpdate() {
29 | this.computeContent()
30 | }
31 |
32 | computeContent() {
33 | const dom = ReactDOM.findDOMNode(this)
34 | let parentBottom = dom.getBoundingClientRect().bottom
35 | const style = document.defaultView.getComputedStyle(dom, null)
36 | parentBottom = parentBottom - parseFloat(style.paddingBottom) -
37 | parseFloat(style.borderBottomWidth)
38 |
39 | const range = document.createRange()
40 | range.selectNodeContents(dom)
41 | let bottom = range.getBoundingClientRect().bottom
42 | if (bottom > parentBottom) {
43 | let content = this.props.content
44 | if (this.props.addTitle) {
45 | dom.setAttribute('title', content)
46 | } else {
47 | dom.removeAttribute('title')
48 | }
49 |
50 | const container = dom.firstChild
51 | let endPoint = content.length - 1
52 | range.setStart(container, 0)
53 | while(endPoint >= 0) {
54 | range.setEnd(container, endPoint)
55 | bottom = range.getBoundingClientRect().bottom
56 | if (bottom <= parentBottom) {
57 | if (endPoint - 3 > 0) {
58 | content = content.slice(0, endPoint - 3)
59 | content += '...'
60 | } else {
61 | content = '...'
62 | }
63 | dom.innerHTML = content
64 | break
65 | }
66 | endPoint--
67 | }
68 | } else {
69 | dom.removeAttribute('title')
70 | }
71 | }
72 |
73 | render() {
74 | const props = {
75 | styleName: 'root',
76 | }
77 | const {tag, content} = this.props
78 | return React.createElement(tag, props, content)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------