UNPKG

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