UNPKG

@progress/kendo-angular-layout

Version:

Kendo UI for Angular Layout Package - a collection of components to create professional application layoyts

420 lines (419 loc) 21.1 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ChangeDetectorRef, Injectable, NgZone, Renderer2 } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; import { LocalizationService } from '@progress/kendo-angular-l10n'; import { calculateCellFromPosition, getDropTarget, isRowItemPresent, normalizeValue, propsChanged, setElementStyles } from './util'; import { closestInScope, isFocusable } from '../common/dom-queries'; import { DRAGGED_ZINDEX, HINT_BORDERS_HEIGHT, OVERLAP_THRESHOLD, REVERSE_OVERLAP_THRESHOLD } from './constants'; import { TileLayoutReorderEvent } from './reorder-event'; import { TileLayoutResizeEvent } from './resize-event'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-l10n"; /** * @hidden */ export class TileLayoutDraggingService { zone; renderer; cdr; localization; reorderable = new BehaviorSubject(null); resizable = new BehaviorSubject(null); reorder = new Subject(); resize = new Subject(); tileLayoutSettings; get colStart() { return this.currentColStart; } get rowStart() { return this.currentRowStart; } get itemWrapper() { return this.draggedItemWrapper; } get order() { return this.targetOrder; } draggedItem; draggedItemWrapper; reordering; resizing; offset; targetSize; cellSize; targetOrder; currentColStart; currentRowStart; startingPoint; currentResizingColSpan; currentResizingRowSpan; direction; lastDragCursorOffset = { x: 0, y: 0 }; localizationSubscription; rtl; constructor(zone, renderer, cdr, localization) { this.zone = zone; this.renderer = renderer; this.cdr = cdr; this.localization = localization; this.localizationSubscription = this.localization.changes.subscribe(({ rtl }) => this.rtl = rtl); } ngOnDestroy() { this.localizationSubscription.unsubscribe(); } handlePress(originalEvent) { const resizing = !!originalEvent.target.classList.contains('k-resize-handle'); const closestTile = closestInScope(originalEvent.target, el => el.classList.contains('k-tilelayout-item'), this.tileLayoutSettings.tileLayoutElement); const closestHeader = closestInScope(originalEvent.target, el => el.classList.contains('k-tilelayout-item-header'), this.tileLayoutSettings.tileLayoutElement); if (!closestTile) { return; } this.zone.run(() => { this.draggedItemWrapper = closestTile; this.draggedItem = this.tileLayoutSettings.items .find(item => item.order === +closestTile.style.order); }); const reordering = !resizing && this.reorderable.getValue() && this.draggedItem.reorderable && closestHeader; const focusableTarget = isFocusable(originalEvent.target); if (!(reordering || resizing) || focusableTarget) { return; } else { originalEvent.preventDefault(); } this.zone.run(() => { this.reordering = reordering; this.resizing = resizing; }); const tileRect = this.draggedItemWrapper.getBoundingClientRect(); this.zone.run(() => { this.offset = { top: originalEvent.clientY - tileRect.top, left: originalEvent.clientX - tileRect.left, x: tileRect.x, y: tileRect.y, width: tileRect.width, height: tileRect.height }; this.targetSize = { rowSpan: this.draggedItem.rowSpan, colSpan: this.draggedItem.colSpan }; this.cellSize = { width: (tileRect.width - ((this.targetSize.colSpan - 1) * this.tileLayoutSettings.gap.columns)) / this.targetSize.colSpan, height: (tileRect.height - ((this.targetSize.rowSpan - 1) * this.tileLayoutSettings.gap.rows)) / this.targetSize.rowSpan }; this.lastDragCursorOffset = { x: originalEvent.clientX, y: originalEvent.clientY }; }); setElementStyles(this.renderer, this.draggedItemWrapper, { left: tileRect.left + window.pageXOffset - window.scrollX + 'px', top: tileRect.top + window.pageYOffset - window.scrollY + 'px', width: tileRect.width + 'px', height: tileRect.height + 'px', zIndex: DRAGGED_ZINDEX }); setElementStyles(this.renderer, this.tileLayoutSettings.hintElement, { display: 'flex', height: (tileRect.height - HINT_BORDERS_HEIGHT) + 'px' }); this.zone.run(() => this.targetOrder = this.draggedItem.order); this.cdr.markForCheck(); setElementStyles(this.renderer, this.draggedItemWrapper, { position: 'fixed' }); if (this.reorderable.getValue() && !resizing) { this.zone.run(() => { this.currentColStart = this.draggedItem.colStart; this.currentRowStart = this.draggedItem.rowStart; }); this.cdr.markForCheck(); } else if (this.resizable && resizing) { this.zone.run(() => { this.startingPoint = { top: originalEvent.clientY, left: originalEvent.clientX }; this.currentResizingColSpan = this.draggedItem.colSpan; this.currentResizingRowSpan = this.draggedItem.rowSpan; if (this.draggedItem.col) { this.currentColStart = this.draggedItem.col.toString(); } if (this.draggedItem.row) { this.currentRowStart = this.draggedItem.row.toString(); } this.direction = originalEvent.target.classList[1]; }); } } handleDrag(originalEvent) { if (this.draggedItemWrapper) { if (this.reordering) { this.reorderItems(originalEvent); } else if (this.resizing) { this.resizeItem(originalEvent); } this.lastDragCursorOffset = { x: originalEvent.clientX, y: originalEvent.clientY }; } } handleRelease(originalEvent) { originalEvent.preventDefault(); if (this.reordering) { const initialOrder = this.draggedItem.order; const initialCol = this.draggedItem.col; const initialRow = this.draggedItem.row; const targetCol = normalizeValue(this.currentColStart); const targetRow = normalizeValue(this.currentRowStart); if (propsChanged([this.targetOrder, targetCol, targetRow], [initialOrder, initialCol, initialRow])) { const reorderEvent = new TileLayoutReorderEvent(this.draggedItem, this.tileLayoutSettings.items, this.targetOrder, initialOrder, normalizeValue(this.currentColStart), initialCol, targetRow, initialRow); this.reorder.next(reorderEvent); if (!reorderEvent.isDefaultPrevented()) { if (this.targetOrder > initialOrder) { this.zone.run(() => { for (let i = initialOrder + 1; i <= this.targetOrder; i++) { this.tileLayoutSettings.items.find(item => item.order === i).order = i - 1; } }); } else { this.zone.run(() => { for (let i = initialOrder - 1; i >= this.targetOrder; i--) { this.tileLayoutSettings.items.find(item => item.order === i).order = i + 1; } }); } this.draggedItem.order = this.targetOrder; if (this.draggedItem.col) { this.draggedItem.col = +this.currentColStart; } if (this.draggedItem.row) { this.draggedItem.row = +this.currentRowStart; } } } this.tileLayoutSettings.tileLayoutElement.appendChild(this.tileLayoutSettings.hintElement); this.cdr.markForCheck(); this.zone.run(() => this.cleanUp()); } else if (!this.reordering && this.resizing) { const initialRowSpan = this.draggedItem.rowSpan; const initialColSpan = this.draggedItem.colSpan; const { targetColSpan, targetRowSpan } = isRowItemPresent(this.tileLayoutSettings.items) ? this.targetSpan() : { targetColSpan: this.currentResizingColSpan, targetRowSpan: this.currentResizingRowSpan }; if (propsChanged([initialRowSpan, initialColSpan], [targetRowSpan, targetColSpan])) { const resizeEvent = new TileLayoutResizeEvent(this.draggedItem, this.tileLayoutSettings.items, targetRowSpan, initialRowSpan, targetColSpan, initialColSpan); this.resize.next(resizeEvent); if (!resizeEvent.isDefaultPrevented()) { this.draggedItem.colSpan = this.currentResizingColSpan; this.draggedItem.rowSpan = this.currentResizingRowSpan; } } this.zone.run(() => this.cleanUp()); } } reorderItems(event) { const targets = getDropTarget(event); const closestTile = targets.find(t => t !== this.draggedItemWrapper); const tileOrder = closestTile ? +closestTile.style.order : +this.draggedItemWrapper.style.order; if (this.tileLayoutSettings.autoFlow !== 'none') { const deltaX = event.clientX - this.lastDragCursorOffset.x; const deltaY = event.clientY - this.lastDragCursorOffset.y; const directionX = deltaX > 0 ? 'right' : deltaX < 0 ? 'left' : undefined; const directionY = deltaY > 0 ? 'down' : deltaX < 0 ? 'up' : undefined; const rect = this.draggedItemWrapper.getBoundingClientRect(); const horizontalGap = this.tileLayoutSettings.gap.columns; const verticalGap = this.tileLayoutSettings.gap.rows; if (directionX && this.draggedItem.col) { const { col } = calculateCellFromPosition({ x: directionX === 'right' ? rect.right - horizontalGap : rect.left + horizontalGap, y: event.clientY }, this.tileLayoutSettings.tileLayoutElement, this.tileLayoutSettings.gap, this.cellSize, this.tileLayoutSettings.columns, this.rtl); const targetStartCol = this.getTargetCol(col, directionX); this.currentColStart = targetStartCol.toString(); } if (directionY && this.draggedItem.row) { const { row } = calculateCellFromPosition({ x: event.clientX, y: directionY === 'down' ? rect.bottom - verticalGap : rect.top + verticalGap }, this.tileLayoutSettings.tileLayoutElement, this.tileLayoutSettings.gap, this.cellSize, this.tileLayoutSettings.columns, this.rtl); const targetStartRow = this.getTargetRow(row, directionY); this.currentRowStart = targetStartRow.toString(); } } const hintBefore = tileOrder < this.targetOrder; const hintAfter = tileOrder > this.targetOrder; this.zone.run(() => this.targetOrder = tileOrder); if (hintBefore) { this.tileLayoutSettings.tileLayoutElement .insertBefore(this.tileLayoutSettings.hintElement, this.tileLayoutSettings.tileLayoutElement.firstChild); } else if (hintAfter) { this.tileLayoutSettings.tileLayoutElement.appendChild(this.tileLayoutSettings.hintElement); } setElementStyles(this.renderer, this.draggedItemWrapper, { top: (event.pageY - this.offset.top - window.scrollY) + 'px', left: (event.pageX - this.offset.left - window.scrollX) + 'px' }); this.cdr.markForCheck(); } resizeItem(event) { setElementStyles(this.renderer, this.tileLayoutSettings.tileLayoutElement, { cursor: this.direction.split('k-cursor-')[1] }); const currentWidth = this.rtl ? this.offset.width + (this.offset.x - event.clientX) : this.offset.width + (event.clientX - this.startingPoint.left); const currentHeight = this.offset.height + (event.clientY - this.startingPoint.top); const hintRect = this.tileLayoutSettings.hintElement.getBoundingClientRect(); const hintWidth = hintRect.width; const hintHeight = hintRect.height; const horizontalDragDirection = event.clientX - this.lastDragCursorOffset.x; const verticalDragDirection = event.clientY - this.lastDragCursorOffset.y; const startCol = this.draggedItem.col ? this.draggedItem.col : calculateCellFromPosition({ x: this.rtl ? hintRect.right : hintRect.x, y: hintRect.y }, this.tileLayoutSettings.tileLayoutElement, this.tileLayoutSettings.gap, this.cellSize, this.tileLayoutSettings.columns, this.rtl).col; const maxWidth = (this.tileLayoutSettings.columns - startCol) * (this.cellSize.width + this.tileLayoutSettings.gap.columns) + this.cellSize.width; const resizeHorizontally = () => { setElementStyles(this.renderer, this.draggedItemWrapper, { width: Math.min(Math.max(currentWidth, this.cellSize.width), maxWidth) + 'px' }); if (this.rtl && currentWidth > this.cellSize.width) { const totalWidth = this.tileLayoutSettings.columns * (this.cellSize.width + this.tileLayoutSettings.gap.columns); const leftBoundary = this.tileLayoutSettings.tileLayoutElement.getBoundingClientRect().right - totalWidth; setElementStyles(this.renderer, this.draggedItemWrapper, { left: Math.max(event.clientX, leftBoundary) + 'px' }); } const deltaX = currentWidth - hintWidth; const { x, y, right } = this.draggedItem.elem.nativeElement.getBoundingClientRect(); const { col } = calculateCellFromPosition({ x: (this.rtl ? right : x), y: y }, this.tileLayoutSettings.tileLayoutElement, this.tileLayoutSettings.gap, this.cellSize, this.tileLayoutSettings.columns, this.rtl); const resizedColSpan = col + this.currentResizingColSpan; const expandingCondition = this.rtl ? horizontalDragDirection < 0 : horizontalDragDirection > 0; const shrinkingCondition = this.rtl ? horizontalDragDirection > 0 : horizontalDragDirection < 0; if (deltaX > OVERLAP_THRESHOLD * this.cellSize.width && expandingCondition && resizedColSpan <= this.tileLayoutSettings.columns) { this.currentResizingColSpan++; } else if (this.currentResizingColSpan > 1 && shrinkingCondition && deltaX < REVERSE_OVERLAP_THRESHOLD * this.cellSize.width) { this.currentResizingColSpan--; } setElementStyles(this.renderer, this.tileLayoutSettings.hintElement, { gridColumnEnd: `span ${this.currentResizingColSpan}` }); }; const resizeVertically = () => { setElementStyles(this.renderer, this.draggedItemWrapper, { height: Math.max(currentHeight, this.cellSize.height) + 'px' }); const deltaY = currentHeight - hintHeight; if (deltaY > OVERLAP_THRESHOLD * this.cellSize.height && verticalDragDirection > 0) { this.currentResizingRowSpan++; } else if (this.currentResizingRowSpan > 1 && verticalDragDirection < 0 && deltaY < REVERSE_OVERLAP_THRESHOLD * this.cellSize.height) { this.currentResizingRowSpan--; } setElementStyles(this.renderer, this.tileLayoutSettings.hintElement, { gridRowEnd: `span ${this.currentResizingRowSpan}` }); setElementStyles(this.renderer, this.tileLayoutSettings.hintElement, { height: `${this.calculateHintHeight()}px` }); }; if (this.direction.indexOf('ew') > -1) { resizeHorizontally(); } else if (this.direction.indexOf('ns') > -1) { resizeVertically(); } else { resizeHorizontally(); resizeVertically(); } } cleanUp() { this.targetOrder = this.currentResizingColSpan = this.currentColStart = this.currentResizingRowSpan = this.currentRowStart = undefined; this.resizing = this.reordering = false; this.direction = null; if (this.draggedItemWrapper) { setElementStyles(this.renderer, this.draggedItemWrapper, { top: '', left: '', display: '', width: '', height: '', zIndex: '', position: '' }); setElementStyles(this.renderer, this.tileLayoutSettings.hintElement, { display: 'none', height: 'auto' }); setElementStyles(this.renderer, this.tileLayoutSettings.tileLayoutElement, { cursor: 'default' }); this.draggedItemWrapper = this.offset = this.draggedItem = this.resizing = this.reordering = this.currentResizingColSpan = this.currentResizingRowSpan = this.startingPoint = undefined; this.lastDragCursorOffset = { x: 0, y: 0 }; } } targetSpan() { const itemRect = this.draggedItem.elem.nativeElement.getBoundingClientRect(); const startingCell = calculateCellFromPosition({ x: this.rtl ? itemRect.right : itemRect.x, y: itemRect.y }, this.tileLayoutSettings.tileLayoutElement, this.tileLayoutSettings.gap, this.cellSize, this.tileLayoutSettings.columns, this.rtl); const targetEndCell = calculateCellFromPosition({ x: this.rtl ? itemRect.x + OVERLAP_THRESHOLD * this.cellSize.width : itemRect.right - OVERLAP_THRESHOLD * this.cellSize.width, y: itemRect.bottom - OVERLAP_THRESHOLD * this.cellSize.height }, this.tileLayoutSettings.tileLayoutElement, this.tileLayoutSettings.gap, this.cellSize, this.tileLayoutSettings.columns, this.rtl); return { targetColSpan: targetEndCell.col - startingCell.col + 1, targetRowSpan: targetEndCell.row - startingCell.row + 1 }; } getTargetCol(col, direction) { if (this.rtl) { return direction === 'left' ? col - this.draggedItem.colSpan + 1 : col; } return direction === 'right' ? col - this.draggedItem.colSpan + 1 : col; } getTargetRow(row, direction) { return direction === 'down' ? row - this.draggedItem.rowSpan + 1 : row; } calculateHintHeight() { const totalHintCellsHeight = this.currentResizingRowSpan * this.cellSize.height; const totalHintGapsHeight = (this.currentResizingRowSpan - 1) * this.tileLayoutSettings.gap.rows; const hintHeight = totalHintCellsHeight + totalHintGapsHeight - HINT_BORDERS_HEIGHT; return hintHeight; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutDraggingService, deps: [{ token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutDraggingService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutDraggingService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }, { type: i1.LocalizationService }]; } });