├── .babelrc
├── .editorconfig
├── .eslintrc
├── .flowconfig
├── .gitignore
├── .prettierrc
├── .travis.yml
├── README.md
├── now.json
├── package.json
├── rollup.config.js
├── src
├── .eslintrc
├── __tests__
│ ├── ReactTruncatedComponent.test.js
│ ├── getLevelInfo.test.js
│ └── truncateLevel.test.js
├── index.js
├── resultBox.js
├── treeUtils.js
├── types.js
├── updater.js
└── utils.js
├── website
├── .babelrc
├── README.md
├── next.config.js
├── package.json
├── pages
│ ├── _document.js
│ └── index
│ │ ├── docs.md
│ │ ├── docsContainer.js
│ │ ├── ellipsis.js
│ │ ├── githubLogo.js
│ │ ├── index.js
│ │ ├── introContainer.js
│ │ ├── introContentContainer.js
│ │ ├── introTitle.js
│ │ ├── liveResult.js
│ │ ├── liveResultBox.js
│ │ ├── liveSettingsContainer.js
│ │ ├── logo.js
│ │ ├── numberOfLinesInput.js
│ │ ├── prism.css
│ │ ├── topBar.js
│ │ └── truncationEnabled.js
├── static
│ ├── 7xT19rLg5W.gif
│ ├── favicon.ico
│ ├── index.html
│ ├── logo.png
│ └── manifest.json
├── utils
│ └── mediaTemplates.js
└── yarn.lock
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/env",
5 | {
6 | "modules": false
7 | }
8 | ],
9 | "@babel/preset-react",
10 | "@babel/preset-flow"
11 | ],
12 | "plugins": [["@babel/proposal-class-properties", { "loose": true }]]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "react-app",
4 | "plugin:prettier/recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.flowconfig:
--------------------------------------------------------------------------------
1 | [ignore]
2 |
3 | [include]
4 |
5 | [libs]
6 |
7 | [lints]
8 |
9 | [options]
10 |
11 | [strict]
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # See https://help.github.com/ignore-files/ for more about ignoring files.
3 |
4 | # dependencies
5 | node_modules
6 |
7 | # builds
8 | build
9 | dist
10 | .rpt2_cache
11 | .next
12 |
13 | # misc
14 | .DS_Store
15 | .env
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all"
3 | }
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - stable
5 | cache:
6 | yarn: true
7 | directories:
8 | - $HOME/.yarn-cache
9 | - node_modules
10 | script:
11 | - npm test
12 | deploy:
13 | - provider: npm
14 | email: $NPM_EMAIL
15 | api_key: $NPM_AUTH_TOKEN
16 | skip_cleanup: true
17 | on:
18 | tags: true
19 | branch: master
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 | React component to truncate your text with format and paragraphs support included.
11 |
12 |
13 |
14 |
15 | [](https://www.npmjs.com/package/react-truncated-component) [](https://travis-ci.com/juangl/react-truncated-component)
16 |
17 |
22 |
23 | ## Install
24 |
25 | ```bash
26 | npm install --save react-truncated-component
27 | ```
28 |
29 | ## Usage
30 |
31 | ```javascript
32 | function TruncatedText() {
33 | return (
34 |
35 | {/*
36 |
37 |
38 | put your long text right here
39 |
40 |
41 | */}
42 |
43 | );
44 | }
45 | ```
46 |
47 | ## Docs
48 |
49 | [To see the documentation visit the website.](https://react-truncated-component.now.sh)
50 |
51 | ## License
52 |
53 | MIT © [Juan Garcia](https://github.com/juangl)
54 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "alias": "react-truncated-component.now.sh",
4 | "name": "react-truncated-component",
5 | "builds": [{ "src": "website/next.config.js", "use": "@now/next" }],
6 | "routes": [{ "src": "/(.*)", "dest": "/website/$1" }]
7 | }
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-truncated-component",
3 | "version": "1.0.4",
4 | "description": "A React component to truncate text with format",
5 | "author": "juangl",
6 | "license": "MIT",
7 | "repository": "juangl/react-truncated-component",
8 | "main": "dist/index.js",
9 | "module": "dist/index.es.js",
10 | "jsnext:main": "dist/index.es.js",
11 | "engines": {
12 | "node": ">=8",
13 | "npm": ">=5"
14 | },
15 | "scripts": {
16 | "test": "cross-env CI=1 SKIP_PREFLIGHT_CHECK=true react-scripts test",
17 | "flow": "flow check --max-warnings=0 src && flow check example",
18 | "test:watch": "react-scripts test",
19 | "build": "rollup -c",
20 | "start": "rollup -c -w",
21 | "prepare": "yarn run build",
22 | "prettier": "prettier --write '**/*.{js,json,css,md}'",
23 | "eslint-check-config": "eslint --print-config . | eslint-config-prettier-check",
24 | "lint": "eslint src"
25 | },
26 | "husky": {
27 | "hooks": {
28 | "pre-commit": "lint-staged"
29 | }
30 | },
31 | "lint-staged": {
32 | "src/*.js": [
33 | "eslint"
34 | ],
35 | "**/*.{js,json,css,md}": [
36 | "prettier --write",
37 | "yarn test --findRelatedTests",
38 | "git add"
39 | ]
40 | },
41 | "peerDependencies": {
42 | "react": "^15.0.0 || ^16.0.0 || ^17.0.0",
43 | "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.0.0",
47 | "@babel/plugin-proposal-class-properties": "^7.0.0",
48 | "@babel/preset-env": "^7.0.0",
49 | "@babel/preset-flow": "^7.0.0",
50 | "@babel/preset-react": "^7.0.0",
51 | "@svgr/rollup": "^2.4.1",
52 | "@testing-library/react": "^8.0.1",
53 | "babel-eslint": "^9.0.0",
54 | "cross-env": "^5.1.4",
55 | "eslint": "^5.10.0",
56 | "eslint-config-prettier": "^3.3.0",
57 | "eslint-config-react-app": "^3.0.5",
58 | "eslint-plugin-flowtype": "^3.2.0",
59 | "eslint-plugin-import": "^2.14.0",
60 | "eslint-plugin-jsx-a11y": "^6.1.2",
61 | "eslint-plugin-node": "^7.0.1",
62 | "eslint-plugin-prettier": "^3.0.0",
63 | "eslint-plugin-react": "^7.11.1",
64 | "flow-bin": "^0.89.0",
65 | "husky": "^1.2.1",
66 | "jest-dom": "^3.4.0",
67 | "lint-staged": "^8.1.0",
68 | "prettier": "1.15.3",
69 | "react": "^16.8.6",
70 | "react-dom": "^16.8.6",
71 | "react-scripts": "^3.0.1",
72 | "rollup": "^0.67.4",
73 | "rollup-plugin-babel": "^4.1.0",
74 | "rollup-plugin-commonjs": "^9.1.3",
75 | "rollup-plugin-node-resolve": "^3.3.0",
76 | "rollup-plugin-peer-deps-external": "^2.2.0",
77 | "rollup-plugin-postcss": "^1.6.2",
78 | "rollup-plugin-url": "^1.4.0"
79 | },
80 | "files": [
81 | "dist"
82 | ]
83 | }
84 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import commonjs from "rollup-plugin-commonjs";
3 | import external from "rollup-plugin-peer-deps-external";
4 | import postcss from "rollup-plugin-postcss";
5 | import resolve from "rollup-plugin-node-resolve";
6 | import url from "rollup-plugin-url";
7 | import svgr from "@svgr/rollup";
8 |
9 | import pkg from "./package.json";
10 |
11 | export default {
12 | input: "src/index.js",
13 | output: [
14 | {
15 | file: pkg.main,
16 | format: "cjs",
17 | sourcemap: true,
18 | },
19 | {
20 | file: pkg.module,
21 | format: "es",
22 | sourcemap: true,
23 | },
24 | ],
25 | plugins: [
26 | external(),
27 | postcss({
28 | modules: true,
29 | }),
30 | url(),
31 | svgr(),
32 | babel({
33 | exclude: "node_modules/**",
34 | }),
35 | resolve(),
36 | commonjs(),
37 | ],
38 | };
39 |
--------------------------------------------------------------------------------
/src/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/__tests__/ReactTruncatedComponent.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, cleanup } from "@testing-library/react";
3 | import "jest-dom/extend-expect";
4 |
5 | import ReactTruncatedComponent from "../";
6 |
7 | describe(" ", () => {
8 | afterEach(cleanup);
9 | test("Using cache key", () => {
10 | const { container } = render(
11 |
12 |
18 | HELLO!
19 |
20 |
,
21 | );
22 |
23 | const { container: container2 } = render(
24 |
25 |
31 | HELLO!
32 |
33 |
,
34 | );
35 |
36 | expect(container.innerHTML === container2.innerHTML).toBe(true);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/__tests__/getLevelInfo.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getLevelInfo } from "../treeUtils";
3 |
4 | describe("getLevelInfo", () => {
5 | test("basic usage", () => {
6 | const tree = (
7 | <>
8 |
9 | text
10 | >
11 | );
12 |
13 | const childrenArray = tree.props.children;
14 |
15 | expect(getLevelInfo(childrenArray, 0)).toEqual({ length: 2 });
16 | expect(getLevelInfo(childrenArray, 1)).toEqual({ length: 1 });
17 | expect(getLevelInfo(childrenArray, 2)).toEqual({ length: 4 });
18 | expect(getLevelInfo(childrenArray, 3)).toEqual({ length: 0 });
19 | });
20 |
21 | test("getLevelInfo with falsy children", () => {
22 | const tree = (
23 | <>
24 |
25 |
26 | >
27 | );
28 |
29 | const childrenArray = tree.props.children;
30 |
31 | expect(getLevelInfo(childrenArray, 1)).toEqual({ length: 0 });
32 | });
33 |
34 | test("getLevelInfo with arrays as children", () => {
35 | const tree = (
36 | <>
37 |
38 | {["text",
text
]}
39 | >
40 | );
41 |
42 | const childrenArray = tree.props.children;
43 |
44 | expect(getLevelInfo(childrenArray, 0)).toEqual({ length: 2 });
45 | expect(getLevelInfo(childrenArray, 1)).toEqual({ length: 2 });
46 | expect(getLevelInfo(childrenArray, 2)).toEqual({ length: 1 });
47 | expect(getLevelInfo(childrenArray, 3)).toEqual({ length: 4 });
48 | });
49 |
50 | test("getLevelInfo with arrays as children and text", () => {
51 | const tree = (
52 | <>
53 |
54 |
{["text"]} text
55 |
56 | >
57 | );
58 |
59 | const childrenArray = tree.props.children;
60 |
61 | expect(getLevelInfo(childrenArray, 0)).toEqual({ length: 1 });
62 | expect(getLevelInfo(childrenArray, 1)).toEqual({ length: 1 });
63 | expect(getLevelInfo(childrenArray, 2)).toEqual({ length: 2 });
64 | expect(getLevelInfo(childrenArray, 3)).toEqual({ length: 5 });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/src/__tests__/truncateLevel.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getLevelInfo, truncateLevel } from "../treeUtils";
3 |
4 | describe("TruncateLevel", () => {
5 | const tree = (
6 | <>
7 |
8 | {["text",
text
]}
9 | >
10 | );
11 |
12 | const childrenArray = tree.props.children;
13 |
14 | test("level 0", () => {
15 | expect(getLevelInfo(childrenArray, 0)).toEqual({ length: 2 });
16 | expect(truncateLevel(childrenArray, 0)).toMatchInlineSnapshot(`
`);
17 | });
18 |
19 | test("level 1", () => {
20 | expect(getLevelInfo(childrenArray, 1)).toEqual({ length: 2 });
21 | expect(truncateLevel(childrenArray, 1)).toMatchInlineSnapshot(`
22 | Array [
23 |
,
24 |
25 | text
26 |
,
27 | ]
28 | `);
29 | });
30 |
31 | test("level 2", () => {
32 | expect(getLevelInfo(childrenArray, 2)).toEqual({ length: 1 });
33 | expect(truncateLevel(childrenArray, 2)).toMatchInlineSnapshot(`
34 | Array [
35 |
,
36 |
37 | text
38 |
,
39 | ]
40 | `);
41 | });
42 |
43 | test("level 3", () => {
44 | expect(getLevelInfo(childrenArray, 3)).toEqual({ length: 4 });
45 | expect(truncateLevel(childrenArray, 3)).toMatchInlineSnapshot(`
46 | Array [
47 |
,
48 |
49 | text
50 |
51 | tex
52 |
53 | ,
54 | ]
55 | `);
56 | });
57 |
58 | test("Truncating an array shound always keep the children array", () => {
59 | const tree = (
60 | <>
61 | {["text", "test"]}
62 | >
63 | );
64 |
65 | const childrenArray = tree.props.children;
66 |
67 | expect(getLevelInfo(childrenArray, 1)).toEqual({ length: 2 });
68 | const truncate1 = truncateLevel(childrenArray, 1);
69 | expect(getLevelInfo(truncate1, 1)).toEqual({ length: 1 });
70 | });
71 |
72 | test("truncateLevel to specific index ", () => {
73 | const tree = (
74 | <>
75 |
76 |
text
77 |
78 | >
79 | );
80 |
81 | const childrenArray = tree.props.children;
82 |
83 | expect(truncateLevel(childrenArray, 3, 1)).toMatchInlineSnapshot(`
84 |
85 |
86 | t
87 |
88 |
89 | `);
90 | });
91 |
92 | test("truncateLevel removes void elements", () => {
93 | const tree = (
94 | <>
95 |
96 |
text
97 |
98 | >
99 | );
100 |
101 | const childrenArray = tree.props.children;
102 |
103 | expect(truncateLevel(childrenArray, 2)).toMatchInlineSnapshot(`null`);
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from "react";
3 |
4 | import ResultBox from "./resultBox";
5 | import { getLevelInfo } from "./treeUtils";
6 | import updater from "./updater";
7 | import { didContentChange } from "./utils";
8 | import type { TruncatedComponentProps, TruncatedComponentState } from "./types";
9 |
10 | class TruncatedComponent extends React.Component<
11 | TruncatedComponentProps,
12 | TruncatedComponentState,
13 | > {
14 | static defaultProps = {
15 | numberOfLines: undefined,
16 | onTruncate: () => {},
17 | ellipsis: "...",
18 | };
19 |
20 | state = updater.getInitialState(this.props.children, this.props.cacheKey);
21 |
22 | containerRef: ?React.ElementRef<"div">;
23 |
24 | componentDidMount() {
25 | if (this.state.isTruncationCompleted) {
26 | this.callOnTruncate();
27 | }
28 | }
29 |
30 | componentDidUpdate(
31 | prevProps: TruncatedComponentProps,
32 | prevState: TruncatedComponentState,
33 | ) {
34 | const { isTruncationCompleted } = this.state;
35 | if (isTruncationCompleted && !prevState.isTruncationCompleted) {
36 | this.callOnTruncate();
37 | }
38 |
39 | this.reRunTruncationIfNeeded(prevProps);
40 | }
41 |
42 | componentWillUnmount() {
43 | const { cacheKey } = this.props;
44 | if (!this.state.isTruncationCompleted) {
45 | updater.clearCache(cacheKey);
46 | }
47 | }
48 |
49 | reRunTruncationIfNeeded(prevProps: TruncatedComponentProps) {
50 | if (
51 | didContentChange(prevProps, this.props) ||
52 | this.props.numberOfLines !== prevProps.numberOfLines
53 | ) {
54 | this.setState(
55 | updater.getInitialState(this.props.children, this.props.cacheKey),
56 | );
57 | }
58 | }
59 |
60 | callOnTruncate() {
61 | this.props.onTruncate(this.state.isTruncated);
62 | }
63 |
64 | getCurrentNumberOfLines(heightByParagraph: Array) {
65 | const { lineHeight } = this.props;
66 | return heightByParagraph.reduce((acc, measure) => {
67 | return acc + measure / lineHeight;
68 | }, 0);
69 | }
70 |
71 | // this works using a binary-search across each level of the React tree
72 | // to find the most amount of text that fits in the desired number
73 | // of lines.
74 | // We can think of a React tree as a structure where React.Node is the type
75 | // of the node and it could have and array of children (`node.props.children`)
76 | // and leaf nodes are usually `string`.
77 | onMeasure = (heightByParagraph: Array) => {
78 | const { numberOfLines, cacheKey } = this.props;
79 | const { currentTree, previousTree, level, startOffset } = this.state;
80 |
81 | const currentLines = this.getCurrentNumberOfLines(heightByParagraph);
82 | const currentLevelInfo = getLevelInfo(currentTree, level);
83 |
84 | // $FlowFixMe suppresses the curretLines error since it could be null
85 | if (currentLines <= numberOfLines) {
86 | if (this.state.isTruncated) {
87 | // this is not the first measurement
88 | const deeperLevelInfo = getLevelInfo(previousTree, level + 1);
89 | const lastLevelInfo = getLevelInfo(previousTree, level);
90 |
91 | if (startOffset + 1 >= lastLevelInfo.length) {
92 | // this level is done
93 | updater.updateLevelCache(
94 | cacheKey,
95 | level,
96 | deeperLevelInfo.length
97 | ? lastLevelInfo.length
98 | : currentLevelInfo.length,
99 | );
100 |
101 | if (deeperLevelInfo.length) {
102 | this.setState(updater.goToDeeperLevel);
103 | } else {
104 | // we can't go deeper. Truncation has done
105 | this.setState(updater.truncationCompleted);
106 | }
107 | } else {
108 | // reduce the range of search
109 |
110 | // $FlowFixMe at this point, truncatedAt shouldn't be null
111 | this.setState(updater.setStartOffset);
112 | }
113 | } else {
114 | // first measurement. Fits without truncate
115 | this.setState(updater.truncationCompleted);
116 | }
117 | } else {
118 | if (currentLevelInfo.length) {
119 | this.setState(updater.truncateTotheMidddle(currentLevelInfo));
120 | }
121 | }
122 | };
123 |
124 | render() {
125 | const shouldMeasure =
126 | !this.state.isTruncationCompleted && !!this.props.numberOfLines;
127 |
128 | return (
129 | {
131 | this.containerRef = ref;
132 | }}
133 | style={{
134 | lineHeight: `${this.props.lineHeight}px`,
135 | }}
136 | >
137 | {
138 |
144 | {this.state.currentTree}
145 |
146 | }
147 |
148 | );
149 | }
150 | }
151 |
152 | export default TruncatedComponent;
153 |
--------------------------------------------------------------------------------
/src/resultBox.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from "react";
3 |
4 | import { didContentChange } from "./utils";
5 |
6 | const getChildrenInArray = element => {
7 | const { children } = element.props;
8 | return React.Children.toArray(children);
9 | };
10 |
11 | const addEllipsis = (children, ellipsis) => {
12 | const paragraphs = React.Children.toArray(children);
13 |
14 | if (!paragraphs.length) {
15 | return null;
16 | }
17 |
18 | const lastParagraph = paragraphs[paragraphs.length - 1];
19 | const lastParagraphChildren = getChildrenInArray(lastParagraph);
20 |
21 | // add the ellipsis at the end of the last paragraph
22 | paragraphs[paragraphs.length - 1] = React.cloneElement(
23 | lastParagraph,
24 | undefined,
25 | ...lastParagraphChildren,
26 | ellipsis,
27 | );
28 |
29 | return paragraphs;
30 | };
31 |
32 | const NESTED_UPDATE_LIMIT = 50;
33 |
34 | type Props = {|
35 | ellipsis: React.Node,
36 | children: React.Node,
37 | onMeasure: (Array) => void,
38 | shouldMeasure: boolean,
39 | isTruncated: boolean,
40 | |};
41 |
42 | class ResultBox extends React.Component {
43 | paragraphRefs = [];
44 | containerRef: ?React.ElementRef<"div"> = null;
45 | nestedUpdatesCount = 0;
46 |
47 | shouldComponentUpdate(nextProps: Props) {
48 | return didContentChange(this.props, nextProps);
49 | }
50 |
51 | componentDidMount() {
52 | if (this.props.shouldMeasure) {
53 | this.measureParagraphs();
54 | }
55 | }
56 |
57 | componentDidUpdate(prevProps: Props) {
58 | if (didContentChange(this.props, prevProps) && this.props.shouldMeasure) {
59 | this.measureParagraphs();
60 | }
61 | }
62 |
63 | measureParagraphs = () => {
64 | const measureByParagraph = [];
65 | const children = this.containerRef ? this.containerRef.children : [];
66 |
67 | for (const paragraph of children) {
68 | let containerheight = Math.floor(paragraph.clientHeight);
69 | measureByParagraph.push(containerheight);
70 | }
71 |
72 | this.nestedUpdatesCount++;
73 | if (this.nestedUpdatesCount === NESTED_UPDATE_LIMIT) {
74 | requestAnimationFrame(() => {
75 | this.nestedUpdatesCount = 0;
76 | this.props.onMeasure(measureByParagraph);
77 | });
78 | } else {
79 | this.props.onMeasure(measureByParagraph);
80 | }
81 | };
82 |
83 | render() {
84 | const { isTruncated, ellipsis, children } = this.props;
85 |
86 | let newChildren = children;
87 | if (isTruncated) {
88 | newChildren = addEllipsis(children, ellipsis);
89 | }
90 |
91 | return (this.containerRef = ref)}>{newChildren}
;
92 | }
93 | }
94 |
95 | export default ResultBox;
96 |
--------------------------------------------------------------------------------
/src/treeUtils.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from "react";
3 |
4 | type visitorCallback = (
5 | Node: React.Node,
6 | parent: React.Node,
7 | level: number,
8 | ) => void;
9 |
10 | type visitorObject = {|
11 | enter?: visitorCallback,
12 | exit?: visitorCallback,
13 | |};
14 |
15 | export function isIterableChildren(node: React.Node): boolean %checks {
16 | return Array.isArray(node) || typeof node === "string";
17 | }
18 |
19 | export function getChildren(node: React.Node): ?Array {
20 | if (
21 | node &&
22 | React.isValidElement(node) &&
23 | // in the next line node.props couldn't be access
24 | // because props is missing in React.Portal
25 | // $FlowFixMe
26 | node.props &&
27 | node.props.children
28 | ) {
29 | // `toArray` will flatten the array. We need that for consistency
30 | return React.Children.toArray(node.props.children);
31 | }
32 |
33 | // no children
34 | return null;
35 | }
36 |
37 | // function to traverse only the right-most branch of the tree
38 | // the visitor callback receives `node` which is:
39 | // 1. `currentNode.props.children` if currentNode is an element
40 | // 2. `currentNode` otherwise (string, in most cases).
41 | // this means that `node` is expected to be either `Array` or `string`.
42 | // TODO: Are we running into unexpected edge cases when currentNode is e.g `boolean`?
43 | export function traverseToTheRight(tree: React.Node, visitor: visitorObject) {
44 | function traverseChildren(
45 | node: React.Node,
46 | parent: React.Node,
47 | levelCount: number,
48 | ) {
49 | let children = getChildren(node);
50 |
51 | let nodeToVisit = children ? children : node;
52 |
53 | if (visitor.enter) {
54 | visitor.enter(nodeToVisit, parent, levelCount);
55 | }
56 |
57 | if (Array.isArray(children)) {
58 | if (children.length > 0) {
59 | traverseChildren(children[children.length - 1], node, levelCount + 1);
60 | }
61 | }
62 |
63 | if (visitor.exit) {
64 | visitor.exit(node, parent, levelCount);
65 | }
66 | }
67 |
68 | traverseChildren(<>{tree}>, null, 0);
69 | }
70 |
71 | // traverses the right-most branch and return the number of
72 | // children on a specific level
73 | export function getLevelInfo(tree: React.Node, level: number) {
74 | let result = { length: 0 };
75 | traverseToTheRight(tree, {
76 | enter(node, parent, currentLevel) {
77 | if (currentLevel === level) {
78 | const children = getChildren(node);
79 | let length = 0;
80 | if (children) {
81 | length = children.length;
82 | } else if (isIterableChildren(node)) {
83 | length = node.length;
84 | }
85 | result = { length };
86 | }
87 | },
88 | });
89 | return result;
90 | }
91 |
92 | export function truncateLevel(
93 | tree: React.Node,
94 | level: number,
95 | index: number = -1,
96 | ) {
97 | function truncateNode(node: React.Node, currentLevel: number): ?React.Node {
98 | let newChildren = React.isValidElement(node) ? getChildren(node) : node;
99 |
100 | if (currentLevel === level) {
101 | if (newChildren && isIterableChildren(newChildren)) {
102 | newChildren = newChildren.slice(0, index);
103 | }
104 | } else if (Array.isArray(newChildren) && newChildren.length > 0) {
105 | newChildren = [
106 | ...newChildren.slice(0, -1),
107 | truncateNode(newChildren[newChildren.length - 1], currentLevel + 1),
108 | ];
109 | }
110 |
111 | if (React.isValidElement(node)) {
112 | // if children is an array
113 | if (Array.isArray(newChildren)) {
114 | // remove falsy children
115 | newChildren = newChildren.filter(Boolean);
116 | // discards node if children is empty (leaf node)
117 | if (newChildren.filter(Boolean).length < 1) {
118 | return null;
119 | }
120 | return React.cloneElement(
121 | // $FlowFixMe node is validated as a valid element above
122 | node,
123 | { children: undefined },
124 | ...newChildren,
125 | );
126 | }
127 |
128 | // no children present
129 | if (!newChildren) {
130 | return null;
131 | }
132 |
133 | // children is a single element
134 | return React.cloneElement(
135 | // $FlowFixMe
136 | node,
137 | { children: undefined },
138 | newChildren,
139 | );
140 | }
141 |
142 | return newChildren;
143 | }
144 |
145 | const truncatedRoot = truncateNode(<>{tree}>, 0);
146 | // $FlowFixMe
147 | return truncatedRoot && truncatedRoot.props
148 | ? truncatedRoot.props.children
149 | : null;
150 | }
151 |
152 | export function cacheToTree(
153 | currentTree: React.Node,
154 | currentCache: Array,
155 | ) {
156 | return currentCache.reduce((previousTree, offset, level) => {
157 | return truncateLevel(previousTree, level, offset);
158 | }, currentTree);
159 | }
160 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from "react";
3 |
4 | export type onTruncateEvent = (isTruncated: boolean) => void;
5 |
6 | export type TruncatedComponentState = {|
7 | previousTree: React.Node,
8 | currentTree: React.Node,
9 | isTruncationCompleted: boolean,
10 | isTruncated: boolean,
11 | level: number,
12 | // for binary-search
13 | startOffset: number,
14 | truncatedAt: ?number,
15 | |};
16 |
17 | export type TruncatedComponentProps = {|
18 | numberOfLines?: ?number,
19 | children: React.Node,
20 | ellipsis: React.Node,
21 | onTruncate: onTruncateEvent,
22 | cacheKey?: any,
23 | lineHeight: number,
24 | |};
25 |
26 | export type LevelInfoType = {| length: number |};
27 |
--------------------------------------------------------------------------------
/src/updater.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import * as React from "react";
3 | import type { LevelInfoType, TruncatedComponentState } from "./types";
4 |
5 | import { truncateLevel, cacheToTree } from "./treeUtils";
6 |
7 | const cache: Map> = new Map();
8 |
9 | function getCachedTree(cacheKey, tree) {
10 | if (cacheKey) {
11 | const isCached = cache.has(cacheKey);
12 | if (isCached) {
13 | const cacheItem = cache.get(cacheKey);
14 | if (cacheItem && cacheItem.length) {
15 | const nextTree = cacheToTree(tree, cache[cacheKey]);
16 | return nextTree;
17 | }
18 | // no need to truncate. Return tree not truncated
19 | return tree;
20 | } else {
21 | cache.set(cacheKey, []);
22 | }
23 | }
24 | return null;
25 | }
26 |
27 | const updater = {
28 | getInitialState(
29 | children: React.Node,
30 | cacheKey?: any,
31 | ): TruncatedComponentState {
32 | const cachedTree = getCachedTree(cacheKey, children);
33 |
34 | let currentTree = children;
35 | let isTruncationCompleted = false;
36 | let isTruncated = false;
37 |
38 | if (cachedTree) {
39 | currentTree = cachedTree;
40 | isTruncationCompleted = true;
41 |
42 | isTruncated = cachedTree !== children;
43 | }
44 |
45 | return {
46 | currentTree,
47 | previousTree: children,
48 | isTruncationCompleted,
49 | isTruncated,
50 | level: 0,
51 | // for binary-search
52 | startOffset: 0,
53 | truncatedAt: null,
54 | };
55 | },
56 |
57 | truncationCompleted() {
58 | return { isTruncationCompleted: true };
59 | },
60 |
61 | truncateTotheMidddle: (levelInfo: LevelInfoType) =>
62 | function innerTruncateToTheMiddleUpdater({
63 | startOffset,
64 | currentTree,
65 | level,
66 | }: TruncatedComponentState) {
67 | const middle = Math.floor((startOffset + levelInfo.length) / 2);
68 | return {
69 | isTruncated: true,
70 | currentTree: truncateLevel(currentTree, level, middle),
71 | previousTree: currentTree,
72 | truncatedAt: middle,
73 | };
74 | },
75 |
76 | goToDeeperLevel({
77 | level: currentLevel,
78 | previousTree,
79 | }: TruncatedComponentState) {
80 | return {
81 | currentTree: previousTree,
82 | level: currentLevel + 1,
83 | startOffset: 0,
84 | truncatedAt: null,
85 | };
86 | },
87 |
88 | setStartOffset: ({ previousTree, truncatedAt }: TruncatedComponentState) => {
89 | return {
90 | currentTree: previousTree,
91 | startOffset: truncatedAt,
92 | };
93 | },
94 |
95 | setTruncatedTree: (nextTree: React.Node) =>
96 | function innerSetTruncatedTreeUpdater({
97 | currentTree,
98 | }: TruncatedComponentState) {
99 | return {
100 | currentTree: nextTree,
101 | isTruncationCompleted: true,
102 | isTruncated: true,
103 | };
104 | },
105 |
106 | clearCache(cacheKey?: any) {
107 | if (cache.has(cacheKey)) {
108 | cache.delete(cacheKey);
109 | }
110 | },
111 |
112 | updateLevelCache(cacheKey?: any, level: number, truncatedAt: number) {
113 | if (cache.has(cacheKey)) {
114 | const prevCache = cache.get(cacheKey);
115 | // $FlowFixMe map.has() above should be enough. This is a flow bug I guess.
116 | prevCache[level] = truncatedAt;
117 | }
118 | },
119 | };
120 |
121 | export default updater;
122 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function didContentChange(currentProps, nextProps) {
2 | return (
3 | currentProps.ellipsis !== nextProps.ellipsis ||
4 | currentProps.children !== nextProps.children
5 | );
6 | }
7 |
--------------------------------------------------------------------------------
/website/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | [
5 | "babel-plugin-styled-components",
6 | {
7 | "ssr": true,
8 | "displayName": true,
9 | "preprocess": false
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | # Example project
2 |
--------------------------------------------------------------------------------
/website/next.config.js:
--------------------------------------------------------------------------------
1 | // next.config.js
2 | const { PHASE_PRODUCTION_SERVER } =
3 | process.env.NODE_ENV === "development"
4 | ? {}
5 | : !process.env.NOW_REGION
6 | ? require("next/constants")
7 | : require("next-server/constants");
8 |
9 | module.exports = (phase, ...rest) => {
10 | if (phase === PHASE_PRODUCTION_SERVER) {
11 | // Config used to run in production.
12 | return {};
13 | }
14 |
15 | const withPlugins = require("next-compose-plugins");
16 | const css = require("@zeit/next-css");
17 | const mdx = require("@zeit/next-mdx");
18 |
19 | const withMDX = mdx({
20 | extension: /\.(md|mdx)$/,
21 | options: {
22 | hastPlugins: [require("@mapbox/rehype-prism")],
23 | },
24 | });
25 |
26 | let config = withPlugins([[withMDX], [css]], {
27 | target: "serverless",
28 | })(phase, ...rest);
29 |
30 | return config;
31 | };
32 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-truncated-component-example",
3 | "homepage": "https://juangl.github.io/react-truncated-component",
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "private": true,
7 | "dependencies": {
8 | "@mapbox/rehype-prism": "^0.3.0",
9 | "@mdx-js/mdx": "^1.0.20",
10 | "@zeit/next-css": "^1.0.1",
11 | "@zeit/next-mdx": "^1.2.0",
12 | "next": "^9.3.2",
13 | "next-compose-plugins": "^2.2.0",
14 | "prop-types": "^15.6.2",
15 | "react": "^16.8.6",
16 | "react-dom": "^16.8.6",
17 | "react-scripts": "^3.0.1",
18 | "react-truncated-component": "^1.0.4",
19 | "react-typography": "^0.16.18",
20 | "styled-components": "^4.1.3",
21 | "typography": "^0.16.18",
22 | "typography-plugin-code": "^0.16.18",
23 | "typography-theme-stern-grove": "^0.16.18"
24 | },
25 | "scripts": {
26 | "start": "next start",
27 | "build": "next build",
28 | "dev": "next",
29 | "test": "SKIP_PREFLIGHT_CHECK=true react-scripts test",
30 | "eject": "react-scripts eject"
31 | },
32 | "eslintConfig": {
33 | "extends": "react-app"
34 | },
35 | "browserslist": [
36 | ">0.2%",
37 | "not dead",
38 | "not ie <= 11",
39 | "not op_mini all"
40 | ],
41 | "devDependencies": {
42 | "babel-plugin-styled-components": "^1.10.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/website/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Document, { Head, Main, NextScript } from "next/document";
3 | import { ServerStyleSheet, createGlobalStyle } from "styled-components";
4 | import { TypographyStyle, GoogleFont } from "react-typography";
5 | import Typography from "typography";
6 | import CodePlugin from "typography-plugin-code";
7 | import SternGroveTheme from "typography-theme-stern-grove";
8 |
9 | SternGroveTheme.plugins = [
10 | new CodePlugin(),
11 | () => ({ "p:last-child": { marginBottom: 0 } }),
12 | ];
13 |
14 | const typography = new Typography(SternGroveTheme);
15 |
16 | const GlobalStyle = createGlobalStyle`
17 | html {
18 | box-sizing: border-box;
19 | }
20 | *,
21 | *:before,
22 | *:after {
23 | box-sizing: inherit;
24 | }
25 |
26 | body {
27 | margin: 0;
28 | padding: 0;
29 | }
30 | `;
31 |
32 | export default class MyDocument extends Document {
33 | static getInitialProps({ renderPage }) {
34 | const sheet = new ServerStyleSheet();
35 | const page = renderPage(App => props =>
36 | sheet.collectStyles( ),
37 | );
38 | const styleTags = sheet.getStyleElement();
39 | return { ...page, styleTags };
40 | }
41 |
42 | render() {
43 | return (
44 |
45 |
46 |
50 |
51 |
52 |
53 | {this.props.styleTags}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/website/pages/index/docs.md:
--------------------------------------------------------------------------------
1 | # How it works
2 |
3 | This library works by traversing the React tree (unlike other libraries that directly manipulate the DOM tree) to find the longest text string that fits in the number of lines specified. It uses a binary search-like algorithm to speed up the proccess.
4 |
5 | The number of lines being rendered is measured based on the line-height.
6 |
7 | # Install
8 |
9 | ```bash
10 | npm install --save react-truncated-component
11 | ```
12 |
13 | # Usage
14 |
15 | The following code is a minimal example of how your implementation would look like. To see a more advanced example go to [this page source code](https://github.com/juangl/react-truncated-component/blob/master/website/pages/index/liveResult.js).
16 |
17 | ```javascript
18 | function TruncatedText() {
19 | return (
20 |
21 | {/*
22 |
23 |
24 | put your long text right here
25 |
26 |
27 | */}
28 |
29 | );
30 | }
31 | ```
32 |
33 | # API
34 |
35 | **numberOfLines: ?number**
36 | it can be a number of null if you want disable the truncation.
37 |
38 | **children: React.Node (Required)**
39 | This should be a React tree that reflects the document format. Direct children are considered to be paragraphs. See the craveats sections to know more about this.
40 |
41 | **lineHeight: number (Required)**
42 | `line-height` in pixels.
43 |
44 | **ellipsis: ?React.Node**
45 | It can be anything that can be render. The default value is `...`.
46 |
47 | **onTruncate: function(isTruncated: boolean)**
48 | A callback function. Receives a boolean value that is `true` when the content is truncated, `false` if the content fits without truncate it.
49 |
50 | **cacheKey: ?string**
51 | If cacheKey is passed, stores lightweight cache. Useful if you're using it in a virtualized list and you want to make sure that the truncation is calculated only once.
52 |
53 | # Caveats
54 |
55 | If you're running into some strange behavior, read the next section to make sure that you're using the library properly.
56 |
57 | - ** In order to support paragraphs, direct children are considered to be paragraphs.** If you don't need paragraphs just make sure to wrap your content in a single paragraph.
58 | - ** Use margin to separate paragraphs.** If you using padding instead, the padding will be measured as part of the content since we are measuring with `clientHeight`.
59 | - **Children should reflect the document format.** all the text that you want to truncate must be children of an element.
60 |
61 | ```javascript
62 |
63 |
64 | {/* ✅ the following children is a valid because all the text is children of an element */}
65 |
66 | the rest of the content... go to somewhere
67 |
68 |
69 | {/* ✅ you can call functions too that returns a valid children format*/}
70 | {renderParagraph()}
71 |
72 |
73 | {/* 🚫 the next children won't be truncated propertly
74 | bacause the property name is not `children` but `value`*/ }
75 |
76 |
77 | {/* 🚫 the next component won't be truncated truncate
78 | because we can't see inside its render function */}
79 |
80 |
81 |
82 | ```
83 |
--------------------------------------------------------------------------------
/website/pages/index/docsContainer.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const DocsContainer = styled.div`
4 | width: 100%;
5 | max-width: 1024px;
6 | margin: 0 auto;
7 | `;
8 |
9 | export default DocsContainer;
10 |
--------------------------------------------------------------------------------
/website/pages/index/ellipsis.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Button = styled.button`
5 | background: none;
6 | color: #07e;
7 | border: none;
8 | padding: 0;
9 | font: inherit;
10 | cursor: pointer;
11 | text-decoration: none;
12 | `;
13 |
14 | function Ellipsis(props) {
15 | return ...see more. ;
16 | }
17 |
18 | export default Ellipsis;
19 |
--------------------------------------------------------------------------------
/website/pages/index/githubLogo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function GitHub(props) {
4 | return (
5 |
11 | GitHub icon
12 |
13 |
14 | );
15 | }
16 |
17 | export default GitHub;
18 |
--------------------------------------------------------------------------------
/website/pages/index/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import Head from "next/head";
3 |
4 | import "./prism.css";
5 |
6 | import TopBar from "./topBar";
7 | import Docs from "./docs.md";
8 | import IntroContainer from "./introContainer";
9 | import Logo from "./logo";
10 | import IntroTitle from "./introTitle";
11 | import LiveResult from "./liveResult";
12 | import IntroContentContainer from "./introContentContainer";
13 | import DocsContainer from "./docsContainer";
14 |
15 | export default class App extends Component {
16 | render() {
17 | return (
18 | <>
19 |
20 |
21 |
22 | React Truncated Component
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | >
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/website/pages/index/introContainer.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Container = styled.div`
4 | display: flex;
5 | background: linear-gradient(15deg, #ffab65, #4ecdc4);
6 | background-repeat: no-repeat;
7 | background-attachment: fixed;
8 | min-height: 700px;
9 | width: 100%;
10 | color: #fff;
11 | `;
12 |
13 | export default Container;
14 |
--------------------------------------------------------------------------------
/website/pages/index/introContentContainer.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const LiveResultContainer = styled.div`
4 | width: 100%;
5 | max-width: 1024px;
6 | margin: 0 auto;
7 | `;
8 |
9 | export default LiveResultContainer;
10 |
--------------------------------------------------------------------------------
/website/pages/index/introTitle.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Container = styled.div`
5 | margin-top: 40px;
6 | margin-bottom: 40px;
7 | text-align: center;
8 | text-shadow: rgba(20, 20, 20, 0.1) 0.09rem 0.09em 0.9rem;
9 | `;
10 |
11 | const Title = styled.h1`
12 | font-size: 1.5rem;
13 | margin: 0.5rem;
14 | color: #fff;
15 | `;
16 |
17 | const Subtitle = styled.h2`
18 | font-size: 1.1rem;
19 | font-weight: 400;
20 | margin: 0;
21 | `;
22 |
23 | function IntroTitle() {
24 | return (
25 |
26 | React component to truncate text
27 | With format and paragraphs support included
28 |
29 | );
30 | }
31 |
32 | export default IntroTitle;
33 |
--------------------------------------------------------------------------------
/website/pages/index/liveResult.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import dynamic from "next/dynamic";
3 | import styled from "styled-components";
4 |
5 | import TruncationEnabled from "./truncationEnabled";
6 | import LiveSettingsContainer from "./liveSettingsContainer";
7 | import Ellipsis from "./ellipsis";
8 | import NumberOfLinesInput from "./numberOfLinesInput";
9 | import LiveResultBox from "./liveResultBox";
10 | import mediaTemplates from "../../utils/mediaTemplates";
11 |
12 | const ReactTruncateFormat = dynamic(() => import("react-truncated-component"), {
13 | ssr: false,
14 | loading: () => (
15 |
22 | ),
23 | });
24 |
25 | const Container = styled.div`
26 | display: flex;
27 | flex-direction: row;
28 | justify-content: flex-start;
29 | margin-bottom: 30px;
30 |
31 | ${mediaTemplates.phone`
32 | flex-direction: column;
33 | `}
34 | `;
35 |
36 | class LiveResult extends React.Component {
37 | state = {
38 | numberOfLines: 5,
39 | isTruncationEnabled: true,
40 | };
41 |
42 | onSeeMoreClick = () => {
43 | this.setState({
44 | isTruncationEnabled: false,
45 | });
46 | };
47 |
48 | render() {
49 | return (
50 |
51 |
52 | {
56 | this.setState({
57 | numberOfLines: e.target.value,
58 | });
59 | }}
60 | />
61 |
62 | {
65 | this.setState({
66 | isTruncationEnabled: e.target.checked,
67 | });
68 | }}
69 | />
70 |
71 |
72 |
73 | }
75 | numberOfLines={
76 | this.state.isTruncationEnabled ? this.state.numberOfLines : null
77 | }
78 | lineHeight={23}
79 | >
80 |
81 |
82 | Nothomyrmecia
83 |
84 | , also known as the dinosaur ant or dawn ant , is a
85 | rare{" "}
86 |
91 | genus
92 | {" "}
93 | of{" "}
94 |
99 | ants
100 | {" "}
101 | consisting of a single{" "}
102 |
107 | species
108 |
109 | ,{" "}
110 |
111 | Nothomyrmecia macrops
112 |
113 | . These ants live in South Australia, nesting in old-growth{" "}
114 |
119 | mallee
120 | {" "}
121 | woodland and{" "}
122 |
123 |
128 | Eucalyptus
129 |
130 | {" "}
131 | woodland. The full distribution of Nothomyrmecia has never
132 | been assessed, and it is unknown how widespread the species truly
133 | is; its potential range may be wider if it does favour{" "}
134 |
139 | old-growth
140 | {" "}
141 | mallee woodland.
142 |
143 |
144 | Possible threats to its survival include habitat destruction and
145 | climate change. Nothomyrmecia is most active when it is
146 | cold because workers encounter fewer competitors and predators
147 | such as{" "}
148 |
149 |
154 | Camponotus
155 |
156 | {" "}
157 | and{" "}
158 |
159 |
164 | Iridomyrmex
165 |
166 |
167 | , and it also increases hunting success. Thus, the increase of
168 | temperature may prevent them from foraging and very few areas
169 | would be suitable for the ant to live in. As a result, the{" "}
170 |
174 | IUCN
175 | {" "}
176 | lists the ant as{" "}
177 |
182 | Critically Endangered
183 |
184 | .
185 |
186 |
187 |
188 |
189 | );
190 | }
191 | }
192 |
193 | export default LiveResult;
194 |
--------------------------------------------------------------------------------
/website/pages/index/liveResultBox.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import mediaTemplates from "../../utils/mediaTemplates";
3 |
4 | const LiveResultBox = styled.div`
5 | width: 500px;
6 | background-color: #fff;
7 | padding: 10px 20px;
8 | color: #000;
9 | box-shadow: rgba(20, 20, 20, 0.27) 0.0555556rem 0.0555556rem 1.11111rem;
10 | margin: 0 auto;
11 |
12 | ${mediaTemplates.smallScreen`
13 | width: auto;
14 | flex: 1;
15 | margin-right: 10px;
16 | `}
17 |
18 | ${mediaTemplates.phone`
19 | margin-right: 0;
20 | `}
21 | `;
22 |
23 | export default LiveResultBox;
24 |
--------------------------------------------------------------------------------
/website/pages/index/liveSettingsContainer.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import mediaTemplates from "../../utils/mediaTemplates";
3 |
4 | const LiveSettingsContainer = styled.div`
5 | position: absolute;
6 | display: flex;
7 | align-items: flex-end;
8 | flex-direction: column;
9 | margin: 10px;
10 |
11 | ${mediaTemplates.smallScreen`
12 | position: static;
13 | `}
14 | `;
15 |
16 | export default LiveSettingsContainer;
17 |
--------------------------------------------------------------------------------
/website/pages/index/logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const LogoImg = styled.img.attrs({
5 | src: "/static/logo.png",
6 | alt: "React Truncate Component",
7 | })`
8 | width: 251px;
9 | height: 83px;
10 | margin-bottom: 0;
11 | `;
12 |
13 | const Container = styled.div`
14 | flex: 1;
15 | display: flex;
16 | justify-content: center;
17 | margin-top: 90px;
18 | `;
19 |
20 | function Logo() {
21 | return (
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default Logo;
29 |
--------------------------------------------------------------------------------
/website/pages/index/numberOfLinesInput.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Container = styled.div`
5 | margin-bottom: 5px;
6 | display: flex;
7 | flex-direction: column;
8 | `;
9 |
10 | const Input = styled.input.attrs({
11 | name: "number-of-lines",
12 | type: "range",
13 | min: "0",
14 | max: "13",
15 | })``;
16 |
17 | function NumberOfLinesInput(props) {
18 | return (
19 |
20 | number of lines: {props.value}
21 |
22 |
23 | );
24 | }
25 |
26 | export default NumberOfLinesInput;
27 |
--------------------------------------------------------------------------------
/website/pages/index/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.15.0
2 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */
3 | /**
4 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
5 | * Based on https://github.com/chriskempson/tomorrow-theme
6 | * @author Rose Pritchard
7 | */
8 |
9 | code[class*="language-"],
10 | pre[class*="language-"] {
11 | color: #ccc;
12 | background: none;
13 | font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.5;
20 |
21 | -moz-tab-size: 4;
22 | -o-tab-size: 4;
23 | tab-size: 4;
24 |
25 | -webkit-hyphens: none;
26 | -moz-hyphens: none;
27 | -ms-hyphens: none;
28 | hyphens: none;
29 | }
30 |
31 | /* Code blocks */
32 | pre[class*="language-"] {
33 | padding: 1em;
34 | margin: 0.5em 0;
35 | overflow: auto;
36 | }
37 |
38 | :not(pre) > code[class*="language-"],
39 | pre[class*="language-"] {
40 | background: #2d2d2d;
41 | }
42 |
43 | /* Inline code */
44 | :not(pre) > code[class*="language-"] {
45 | padding: 0.1em;
46 | border-radius: 0.3em;
47 | white-space: normal;
48 | }
49 |
50 | .token.comment,
51 | .token.block-comment,
52 | .token.prolog,
53 | .token.doctype,
54 | .token.cdata {
55 | color: #999;
56 | }
57 |
58 | .token.punctuation {
59 | color: #ccc;
60 | }
61 |
62 | .token.tag,
63 | .token.attr-name,
64 | .token.namespace,
65 | .token.deleted {
66 | color: #e2777a;
67 | }
68 |
69 | .token.function-name {
70 | color: #6196cc;
71 | }
72 |
73 | .token.boolean,
74 | .token.number,
75 | .token.function {
76 | color: #f08d49;
77 | }
78 |
79 | .token.property,
80 | .token.class-name,
81 | .token.constant,
82 | .token.symbol {
83 | color: #f8c555;
84 | }
85 |
86 | .token.selector,
87 | .token.important,
88 | .token.atrule,
89 | .token.keyword,
90 | .token.builtin {
91 | color: #cc99cd;
92 | }
93 |
94 | .token.string,
95 | .token.char,
96 | .token.attr-value,
97 | .token.regex,
98 | .token.variable {
99 | color: #7ec699;
100 | }
101 |
102 | .token.operator,
103 | .token.entity,
104 | .token.url {
105 | color: #67cdcc;
106 | }
107 |
108 | .token.important,
109 | .token.bold {
110 | font-weight: bold;
111 | }
112 | .token.italic {
113 | font-style: italic;
114 | }
115 |
116 | .token.entity {
117 | cursor: help;
118 | }
119 |
120 | .token.inserted {
121 | color: green;
122 | }
123 |
--------------------------------------------------------------------------------
/website/pages/index/topBar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import GithubLogo from "./githubLogo";
5 |
6 | const Container = styled.div`
7 | position: fixed;
8 | z-index: 9999;
9 | width: 100%;
10 | background-color: rgba(0, 0, 0, 0.1);
11 | `;
12 |
13 | const Nav = styled.nav`
14 | padding: 15px 30px;
15 | display: flex;
16 | max-width: 1024px;
17 | margin: 0 auto;
18 | `;
19 |
20 | const StyledLink = styled.a`
21 | color: #fff;
22 | text-decoration: none;
23 | `;
24 |
25 | const RightBox = styled.div`
26 | margin-left: auto;
27 | `;
28 |
29 | const StyledGithubLogo = styled(GithubLogo)`
30 | vertical-align: middle;
31 | width: 25px;
32 | height: 25px;
33 | fill: #fff;
34 | `;
35 |
36 | const TopBar = () => (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 |
48 | export default TopBar;
49 |
--------------------------------------------------------------------------------
/website/pages/index/truncationEnabled.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Input = styled.input.attrs({
5 | type: "checkbox",
6 | name: "truncation-enabled",
7 | })``;
8 |
9 | function TruncationEnabled(props) {
10 | return (
11 |
12 | Truncation enabled:
13 |
14 | );
15 | }
16 |
17 | export default TruncationEnabled;
18 |
--------------------------------------------------------------------------------
/website/static/7xT19rLg5W.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juangl/react-truncated-component/13cffcec1001bb4f60961459dcb517f814fba547/website/static/7xT19rLg5W.gif
--------------------------------------------------------------------------------
/website/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juangl/react-truncated-component/13cffcec1001bb4f60961459dcb517f814fba547/website/static/favicon.ico
--------------------------------------------------------------------------------
/website/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
17 |
18 | React Truncated Component
19 |
20 |
21 |
22 | You need to enable JavaScript to run this app.
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/website/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/juangl/react-truncated-component/13cffcec1001bb4f60961459dcb517f814fba547/website/static/logo.png
--------------------------------------------------------------------------------
/website/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "react-truncated-component",
3 | "name": "react-truncated-component",
4 | "start_url": "./index.html",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff"
8 | }
9 |
--------------------------------------------------------------------------------
/website/utils/mediaTemplates.js:
--------------------------------------------------------------------------------
1 | import { css } from "styled-components";
2 |
3 | const sizes = {
4 | desktop: 1000,
5 | smallScreen: 900,
6 | tablet: 768,
7 | phone: 576,
8 | };
9 |
10 | // Iterate through the sizes and create a media template
11 | const mediaTemplates = Object.keys(sizes).reduce((acc, label) => {
12 | acc[label] = (...args) => css`
13 | @media (max-width: ${sizes[label] / 16}em) {
14 | ${css(...args)}
15 | }
16 | `;
17 |
18 | return acc;
19 | }, {});
20 |
21 | export default mediaTemplates;
22 |
--------------------------------------------------------------------------------