@versini/ui-truncate 5.1.8 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +37 -37
- package/dist/index.js +206 -11
- package/package.json +9 -9
- package/dist/components/Truncate/Truncate.js +0 -95
package/dist/index.d.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export {
|
|
1
|
+
import { JSX } from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
export declare const Truncate: ({ children, length, mode, focusMode, enableRichTruncation, }: TruncateProps) => JSX.Element;
|
|
4
|
+
|
|
5
|
+
declare type TruncateProps = {
|
|
6
|
+
/**
|
|
7
|
+
* The children to render.
|
|
8
|
+
*/
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* The maximum length of the text.
|
|
12
|
+
* @default 200
|
|
13
|
+
*/
|
|
14
|
+
length?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Opt-in flag to enable truncation of rich React nodes (HTML / nested elements).
|
|
17
|
+
* When false (default), only plain string children are truncated; non-string
|
|
18
|
+
* children are rendered as-is. When true, the component will traverse the
|
|
19
|
+
* React node tree, measure the concatenated textual content, and rebuild a
|
|
20
|
+
* structurally equivalent subtree truncated at the calculated boundary.
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
enableRichTruncation?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The mode of Button. This will change the color of the Button.
|
|
26
|
+
* @default "system"
|
|
27
|
+
*/
|
|
28
|
+
mode?: "dark" | "light" | "system" | "alt-system";
|
|
29
|
+
/**
|
|
30
|
+
* The type of focus for the Button. This will change the color
|
|
31
|
+
* of the focus ring around the Button.
|
|
32
|
+
* @default "system"
|
|
33
|
+
*/
|
|
34
|
+
focusMode?: "dark" | "light" | "system" | "alt-system";
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,212 @@
|
|
|
1
|
-
import { Truncate as o } from "./components/Truncate/Truncate.js";
|
|
2
1
|
/*!
|
|
3
|
-
@versini/ui-truncate v5.
|
|
2
|
+
@versini/ui-truncate v5.2.0
|
|
4
3
|
© 2025 gizmette.com
|
|
5
4
|
*/
|
|
6
5
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
6
|
+
if (!window.__VERSINI_UI_TRUNCATE__) {
|
|
7
|
+
window.__VERSINI_UI_TRUNCATE__ = {
|
|
8
|
+
version: "5.2.0",
|
|
9
|
+
buildTime: "11/04/2025 03:45 PM EST",
|
|
10
|
+
homepage: "https://github.com/aversini/ui-components",
|
|
11
|
+
license: "MIT",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// nothing to declare officer
|
|
14
16
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
+
import { Button } from "@versini/ui-button";
|
|
20
|
+
import { isValidElement, useMemo, useState } from "react";
|
|
21
|
+
|
|
22
|
+
;// CONCATENATED MODULE: external "react/jsx-runtime"
|
|
23
|
+
|
|
24
|
+
;// CONCATENATED MODULE: external "@versini/ui-button"
|
|
25
|
+
|
|
26
|
+
;// CONCATENATED MODULE: external "react"
|
|
27
|
+
|
|
28
|
+
;// CONCATENATED MODULE: ./src/components/Truncate/utilities.ts
|
|
29
|
+
const DEFAULT_LENGTH = 200;
|
|
30
|
+
/**
|
|
31
|
+
* This function will truncate the string at the last word boundary
|
|
32
|
+
* before the ideal length.
|
|
33
|
+
* - If ideal length ends up in the middle of a word, truncate at the next space.
|
|
34
|
+
* - If ideal length ends up on a space, truncate at the ideal length.
|
|
35
|
+
*/ const truncate = ({ string, idealLength = DEFAULT_LENGTH })=>{
|
|
36
|
+
const strLength = string.length;
|
|
37
|
+
if (strLength <= idealLength) {
|
|
38
|
+
return {
|
|
39
|
+
string: string,
|
|
40
|
+
isTruncated: false
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const originalTrunc = string.charAt(idealLength);
|
|
44
|
+
if (originalTrunc === " ") {
|
|
45
|
+
return {
|
|
46
|
+
string: string.slice(0, idealLength),
|
|
47
|
+
isTruncated: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const nextSpace = string.slice(idealLength).search(" ");
|
|
51
|
+
return {
|
|
52
|
+
string: string.slice(0, idealLength + nextSpace),
|
|
53
|
+
isTruncated: true
|
|
54
|
+
};
|
|
17
55
|
};
|
|
56
|
+
|
|
57
|
+
;// CONCATENATED MODULE: ./src/components/Truncate/Truncate.tsx
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Recursively extract concatenated textual content from arbitrary React nodes.
|
|
64
|
+
* This is defined outside the component to keep a stable reference and avoid
|
|
65
|
+
* unnecessary re-creation on each render.
|
|
66
|
+
*
|
|
67
|
+
* NOTE: we are keeping this function here instead of utilities because it
|
|
68
|
+
* relies on React.
|
|
69
|
+
*
|
|
70
|
+
*/ const extractText = (node)=>{
|
|
71
|
+
if (node == null || typeof node === "boolean") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
75
|
+
return String(node);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(node)) {
|
|
78
|
+
return node.map(extractText).join("");
|
|
79
|
+
}
|
|
80
|
+
if (/*#__PURE__*/ isValidElement(node)) {
|
|
81
|
+
return extractText(node.props.children);
|
|
82
|
+
}
|
|
83
|
+
/* c8 ignore next 1 */ return ""; // Unsupported node type fallback
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Block-level HTML tag detection (upper-case because React string element types
|
|
87
|
+
* are lowercase).
|
|
88
|
+
*/ const BLOCK_TAGS = new Set([
|
|
89
|
+
"P",
|
|
90
|
+
"DIV",
|
|
91
|
+
"UL",
|
|
92
|
+
"OL",
|
|
93
|
+
"LI",
|
|
94
|
+
"SECTION",
|
|
95
|
+
"ARTICLE",
|
|
96
|
+
"HEADER",
|
|
97
|
+
"FOOTER",
|
|
98
|
+
"MAIN",
|
|
99
|
+
"ASIDE",
|
|
100
|
+
"TABLE",
|
|
101
|
+
"THEAD",
|
|
102
|
+
"TBODY",
|
|
103
|
+
"TFOOT",
|
|
104
|
+
"TR",
|
|
105
|
+
"TD",
|
|
106
|
+
"TH",
|
|
107
|
+
"BLOCKQUOTE",
|
|
108
|
+
"PRE",
|
|
109
|
+
"H1",
|
|
110
|
+
"H2",
|
|
111
|
+
"H3",
|
|
112
|
+
"H4",
|
|
113
|
+
"H5",
|
|
114
|
+
"H6"
|
|
115
|
+
]);
|
|
116
|
+
/**
|
|
117
|
+
* Detect whether a React node tree contains any block-level HTML elements. Used
|
|
118
|
+
* to decide if inline spacing (margin-left) should be suppressed when expanded
|
|
119
|
+
* rich content is displayed on its own line(s).
|
|
120
|
+
*/ const hasBlockLike = (node)=>{
|
|
121
|
+
if (node == null || typeof node === "boolean") {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(node)) {
|
|
128
|
+
return node.some(hasBlockLike);
|
|
129
|
+
}
|
|
130
|
+
if (/*#__PURE__*/ isValidElement(node)) {
|
|
131
|
+
const el = node;
|
|
132
|
+
if (typeof el.type === "string" && BLOCK_TAGS.has(el.type.toUpperCase())) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return hasBlockLike(el.props.children);
|
|
136
|
+
}
|
|
137
|
+
/* c8 ignore next 1 */ return false; // Unsupported node types treated as non-block for spacing
|
|
138
|
+
};
|
|
139
|
+
const Truncate = ({ children, length = 200, mode = "system", focusMode = "system", enableRichTruncation = false })=>{
|
|
140
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
141
|
+
const nonStringBypass = typeof children !== "string" && !enableRichTruncation;
|
|
142
|
+
const fullText = useMemo(()=>{
|
|
143
|
+
return typeof children === "string" ? children : extractText(children);
|
|
144
|
+
}, [
|
|
145
|
+
children
|
|
146
|
+
]);
|
|
147
|
+
const { string: truncatedText, isTruncated } = useMemo(()=>{
|
|
148
|
+
return truncate({
|
|
149
|
+
string: fullText,
|
|
150
|
+
idealLength: length
|
|
151
|
+
});
|
|
152
|
+
}, [
|
|
153
|
+
fullText,
|
|
154
|
+
length
|
|
155
|
+
]);
|
|
156
|
+
/**
|
|
157
|
+
* We flatten rich truncated output to raw text so styling doesn't cause the
|
|
158
|
+
* toggle button to wrap onto a new line.
|
|
159
|
+
*/ const showPlainTruncated = enableRichTruncation && isTruncated && !isExpanded;
|
|
160
|
+
const handleToggleExpanded = (e)=>{
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
setIsExpanded(!isExpanded);
|
|
163
|
+
};
|
|
164
|
+
if (nonStringBypass) {
|
|
165
|
+
return /*#__PURE__*/ jsx(Fragment, {
|
|
166
|
+
children: children
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
let displayContent;
|
|
170
|
+
if (!isTruncated) {
|
|
171
|
+
displayContent = children; // Nothing to truncate
|
|
172
|
+
} else if (isExpanded) {
|
|
173
|
+
displayContent = children; // Show full rich content
|
|
174
|
+
} else if (showPlainTruncated) {
|
|
175
|
+
displayContent = truncatedText; // Plain flattened version
|
|
176
|
+
} else {
|
|
177
|
+
displayContent = truncatedText; // String case
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Spacing strategy:
|
|
181
|
+
* - Always keep margin for classic string truncation (maintains original behavior).
|
|
182
|
+
* - Keep margin for collapsed flattened rich text (button inline with plain snippet).
|
|
183
|
+
* - When expanded AND rich truncation enabled: remove margin ONLY if the rich content
|
|
184
|
+
* tree actually contains a block-level element that likely puts the button on a new line.
|
|
185
|
+
*/ const contentHasBlock = enableRichTruncation ? hasBlockLike(children) : false;
|
|
186
|
+
const removeMargin = enableRichTruncation && isExpanded && contentHasBlock;
|
|
187
|
+
const needsInlineSpacing = !removeMargin;
|
|
188
|
+
return /*#__PURE__*/ jsxs("span", {
|
|
189
|
+
style: {
|
|
190
|
+
wordBreak: "break-word"
|
|
191
|
+
},
|
|
192
|
+
"data-testid": "truncate-root",
|
|
193
|
+
"aria-expanded": isTruncated ? isExpanded : undefined,
|
|
194
|
+
children: [
|
|
195
|
+
displayContent,
|
|
196
|
+
isTruncated && /*#__PURE__*/ jsx(Button, {
|
|
197
|
+
mode: mode,
|
|
198
|
+
focusMode: focusMode,
|
|
199
|
+
className: needsInlineSpacing ? "ml-2" : undefined,
|
|
200
|
+
size: "small",
|
|
201
|
+
onClick: handleToggleExpanded,
|
|
202
|
+
"aria-label": isExpanded ? "Show less" : "Show more",
|
|
203
|
+
children: isExpanded ? "less..." : "more..."
|
|
204
|
+
})
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
;// CONCATENATED MODULE: ./src/components/index.ts
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
export { Truncate };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-truncate",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build:check": "tsc",
|
|
23
|
-
"build:js": "
|
|
24
|
-
"build:types": "
|
|
25
|
-
"build": "npm-run-all --serial clean build:check build:js
|
|
23
|
+
"build:js": "rslib build",
|
|
24
|
+
"build:types": "echo 'Types now built with rslib'",
|
|
25
|
+
"build": "npm-run-all --serial clean build:check build:js",
|
|
26
26
|
"clean": "rimraf dist tmp",
|
|
27
|
-
"dev:js": "
|
|
28
|
-
"dev:types": "
|
|
29
|
-
"dev": "
|
|
27
|
+
"dev:js": "rslib build --watch",
|
|
28
|
+
"dev:types": "echo 'Types now watched with rslib'",
|
|
29
|
+
"dev": "rslib build --watch",
|
|
30
30
|
"lint": "biome lint src",
|
|
31
31
|
"lint:fix": "biome check src --write --no-errors-on-unmatched",
|
|
32
32
|
"prettier": "biome check --write --no-errors-on-unmatched",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@tailwindcss/typography": "0.5.19",
|
|
41
41
|
"@testing-library/jest-dom": "6.9.1",
|
|
42
|
-
"@versini/ui-button": "8.3.
|
|
42
|
+
"@versini/ui-button": "8.3.1",
|
|
43
43
|
"tailwindcss": "4.1.16"
|
|
44
44
|
},
|
|
45
45
|
"sideEffects": [
|
|
46
46
|
"**/*.css"
|
|
47
47
|
],
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "7484ad443b77ef31e52ae3a7d88b8129bc6cdf1d"
|
|
49
49
|
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { jsx as p, Fragment as H, jsxs as O } from "react/jsx-runtime";
|
|
2
|
-
import { Button as B } from "@versini/ui-button";
|
|
3
|
-
import { useState as I, useMemo as T, isValidElement as m } from "react";
|
|
4
|
-
const L = 200, D = ({
|
|
5
|
-
string: t,
|
|
6
|
-
idealLength: e = L
|
|
7
|
-
}) => {
|
|
8
|
-
if (t.length <= e)
|
|
9
|
-
return { string: t, isTruncated: !1 };
|
|
10
|
-
if (t.charAt(e) === " ")
|
|
11
|
-
return { string: t.slice(0, e), isTruncated: !0 };
|
|
12
|
-
const s = t.slice(e).search(" ");
|
|
13
|
-
return {
|
|
14
|
-
string: t.slice(0, e + s),
|
|
15
|
-
isTruncated: !0
|
|
16
|
-
};
|
|
17
|
-
}, o = (t) => t == null || typeof t == "boolean" ? "" : typeof t == "string" || typeof t == "number" ? String(t) : Array.isArray(t) ? t.map(o).join("") : m(t) ? o(t.props.children) : "", C = /* @__PURE__ */ new Set([
|
|
18
|
-
"P",
|
|
19
|
-
"DIV",
|
|
20
|
-
"UL",
|
|
21
|
-
"OL",
|
|
22
|
-
"LI",
|
|
23
|
-
"SECTION",
|
|
24
|
-
"ARTICLE",
|
|
25
|
-
"HEADER",
|
|
26
|
-
"FOOTER",
|
|
27
|
-
"MAIN",
|
|
28
|
-
"ASIDE",
|
|
29
|
-
"TABLE",
|
|
30
|
-
"THEAD",
|
|
31
|
-
"TBODY",
|
|
32
|
-
"TFOOT",
|
|
33
|
-
"TR",
|
|
34
|
-
"TD",
|
|
35
|
-
"TH",
|
|
36
|
-
"BLOCKQUOTE",
|
|
37
|
-
"PRE",
|
|
38
|
-
"H1",
|
|
39
|
-
"H2",
|
|
40
|
-
"H3",
|
|
41
|
-
"H4",
|
|
42
|
-
"H5",
|
|
43
|
-
"H6"
|
|
44
|
-
]), i = (t) => {
|
|
45
|
-
if (t == null || typeof t == "boolean" || typeof t == "string" || typeof t == "number")
|
|
46
|
-
return !1;
|
|
47
|
-
if (Array.isArray(t))
|
|
48
|
-
return t.some(i);
|
|
49
|
-
if (m(t)) {
|
|
50
|
-
const e = t;
|
|
51
|
-
return typeof e.type == "string" && C.has(e.type.toUpperCase()) ? !0 : i(e.props.children);
|
|
52
|
-
}
|
|
53
|
-
return !1;
|
|
54
|
-
}, N = ({
|
|
55
|
-
children: t,
|
|
56
|
-
length: e = 200,
|
|
57
|
-
mode: u = "system",
|
|
58
|
-
focusMode: f = "system",
|
|
59
|
-
enableRichTruncation: s = !1
|
|
60
|
-
}) => {
|
|
61
|
-
const [r, y] = I(!1), E = typeof t != "string" && !s, l = T(() => typeof t == "string" ? t : o(t), [t]), { string: c, isTruncated: a } = T(() => D({ string: l, idealLength: e }), [l, e]), g = s && a && !r, A = (S) => {
|
|
62
|
-
S.preventDefault(), y(!r);
|
|
63
|
-
};
|
|
64
|
-
if (E)
|
|
65
|
-
return /* @__PURE__ */ p(H, { children: t });
|
|
66
|
-
let n;
|
|
67
|
-
a ? r ? n = t : n = c : n = t;
|
|
68
|
-
const x = s ? i(t) : !1;
|
|
69
|
-
return /* @__PURE__ */ O(
|
|
70
|
-
"span",
|
|
71
|
-
{
|
|
72
|
-
style: { wordBreak: "break-word" },
|
|
73
|
-
"data-testid": "truncate-root",
|
|
74
|
-
"aria-expanded": a ? r : void 0,
|
|
75
|
-
children: [
|
|
76
|
-
n,
|
|
77
|
-
a && /* @__PURE__ */ p(
|
|
78
|
-
B,
|
|
79
|
-
{
|
|
80
|
-
mode: u,
|
|
81
|
-
focusMode: f,
|
|
82
|
-
className: !(s && r && x) ? "ml-2" : void 0,
|
|
83
|
-
size: "small",
|
|
84
|
-
onClick: A,
|
|
85
|
-
"aria-label": r ? "Show less" : "Show more",
|
|
86
|
-
children: r ? "less..." : "more..."
|
|
87
|
-
}
|
|
88
|
-
)
|
|
89
|
-
]
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
};
|
|
93
|
-
export {
|
|
94
|
-
N as Truncate
|
|
95
|
-
};
|