@progress/kendo-angular-layout
Version:
Kendo UI for Angular Layout Package - a collection of components to create professional application layoyts
376 lines (375 loc) • 16.5 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 { Component, HostBinding, Input, Output, EventEmitter, ContentChildren, QueryList, ElementRef, Renderer2, NgZone, ViewChild } from '@angular/core';
import { TileLayoutDraggingService } from './dragging-service';
import { Subscription } from 'rxjs';
import { Draggable } from '@progress/kendo-draggable';
import { TileLayoutItemComponent } from './tilelayout-item.component';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { hasObservers, isChanged, shouldShowValidationUI, WatermarkOverlayComponent } from '@progress/kendo-angular-common';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { isPresent } from '../common/util';
import { TileLayoutKeyboardNavigationService } from './keyboard-navigation.service';
import { NgIf } from '@angular/common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "./dragging-service";
import * as i3 from "./keyboard-navigation.service";
const autoFlowClasses = {
column: 'k-grid-flow-col',
row: 'k-grid-flow-row',
'column-dense': 'k-grid-flow-col-dense',
'row-dense': 'k-grid-flow-row-dense'
};
/**
* Represents the [Kendo UI TileLayout component for Angular]({% slug overview_tilelayout %})
*/
export class TileLayoutComponent {
zone;
elem;
renderer;
localization;
draggingService;
navigationService;
/**
* Defines the number of columns ([see example](slug:tiles_tilelayout#size-and-position)).
* @default 1
*/
columns = 1;
/**
* Determines the width of the columns. Numeric values are treated as pixels ([see example](slug:tiles_tilelayout#size-and-position)).
* @default '1fr'
*/
columnWidth = '1fr';
/**
* The numeric values which determine the spacing in pixels between the layout items horizontally and vertically.
* Properties:
* * rows - the vertical spacing. Numeric values are treated as pixels. Defaults to `16`.
* * columns - the horizontal spacing. Numeric values are treated as pixels. Defaults to `16`.
*
* When bound to a single numeric value, it will be set to both `rows` and `columns` properties.
*/
set gap(value) {
this._gap = (typeof value === 'number') ? { rows: value, columns: value } : Object.assign(this._gap, value);
}
get gap() {
return this._gap;
}
/**
* Determines whether the reordering functionality will be enabled ([see example]({% slug reordering_tilelayout %})).
* @default false
*/
reorderable = false;
/**
* Determines whether the resizing functionality will be enabled ([see example]({% slug resizing_tilelayout %})).
* @default false
*/
resizable = false;
/**
* Determines the height of the rows. Numeric values are treated as pixels ([see example](slug:tiles_tilelayout#size-and-position)).
* @default '1fr'
*/
rowHeight = '1fr';
/**
* Controls how the auto-placement algorithm works, specifying exactly how auto-placed items are flowed in the TileLayout ([see example]({% slug tiles_autoflow_tilelayout %})).
* For further reference, check the [grid-auto-flow CSS article](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-flow).
*
* The possible values are:
* * (Default) `column`
* * `row`
* * `row dense`
* * `column dense`
* * `none`
*
*/
autoFlow = 'column';
/**
* When the keyboard navigation is enabled, the user can use dedicated shortcuts to interact with the TileLayout.
* By default, navigation is enabled. To disable it and include focusable TileLayout content as a part of the natural tab sequence of the page, set the property to `false`.
*
* @default true
*/
navigable = true;
/**
* Fires when the user completes the reordering of the item ([see example]({% slug reordering_tilelayout %})).
* This event is preventable. If you cancel it, the item will not be reordered.
*/
reorder = new EventEmitter();
/**
* Fires when the user completes the resizing of the item ([see example]({% slug resizing_tilelayout %})).
* This event is preventable. If you cancel it, the item will not be resized.
*/
resize = new EventEmitter();
hostClass = true;
hostRole = 'list';
get gapStyle() {
return `${this.gap.rows}px ${this.gap.columns}px`;
}
direction;
get currentColStart() {
return this.draggingService.colStart;
}
get currentRowStart() {
return this.draggingService.rowStart;
}
get draggedItemWrapper() {
return this.draggingService.itemWrapper;
}
get targetOrder() {
return this.draggingService.order;
}
/**
* A query list of all declared [TileLayoutItemComponent]({% slug api_layout_tilelayoutitemcomponent %}) items.
*/
items;
hint;
/**
* @hidden
*/
showLicenseWatermark = false;
draggable;
subs = new Subscription();
_gap = {
rows: 16,
columns: 16
};
constructor(zone, elem, renderer, localization, draggingService, navigationService) {
this.zone = zone;
this.elem = elem;
this.renderer = renderer;
this.localization = localization;
this.draggingService = draggingService;
this.navigationService = navigationService;
const isValid = validatePackage(packageMetadata);
this.showLicenseWatermark = shouldShowValidationUI(isValid);
}
ngOnInit() {
this.applyColStyling();
this.applyRowStyling();
this.draggingService.reorderable.next(this.reorderable);
this.draggingService.resizable.next(this.resizable);
this.navigationService.owner = this;
this.navigationService.navigable.next(this.navigable);
if (hasObservers(this.reorder)) {
this.subs.add(this.draggingService.reorder.subscribe(e => this.reorder.emit(e)));
}
if (hasObservers(this.resize)) {
this.subs.add(this.draggingService.resize.subscribe(e => this.resize.emit(e)));
}
this.subs.add(this.draggingService.reorderable.subscribe(reorderable => {
if (reorderable && !this.draggable) {
this.initializeDraggable();
}
}));
this.subs.add(this.draggingService.resizable.subscribe(resizable => {
if (resizable && !this.draggable) {
this.initializeDraggable();
}
}));
this.subs.add(this.localization.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
}));
}
ngAfterViewInit() {
this.draggingService.tileLayoutSettings = this.draggingServiceConfig();
this.applyAutoFlow(null, autoFlowClasses[this.autoFlow]);
this.items.changes.subscribe(() => {
this.setItemsOrder();
this.draggingService.tileLayoutSettings.items = this.items.toArray();
});
this.zone.runOutsideAngular(() => {
this.elem.nativeElement.addEventListener('focusin', this.onFocusIn);
});
}
ngAfterContentInit() {
this.setItemsOrder();
}
ngOnChanges(changes) {
if (changes['columns'] || changes['columnWidth']) {
this.applyColStyling();
}
if (changes['rowHeight']) {
this.applyRowStyling();
}
if (isChanged('reorderable', changes)) {
this.draggingService.reorderable.next(changes['reorderable'].currentValue);
}
if (isChanged('resizable', changes)) {
this.draggingService.resizable.next(changes['resizable'].currentValue);
}
if (changes['gap'] || changes['autoFlow'] || changes['columns']) {
this.draggingService.tileLayoutSettings = this.draggingServiceConfig();
if (changes['autoFlow']) {
this.applyAutoFlow(autoFlowClasses[changes['autoFlow'].previousValue] || '', autoFlowClasses[changes['autoFlow'].currentValue]);
}
}
if (isChanged('navigable', changes)) {
this.navigationService.navigable.next(changes['navigable'].currentValue);
}
}
ngOnDestroy() {
if (this.draggable) {
this.draggable.destroy();
}
this.subs.unsubscribe();
this.elem.nativeElement.removeEventListener('focusin', this.onFocusIn);
}
handlePress({ originalEvent }) {
this.draggingService.handlePress(originalEvent);
}
handleDrag({ originalEvent }) {
this.draggingService.handleDrag(originalEvent);
}
handleRelease({ originalEvent }) {
this.draggingService.handleRelease(originalEvent);
}
applyColStyling() {
const colWidth = typeof this.columnWidth === 'number' ? `${this.columnWidth}px` : this.columnWidth;
const gridTemplateColumnsStyle = `repeat(${this.columns}, ${colWidth})`;
this.renderer.setStyle(this.elem.nativeElement, 'grid-template-columns', gridTemplateColumnsStyle);
}
applyRowStyling() {
const rowHeight = typeof this.rowHeight === 'number' ? `${this.rowHeight}px` : this.rowHeight;
const gridAutoRowsStyle = `${rowHeight}`;
this.renderer.setStyle(this.elem.nativeElement, 'grid-auto-rows', gridAutoRowsStyle);
}
draggingServiceConfig() {
return {
tileLayoutElement: this.elem ? this.elem.nativeElement : undefined,
hintElement: this.hint ? this.hint.nativeElement : undefined,
gap: this.gap,
columns: this.columns,
autoFlow: this.autoFlow,
items: this.items ? this.items.toArray() : []
};
}
initializeDraggable() {
this.draggable = new Draggable({
press: this.handlePress.bind(this),
drag: this.handleDrag.bind(this),
release: this.handleRelease.bind(this)
});
this.zone.runOutsideAngular(() => this.draggable.bindTo(this.elem.nativeElement));
}
applyAutoFlow(classToRemove, classToAdd) {
const element = this.elem.nativeElement;
if (classToRemove) {
this.renderer.removeClass(element, classToRemove);
}
if (this.autoFlow !== 'none' && isPresent(classToAdd)) {
this.renderer.addClass(element, classToAdd);
}
}
setItemsOrder() {
this.items.forEach((item, index) => {
if (!isPresent(item.order)) {
item.order = index;
}
});
}
onFocusIn = (e) => {
if (!this.navigable || this.navigationService.mousedown || !e.relatedTarget) {
this.navigationService.mousedown = false;
return;
}
if (!(this.elem.nativeElement.compareDocumentPosition(e.relatedTarget) & Node.DOCUMENT_POSITION_CONTAINED_BY)) {
this.navigationService.returnFocus();
}
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutComponent, deps: [{ token: i0.NgZone }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i1.LocalizationService }, { token: i2.TileLayoutDraggingService }, { token: i3.TileLayoutKeyboardNavigationService }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TileLayoutComponent, isStandalone: true, selector: "kendo-tilelayout", inputs: { columns: "columns", columnWidth: "columnWidth", gap: "gap", reorderable: "reorderable", resizable: "resizable", rowHeight: "rowHeight", autoFlow: "autoFlow", navigable: "navigable" }, outputs: { reorder: "reorder", resize: "resize" }, host: { properties: { "class.k-tilelayout": "this.hostClass", "attr.role": "this.hostRole", "style.gap": "this.gapStyle", "style.padding": "this.gapStyle", "attr.dir": "this.direction" } }, providers: [
LocalizationService,
TileLayoutDraggingService,
TileLayoutKeyboardNavigationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.tilelayout.component'
}
], queries: [{ propertyName: "items", predicate: TileLayoutItemComponent }], viewQueries: [{ propertyName: "hint", first: true, predicate: ["hint"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
<ng-content></ng-content>
<div #hint class="k-layout-item-hint"
[style.display]="'none'"
[style.order]="targetOrder"
[style.gridColumnEnd]="draggedItemWrapper?.style.gridColumnEnd"
[style.gridRowEnd]="draggedItemWrapper?.style.gridRowEnd"
[style.gridColumnStart]="currentColStart"
[style.gridRowStart]="currentRowStart"
[style.zIndex]="'1'">
</div>
<div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div>
`, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: WatermarkOverlayComponent, selector: "div[kendoWatermarkOverlay]" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TileLayoutComponent, decorators: [{
type: Component,
args: [{
selector: 'kendo-tilelayout',
providers: [
LocalizationService,
TileLayoutDraggingService,
TileLayoutKeyboardNavigationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.tilelayout.component'
}
],
template: `
<ng-content></ng-content>
<div #hint class="k-layout-item-hint"
[style.display]="'none'"
[style.order]="targetOrder"
[style.gridColumnEnd]="draggedItemWrapper?.style.gridColumnEnd"
[style.gridRowEnd]="draggedItemWrapper?.style.gridRowEnd"
[style.gridColumnStart]="currentColStart"
[style.gridRowStart]="currentRowStart"
[style.zIndex]="'1'">
</div>
<div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div>
`,
standalone: true,
imports: [NgIf, WatermarkOverlayComponent]
}]
}], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i2.TileLayoutDraggingService }, { type: i3.TileLayoutKeyboardNavigationService }]; }, propDecorators: { columns: [{
type: Input
}], columnWidth: [{
type: Input
}], gap: [{
type: Input
}], reorderable: [{
type: Input
}], resizable: [{
type: Input
}], rowHeight: [{
type: Input
}], autoFlow: [{
type: Input
}], navigable: [{
type: Input
}], reorder: [{
type: Output
}], resize: [{
type: Output
}], hostClass: [{
type: HostBinding,
args: ['class.k-tilelayout']
}], hostRole: [{
type: HostBinding,
args: ['attr.role']
}], gapStyle: [{
type: HostBinding,
args: ['style.gap']
}, {
type: HostBinding,
args: ['style.padding']
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}], items: [{
type: ContentChildren,
args: [TileLayoutItemComponent]
}], hint: [{
type: ViewChild,
args: ['hint', { static: false }]
}] } });