Хабрахабр

[Из песочницы] Создание динамического tooltip в Angular2+ приложениях

В нашем приложении передо мной встала задача о создании красивого тултипа, в Angular Material таблице. Дизайн нам нарисовали, и я начала поиск в интернете нужных материалов. Но натыкалась уже или на готовые решения(библиотеки) или на очень простые решения, которые мне не подходили. В итоге объединив кучу статей и каких то заметок, я сделала тултип который при наведении рассчитывает высоту строки таблицы, длину от места наведения до конца и показывает список из людей. Для чего такие сложности? Да просто потому что, количество человек может быть разным и всех надо отобразить без "наезда" друг на друга, ну и сама иконка с количеством человек(при наведении на которую показывается тултип) может находиться в разных метах
Итог выглядит так:

image

Я не буду тут описывать полное создание таблицы, ячеек и тп, начну сразу с тултипа.
Первое это мы создаем файл директивы и присваиваем ему имя: "tool-tip.directive.ts"
Начинаем создание директивы:

import { Directive } from '@angular/core'; @Directive({ selector: '[tooltip]',
}) export class ToolTipDirective {
}

Каждый тултип должен появляться при наведении курсора и пропадать при его "уходе" с элемента, это значит надо добавить лисенеры

export class ToolTipDirective { @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { } @HostListener('mouseleave') hideTooltip() { }
}

Добавим переменную "isClear" которая будет отвечать за показ тултипа, т.е если он уже создан то мы его не отображаем. Предвижу вопрос: "Зачем?". Все дело в том что я столкнулась со странным явлением, если тултип создан, а элемент довольно большой, такой что по нему можно двигать мышкой, то он начинает пересоздаваться, и не всегда удаляется. Очень странное поведение, захотите экспериментов — попробуйте убрать и посмотрите что будет.

export class ToolTipDirective { private isClear: boolean = true; @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { if (!this.isClear) { return; } } @HostListener('mouseleave') hideTooltip() { this.isClear = true; }
}

В папке с директивой я создала подпапку в которую кладу все компоненты для тултипа(в нашем приложении пока их 3ри разных) назвала ее "content".

Создадим файл с классом опций для тултипа в папке "content", я его назвала просто "options.ts".

export class ContentOptions { x: number; y: number; height?: number; width?: number; content?: string; }

И импортируем его в наш файлик с директивой:

import { ContentOptions } from './content/options'; //у вас могут быть другие пути

Далее добавим метод который будет высчитывать рамку для нашего тултипа и добавим конструктор, с помощью ElementRef мы получаем доступ к элементам

