ohayolibs
Version:
Ohayo is a set of essential modules for ohayojp.
225 lines (204 loc) • 6.82 kB
text/typescript
import { DOCUMENT } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
Input,
NgZone,
OnChanges,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { BooleanInput, InputBoolean, InputNumber, NumberInput } from '@ohayo/util';
import { take } from 'rxjs/operators';
export class EllipsisComponent implements AfterViewInit, OnChanges {
static ngAcceptInputType_tooltip: BooleanInput;
static ngAcceptInputType_length: NumberInput;
static ngAcceptInputType_lines: NumberInput;
static ngAcceptInputType_fullWidthRecognition: BooleanInput;
// tslint:disable-next-line:no-string-literal
private isSupportLineClamp = this.doc.body.style['webkitLineClamp'] !== undefined;
private orgEl: ElementRef;
private shadowOrgEl: ElementRef;
private shadowTextEl: ElementRef;
private inited = false;
orgHtml: SafeHtml;
type = 'default';
cls = {};
text = '';
targetCount = 0;
// #region fields
tooltip = false;
length: number;
lines: number;
fullWidthRecognition = false;
tail = '...';
// #endregion
get linsWord(): string {
const { targetCount, text, tail } = this;
return (targetCount > 0 ? text.substring(0, targetCount) : '') + (targetCount > 0 && targetCount < text.length ? tail : '');
}
constructor(
private el: ElementRef,
private ngZone: NgZone,
private dom: DomSanitizer,
private doc: Document,
private cdr: ChangeDetectorRef,
) { }
private getStrFullLength(str: string): number {
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0);
if (charCode >= 0 && charCode <= 128) {
return pre + 1;
}
return pre + 2;
}, 0);
}
private cutStrByFullLength(str: string, maxLength: number): string {
let showLength = 0;
return str.split('').reduce((pre, cur) => {
const charCode = cur.charCodeAt(0);
if (charCode >= 0 && charCode <= 128) {
showLength += 1;
} else {
showLength += 2;
}
if (showLength <= maxLength) {
return pre + cur;
}
return pre;
}, '');
}
private bisection(targetHeight: number, mid: number, begin: number, end: number, text: string, node: HTMLElement): number {
const suffix = this.tail;
node.innerHTML = text.substring(0, mid) + suffix;
let sh = node.offsetHeight;
if (sh <= targetHeight) {
node.innerHTML = text.substring(0, mid + 1) + suffix;
sh = node.offsetHeight;
if (sh > targetHeight || mid === begin) {
return mid;
}
begin = mid;
mid = end - begin === 1 ? begin + 1 : Math.floor((end - begin) / 2) + begin;
return this.bisection(targetHeight, mid, begin, end, text, node);
}
if (mid - 1 < 0) {
return mid;
}
node.innerHTML = text.substring(0, mid - 1) + suffix;
sh = node.offsetHeight;
if (sh <= targetHeight) {
return mid - 1;
}
end = mid;
mid = Math.floor((end - begin) / 2) + begin;
return this.bisection(targetHeight, mid, begin, end, text, node);
}
private genType(): void {
const { lines, length, isSupportLineClamp } = this;
this.cls = {
ellipsis: true,
ellipsis__lines: lines && !isSupportLineClamp,
'ellipsis__line-clamp': lines && isSupportLineClamp,
};
if (!lines && !length) {
this.type = 'default';
} else if (!lines) {
this.type = 'length';
} else if (isSupportLineClamp) {
this.type = 'line-clamp';
} else {
this.type = 'line';
}
}
private gen(): void {
const { type, lines, length, fullWidthRecognition, tail, orgEl, cdr, ngZone } = this;
if (type === 'length') {
const el = orgEl.nativeElement as HTMLElement;
if (el.children.length > 0) {
throw new Error('Ellipsis content must be string.');
}
const lengthText = el.textContent!;
const textLength = fullWidthRecognition ? this.getStrFullLength(lengthText) : lengthText.length;
if (textLength <= length || length < 0) {
this.text = lengthText;
} else {
let displayText: string;
if (length - tail.length <= 0) {
displayText = '';
} else {
displayText = fullWidthRecognition ? this.cutStrByFullLength(lengthText, length) : lengthText.slice(0, length);
}
this.text = displayText + tail;
}
ngZone.run(() => cdr.detectChanges());
} else if (type === 'line') {
const { shadowOrgEl, shadowTextEl } = this;
const orgNode = shadowOrgEl.nativeElement as HTMLElement;
const lineText = orgNode.innerText || orgNode.textContent!;
const lineHeight = parseInt(getComputedStyle(this.getEl('.ellipsis')).lineHeight!, 10);
const targetHeight = lines * lineHeight;
this.getEl('.ellipsis__handle').style.height = `${targetHeight}px`;
if (orgNode.offsetHeight <= targetHeight) {
this.text = lineText;
this.targetCount = lineText.length;
} else {
// bisection
const len = lineText.length;
const mid = Math.ceil(len / 2);
const count = this.bisection(targetHeight, mid, 0, len, lineText, shadowTextEl.nativeElement.firstChild);
this.text = lineText;
this.targetCount = count;
}
ngZone.run(() => cdr.detectChanges());
}
}
private getEl(cls: string): HTMLElement {
return this.el.nativeElement.querySelector(cls);
}
private executeOnStable(fn: () => void): void {
if (this.ngZone.isStable) {
fn();
} else {
this.ngZone.onStable.asObservable().pipe(take(1)).subscribe(fn);
}
}
refresh(): void {
this.genType();
const { type, dom, orgEl, cdr } = this;
const html = orgEl.nativeElement.innerHTML;
this.orgHtml = dom.bypassSecurityTrustHtml(html);
cdr.detectChanges();
this.executeOnStable(() => {
this.gen();
if (type !== 'line') {
const el = this.getEl('.ellipsis');
if (el) {
el.innerHTML = html;
}
}
});
}
ngAfterViewInit(): void {
this.inited = true;
this.refresh();
}
ngOnChanges(): void {
if (this.inited) {
this.refresh();
}
}
}