I made an image for this question to make it easier to understand.
Is it possible to create an ellipsis on a
I wrote an angular component that solves the problem. It splits a given text into span elements. After rendering, it removes all overflowing elements and places the ellipsis right after the last visible element.
Usage example:
<app-text-overflow-ellipsis [text]="someText" style="max-height: 50px"></app-text-overflow-ellipsis>
Stackblitz demo: https://stackblitz.com/edit/angular-wfdqtd
The component:
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef, HostListener,
Input,
OnChanges,
ViewChild
} from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-text-overflow-ellipsis',
template: `
<span *ngFor="let word of words; let i = index" [innerHTML]="word + (!endsWithHyphen(i) ? ' ' : '')"></span>
<span #ellipsis [hidden]="!showEllipsis && !initializing" [class.initializing]="initializing" [innerHTML]="'...' + (initializing ? ' ' : '')"></span>
`,
styles: [`
:host {
display: block;
position: relative;
}
.initializing {
opacity: 0;
}
`
]
})
export class TextOverflowEllipsisComponent implements OnChanges {
@Input()
text: string;
showEllipsis: boolean;
initializing: boolean;
words: string[];
@ViewChild('ellipsis')
ellipsisElement: ElementRef;
constructor(private element: ElementRef, private cdRef: ChangeDetectorRef) {}
ngOnChanges(){
this.init();
}
@HostListener('window:resize')
init(){
// add space after hyphens
let text = this.text.replace(/-/g, '- ') ;
this.words = text.split(' ');
this.initializing = true;
this.showEllipsis = false;
this.cdRef.detectChanges();
setTimeout(() => {
this.initializing = false;
let containerElement = this.element.nativeElement;
let containerWidth = containerElement.clientWidth;
let wordElements = (<HTMLElement[]>Array.from(containerElement.childNodes)).filter((element) =>
element.getBoundingClientRect && element !== this.ellipsisElement.nativeElement
);
let lines = this.getLines(wordElements, containerWidth);
let indexOfLastLine = lines.length - 1;
let lineHeight = this.deductLineHeight(lines);
if (!lineHeight) {
return;
}
let indexOfLastVisibleLine = Math.floor(containerElement.clientHeight / lineHeight) - 1;
if (indexOfLastVisibleLine < indexOfLastLine) {
// remove overflowing lines
for (let i = indexOfLastLine; i > indexOfLastVisibleLine; i--) {
for (let j = 0; j < lines[i].length; j++) {
this.words.splice(-1, 1);
}
}
// make ellipsis fit into last visible line
let lastVisibleLine = lines[indexOfLastVisibleLine];
let indexOfLastWord = lastVisibleLine.length - 1;
let lastVisibleLineWidth = lastVisibleLine.map(
(element) => element.getBoundingClientRect().width
).reduce(
(width, sum) => width + sum, 0
);
let ellipsisWidth = this.ellipsisElement.nativeElement.getBoundingClientRect().width;
for (let i = indexOfLastWord; lastVisibleLineWidth + ellipsisWidth >= containerWidth; i--) {
let wordWidth = lastVisibleLine[i].getBoundingClientRect().width;
lastVisibleLineWidth -= wordWidth;
this.words.splice(-1, 1);
}
this.showEllipsis = true;
}
this.cdRef.detectChanges();
// delay is to prevent from font loading issues
}, 1000);
}
deductLineHeight(lines: HTMLElement[][]): number {
try {
let rect0 = lines[0][0].getBoundingClientRect();
let y0 = rect0['y'] || rect0['top'] || 0;
let rect1 = lines[1][0].getBoundingClientRect();
let y1 = rect1['y'] || rect1['top'] || 0;
let lineHeight = y1 - y0;
if (lineHeight > 0){
return lineHeight;
}
} catch (e) {}
return null;
}
getLines(nodes: HTMLElement[], clientWidth: number): HTMLElement[][] {
let lines = [];
let currentLine = [];
let currentLineWidth = 0;
nodes.forEach((node) => {
if (!node.getBoundingClientRect){
return;
}
let nodeWidth = node.getBoundingClientRect().width;
if (currentLineWidth + nodeWidth > clientWidth){
lines.push(currentLine);
currentLine = [];
currentLineWidth = 0;
}
currentLine.push(node);
currentLineWidth += nodeWidth;
});
lines.push(currentLine);
return lines;
}
endsWithHyphen(index: number): boolean {
let length = this.words[index].length;
return this.words[index][length - 1] === '-' && this.words[index + 1] && this.words[index + 1][0];
}
}
Try the jQuery.dotdotdot plugin.
$(".ellipsis").dotdotdot();
With this code there is no need for an extra wrapper div if the element has it's height limited by a max-height style.
// Shorten texts in overflowed paragraphs to emulate Operas text-overflow: -o-ellipsis-lastline
$('.ellipsis-lastline').each(function(i, e) {
var $e = $(e), original_content = $e.text();
while (e.scrollHeight > e.clientHeight)
$e.text($e.text().replace(/\W*\w+\W*$/, '…'));
$e.attr('data-original-content', original_content);
});
Also it saves the original text in a data attribute that can be displayed using only styles, eg. on mouse over:
.ellipsis-lastline {
max-height: 5em;
}
.ellipsis-lastline:before {
content: attr(data-original-content);
position: absolute;
display: none;
}
.ellipsis-lastline:hover:before {
display: block;
}
Very simple func will do.
Directive:
$scope.truncateAlbumName = function (name) {
if (name.length > 36) {
return name.slice(0, 34) + "..";
} else {
return name;
}
};
View:
<#p>{{truncateAlbumName(album.name)}}<#/p>
I have a solution which works well but instead an ellipsis it uses a gradient. The advantages are that you don't have to do any JavaScript calculations and it works for variable width containers including table cells. It uses a couple of extra divs, but it's very easy to implement.
http://salzerdesign.com/blog/?p=453
Edit: Sorry, I did't know that the link wasn't enough. The solution is to put a div around the text, and style the div to control the overflow. Inside the div put another div with a "fade" gradient which can be made by using CSS or an image (for old IE). The gradient goes from transparent to the background color of the table cell and is a bit wider than an ellipsis. If the text is long and overflows, it goes under the "fade" div and looks "faded out". If the text is short, the fade is invisible so there is no problem. The two containers can be adjusted to let one or multiple lines show by setting the height of the container as a multiple of the text line height. The "fade" div can be positioned to only cover the last line.
Just a quick basic idea.
I was testing with the following markup:
<div id="fos">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nisi ligula, dapibus a volutpat sit amet, mattis et dui. Nunc porttitor accumsan orci id luctus. Phasellus ipsum metus, tincidunt non rhoncus id, dictum a lectus. Nam sed ipsum a lacus sodales eleifend. Vestibulum lorem felis, rhoncus elementum vestibulum eget, dictum ut velit. Nullam venenatis, elit in suscipit imperdiet, orci purus posuere mauris, quis adipiscing ipsum urna ac quam.</p>
</div>
And CSS:
#fos { width: 300px; height: 190px; overflow: hidden; }
#fos p { padding: 10px; margin: 0; }
Applying this jQuery will accomplish the desired result:
var $p = $('#fos p');
var divh = $('#fos').height();
while ($p.outerHeight() > divh) {
$p.text(function (index, text) {
return text.replace(/\W*\s(\S)*$/, '...');
});
}
It repeatedly tries to remove the last word of the text until it reaches the desired size. Because of the overflow: hidden; the process remains invisible and even with JS turned off the result remains 'visually correct' (without the "..." of course).
If you combine this with a sensible truncation on the server-side (that leaves only a small overhead) then it will run quicker :).
Again, this is not a complete solution, just an idea.
UPDATE: Added a jsFiddle Demo.