@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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 }]; } });