@chitovas/ngx-clamp 0.1.1 → 0.2.2

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
@@ -1,48 +1,209 @@
1
- # @chitova/ngx-clamp
1
+ # @chitovas/ngx-clamp
2
2
 
3
- Welcome to @chitova/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.
3
+ ![Build](https://github.com/Chitova263/ngx-clamp/workflows/main/badge.svg)
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
+ ![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)
4
7
 
5
- # Motivation
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.
6
9
 
7
- 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.
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.
8
13
 
9
14
  ## Features
10
15
 
11
- - **Easy Integration**: Seamlessly integrate `ngx-clamp` directive into your Angular applications to manage text overflow.
12
- - **Customizable Truncation**: Use ellipsis or specify your own truncation text for clamped content.
13
- - **Nested Element Support**: Clamp text within nested HTML elements effortlessly.
14
- - **Height Configuration**: Set a maximum height for text before clamping activates.
15
- - **Legacy Browser Compatibility**: Solves overflow issues on older browsers that don't support the -webkit-line-clamp property, ensuring consistent behavior across different platforms.
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
16
22
 
17
23
  ## Installation
18
24
 
19
- Install the package using npm:
20
-
21
25
  ```bash
22
- npm install @chitova/ngx-clamp
26
+ npm install @chitovas/ngx-clamp
23
27
  ```
24
28
 
25
- ## Usage
29
+ ## Quick Start
26
30
 
27
- ```ts
28
- import { NgxClamp } from '@chitova/ngx-clamp';
31
+ ### 1. Import the Directive
32
+
33
+ ```typescript
34
+ import { Component } from '@angular/core';
35
+ import { NgxClamp } from '@chitovas/ngx-clamp';
29
36
 
30
37
  @Component({
31
- selector: 'my-component',
38
+ selector: 'app-example',
39
+ standalone: true,
40
+ imports: [NgxClamp],
41
+ template: ` <div ngxClamp [lines]="3">Your long text content here...</div> `,
42
+ })
43
+ export class ExampleComponent {}
44
+ ```
45
+
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
65
+
66
+ ```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],
32
104
  template: `
33
- <div ngxClamp [maxHeight]="100" truncationText="...">
34
- Your long text goes here, and it will be clamped if it exceeds the specified height.
35
- <p>Your long text goes here in nested element</p>
36
- </div>
37
- <div ngxClamp [maxHeight]="150" truncationText="Read more...">
38
- This is a longer paragraph that will be clamped to fit within the specified height.
39
- </div>
105
+ <article class="card">
106
+ <h2>{{ article.title }}</h2>
107
+ <div ngxClamp [lines]="3" truncationText=" [Read more]">
108
+ {{ article.content }}
109
+ </div>
110
+ </article>
40
111
  `,
112
+ })
113
+ export class ArticleCardComponent {
114
+ article = {
115
+ title: 'Understanding Angular',
116
+ content: 'Very long article content...',
117
+ };
118
+ }
119
+ ```
120
+
121
+ ### Product Description
122
+
123
+ ```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,
41
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
+ `,
42
152
  })
43
- export class MyComponent {}
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
+ }
44
164
  ```
