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
JavaScript
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', `
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