@chitovas/ngx-clamp 0.1.2 → 1.0.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/README.md +34 -27
- package/fesm2022/chitovas-ngx-clamp.mjs +109 -80
- package/fesm2022/chitovas-ngx-clamp.mjs.map +1 -1
- package/index.d.ts +21 -9
- package/package.json +24 -14
package/README.md
CHANGED
|
@@ -3,50 +3,57 @@
|
|
|
3
3
|

|
|
4
4
|
[](https://www.npmjs.com/package/@chitovas/ngx-clamp)
|
|
5
5
|

|
|
6
|
+
[](https://www.npmjs.com/package/@chitovas/ngx-clamp)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
A lightweight Angular directive for clamping text to a specified number of lines or height. A fast, cross-browser alternative to CSS `line-clamp` that works in all browsers, including legacy browsers where native support is unavailable.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
- **Easy Integration**: Seamlessly integrate `ngx-clamp` directive into your Angular applications to manage text overflow.
|
|
16
|
-
- **Customizable Truncation**: Use ellipsis or specify your own truncation text for clamped content.
|
|
17
|
-
- **Nested Element Support**: Clamp text within nested HTML elements effortlessly.
|
|
18
|
-
- **Height Configuration**: Set a maximum height for text before clamping activates.
|
|
19
|
-
- **Legacy Browser Compatibility**: Solves overflow issues on older browsers that don't support the -webkit-line-clamp property, ensuring consistent behavior across different platforms.
|
|
10
|
+
- **Fast** - Uses binary search algorithm for O(log n) truncation performance
|
|
11
|
+
- **Universal** - Works across all browsers including IE11 and older
|
|
12
|
+
- **Lightweight** - Zero dependencies, tree-shakeable
|
|
20
13
|
|
|
21
14
|
## Installation
|
|
22
15
|
|
|
23
|
-
Install the package using npm:
|
|
24
|
-
|
|
25
16
|
```bash
|
|
26
17
|
npm install @chitovas/ngx-clamp
|
|
27
18
|
```
|
|
28
19
|
|
|
29
20
|
## Usage
|
|
30
21
|
|
|
31
|
-
```
|
|
22
|
+
```typescript
|
|
23
|
+
import { Component } from '@angular/core';
|
|
32
24
|
import { NgxClamp } from '@chitovas/ngx-clamp';
|
|
33
25
|
|
|
34
26
|
@Component({
|
|
35
|
-
selector: '
|
|
36
|
-
|
|
37
|
-
<div ngxClamp [maxHeight]="100" truncationText="...">
|
|
38
|
-
Your long text goes here, and it will be clamped if it exceeds the specified height.
|
|
39
|
-
<p>Your long text goes here in nested element</p>
|
|
40
|
-
</div>
|
|
41
|
-
<div ngxClamp [maxHeight]="150" truncationText="Read more...">
|
|
42
|
-
This is a longer paragraph that will be clamped to fit within the specified height.
|
|
43
|
-
</div>
|
|
44
|
-
`,
|
|
27
|
+
selector: 'app-example',
|
|
28
|
+
standalone: true,
|
|
45
29
|
imports: [NgxClamp],
|
|
30
|
+
template: ` <div ngxClamp [lines]="3">Long text content that will be clamped...</div> `,
|
|
46
31
|
})
|
|
47
|
-
export class
|
|
32
|
+
export class ExampleComponent {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Clamp by Height
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<div ngxClamp [maxHeight]="100">Content clamped at 100px height...</div>
|
|
48
39
|
```
|
|
49
40
|
|
|
41
|
+
### Custom Truncation Text
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<div ngxClamp [lines]="3" truncationText=" Read more...">Content with custom truncation indicator...</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
| Input | Type | Default | Description |
|
|
50
|
+
| ---------------- | -------- | ------- | ----------------------------------- |
|
|
51
|
+
| `lines` | `number` | - | Number of lines before clamping |
|
|
52
|
+
| `maxHeight` | `number` | - | Maximum height (px) before clamping |
|
|
53
|
+
| `truncationText` | `string` | `'...'` | Text appended to clamped content |
|
|
54
|
+
|
|
55
|
+
Use either `lines` or `maxHeight`. If both are provided, `lines` takes precedence.
|
|
56
|
+
|
|
50
57
|
## License
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
MIT
|
|
@@ -1,124 +1,153 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Input, Directive } from '@angular/core';
|
|
3
3
|
|
|
4
|
+
const WORD_SEPARATOR = ' ';
|
|
5
|
+
const DEFAULT_LINE_HEIGHT_MULTIPLIER = 1.187;
|
|
4
6
|
class NgxClamp {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
maxHeight = null;
|
|
8
|
+
lines = 0;
|
|
7
9
|
truncationCharacters = '...';
|
|
8
10
|
maxLines = 0;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
cachedLineHeight = null;
|
|
12
|
+
originalContent = null;
|
|
13
|
+
isInitialized = false;
|
|
14
|
+
element;
|
|
15
|
+
constructor(elementRef) {
|
|
16
|
+
this.element = elementRef.nativeElement;
|
|
12
17
|
}
|
|
13
18
|
ngAfterViewInit() {
|
|
19
|
+
this.originalContent = this.element.innerHTML;
|
|
20
|
+
this.isInitialized = true;
|
|
14
21
|
this.clamp();
|
|
15
22
|
}
|
|
16
23
|
ngOnChanges(changes) {
|
|
17
|
-
|
|
24
|
+
const shouldReclamp = changes['maxHeight'] || changes['lines'];
|
|
25
|
+
if (shouldReclamp && this.isInitialized) {
|
|
26
|
+
this.cachedLineHeight = null;
|
|
27
|
+
this.restoreOriginalContent();
|
|
18
28
|
this.clamp();
|
|
19
29
|
}
|
|
20
30
|
}
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
const maxRequiredHeight = Math.max(this.maxHeight, this.getMaxHeight(this.maxLines, hostHtmlElement));
|
|
25
|
-
if (maxRequiredHeight < hostHtmlElement.clientHeight) {
|
|
26
|
-
const lastChild = this.getLastChild(hostHtmlElement);
|
|
27
|
-
if (lastChild) {
|
|
28
|
-
this.truncate(maxRequiredHeight, lastChild);
|
|
29
|
-
}
|
|
31
|
+
restoreOriginalContent() {
|
|
32
|
+
if (this.originalContent !== null) {
|
|
33
|
+
this.element.innerHTML = this.originalContent;
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!words && nodeValue) {
|
|
37
|
-
words = nodeValue.split(this.splitOnWordsCharacter);
|
|
38
|
-
isCurrentNodeValueSplitIntoWords = true;
|
|
36
|
+
clamp() {
|
|
37
|
+
const hasConstraints = this.maxHeight || this.lines;
|
|
38
|
+
if (!hasConstraints) {
|
|
39
|
+
return;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
this.maxLines = this.lines || this.calculateMaxLines();
|
|
42
|
+
const maxAllowedHeight = Math.max(this.maxHeight ?? 0, this.getLineHeight() * this.maxLines);
|
|
43
|
+
const needsTruncation = this.element.clientHeight > maxAllowedHeight;
|
|
44
|
+
if (needsTruncation) {
|
|
45
|
+
const textNode = this.findDeepestTextNode(this.element);
|
|
46
|
+
if (textNode) {
|
|
47
|
+
this.truncateNode(textNode, maxAllowedHeight);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Uses binary search to find the optimal number of words that fit within maxHeight.
|
|
53
|
+
* Falls back to previous text node if current node's first word doesn't fit.
|
|
54
|
+
*/
|
|
55
|
+
truncateNode(node, maxHeight) {
|
|
56
|
+
const text = node.nodeValue;
|
|
57
|
+
if (!text) {
|
|
58
|
+
return;
|
|
55
59
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const words = text.split(WORD_SEPARATOR);
|
|
61
|
+
// Early exit: text already fits
|
|
62
|
+
if (this.textFits(node, text + this.truncationCharacters, maxHeight)) {
|
|
63
|
+
node.nodeValue = text;
|
|
64
|
+
return;
|
|
61
65
|
}
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
// No valid words even when splitting by letter, time to move on to the next node
|
|
65
|
-
// Set the current node value to the truncation character
|
|
66
|
+
// First word doesn't fit: move to previous node
|
|
67
|
+
if (!this.textFits(node, words[0] + this.truncationCharacters, maxHeight)) {
|
|
66
68
|
node.nodeValue = this.truncationCharacters;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const previousNode = this.findDeepestTextNode(this.element);
|
|
70
|
+
if (previousNode) {
|
|
71
|
+
this.truncateNode(previousNode, maxHeight);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
69
74
|
}
|
|
70
|
-
|
|
75
|
+
// Binary search for optimal word count
|
|
76
|
+
const optimalWordCount = this.findOptimalWordCount(node, words, maxHeight);
|
|
77
|
+
node.nodeValue = words.slice(0, optimalWordCount).join(WORD_SEPARATOR) + this.truncationCharacters;
|
|
71
78
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
79
|
+
textFits(node, text, maxHeight) {
|
|
80
|
+
node.nodeValue = text;
|
|
81
|
+
return this.element.clientHeight <= maxHeight;
|
|
75
82
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
findOptimalWordCount(node, words, maxHeight) {
|
|
84
|
+
let low = 1;
|
|
85
|
+
let high = words.length;
|
|
86
|
+
let result = 1;
|
|
87
|
+
while (low <= high) {
|
|
88
|
+
const mid = Math.floor((low + high) / 2);
|
|
89
|
+
const testText = words.slice(0, mid).join(WORD_SEPARATOR) + this.truncationCharacters;
|
|
90
|
+
if (this.textFits(node, testText, maxHeight)) {
|
|
91
|
+
result = mid;
|
|
92
|
+
low = mid + 1;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
high = mid - 1;
|
|
96
|
+
}
|
|
82
97
|
}
|
|
83
|
-
return
|
|
98
|
+
return result;
|
|
84
99
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
getLineHeight() {
|
|
101
|
+
if (this.cachedLineHeight !== null) {
|
|
102
|
+
return this.cachedLineHeight;
|
|
103
|
+
}
|
|
104
|
+
const styles = getComputedStyle(this.element);
|
|
105
|
+
if (styles.lineHeight === 'normal') {
|
|
106
|
+
const fontSize = parseFloat(styles.fontSize);
|
|
107
|
+
this.cachedLineHeight = Math.ceil(fontSize * DEFAULT_LINE_HEIGHT_MULTIPLIER);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.cachedLineHeight = Math.ceil(parseFloat(styles.lineHeight));
|
|
111
|
+
}
|
|
112
|
+
return this.cachedLineHeight;
|
|
113
|
+
}
|
|
114
|
+
calculateMaxLines() {
|
|
115
|
+
const availableHeight = this.maxHeight ?? this.element.clientHeight;
|
|
116
|
+
return Math.max(Math.floor(availableHeight / this.getLineHeight()), 0);
|
|
90
117
|
}
|
|
91
|
-
|
|
92
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Recursively finds the deepest text node, skipping empty nodes.
|
|
120
|
+
*/
|
|
121
|
+
findDeepestTextNode(node) {
|
|
93
122
|
if (!node.lastChild) {
|
|
94
123
|
return node;
|
|
95
124
|
}
|
|
96
|
-
//
|
|
97
|
-
if (node.lastChild.childNodes
|
|
98
|
-
|
|
125
|
+
// Traverse to nested children
|
|
126
|
+
if (node.lastChild.childNodes?.length > 0) {
|
|
127
|
+
const lastChildIndex = node.childNodes.length - 1;
|
|
128
|
+
return this.findDeepestTextNode(node.childNodes.item(lastChildIndex));
|
|
99
129
|
}
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
// Skip empty or truncation-only nodes
|
|
131
|
+
const isEmptyNode = !node.lastChild.nodeValue || node.lastChild.nodeValue === '' || node.lastChild.nodeValue === this.truncationCharacters;
|
|
132
|
+
if (isEmptyNode && node.lastChild.parentNode) {
|
|
103
133
|
node.lastChild.parentNode.removeChild(node.lastChild);
|
|
104
|
-
return this.
|
|
105
|
-
}
|
|
106
|
-
// This is the last child we want, return it
|
|
107
|
-
else {
|
|
108
|
-
return node.lastChild;
|
|
134
|
+
return this.findDeepestTextNode(this.element);
|
|
109
135
|
}
|
|
136
|
+
return node.lastChild;
|
|
110
137
|
}
|
|
111
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.
|
|
112
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.
|
|
138
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxClamp, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
139
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.16", type: NgxClamp, isStandalone: true, selector: "[ngxClamp]", inputs: { maxHeight: "maxHeight", lines: "lines", truncationCharacters: "truncationCharacters" }, usesOnChanges: true, ngImport: i0 });
|
|
113
140
|
}
|
|
114
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.
|
|
141
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxClamp, decorators: [{
|
|
115
142
|
type: Directive,
|
|
116
143
|
args: [{
|
|
117
144
|
selector: '[ngxClamp]',
|
|
145
|
+
standalone: true,
|
|
118
146
|
}]
|
|
119
147
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { maxHeight: [{
|
|
120
|
-
type: Input
|
|
121
|
-
|
|
148
|
+
type: Input
|
|
149
|
+
}], lines: [{
|
|
150
|
+
type: Input
|
|
122
151
|
}], truncationCharacters: [{
|
|
123
152
|
type: Input
|
|
124
153
|
}] } });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chitovas-ngx-clamp.mjs","sources":["../../../../projects/chitova/ngx-clamp/src/lib/ngx-clamp.ts","../../../../projects/chitova/ngx-clamp/src/chitovas-ngx-clamp.ts"],"sourcesContent":["import { AfterViewInit, Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core';\n\n@Directive({\n selector: '[ngxClamp]',\n})\nexport class NgxClamp implements AfterViewInit, OnChanges {\n @Input({ required: true })\n public maxHeight: number = 0;\n\n @Input()\n public truncationCharacters: string = '...';\n\n public maxLines: number = 0;\n\n private readonly splitOnWordsCharacter: string = ' ';\n\n constructor(private readonly htmlElementRef: ElementRef<HTMLElement>) {}\n\n public ngAfterViewInit(): void {\n this.clamp();\n }\n\n public ngOnChanges(changes: SimpleChanges): void {\n if (changes['height']) {\n this.clamp();\n }\n }\n\n private clamp(): void {\n this.maxLines = this.getMaxLines();\n const hostHtmlElement: HTMLElement = this.htmlElementRef.nativeElement;\n const maxRequiredHeight: number = Math.max(this.maxHeight, this.getMaxHeight(this.maxLines, hostHtmlElement));\n if (maxRequiredHeight < hostHtmlElement.clientHeight) {\n const lastChild: ChildNode = this.getLastChild(hostHtmlElement);\n if (lastChild) {\n this.truncate(maxRequiredHeight, lastChild);\n }\n }\n }\n\n // Recursively removes words from the text until its width or height is beneath maximum required height.\n private truncate(\n maxRequiredHeight: number,\n node: ChildNode,\n words: string[] | undefined = undefined,\n isCurrentNodeValueSplitIntoWords: boolean = false\n ): void {\n // Removes truncation characters from node text\n const nodeValue: string | undefined = node.nodeValue?.replace(this.truncationCharacters, '');\n\n if (!words && nodeValue) {\n words = nodeValue.split(this.splitOnWordsCharacter);\n isCurrentNodeValueSplitIntoWords = true;\n }\n\n if (words) {\n const isTexFits: boolean = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;\n node.nodeValue = words.join(this.splitOnWordsCharacter) + this.truncationCharacters;\n if (isTexFits) {\n return;\n }\n }\n\n // If there are words left to remove, remove the last one and see if the nodeValue fits.\n if (words && words.length > 1) {\n words.pop();\n node.nodeValue = words.join(this.splitOnWordsCharacter) + this.truncationCharacters;\n }\n // No more words can be removed using this character\n else {\n words = undefined;\n }\n\n if (words) {\n const isTexFits: boolean = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;\n if (isTexFits) {\n return;\n }\n }\n\n // No valid words produced\n else if (isCurrentNodeValueSplitIntoWords) {\n // No valid words even when splitting by letter, time to move on to the next node\n\n // Set the current node value to the truncation character\n node.nodeValue = this.truncationCharacters;\n node = this.getLastChild(this.htmlElementRef.nativeElement);\n return this.truncate(maxRequiredHeight, node);\n }\n return this.truncate(maxRequiredHeight, node, words, isCurrentNodeValueSplitIntoWords);\n }\n\n private getMaxHeight(maxLines: number, element: HTMLElement): number {\n const lineHeight: number = this.getLineHeight(element);\n return lineHeight * maxLines;\n }\n\n private getLineHeight(element: HTMLElement): number {\n const cssStyleDeclaration: CSSStyleDeclaration = getComputedStyle(element, 'line-height');\n const lineHeight: string = cssStyleDeclaration.lineHeight;\n if (cssStyleDeclaration.lineHeight === 'normal') {\n // Normal line heights vary from browser to browser. The spec recommends a value between 1.0 and 1.2 of the font size.\n return Math.ceil(parseInt(getComputedStyle(element, 'font-size').fontSize, 10) * 1.187);\n }\n return Math.ceil(parseInt(lineHeight, 10));\n }\n\n private getMaxLines(): number {\n // Returns the maximum number of lines of text that should be rendered based on the current height of the element and the line-height of the text.\n const hostHtmlElement: HTMLElement = this.htmlElementRef.nativeElement;\n const availableHeight: number = this.maxHeight ?? hostHtmlElement.clientHeight;\n return Math.max(Math.floor(availableHeight / this.getLineHeight(hostHtmlElement)), 0);\n }\n\n private getLastChild(node: ChildNode): ChildNode {\n // Gets an element's last child. That may be another node or a node's contents.\n if (!node.lastChild) {\n return node;\n }\n // Current element has children, need to go deeper and get last child as a text node\n if (node.lastChild.childNodes && node.lastChild.childNodes.length > 0) {\n return this.getLastChild(node.childNodes.item(node.childNodes.length - 1));\n }\n // This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying\n else if (\n node.lastChild.parentNode &&\n (!node.lastChild?.nodeValue || node.lastChild.nodeValue === '' || node.lastChild.nodeValue === this.truncationCharacters)\n ) {\n node.lastChild.parentNode.removeChild(node.lastChild);\n return this.getLastChild(this.htmlElementRef.nativeElement);\n }\n // This is the last child we want, return it\n else {\n return node.lastChild;\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chitovas-ngx-clamp.mjs","sources":["../../../../projects/chitova/ngx-clamp/src/lib/ngx-clamp.ts","../../../../projects/chitova/ngx-clamp/src/chitovas-ngx-clamp.ts"],"sourcesContent":["import { AfterViewInit, Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core';\n\nconst WORD_SEPARATOR = ' ';\nconst DEFAULT_LINE_HEIGHT_MULTIPLIER = 1.187;\n\n@Directive({\n selector: '[ngxClamp]',\n standalone: true,\n})\nexport class NgxClamp implements AfterViewInit, OnChanges {\n @Input() maxHeight: number | null = null;\n @Input() lines: number = 0;\n @Input() truncationCharacters: string = '...';\n\n maxLines: number = 0;\n\n private cachedLineHeight: number | null = null;\n private originalContent: string | null = null;\n private isInitialized: boolean = false;\n private readonly element: HTMLElement;\n\n constructor(elementRef: ElementRef<HTMLElement>) {\n this.element = elementRef.nativeElement;\n }\n\n ngAfterViewInit(): void {\n this.originalContent = this.element.innerHTML;\n this.isInitialized = true;\n this.clamp();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n const shouldReclamp = changes['maxHeight'] || changes['lines'];\n if (shouldReclamp && this.isInitialized) {\n this.cachedLineHeight = null;\n this.restoreOriginalContent();\n this.clamp();\n }\n }\n\n private restoreOriginalContent(): void {\n if (this.originalContent !== null) {\n this.element.innerHTML = this.originalContent;\n }\n }\n\n private clamp(): void {\n const hasConstraints = this.maxHeight || this.lines;\n if (!hasConstraints) {\n return;\n }\n\n this.maxLines = this.lines || this.calculateMaxLines();\n\n const maxAllowedHeight = Math.max(this.maxHeight ?? 0, this.getLineHeight() * this.maxLines);\n\n const needsTruncation = this.element.clientHeight > maxAllowedHeight;\n if (needsTruncation) {\n const textNode = this.findDeepestTextNode(this.element);\n if (textNode) {\n this.truncateNode(textNode, maxAllowedHeight);\n }\n }\n }\n\n /**\n * Uses binary search to find the optimal number of words that fit within maxHeight.\n * Falls back to previous text node if current node's first word doesn't fit.\n */\n private truncateNode(node: ChildNode, maxHeight: number): void {\n const text = node.nodeValue;\n if (!text) {\n return;\n }\n\n const words = text.split(WORD_SEPARATOR);\n\n // Early exit: text already fits\n if (this.textFits(node, text + this.truncationCharacters, maxHeight)) {\n node.nodeValue = text;\n return;\n }\n\n // First word doesn't fit: move to previous node\n if (!this.textFits(node, words[0] + this.truncationCharacters, maxHeight)) {\n node.nodeValue = this.truncationCharacters;\n const previousNode = this.findDeepestTextNode(this.element);\n if (previousNode) {\n this.truncateNode(previousNode, maxHeight);\n }\n return;\n }\n\n // Binary search for optimal word count\n const optimalWordCount = this.findOptimalWordCount(node, words, maxHeight);\n node.nodeValue = words.slice(0, optimalWordCount).join(WORD_SEPARATOR) + this.truncationCharacters;\n }\n\n private textFits(node: ChildNode, text: string, maxHeight: number): boolean {\n node.nodeValue = text;\n return this.element.clientHeight <= maxHeight;\n }\n\n private findOptimalWordCount(node: ChildNode, words: string[], maxHeight: number): number {\n let low = 1;\n let high = words.length;\n let result = 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const testText = words.slice(0, mid).join(WORD_SEPARATOR) + this.truncationCharacters;\n\n if (this.textFits(node, testText, maxHeight)) {\n result = mid;\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n return result;\n }\n\n private getLineHeight(): number {\n if (this.cachedLineHeight !== null) {\n return this.cachedLineHeight;\n }\n\n const styles = getComputedStyle(this.element);\n\n if (styles.lineHeight === 'normal') {\n const fontSize = parseFloat(styles.fontSize);\n this.cachedLineHeight = Math.ceil(fontSize * DEFAULT_LINE_HEIGHT_MULTIPLIER);\n } else {\n this.cachedLineHeight = Math.ceil(parseFloat(styles.lineHeight));\n }\n\n return this.cachedLineHeight;\n }\n\n private calculateMaxLines(): number {\n const availableHeight = this.maxHeight ?? this.element.clientHeight;\n return Math.max(Math.floor(availableHeight / this.getLineHeight()), 0);\n }\n\n /**\n * Recursively finds the deepest text node, skipping empty nodes.\n */\n private findDeepestTextNode(node: ChildNode): ChildNode | null {\n if (!node.lastChild) {\n return node;\n }\n\n // Traverse to nested children\n if (node.lastChild.childNodes?.length > 0) {\n const lastChildIndex = node.childNodes.length - 1;\n return this.findDeepestTextNode(node.childNodes.item(lastChildIndex)!);\n }\n\n // Skip empty or truncation-only nodes\n const isEmptyNode =\n !node.lastChild.nodeValue || node.lastChild.nodeValue === '' || node.lastChild.nodeValue === this.truncationCharacters;\n\n if (isEmptyNode && node.lastChild.parentNode) {\n node.lastChild.parentNode.removeChild(node.lastChild);\n return this.findDeepestTextNode(this.element);\n }\n\n return node.lastChild;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/index.d.ts
CHANGED
|
@@ -2,22 +2,34 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { AfterViewInit, OnChanges, ElementRef, SimpleChanges } from '@angular/core';
|
|
3
3
|
|
|
4
4
|
declare class NgxClamp implements AfterViewInit, OnChanges {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
maxHeight: number | null;
|
|
6
|
+
lines: number;
|
|
7
7
|
truncationCharacters: string;
|
|
8
8
|
maxLines: number;
|
|
9
|
-
private
|
|
10
|
-
|
|
9
|
+
private cachedLineHeight;
|
|
10
|
+
private originalContent;
|
|
11
|
+
private isInitialized;
|
|
12
|
+
private readonly element;
|
|
13
|
+
constructor(elementRef: ElementRef<HTMLElement>);
|
|
11
14
|
ngAfterViewInit(): void;
|
|
12
15
|
ngOnChanges(changes: SimpleChanges): void;
|
|
16
|
+
private restoreOriginalContent;
|
|
13
17
|
private clamp;
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Uses binary search to find the optimal number of words that fit within maxHeight.
|
|
20
|
+
* Falls back to previous text node if current node's first word doesn't fit.
|
|
21
|
+
*/
|
|
22
|
+
private truncateNode;
|
|
23
|
+
private textFits;
|
|
24
|
+
private findOptimalWordCount;
|
|
16
25
|
private getLineHeight;
|
|
17
|
-
private
|
|
18
|
-
|
|
26
|
+
private calculateMaxLines;
|
|
27
|
+
/**
|
|
28
|
+
* Recursively finds the deepest text node, skipping empty nodes.
|
|
29
|
+
*/
|
|
30
|
+
private findDeepestTextNode;
|
|
19
31
|
static ɵfac: i0.ɵɵFactoryDeclaration<NgxClamp, never>;
|
|
20
|
-
static ɵdir: i0.ɵɵDirectiveDeclaration<NgxClamp, "[ngxClamp]", never, { "maxHeight": { "alias": "maxHeight"; "required":
|
|
32
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<NgxClamp, "[ngxClamp]", never, { "maxHeight": { "alias": "maxHeight"; "required": false; }; "lines": { "alias": "lines"; "required": false; }; "truncationCharacters": { "alias": "truncationCharacters"; "required": false; }; }, {}, never, never, true, never>;
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
export { NgxClamp };
|
package/package.json
CHANGED
|
@@ -1,40 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chitovas/ngx-clamp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Angular directive for clamping text to a specified number of lines or height with cross-browser support.",
|
|
4
5
|
"author": "Nigel Mukandi",
|
|
5
|
-
"
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
|
-
"url": "git://github.com/Chitova263/ngx-clamp.git"
|
|
9
|
+
"url": "git+https://github.com/Chitova263/ngx-clamp.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Chitova263/ngx-clamp/issues"
|
|
9
13
|
},
|
|
14
|
+
"homepage": "https://github.com/Chitova263/ngx-clamp#readme",
|
|
10
15
|
"keywords": [
|
|
16
|
+
"angular",
|
|
17
|
+
"directive",
|
|
11
18
|
"clamp",
|
|
19
|
+
"text-clamp",
|
|
12
20
|
"line-clamp",
|
|
13
21
|
"webkit-line-clamp",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
22
|
+
"truncate",
|
|
23
|
+
"ellipsis",
|
|
24
|
+
"overflow"
|
|
16
25
|
],
|
|
17
|
-
"license": "MIT",
|
|
18
26
|
"publishConfig": {
|
|
19
27
|
"access": "public"
|
|
20
28
|
},
|
|
21
29
|
"peerDependencies": {
|
|
22
|
-
"@angular/common": "
|
|
23
|
-
"@angular/core": "
|
|
30
|
+
"@angular/common": ">=18.0.0",
|
|
31
|
+
"@angular/core": ">=18.0.0"
|
|
24
32
|
},
|
|
25
33
|
"dependencies": {
|
|
26
34
|
"tslib": "^2.3.0"
|
|
27
35
|
},
|
|
28
36
|
"sideEffects": false,
|
|
29
|
-
"module": "fesm2022/chitovas-ngx-clamp.mjs",
|
|
30
|
-
"typings": "index.d.ts",
|
|
31
37
|
"exports": {
|
|
32
|
-
"./package.json": {
|
|
33
|
-
"default": "./package.json"
|
|
34
|
-
},
|
|
35
38
|
".": {
|
|
36
39
|
"types": "./index.d.ts",
|
|
40
|
+
"esm2022": "./esm2022/chitovas-ngx-clamp.mjs",
|
|
41
|
+
"esm": "./esm2022/chitovas-ngx-clamp.mjs",
|
|
37
42
|
"default": "./fesm2022/chitovas-ngx-clamp.mjs"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": {
|
|
45
|
+
"default": "./package.json"
|
|
38
46
|
}
|
|
39
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"module": "fesm2022/chitovas-ngx-clamp.mjs",
|
|
49
|
+
"typings": "index.d.ts"
|
|
40
50
|
}
|