├── .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 [![Build Status](https://travis-ci.org/ideal-react/auto-ellipsis.svg)](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 | --------------------------------------------------------------------------------