45
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
+ }
187
+ ```
188
+
189
+ **After** (with ngx-clamp):
190
+
191
+ ```html
192
+ <div ngxClamp [lines]="3" class="text">Your content here</div>
193
+ ```
194
+
195
+ ## Contributing
196
+
197
+ Contributions are welcome! Please feel free to submit a Pull Request.
198
+
46
199
  ## License
47
200
 
48
- @chitova/ngx-clamp is released under the MIT License.
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)
@@ -3,10 +3,11 @@ import { Input, Directive } from '@angular/core';
3
3
 
4
4
  class NgxClamp {
5
5
  htmlElementRef;
6
- maxHeight = 0;
7
- truncationCharacters = '...';
6
+ maxHeight = null;
7
+ lines = 0;
8
8
  maxLines = 0;
9
9
  splitOnWordsCharacter = ' ';
10
+ truncationCharacters = '...';
10
11
  constructor(htmlElementRef) {
11
12
  this.htmlElementRef = htmlElementRef;
12
13
  }
@@ -14,14 +15,17 @@ class NgxClamp {
14
15
  this.clamp();
15
16
  }
16
17
  ngOnChanges(changes) {
17
- if (changes['height']) {
18
+ if (changes['maxHeight'] || changes['lines']) {
18
19
  this.clamp();
19
20
  }
20
21
  }
21
22
  clamp() {
22
- this.maxLines = this.getMaxLines();
23
+ if (!this.maxHeight && !this.lines) {
24
+ return;
25
+ }
26
+ this.maxLines = this.lines ? this.lines : this.getMaxLines();
23
27
  const hostHtmlElement = this.htmlElementRef.nativeElement;
24
- const maxRequiredHeight = Math.max(this.maxHeight, this.getMaxHeight(this.maxLines, hostHtmlElement));
28
+ const maxRequiredHeight = Math.max(this.maxHeight ?? 0, this.getMaxHeight(this.maxLines, hostHtmlElement));
25
29
  if (maxRequiredHeight < hostHtmlElement.clientHeight) {
26
30
  const lastChild = this.getLastChild(hostHtmlElement);
27
31
  if (lastChild) {
@@ -31,7 +35,7 @@ class NgxClamp {
31
35
  }
32
36
  // Recursively removes words from the text until its width or height is beneath maximum required height.
33
37
  truncate(maxRequiredHeight, node, words = undefined, isCurrentNodeValueSplitIntoWords = false) {
34
- // Removes truncation characters from node text
38
+ // Removes truncation character from node text
35
39
  const nodeValue = node.nodeValue?.replace(this.truncationCharacters, '');
36
40
  if (!words && nodeValue) {
37
41
  words = nodeValue.split(this.splitOnWordsCharacter);
@@ -109,16 +113,18 @@ class NgxClamp {
109
113
  }
110
114
  }
111
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 });
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 });
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 });
113
117
  }
114
118
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: NgxClamp, decorators: [{
115
119
  type: Directive,
116
120
  args: [{
117
121
  selector: '[ngxClamp]',
122
+ standalone: true,
118
123
  }]
119
124
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { maxHeight: [{
120
- type: Input,
121
- args: [{ required: true }]
125
+ type: Input
126
+ }], lines: [{
127
+ type: Input
122
128
  }], truncationCharacters: [{
123
129
  type: Input
124
130
  }] } });
@@ -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\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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"}
package/index.d.ts CHANGED
@@ -3,10 +3,11 @@ import { AfterViewInit, OnChanges, ElementRef, SimpleChanges } from '@angular/co
3
3
 
4
4
  declare class NgxClamp implements AfterViewInit, OnChanges {
5
5
  private readonly htmlElementRef;
6
- maxHeight: number;
7
- truncationCharacters: string;
6
+ maxHeight: number | null;
7
+ lines: number;
8
8
  maxLines: number;
9
9
  private readonly splitOnWordsCharacter;
10
+ truncationCharacters: string;
10
11
  constructor(htmlElementRef: ElementRef<HTMLElement>);
11
12
  ngAfterViewInit(): void;
12
13
  ngOnChanges(changes: SimpleChanges): void;
@@ -17,7 +18,7 @@ declare class NgxClamp implements AfterViewInit, OnChanges {
17
18
  private getMaxLines;
18
19
  private getLastChild;
19
20
  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>;
21
+ 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
22
  }
22
23
 
23
24
  export { NgxClamp };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chitovas/ngx-clamp",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "author": "Nigel Mukandi",
5
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
6
  "repository": {
@@ -19,8 +19,8 @@
19
19
  "access": "public"
20
20
  },
21
21
  "peerDependencies": {
22
- "@angular/common": "^20.3.0",
23
- "@angular/core": "^20.3.0"
22
+ "@angular/common": ">=18.0.0",
23
+ "@angular/core": ">=18.0.0"
24
24
  },
25
25
  "dependencies": {
26
26
  "tslib": "^2.3.0"