import { Directive, ElementRef, HostListener, Input} from '@angular/core';
import { ContentOptions } from './content/options';
export class ToolTipDirective { @Input() public list: any[];//передаем списком массив с сотрудниками private isClear: boolean = true; constructor(private _ef: ElementRef) { } @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { if (!this.isClear) { return; } this.buildTooltip(event); //при наведении на иконку, метод высчитывает размер строки } @HostListener('mouseleave') hideTooltip() { this.isClear = true; } private buildTooltip(event: any) { //передаем эвент чтоб рассчитать точку начала тултипа let options: ContentOptions; let parent = this._ef.nativeElement.parentNode; //находим родительский элемент
*/т.к мы используем таблицу из библиотеки Angular Material, то мы знаем что элемент строки будет иметь класс 'mat-row', и для вычисления высоты нашего тултипа мы начинаем его искать в родительских элементах, и если находим то отдаем элемент строки/* let matRow = this.findMatRowInClassList(parent.classList); if (!matRow) { do { parent = parent.parentNode; matRow = this.findMatRowInClassList(parent.classList); } while (!matRow); } const parentViewPort = parent.getBoundingClientRect(); //получаем все размеры строки const cellViewPort = this._ef.nativeElement.getBoundingClientRect(); //получаем все размеры ячейки, содержащей нашу иконку const rowHeight = parentViewPort.height; //высота одной строки const rightPoint = cellViewPort.right + 25; // чтобы не перекрывать ячейку надо сдвинуть току начала тултипа let topPoint = parentViewPort.top; // верхняя точка тултипа let height = parentViewPort.height; // добавляем переменную, на случай если сотрудники не помещаются в одну строку тултипа const countPerson = this.list.length; //вычисляем количество человек в списке const width = parentViewPort.right - rightPoint; //вычисляем длину тултипа const countInOneRow = Math.floor(width / 160); //предолагаем что средняя длина элемента для сотрудника в тултипе примерно 160 пикселей, можно увеличить до 200 если учитывать что может быть длинная фамилия if (countInOneRow > 0) { //если справа не хватает места для показания тултипа, хотяб с одним человеком, то мы его покажем слева const countRow = Math.ceil(countPerson / countInOneRow); //количество людей которых мы можем показать в одной строке без "обрезания" фамилий if (this.list.length > countInOneRow) { // высчитывается высота показываемого тултипа for (let i = 1; i <= countRow; i++) { if (i % 2 === 0) { topPoint -= rowHeight; } height = rowHeight * i; } } const options: ContentOptions = { // запись опций тултипа для передачи их компоненту, в котором все построится x: rightPoint, y: topPoint, height: height, width: width } return options; } else { //вычисляем те же самые параметры, для построения тултипа слева const leftEndPoint = cellViewPort.left - 25; const leftWidth = leftEndPoint - parentViewPort.left; const countInOneRowLeft = Math.floor(leftWidth / 160); if (countInOneRowLeft > 0) { const countRow = Math.ceil(countPerson / countInOneRowLeft); if (this.list.length > countInOneRowLeft) { for (let i = 1; i <= countRow; i++) { if (i % 2 === 0) { topPoint -= rowHeight; } height = rowHeight * i; } } const options: ContentOptions = { x: parentViewPort.left, y: topPoint, height: height, width: leftWidth, } return options; } } this.showTooltip(options); //метод для создания элемента(соответвенно его отображения) } private findMatRowInClassList(classList: DOMTokenList): string { let matRow = undefined; if (classList.length > 0) { const index = classList.contains('mat-row'); if (index) { matRow = 'mat-row'; } } return matRow; }
}

Чтобы код не показался очень длинным я разделю его. Я не буду объяснять как работают методы фабрик создания элементов, потому что если вы новичок то это перегрузит вас лишней информацией, тем более есть много источников где это можно найти.

Далее мы добавляем метод создания нашего тултипа, и соответственно вносим кое какие изменения в конструктор и импортируем нужные классы.

*/появились изменения/*
import { Directive, Inject, ComponentFactoryResolver, Input, ElementRef, ViewContainerRef, ComponentRef, HostListener} from '@angular/core';
import { DOCUMENT } from '@angular/platform-browser';
import { ContentOptions } from './content/options';
export class ToolTipDirective { @Input() public list: any[];//передаем списком массив с сотрудниками private isClear: boolean = true;
*/появились изменения/* constructor(private _componentFactoryResolver: ComponentFactoryResolver, private _viewContainerRef: ViewContainerRef, private _ef: ElementRef, @Inject(DOCUMENT) private _document: any) { } @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { ..... } @HostListener('mouseleave') hideTooltip() { ..... } private buildTooltip(event: any) { ...... } private showTooltip(options: any) { let componentFactory: any; componentFactory = this._componentFactoryResolver.resolveComponentFactory(*/сюда мы потом вставим название компонента для его создания/*); this.contentCmpRef = this._viewContainerRef.createComponent(componentFactory); //в тело страницы нам надо вставить созданный нами новый элемент this._document.querySelector('body').appendChild(this.contentCmpRef.location.nativeElement); //после его создания передаем в него наши параметры, список сотрудников и размеры тултипа this.contentCmpRef.instance.options = options; this.contentCmpRef.instance.empolyees = this.list; this.isClear = false; //помечаем что элемент уже создан } private findMatRowInClassList(classList: DOMTokenList): string { .... }
}

С директивой мы можно сказать закончили, там еще будет пара небольших изменений.
Далее приступаем к созданию компонента который будет отображать наш тултип.

В папке "content" создадим файлы: "tooltip-employees.component.ts" и "tooltip-employees.component.scss".

Начнем с "tooltip-employees.component.ts"

import { Component, AfterContentInit, ElementRef} from '@angular/core';
import { ContentOptions } from './options'; //в общем вот тут и происходит построение рамки по переданным параметрам
@Component({ template : ` <div class="ng-tool-tip-content" [ngStyle]="{'width.px': options.width, 'height.px': options.height, 'top.px': options.y, 'left.px': options.x}"> <div *ngFor="let employee of empolyees" class="employee"> <div fxLayout="row" fxLayoutGap="1em"> <img mat-card-image [src]="employee.userPhoto" class="avatar"> <div fxLayout="column"> <div class="employee-name">{{employee.name}}</div> <div class="department-name"> {{employee.department.name}}</div> </div> <div> </div> </div> `,
styleUrls : ['tooltip-employees.component.scss']
}) export class TooltipEmployeesComponent { public empolyees: any[]; private _options: ContentOptions; set options(op: ContentOptions) { if (op) { this._options = op; this.options.height -= 8; // add padding in css } } get options(): ContentOptions { return this._options; } constructor(private elRef: ElementRef) { }
}

Далее добавляем в "tooltip-employees.component.scss" файл:

$small-font-size: 12px;
.ng-tool-tip-content{ z-index : 10; display: flex; flex-wrap: wrap; padding-top: 8px; background-color: #757575; position: absolute; .employee{ margin-left: 0.5em; margin-right: 0.5em; margin-bottom: 0.2em; width: 160px; .photo{ width: 40px; height: 40px; } .employee-name{ color: #FFFFFF; font-size: $small-font-size; } .department-name{ color:#C4C4C4; font-size: $small-font-size; } } }

Теперь все для нашего компонента создано!
У меня в основном файле для стилей таблиц есть такой класс:

"
.mousehover-person-icon{ color: black; .mat-icon{ color: black; }
} "

для изменения цвета нашей иконки при наведении курсора.

Возвращаемся в файл с нашей директивой:

import ......
*/появились изменения/*
import { TooltipEmployeesComponent } from './content/tooltip-employees.component';
export class ToolTipDirective { @Input() public list: any[];//передаем списком массив с сотрудниками */появились изменения/*
/** set it to true, если мы хотим чтоб тултип показывался по клику */ @Input() showOnClick: boolean = false; @Input() autoShowHide: boolean = true; private isClear: boolean = true; constructor(.......) { } */появились изменения/* @HostListener('mouseover', ['$event']) onMouseHover(event: MouseEvent) { if (!this.autoShowHide || this.showOnClick) { return; } if (!this.isClear) { return; } this.iconElement = event.srcElement.parentElement.parentElement; this.iconElement.classList.add('mousehover-person-icon'); this.buildTooltip(event); } @HostListener('mouseleave') hideTooltip() { this.iconElement.classList.remove('mousehover-person-icon') //удаляем класс с изменением цвета if (this.contentCmpRef) { this.contentCmpRef.destroy(); //уничтожаем сам компонент this.isClear = true; } } private buildTooltip(event: any) { ...... } private showTooltip(options: any) { let componentFactory: any;
*/появились изменения/* componentFactory = this._componentFactoryResolver.resolveComponentFactory(TooltipEmployeesComponent); this.contentCmpRef = this._viewContainerRef.createComponent(componentFactory); //в тело нам надо вставить созданный нами новый элемент this._document.querySelector('body').appendChild(this.contentCmpRef.location.nativeElement); //после его создания передаем в него наши параметры, список сотрудников и размеры тултипа this.contentCmpRef.instance.options = options; this.contentCmpRef.instance.empolyees = this.list; this.isClear = false; //помечаем что элемент уже создан } private findMatRowInClassList(classList: DOMTokenList): string { .... }
}

Вот в принципе и все! Мы создали директиву и компонент для тултипа.
Так же при желании можно в директиву добавить эвенты, которые будут сообщать о начале создания тултипа и тп, но тут я не стала перегружать этой информацией. Так же можно сделать много других компонентов для тултипа и переиспользовать их, но это тоже усложнение, и если разобраться сначала с этой версией, то добавление компонентов у Вас не составит труда.

Ну и само использование нашего тултипа:

@Component({ template: ` <mat-icon class="mat-24" tooltip [list]="listItem">{{icon}}</mat-icon>
`
export class IconComponent { public icon: string; public listItem: any[]; @Input() set data(cellData: any) { if (cellData) { if (cellData['icon']) { this.icon = (cellData['icon'] as string).toLowerCase(); } this.listItem = cellData['list']; } };
}

Вот и все! Мы завершили создание тултипа!

Как это выглядит с большим количеством сотрудников:
image

Надеюсь, данная статья была полезна Вам! Я бы с радостью указала источники, откуда я брала некоторый материал, но к сожалению это было довольно давно у ссылок у меня уже не осталось!

Удачи в освоении динамических тултипов!

Показать больше

Похожие публикации

Кнопка «Наверх»