gp-crm-ui
Version:
Модуль компонентов UI Имя модуля: `gp-crm-ui`
562 lines (475 loc) • 16.6 kB
text/typescript
import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
Renderer2,
SimpleChanges,
ViewChild
} from '@angular/core';
// Интерфейсы
import {
IColumn,
IOffset
} from '../../interfaces';
// Перечисления
import {
CursorType,
SortType,
SwapCommand
} from '../../enums';
// Сервисы
import { CrmHelperService } from '../../services';
// Шапка таблицы
export class CrmTableHeaderComponent
implements OnChanges, OnDestroy {
// Конструктор
constructor(private readonly renderer: Renderer2) {}
// Закрепленные столбцы
public pinnedColumns: IColumn[];
// Ширина области контента закрепленных столбцов
public pinnedCanvasWidth: number;
// Обычные столбцы
public defaultColumns: IColumn[];
// Ширина области контента обычных столбцов
public defaultCanvasWidth: number;
// Ширина области отображения обычных столбцов
public defaultViewportWidth: number;
// Смещение области контента в области отображения
public offset: number;
// Событие изменения сортировки
public changeSort = new EventEmitter<IColumn>();
// Событие изменения закрепления столбца
public changePin = new EventEmitter<IColumn>();
// Событие изменения ширины столбца
public changeSize = new EventEmitter<IColumn>();
// Событие изменения позиции
public changeSwap = new EventEmitter<SwapCommand>();
// Событие прокрутки контента
public changeScroll = new EventEmitter<number>();
// Ссылка на область контента обычных столбцов
public view: ElementRef;
// Ссылка на обьект для touch событий
private touchTarget: Element;
// Минимальная ширина столбца при изменении размера (px)
private readonly COLUMN_MIN_SIZE: number = 100;
// Граница срабатывания (закрепить / снять) (px)
private readonly PIN_BORDER_SIZE: number = 25;
// Столбец для изменения размера
private resizerColumn: IColumn;
// Размер столбца перед изменением ширины
private resizerColumnWidthBefore: number = 0;
// Смещение от разделителя до левого края экрана
private resizerPosStart: number = 0;
// Столбец для перемещения
private swapColumn: IColumn;
// Расстояние срабатывания начала перемещения (px)
private readonly SWAP_TRIGGER_DISTANCE: number = 16;
// Позиция до начала перемещения (для расчета расстояния)
private readonly swapPosStart: IOffset = {
left: 0,
top: 0
};
// Последняя позиция курсора
private swapPosLast: number = 0;
// Функции для отключения прослушивания событий
private mouseMoveListen: () => void;
private mouseUpListen: () => void;
private touchMoveListen: () => void;
private touchEndListen: () => void;
// Типы сортировки
public SortType: typeof SortType = SortType;
// Тип курсора для всей страницы
public cursorType: CursorType = CursorType.Auto;
// Граница срабатывания автопрокрутки (px)
private readonly SCROLL_BORDER_SIZE: number = 1;
// Интервал автопрокрутки (ms)
private readonly SCROLL_DELAY: number = 20;
// Шаг автопрокрутки (px)
private readonly SCROLL_STEP: number = 10;
// Ссылка на запущенную автопрокрутку
private scrollTimerId: any = null;
// Флаг, прокрутка влево возможна
private get canLeftScroll(): boolean {
return !!CrmHelperService.clamp(this.offset);
}
// Флаг, прокрутка вправо возможна
private get canRightScroll(): boolean {
return !!CrmHelperService.clamp(
this.defaultCanvasWidth -
this.defaultViewportWidth -
this.offset
);
}
// Размер допустимой прокрутки влево
private get leftScroll(): number {
return this.canLeftScroll
? CrmHelperService.clamp(this.offset - this.swapColumn.offset)
: 0;
}
// Размер допустимой прокрутки вправо
private get rightScroll(): number {
return this.canRightScroll
? CrmHelperService.clamp(
(this.swapColumn.offset + this.swapColumn.width) -
(this.offset + this.defaultViewportWidth)
)
: 0;
}
// --------------------------------------------------------------------------
// Смена сортировки
public onSortToggle(column: IColumn): void {
if (column.isSorted) {
column.sortType = column.sortType === SortType.None
? SortType.Asc
: column.sortType === SortType.Asc
? SortType.Desc
: SortType.None;
this.changeSort.emit(column);
}
}
// Закрепление столбца
public onPinToggle(column: IColumn): void {
if (column.isPinned) {
// Снимаем все пины справа
let isPinned = true;
this.pinnedColumns.forEach((value: IColumn) => {
isPinned = isPinned && value !== column;
value.isPinned = isPinned;
});
} else {
// Установить пин можно только крайнему столбцу слева
column.isPinned = column === this.defaultColumns[0];
}
this.changePin.emit(column);
}
// --------------------------------------------------------------------------
// RESIZE MOUSE
// Обработчик нажатия мыши на разделитель столбцов
public onResizeMouseDown(event: MouseEvent, column: IColumn): void {
if (event && event.button === 0 && column) {
this.resizerColumn = column;
this.resizerPressed(event.clientX);
this.setCursorStyle(CursorType.ColResize);
// Подписка на события
this.mouseMoveListen = this.renderer.listen(
document,
'mousemove',
this.onResizeMouseMove.bind(this)
);
this.mouseUpListen = this.renderer.listen(
document,
'mouseup',
this.onResizeMouseUp.bind(this)
);
}
}
// Обработчик перемещения мыши
public onResizeMouseMove(event: MouseEvent): void {
this.resizerMove(event.clientX);
}
// Обработчик отжатия мыши
public onResizeMouseUp(event: MouseEvent): void {
this.resizerReleased();
}
// --------------------------------------------------------------------------
// RESIZE TOUCH
// Обработчик нажатия сенсором на разделитель столбцов
public onResizeTouchStart(event: TouchEvent, column: IColumn): void {
const touch = event.touches[event.touches.length - 1];
const touchTarget = (
event.target ||
event.srcElement ||
event.currentTarget
) as Element;
if (touch && column) {
this.touchTarget = touchTarget;
this.resizerColumn = column;
this.resizerPressed(touch.clientX);
this.setCursorStyle(CursorType.ColResize);
// Подписка на события
this.touchMoveListen = this.renderer.listen(
this.touchTarget,
'touchmove',
this.onResizeTouchMove.bind(this)
);
this.touchEndListen = this.renderer.listen(
this.touchTarget,
'touchend',
this.onResizeTouchEnd.bind(this)
);
}
}
// Обработчик перемещения
public onResizeTouchMove(event: TouchEvent): void {
const touch = event.touches[event.touches.length - 1];
this.resizerMove(touch.clientX);
}
// Обработчик отжатия
public onResizeTouchEnd(event: TouchEvent): void {
this.resizerReleased();
}
// --------------------------------------------------------------------------
// RESIZE
// Обновить смещения
public updateOffset(): void {
const view = this.view && this.view.nativeElement;
if (view) {
view.scrollLeft = this.offset;
}
}
// Захват разделителя
public resizerPressed(x: number): void {
this.resizerColumnWidthBefore = this.resizerColumn.width;
this.resizerPosStart = x;
}
// Перемещение разделителя
public resizerMove(x: number): void {
const delta = this.resizerPosStart - x;
const width = this.resizerColumnWidthBefore - delta;
this.resizerColumn.width = CrmHelperService.clamp(width, this.COLUMN_MIN_SIZE);
this.changeSize.emit(this.resizerColumn);
}
// Освобождение разделителя от захвата
public resizerReleased(): void {
this.resizerColumn = null;
this.setCursorStyle();
this.removeEvents();
this.removeLinks();
}
// --------------------------------------------------------------------------
// SWAP MOUSE
// Обработчик нажатия мыши на шапку столбца
public onSwapMouseDown(event: MouseEvent, column: IColumn): void {
if (event && event.button === 0 && column) {
this.swapColumn = column;
this.swapPressed(event.clientX, event.clientY);
// Подписка на события
this.mouseMoveListen = this.renderer.listen(
document,
'mousemove',
this.onSwapMouseMove.bind(this)
);
this.mouseUpListen = this.renderer.listen(
document,
'mouseup',
this.onSwapMouseUp.bind(this)
);
}
}
// Обработчик перемещения мыши
public onSwapMouseMove(event: MouseEvent): void {
this.swapMove(event.clientX, event.clientY);
}
// Обработчик отжатия мыши
public onSwapMouseUp(event: MouseEvent): void {
this.swapReleased();
}
// --------------------------------------------------------------------------
// SWAP TOUCH
// Обработчик касания шапки столбца
public onSwapTouchStart(event: TouchEvent, column: IColumn): void {
const touch = event.touches[event.touches.length - 1];
const touchTarget = (
event.target ||
event.srcElement ||
event.currentTarget
) as Element;
if (touch && column) {
this.touchTarget = touchTarget;
this.swapColumn = column;
this.swapPressed(touch.clientX, touch.clientY);
// Подписка на события
this.touchMoveListen = this.renderer.listen(
this.touchTarget,
'touchmove',
this.onSwapTouchMove.bind(this)
);
this.touchEndListen = this.renderer.listen(
this.touchTarget,
'touchend',
this.onSwapTouchEnd.bind(this)
);
}
}
// Обработчик перемещения
public onSwapTouchMove(event: TouchEvent): void {
const touch = event.touches[event.touches.length - 1];
this.swapMove(touch.clientX, touch.clientY);
}
// Обработчик отжатия
public onSwapTouchEnd(event: TouchEvent): void {
this.swapReleased();
}
// --------------------------------------------------------------------------
// SWAP
// Захват столбца
private swapPressed(x: number, y: number): void {
this.swapPosStart.left = x;
this.swapPosStart.top = y;
}
// Перемещение столбца
private swapMove(x: number, y: number): void {
if (this.swapColumn.isSwap) {
this.updateSwapOffset(x);
} else {
// Расчет расстояния для срабатывания перемещения
const offset = { left: x, top: y };
const distance = CrmHelperService.distance(this.swapPosStart, offset);
if (distance > this.SWAP_TRIGGER_DISTANCE) {
// Запуск перемещения
this.swapColumn.isSwap = true;
this.swapPosLast = x;
this.setCursorStyle(CursorType.Move);
this.changeSwap.emit();
}
}
}
// Освобождение столбца от захвата
private swapReleased(): void {
// Завершение перемещения (если было активированно)
if (this.swapColumn.isSwap) {
this.swapColumn.isSwap = false;
this.captureClick();
this.setCursorStyle();
this.cancelAutoScroll();
this.changeSwap.emit();
}
// Базовый сброс
this.swapColumn = null;
this.removeEvents();
this.removeLinks();
}
// Обновить смещение столбца
private updateSwapOffset(left: number): void {
const delta = this.swapPosLast - left;
this.swapPosLast = left;
this.swapColumn.offset -= delta;
this.checkSwap();
this.checkScroll();
}
// Проверка необходимости прокрутки контента
private checkScroll(): void {
const needStop = !this.leftScroll === !this.rightScroll;
if (needStop || this.swapColumn.isPinned) {
this.cancelAutoScroll();
} else {
this.checkRunScroll();
}
}
// Проверка на необходимость запустить автопрокрутку
private checkRunScroll(): void {
const step = this.leftScroll > this.SCROLL_BORDER_SIZE
? -this.SCROLL_STEP
: this.rightScroll > this.SCROLL_BORDER_SIZE
? this.SCROLL_STEP
: 0;
if (step) {
this.scrollTimerId = this.scrollTimerId || setInterval(
(): void => {
this.autoScroll(step);
},
this.SCROLL_DELAY
);
}
}
// Автопрокрутка
private autoScroll(step: number): void {
this.swapColumn.offset += step;
this.changeScroll.emit(this.offset + step);
this.checkScroll();
}
// Остановить автопрокрутку
private cancelAutoScroll(): void {
clearInterval(this.scrollTimerId);
this.scrollTimerId = null;
}
// Проверка перемещения столбцов
private checkSwap(): void {
const cur = this.swapColumn;
const columns = (
cur.isPinned
? this.pinnedColumns
: this.defaultColumns
)
.filter((value: any) => !value.isPlaceholder);
const index = columns.indexOf(this.swapColumn);
const prev = columns[index - 1];
const next = columns[index + 1];
const HALF = 0.5;
// Перемещение влево
if (prev && (cur.offset < prev.offset + prev.width * HALF)) {
this.changeSwap.emit(SwapCommand.SwapLeft);
return;
}
// Перемещение вправо
if (next && (cur.offset + cur.width > next.offset + next.width * HALF)) {
this.changeSwap.emit(SwapCommand.SwapRight);
return;
}
// Проверки зоны закрепления
if (cur.isPinned) {
// Перемещение из зоны закрепления
if (cur.offset + cur.width - this.PIN_BORDER_SIZE > this.pinnedCanvasWidth) {
cur.isPinned = false;
cur.offset = cur.offset - (this.pinnedCanvasWidth - cur.width);
this.changeSwap.emit();
return;
}
} else {
// Перемещение в зону закрепления
if (cur.offset < - this.PIN_BORDER_SIZE) {
cur.isPinned = true;
cur.offset = cur.offset + this.pinnedCanvasWidth;
this.changeSwap.emit();
return;
}
}
}
// Установка типа курсора для всей страницы
private setCursorStyle(type: CursorType = CursorType.Auto): void {
this.cursorType = type;
}
// Убрать обработчики
private removeEvents(): void {
if (this.mouseMoveListen) { this.mouseMoveListen(); }
if (this.mouseUpListen) { this.mouseUpListen(); }
if (this.touchMoveListen) { this.touchMoveListen(); }
if (this.touchEndListen) { this.touchEndListen(); }
}
// Очистить ссылки на DOM
public removeLinks(): void {
this.touchTarget = null;
}
// Перехват и отмена события клика
private captureClick(): void {
const fn = (event: MouseEvent): void => {
event.stopPropagation();
document.removeEventListener('click', fn, true);
};
document.addEventListener('click', fn, true);
}
// --------------------------------------------------------------------------
// HOOKS
// Изменение входных параметров
public ngOnChanges(changes: SimpleChanges): void {
if (changes.offset) {
this.updateOffset();
}
}
// Уничтожение
public ngOnDestroy(): void {
this.cancelAutoScroll();
this.setCursorStyle();
this.removeEvents();
this.removeLinks();
}
}