@frontfactory/readmore 1.0.2 → 1.1.1
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/README.md +46 -15
- package/dist/readmore.css +1 -7
- package/dist/readmore.d.ts +6 -1
- package/dist/readmore.js +46 -26
- package/package.json +11 -4
- package/dist/readmore.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Readmore JS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@frontfactory/readmore)
|
|
4
|
+
[](https://www.npmjs.com/package/@frontfactory/readmore)
|
|
5
|
+
[](https://www.npmjs.com/package/@frontfactory/readmore)
|
|
6
|
+
|
|
7
|
+
Lightweight, framework-agnostic plugin to clamp text to N lines (or a fixed pixel height) with an ellipsis and a
|
|
8
|
+
`Read more` / `Read less` toggle button.
|
|
5
9
|
|
|
6
10
|
- Zero dependencies, ~1 KB
|
|
7
11
|
- TypeScript types included
|
|
8
|
-
- CSS-driven clamping via `-webkit-line-clamp`
|
|
12
|
+
- CSS-driven clamping via `-webkit-line-clamp` or `max-height`
|
|
9
13
|
- The toggle button is only created when the text actually overflows
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
@@ -32,9 +36,15 @@ npm run dev
|
|
|
32
36
|
import { ReadMore } from '@frontfactory/readmore';
|
|
33
37
|
import '@frontfactory/readmore/style.css';
|
|
34
38
|
|
|
39
|
+
// clamp to 3 lines
|
|
35
40
|
ReadMore.init('.excerpt', {
|
|
36
41
|
lines: 3
|
|
37
42
|
});
|
|
43
|
+
|
|
44
|
+
// clamp to a fixed pixel height
|
|
45
|
+
ReadMore.init('.excerpt', {
|
|
46
|
+
height: 80
|
|
47
|
+
});
|
|
38
48
|
```
|
|
39
49
|
|
|
40
50
|
Or instantiate against a single element:
|
|
@@ -49,13 +59,16 @@ if (el) new ReadMore(el, {
|
|
|
49
59
|
|
|
50
60
|
## Options
|
|
51
61
|
|
|
52
|
-
| Option | Type | Default | Description
|
|
53
|
-
|
|
54
|
-
| `lines` | `number` | `3` | Number of lines to clamp to.
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
62
|
+
| Option | Type | Default | Description |
|
|
63
|
+
|-----------------|----------|------------------|--------------------------------------------------------------------|
|
|
64
|
+
| `lines` | `number` | `3` | Number of lines to clamp to. Ignored if `height` is set. |
|
|
65
|
+
| `height` | `number` | — | Max height in pixels. Takes precedence over `lines` when provided. |
|
|
66
|
+
| `moreText` | `string` | `'Read more'` | Label of the button when the text is collapsed. |
|
|
67
|
+
| `lessText` | `string` | `'Read less'` | Label of the button when the text is expanded. |
|
|
68
|
+
| `buttonClass` | `string` | `'readmore-btn'` | Class applied to the toggle button. |
|
|
69
|
+
| `expandedClass` | `string` | `'is-expanded'` | Class applied to the target element while expanded. |
|
|
70
|
+
| `openingClass` | `string` | `'is-opening'` | Transient class applied while expanding, removed after transition. |
|
|
71
|
+
| `closingClass` | `string` | `'is-closing'` | Transient class applied while collapsing, removed after transition.|
|
|
59
72
|
|
|
60
73
|
## API
|
|
61
74
|
|
|
@@ -71,7 +84,25 @@ ReadMore.init(selector, options); // returns ReadMore[]
|
|
|
71
84
|
|
|
72
85
|
## How it works
|
|
73
86
|
|
|
74
|
-
The plugin sets
|
|
75
|
-
|
|
76
|
-
the
|
|
77
|
-
|
|
87
|
+
The plugin sets a CSS custom property on the target element and toggles `.readmore-clamp`. In **lines mode** (default),
|
|
88
|
+
it sets `--readmore-lines` and the bundled stylesheet applies `-webkit-line-clamp`, producing an ellipsis at the end of
|
|
89
|
+
the last visible line. In **height mode** (`height` option), it sets `--readmore-height` and applies `max-height` via
|
|
90
|
+
the `.readmore-clamp--height` modifier class instead. The toggle button is mounted right after the element only when the
|
|
91
|
+
content actually overflows, and unmounted again on resize if the text ends up fitting.
|
|
92
|
+
|
|
93
|
+
### Transient classes for animations
|
|
94
|
+
|
|
95
|
+
In addition to `expandedClass` (which stays applied for the entire expanded state), the plugin applies short-lived
|
|
96
|
+
`openingClass` and `closingClass` markers on the target element at the moment of expand and collapse respectively.
|
|
97
|
+
Each is added when the toggle starts and removed automatically once the CSS transition on the element ends — or on the
|
|
98
|
+
next tick if no transition is declared. Use them as hooks to drive enter/leave animations:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
.is-opening {
|
|
102
|
+
/* expand animation */
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.is-closing {
|
|
106
|
+
/* collapse animation */
|
|
107
|
+
}
|
|
108
|
+
```
|
package/dist/readmore.css
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
.readmore-clamp
|
|
2
|
-
display: -webkit-box;
|
|
3
|
-
-webkit-box-orient: vertical;
|
|
4
|
-
overflow: hidden;
|
|
5
|
-
-webkit-line-clamp: var(--readmore-lines, 3);
|
|
6
|
-
line-clamp: var(--readmore-lines, 3);
|
|
7
|
-
}
|
|
1
|
+
.readmore-clamp{-webkit-line-clamp:var(--readmore-lines,3);line-clamp:var(--readmore-lines,3);-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.readmore-clamp--height{max-height:var(--readmore-height);-webkit-line-clamp:none;line-clamp:none;display:block}
|
package/dist/readmore.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export interface ReadMoreOptions {
|
|
2
2
|
lines?: number;
|
|
3
|
+
height?: number;
|
|
3
4
|
moreText?: string;
|
|
4
5
|
lessText?: string;
|
|
5
6
|
buttonClass?: string;
|
|
6
7
|
expandedClass?: string;
|
|
8
|
+
openingClass?: string;
|
|
9
|
+
closingClass?: string;
|
|
7
10
|
}
|
|
8
|
-
type ResolvedOptions = Required<ReadMoreOptions
|
|
11
|
+
type ResolvedOptions = Required<Omit<ReadMoreOptions, 'height'>> & {
|
|
12
|
+
height?: number;
|
|
13
|
+
};
|
|
9
14
|
export declare class ReadMore {
|
|
10
15
|
#private;
|
|
11
16
|
readonly el: HTMLElement;
|
package/dist/readmore.js
CHANGED
|
@@ -1,57 +1,77 @@
|
|
|
1
1
|
//#region src/readmore.ts
|
|
2
|
-
var e = "readmore-clamp", t = {
|
|
2
|
+
var e = "readmore-clamp", t = "readmore-clamp--height", n = {
|
|
3
3
|
lines: 3,
|
|
4
4
|
moreText: "Read more",
|
|
5
5
|
lessText: "Read less",
|
|
6
6
|
buttonClass: "readmore-btn",
|
|
7
|
-
expandedClass: "is-expanded"
|
|
8
|
-
|
|
7
|
+
expandedClass: "is-expanded",
|
|
8
|
+
openingClass: "is-opening",
|
|
9
|
+
closingClass: "is-closing"
|
|
10
|
+
}, r = class r {
|
|
9
11
|
el;
|
|
10
12
|
options;
|
|
11
13
|
#e = null;
|
|
12
14
|
#t = !1;
|
|
13
|
-
#n =
|
|
14
|
-
#r =
|
|
15
|
-
|
|
15
|
+
#n = null;
|
|
16
|
+
#r = null;
|
|
17
|
+
#i = () => {
|
|
18
|
+
this.#n !== null && clearTimeout(this.#n), this.#n = setTimeout(() => {
|
|
19
|
+
this.#n = null, this.#c();
|
|
20
|
+
}, 100);
|
|
21
|
+
};
|
|
22
|
+
#a = () => this.toggle();
|
|
23
|
+
constructor(e, t = {}) {
|
|
16
24
|
if (!(e instanceof HTMLElement)) throw TypeError("ReadMore: an HTMLElement is required.");
|
|
17
25
|
this.el = e, this.options = {
|
|
18
|
-
...
|
|
19
|
-
...
|
|
20
|
-
}, this.#
|
|
26
|
+
...n,
|
|
27
|
+
...t
|
|
28
|
+
}, this.#o();
|
|
21
29
|
}
|
|
22
30
|
static init(e, t) {
|
|
23
|
-
let
|
|
24
|
-
return Array.from(
|
|
31
|
+
let n = typeof e == "string" ? document.querySelectorAll(e) : e;
|
|
32
|
+
return Array.from(n).map((e) => new r(e, t));
|
|
25
33
|
}
|
|
26
34
|
get expanded() {
|
|
27
35
|
return this.#t;
|
|
28
36
|
}
|
|
29
37
|
toggle() {
|
|
30
|
-
this.#t = !this.#t, this.el.classList.toggle(e, !this.#t), this.el.classList.toggle(this.options.expandedClass, this.#t), this.#e && (this.#e.textContent = this.#t ? this.options.lessText : this.options.moreText)
|
|
38
|
+
this.#t = !this.#t, this.el.classList.toggle(e, !this.#t), this.options.height != null && this.el.classList.toggle(t, !this.#t), this.el.classList.toggle(this.options.expandedClass, this.#t), this.#e && (this.#e.textContent = this.#t ? this.options.lessText : this.options.moreText);
|
|
39
|
+
let n = this.#t ? this.options.openingClass : this.options.closingClass, r = this.#t ? this.options.closingClass : this.options.openingClass;
|
|
40
|
+
this.el.classList.remove(r), this.#l(n), this.#t || this.#c();
|
|
31
41
|
}
|
|
32
42
|
destroy() {
|
|
33
|
-
window.removeEventListener("resize", this.#n), this.#
|
|
43
|
+
window.removeEventListener("resize", this.#i), this.#n !== null && (clearTimeout(this.#n), this.#n = null), this.#r &&= (this.el.removeEventListener("transitionend", this.#r), null), this.#d(), this.el.classList.remove(e, t, this.options.expandedClass, this.options.openingClass, this.options.closingClass), this.el.style.removeProperty("--readmore-lines"), this.el.style.removeProperty("--readmore-height");
|
|
34
44
|
}
|
|
35
|
-
#
|
|
36
|
-
this.el.style.setProperty("--readmore-lines", String(this.options.lines)), this.el.classList.add(e), window.addEventListener("resize", this.#
|
|
45
|
+
#o() {
|
|
46
|
+
this.options.height == null ? (this.el.style.setProperty("--readmore-lines", String(this.options.lines)), this.el.classList.add(e)) : (this.el.style.setProperty("--readmore-height", `${this.options.height}px`), this.el.classList.add(e, t)), window.addEventListener("resize", this.#i), this.#c();
|
|
37
47
|
}
|
|
38
|
-
#
|
|
48
|
+
#s() {
|
|
39
49
|
return this.el.scrollHeight - this.el.clientHeight > 1;
|
|
40
50
|
}
|
|
41
|
-
#
|
|
51
|
+
#c() {
|
|
42
52
|
if (this.#t) return;
|
|
43
|
-
let e = this.#
|
|
44
|
-
e && !this.#e ? this.#
|
|
53
|
+
let e = this.#s();
|
|
54
|
+
e && !this.#e ? this.#u() : !e && this.#e && this.#d();
|
|
45
55
|
}
|
|
46
|
-
#
|
|
56
|
+
#l(e) {
|
|
57
|
+
this.#r &&= (this.el.removeEventListener("transitionend", this.#r), null), this.el.classList.add(e);
|
|
58
|
+
let t = () => this.el.classList.remove(e);
|
|
59
|
+
if (!getComputedStyle(this.el).transitionDuration.split(",").some((e) => parseFloat(e) > 0)) {
|
|
60
|
+
setTimeout(t, 0);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let n = (e) => {
|
|
64
|
+
e.target === this.el && (this.el.removeEventListener("transitionend", n), this.#r = null, t());
|
|
65
|
+
};
|
|
66
|
+
this.#r = n, this.el.addEventListener("transitionend", n);
|
|
67
|
+
}
|
|
68
|
+
#u() {
|
|
47
69
|
let e = document.createElement("button");
|
|
48
|
-
e.type = "button", e.className = this.options.buttonClass, e.textContent = this.options.moreText, e.addEventListener("click", this.#
|
|
70
|
+
e.type = "button", e.className = this.options.buttonClass, e.textContent = this.options.moreText, e.addEventListener("click", this.#a), this.el.insertAdjacentElement("afterend", e), this.#e = e;
|
|
49
71
|
}
|
|
50
|
-
#
|
|
51
|
-
this.#e &&= (this.#e.removeEventListener("click", this.#
|
|
72
|
+
#d() {
|
|
73
|
+
this.#e &&= (this.#e.removeEventListener("click", this.#a), this.#e.remove(), null);
|
|
52
74
|
}
|
|
53
75
|
};
|
|
54
76
|
//#endregion
|
|
55
|
-
export {
|
|
56
|
-
|
|
57
|
-
//# sourceMappingURL=readmore.js.map
|
|
77
|
+
export { r as ReadMore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontfactory/readmore",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Lightweight plugin to clamp text to N lines with ellipsis and a read more action.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"build": "vite build",
|
|
9
9
|
"preview": "vite preview",
|
|
10
10
|
"typecheck": "tsc --noEmit",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:coverage": "vitest run --coverage",
|
|
11
14
|
"prepare": "husky && npm run build",
|
|
12
15
|
"release": "semantic-release"
|
|
13
16
|
},
|
|
@@ -15,7 +18,6 @@
|
|
|
15
18
|
"node": ">=20.19"
|
|
16
19
|
},
|
|
17
20
|
"main": "./dist/readmore.js",
|
|
18
|
-
"module": "./dist/readmore.js",
|
|
19
21
|
"types": "./dist/readmore.d.ts",
|
|
20
22
|
"exports": {
|
|
21
23
|
".": {
|
|
@@ -31,13 +33,17 @@
|
|
|
31
33
|
"@semantic-release/changelog": "^6.0.3",
|
|
32
34
|
"@semantic-release/git": "^10.0.1",
|
|
33
35
|
"@semantic-release/github": "^12.0.6",
|
|
36
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
34
37
|
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
35
38
|
"husky": "^9.1.7",
|
|
39
|
+
"jsdom": "^29.1.1",
|
|
40
|
+
"lightningcss": "^1.32.0",
|
|
36
41
|
"semantic-release": "^25.0.3",
|
|
37
42
|
"typescript": "^5.4.0",
|
|
38
43
|
"vite": "^8.0.11",
|
|
39
44
|
"vite-plugin-dts": "^5.0.0",
|
|
40
|
-
"vite-plugin-static-copy": "^4.1.0"
|
|
45
|
+
"vite-plugin-static-copy": "^4.1.0",
|
|
46
|
+
"vitest": "^4.1.5"
|
|
41
47
|
},
|
|
42
48
|
"files": [
|
|
43
49
|
"dist"
|
|
@@ -58,6 +64,7 @@
|
|
|
58
64
|
"url": "git+https://github.com/front-factory/readmore.git"
|
|
59
65
|
},
|
|
60
66
|
"publishConfig": {
|
|
61
|
-
"access": "public"
|
|
67
|
+
"access": "public",
|
|
68
|
+
"provenance": true
|
|
62
69
|
}
|
|
63
70
|
}
|
package/dist/readmore.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"readmore.js","names":["#refresh","#init","#expanded","#button","#onResize","#unmountButton","#isOverflowing","#mountButton","#onClick"],"sources":["../src/readmore.ts"],"sourcesContent":["export interface ReadMoreOptions {\n lines?: number;\n moreText?: string;\n lessText?: string;\n buttonClass?: string;\n expandedClass?: string;\n}\n\ntype ResolvedOptions = Required<ReadMoreOptions>;\n\nconst CLAMP_CLASS = 'readmore-clamp';\n\nconst DEFAULTS: ResolvedOptions = {\n lines: 3,\n moreText: 'Read more',\n lessText: 'Read less',\n buttonClass: 'readmore-btn',\n expandedClass: 'is-expanded'\n};\n\nexport class ReadMore {\n readonly el: HTMLElement;\n readonly options: ResolvedOptions;\n\n #button: HTMLButtonElement | null = null;\n #expanded = false;\n #onResize = (): void => this.#refresh();\n #onClick = (): void => this.toggle();\n\n constructor(element: HTMLElement, options: ReadMoreOptions = {}) {\n if (!(element instanceof HTMLElement)) {\n throw new TypeError('ReadMore: an HTMLElement is required.');\n }\n this.el = element;\n this.options = { ...DEFAULTS, ...options };\n this.#init();\n }\n\n static init(\n target: string | NodeListOf<HTMLElement> | HTMLElement[],\n options?: ReadMoreOptions\n ): ReadMore[] {\n const nodes =\n typeof target === 'string'\n ? document.querySelectorAll<HTMLElement>(target)\n : target;\n return Array.from(nodes).map((n) => new ReadMore(n, options));\n }\n\n get expanded(): boolean {\n return this.#expanded;\n }\n\n toggle(): void {\n this.#expanded = !this.#expanded;\n this.el.classList.toggle(CLAMP_CLASS, !this.#expanded);\n this.el.classList.toggle(this.options.expandedClass, this.#expanded);\n if (this.#button) {\n this.#button.textContent = this.#expanded\n ? this.options.lessText\n : this.options.moreText;\n }\n if (!this.#expanded) this.#refresh();\n }\n\n destroy(): void {\n window.removeEventListener('resize', this.#onResize);\n this.#unmountButton();\n this.el.classList.remove(CLAMP_CLASS, this.options.expandedClass);\n this.el.style.removeProperty('--readmore-lines');\n }\n\n #init(): void {\n this.el.style.setProperty('--readmore-lines', String(this.options.lines));\n this.el.classList.add(CLAMP_CLASS);\n window.addEventListener('resize', this.#onResize);\n this.#refresh();\n }\n\n #isOverflowing(): boolean {\n return this.el.scrollHeight - this.el.clientHeight > 1;\n }\n\n #refresh(): void {\n if (this.#expanded) return;\n const overflowing = this.#isOverflowing();\n if (overflowing && !this.#button) {\n this.#mountButton();\n } else if (!overflowing && this.#button) {\n this.#unmountButton();\n }\n }\n\n #mountButton(): void {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = this.options.buttonClass;\n btn.textContent = this.options.moreText;\n btn.addEventListener('click', this.#onClick);\n this.el.insertAdjacentElement('afterend', btn);\n this.#button = btn;\n }\n\n #unmountButton(): void {\n if (!this.#button) return;\n this.#button.removeEventListener('click', this.#onClick);\n this.#button.remove();\n this.#button = null;\n }\n}\n"],"mappings":";AAUA,IAAM,IAAc,kBAEd,IAA4B;CAC9B,OAAO;CACP,UAAU;CACV,UAAU;CACV,aAAa;CACb,eAAe;CAClB,EAEY,IAAb,MAAa,EAAS;CAClB;CACA;CAEA,KAAoC;CACpC,KAAY;CACZ,WAAwB,KAAKA,IAAU;CACvC,WAAuB,KAAK,QAAQ;CAEpC,YAAY,GAAsB,IAA2B,EAAE,EAAE;EAC7D,IAAI,EAAE,aAAmB,cACrB,MAAU,UAAU,wCAAwC;EAIhE,AAFA,KAAK,KAAK,GACV,KAAK,UAAU;GAAE,GAAG;GAAU,GAAG;GAAS,EAC1C,KAAKC,IAAO;;CAGhB,OAAO,KACH,GACA,GACU;EACV,IAAM,IACF,OAAO,KAAW,WACZ,SAAS,iBAA8B,EAAO,GAC9C;EACV,OAAO,MAAM,KAAK,EAAM,CAAC,KAAK,MAAM,IAAI,EAAS,GAAG,EAAQ,CAAC;;CAGjE,IAAI,WAAoB;EACpB,OAAO,KAAKC;;CAGhB,SAAe;EASX,AARA,KAAKA,KAAY,CAAC,KAAKA,IACvB,KAAK,GAAG,UAAU,OAAO,GAAa,CAAC,KAAKA,GAAU,EACtD,KAAK,GAAG,UAAU,OAAO,KAAK,QAAQ,eAAe,KAAKA,GAAU,EAChE,KAAKC,OACL,KAAKA,GAAQ,cAAc,KAAKD,KAC1B,KAAK,QAAQ,WACb,KAAK,QAAQ,WAElB,KAAKA,MAAW,KAAKF,IAAU;;CAGxC,UAAgB;EAIZ,AAHA,OAAO,oBAAoB,UAAU,KAAKI,GAAU,EACpD,KAAKC,IAAgB,EACrB,KAAK,GAAG,UAAU,OAAO,GAAa,KAAK,QAAQ,cAAc,EACjE,KAAK,GAAG,MAAM,eAAe,mBAAmB;;CAGpD,KAAc;EAIV,AAHA,KAAK,GAAG,MAAM,YAAY,oBAAoB,OAAO,KAAK,QAAQ,MAAM,CAAC,EACzE,KAAK,GAAG,UAAU,IAAI,EAAY,EAClC,OAAO,iBAAiB,UAAU,KAAKD,GAAU,EACjD,KAAKJ,IAAU;;CAGnB,KAA0B;EACtB,OAAO,KAAK,GAAG,eAAe,KAAK,GAAG,eAAe;;CAGzD,KAAiB;EACb,IAAI,KAAKE,IAAW;EACpB,IAAM,IAAc,KAAKI,IAAgB;EACzC,AAAI,KAAe,CAAC,KAAKH,KACrB,KAAKI,IAAc,GACZ,CAAC,KAAe,KAAKJ,MAC5B,KAAKE,IAAgB;;CAI7B,KAAqB;EACjB,IAAM,IAAM,SAAS,cAAc,SAAS;EAM5C,AALA,EAAI,OAAO,UACX,EAAI,YAAY,KAAK,QAAQ,aAC7B,EAAI,cAAc,KAAK,QAAQ,UAC/B,EAAI,iBAAiB,SAAS,KAAKG,GAAS,EAC5C,KAAK,GAAG,sBAAsB,YAAY,EAAI,EAC9C,KAAKL,KAAU;;CAGnB,KAAuB;EACd,AAGL,KAAKA,QAFL,KAAKA,GAAQ,oBAAoB,SAAS,KAAKK,GAAS,EACxD,KAAKL,GAAQ,QAAQ,EACN"}
|