@chitovas/ngx-clamp 0.1.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.
@@ -0,0 +1,131 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Input, Directive } from '@angular/core';
3
+
4
+ class NgxClamp {
5
+ htmlElementRef;
6
+ maxHeight = 0;
7
+ truncationCharacters = '...';
8
+ maxLines = 0;
9
+ splitOnWordsCharacter = ' ';
10
+ constructor(htmlElementRef) {
11
+ this.htmlElementRef = htmlElementRef;
12
+ }
13
+ ngAfterViewInit() {
14
+ this.clamp();
15
+ }
16
+ ngOnChanges(changes) {
17
+ if (changes['height']) {
18
+ this.clamp();
19
+ }
20
+ }
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
+ }
30
+ }
31
+ }
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;
39
+ }
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;
45
+ }
46
+ }
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;
55
+ }
56
+ if (words) {
57
+ const isTexFits = this.htmlElementRef.nativeElement.clientHeight <= maxRequiredHeight;
58
+ if (isTexFits) {
59
+ return;
60
+ }
61
+ }
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
+ node.nodeValue = this.truncationCharacters;
67
+ node = this.getLastChild(this.htmlElementRef.nativeElement);
68
+ return this.truncate(maxRequiredHeight, node);
69
+ }
70
+ return this.truncate(maxRequiredHeight, node, words, isCurrentNodeValueSplitIntoWords);
71
+ }
72
+ getMaxHeight(maxLines, element) {
73
+ const lineHeight = this.getLineHeight(element);
74
+ return lineHeight * maxLines;
75
+ }
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);
82
+ }
83
+ return Math.ceil(parseInt(lineHeight, 10));
84
+ }
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);
90
+ }
91
+ getLastChild(node) {
92
+ // Gets an element's last child. That may be another node or a node's contents.
93
+ if (!node.lastChild) {
94
+ return node;
95
+ }
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));
99
+ }
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)) {
103
+ 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;
109
+ }
110
+ }
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 });
113
+ }
114
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.10", ngImport: i0, type: NgxClamp, decorators: [{
115
+ type: Directive,
116
+ args: [{
117
+ selector: '[ngxClamp]',
118
+ }]
119
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { maxHeight: [{
120
+ type: Input,
121
+ args: [{ required: true }]
122
+ }], truncationCharacters: [{
123
+ type: Input
124
+ }] } });
125
+
126
+ /**
127
+ * Generated bundle index. Do not edit.
128
+ */
129
+
130
+ export { NgxClamp };
131
+ //# sourceMappingURL=chitovas-ngx-clamp.mjs.map
@@ -0,0 +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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"}
package/index.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import * as i0 from '@angular/core';
2
+ import { AfterViewInit, OnChanges, ElementRef, SimpleChanges } from '@angular/core';
3
+
4
+ declare class NgxClamp implements AfterViewInit, OnChanges {
5
+ private readonly htmlElementRef;
6
+ maxHeight: number;
7
+ truncationCharacters: string;
8
+ maxLines: number;
9
+ private readonly splitOnWordsCharacter;
10
+ constructor(htmlElementRef: ElementRef<HTMLElement>);
11
+ ngAfterViewInit(): void;
12
+ ngOnChanges(changes: SimpleChanges): void;
13
+ private clamp;
14
+ private truncate;
15
+ private getMaxHeight;
16
+ private getLineHeight;
17
+ private getMaxLines;
18
+ private getLastChild;
19
+ 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
+ }
22
+
23
+ export { NgxClamp };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@chitovas/ngx-clamp",
3
+ "version": "0.1.0",
4
+ "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
+ "repository": {
7
+ "type": "git",
8
+ "url": "git://github.com/Chitova263/ngx-clamp.git"
9
+ },
10
+ "keywords": [
11
+ "clamp",
12
+ "line-clamp",
13
+ "webkit-line-clamp",
14
+ "angular",
15
+ "directive"
16
+ ],
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "peerDependencies": {
22
+ "@angular/common": "^20.3.0",
23
+ "@angular/core": "^20.3.0"
24
+ },
25
+ "dependencies": {
26
+ "tslib": "^2.3.0"
27
+ },
28
+ "sideEffects": false,
29
+ "module": "fesm2022/chitovas-ngx-clamp.mjs",
30
+ "typings": "index.d.ts",
31
+ "exports": {
32
+ "./package.json": {
33
+ "default": "./package.json"
34
+ },
35
+ ".": {
36
+ "types": "./index.d.ts",
37
+ "default": "./fesm2022/chitovas-ngx-clamp.mjs"
38
+ }
39
+ }
40
+ }