UNPKG

gp-crm-ui

Version:

Модуль компонентов UI Имя модуля: `gp-crm-ui`

562 lines (475 loc) 16.6 kB
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'; // Шапка таблицы @Component({ selector: 'crm-table-header', templateUrl: './crm-table-header.component.html', styleUrls: ['./crm-table-header.component.scss'] }) export class CrmTableHeaderComponent implements OnChanges, OnDestroy { // Конструктор constructor(private readonly renderer: Renderer2) {} // Закрепленные столбцы @Input() public pinnedColumns: IColumn[]; // Ширина области контента закрепленных столбцов @Input() public pinnedCanvasWidth: number; // Обычные столбцы @Input() public defaultColumns: IColumn[]; // Ширина области контента обычных столбцов @Input() public defaultCanvasWidth: number; // Ширина области отображения обычных столбцов @Input() public defaultViewportWidth: number; // Смещение области контента в области отображения @Input() public offset: number; // Событие изменения сортировки @Output() public changeSort = new EventEmitter<IColumn>(); // Событие изменения закрепления столбца @Output() public changePin = new EventEmitter<IColumn>(); // Событие изменения ширины столбца @Output() public changeSize = new EventEmitter<IColumn>(); // Событие изменения позиции @Output() public changeSwap = new EventEmitter<SwapCommand>(); // Событие прокрутки контента @Output() public changeScroll = new EventEmitter<number>(); // Ссылка на область контента обычных столбцов @ViewChild('view') 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(); } }