@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 CHANGED
@@ -3,50 +3,57 @@
3
3
  ![Build](https://github.com/Chitova263/ngx-clamp/workflows/main/badge.svg)
4
4
  [![NPM version](https://img.shields.io/npm/v/@chitovas/ngx-clamp.svg?style=flat-square)](https://www.npmjs.com/package/@chitovas/ngx-clamp)
5
5
  ![bundle size](https://img.shields.io/bundlephobia/minzip/@chitovas/ngx-clamp)
6
+ [![npm](https://img.shields.io/npm/dt/@chitovas/ngx-clamp.svg)](https://www.npmjs.com/package/@chitovas/ngx-clamp)
6
7
 
7
- Welcome to @chitovas/ngx-clamp, an 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.
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
- ## Motivation
10
-
11
- Solves overflow issues on older legacy browsers that don't support the `-webkit-line-clamp` or `line-clamp` property, ensuring consistent behavior across different platforms.
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
- ```ts
22
+ ```typescript
23
+ import { Component } from '@angular/core';
32
24
  import { NgxClamp } from '@chitovas/ngx-clamp';
33
25
 
34
26
  @Component({
35
- selector: 'my-component',
36
- template: `
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 MyComponent {}
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
- @chitovas/ngx-clamp is released under the MIT License.
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
- htmlElementRef;
6
- maxHeight = 0;
7
+ maxHeight = null;
8
+ lines = 0;
7
9
  truncationCharacters = '...';
8
10
  maxLines = 0;
9
- splitOnWordsCharacter = ' ';
10
- constructor(htmlElementRef) {
11
- this.htmlElementRef = htmlElementRef;
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
- if (changes['height']) {
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
- clamp() {
22
- this.maxLines = this.getMaxLines();
23
- const hostHtmlElement = this.htmlElementRef.nativeElement;
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
- // Recursively removes words from the text until its width or height is beneath maximum required height.
33
- truncate(maxRequiredHeight, node, words = undefined, isCurrentNodeValueSplitIntoWords = false) {
34
- // Removes truncation characters from node text
35
- const nodeValue = node.nodeValue?.replace(this.truncationCharacters, '');
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
- if (words) {
41
- const isTexFits = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;
42
- node.nodeValue = words.join(this.splitOnWordsCharacter) + this.truncationCharacters;
43
- if (isTexFits) {
44
- return;
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
- // If there are words left to remove, remove the last one and see if the nodeValue fits.
48
- if (words && words.length > 1) {
49
- words.pop();
50
- node.nodeValue = words.join(this.splitOnWordsCharacter) + this.truncationCharacters;
51
- }
52
- // No more words can be removed using this character
53
- else {
54
- words = undefined;
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
- if (words) {
57
- const isTexFits = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;
58
- if (isTexFits) {
59
- return;
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
- // No valid words produced
63
- else if (isCurrentNodeValueSplitIntoWords) {
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
- node = this.getLastChild(this.htmlElementRef.nativeElement);
68
- return this.truncate(maxRequiredHeight, node);
69
+ const previousNode = this.findDeepestTextNode(this.element);
70
+ if (previousNode) {
71
+ this.truncateNode(previousNode, maxHeight);
72
+ }
73
+ return;
69
74
  }
70
- 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;
71
78
  }
72
- getMaxHeight(maxLines, element) {
73
- const lineHeight = this.getLineHeight(element);
74
- return lineHeight * maxLines;
79
+ textFits(node, text, maxHeight) {
80
+ node.nodeValue = text;
81
+ return this.element.clientHeight <= maxHeight;
75
82
  }
76
- getLineHeight(element) {
77
- const cssStyleDeclaration = getComputedStyle(element, 'line-height');
78
- const lineHeight = cssStyleDeclaration.lineHeight;
79
- if (cssStyleDeclaration.lineHeight === 'normal') {
80
- // Normal line heights vary from browser to browser. The spec recommends a value between 1.0 and 1.2 of the font size.
81
- 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
+ }
82
97
  }
83
- return Math.ceil(parseInt(lineHeight, 10));
98
+ return result;
84
99
  }
85
- getMaxLines() {
86
- // 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.
87
- const hostHtmlElement = this.htmlElementRef.nativeElement;
88
- const availableHeight = this.maxHeight ?? hostHtmlElement.clientHeight;
89
- 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);
90
117
  }
91
- getLastChild(node) {
92
- // 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) {
93
122
  if (!node.lastChild) {
94
123
  return node;
95
124
  }
96
- // Current element has children, need to go deeper and get last child as a text node
97
- if (node.lastChild.childNodes && node.lastChild.childNodes.length > 0) {
98
- 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));
99
129
  }
100
- // This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying
101
- else if (node.lastChild.parentNode &&
102
- (!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) {
103
133
  node.lastChild.parentNode.removeChild(node.lastChild);
104
- return this.getLastChild(this.htmlElementRef.nativeElement);
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.10", ngImport: i0, type: NgxClamp, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
112
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.10", type: NgxClamp, isStandalone: true, selector: "[ngxClamp]", inputs: { maxHeight: "maxHeight", 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 });
113
140
  }
114
- 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: [{
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
- args: [{ required: true }]
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
- private readonly htmlElementRef;
6
- maxHeight: number;
5
+ maxHeight: number | null;
6
+ lines: number;
7
7
  truncationCharacters: string;
8
8
  maxLines: number;
9
- private readonly splitOnWordsCharacter;
10
- constructor(htmlElementRef: ElementRef<HTMLElement>);
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
- private truncate;
15
- 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;
16
25
  private getLineHeight;
17
- private getMaxLines;
18
- private getLastChild;
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": true; }; "truncationCharacters": { "alias": "truncationCharacters"; "required": false; }; }, {}, never, never, true, never>;
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.1.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
  },
21
29
  "peerDependencies": {
22
- "@angular/common": "^20.3.0",
23
- "@angular/core": "^20.3.0"
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
  }