UNPKG

m-truncate

Version:

`mTruncate` is a lightweight and customizable Angular directive for truncating text with support for tooltips. It allows you to truncate text based on width or a specified number of lines and display a tooltip when the full text is not visible.

451 lines (443 loc) 19.9 kB
import * as i0 from '@angular/core'; import { Injectable, Component, Directive, Input, HostListener, NgModule } from '@angular/core'; import { Subject, fromEvent, debounceTime, takeUntil } from 'rxjs'; class MTruncateService { constructor() { this.direction = 'ltr'; // Default to 'ltr' } setDirection(dir) { this.direction = dir; } getDirection() { return this.direction; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [] }); class MTruncateComponent { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.1", type: MTruncateComponent, isStandalone: true, selector: "lib-m-truncate", ngImport: i0, template: ` <p> m-truncate works! </p> `, isInline: true, styles: [""] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateComponent, decorators: [{ type: Component, args: [{ selector: 'lib-m-truncate', standalone: true, imports: [], template: ` <p> m-truncate works! </p> ` }] }] }); class MTruncateDirective { constructor(el, renderer, mTruncateService) { this.el = el; this.renderer = renderer; this.mTruncateService = mTruncateService; /** Whether to show tooltip on truncated text hover */ this.showTooltip = true; /** Number of lines to truncate the text to */ this.truncateLines = 0; /** Maximum width for truncation */ this.maxTruncateWidth = 0; /** Maximum width for tooltip */ this.maxTooltipWidth = 0; /** Background color of the tooltip */ this.bgColor = '#fff'; /** Text color of the tooltip */ this.color = '#1c1c28'; /** Tooltip position (top, bottom, left, right) */ this.tooltipPosition = 'top'; /** Text direction (LTR, RTL) */ this.tooltipDirection = 'ltr'; /** Suffix to append when text is truncated */ this.truncateSuffix = '...'; /** ID for the tooltip element */ this.id = 'mId'; /** Maximum number of characters before truncation */ this.truncateChar = 0; this.fullText = ''; this.tooltip = null; this.tooltipArrow = null; this.destroy$ = new Subject(); } ngAfterViewInit() { this.element = this.el.nativeElement; this.fullText = this.el.nativeElement.innerText.trim(); this.tooltipDirection = this.mTruncateService.getDirection(); if (!this.fullText || this.fullText.trim().length <= 3) return; fromEvent(window, 'resize') .pipe(debounceTime(500), takeUntil(this.destroy$)) .subscribe(() => (this.resize ? this.onResize() : '')); this.truncateMethod(); } ngOnChanges(changes) { if (changes['truncateChar']) { this.truncateMethod(); } if (changes['truncateLines']) { this.truncateMethod(); } if (changes['maxTruncateWidth']) { setTimeout(() => { this.truncateMethod(); }); } if (changes['textChange']) { this.fullText = this.textChange; setTimeout(() => { this.truncateMethod(); }); } } truncateMethod() { setTimeout(() => { if (this.truncateChar > 0) { this.truncateByChar(); } else { if (this.truncateLines > 1) { this.applyRowsTruncate(); } else { setTimeout(() => { this.applyTruncate(); }); } } }); } truncateByChar() { return this.fullText.length <= this.truncateChar ? this.fullText : (this.element.innerText = this.fullText.slice(0, this.truncateChar) + this.truncateSuffix); } applyTruncate() { const element = this.el.nativeElement; element.innerText = this.fullText; // Reset to full text if (this.maxTruncateWidth) { element.style.maxWidth = this.maxTruncateWidth + 'px'; } const maxWidth = element.clientWidth; // Get available width element.style.textWrap = 'nowrap'; element.style.wordBreak = 'break-word'; let left = 0; let right = this.fullText.length; let truncatedText = this.fullText; if (element.scrollWidth != maxWidth) { while (left <= right) { let mid = Math.floor((left + right) / 2); element.innerText = this.fullText.substring(0, mid) + this.truncateSuffix; if (element.scrollWidth > maxWidth) { right = mid - 1; // Too long, reduce size } else { left = mid + 1; // Too short, increase size truncatedText = this.fullText.substring(0, mid) + this.truncateSuffix; } } } element.innerText = truncatedText; // Set final truncated text } applyRowsTruncate() { const element = this.el.nativeElement; element.innerText = this.fullText; // Reset to full text const lineHeight = parseFloat(window.getComputedStyle(element).lineHeight); const maxHeight = lineHeight * this.truncateLines; if (element.scrollHeight > maxHeight) { let left = 0; let right = this.fullText.length; let truncatedText = this.fullText; while (left <= right) { let mid = Math.floor((left + right) / 2); element.innerText = this.fullText.substring(0, mid) + this.truncateSuffix; if (element.scrollHeight > maxHeight) { right = mid - 1; // Too long, reduce text } else { left = mid + 1; // Too short, increase text truncatedText = this.fullText.substring(0, mid) + this.truncateSuffix; } } element.innerText = truncatedText; } } getTooltipDirection(dir) { this.tooltipDirection = dir; } onResize() { if (this.truncateChar > 0) { this.truncateByChar(); } else { if (this.truncateLines > 1) { this.applyRowsTruncate(); } else { this.applyTruncate(); } } } createTooltip() { if (this.tooltip) return; // Avoid creating multiple tooltips let maxWidth = this.el.nativeElement.clientWidth; this.tooltip = this.renderer.createElement('div'); this.tooltip?.classList.add('mTooltip'); this.tooltip?.setAttribute('id', this.id); this.renderer.appendChild(document.body, this.tooltip); this.renderer.setProperty(this.tooltip, 'innerText', this.fullText); this.renderer.setStyle(this.tooltip, 'opacity', '0'); // Start hidden this.renderer.setStyle(this.tooltip, 'animation', 'fadeIn 0.35s ease-out forwards'); // Apply animation // Apply styles this.renderer.setStyle(this.tooltip, 'position', 'fixed'); this.renderer.setStyle(this.tooltip, 'background', this.bgColor); this.renderer.setStyle(this.tooltip, 'color', this.color); this.renderer.setStyle(this.tooltip, 'whiteSpace', 'pre-wrap'); // Allows multi-line text this.renderer.setStyle(this.tooltip, 'wordWrap', 'break-word'); // Wrap long words this.renderer.setStyle(this.tooltip, 'direction', this.tooltipDirection); // direction this.renderer.setStyle(this.tooltip, 'box-shadow', '0px 4px 15px 0px rgba(0, 0, 0, 0.2)'); this.renderer.setStyle(this.tooltip, 'width', 'fit-content'); this.renderer.setStyle(this.tooltip, 'max-width', this.maxTooltipWidth ? this.maxTooltipWidth + 'px' : maxWidth + 'px'); this.renderer.setStyle(this.tooltip, 'textOverflow', 'ellipsis'); this.renderer.setStyle(this.tooltip, 'padding', '10px'); this.renderer.setStyle(this.tooltip, 'borderRadius', '6px'); this.renderer.setStyle(this.tooltip, 'fontSize', '14px'); this.renderer.setStyle(this.tooltip, 'zIndex', '999999999999999'); this.renderer.setStyle(this.tooltip, 'pointerEvents', 'none'); // Prevent interference // Tooltip Arrow this.tooltipArrow = this.renderer.createElement('div'); this.tooltipArrow?.classList.add('mTooltipArrow'); this.renderer.appendChild(this.tooltip, this.tooltipArrow); this.renderer.setStyle(this.tooltipArrow, 'position', 'absolute'); this.renderer.setStyle(this.tooltipArrow, 'width', '0'); this.renderer.setStyle(this.tooltipArrow, 'height', '0'); this.renderer.setStyle(this.tooltipArrow, 'borderStyle', 'solid'); this.positionTooltip(); // Animation this.addKeyframes(); } removeTooltip() { if (this.tooltip) { this.renderer.setStyle(this.tooltip, 'animation', 'fadeOut 0.5s ease-out forwards'); setTimeout(() => { this.renderer.removeChild(document.body, this.tooltip); this.tooltip = null; }); } } positionTooltip() { if (!this.tooltip) return; const rect = this.el.nativeElement.getBoundingClientRect(); const tooltipRect = this.tooltip.getBoundingClientRect(); let top = 0, left = 0; switch (this.tooltipPosition) { case 'top': top = rect.top - tooltipRect.height - 5; left = rect.left + (rect.width - tooltipRect.width) / 2; if (top < 0) { top = rect.bottom + 5; // Move below if not enough space this.arrowPositionUp(); } else { // Arrow pointing **down** (placed at bottom) this.arrowPositionDown(); } break; case 'bottom': top = rect.bottom + 5; left = rect.left + (rect.width - tooltipRect.width) / 2; if (top + tooltipRect.height > window.innerHeight) { top = rect.top - tooltipRect.height - 5; this.arrowPositionDown(); } else { // Arrow pointing **up** (placed at top) this.arrowPositionUp(); } break; case 'inset-inline-start': case 'right': top = rect.top + (rect.height - tooltipRect.height) / 2; left = rect.left - tooltipRect.width - 5; if (left < 0) { left = rect.right + 5; // Move to the right if not enough space this.arrowPositionLeft(); } else { this.arrowPositionRight(); } // Arrow pointing **right** (placed at end) break; case 'inset-inline-end': case 'left': top = rect.top + (rect.height - tooltipRect.height) / 2; left = rect.right + 5; if (left + tooltipRect.width > window.innerWidth) { left = rect.left - tooltipRect.width - 5; this.arrowPositionRight(); } else { // Arrow pointing **left** (placed at start) this.arrowPositionLeft(); } break; default: top = rect.top - tooltipRect.height - 5; left = rect.left + (rect.width - tooltipRect.width) / 2; if (top < 0) { top = rect.bottom + 5; // Move below if not enough space this.arrowPositionUp(); } else { // Arrow pointing **down** (placed at bottom) this.arrowPositionDown(); } break; } this.renderer.setStyle(this.tooltip, 'top', `${top}px`); this.renderer.setStyle(this.tooltip, 'left', `${left}px`); } arrowPositionDown() { this.renderer.setStyle(this.tooltipArrow, 'borderWidth', '10px 10px 0 10px'); this.renderer.setStyle(this.tooltipArrow, 'borderColor', `${this.bgColor} transparent transparent transparent`); this.renderer.setStyle(this.tooltipArrow, 'left', '50%'); this.renderer.setStyle(this.tooltipArrow, 'bottom', '-8px'); this.renderer.setStyle(this.tooltipArrow, 'transform', 'translateX(-50%)'); } arrowPositionUp() { this.renderer.setStyle(this.tooltipArrow, 'borderWidth', '0 10px 10px 10px'); this.renderer.setStyle(this.tooltipArrow, 'borderColor', `transparent transparent ${this.bgColor} transparent`); this.renderer.setStyle(this.tooltipArrow, 'left', '50%'); this.renderer.setStyle(this.tooltipArrow, 'top', '-8px'); this.renderer.setStyle(this.tooltipArrow, 'transform', 'translateX(-50%)'); } arrowPositionRight() { this.renderer.setStyle(this.tooltipArrow, 'borderWidth', '10px 0 10px 10px'); this.renderer.setStyle(this.tooltipArrow, 'borderColor', `transparent transparent transparent ${this.bgColor}`); this.renderer.setStyle(this.tooltipArrow, 'top', '50%'); this.renderer.setStyle(this.tooltipArrow, 'right', '-8px'); this.renderer.setStyle(this.tooltipArrow, 'transform', 'translateY(-50%)'); } arrowPositionLeft() { this.renderer.setStyle(this.tooltipArrow, 'borderWidth', '10px 10px 10px 0'); this.renderer.setStyle(this.tooltipArrow, 'borderColor', `transparent ${this.bgColor} transparent transparent`); this.renderer.setStyle(this.tooltipArrow, 'top', '50%'); this.renderer.setStyle(this.tooltipArrow, 'left', '-8px'); this.renderer.setStyle(this.tooltipArrow, 'transform', 'translateY(-50%)'); } addKeyframes() { if (document.getElementById('fadeInKeyframes')) return; // Avoid duplicates const styleSheet = this.renderer.createElement('style'); styleSheet.id = 'fadeInKeyframes'; this.renderer.setProperty(styleSheet, 'innerHTML', ` @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } } `); this.renderer.appendChild(document.head, styleSheet); } handleMouseOver(event) { const target = event.target; if (target === this.el.nativeElement) { let currentText = this.element.innerText; // Remove truncateSuffix only if it exists at the end if (currentText.endsWith(this.truncateSuffix)) { currentText = currentText.slice(0, -this.truncateSuffix.length); } if (currentText.length < this.fullText.length && this.showTooltip) { this.createTooltip(); } } } handleMouseOut(event) { this.removeTooltip(); } // Unsubscribe when the directive is destroyed ngOnDestroy() { this.removeTooltip(); this.destroy$.next(); this.destroy$.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: MTruncateService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.1", type: MTruncateDirective, isStandalone: true, selector: "[mTruncate]", inputs: { showTooltip: "showTooltip", truncateLines: "truncateLines", maxTruncateWidth: "maxTruncateWidth", maxTooltipWidth: "maxTooltipWidth", bgColor: "bgColor", color: "color", tooltipPosition: "tooltipPosition", tooltipDirection: "tooltipDirection", truncateSuffix: "truncateSuffix", id: "id", truncateChar: "truncateChar", resize: "resize", textChange: "textChange" }, host: { listeners: { "mouseenter": "handleMouseOver($event)", "mouseleave": "handleMouseOut()" } }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateDirective, decorators: [{ type: Directive, args: [{ selector: '[mTruncate]', standalone: true, }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: MTruncateService }], propDecorators: { showTooltip: [{ type: Input }], truncateLines: [{ type: Input }], maxTruncateWidth: [{ type: Input }], maxTooltipWidth: [{ type: Input }], bgColor: [{ type: Input }], color: [{ type: Input }], tooltipPosition: [{ type: Input }], tooltipDirection: [{ type: Input }], truncateSuffix: [{ type: Input }], id: [{ type: Input }], truncateChar: [{ type: Input }], resize: [{ type: Input }], textChange: [{ type: Input }], handleMouseOver: [{ type: HostListener, args: ['mouseenter', ['$event']] }], handleMouseOut: [{ type: HostListener, args: ['mouseleave'] }] } }); class MTruncateModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.0.1", ngImport: i0, type: MTruncateModule, imports: [MTruncateDirective], exports: [MTruncateDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.1", ngImport: i0, type: MTruncateModule, decorators: [{ type: NgModule, args: [{ imports: [MTruncateDirective], exports: [MTruncateDirective], }] }] }); /* * Public API Surface of m-truncate */ /** * Generated bundle index. Do not edit. */ export { MTruncateComponent, MTruncateDirective, MTruncateModule, MTruncateService }; //# sourceMappingURL=m-truncate.mjs.map