@chitovas/ngx-clamp 0.2.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 CHANGED
@@ -5,20 +5,11 @@
5
5
  ![bundle size](https://img.shields.io/bundlephobia/minzip/@chitovas/ngx-clamp)
6
6
  [![npm](https://img.shields.io/npm/dt/@chitovas/ngx-clamp.svg)](https://www.npmjs.com/package/@chitovas/ngx-clamp)
7
7
 
8
- An Angular library that provides elegant text overflow management with support for legacy browsers. Clamp text content to a specific number of lines or maximum height, with customizable truncation indicators.
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.
9
9
 
10
- ## Why ngx-clamp?
11
-
12
- Modern CSS properties like `-webkit-line-clamp` and `line-clamp` aren't supported in older browsers. This library provides a cross-browser solution that works consistently everywhere, with additional features like custom truncation text and nested element support.
13
-
14
- ## Features
15
-
16
- - ✨ **Simple Integration** - Single directive, no complex setup required
17
- - 🎯 **Flexible Clamping** - Clamp by line count or maximum height
18
- - 🎨 **Customizable Truncation** - Use ellipsis or custom text (e.g., "Read more...")
19
- - 🏗️ **Nested HTML Support** - Works seamlessly with complex nested structures
20
- - 🌐 **Universal Browser Support** - Reliable fallback for legacy browsers
21
- - 📦 **Lightweight** - Minimal bundle size impact
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
22
13
 
23
14
  ## Installation
24
15
 
@@ -26,9 +17,7 @@ Modern CSS properties like `-webkit-line-clamp` and `line-clamp` aren't supporte
26
17
  npm install @chitovas/ngx-clamp
27
18
  ```
28
19
 
29
- ## Quick Start
30
-
31
- ### 1. Import the Directive
20
+ ## Usage
32
21
 
33
22
  ```typescript
34
23
  import { Component } from '@angular/core';
@@ -38,172 +27,33 @@ import { NgxClamp } from '@chitovas/ngx-clamp';
38
27
  selector: 'app-example',
39
28
  standalone: true,
40
29
  imports: [NgxClamp],
41
- template: ` <div ngxClamp [lines]="3">Your long text content here...</div> `,
30
+ template: ` <div ngxClamp [lines]="3">Long text content that will be clamped...</div> `,
42
31
  })
43
32
  export class ExampleComponent {}
44
33
  ```
45
34
 
46
- ### 2. Basic Usage Examples
47
-
48
- #### Clamp by Line Count
49
-
50
- ```html
51
- <div ngxClamp [lines]="3">
52
- This text will be clamped to 3 lines. Any content exceeding this limit will be truncated and replaced with an ellipsis (...)
53
- </div>
54
- ```
55
-
56
- #### Clamp by Maximum Height
57
-
58
- ```html
59
- <div ngxClamp [maxHeight]="100">
60
- This text will be clamped when it exceeds 100 pixels in height. The overflow content will be hidden with an ellipsis.
61
- </div>
62
- ```
63
-
64
- #### Custom Truncation Text
35
+ ### Clamp by Height
65
36
 
66
37
  ```html
67
- <div ngxClamp [lines]="3" truncationText=" Read more...">
68
- This text will show "Read more..." instead of the default ellipsis when the content is clamped.
69
- </div>
70
- ```
71
-
72
- #### With Nested Elements
73
-
74
- ```html
75
- <div ngxClamp [lines]="4" truncationText="...">
76
- <h3>Article Title</h3>
77
- <p>First paragraph with some content...</p>
78
- <p>Second paragraph that might get clamped...</p>
79
- <span>Additional nested content</span>
80
- </div>
81
- ```
82
-
83
- ## API Reference
84
-
85
- ### Directive: `ngxClamp`
86
-
87
- | Input | Type | Default | Description |
88
- | ---------------- | -------- | ------- | ------------------------------------------ |
89
- | `lines` | `number` | - | Number of lines to display before clamping |
90
- | `maxHeight` | `number` | - | Maximum height in pixels before clamping |
91
- | `truncationText` | `string` | `'...'` | Text to display when content is clamped |
92
-
93
- **Note**: Use either `lines` or `maxHeight`, not both. If both are provided, `lines` takes precedence.
94
-
95
- ## Advanced Examples
96
-
97
- ### Article Preview Card
98
-
99
- ```typescript
100
- @Component({
101
- selector: 'app-article-card',
102
- standalone: true,
103
- imports: [NgxClamp],
104
- template: `
105
- <article class="card">
106
- <h2>{{ article.title }}</h2>
107
- <div ngxClamp [lines]="3" truncationText=" [Read more]">
108
- {{ article.content }}
109
- </div>
110
- </article>
111
- `,
112
- })
113
- export class ArticleCardComponent {
114
- article = {
115
- title: 'Understanding Angular',
116
- content: 'Very long article content...',
117
- };
118
- }
38
+ <div ngxClamp [maxHeight]="100">Content clamped at 100px height...</div>
119
39
  ```
120
40
 
121
- ### Product Description
41
+ ### Custom Truncation Text
122
42
 
123
43
  ```html
124
- <div class="product-info">
125
- <div ngxClamp [maxHeight]="120" truncationText="... See full description">
126
- <h4>Product Features</h4>
127
- <ul>
128
- <li>Feature one with detailed description</li>
129
- <li>Feature two with more information</li>
130
- <li>Feature three that might be hidden</li>
131
- </ul>
132
- <p>Additional product details and specifications...</p>
133
- </div>
134
- </div>
135
- ```
136
-
137
- ### Dynamic Content
138
-
139
- ```typescript
140
- @Component({
141
- selector: 'app-dynamic-clamp',
142
- standalone: true,
143
- imports: [NgxClamp],
144
- template: `
145
- <div ngxClamp [lines]="maxLines" [truncationText]="truncText">
146
- {{ dynamicContent }}
147
- </div>
148
- <button (click)="toggleExpand()">
149
- {{ expanded ? 'Show Less' : 'Show More' }}
150
- </button>
151
- `,
152
- })
153
- export class DynamicClampComponent {
154
- maxLines = 3;
155
- expanded = false;
156
- truncText = ' ...more';
157
- dynamicContent = 'Your long dynamic content...';
158
-
159
- toggleExpand() {
160
- this.maxLines = this.expanded ? 3 : 999;
161
- this.expanded = !this.expanded;
162
- }
163
- }
164
- ```
165
-
166
- ## Browser Support
167
-
168
- - ✅ Chrome (all versions)
169
- - ✅ Firefox (all versions)
170
- - ✅ Safari (all versions)
171
- - ✅ Edge (all versions)
172
- - ✅ IE11 and older legacy browsers
173
-
174
- ## Migration Guide
175
-
176
- ### From CSS line-clamp
177
-
178
- **Before** (CSS only):
179
-
180
- ```css
181
- .text {
182
- display: -webkit-box;
183
- -webkit-line-clamp: 3;
184
- -webkit-box-orient: vertical;
185
- overflow: hidden;
186
- }
44
+ <div ngxClamp [lines]="3" truncationText=" Read more...">Content with custom truncation indicator...</div>
187
45
  ```
188
46
 
189
- **After** (with ngx-clamp):
47
+ ## API
190
48
 
191
- ```html
192
- <div ngxClamp [lines]="3" class="text">Your content here</div>
193
- ```
194
-
195
- ## Contributing
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 |
196
54
 
197
- Contributions are welcome! Please feel free to submit a Pull Request.
55
+ Use either `lines` or `maxHeight`. If both are provided, `lines` takes precedence.
198
56
 
199
57
  ## License
200
58
 
201
- MIT License - see the [LICENSE](LICENSE) file for details.
202
-
203
- ## Support
204
-
205
- - 🐛 [Report Issues](https://github.com/Chitova263/ngx-clamp/issues)
206
-
207
- ---
208
-
209
- Made with ❤️ by [@chitovas](https://github.com/Chitova263)
59
+ MIT
@@ -1,121 +1,144 @@
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
- htmlElementRef;
6
7
  maxHeight = null;
7
8
  lines = 0;
8
- maxLines = 0;
9
- splitOnWordsCharacter = ' ';
10
9
  truncationCharacters = '...';
11
- constructor(htmlElementRef) {
12
- this.htmlElementRef = htmlElementRef;
10
+ maxLines = 0;
11
+ cachedLineHeight = null;
12
+ originalContent = null;
13
+ isInitialized = false;
14
+ element;
15
+ constructor(elementRef) {
16
+ this.element = elementRef.nativeElement;
13
17
  }
14
18
  ngAfterViewInit() {
19
+ this.originalContent = this.element.innerHTML;
20
+ this.isInitialized = true;
15
21
  this.clamp();
16
22
  }
17
23
  ngOnChanges(changes) {
18
- if (changes['maxHeight'] || changes['lines']) {
24
+ const shouldReclamp = changes['maxHeight'] || changes['lines'];
25
+ if (shouldReclamp && this.isInitialized) {
26
+ this.cachedLineHeight = null;
27
+ this.restoreOriginalContent();
19
28
  this.clamp();
20
29
  }
21
30
  }
31
+ restoreOriginalContent() {
32
+ if (this.originalContent !== null) {
33
+ this.element.innerHTML = this.originalContent;
34
+ }
35
+ }
22
36
  clamp() {
23
- if (!this.maxHeight && !this.lines) {
37
+ const hasConstraints = this.maxHeight || this.lines;
38
+ if (!hasConstraints) {
24
39
  return;
25
40
  }
26
- this.maxLines = this.lines ? this.lines : this.getMaxLines();
27
- const hostHtmlElement = this.htmlElementRef.nativeElement;
28
- const maxRequiredHeight = Math.max(this.maxHeight ?? 0, this.getMaxHeight(this.maxLines, hostHtmlElement));
29
- if (maxRequiredHeight < hostHtmlElement.clientHeight) {
30
- const lastChild = this.getLastChild(hostHtmlElement);
31
- if (lastChild) {
32
- this.truncate(maxRequiredHeight, lastChild);
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);
33
48
  }
34
49
  }
35
50
  }
36
- // Recursively removes words from the text until its width or height is beneath maximum required height.
37
- truncate(maxRequiredHeight, node, words = undefined, isCurrentNodeValueSplitIntoWords = false) {
38
- // Removes truncation character from node text
39
- const nodeValue = node.nodeValue?.replace(this.truncationCharacters, '');
40
- if (!words && nodeValue) {
41
- words = nodeValue.split(this.splitOnWordsCharacter);
42
- isCurrentNodeValueSplitIntoWords = true;
43
- }
44
- if (words) {
45
- const isTexFits = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;
46
- node.nodeValue = words.join(this.splitOnWordsCharacter) + this.truncationCharacters;
47
- if (isTexFits) {
48
- return;
49
- }
50
- }
51
- // If there are words left to remove, remove the last one and see if the nodeValue fits.
52
- if (words && words.length > 1) {
53
- words.pop();
54
- node.nodeValue = words.join(this.splitOnWordsCharacter) + this.truncationCharacters;
55
- }
56
- // No more words can be removed using this character
57
- else {
58
- words = undefined;
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;
59
59
  }
60
- if (words) {
61
- const isTexFits = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;
62
- if (isTexFits) {
63
- return;
64
- }
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;
65
65
  }
66
- // No valid words produced
67
- else if (isCurrentNodeValueSplitIntoWords) {
68
- // No valid words even when splitting by letter, time to move on to the next node
69
- // 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)) {
70
68
  node.nodeValue = this.truncationCharacters;
71
- node = this.getLastChild(this.htmlElementRef.nativeElement);
72
- return this.truncate(maxRequiredHeight, node);
69
+ const previousNode = this.findDeepestTextNode(this.element);
70
+ if (previousNode) {
71
+ this.truncateNode(previousNode, maxHeight);
72
+ }
73
+ return;
73
74
  }
74
- return this.truncate(maxRequiredHeight, node, words, isCurrentNodeValueSplitIntoWords);
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;
75
78
  }
76
- getMaxHeight(maxLines, element) {
77
- const lineHeight = this.getLineHeight(element);
78
- return lineHeight * maxLines;
79
+ textFits(node, text, maxHeight) {
80
+ node.nodeValue = text;
81
+ return this.element.clientHeight <= maxHeight;
79
82
  }
80
- getLineHeight(element) {
81
- const cssStyleDeclaration = getComputedStyle(element, 'line-height');
82
- const lineHeight = cssStyleDeclaration.lineHeight;
83
- if (cssStyleDeclaration.lineHeight === 'normal') {
84
- // Normal line heights vary from browser to browser. The spec recommends a value between 1.0 and 1.2 of the font size.
85
- return Math.ceil(parseInt(getComputedStyle(element, 'font-size').fontSize, 10) * 1.187);
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
+ }
86
97
  }
87
- return Math.ceil(parseInt(lineHeight, 10));
98
+ return result;
88
99
  }
89
- getMaxLines() {
90
- // 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.
91
- const hostHtmlElement = this.htmlElementRef.nativeElement;
92
- const availableHeight = this.maxHeight ?? hostHtmlElement.clientHeight;
93
- return Math.max(Math.floor(availableHeight / this.getLineHeight(hostHtmlElement)), 0);
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);
94
117
  }
95
- getLastChild(node) {
96
- // Gets an element's last child. That may be another node or a node's contents.
118
+ /**
119
+ * Recursively finds the deepest text node, skipping empty nodes.
120
+ */
121
+ findDeepestTextNode(node) {
97
122
  if (!node.lastChild) {
98
123
  return node;
99
124
  }
100
- // Current element has children, need to go deeper and get last child as a text node
101
- if (node.lastChild.childNodes && node.lastChild.childNodes.length > 0) {
102
- return this.getLastChild(node.childNodes.item(node.childNodes.length - 1));
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));
103
129
  }
104
- // This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying
105
- else if (node.lastChild.parentNode &&
106
- (!node.lastChild?.nodeValue || node.lastChild.nodeValue === '' || node.lastChild.nodeValue === this.truncationCharacters)) {
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) {
107
133
  node.lastChild.parentNode.removeChild(node.lastChild);
108
- return this.getLastChild(this.htmlElementRef.nativeElement);
109
- }
110
- // This is the last child we want, return it
111
- else {
112
- return node.lastChild;
134
+ return this.findDeepestTextNode(this.element);
113
135
  }
136
+ return node.lastChild;
114
137
  }
115
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: NgxClamp, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
116
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: NgxClamp, isStandalone: true, selector: "[ngxClamp]", inputs: { maxHeight: "maxHeight", lines: "lines", truncationCharacters: "truncationCharacters" }, usesOnChanges: true, ngImport: i0 });
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 });
117
140
  }
118
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: NgxClamp, decorators: [{
141
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NgxClamp, decorators: [{
119
142
  type: Directive,
120
143
  args: [{
121
144
  selector: '[ngxClamp]',
@@ -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 standalone: true,\n})\nexport class NgxClamp implements AfterViewInit, OnChanges {\n @Input()\n public maxHeight: number | null = null;\n\n @Input()\n public lines: number = 0;\n\n public maxLines: number = 0;\n\n private readonly splitOnWordsCharacter: string = ' ';\n\n @Input()\n public truncationCharacters: 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['maxHeight'] || changes['lines']) {\n this.clamp();\n }\n }\n\n private clamp(): void {\n if (!this.maxHeight && !this.lines) {\n return;\n }\n this.maxLines = this.lines ? this.lines : this.getMaxLines();\n const hostHtmlElement: HTMLElement = this.htmlElementRef.nativeElement;\n const maxRequiredHeight: number = Math.max(this.maxHeight ?? 0, 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 character 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,21 +2,32 @@ 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
- private readonly htmlElementRef;
6
5
  maxHeight: number | null;
7
6
  lines: number;
8
- maxLines: number;
9
- private readonly splitOnWordsCharacter;
10
7
  truncationCharacters: string;
11
- constructor(htmlElementRef: ElementRef<HTMLElement>);
8
+ maxLines: number;
9
+ private cachedLineHeight;
10
+ private originalContent;
11
+ private isInitialized;
12
+ private readonly element;
13
+ constructor(elementRef: ElementRef<HTMLElement>);
12
14
  ngAfterViewInit(): void;
13
15
  ngOnChanges(changes: SimpleChanges): void;
16
+ private restoreOriginalContent;
14
17
  private clamp;
15
- private truncate;
16
- private getMaxHeight;
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;
17
25
  private getLineHeight;
18
- private getMaxLines;
19
- private getLastChild;
26
+ private calculateMaxLines;
27
+ /**
28
+ * Recursively finds the deepest text node, skipping empty nodes.
29
+ */
30
+ private findDeepestTextNode;
20
31
  static ɵfac: i0.ɵɵFactoryDeclaration<NgxClamp, never>;
21
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>;
22
33
  }
package/package.json CHANGED
@@ -1,20 +1,28 @@
1
1
  {
2
2
  "name": "@chitovas/ngx-clamp",
3
- "version": "0.2.2",
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
- "description": "angular library designed to elegantly manage text overflow within HTML elements. This library allows you to clamp text, adding ellipsis or a truncation text of your choice when the content exceeds a specified height.",
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
- "angular",
15
- "directive"
22
+ "truncate",
23
+ "ellipsis",
24
+ "overflow"
16
25
  ],
17
- "license": "MIT",
18
26
  "publishConfig": {
19
27
  "access": "public"
20
28
  },
@@ -26,15 +34,17 @@
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
  }