diff --git a/apps/demos/configs/Angular/config.js b/apps/demos/configs/Angular/config.js index 60c4d7e1a4cc..af6ea6f9da5c 100644 --- a/apps/demos/configs/Angular/config.js +++ b/apps/demos/configs/Angular/config.js @@ -12,6 +12,7 @@ const componentNames = [ 'button-group', 'button', 'calendar', + 'card-view', 'chart', 'chat', 'check-box', diff --git a/packages/devextreme-angular/src/index.ts b/packages/devextreme-angular/src/index.ts index f6a3c60e1dc4..e0beea6c315d 100644 --- a/packages/devextreme-angular/src/index.ts +++ b/packages/devextreme-angular/src/index.ts @@ -12,6 +12,7 @@ export { DxBulletComponent, DxBulletModule } from 'devextreme-angular/ui/bullet' export { DxButtonComponent, DxButtonModule } from 'devextreme-angular/ui/button'; export { DxButtonGroupComponent, DxButtonGroupModule } from 'devextreme-angular/ui/button-group'; export { DxCalendarComponent, DxCalendarModule } from 'devextreme-angular/ui/calendar'; +export { DxCardViewComponent, DxCardViewModule } from 'devextreme-angular/ui/card-view'; export { DxChartComponent, DxChartModule } from 'devextreme-angular/ui/chart'; export { DxChatComponent, DxChatModule } from 'devextreme-angular/ui/chat'; export { DxCheckBoxComponent, DxCheckBoxModule } from 'devextreme-angular/ui/check-box'; diff --git a/packages/devextreme-angular/src/server/render.ts b/packages/devextreme-angular/src/server/render.ts index 04773ba08834..3027dd54b1af 100644 --- a/packages/devextreme-angular/src/server/render.ts +++ b/packages/devextreme-angular/src/server/render.ts @@ -37,6 +37,12 @@ export class DxServerModule { container.innerHTML = childString; }, + renderIntoContainer: ( + jsx, + container, + ) => { + container.innerHTML = renderToString(jsx); + }, }); } } diff --git a/packages/devextreme-angular/src/ui/all.ts b/packages/devextreme-angular/src/ui/all.ts index b877b2c65df4..716eee7e63e2 100644 --- a/packages/devextreme-angular/src/ui/all.ts +++ b/packages/devextreme-angular/src/ui/all.ts @@ -8,6 +8,7 @@ import { DxBulletModule } from 'devextreme-angular/ui/bullet'; import { DxButtonModule } from 'devextreme-angular/ui/button'; import { DxButtonGroupModule } from 'devextreme-angular/ui/button-group'; import { DxCalendarModule } from 'devextreme-angular/ui/calendar'; +import { DxCardViewModule } from 'devextreme-angular/ui/card-view'; import { DxChartModule } from 'devextreme-angular/ui/chart'; import { DxChatModule } from 'devextreme-angular/ui/chat'; import { DxCheckBoxModule } from 'devextreme-angular/ui/check-box'; @@ -94,6 +95,7 @@ import { DxTemplateModule } from 'devextreme-angular/core'; DxButtonModule, DxButtonGroupModule, DxCalendarModule, + DxCardViewModule, DxChartModule, DxChatModule, DxCheckBoxModule, @@ -179,6 +181,7 @@ import { DxTemplateModule } from 'devextreme-angular/core'; DxButtonModule, DxButtonGroupModule, DxCalendarModule, + DxCardViewModule, DxChartModule, DxChatModule, DxCheckBoxModule, diff --git a/packages/devextreme-angular/src/ui/card-view/index.ts b/packages/devextreme-angular/src/ui/card-view/index.ts new file mode 100644 index 000000000000..fb58cd0681c9 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/index.ts @@ -0,0 +1,815 @@ +/* tslint:disable:max-line-length */ + + +import { + TransferState, + Component, + NgModule, + ElementRef, + NgZone, + PLATFORM_ID, + Inject, + + Input, + Output, + OnDestroy, + EventEmitter, + OnChanges, + DoCheck, + SimpleChanges, + ContentChildren, + QueryList +} from '@angular/core'; + +export { ExplicitTypes } from 'devextreme/ui/card_view'; + +import DataSource from 'devextreme/data/data_source'; +import { CardCover, CardHeader, ColumnProperties, HeaderPanel, Paging, RemoteOperations, dxCardViewToolbar } from 'devextreme/ui/card_view'; +import { Mode } from 'devextreme/common'; +import { DataSourceOptions } from 'devextreme/data/data_source'; +import { Store } from 'devextreme/data/store'; +import { EventInfo } from 'devextreme/common/core/events'; +import { Pager } from 'devextreme/common/grids'; + +import DxCardView from 'devextreme/ui/card_view'; + + +import { + DxComponent, + DxTemplateHost, + DxIntegrationModule, + DxTemplateModule, + NestedOptionHost, + IterableDifferHelper, + WatcherHelper +} from 'devextreme-angular/core'; + +import { DxoCardCoverModule } from 'devextreme-angular/ui/nested'; +import { DxoCardHeaderModule } from 'devextreme-angular/ui/nested'; +import { DxiColumnModule } from 'devextreme-angular/ui/nested'; +import { DxoHeaderPanelModule } from 'devextreme-angular/ui/nested'; +import { DxoPagerModule } from 'devextreme-angular/ui/nested'; +import { DxoPagingModule } from 'devextreme-angular/ui/nested'; +import { DxoRemoteOperationsModule } from 'devextreme-angular/ui/nested'; +import { DxoToolbarModule } from 'devextreme-angular/ui/nested'; +import { DxiItemModule } from 'devextreme-angular/ui/nested'; + +import { DxoCardViewCardCoverModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxoCardViewCardHeaderModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxiCardViewColumnModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxoCardViewHeaderPanelModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxiCardViewItemModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxoCardViewPagerModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxoCardViewPagingModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxoCardViewRemoteOperationsModule } from 'devextreme-angular/ui/card-view/nested'; +import { DxoCardViewToolbarModule } from 'devextreme-angular/ui/card-view/nested'; + +import { DxiColumnComponent } from 'devextreme-angular/ui/nested'; + +import { DxiCardViewColumnComponent } from 'devextreme-angular/ui/card-view/nested'; + + +/** + * [descr:dxCardView] + + */ +@Component({ + selector: 'dx-card-view', + template: '', + host: { ngSkipHydration: 'true' }, + providers: [ + DxTemplateHost, + WatcherHelper, + NestedOptionHost, + IterableDifferHelper + ] +}) +export class DxCardViewComponent extends DxComponent implements OnDestroy, OnChanges, DoCheck { + instance: DxCardView = null; + + /** + * [descr:WidgetOptions.accessKey] + + */ + @Input() + get accessKey(): string | undefined { + return this._getOption('accessKey'); + } + set accessKey(value: string | undefined) { + this._setOption('accessKey', value); + } + + + /** + * [descr:WidgetOptions.activeStateEnabled] + + */ + @Input() + get activeStateEnabled(): boolean { + return this._getOption('activeStateEnabled'); + } + set activeStateEnabled(value: boolean) { + this._setOption('activeStateEnabled', value); + } + + + /** + * [descr:ContentViewConfiguration.cardCover] + + */ + @Input() + get cardCover(): CardCover { + return this._getOption('cardCover'); + } + set cardCover(value: CardCover) { + this._setOption('cardCover', value); + } + + + /** + * [descr:ContentViewConfiguration.cardHeader] + + */ + @Input() + get cardHeader(): CardHeader { + return this._getOption('cardHeader'); + } + set cardHeader(value: CardHeader) { + this._setOption('cardHeader', value); + } + + + /** + * [descr:ContentViewConfiguration.cardMaxWidth] + + */ + @Input() + get cardMaxWidth(): number { + return this._getOption('cardMaxWidth'); + } + set cardMaxWidth(value: number) { + this._setOption('cardMaxWidth', value); + } + + + /** + * [descr:ContentViewConfiguration.cardMinWidth] + + */ + @Input() + get cardMinWidth(): number { + return this._getOption('cardMinWidth'); + } + set cardMinWidth(value: number) { + this._setOption('cardMinWidth', value); + } + + + /** + * [descr:ContentViewConfiguration.cardsPerRow] + + */ + @Input() + get cardsPerRow(): Mode | number { + return this._getOption('cardsPerRow'); + } + set cardsPerRow(value: Mode | number) { + this._setOption('cardsPerRow', value); + } + + + /** + * [descr:ContentViewConfiguration.cardTemplate] + + */ + @Input() + get cardTemplate(): any { + return this._getOption('cardTemplate'); + } + set cardTemplate(value: any) { + this._setOption('cardTemplate', value); + } + + + /** + * [descr:ColumnsControllerConfiguration.columns] + + */ + @Input() + get columns(): Array { + return this._getOption('columns'); + } + set columns(value: Array) { + this._setOption('columns', value); + } + + + /** + * [descr:DataControllerConfiguration.dataSource] + + */ + @Input() + get dataSource(): Array | DataSource | DataSourceOptions | Store | string { + return this._getOption('dataSource'); + } + set dataSource(value: Array | DataSource | DataSourceOptions | Store | string) { + this._setOption('dataSource', value); + } + + + /** + * [descr:WidgetOptions.disabled] + + */ + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + + /** + * [descr:DOMComponentOptions.elementAttr] + + */ + @Input() + get elementAttr(): Record { + return this._getOption('elementAttr'); + } + set elementAttr(value: Record) { + this._setOption('elementAttr', value); + } + + + /** + * [descr:WidgetOptions.focusStateEnabled] + + */ + @Input() + get focusStateEnabled(): boolean { + return this._getOption('focusStateEnabled'); + } + set focusStateEnabled(value: boolean) { + this._setOption('focusStateEnabled', value); + } + + + /** + * [descr:HeaderPanelConfiguration.headerPanel] + + */ + @Input() + get headerPanel(): HeaderPanel { + return this._getOption('headerPanel'); + } + set headerPanel(value: HeaderPanel) { + this._setOption('headerPanel', value); + } + + + /** + * [descr:DOMComponentOptions.height] + + */ + @Input() + get height(): (() => number | string) | number | string | undefined { + return this._getOption('height'); + } + set height(value: (() => number | string) | number | string | undefined) { + this._setOption('height', value); + } + + + /** + * [descr:WidgetOptions.hint] + + */ + @Input() + get hint(): string | undefined { + return this._getOption('hint'); + } + set hint(value: string | undefined) { + this._setOption('hint', value); + } + + + /** + * [descr:WidgetOptions.hoverStateEnabled] + + */ + @Input() + get hoverStateEnabled(): boolean { + return this._getOption('hoverStateEnabled'); + } + set hoverStateEnabled(value: boolean) { + this._setOption('hoverStateEnabled', value); + } + + + /** + * [descr:DataControllerConfiguration.keyExpr] + + */ + @Input() + get keyExpr(): Array | string { + return this._getOption('keyExpr'); + } + set keyExpr(value: Array | string) { + this._setOption('keyExpr', value); + } + + + /** + * [descr:PagerConfiguration.pager] + + */ + @Input() + get pager(): Pager { + return this._getOption('pager'); + } + set pager(value: Pager) { + this._setOption('pager', value); + } + + + /** + * [descr:DataControllerConfiguration.paging] + + */ + @Input() + get paging(): Paging { + return this._getOption('paging'); + } + set paging(value: Paging) { + this._setOption('paging', value); + } + + + /** + * [descr:DataControllerConfiguration.remoteOperations] + + */ + @Input() + get remoteOperations(): boolean | Mode | RemoteOperations { + return this._getOption('remoteOperations'); + } + set remoteOperations(value: boolean | Mode | RemoteOperations) { + this._setOption('remoteOperations', value); + } + + + /** + * [descr:DOMComponentOptions.rtlEnabled] + + */ + @Input() + get rtlEnabled(): boolean { + return this._getOption('rtlEnabled'); + } + set rtlEnabled(value: boolean) { + this._setOption('rtlEnabled', value); + } + + + /** + * [descr:WidgetOptions.tabIndex] + + */ + @Input() + get tabIndex(): number { + return this._getOption('tabIndex'); + } + set tabIndex(value: number) { + this._setOption('tabIndex', value); + } + + + /** + * [descr:dxCardViewOptions.toolbar] + + */ + @Input() + get toolbar(): dxCardViewToolbar | undefined { + return this._getOption('toolbar'); + } + set toolbar(value: dxCardViewToolbar | undefined) { + this._setOption('toolbar', value); + } + + + /** + * [descr:WidgetOptions.visible] + + */ + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + + + /** + * [descr:DOMComponentOptions.width] + + */ + @Input() + get width(): (() => number | string) | number | string | undefined { + return this._getOption('width'); + } + set width(value: (() => number | string) | number | string | undefined) { + this._setOption('width', value); + } + + /** + + * [descr:WidgetOptions.onContentReady] + + + */ + @Output() onContentReady: EventEmitter>; + + /** + + * [descr:dxCardViewOptions.onDataErrorOccurred] + + + */ + @Output() onDataErrorOccurred: EventEmitter; + + /** + + * [descr:DOMComponentOptions.onDisposing] + + + */ + @Output() onDisposing: EventEmitter>; + + /** + + * [descr:ComponentOptions.onInitialized] + + + */ + @Output() onInitialized: EventEmitter; + + /** + + * [descr:DOMComponentOptions.onOptionChanged] + + + */ + @Output() onOptionChanged: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() accessKeyChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() activeStateEnabledChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() cardCoverChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() cardHeaderChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() cardMaxWidthChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() cardMinWidthChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() cardsPerRowChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() cardTemplateChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() columnsChange: EventEmitter>; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() dataSourceChange: EventEmitter | DataSource | DataSourceOptions | Store | string>; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() disabledChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() elementAttrChange: EventEmitter>; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() focusStateEnabledChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() headerPanelChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() heightChange: EventEmitter<(() => number | string) | number | string | undefined>; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() hintChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() hoverStateEnabledChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() keyExprChange: EventEmitter | string>; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() pagerChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() pagingChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() remoteOperationsChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() rtlEnabledChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() tabIndexChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() toolbarChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() visibleChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() widthChange: EventEmitter<(() => number | string) | number | string | undefined>; + + + + + @ContentChildren(DxiCardViewColumnComponent) + get columnsChildren(): QueryList { + return this._getOption('columns'); + } + set columnsChildren(value) { + this._setChildren('columns', value, 'DxiCardViewColumnComponent'); + } + + + @ContentChildren(DxiColumnComponent) + get columnsLegacyChildren(): QueryList { + return this._getOption('columns'); + } + set columnsLegacyChildren(value) { + this._setChildren('columns', value, 'DxiColumnComponent'); + } + + + + + constructor(elementRef: ElementRef, ngZone: NgZone, templateHost: DxTemplateHost, + private _watcherHelper: WatcherHelper, + private _idh: IterableDifferHelper, + optionHost: NestedOptionHost, + transferState: TransferState, + @Inject(PLATFORM_ID) platformId: any) { + + super(elementRef, ngZone, templateHost, _watcherHelper, transferState, platformId); + + this._createEventEmitters([ + { subscribe: 'contentReady', emit: 'onContentReady' }, + { subscribe: 'dataErrorOccurred', emit: 'onDataErrorOccurred' }, + { subscribe: 'disposing', emit: 'onDisposing' }, + { subscribe: 'initialized', emit: 'onInitialized' }, + { subscribe: 'optionChanged', emit: 'onOptionChanged' }, + { emit: 'accessKeyChange' }, + { emit: 'activeStateEnabledChange' }, + { emit: 'cardCoverChange' }, + { emit: 'cardHeaderChange' }, + { emit: 'cardMaxWidthChange' }, + { emit: 'cardMinWidthChange' }, + { emit: 'cardsPerRowChange' }, + { emit: 'cardTemplateChange' }, + { emit: 'columnsChange' }, + { emit: 'dataSourceChange' }, + { emit: 'disabledChange' }, + { emit: 'elementAttrChange' }, + { emit: 'focusStateEnabledChange' }, + { emit: 'headerPanelChange' }, + { emit: 'heightChange' }, + { emit: 'hintChange' }, + { emit: 'hoverStateEnabledChange' }, + { emit: 'keyExprChange' }, + { emit: 'pagerChange' }, + { emit: 'pagingChange' }, + { emit: 'remoteOperationsChange' }, + { emit: 'rtlEnabledChange' }, + { emit: 'tabIndexChange' }, + { emit: 'toolbarChange' }, + { emit: 'visibleChange' }, + { emit: 'widthChange' } + ]); + + this._idh.setHost(this); + optionHost.setHost(this); + } + + protected _createInstance(element, options) { + + return new DxCardView(element, options); + } + + + ngOnDestroy() { + this._destroyWidget(); + } + + ngOnChanges(changes: SimpleChanges) { + super.ngOnChanges(changes); + this.setupChanges('columns', changes); + this.setupChanges('dataSource', changes); + this.setupChanges('keyExpr', changes); + } + + setupChanges(prop: string, changes: SimpleChanges) { + if (!(prop in this._optionsToUpdate)) { + this._idh.setup(prop, changes); + } + } + + ngDoCheck() { + this._idh.doCheck('columns'); + this._idh.doCheck('dataSource'); + this._idh.doCheck('keyExpr'); + this._watcherHelper.checkWatchers(); + super.ngDoCheck(); + super.clearChangedOptions(); + } + + _setOption(name: string, value: any) { + let isSetup = this._idh.setupSingle(name, value); + let isChanged = this._idh.getChanges(name, value) !== null; + + if (isSetup || isChanged) { + super._setOption(name, value); + } + } +} + +@NgModule({ + imports: [ + DxoCardCoverModule, + DxoCardHeaderModule, + DxiColumnModule, + DxoHeaderPanelModule, + DxoPagerModule, + DxoPagingModule, + DxoRemoteOperationsModule, + DxoToolbarModule, + DxiItemModule, + DxoCardViewCardCoverModule, + DxoCardViewCardHeaderModule, + DxiCardViewColumnModule, + DxoCardViewHeaderPanelModule, + DxiCardViewItemModule, + DxoCardViewPagerModule, + DxoCardViewPagingModule, + DxoCardViewRemoteOperationsModule, + DxoCardViewToolbarModule, + DxIntegrationModule, + DxTemplateModule + ], + declarations: [ + DxCardViewComponent + ], + exports: [ + DxCardViewComponent, + DxoCardCoverModule, + DxoCardHeaderModule, + DxiColumnModule, + DxoHeaderPanelModule, + DxoPagerModule, + DxoPagingModule, + DxoRemoteOperationsModule, + DxoToolbarModule, + DxiItemModule, + DxoCardViewCardCoverModule, + DxoCardViewCardHeaderModule, + DxiCardViewColumnModule, + DxoCardViewHeaderPanelModule, + DxiCardViewItemModule, + DxoCardViewPagerModule, + DxoCardViewPagingModule, + DxoCardViewRemoteOperationsModule, + DxoCardViewToolbarModule, + DxTemplateModule + ] +}) +export class DxCardViewModule { } + +import type * as DxCardViewTypes from "devextreme/ui/card_view_types"; +export { DxCardViewTypes }; + + diff --git a/packages/devextreme-angular/src/ui/card-view/nested/card-cover.ts b/packages/devextreme-angular/src/ui/card-view/nested/card-cover.ts new file mode 100644 index 000000000000..2e528065ea1b --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/card-cover.ts @@ -0,0 +1,80 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-card-cover', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewCardCoverComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get altExpr(): ((data: any) => string) | string { + return this._getOption('altExpr'); + } + set altExpr(value: ((data: any) => string) | string) { + this._setOption('altExpr', value); + } + + @Input() + get imageExpr(): ((data: any) => string) | string { + return this._getOption('imageExpr'); + } + set imageExpr(value: ((data: any) => string) | string) { + this._setOption('imageExpr', value); + } + + + protected get _optionPath() { + return 'cardCover'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewCardCoverComponent + ], + exports: [ + DxoCardViewCardCoverComponent + ], +}) +export class DxoCardViewCardCoverModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/card-header.ts b/packages/devextreme-angular/src/ui/card-view/nested/card-header.ts new file mode 100644 index 000000000000..f79ad3dc4cf5 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/card-header.ts @@ -0,0 +1,80 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-card-header', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewCardHeaderComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get captionExpr(): ((data: any) => string) | string { + return this._getOption('captionExpr'); + } + set captionExpr(value: ((data: any) => string) | string) { + this._setOption('captionExpr', value); + } + + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + + + protected get _optionPath() { + return 'cardHeader'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewCardHeaderComponent + ], + exports: [ + DxoCardViewCardHeaderComponent + ], +}) +export class DxoCardViewCardHeaderModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/column-dxi.ts b/packages/devextreme-angular/src/ui/card-view/nested/column-dxi.ts new file mode 100644 index 000000000000..1461ef81a741 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/column-dxi.ts @@ -0,0 +1,98 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { CollectionNestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxi-card-view-column', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxiCardViewColumnComponent extends CollectionNestedOption { + @Input() + get fieldCaptionTemplate(): any { + return this._getOption('fieldCaptionTemplate'); + } + set fieldCaptionTemplate(value: any) { + this._setOption('fieldCaptionTemplate', value); + } + + @Input() + get fieldTemplate(): any { + return this._getOption('fieldTemplate'); + } + set fieldTemplate(value: any) { + this._setOption('fieldTemplate', value); + } + + @Input() + get fieldValueTemplate(): any { + return this._getOption('fieldValueTemplate'); + } + set fieldValueTemplate(value: any) { + this._setOption('fieldValueTemplate', value); + } + + @Input() + get headerItemCssClass(): string { + return this._getOption('headerItemCssClass'); + } + set headerItemCssClass(value: string) { + this._setOption('headerItemCssClass', value); + } + + @Input() + get headerItemTemplate(): any { + return this._getOption('headerItemTemplate'); + } + set headerItemTemplate(value: any) { + this._setOption('headerItemTemplate', value); + } + + + protected get _optionPath() { + return 'columns'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } + +} + +@NgModule({ + declarations: [ + DxiCardViewColumnComponent + ], + exports: [ + DxiCardViewColumnComponent + ], +}) +export class DxiCardViewColumnModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/header-panel.ts b/packages/devextreme-angular/src/ui/card-view/nested/header-panel.ts new file mode 100644 index 000000000000..dadf8bc39bcd --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/header-panel.ts @@ -0,0 +1,96 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-header-panel', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewHeaderPanelComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get dragging(): Record { + return this._getOption('dragging'); + } + set dragging(value: Record) { + this._setOption('dragging', value); + } + + @Input() + get itemCssClass(): string { + return this._getOption('itemCssClass'); + } + set itemCssClass(value: string) { + this._setOption('itemCssClass', value); + } + + @Input() + get itemTemplate(): any { + return this._getOption('itemTemplate'); + } + set itemTemplate(value: any) { + this._setOption('itemTemplate', value); + } + + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + + + protected get _optionPath() { + return 'headerPanel'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewHeaderPanelComponent + ], + exports: [ + DxoCardViewHeaderPanelComponent + ], +}) +export class DxoCardViewHeaderPanelModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/index.ts b/packages/devextreme-angular/src/ui/card-view/nested/index.ts new file mode 100644 index 000000000000..e4d1829db8fc --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/index.ts @@ -0,0 +1,10 @@ +export * from './card-cover'; +export * from './card-header'; +export * from './column-dxi'; +export * from './header-panel'; +export * from './item-dxi'; +export * from './pager'; +export * from './paging'; +export * from './remote-operations'; +export * from './toolbar'; + diff --git a/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts b/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts new file mode 100644 index 000000000000..93c136a21c3c --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/item-dxi.ts @@ -0,0 +1,186 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + NgModule, + Host, + ElementRef, + Renderer2, + Inject, + AfterViewInit, + SkipSelf, + Input +} from '@angular/core'; + +import { DOCUMENT } from '@angular/common'; + + +import { LocateInMenuMode, ShowTextMode } from 'devextreme/ui/toolbar'; +import { ToolbarItemLocation, ToolbarItemComponent } from 'devextreme/common'; +import { PredefinedToolbarItem } from 'devextreme/ui/card_view'; + +import { + NestedOptionHost, + extractTemplate, + DxTemplateDirective, + IDxTemplateHost, + DxTemplateHost +} from 'devextreme-angular/core'; +import { CollectionNestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxi-card-view-item', + template: '', + styles: [':host { display: block; }'], + providers: [NestedOptionHost, DxTemplateHost] +}) +export class DxiCardViewItemComponent extends CollectionNestedOption implements AfterViewInit, + IDxTemplateHost { + @Input() + get cssClass(): string | undefined { + return this._getOption('cssClass'); + } + set cssClass(value: string | undefined) { + this._setOption('cssClass', value); + } + + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get html(): string { + return this._getOption('html'); + } + set html(value: string) { + this._setOption('html', value); + } + + @Input() + get locateInMenu(): LocateInMenuMode { + return this._getOption('locateInMenu'); + } + set locateInMenu(value: LocateInMenuMode) { + this._setOption('locateInMenu', value); + } + + @Input() + get location(): ToolbarItemLocation { + return this._getOption('location'); + } + set location(value: ToolbarItemLocation) { + this._setOption('location', value); + } + + @Input() + get menuItemTemplate(): any { + return this._getOption('menuItemTemplate'); + } + set menuItemTemplate(value: any) { + this._setOption('menuItemTemplate', value); + } + + @Input() + get name(): PredefinedToolbarItem | string { + return this._getOption('name'); + } + set name(value: PredefinedToolbarItem | string) { + this._setOption('name', value); + } + + @Input() + get options(): any { + return this._getOption('options'); + } + set options(value: any) { + this._setOption('options', value); + } + + @Input() + get showText(): ShowTextMode { + return this._getOption('showText'); + } + set showText(value: ShowTextMode) { + this._setOption('showText', value); + } + + @Input() + get template(): any { + return this._getOption('template'); + } + set template(value: any) { + this._setOption('template', value); + } + + @Input() + get text(): string { + return this._getOption('text'); + } + set text(value: string) { + this._setOption('text', value); + } + + @Input() + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } + + @Input() + get widget(): ToolbarItemComponent { + return this._getOption('widget'); + } + set widget(value: ToolbarItemComponent) { + this._setOption('widget', value); + } + + + protected get _optionPath() { + return 'items'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost, + private renderer: Renderer2, + @Inject(DOCUMENT) private document: any, + @Host() templateHost: DxTemplateHost, + private element: ElementRef) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + templateHost.setHost(this); + } + + setTemplate(template: DxTemplateDirective) { + this.template = template; + } + ngAfterViewInit() { + extractTemplate(this, this.element, this.renderer, this.document); + } + + + + ngOnDestroy() { + this._deleteRemovedOptions(this._fullOptionPath()); + } + +} + +@NgModule({ + declarations: [ + DxiCardViewItemComponent + ], + exports: [ + DxiCardViewItemComponent + ], +}) +export class DxiCardViewItemModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/ng-package.json b/packages/devextreme-angular/src/ui/card-view/nested/ng-package.json new file mode 100644 index 000000000000..3360c83b3395 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/packages/devextreme-angular/src/ui/card-view/nested/pager.ts b/packages/devextreme-angular/src/ui/card-view/nested/pager.ts new file mode 100644 index 000000000000..d24455716cf5 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/pager.ts @@ -0,0 +1,130 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + +import { PagerPageSize } from 'devextreme/common/grids'; +import { Mode, DisplayMode } from 'devextreme/common'; + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-pager', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewPagerComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get allowedPageSizes(): Array | Mode { + return this._getOption('allowedPageSizes'); + } + set allowedPageSizes(value: Array | Mode) { + this._setOption('allowedPageSizes', value); + } + + @Input() + get displayMode(): DisplayMode { + return this._getOption('displayMode'); + } + set displayMode(value: DisplayMode) { + this._setOption('displayMode', value); + } + + @Input() + get infoText(): string { + return this._getOption('infoText'); + } + set infoText(value: string) { + this._setOption('infoText', value); + } + + @Input() + get label(): string { + return this._getOption('label'); + } + set label(value: string) { + this._setOption('label', value); + } + + @Input() + get showInfo(): boolean { + return this._getOption('showInfo'); + } + set showInfo(value: boolean) { + this._setOption('showInfo', value); + } + + @Input() + get showNavigationButtons(): boolean { + return this._getOption('showNavigationButtons'); + } + set showNavigationButtons(value: boolean) { + this._setOption('showNavigationButtons', value); + } + + @Input() + get showPageSizeSelector(): boolean { + return this._getOption('showPageSizeSelector'); + } + set showPageSizeSelector(value: boolean) { + this._setOption('showPageSizeSelector', value); + } + + @Input() + get visible(): boolean | Mode { + return this._getOption('visible'); + } + set visible(value: boolean | Mode) { + this._setOption('visible', value); + } + + + protected get _optionPath() { + return 'pager'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewPagerComponent + ], + exports: [ + DxoCardViewPagerComponent + ], +}) +export class DxoCardViewPagerModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/paging.ts b/packages/devextreme-angular/src/ui/card-view/nested/paging.ts new file mode 100644 index 000000000000..767181e3c1ed --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/paging.ts @@ -0,0 +1,109 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input, + Output, + EventEmitter +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-paging', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewPagingComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get enabled(): boolean { + return this._getOption('enabled'); + } + set enabled(value: boolean) { + this._setOption('enabled', value); + } + + @Input() + get pageIndex(): number { + return this._getOption('pageIndex'); + } + set pageIndex(value: number) { + this._setOption('pageIndex', value); + } + + @Input() + get pageSize(): number { + return this._getOption('pageSize'); + } + set pageSize(value: number) { + this._setOption('pageSize', value); + } + + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() pageIndexChange: EventEmitter; + + /** + + * This member supports the internal infrastructure and is not intended to be used directly from your code. + + */ + @Output() pageSizeChange: EventEmitter; + protected get _optionPath() { + return 'paging'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + + this._createEventEmitters([ + { emit: 'pageIndexChange' }, + { emit: 'pageSizeChange' } + ]); + + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewPagingComponent + ], + exports: [ + DxoCardViewPagingComponent + ], +}) +export class DxoCardViewPagingModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/remote-operations.ts b/packages/devextreme-angular/src/ui/card-view/nested/remote-operations.ts new file mode 100644 index 000000000000..2a091e46a620 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/remote-operations.ts @@ -0,0 +1,96 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; + + +@Component({ + selector: 'dxo-card-view-remote-operations', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewRemoteOperationsComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get filtering(): boolean { + return this._getOption('filtering'); + } + set filtering(value: boolean) { + this._setOption('filtering', value); + } + + @Input() + get paging(): boolean { + return this._getOption('paging'); + } + set paging(value: boolean) { + this._setOption('paging', value); + } + + @Input() + get sorting(): boolean { + return this._getOption('sorting'); + } + set sorting(value: boolean) { + this._setOption('sorting', value); + } + + @Input() + get summary(): boolean { + return this._getOption('summary'); + } + set summary(value: boolean) { + this._setOption('summary', value); + } + + + protected get _optionPath() { + return 'remoteOperations'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewRemoteOperationsComponent + ], + exports: [ + DxoCardViewRemoteOperationsComponent + ], +}) +export class DxoCardViewRemoteOperationsModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/nested/toolbar.ts b/packages/devextreme-angular/src/ui/card-view/nested/toolbar.ts new file mode 100644 index 000000000000..26b452c46098 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/nested/toolbar.ts @@ -0,0 +1,101 @@ +/* tslint:disable:max-line-length */ + + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf, + Input, + ContentChildren, + forwardRef, + QueryList +} from '@angular/core'; + + + + +import { dxCardViewToolbarItem, PredefinedToolbarItem } from 'devextreme/ui/card_view'; + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { NestedOption } from 'devextreme-angular/core'; +import { DxiCardViewItemComponent } from './item-dxi'; + + +@Component({ + selector: 'dxo-card-view-toolbar', + template: '', + styles: [''], + providers: [NestedOptionHost] +}) +export class DxoCardViewToolbarComponent extends NestedOption implements OnDestroy, OnInit { + @Input() + get disabled(): boolean { + return this._getOption('disabled'); + } + set disabled(value: boolean) { + this._setOption('disabled', value); + } + + @Input() + get items(): Array { + return this._getOption('items'); + } + set items(value: Array) { + this._setOption('items', value); + } + + @Input() + get visible(): boolean | undefined { + return this._getOption('visible'); + } + set visible(value: boolean | undefined) { + this._setOption('visible', value); + } + + + protected get _optionPath() { + return 'toolbar'; + } + + + @ContentChildren(forwardRef(() => DxiCardViewItemComponent)) + get itemsChildren(): QueryList { + return this._getOption('items'); + } + set itemsChildren(value) { + this.setChildren('items', value); + } + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardViewToolbarComponent + ], + exports: [ + DxoCardViewToolbarComponent + ], +}) +export class DxoCardViewToolbarModule { } diff --git a/packages/devextreme-angular/src/ui/card-view/ng-package.json b/packages/devextreme-angular/src/ui/card-view/ng-package.json new file mode 100644 index 000000000000..3360c83b3395 --- /dev/null +++ b/packages/devextreme-angular/src/ui/card-view/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} \ No newline at end of file diff --git a/packages/devextreme-angular/src/ui/nested/base/button-group-item-dxi.ts b/packages/devextreme-angular/src/ui/nested/base/button-group-item-dxi.ts index 40eeb038ca34..a402bf625ab3 100644 --- a/packages/devextreme-angular/src/ui/nested/base/button-group-item-dxi.ts +++ b/packages/devextreme-angular/src/ui/nested/base/button-group-item-dxi.ts @@ -8,6 +8,7 @@ import { import { AsyncRule, ButtonStyle, ButtonType, CompareRule, CustomRule, EmailRule, HorizontalAlignment, NumericRule, PatternRule, RangeRule, RequiredRule, StringLengthRule, ToolbarItemComponent, ToolbarItemLocation, VerticalAlignment } from 'devextreme/common'; import { Properties as dxBoxOptions } from 'devextreme/ui/box'; import { Properties as dxButtonOptions } from 'devextreme/ui/button'; +import { PredefinedToolbarItem } from 'devextreme/ui/card_view'; import { User } from 'devextreme/ui/chat'; import { dxContextMenuItem } from 'devextreme/ui/context_menu'; import { DataGridPredefinedToolbarItem } from 'devextreme/ui/data_grid'; @@ -147,6 +148,62 @@ export abstract class DxiButtonGroupItem extends CollectionNestedOption { this._setOption('hint', value); } + get cssClass(): string | undefined { + return this._getOption('cssClass'); + } + set cssClass(value: string | undefined) { + this._setOption('cssClass', value); + } + + get locateInMenu(): LocateInMenuMode { + return this._getOption('locateInMenu'); + } + set locateInMenu(value: LocateInMenuMode) { + this._setOption('locateInMenu', value); + } + + get location(): ToolbarItemLocation | Array { + return this._getOption('location'); + } + set location(value: ToolbarItemLocation | Array) { + this._setOption('location', value); + } + + get menuItemTemplate(): any { + return this._getOption('menuItemTemplate'); + } + set menuItemTemplate(value: any) { + this._setOption('menuItemTemplate', value); + } + + get name(): PredefinedToolbarItem | string | undefined | DataGridPredefinedToolbarItem | Command | FileManagerPredefinedContextMenuItem | FileManagerPredefinedToolbarItem | GanttPredefinedContextMenuItem | GanttPredefinedToolbarItem | HtmlEditorPredefinedContextMenuItem | HtmlEditorPredefinedToolbarItem | TreeListPredefinedToolbarItem { + return this._getOption('name'); + } + set name(value: PredefinedToolbarItem | string | undefined | DataGridPredefinedToolbarItem | Command | FileManagerPredefinedContextMenuItem | FileManagerPredefinedToolbarItem | GanttPredefinedContextMenuItem | GanttPredefinedToolbarItem | HtmlEditorPredefinedContextMenuItem | HtmlEditorPredefinedToolbarItem | TreeListPredefinedToolbarItem) { + this._setOption('name', value); + } + + get options(): any { + return this._getOption('options'); + } + set options(value: any) { + this._setOption('options', value); + } + + get showText(): ShowTextMode { + return this._getOption('showText'); + } + set showText(value: ShowTextMode) { + this._setOption('showText', value); + } + + get widget(): ToolbarItemComponent { + return this._getOption('widget'); + } + set widget(value: ToolbarItemComponent) { + this._setOption('widget', value); + } + get author(): User { return this._getOption('author'); } @@ -210,13 +267,6 @@ export abstract class DxiButtonGroupItem extends CollectionNestedOption { this._setOption('colSpan', value); } - get cssClass(): string | undefined { - return this._getOption('cssClass'); - } - set cssClass(value: string | undefined) { - this._setOption('cssClass', value); - } - get dataField(): string | undefined { return this._getOption('dataField'); } @@ -266,13 +316,6 @@ export abstract class DxiButtonGroupItem extends CollectionNestedOption { this._setOption('label', value); } - get name(): string | undefined | DataGridPredefinedToolbarItem | Command | FileManagerPredefinedContextMenuItem | FileManagerPredefinedToolbarItem | GanttPredefinedContextMenuItem | GanttPredefinedToolbarItem | HtmlEditorPredefinedContextMenuItem | HtmlEditorPredefinedToolbarItem | TreeListPredefinedToolbarItem { - return this._getOption('name'); - } - set name(value: string | undefined | DataGridPredefinedToolbarItem | Command | FileManagerPredefinedContextMenuItem | FileManagerPredefinedToolbarItem | GanttPredefinedContextMenuItem | GanttPredefinedToolbarItem | HtmlEditorPredefinedContextMenuItem | HtmlEditorPredefinedToolbarItem | TreeListPredefinedToolbarItem) { - this._setOption('name', value); - } - get validationRules(): Array { return this._getOption('validationRules'); } @@ -371,48 +414,6 @@ export abstract class DxiButtonGroupItem extends CollectionNestedOption { this._setOption('verticalAlignment', value); } - get locateInMenu(): LocateInMenuMode { - return this._getOption('locateInMenu'); - } - set locateInMenu(value: LocateInMenuMode) { - this._setOption('locateInMenu', value); - } - - get location(): ToolbarItemLocation | Array { - return this._getOption('location'); - } - set location(value: ToolbarItemLocation | Array) { - this._setOption('location', value); - } - - get menuItemTemplate(): any { - return this._getOption('menuItemTemplate'); - } - set menuItemTemplate(value: any) { - this._setOption('menuItemTemplate', value); - } - - get options(): any { - return this._getOption('options'); - } - set options(value: any) { - this._setOption('options', value); - } - - get showText(): ShowTextMode { - return this._getOption('showText'); - } - set showText(value: ShowTextMode) { - this._setOption('showText', value); - } - - get widget(): ToolbarItemComponent { - return this._getOption('widget'); - } - set widget(value: ToolbarItemComponent) { - this._setOption('widget', value); - } - get height(): number { return this._getOption('height'); } diff --git a/packages/devextreme-angular/src/ui/nested/base/card-cover.ts b/packages/devextreme-angular/src/ui/nested/base/card-cover.ts new file mode 100644 index 000000000000..0ac47fbb0549 --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/base/card-cover.ts @@ -0,0 +1,26 @@ +/* tslint:disable:max-line-length */ + +import { NestedOption } from 'devextreme-angular/core'; +import { + Component, +} from '@angular/core'; + + +@Component({ + template: '' +}) +export abstract class DxoCardCover extends NestedOption { + get altExpr(): Function | string { + return this._getOption('altExpr'); + } + set altExpr(value: Function | string) { + this._setOption('altExpr', value); + } + + get imageExpr(): Function | string { + return this._getOption('imageExpr'); + } + set imageExpr(value: Function | string) { + this._setOption('imageExpr', value); + } +} diff --git a/packages/devextreme-angular/src/ui/nested/base/card-header.ts b/packages/devextreme-angular/src/ui/nested/base/card-header.ts new file mode 100644 index 000000000000..77e747e6aa72 --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/base/card-header.ts @@ -0,0 +1,26 @@ +/* tslint:disable:max-line-length */ + +import { NestedOption } from 'devextreme-angular/core'; +import { + Component, +} from '@angular/core'; + + +@Component({ + template: '' +}) +export abstract class DxoCardHeader extends NestedOption { + get captionExpr(): Function | string { + return this._getOption('captionExpr'); + } + set captionExpr(value: Function | string) { + this._setOption('captionExpr', value); + } + + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } +} diff --git a/packages/devextreme-angular/src/ui/nested/base/data-grid-toolbar.ts b/packages/devextreme-angular/src/ui/nested/base/card-view-toolbar.ts similarity index 53% rename from packages/devextreme-angular/src/ui/nested/base/data-grid-toolbar.ts rename to packages/devextreme-angular/src/ui/nested/base/card-view-toolbar.ts index 9731be2b793d..4356ac1b0789 100644 --- a/packages/devextreme-angular/src/ui/nested/base/data-grid-toolbar.ts +++ b/packages/devextreme-angular/src/ui/nested/base/card-view-toolbar.ts @@ -5,17 +5,20 @@ import { Component, } from '@angular/core'; +import { ToolbarItemComponent, ToolbarItemLocation } from 'devextreme/common'; import { UserDefinedElement } from 'devextreme/core/element'; +import { PredefinedToolbarItem } from 'devextreme/ui/card_view'; import { DataGridPredefinedToolbarItem, dxDataGridToolbarItem } from 'devextreme/ui/data_grid'; import { dxFileManagerToolbarItem, FileManagerPredefinedToolbarItem } from 'devextreme/ui/file_manager'; import { dxGanttToolbarItem, GanttPredefinedToolbarItem } from 'devextreme/ui/gantt'; import { dxHtmlEditorToolbarItem, HtmlEditorPredefinedToolbarItem } from 'devextreme/ui/html_editor'; +import { LocateInMenuMode, ShowTextMode } from 'devextreme/ui/toolbar'; import { dxTreeListToolbarItem, TreeListPredefinedToolbarItem } from 'devextreme/ui/tree_list'; @Component({ template: '' }) -export abstract class DxoDataGridToolbar extends NestedOption { +export abstract class DxoCardViewToolbar extends NestedOption { get disabled(): boolean { return this._getOption('disabled'); } @@ -23,10 +26,10 @@ export abstract class DxoDataGridToolbar extends NestedOption { this._setOption('disabled', value); } - get items(): Array { + get items(): Array { return this._getOption('items'); } - set items(value: Array) { + set items(value: Array) { this._setOption('items', value); } diff --git a/packages/devextreme-angular/src/ui/nested/base/data-grid-column-dxi.ts b/packages/devextreme-angular/src/ui/nested/base/column-properties-dxi.ts similarity index 93% rename from packages/devextreme-angular/src/ui/nested/base/data-grid-column-dxi.ts rename to packages/devextreme-angular/src/ui/nested/base/column-properties-dxi.ts index ef55b211cdb5..ab431b139cc5 100644 --- a/packages/devextreme-angular/src/ui/nested/base/data-grid-column-dxi.ts +++ b/packages/devextreme-angular/src/ui/nested/base/column-properties-dxi.ts @@ -17,7 +17,42 @@ import { dxTreeListColumn, dxTreeListColumnButton, TreeListCommandColumnType, Tr @Component({ template: '' }) -export abstract class DxiDataGridColumn extends CollectionNestedOption { +export abstract class DxiColumnProperties extends CollectionNestedOption { + get fieldCaptionTemplate(): any { + return this._getOption('fieldCaptionTemplate'); + } + set fieldCaptionTemplate(value: any) { + this._setOption('fieldCaptionTemplate', value); + } + + get fieldTemplate(): any { + return this._getOption('fieldTemplate'); + } + set fieldTemplate(value: any) { + this._setOption('fieldTemplate', value); + } + + get fieldValueTemplate(): any { + return this._getOption('fieldValueTemplate'); + } + set fieldValueTemplate(value: any) { + this._setOption('fieldValueTemplate', value); + } + + get headerItemCssClass(): string { + return this._getOption('headerItemCssClass'); + } + set headerItemCssClass(value: string) { + this._setOption('headerItemCssClass', value); + } + + get headerItemTemplate(): any { + return this._getOption('headerItemTemplate'); + } + set headerItemTemplate(value: any) { + this._setOption('headerItemTemplate', value); + } + get alignment(): HorizontalAlignment | string | undefined { return this._getOption('alignment'); } diff --git a/packages/devextreme-angular/src/ui/nested/base/header-panel.ts b/packages/devextreme-angular/src/ui/nested/base/header-panel.ts new file mode 100644 index 000000000000..810d518eaa34 --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/base/header-panel.ts @@ -0,0 +1,40 @@ +/* tslint:disable:max-line-length */ + +import { NestedOption } from 'devextreme-angular/core'; +import { + Component, +} from '@angular/core'; + + +@Component({ + template: '' +}) +export abstract class DxoHeaderPanel extends NestedOption { + get dragging(): any { + return this._getOption('dragging'); + } + set dragging(value: any) { + this._setOption('dragging', value); + } + + get itemCssClass(): string { + return this._getOption('itemCssClass'); + } + set itemCssClass(value: string) { + this._setOption('itemCssClass', value); + } + + get itemTemplate(): any { + return this._getOption('itemTemplate'); + } + set itemTemplate(value: any) { + this._setOption('itemTemplate', value); + } + + get visible(): boolean { + return this._getOption('visible'); + } + set visible(value: boolean) { + this._setOption('visible', value); + } +} diff --git a/packages/devextreme-angular/src/ui/nested/base/index.ts b/packages/devextreme-angular/src/ui/nested/base/index.ts index 0f0a3b1e3d89..dc4e7fe33cb3 100644 --- a/packages/devextreme-angular/src/ui/nested/base/index.ts +++ b/packages/devextreme-angular/src/ui/nested/base/index.ts @@ -5,6 +5,9 @@ export * from './box-options'; export * from './button-group-item-dxi'; export * from './button-options'; export * from './calendar-options'; +export * from './card-cover'; +export * from './card-header'; +export * from './card-view-toolbar'; export * from './chart-annotation-config-dxi'; export * from './chart-common-annotation-config'; export * from './chart-common-series-settings'; @@ -12,10 +15,9 @@ export * from './chart-series-dxi'; export * from './charts-color'; export * from './column-chooser-search-config'; export * from './column-chooser-selection-config'; +export * from './column-properties-dxi'; export * from './converter'; export * from './data-change-dxi'; -export * from './data-grid-column-dxi'; -export * from './data-grid-toolbar'; export * from './diagram-custom-command-dxi'; export * from './file-manager-context-menu'; export * from './file-manager-toolbar-item-dxi'; @@ -33,6 +35,7 @@ export * from './gantt-header-filter'; export * from './gantt-sorting'; export * from './gantt-strip-line-dxi'; export * from './gauge-indicator'; +export * from './header-panel'; export * from './html-editor-image-upload-tab-item-dxi'; export * from './html-editor-image-upload'; export * from './html-editor-media-resizing'; @@ -41,8 +44,10 @@ export * from './html-editor-table-context-menu'; export * from './html-editor-table-resizing'; export * from './html-editor-variables'; export * from './pager'; +export * from './paging'; export * from './popup-options'; export * from './position-config'; +export * from './remote-operations'; export * from './scheduler-scrolling'; export * from './sortable-options'; export * from './splitter-options'; diff --git a/packages/devextreme-angular/src/ui/nested/base/paging.ts b/packages/devextreme-angular/src/ui/nested/base/paging.ts new file mode 100644 index 000000000000..73498c05499f --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/base/paging.ts @@ -0,0 +1,33 @@ +/* tslint:disable:max-line-length */ + +import { NestedOption } from 'devextreme-angular/core'; +import { + Component, +} from '@angular/core'; + + +@Component({ + template: '' +}) +export abstract class DxoPaging extends NestedOption { + get enabled(): boolean { + return this._getOption('enabled'); + } + set enabled(value: boolean) { + this._setOption('enabled', value); + } + + get pageIndex(): number { + return this._getOption('pageIndex'); + } + set pageIndex(value: number) { + this._setOption('pageIndex', value); + } + + get pageSize(): number { + return this._getOption('pageSize'); + } + set pageSize(value: number) { + this._setOption('pageSize', value); + } +} diff --git a/packages/devextreme-angular/src/ui/nested/base/remote-operations.ts b/packages/devextreme-angular/src/ui/nested/base/remote-operations.ts new file mode 100644 index 000000000000..e9adbce2da1f --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/base/remote-operations.ts @@ -0,0 +1,54 @@ +/* tslint:disable:max-line-length */ + +import { NestedOption } from 'devextreme-angular/core'; +import { + Component, +} from '@angular/core'; + + +@Component({ + template: '' +}) +export abstract class DxoRemoteOperations extends NestedOption { + get filtering(): boolean { + return this._getOption('filtering'); + } + set filtering(value: boolean) { + this._setOption('filtering', value); + } + + get paging(): boolean { + return this._getOption('paging'); + } + set paging(value: boolean) { + this._setOption('paging', value); + } + + get sorting(): boolean { + return this._getOption('sorting'); + } + set sorting(value: boolean) { + this._setOption('sorting', value); + } + + get summary(): boolean { + return this._getOption('summary'); + } + set summary(value: boolean) { + this._setOption('summary', value); + } + + get grouping(): boolean { + return this._getOption('grouping'); + } + set grouping(value: boolean) { + this._setOption('grouping', value); + } + + get groupPaging(): boolean { + return this._getOption('groupPaging'); + } + set groupPaging(value: boolean) { + this._setOption('groupPaging', value); + } +} diff --git a/packages/devextreme-angular/src/ui/nested/card-cover.ts b/packages/devextreme-angular/src/ui/nested/card-cover.ts new file mode 100644 index 000000000000..c46e49477abf --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/card-cover.ts @@ -0,0 +1,68 @@ +/* tslint:disable:max-line-length */ + +/* tslint:disable:use-input-property-decorator */ + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { DxoCardCover } from './base/card-cover'; + + +@Component({ + selector: 'dxo-card-cover', + template: '', + styles: [''], + providers: [NestedOptionHost], + inputs: [ + 'altExpr', + 'imageExpr' + ] +}) +export class DxoCardCoverComponent extends DxoCardCover implements OnDestroy, OnInit { + + protected get _optionPath() { + return 'cardCover'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardCoverComponent + ], + exports: [ + DxoCardCoverComponent + ], +}) +export class DxoCardCoverModule { } diff --git a/packages/devextreme-angular/src/ui/nested/card-header.ts b/packages/devextreme-angular/src/ui/nested/card-header.ts new file mode 100644 index 000000000000..825e09a116fe --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/card-header.ts @@ -0,0 +1,68 @@ +/* tslint:disable:max-line-length */ + +/* tslint:disable:use-input-property-decorator */ + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { DxoCardHeader } from './base/card-header'; + + +@Component({ + selector: 'dxo-card-header', + template: '', + styles: [''], + providers: [NestedOptionHost], + inputs: [ + 'captionExpr', + 'visible' + ] +}) +export class DxoCardHeaderComponent extends DxoCardHeader implements OnDestroy, OnInit { + + protected get _optionPath() { + return 'cardHeader'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoCardHeaderComponent + ], + exports: [ + DxoCardHeaderComponent + ], +}) +export class DxoCardHeaderModule { } diff --git a/packages/devextreme-angular/src/ui/nested/column-dxi.ts b/packages/devextreme-angular/src/ui/nested/column-dxi.ts index ad2ec1c22001..e3712facf346 100644 --- a/packages/devextreme-angular/src/ui/nested/column-dxi.ts +++ b/packages/devextreme-angular/src/ui/nested/column-dxi.ts @@ -23,7 +23,7 @@ import { SelectedFilterOperation } from 'devextreme/common/grids'; import { NestedOptionHost, } from 'devextreme-angular/core'; -import { DxiDataGridColumn } from './base/data-grid-column-dxi'; +import { DxiColumnProperties } from './base/column-properties-dxi'; import { DxiButtonComponent } from './button-dxi'; import { DxiValidationRuleComponent } from './validation-rule-dxi'; @@ -34,6 +34,11 @@ import { DxiValidationRuleComponent } from './validation-rule-dxi'; styles: [''], providers: [NestedOptionHost], inputs: [ + 'fieldCaptionTemplate', + 'fieldTemplate', + 'fieldValueTemplate', + 'headerItemCssClass', + 'headerItemTemplate', 'alignment', 'allowEditing', 'allowExporting', @@ -99,7 +104,7 @@ import { DxiValidationRuleComponent } from './validation-rule-dxi'; 'width' ] }) -export class DxiColumnComponent extends DxiDataGridColumn { +export class DxiColumnComponent extends DxiColumnProperties { /** diff --git a/packages/devextreme-angular/src/ui/nested/header-panel.ts b/packages/devextreme-angular/src/ui/nested/header-panel.ts new file mode 100644 index 000000000000..83578ab82417 --- /dev/null +++ b/packages/devextreme-angular/src/ui/nested/header-panel.ts @@ -0,0 +1,70 @@ +/* tslint:disable:max-line-length */ + +/* tslint:disable:use-input-property-decorator */ + +import { + Component, + OnInit, + OnDestroy, + NgModule, + Host, + SkipSelf +} from '@angular/core'; + + + + + +import { + NestedOptionHost, +} from 'devextreme-angular/core'; +import { DxoHeaderPanel } from './base/header-panel'; + + +@Component({ + selector: 'dxo-header-panel', + template: '', + styles: [''], + providers: [NestedOptionHost], + inputs: [ + 'dragging', + 'itemCssClass', + 'itemTemplate', + 'visible' + ] +}) +export class DxoHeaderPanelComponent extends DxoHeaderPanel implements OnDestroy, OnInit { + + protected get _optionPath() { + return 'headerPanel'; + } + + + constructor(@SkipSelf() @Host() parentOptionHost: NestedOptionHost, + @Host() optionHost: NestedOptionHost) { + super(); + parentOptionHost.setNestedOption(this); + optionHost.setHost(this, this._fullOptionPath.bind(this)); + } + + + ngOnInit() { + this._addRecreatedComponent(); + } + + ngOnDestroy() { + this._addRemovedOption(this._getOptionPath()); + } + + +} + +@NgModule({ + declarations: [ + DxoHeaderPanelComponent + ], + exports: [ + DxoHeaderPanelComponent + ], +}) +export class DxoHeaderPanelModule { } diff --git a/packages/devextreme-angular/src/ui/nested/index.ts b/packages/devextreme-angular/src/ui/nested/index.ts index 7c291f7dc04e..b3a455a34ed1 100644 --- a/packages/devextreme-angular/src/ui/nested/index.ts +++ b/packages/devextreme-angular/src/ui/nested/index.ts @@ -27,6 +27,8 @@ export * from './button-dxi'; export * from './button-options'; export * from './calendar-options'; export * from './candlestick'; +export * from './card-cover'; +export * from './card-header'; export * from './center-dxi'; export * from './change-dxi'; export * from './chart'; @@ -98,6 +100,7 @@ export * from './group'; export * from './grouping'; export * from './hatching'; export * from './header-filter'; +export * from './header-panel'; export * from './height'; export * from './hide-event'; export * from './hide'; diff --git a/packages/devextreme-angular/src/ui/nested/item-dxi.ts b/packages/devextreme-angular/src/ui/nested/item-dxi.ts index 9fb38347ef50..7c219bdae536 100644 --- a/packages/devextreme-angular/src/ui/nested/item-dxi.ts +++ b/packages/devextreme-angular/src/ui/nested/item-dxi.ts @@ -56,6 +56,14 @@ import { DxiLocationComponent } from './location-dxi'; 'shrink', 'elementAttr', 'hint', + 'cssClass', + 'locateInMenu', + 'location', + 'menuItemTemplate', + 'name', + 'options', + 'showText', + 'widget', 'author', 'id', 'timestamp', @@ -65,7 +73,6 @@ import { DxiLocationComponent } from './location-dxi'; 'selectable', 'selected', 'colSpan', - 'cssClass', 'dataField', 'editorOptions', 'editorType', @@ -73,7 +80,6 @@ import { DxiLocationComponent } from './location-dxi'; 'isRequired', 'itemType', 'label', - 'name', 'validationRules', 'visibleIndex', 'alignItemLabels', @@ -88,12 +94,6 @@ import { DxiLocationComponent } from './location-dxi'; 'buttonOptions', 'horizontalAlignment', 'verticalAlignment', - 'locateInMenu', - 'location', - 'menuItemTemplate', - 'options', - 'showText', - 'widget', 'height', 'width', 'imageAlt', diff --git a/packages/devextreme-angular/src/ui/nested/paging.ts b/packages/devextreme-angular/src/ui/nested/paging.ts index 60c34267d7de..2bd4d2055c47 100644 --- a/packages/devextreme-angular/src/ui/nested/paging.ts +++ b/packages/devextreme-angular/src/ui/nested/paging.ts @@ -1,5 +1,6 @@ /* tslint:disable:max-line-length */ +/* tslint:disable:use-input-property-decorator */ import { Component, @@ -8,7 +9,6 @@ import { NgModule, Host, SkipSelf, - Input, Output, EventEmitter } from '@angular/core'; @@ -20,40 +20,21 @@ import { import { NestedOptionHost, } from 'devextreme-angular/core'; -import { NestedOption } from 'devextreme-angular/core'; +import { DxoPaging } from './base/paging'; @Component({ selector: 'dxo-paging', template: '', styles: [''], - providers: [NestedOptionHost] + providers: [NestedOptionHost], + inputs: [ + 'enabled', + 'pageIndex', + 'pageSize' + ] }) -export class DxoPagingComponent extends NestedOption implements OnDestroy, OnInit { - @Input() - get enabled(): boolean { - return this._getOption('enabled'); - } - set enabled(value: boolean) { - this._setOption('enabled', value); - } - - @Input() - get pageIndex(): number { - return this._getOption('pageIndex'); - } - set pageIndex(value: number) { - this._setOption('pageIndex', value); - } - - @Input() - get pageSize(): number { - return this._getOption('pageSize'); - } - set pageSize(value: number) { - this._setOption('pageSize', value); - } - +export class DxoPagingComponent extends DxoPaging implements OnDestroy, OnInit { /** diff --git a/packages/devextreme-angular/src/ui/nested/remote-operations.ts b/packages/devextreme-angular/src/ui/nested/remote-operations.ts index b123cd472c8e..c3cd0511d4c9 100644 --- a/packages/devextreme-angular/src/ui/nested/remote-operations.ts +++ b/packages/devextreme-angular/src/ui/nested/remote-operations.ts @@ -1,5 +1,6 @@ /* tslint:disable:max-line-length */ +/* tslint:disable:use-input-property-decorator */ import { Component, @@ -7,8 +8,7 @@ import { OnDestroy, NgModule, Host, - SkipSelf, - Input + SkipSelf } from '@angular/core'; @@ -18,64 +18,24 @@ import { import { NestedOptionHost, } from 'devextreme-angular/core'; -import { NestedOption } from 'devextreme-angular/core'; +import { DxoRemoteOperations } from './base/remote-operations'; @Component({ selector: 'dxo-remote-operations', template: '', styles: [''], - providers: [NestedOptionHost] + providers: [NestedOptionHost], + inputs: [ + 'filtering', + 'paging', + 'sorting', + 'summary', + 'grouping', + 'groupPaging' + ] }) -export class DxoRemoteOperationsComponent extends NestedOption implements OnDestroy, OnInit { - @Input() - get filtering(): boolean { - return this._getOption('filtering'); - } - set filtering(value: boolean) { - this._setOption('filtering', value); - } - - @Input() - get grouping(): boolean { - return this._getOption('grouping'); - } - set grouping(value: boolean) { - this._setOption('grouping', value); - } - - @Input() - get groupPaging(): boolean { - return this._getOption('groupPaging'); - } - set groupPaging(value: boolean) { - this._setOption('groupPaging', value); - } - - @Input() - get paging(): boolean { - return this._getOption('paging'); - } - set paging(value: boolean) { - this._setOption('paging', value); - } - - @Input() - get sorting(): boolean { - return this._getOption('sorting'); - } - set sorting(value: boolean) { - this._setOption('sorting', value); - } - - @Input() - get summary(): boolean { - return this._getOption('summary'); - } - set summary(value: boolean) { - this._setOption('summary', value); - } - +export class DxoRemoteOperationsComponent extends DxoRemoteOperations implements OnDestroy, OnInit { protected get _optionPath() { return 'remoteOperations'; diff --git a/packages/devextreme-angular/src/ui/nested/toolbar.ts b/packages/devextreme-angular/src/ui/nested/toolbar.ts index 687c36cef2e3..9a7babb07049 100644 --- a/packages/devextreme-angular/src/ui/nested/toolbar.ts +++ b/packages/devextreme-angular/src/ui/nested/toolbar.ts @@ -21,7 +21,7 @@ import { import { NestedOptionHost, } from 'devextreme-angular/core'; -import { DxoDataGridToolbar } from './base/data-grid-toolbar'; +import { DxoCardViewToolbar } from './base/card-view-toolbar'; import { DxiItemComponent } from './item-dxi'; import { DxiFileSelectionItemComponent } from './file-selection-item-dxi'; @@ -40,7 +40,7 @@ import { DxiFileSelectionItemComponent } from './file-selection-item-dxi'; 'multiline' ] }) -export class DxoToolbarComponent extends DxoDataGridToolbar implements OnDestroy, OnInit { +export class DxoToolbarComponent extends DxoCardViewToolbar implements OnDestroy, OnInit { protected get _optionPath() { return 'toolbar'; diff --git a/packages/devextreme-angular/tests/src/server/component-names.ts b/packages/devextreme-angular/tests/src/server/component-names.ts index d5914606f2ba..baed4ebe863c 100644 --- a/packages/devextreme-angular/tests/src/server/component-names.ts +++ b/packages/devextreme-angular/tests/src/server/component-names.ts @@ -7,6 +7,7 @@ export const componentNames = [ 'bullet', 'button', 'calendar', + 'card-view', 'chart', 'chat', 'check-box', diff --git a/packages/devextreme-react/src/card-view.ts b/packages/devextreme-react/src/card-view.ts new file mode 100644 index 000000000000..1f8bee8d4c09 --- /dev/null +++ b/packages/devextreme-react/src/card-view.ts @@ -0,0 +1,358 @@ +"use client" +export { ExplicitTypes } from "devextreme/ui/card_view"; +import * as React from "react"; +import { memo, forwardRef, useImperativeHandle, useRef, useMemo, ForwardedRef, Ref, ReactElement } from "react"; +import dxCardView, { + Properties +} from "devextreme/ui/card_view"; + +import { Component as BaseComponent, IHtmlOptions, ComponentRef, NestedComponentMeta } from "./core/component"; +import NestedOption from "./core/nested-option"; + +import type { DataRow, Column as CardViewColumn, PredefinedToolbarItem, dxCardViewToolbarItem } from "devextreme/ui/card_view"; +import type { template, ToolbarItemLocation, ToolbarItemComponent, Mode, DisplayMode } from "devextreme/common"; +import type { LocateInMenuMode, ShowTextMode } from "devextreme/ui/toolbar"; +import type { CollectionWidgetItem } from "devextreme/ui/collection/ui.collection_widget.base"; +import type { PagerPageSize } from "devextreme/common/grids"; + +type ICardViewOptions = React.PropsWithChildren & IHtmlOptions & { + dataSource?: Properties["dataSource"]; + cardRender?: (...params: any) => React.ReactNode; + cardComponent?: React.ComponentType; +}> + +interface CardViewRef { + instance: () => dxCardView; +} + +const CardView = memo( + forwardRef( + (props: React.PropsWithChildren>, ref: ForwardedRef>) => { + const baseRef = useRef(null); + + useImperativeHandle(ref, () => ( + { + instance() { + return baseRef.current?.getInstance(); + } + } + ), [baseRef.current]); + + const independentEvents = useMemo(() => (["onContentReady","onDataErrorOccurred","onDisposing","onInitialized"]), []); + + const expectedChildren = useMemo(() => ({ + cardCover: { optionName: "cardCover", isCollectionItem: false }, + cardHeader: { optionName: "cardHeader", isCollectionItem: false }, + column: { optionName: "columns", isCollectionItem: true }, + headerPanel: { optionName: "headerPanel", isCollectionItem: false }, + pager: { optionName: "pager", isCollectionItem: false }, + paging: { optionName: "paging", isCollectionItem: false }, + remoteOperations: { optionName: "remoteOperations", isCollectionItem: false }, + toolbar: { optionName: "toolbar", isCollectionItem: false } + }), []); + + const templateProps = useMemo(() => ([ + { + tmplOption: "cardTemplate", + render: "cardRender", + component: "cardComponent" + }, + ]), []); + + return ( + React.createElement(BaseComponent>>, { + WidgetClass: dxCardView, + ref: baseRef, + independentEvents, + expectedChildren, + templateProps, + ...props, + }) + ); + }, + ), +) as (props: React.PropsWithChildren> & { ref?: Ref> }) => ReactElement | null; + + +// owners: +// CardView +type ICardCoverProps = React.PropsWithChildren<{ + altExpr?: ((data: any) => string) | string; + imageExpr?: ((data: any) => string) | string; +}> +const _componentCardCover = (props: ICardCoverProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "cardCover", + }, + }); +}; + +const CardCover = Object.assign(_componentCardCover, { + componentType: "option", +}); + +// owners: +// CardView +type ICardHeaderProps = React.PropsWithChildren<{ + captionExpr?: ((data: any) => string) | string; + visible?: boolean; +}> +const _componentCardHeader = (props: ICardHeaderProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "cardHeader", + }, + }); +}; + +const CardHeader = Object.assign(_componentCardHeader, { + componentType: "option", +}); + +// owners: +// CardView +type IColumnProps = React.PropsWithChildren<{ + fieldCaptionTemplate?: ((dataRow: DataRow) => string | any) | template; + fieldTemplate?: ((dataRow: DataRow) => string | any) | template; + fieldValueTemplate?: ((dataRow: DataRow) => string | any) | template; + headerItemCssClass?: string; + headerItemTemplate?: ((column: CardViewColumn) => string | any) | template; + fieldCaptionRender?: (...params: any) => React.ReactNode; + fieldCaptionComponent?: React.ComponentType; + fieldRender?: (...params: any) => React.ReactNode; + fieldComponent?: React.ComponentType; + fieldValueRender?: (...params: any) => React.ReactNode; + fieldValueComponent?: React.ComponentType; + headerItemRender?: (...params: any) => React.ReactNode; + headerItemComponent?: React.ComponentType; +}> +const _componentColumn = (props: IColumnProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "columns", + IsCollectionItem: true, + TemplateProps: [{ + tmplOption: "fieldCaptionTemplate", + render: "fieldCaptionRender", + component: "fieldCaptionComponent" + }, { + tmplOption: "fieldTemplate", + render: "fieldRender", + component: "fieldComponent" + }, { + tmplOption: "fieldValueTemplate", + render: "fieldValueRender", + component: "fieldValueComponent" + }, { + tmplOption: "headerItemTemplate", + render: "headerItemRender", + component: "headerItemComponent" + }], + }, + }); +}; + +const Column = Object.assign(_componentColumn, { + componentType: "option", +}); + +// owners: +// CardView +type IHeaderPanelProps = React.PropsWithChildren<{ + dragging?: Record; + itemCssClass?: string; + itemTemplate?: ((e: { column: CardViewColumn }) => string | any) | template; + visible?: boolean; + itemRender?: (...params: any) => React.ReactNode; + itemComponent?: React.ComponentType; +}> +const _componentHeaderPanel = (props: IHeaderPanelProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "headerPanel", + TemplateProps: [{ + tmplOption: "itemTemplate", + render: "itemRender", + component: "itemComponent" + }], + }, + }); +}; + +const HeaderPanel = Object.assign(_componentHeaderPanel, { + componentType: "option", +}); + +// owners: +// Toolbar +type IItemProps = React.PropsWithChildren<{ + cssClass?: string | undefined; + disabled?: boolean; + html?: string; + locateInMenu?: LocateInMenuMode; + location?: ToolbarItemLocation; + menuItemTemplate?: (() => string | any) | template; + name?: PredefinedToolbarItem | string; + options?: any; + showText?: ShowTextMode; + template?: ((itemData: CollectionWidgetItem, itemIndex: number, itemElement: any) => string | any) | template; + text?: string; + visible?: boolean; + widget?: ToolbarItemComponent; + menuItemRender?: (...params: any) => React.ReactNode; + menuItemComponent?: React.ComponentType; + render?: (...params: any) => React.ReactNode; + component?: React.ComponentType; +}> +const _componentItem = (props: IItemProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "items", + IsCollectionItem: true, + TemplateProps: [{ + tmplOption: "menuItemTemplate", + render: "menuItemRender", + component: "menuItemComponent" + }, { + tmplOption: "template", + render: "render", + component: "component" + }], + }, + }); +}; + +const Item = Object.assign(_componentItem, { + componentType: "option", +}); + +// owners: +// CardView +type IPagerProps = React.PropsWithChildren<{ + allowedPageSizes?: Array | Mode; + displayMode?: DisplayMode; + infoText?: string; + label?: string; + showInfo?: boolean; + showNavigationButtons?: boolean; + showPageSizeSelector?: boolean; + visible?: boolean | Mode; +}> +const _componentPager = (props: IPagerProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "pager", + }, + }); +}; + +const Pager = Object.assign(_componentPager, { + componentType: "option", +}); + +// owners: +// CardView +type IPagingProps = React.PropsWithChildren<{ + enabled?: boolean; + pageIndex?: number; + pageSize?: number; + defaultPageIndex?: number; + onPageIndexChange?: (value: number) => void; + defaultPageSize?: number; + onPageSizeChange?: (value: number) => void; +}> +const _componentPaging = (props: IPagingProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "paging", + DefaultsProps: { + defaultPageIndex: "pageIndex", + defaultPageSize: "pageSize" + }, + }, + }); +}; + +const Paging = Object.assign(_componentPaging, { + componentType: "option", +}); + +// owners: +// CardView +type IRemoteOperationsProps = React.PropsWithChildren<{ + filtering?: boolean; + paging?: boolean; + sorting?: boolean; + summary?: boolean; +}> +const _componentRemoteOperations = (props: IRemoteOperationsProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "remoteOperations", + }, + }); +}; + +const RemoteOperations = Object.assign(_componentRemoteOperations, { + componentType: "option", +}); + +// owners: +// CardView +type IToolbarProps = React.PropsWithChildren<{ + disabled?: boolean; + items?: Array; + visible?: boolean | undefined; +}> +const _componentToolbar = (props: IToolbarProps) => { + return React.createElement(NestedOption, { + ...props, + elementDescriptor: { + OptionName: "toolbar", + ExpectedChildren: { + item: { optionName: "items", isCollectionItem: true } + }, + }, + }); +}; + +const Toolbar = Object.assign(_componentToolbar, { + componentType: "option", +}); + +export default CardView; +export { + CardView, + ICardViewOptions, + CardViewRef, + CardCover, + ICardCoverProps, + CardHeader, + ICardHeaderProps, + Column, + IColumnProps, + HeaderPanel, + IHeaderPanelProps, + Item, + IItemProps, + Pager, + IPagerProps, + Paging, + IPagingProps, + RemoteOperations, + IRemoteOperationsProps, + Toolbar, + IToolbarProps +}; +import type * as CardViewTypes from 'devextreme/ui/card_view_types'; +export { CardViewTypes }; + diff --git a/packages/devextreme-react/src/index.ts b/packages/devextreme-react/src/index.ts index 1c6d1512eb5c..85bc87fdbdcb 100644 --- a/packages/devextreme-react/src/index.ts +++ b/packages/devextreme-react/src/index.ts @@ -8,6 +8,7 @@ export { Bullet } from "./bullet"; export { Button } from "./button"; export { ButtonGroup } from "./button-group"; export { Calendar } from "./calendar"; +export { CardView } from "./card-view"; export { Chart } from "./chart"; export { Chat } from "./chat"; export { CheckBox } from "./check-box"; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss new file mode 100644 index 000000000000..479e1b081f12 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/_index.scss @@ -0,0 +1,14 @@ +@use './content_view'; +@use './header_panel'; +@use './variables' as *; + +// adduse + +.dx-cardview { + display: flex; + flex-direction: column; + gap: $cardview-gap; + padding: $cardview-padding; + background-color: $cardview-background-color; + border-radius: $cardview-border-radius; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss new file mode 100644 index 000000000000..893760ad1f6e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/_variables.scss @@ -0,0 +1,4 @@ +$cardview-gap: null !default; +$cardview-padding: null !default; +$cardview-background-color: null !default; +$cardview-border-radius: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss new file mode 100644 index 000000000000..a7575792f397 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/_index.scss @@ -0,0 +1,7 @@ +@use '../variables' as *; +@use './content'; + +.dx-cardview-contentview { + overflow: hidden; + flex-grow: 1; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss new file mode 100644 index 000000000000..b220687c03b8 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_index.scss @@ -0,0 +1,10 @@ +@use './variables' as *; +@use './card'; + +.dx-cardview-content { + display: grid; + justify-items: center; + column-gap: $cardview-content-column-gap; + row-gap: $cardview-content-row-gap; + grid-template-columns: repeat(var(--dx-cardview-cardsperrow), 1fr); +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss new file mode 100644 index 000000000000..19754837130c --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/_variables.scss @@ -0,0 +1,2 @@ +$cardview-content-column-gap: null !default; +$cardview-content-row-gap: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss new file mode 100644 index 000000000000..c425dc3f79f5 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_index.scss @@ -0,0 +1,13 @@ +@use './header'; +@use './content'; +@use './cover'; +@use './variables' as *; + +.dx-cardview-card { + width: 100%; + min-width: var(--dx-cardview-card-min-width, $cardview-card-min-width); + max-width: var(--dx-cardview-card-max-width); + border: $cardview-card-border-size solid $cardview-card-border-color; + border-radius: $cardview-card-border-radius; + background-color: $cardview-card-background-color; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss new file mode 100644 index 000000000000..6ed020add8f6 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/_variables.scss @@ -0,0 +1,6 @@ +$cardview-card-border-color: null !default; + +$cardview-card-border-size: null !default; +$cardview-card-min-width: null !default; +$cardview-card-border-radius: null !default; +$cardview-card-background-color: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss new file mode 100644 index 000000000000..f93a891a3e8f --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_index.scss @@ -0,0 +1,26 @@ +@use './variables' as *; +@use '../variables' as *; + +.dx-cardview-card-content { + padding: ($cardview-card-content-padding-vertical - $cardview-card-content-field-gap) $cardview-card-content-padding-horizontal; + display: table; + border-spacing: $cardview-card-content-field-gap; + width: 100%; +} + +.dx-cardview-field { + display: table-row; +} + +.dx-cardview-field-name, +.dx-cardview-field-value { + display: table-cell; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: $cardview-card-content-cell-padding-vertical $cardview-card-content-cell-padding-horizontal; +} + +.dx-cardview-field-name { + font-weight: bold; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss new file mode 100644 index 000000000000..b11ad4f18872 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/content/_variables.scss @@ -0,0 +1,5 @@ +$cardview-card-content-padding-vertical: null !default; +$cardview-card-content-padding-horizontal: null !default; +$cardview-card-content-field-gap: null !default; +$cardview-card-content-cell-padding-vertical: null !default; +$cardview-card-content-cell-padding-horizontal: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss new file mode 100644 index 000000000000..fada7426d0d1 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/cover/_index.scss @@ -0,0 +1,16 @@ +@use '../variables' as *; + +.dx-card-cover { + overflow: hidden; + display: flex; + justify-content: center; + border-top: $cardview-card-border-size solid $cardview-card-border-color; + border-bottom: $cardview-card-border-size solid $cardview-card-border-color; + max-height: var(--dx-cardview-card-cover-max-height); + aspect-ratio: var(--dx-cardview-card-cover-ratio); +} + +.dx-card-cover-image { + object-fit: contain; + width: 100%; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss new file mode 100644 index 000000000000..3cc44c0919c0 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_index.scss @@ -0,0 +1,13 @@ +@use './variables' as *; + +.dx-cardview-card-header { + .dx-toolbar { + padding: 0 12px; + border-radius: $cardview-card-header-border-radius; + + + .dx-toolbar-label { + font-size: $cardview-card-header-text-size; + } + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss new file mode 100644 index 000000000000..814471d72bd7 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/content_view/content/card/header/_variables.scss @@ -0,0 +1,2 @@ +$cardview-card-header-text-size: null !default; +$cardview-card-header-border-radius: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss new file mode 100644 index 000000000000..87cbb14463a6 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_index.scss @@ -0,0 +1,8 @@ + +@use './item'; +@use './variables' as *; + +.dx-cardview-headerpanel-content { + display: flex; + gap: $cardview-headerpanel-content-gap; +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss new file mode 100644 index 000000000000..6ab96b987436 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/_variables.scss @@ -0,0 +1 @@ +$cardview-headerpanel-content-gap: null !default; diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss new file mode 100644 index 000000000000..9a145974f853 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_index.scss @@ -0,0 +1,14 @@ +@use './variables' as *; + +.dx-cardview-header-item { + background-color: $cardview-header-item-background-color; + border: solid $cardview-header-item-border-width $cardview-header-item-border-color; + border-radius: $cardview-header-item-border-radius; + padding: ($cardview-header-item-padding-vertical - $cardview-header-item-border-width) $cardview-header-item-padding-horizontal; + min-width: fit-content; + + &:hover { + background-color: $cardview-header-item-hovered-background-color; + border: solid $cardview-header-item-border-width $cardview-header-item-hovered-border-color; + } +} diff --git a/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss new file mode 100644 index 000000000000..8dfadf79f82e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/base/cardView/header_panel/item/_variables.scss @@ -0,0 +1,10 @@ +$cardview-header-item-background-color: null !default; +$cardview-header-item-border-color: null !default; + +$cardview-header-item-hovered-background-color: null !default; +$cardview-header-item-hovered-border-color: null !default; + +$cardview-header-item-border-width: null !default; +$cardview-header-item-border-radius: null !default; +$cardview-header-item-padding-horizontal: 12px !default; +$cardview-header-item-padding-vertical: 6px !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/_index.scss index e9d6526025ba..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/_index.scss @@ -61,3 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss new file mode 100644 index 000000000000..54780b425eb8 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_colors.scss @@ -0,0 +1,20 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-background-color: $base-typography-bg !default; + +$cardview-header-item-background-color: #F0F0F0 !default; +$cardview-header-item-border-color: #E0E0E0 !default; +$cardview-header-item-hovered-background-color: #EBEBEB !default; +$cardview-header-item-hovered-border-color: #BDBDBD !default; + +$cardview-card-border-color: $base-border-color !default; +$cardview-card-background-color: $base-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss new file mode 100644 index 000000000000..2f6c2ca6b355 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/fluent/cardView/_sizes.scss @@ -0,0 +1,63 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: 8px !default; + diff --git a/packages/devextreme-scss/scss/widgets/generic/_index.scss b/packages/devextreme-scss/scss/widgets/generic/_index.scss index aac2ff330556..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/generic/_index.scss +++ b/packages/devextreme-scss/scss/widgets/generic/_index.scss @@ -61,4 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; - +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss new file mode 100644 index 000000000000..7b8ea9381c6c --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_colors.scss @@ -0,0 +1,20 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-background-color: $typography-bg !default; + +$cardview-header-item-background-color: #F0F0F0 !default; +$cardview-header-item-border-color: #E0E0E0 !default; +$cardview-header-item-hovered-background-color: #EBEBEB !default; +$cardview-header-item-hovered-border-color: #BDBDBD !default; + +$cardview-card-border-color: $base-border-color !default; +$cardview-card-background-color: $base-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss new file mode 100644 index 000000000000..b22cd0209b8e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/generic/cardView/_sizes.scss @@ -0,0 +1,62 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: 8px !default; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/material/_index.scss b/packages/devextreme-scss/scss/widgets/material/_index.scss index e9d6526025ba..ff95df79b8ec 100644 --- a/packages/devextreme-scss/scss/widgets/material/_index.scss +++ b/packages/devextreme-scss/scss/widgets/material/_index.scss @@ -61,3 +61,4 @@ @use "./sortable"; @use "./deferRendering"; @use "./map"; +@use "./cardView"; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss new file mode 100644 index 000000000000..7b8ea9381c6c --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_colors.scss @@ -0,0 +1,20 @@ +@use '../colors' as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-background-color: $typography-bg !default; + +$cardview-header-item-background-color: #F0F0F0 !default; +$cardview-header-item-border-color: #E0E0E0 !default; +$cardview-header-item-hovered-background-color: #EBEBEB !default; +$cardview-header-item-hovered-border-color: #BDBDBD !default; + +$cardview-card-border-color: $base-border-color !default; +$cardview-card-background-color: $base-bg !default; diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss new file mode 100644 index 000000000000..29f2262bbe3e --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_index.scss @@ -0,0 +1,5 @@ +@use 'colors'; +@use 'sizes'; +@use '../../base/cardView/index' as *; + +// adduse diff --git a/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss new file mode 100644 index 000000000000..0524c5d6ca62 --- /dev/null +++ b/packages/devextreme-scss/scss/widgets/material/cardView/_sizes.scss @@ -0,0 +1,62 @@ +@use "../sizes" as *; +@use '../../base/cardView/variables' as *; +@use '../../base/cardView/header_panel/variables' as *; +@use '../../base/cardView/header_panel/item/variables' as *; +@use '../../base/cardView/content_view/content/card/variables' as *; +@use '../../base/cardView/content_view/content/card/header/variables' as *; +@use '../../base/cardView/content_view/content/card/content/variables' as *; +@use '../../base/cardView/content_view/content/variables' as *; + +// adduse + +$cardview-fluent-paddings-24: null; +$cardview-fluent-paddings-12: null; +$cardview-fluent-paddings-6: null; +$cardview-fluent-text-size-16: null; +$cardview-fluent-border-radius-8: null; +$cardview-fluent-gaps-16: null; +$cardview-fluent-gaps-24: null; + +@if $size == "default" { + $cardview-fluent-paddings-24: 24px; + $cardview-fluent-paddings-12: 12px; + $cardview-fluent-paddings-6: 6px; + $cardview-fluent-text-size-16: 16px; + $cardview-fluent-border-radius-8: 8px; + $cardview-fluent-gaps-16: 16px; + $cardview-fluent-gaps-24: 24px; +} +@else if $size == "compact" { + $cardview-fluent-paddings-24: 16px; + $cardview-fluent-paddings-12: 8px; + $cardview-fluent-paddings-6: 4px; + $cardview-fluent-text-size-16: 14px; + $cardview-fluent-border-radius-8: 6px; + $cardview-fluent-gaps-16: 12px; + $cardview-fluent-gaps-24: 16px; +} + +$cardview-border-radius: 16px !default; +$cardview-gap: $cardview-fluent-gaps-24 !default; +$cardview-padding: $cardview-fluent-paddings-24 !default; + +$cardview-headerpanel-content-gap: 8px !default; + +$cardview-header-item-border-width: 1px !default; +$cardview-header-item-border-radius: 6px !default; + +$cardview-content-column-gap: $cardview-fluent-gaps-16 !default; +$cardview-content-row-gap: $cardview-fluent-gaps-16 !default; + +$cardview-card-border-size: 1px !default; +$cardview-card-min-width: 250px !default; +$cardview-card-border-radius: $cardview-fluent-border-radius-8 !default; + +$cardview-card-content-padding-vertical: $cardview-fluent-paddings-12 !default; +$cardview-card-content-padding-horizontal: $cardview-fluent-paddings-12 !default; +$cardview-card-content-field-gap: 5px !default; +$cardview-card-content-cell-padding-vertical: $cardview-fluent-paddings-6 !default; +$cardview-card-content-cell-padding-horizontal: $cardview-fluent-paddings-12 !default; + +$cardview-card-header-text-size: $cardview-fluent-text-size-16 !default; +$cardview-card-header-border-radius: 8px !default; diff --git a/packages/devextreme-themebuilder/tests/data/dependencies.ts b/packages/devextreme-themebuilder/tests/data/dependencies.ts index 27ffe61a73de..0297c842bab5 100644 --- a/packages/devextreme-themebuilder/tests/data/dependencies.ts +++ b/packages/devextreme-themebuilder/tests/data/dependencies.ts @@ -16,6 +16,7 @@ export const dependencies: FlatStylesDependencies = { buttongroup: ['validation', 'button'], dropdownbutton: ['validation', 'button', 'buttongroup', 'popup', 'loadindicator', 'loadpanel', 'scrollview', 'list'], calendar: ['validation', 'button'], + cardview: ['button', 'checkbox', 'list', 'loadindicator', 'loadpanel', 'numberbox', 'popup', 'scrollview', 'selectbox', 'sortable', 'textbox', 'toast', 'toolbar', 'validation'], chat: ['button', 'loadindicator', 'loadpanel', 'scrollview', 'textbox', 'validation'], checkbox: ['validation'], numberbox: ['validation', 'button', 'loadindicator'], diff --git a/packages/devextreme-vue/src/card-view.ts b/packages/devextreme-vue/src/card-view.ts new file mode 100644 index 000000000000..b1b8f8136df4 --- /dev/null +++ b/packages/devextreme-vue/src/card-view.ts @@ -0,0 +1,425 @@ +export { ExplicitTypes } from "devextreme/ui/card_view"; +import { PropType } from "vue"; +import { defineComponent } from "vue"; +import { prepareComponentConfig } from "./core/index"; +import CardView, { Properties } from "devextreme/ui/card_view"; +import DataSource from "devextreme/data/data_source"; +import DOMComponent from "devextreme/core/dom_component"; +import { + CardCover, + CardHeader, + ColumnProperties, + HeaderPanel, + Paging, + RemoteOperations, + dxCardViewToolbar, + PredefinedToolbarItem, + dxCardViewToolbarItem, +} from "devextreme/ui/card_view"; +import { + Mode, + ToolbarItemLocation, + ToolbarItemComponent, + DisplayMode, +} from "devextreme/common"; +import { + DataSourceOptions, +} from "devextreme/common/data"; +import { + Store, +} from "devextreme/data/store"; +import { + EventInfo, +} from "devextreme/common/core/events"; +import { + Component, +} from "devextreme/core/component"; +import { + Pager, + PagerPageSize, +} from "devextreme/common/grids"; +import { + PagerBase, +} from "devextreme/ui/pagination"; +import { + LocateInMenuMode, + ShowTextMode, +} from "devextreme/ui/toolbar"; +import { prepareConfigurationComponentConfig } from "./core/index"; + +type AccessibleOptions = Pick; + +interface DxCardView extends AccessibleOptions { + readonly instance?: CardView; +} + +const componentConfig = { + props: { + accessKey: String, + activeStateEnabled: Boolean, + cardCover: Object as PropType, + cardHeader: Object as PropType, + cardMaxWidth: Number, + cardMinWidth: Number, + cardsPerRow: [String, Number] as PropType, + cardTemplate: {}, + columns: Array as PropType>, + dataSource: [Array, Object, String] as PropType | DataSource | DataSourceOptions | Store | string | Record>, + disabled: Boolean, + elementAttr: Object as PropType>, + focusStateEnabled: Boolean, + headerPanel: Object as PropType>, + height: [Function, Number, String] as PropType<((() => number | string)) | number | string>, + hint: String, + hoverStateEnabled: Boolean, + keyExpr: [Array, String] as PropType | string>, + onContentReady: Function as PropType<((e: EventInfo) => void)>, + onDataErrorOccurred: Function as PropType<((e: { component: Object, element: any, error: any, model: any }) => void)>, + onDisposing: Function as PropType<((e: EventInfo) => void)>, + onInitialized: Function as PropType<((e: { component: Component, element: any }) => void)>, + onOptionChanged: Function as PropType<((e: { component: DOMComponent, element: any, fullName: string, model: any, name: string, previousValue: any, value: any }) => void)>, + pager: Object as PropType | PagerBase>, + paging: Object as PropType, + remoteOperations: [Boolean, String, Object] as PropType, + rtlEnabled: Boolean, + tabIndex: Number, + toolbar: Object as PropType>, + visible: Boolean, + width: [Function, Number, String] as PropType<((() => number | string)) | number | string> + }, + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:accessKey": null, + "update:activeStateEnabled": null, + "update:cardCover": null, + "update:cardHeader": null, + "update:cardMaxWidth": null, + "update:cardMinWidth": null, + "update:cardsPerRow": null, + "update:cardTemplate": null, + "update:columns": null, + "update:dataSource": null, + "update:disabled": null, + "update:elementAttr": null, + "update:focusStateEnabled": null, + "update:headerPanel": null, + "update:height": null, + "update:hint": null, + "update:hoverStateEnabled": null, + "update:keyExpr": null, + "update:onContentReady": null, + "update:onDataErrorOccurred": null, + "update:onDisposing": null, + "update:onInitialized": null, + "update:onOptionChanged": null, + "update:pager": null, + "update:paging": null, + "update:remoteOperations": null, + "update:rtlEnabled": null, + "update:tabIndex": null, + "update:toolbar": null, + "update:visible": null, + "update:width": null, + }, + computed: { + instance(): CardView { + return (this as any).$_instance; + } + }, + beforeCreate() { + (this as any).$_WidgetClass = CardView; + (this as any).$_hasAsyncTemplate = true; + (this as any).$_expectedChildren = { + cardCover: { isCollectionItem: false, optionName: "cardCover" }, + cardHeader: { isCollectionItem: false, optionName: "cardHeader" }, + column: { isCollectionItem: true, optionName: "columns" }, + headerPanel: { isCollectionItem: false, optionName: "headerPanel" }, + pager: { isCollectionItem: false, optionName: "pager" }, + paging: { isCollectionItem: false, optionName: "paging" }, + remoteOperations: { isCollectionItem: false, optionName: "remoteOperations" }, + toolbar: { isCollectionItem: false, optionName: "toolbar" } + }; + } +}; + +prepareComponentConfig(componentConfig); + +const DxCardView = defineComponent(componentConfig); + + +const DxCardCoverConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:altExpr": null, + "update:imageExpr": null, + }, + props: { + altExpr: [Function, String] as PropType<(((data: any) => string)) | string>, + imageExpr: [Function, String] as PropType<(((data: any) => string)) | string> + } +}; + +prepareConfigurationComponentConfig(DxCardCoverConfig); + +const DxCardCover = defineComponent(DxCardCoverConfig); + +(DxCardCover as any).$_optionName = "cardCover"; + +const DxCardHeaderConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:captionExpr": null, + "update:visible": null, + }, + props: { + captionExpr: [Function, String] as PropType<(((data: any) => string)) | string>, + visible: Boolean + } +}; + +prepareConfigurationComponentConfig(DxCardHeaderConfig); + +const DxCardHeader = defineComponent(DxCardHeaderConfig); + +(DxCardHeader as any).$_optionName = "cardHeader"; + +const DxColumnConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:fieldCaptionTemplate": null, + "update:fieldTemplate": null, + "update:fieldValueTemplate": null, + "update:headerItemCssClass": null, + "update:headerItemTemplate": null, + }, + props: { + fieldCaptionTemplate: {}, + fieldTemplate: {}, + fieldValueTemplate: {}, + headerItemCssClass: String, + headerItemTemplate: {} + } +}; + +prepareConfigurationComponentConfig(DxColumnConfig); + +const DxColumn = defineComponent(DxColumnConfig); + +(DxColumn as any).$_optionName = "columns"; +(DxColumn as any).$_isCollectionItem = true; + +const DxHeaderPanelConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:dragging": null, + "update:itemCssClass": null, + "update:itemTemplate": null, + "update:visible": null, + }, + props: { + dragging: Object as PropType>, + itemCssClass: String, + itemTemplate: {}, + visible: Boolean + } +}; + +prepareConfigurationComponentConfig(DxHeaderPanelConfig); + +const DxHeaderPanel = defineComponent(DxHeaderPanelConfig); + +(DxHeaderPanel as any).$_optionName = "headerPanel"; + +const DxItemConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:cssClass": null, + "update:disabled": null, + "update:html": null, + "update:locateInMenu": null, + "update:location": null, + "update:menuItemTemplate": null, + "update:name": null, + "update:options": null, + "update:showText": null, + "update:template": null, + "update:text": null, + "update:visible": null, + "update:widget": null, + }, + props: { + cssClass: String, + disabled: Boolean, + html: String, + locateInMenu: String as PropType, + location: String as PropType, + menuItemTemplate: {}, + name: String as PropType, + options: {}, + showText: String as PropType, + template: {}, + text: String, + visible: Boolean, + widget: String as PropType + } +}; + +prepareConfigurationComponentConfig(DxItemConfig); + +const DxItem = defineComponent(DxItemConfig); + +(DxItem as any).$_optionName = "items"; +(DxItem as any).$_isCollectionItem = true; + +const DxPagerConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:allowedPageSizes": null, + "update:displayMode": null, + "update:infoText": null, + "update:label": null, + "update:showInfo": null, + "update:showNavigationButtons": null, + "update:showPageSizeSelector": null, + "update:visible": null, + }, + props: { + allowedPageSizes: [Array, String] as PropType<(Array) | Mode>, + displayMode: String as PropType, + infoText: String, + label: String, + showInfo: Boolean, + showNavigationButtons: Boolean, + showPageSizeSelector: Boolean, + visible: [Boolean, String] as PropType + } +}; + +prepareConfigurationComponentConfig(DxPagerConfig); + +const DxPager = defineComponent(DxPagerConfig); + +(DxPager as any).$_optionName = "pager"; + +const DxPagingConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:enabled": null, + "update:pageIndex": null, + "update:pageSize": null, + }, + props: { + enabled: Boolean, + pageIndex: Number, + pageSize: Number + } +}; + +prepareConfigurationComponentConfig(DxPagingConfig); + +const DxPaging = defineComponent(DxPagingConfig); + +(DxPaging as any).$_optionName = "paging"; + +const DxRemoteOperationsConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:filtering": null, + "update:paging": null, + "update:sorting": null, + "update:summary": null, + }, + props: { + filtering: Boolean, + paging: Boolean, + sorting: Boolean, + summary: Boolean + } +}; + +prepareConfigurationComponentConfig(DxRemoteOperationsConfig); + +const DxRemoteOperations = defineComponent(DxRemoteOperationsConfig); + +(DxRemoteOperations as any).$_optionName = "remoteOperations"; + +const DxToolbarConfig = { + emits: { + "update:isActive": null, + "update:hoveredElement": null, + "update:disabled": null, + "update:items": null, + "update:visible": null, + }, + props: { + disabled: Boolean, + items: Array as PropType>, + visible: Boolean + } +}; + +prepareConfigurationComponentConfig(DxToolbarConfig); + +const DxToolbar = defineComponent(DxToolbarConfig); + +(DxToolbar as any).$_optionName = "toolbar"; +(DxToolbar as any).$_expectedChildren = { + item: { isCollectionItem: true, optionName: "items" } +}; + +export default DxCardView; +export { + DxCardView, + DxCardCover, + DxCardHeader, + DxColumn, + DxHeaderPanel, + DxItem, + DxPager, + DxPaging, + DxRemoteOperations, + DxToolbar +}; +import type * as DxCardViewTypes from "devextreme/ui/card_view_types"; +export { DxCardViewTypes }; diff --git a/packages/devextreme-vue/src/index.ts b/packages/devextreme-vue/src/index.ts index 9943299699ec..fa67f744a20e 100644 --- a/packages/devextreme-vue/src/index.ts +++ b/packages/devextreme-vue/src/index.ts @@ -7,6 +7,7 @@ export { DxBullet } from "./bullet"; export { DxButton } from "./button"; export { DxButtonGroup } from "./button-group"; export { DxCalendar } from "./calendar"; +export { DxCardView } from "./card-view"; export { DxChart } from "./chart"; export { DxChat } from "./chat"; export { DxCheckBox } from "./check-box"; diff --git a/packages/devextreme/js/__internal/core/di/index.test.ts b/packages/devextreme/js/__internal/core/di/index.test.ts new file mode 100644 index 000000000000..4e5ece50a074 --- /dev/null +++ b/packages/devextreme/js/__internal/core/di/index.test.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/no-extraneous-class */ +/* eslint-disable prefer-const */ +/* eslint-disable @typescript-eslint/init-declarations */ +/* eslint-disable max-classes-per-file */ +/* eslint-disable class-methods-use-this */ +import { describe, expect, it } from '@jest/globals'; + +import { DIContext } from './index'; + +describe('basic', () => { + describe('register', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + it('should return registered class', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.get(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.get(MyClass).getNumber()).toBe(1); + }); + + it('should return registered class with tryGet', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.tryGet(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.tryGet(MyClass)?.getNumber()).toBe(1); + }); + + it('should return same instance each time', () => { + const ctx = new DIContext(); + ctx.register(MyClass); + + expect(ctx.get(MyClass)).toBe(ctx.get(MyClass)); + }); + }); + + describe('registerInstance', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + const ctx = new DIContext(); + const instance = new MyClass(); + ctx.registerInstance(MyClass, instance); + + it('should work', () => { + expect(ctx.get(MyClass)).toBe(instance); + }); + }); + + describe('non registered items', () => { + const ctx = new DIContext(); + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + it('should throw', () => { + expect(() => ctx.get(MyClass)).toThrow(); + }); + it('should not throw if tryGet', () => { + expect(ctx.tryGet(MyClass)).toBe(null); + }); + }); +}); + +describe('dependencies', () => { + class MyUtilityClass { + static dependencies = [] as const; + + getNumber(): number { + return 2; + } + } + + class MyClass { + static dependencies = [MyUtilityClass] as const; + + constructor(private readonly utility: MyUtilityClass) {} + + getSuperNumber(): number { + return this.utility.getNumber() * 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyUtilityClass); + ctx.register(MyClass); + + it('should return registered class', () => { + expect(ctx.get(MyClass)).toBeInstanceOf(MyClass); + expect(ctx.get(MyUtilityClass)).toBeInstanceOf(MyUtilityClass); + }); + + it('dependecies should work', () => { + expect(ctx.get(MyClass).getSuperNumber()).toBe(4); + }); +}); + +describe('mocks', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + class MyClassMock implements MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyClass, MyClassMock); + + it('should return mock class when they are registered', () => { + expect(ctx.get(MyClass)).toBeInstanceOf(MyClassMock); + expect(ctx.get(MyClass).getNumber()).toBe(2); + }); +}); + +it('should work regardless of registration order', () => { + class MyClass { + static dependencies = [] as const; + + getNumber(): number { + return 1; + } + } + + class MyDependentClass { + static dependencies = [MyClass] as const; + + constructor(private readonly myClass: MyClass) {} + + getSuperNumber(): number { + return this.myClass.getNumber() * 2; + } + } + + const ctx = new DIContext(); + ctx.register(MyDependentClass); + ctx.register(MyClass); + expect(ctx.get(MyDependentClass).getSuperNumber()).toBe(2); +}); + +describe('dependency cycle', () => { + class MyClass1 { + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-use-before-define + static dependencies = [MyClass2] as const; + + constructor(private readonly myClass2: MyClass2) {} + } + class MyClass2 { + static dependencies = [MyClass1] as const; + + constructor(private readonly myClass1: MyClass1) {} + } + + const ctx = new DIContext(); + ctx.register(MyClass1); + ctx.register(MyClass2); + + it('should throw', () => { + expect(() => ctx.get(MyClass1)).toThrow(); + expect(() => ctx.get(MyClass2)).toThrow(); + }); +}); diff --git a/packages/devextreme/js/__internal/core/di/index.ts b/packages/devextreme/js/__internal/core/di/index.ts new file mode 100644 index 000000000000..51a22bb7227d --- /dev/null +++ b/packages/devextreme/js/__internal/core/di/index.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface AbstractType extends Function { + prototype: T; +} + +type Constructor = new(...deps: TDeps) => T; + +interface DIItem extends Constructor { + dependencies: readonly [...{ [P in keyof TDeps]: AbstractType }]; +} + +export class DIContext { + private readonly instances: Map = new Map(); + + private readonly fabrics: Map = new Map(); + + private readonly antiRecursionSet = new Set(); + + public register( + id: AbstractType, + fabric: DIItem, + ): void; + public register( + idAndFabric: DIItem, + ): void; + public register( + id: DIItem, + fabric?: DIItem, + ): void { + // eslint-disable-next-line no-param-reassign + fabric ??= id; + this.fabrics.set(id, fabric); + } + + public registerInstance( + id: AbstractType, + instance: T, + ): void { + this.instances.set(id, instance); + } + + public get( + id: AbstractType, + ): T { + const instance = this.tryGet(id); + + if (instance) { + return instance; + } + + throw new Error('DI item is not registered'); + } + + public tryGet( + id: AbstractType, + ): T | null { + if (this.instances.get(id)) { + return this.instances.get(id) as T; + } + + const fabric = this.fabrics.get(id); + if (fabric) { + const res: T = this.create(fabric as any); + this.instances.set(id, res); + this.instances.set(fabric, res); + return res; + } + + return null; + } + + private create(fabric: DIItem): T { + if (this.antiRecursionSet.has(fabric)) { + throw new Error('dependency cycle in DI'); + } + + this.antiRecursionSet.add(fabric); + + const args = fabric.dependencies.map((dependency) => this.get(dependency)); + + this.antiRecursionSet.delete(fabric); + + // eslint-disable-next-line new-cap + return new fabric(...args as any); + } +} diff --git a/packages/devextreme/js/__internal/core/m_inferno_renderer.ts b/packages/devextreme/js/__internal/core/m_inferno_renderer.ts index 2a1e550457a0..06ee5e06927b 100644 --- a/packages/devextreme/js/__internal/core/m_inferno_renderer.ts +++ b/packages/devextreme/js/__internal/core/m_inferno_renderer.ts @@ -1,9 +1,9 @@ +/* eslint-disable spellcheck/spell-checker */ import domAdapter from '@js/core/dom_adapter'; import { cleanDataRecursive } from '@js/core/element_data'; import injector from '@js/core/utils/dependency_injector'; import { hydrate, InfernoEffectHost } from '@ts/core/r1/runtime/inferno/index'; import { render } from 'inferno'; -// eslint-disable-next-line import/no-extraneous-dependencies import { createElement } from 'inferno-create-element'; const remove = (element) => { @@ -59,6 +59,14 @@ const infernoRenderer = injector({ render(createElement(component, props), container); } }, + + renderIntoContainer: (jsx, container, replace) => { + if (!replace) { + hydrate(jsx, container); + } else { + render(jsx, container); + } + }, }); export { infernoRenderer }; diff --git a/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts b/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts index 94cda0c2ce25..ddf7e068703d 100644 --- a/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts +++ b/packages/devextreme/js/__internal/core/r1/runtime/inferno/create_context.ts @@ -1,13 +1,13 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable func-names */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable no-plusplus */ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { Component } from 'inferno'; let contextId = 0; -export const createContext = function(defaultValue: T): { id: number; - Provider: any; - defaultValue: unknown; } { +export const createContext = function(defaultValue: T) { const id = contextId++; return { diff --git a/packages/devextreme/js/__internal/core/reactive/core.ts b/packages/devextreme/js/__internal/core/reactive/core.ts new file mode 100644 index 000000000000..e0874385e623 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/core.ts @@ -0,0 +1,88 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ + +import { type Subscription, SubscriptionBag } from './subscription'; +import type { + Callback, Gettable, Subscribable, Updatable, +} from './types'; + +export class Observable implements Subscribable, Updatable, Gettable { + private readonly callbacks: Set> = new Set(); + + constructor(private value: T) {} + + update(value: T): void { + if (this.value === value) { + return; + } + this.value = value; + + this.callbacks.forEach((c) => { + c(value); + }); + } + + updateFunc(func: (oldValue: T) => T): void { + this.update(func(this.value)); + } + + subscribe(callback: Callback): Subscription { + this.callbacks.add(callback); + callback(this.value); + + return { + unsubscribe: () => this.callbacks.delete(callback), + }; + } + + unreactive_get(): T { + return this.value; + } + + dispose(): void { + this.callbacks.clear(); + } +} + +export class InterruptableComputed< + TArgs extends readonly any[], TValue, +> extends Observable { + private readonly depValues: [...TArgs]; + + private readonly depInitialized: boolean[]; + + private isInitialized = false; + + private readonly subscriptions = new SubscriptionBag(); + + constructor( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, + ) { + super(undefined as any); + + this.depValues = deps.map(() => undefined) as any; + this.depInitialized = deps.map(() => false); + + deps.forEach((dep, i) => { + this.subscriptions.add(dep.subscribe((v) => { + this.depValues[i] = v; + + if (!this.isInitialized) { + this.depInitialized[i] = true; + this.isInitialized = this.depInitialized.every((e) => e); + } + + if (this.isInitialized) { + this.update(compute(...this.depValues)); + } + })); + }); + } + + dispose(): void { + super.dispose(); + this.subscriptions.unsubscribe(); + } +} diff --git a/packages/devextreme/js/__internal/core/reactive/index.ts b/packages/devextreme/js/__internal/core/reactive/index.ts new file mode 100644 index 000000000000..e2e8474530df --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/index.ts @@ -0,0 +1,3 @@ +export * from './subscription'; +export * from './types'; +export * from './utilities'; diff --git a/packages/devextreme/js/__internal/core/reactive/subscription.ts b/packages/devextreme/js/__internal/core/reactive/subscription.ts new file mode 100644 index 000000000000..d3bc303311df --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/subscription.ts @@ -0,0 +1,17 @@ +export interface Subscription { + unsubscribe: () => void; +} + +export class SubscriptionBag implements Subscription { + private readonly subscriptions: Subscription[] = []; + + add(subscription: Subscription): void { + this.subscriptions.push(subscription); + } + + unsubscribe(): void { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } +} diff --git a/packages/devextreme/js/__internal/core/reactive/types.ts b/packages/devextreme/js/__internal/core/reactive/types.ts new file mode 100644 index 000000000000..176361809ac4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/types.ts @@ -0,0 +1,28 @@ +/* eslint-disable spellcheck/spell-checker */ +import type { Subscription } from './subscription'; + +export interface Subscribable { + subscribe: (callback: Callback) => Subscription; +} + +export type MaybeSubscribable = T | Subscribable; + +export type MapMaybeSubscribable = { [K in keyof T]: MaybeSubscribable }; + +export function isSubscribable(value: unknown): value is Subscribable { + return typeof value === 'object' && !!value && 'subscribe' in value; +} + +export type Callback = (value: T) => void; + +export interface Updatable { + update: (value: T) => void; + updateFunc: (func: (oldValue: T) => T) => void; +} + +export interface Gettable { + unreactive_get: () => T; +} + +export type SubsGets = Subscribable & Gettable; +export type SubsGetsUpd = Subscribable & Gettable & Updatable; diff --git a/packages/devextreme/js/__internal/core/reactive/utilities.test.ts b/packages/devextreme/js/__internal/core/reactive/utilities.test.ts new file mode 100644 index 000000000000..c2faab3d23c4 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/utilities.test.ts @@ -0,0 +1,217 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + beforeEach, describe, expect, it, jest, +} from '@jest/globals'; + +import { + computed, interruptableComputed, state, toSubscribable, +} from './utilities'; + +describe('state', () => { + let myState = state('some value'); + + beforeEach(() => { + myState = state('some value'); + }); + + describe('unreactive_get', () => { + it('should return value', () => { + expect(myState.unreactive_get()).toBe('some value'); + }); + + it('should return current value if it was updated', () => { + myState.update('new value'); + expect(myState.unreactive_get()).toBe('new value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + }); + + it('should call callback on update', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + myState.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value'); + }); + + it('should not trigger update if value is not changed', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + + myState.update('some value'); + + expect(callback).toBeCalledTimes(1); + }); + }); + + describe('dispose', () => { + it('should prevent all updates', () => { + const callback = jest.fn(); + myState.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + + // @ts-expect-error + myState.dispose(); + myState.update('new value'); + + expect(callback).toBeCalledTimes(1); + }); + }); +}); + +describe('computed', () => { + let myState1 = state('some value'); + let myState2 = state('other value'); + let myComputed = computed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + + beforeEach(() => { + myState1 = state('some value'); + myState2 = state('other value'); + myComputed = computed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + }); + + describe('unreactive_get', () => { + it('should calculate initial value', () => { + expect(myComputed.unreactive_get()).toBe('some value other value'); + }); + + it('should return current value if it dependency is updated', () => { + myState1.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value other value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value other value'); + }); + + it('should call callback on update of dependency', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myState1.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value other value'); + }); + }); +}); + +describe('interruptableComputed', () => { + let myState1 = state('some value'); + let myState2 = state('other value'); + let myComputed = interruptableComputed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + + beforeEach(() => { + myState1 = state('some value'); + myState2 = state('other value'); + myComputed = interruptableComputed( + (v1, v2) => `${v1} ${v2}`, + [myState1, myState2], + ); + }); + + describe('unreactive_get', () => { + it('should calculate initial value', () => { + expect(myComputed.unreactive_get()).toBe('some value other value'); + }); + + it('should return current value if it was updated', () => { + myComputed.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value'); + }); + + it('should return current value if it dependency is updated', () => { + myState1.update('new value'); + expect(myComputed.unreactive_get()).toBe('new value other value'); + }); + }); + + describe('subscribe', () => { + it('should call callback on initial set', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value other value'); + }); + + it('should call callback on update', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myComputed.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value'); + }); + + it('should call callback on update of dependency', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + myState1.update('new value'); + + expect(callback).toBeCalledTimes(2); + expect(callback).toHaveBeenNthCalledWith(1, 'some value other value'); + expect(callback).toHaveBeenNthCalledWith(2, 'new value other value'); + }); + + it('should not trigger update if value is not changed', () => { + const callback = jest.fn(); + myComputed.subscribe(callback); + + expect(callback).toBeCalledTimes(1); + + myComputed.update('some value other value'); + + expect(callback).toBeCalledTimes(1); + }); + }); +}); + +describe('toSubscribable', () => { + it('should wrap value if it is not subscribable', () => { + const callback = jest.fn(); + toSubscribable('some value').subscribe(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('some value'); + }); + + it('should return value as is if subscribable', () => { + const myState = state(1); + expect(toSubscribable(myState)).toBe(myState); + }); +}); diff --git a/packages/devextreme/js/__internal/core/reactive/utilities.ts b/packages/devextreme/js/__internal/core/reactive/utilities.ts new file mode 100644 index 000000000000..bb1ba91d29a3 --- /dev/null +++ b/packages/devextreme/js/__internal/core/reactive/utilities.ts @@ -0,0 +1,222 @@ +/* eslint-disable @typescript-eslint/no-invalid-void-type */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable spellcheck/spell-checker */ +import { InterruptableComputed, Observable } from './core'; +import { type Subscription, SubscriptionBag } from './subscription'; +import type { + Gettable, MapMaybeSubscribable, MaybeSubscribable, Subscribable, SubsGets, SubsGetsUpd, Updatable, +} from './types'; +import { isSubscribable } from './types'; + +/** + * Creates new reactive state atom. + * @example + * ``` + * const myState = state(0); + * myState.update(1); + * ``` + * @param value initial value of state + */ +export function state(value: T): Subscribable & Updatable & Gettable { + return new Observable(value); +} + +/** + * Creates computed atom based on other atoms. + * @example + * ``` + * const myState = state(0); + * const myComputed = computed( + * (value) => value + 1, + * [myState] + * ); + * ``` + * @param compute computation func + * @param deps dependency atoms + */ +export function computed( + compute: (t1: T1) => TValue, + deps: [Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2) => TValue, + deps: [Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3,) => TValue, + deps: [Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => TValue, + // eslint-disable-next-line @stylistic/max-len + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGets; +export function computed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGets; +export function computed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGets { + return new InterruptableComputed(compute, deps); +} + +/** + * Computed, with ability to override value using `.update(...)` method. + * @see {@link computed} + */ +export function interruptableComputed( + compute: (t1: T1) => TValue, + deps: [Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2) => TValue, + deps: [Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2, t3: T3,) => TValue, + deps: [Subscribable, Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (t1: T1, t2: T2, t3: T3, t4: T4) => TValue, + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): SubsGetsUpd; +export function interruptableComputed( + compute: (...args: TArgs) => TValue, + deps: { [I in keyof TArgs]: Subscribable }, +): SubsGetsUpd { + return new InterruptableComputed(compute, deps); +} + +/** + * Allows to subscribe function with some side effects to changes of dependency atoms. + * @param callback function which is executed each time any dependency is updated + * @param deps dependencies + */ +export function effect( + callback: (t1: T1) => ((() => void) | void), + deps: [Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2) => ((() => void) | void), + deps: [Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3,) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => ((() => void) | void), + deps: [Subscribable, Subscribable, Subscribable, Subscribable, Subscribable] +): Subscription; +export function effect( + callback: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6) => ((() => void) | void), + deps: [ + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + Subscribable, + ] +): Subscription; +export function effect( + callback: (...args: TArgs) => ((() => void) | void), + deps: { [I in keyof TArgs]: Subscribable }, +): Subscription { + const depValues: [...TArgs] = deps.map(() => undefined) as any; + const depInitialized = deps.map(() => false); + let isInitialized = false; + + const subscription = new SubscriptionBag(); + + deps.forEach((dep, i) => { + subscription.add(dep.subscribe((v) => { + depValues[i] = v; + + if (!isInitialized) { + depInitialized[i] = true; + isInitialized = depInitialized.every((e) => e); + } + + if (isInitialized) { + callback(...depValues); + } + })); + }); + + return subscription; +} + +export function toSubscribable(v: MaybeSubscribable): Subscribable { + if (isSubscribable(v)) { + return v; + } + + return new Observable(v); +} + +/** + * Condition atom, basing whether `cond` is true or false, + * returns value of `ifTrue` or `ifFalse` param. + * @param cond + * @param ifTrue + * @param ifFalse + */ +export function iif( + cond: MaybeSubscribable, + ifTrue: MaybeSubscribable, + ifFalse: MaybeSubscribable, +): Subscribable { + const obs = state(undefined as any); + // eslint-disable-next-line @typescript-eslint/init-declarations + let subscription: Subscription | undefined; + + // eslint-disable-next-line @typescript-eslint/no-shadow + toSubscribable(cond).subscribe((cond) => { + subscription?.unsubscribe(); + const newSource = cond ? ifTrue : ifFalse; + subscription = toSubscribable(newSource).subscribe(obs.update.bind(obs)); + }); + + return obs; +} + +/** + * Combines object of Subscribables to Subscribable of object. + * @example + * ``` + * const myValueA = state(0); + * const myValueB = state(1); + * const obj = combine({ + * myValueA, myValueB + * }); + * + * obj.unreactive_get(); // {myValueA: 0, myValueB: 1} + * @returns + */ +export function combined( + obj: MapMaybeSubscribable, +): SubsGets { + const entries = Object.entries(obj) as any as [string, Subscribable][]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return computed( + (...args) => Object.fromEntries( + args.map((v, i) => [entries[i][0], v]), + ), + entries.map(([, v]) => toSubscribable(v)), + ) as any; +} diff --git a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts index c4682b9c663d..57b5dd0307ec 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/header_panel/m_header_panel.ts @@ -2,12 +2,12 @@ import messageLocalization from '@js/common/core/localization/message'; import $ from '@js/core/renderer'; import { getPathParts } from '@js/core/utils/data'; -import { extend } from '@js/core/utils/extend'; -import { isDefined, isString } from '@js/core/utils/type'; +import { isDefined } from '@js/core/utils/type'; import type { Properties as ToolbarProperties } from '@js/ui/toolbar'; import Toolbar from '@js/ui/toolbar'; import type { EditingController } from '@ts/grids/grid_core/editing/m_editing'; import type { HeaderFilterController } from '@ts/grids/grid_core/header_filter/m_header_filter'; +import { normalizeToolbarItems } from '@ts/grids/new/grid_core/toolbar/utils'; import type { ModuleType } from '../m_types'; import { ColumnsView } from '../views/m_columns_view'; @@ -72,7 +72,11 @@ export class HeaderPanel extends ColumnsView { }; const userItems = userToolbarOptions?.items; - options.toolbarOptions.items = this._normalizeToolbarItems(options.toolbarOptions.items, userItems); + options.toolbarOptions.items = normalizeToolbarItems( + options.toolbarOptions.items, + userItems, + DEFAULT_TOOLBAR_ITEM_NAMES, + ); this.executeAction('onToolbarPreparing', options); @@ -84,51 +88,6 @@ export class HeaderPanel extends ColumnsView { return options.toolbarOptions; } - private _normalizeToolbarItems(defaultItems, userItems) { - defaultItems.forEach((button) => { - if (!DEFAULT_TOOLBAR_ITEM_NAMES.includes(button.name)) { - throw new Error(`Default toolbar item '${button.name}' is not added to DEFAULT_TOOLBAR_ITEM_NAMES`); - } - }); - - const defaultProps = { - location: 'after', - }; - - const isArray = Array.isArray(userItems); - - if (!isDefined(userItems)) { - return defaultItems; - } - - if (!isArray) { - userItems = [userItems]; - } - - const defaultButtonsByNames = {}; - defaultItems.forEach((button) => { - defaultButtonsByNames[button.name] = button; - }); - - const normalizedItems = userItems.map((button) => { - if (isString(button)) { - button = { name: button }; - } - - if (isDefined(button.name)) { - if (isDefined(defaultButtonsByNames[button.name])) { - button = extend(true, {}, defaultButtonsByNames[button.name], button); - } else if (DEFAULT_TOOLBAR_ITEM_NAMES.includes(button.name)) { - button = { ...button, visible: false }; - } - } - - return extend(true, {}, defaultProps, button); - }); - - return isArray ? normalizedItems : normalizedItems[0]; - } - protected _renderCore() { if (!this._toolbar) { const $headerPanel = this.element(); @@ -217,7 +176,11 @@ export class HeaderPanel extends ColumnsView { this._invalidate(); } else if (parts.length === 3) { // `toolbar.items[i]` case - const normalizedItem = this._normalizeToolbarItems(this._getToolbarItems(), args.value); + const normalizedItem = normalizeToolbarItems( + this._getToolbarItems(), + [args.value], + DEFAULT_TOOLBAR_ITEM_NAMES, + )[0]; this._toolbar?.option(optionName, normalizedItem); } else if (parts.length >= 4) { // `toolbar.items[i].prop` case diff --git a/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap new file mode 100644 index 000000000000..c27b9f04d5ae --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/__snapshots__/widget.test.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`common initial render should be successfull 1`] = ` +
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap new file mode 100644 index 000000000000..ffaddb53d48e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/__snapshots__/card.test.tsx.snap @@ -0,0 +1,151 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rendering should be rendered correctly 1`] = ` +
+
+
+ +
+ Card Cover +
+
+
+ + Field + : + + + devextreme + +
+
+
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx new file mode 100644 index 000000000000..d91b4b2c49ae --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.events.test.tsx @@ -0,0 +1,163 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + +/* eslint-disable @typescript-eslint/init-declarations */ + +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { Card } from './card'; + +const mockOnDblClick = { + called: false, + call() { + this.called = true; + }, +}; + +const mockOnClick = { + called: false, + call() { + this.called = true; + }, +}; + +const props = { + row: { + cells: [ + { + column: { + dataField: 'Name', + name: 'Field', + }, + value: 'devextreme', + text: 'devextreme', + }, + ], + key: 0, + }, + toolbar: [ + { + location: 'before', + widget: 'dxCheckBox', + }, + { + location: 'before', + text: 'Card Header', + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'edit', + stylingMode: 'text', + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'trash', + stylingMode: 'text', + }, + }, + ], + cover: { + src: 'https://www.devexpress.com/support/demos/i/demo-thumbs/aspnetcore-grid.png', + alt: 'Card Cover', + className: 'card-cover', + }, + hoverStateEnabled: true, + maxWidth: 300, + width: 300, + minWidth: 300, + onDblClick: mockOnDblClick.call(), + onClick: mockOnClick.call(), +}; + +const CLASSES = { + card: 'dx-cardview-card', +}; + +describe('Events', () => { + let container: HTMLDivElement; + // @ts-expect-error + beforeEach(() => { + container = document.createElement('div'); + // @ts-expect-error + render(, container); + }); + + it('should trigger onClick event', () => { + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('click')); + + expect(mockOnClick.called).toBe(true); + }); + + it('should trigger onDblClick event', () => { + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('dblclick')); + + expect(mockOnDblClick.called).toBe(true); + }); + + it('should trigger onHoverChanged event on mouse enter', () => { + const mockHover: { called: boolean; fn: ({ isHovered }: { isHovered: boolean }) => void } = { + called: false, + fn: ({ isHovered }: { isHovered: boolean }) => { + mockHover.called = true; + expect(isHovered).toBe(true); + }, + }; + + const newProps = { ...props, hoverStateEnabled: true, onHoverChanged: mockHover.fn }; + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('mouseenter')); + + expect(mockHover.called).toBe(true); + }); + + it('should trigger onHoverChanged event on mouse leave', () => { + const mockHover: { called: boolean; fn: ({ isHovered }: { isHovered: boolean }) => void } = { + called: false, + fn: ({ isHovered }: { isHovered: boolean }) => { + mockHover.called = true; + expect(isHovered).toBe(false); + }, + }; + + const newProps = { ...props, hoverStateEnabled: true, onHoverChanged: mockHover.fn }; + // @ts-expect-error + render(, container); + + const cardElement = container.querySelector(`.${CLASSES.card}`); + cardElement?.dispatchEvent(new MouseEvent('mouseleave')); + + expect(mockHover.called).toBe(true); + }); + + it('should handle hoverStateEnabled prop correctly', () => { + const cardElement = container.querySelector('.dx-cardview-card'); + cardElement?.dispatchEvent(new MouseEvent('mouseenter')); + + const classList = cardElement?.getAttribute('class') || ''; + expect(classList).toContain('dx-cardview-card-hover'); + }); + + it('should render field template correctly', () => { + const fieldName = container.querySelector('.dx-cardview-field-name'); + const fieldValue = container.querySelector('.dx-cardview-field-value'); + + expect(fieldName?.textContent).toBe('Field:'); + expect(fieldValue?.textContent).toBe('devextreme'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.test.tsx new file mode 100644 index 000000000000..3b060f83f28d --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.test.tsx @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { describe, expect, it } from '@jest/globals'; +import { compileGetter } from '@js/common/data'; +import { render } from 'inferno'; + +import { Card } from './card'; + +const props = { + row: { + cells: [ + { + column: { + dataField: 'Name', + name: 'Field', + }, + value: 'devextreme', + text: 'devextreme', + }, + ], + key: 0, + data: { + Field: 'Name', + img: 'https://www.devexpress.com/support/demos/i/demo-thumbs/aspnetcore-grid.png', + alt: 'Card Cover', + }, + }, + toolbar: [ + { + location: 'before', + widget: 'dxCheckBox', + }, + { + location: 'before', + text: 'Card Header', + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'edit', + stylingMode: 'text', + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'trash', + stylingMode: 'text', + }, + }, + ], + cover: { + imageExpr: compileGetter('img'), + altExpr: compileGetter('alt'), + }, +}; + +describe('Rendering', () => { + it('should be rendered correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + expect(container).toMatchSnapshot(); + }); + + it('should render content correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const fieldValue = container.querySelector('.dx-cardview-field-value'); + expect(fieldValue?.textContent).toEqual('devextreme'); + }); +}); + +describe('Card Header', () => { + it('should render the card header components correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const cardHeaderText = container.querySelector('.dx-toolbar-label .dx-toolbar-item-content > div'); + expect(cardHeaderText?.textContent).toBe('Card Header'); + + const checkbox = container.querySelectorAll('.dx-checkbox'); + expect(checkbox).toHaveLength(1); + + const editButton = container.querySelectorAll('.dx-icon-edit'); + expect(editButton).toHaveLength(1); + + const trashButton = container.querySelectorAll('.dx-icon-trash'); + expect(trashButton).toHaveLength(1); + }); +}); + +describe('Image', () => { + it('should render the image correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const image = container.querySelector('img'); + expect(image).not.toBeNull(); + }); +}); + +describe('Field Template', () => { + it('should render field template correctly', () => { + const container = document.createElement('div'); + // @ts-expect-error + render(, container); + + const fieldName = container.querySelector('.dx-cardview-field-name'); + const fieldValue = container.querySelector('.dx-cardview-field-value'); + + expect(fieldName?.textContent).toBe('Field:'); + expect(fieldValue?.textContent).toBe('devextreme'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx new file mode 100644 index 000000000000..2caaae89e1ae --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/card.tsx @@ -0,0 +1,152 @@ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + +import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { DataObject } from '@ts/grids/new/grid_core/data_controller/types'; +import type { InfernoNode, RefObject } from 'inferno'; +import { Component, createRef } from 'inferno'; + +import { Cover } from './cover'; +import { Field } from './field'; +import type { CardHeaderItem } from './header'; +import { CardHeader } from './header'; + +export const CLASSES = { + card: 'dx-cardview-card', + cardHover: 'dx-cardview-card-hoverable', + content: 'dx-cardview-card-content', +}; + +export interface CardClickEvent { + event: MouseEvent; + row: DataRow; +} + +export interface CardHoverEvent { + isHovered: boolean; + row: DataRow; +} + +export interface CardPreparedEvent { + instance: Card; +} + +export interface CardProps { + row: DataRow; + + cover?: { + imageExpr?: (data: DataObject) => string; + + altExpr?: (data: DataObject) => string; + }; + + elementRef?: RefObject; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fieldTemplate?: any; + + hoverStateEnabled?: boolean; + + toolbar?: CardHeaderItem[]; + + template?: (row: DataRow) => JSX.Element; + + onClick?: (e: CardClickEvent) => void; + + onDblClick?: (e: CardClickEvent) => void; + + onHoverChanged?: (e: CardHoverEvent) => void; + + onPrepared?: (e: CardPreparedEvent) => void; +} + +export class Card extends Component { + private containerRef = createRef(); + + private fieldRefs: RefObject[] = []; + + render(): InfernoNode { + if (this.props.elementRef) { + this.containerRef = this.props.elementRef; + } + + this.fieldRefs = new Array(this.props.row.cells.length).fill(undefined).map(() => createRef()); + + const { + fieldTemplate: FieldTemplate = Field, + hoverStateEnabled, + cover, + } = this.props; + + const className = [ + CLASSES.card, + hoverStateEnabled ? CLASSES.cardHover : '', + ].filter(Boolean).join(' '); + + // @ts-expect-error + const imageSrc = cover?.imageExpr?.(this.props.row.data); + // @ts-expect-error + const alt = cover?.altExpr?.(this.props.row.data); + + return ( +
+ + {imageSrc && ( + + )} +
+ {this.props.row.cells.map((cell, index) => ( + + ))} +
+
+ ); + } + + componentDidMount(): void { + const { onPrepared } = this.props; + if (onPrepared) { + onPrepared({ instance: this }); + } + } + + handleMouseEnter = (): void => { + const { onHoverChanged, row } = this.props; + + onHoverChanged?.({ isHovered: true, row }); + }; + + handleMouseLeave = (): void => { + const { onHoverChanged, row } = this.props; + + onHoverChanged?.({ isHovered: false, row }); + }; + + handleClick = (event: MouseEvent): void => { + const { onClick, row } = this.props; + onClick?.({ event, row }); + }; + + handleDoubleClick = (event: MouseEvent): void => { + const { onDblClick, row } = this.props; + onDblClick?.({ event, row }); + }; +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.test.tsx new file mode 100644 index 000000000000..a080c3238937 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.test.tsx @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { Cover } from './cover'; + +describe('Cover', () => { + it('should render the image correctly', () => { + const container = document.createElement('div'); + const props = { + imageSrc: 'https://www.devexpress.com/support/demos/i/demo-thumbs/aspnetcore-grid.png', + alt: 'Card Cover', + className: 'cover-image', + }; + + render(, container); + + const image = container.querySelector('img'); + expect(image).not.toBeNull(); + expect(image?.src).toBe(props.imageSrc); + expect(image?.alt).toBe(props.alt); + expect(image?.className).toContain(props.className); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.tsx new file mode 100644 index 000000000000..d191bdaace20 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/cover.tsx @@ -0,0 +1,39 @@ +import { Component } from 'inferno'; + +const CLASSES = { + cover: 'dx-card-cover', + image: 'dx-card-cover-image', +}; + +export interface CoverProps { + imageSrc?: string; + alt?: string; + template?: (src: string, alt: string | undefined, className: string) => JSX.Element; +} + +export class Cover extends Component { + render(): JSX.Element { + const { + imageSrc, alt, template, + } = this.props; + const src = imageSrc; + + if (!src) { + // @ts-expect-error + return null; + } + + if (template) { + return template(src, alt, CLASSES.image); + } + return ( +
+ {alt} +
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/field.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/field.tsx new file mode 100644 index 000000000000..f775fef55388 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/field.tsx @@ -0,0 +1,90 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ +import type { RefObject } from 'inferno'; +import { Component, createRef } from 'inferno'; + +export const CLASSES = { + field: 'dx-cardview-field', + fieldName: 'dx-cardview-field-name', + fieldValue: 'dx-cardview-field-value', + overflowHint: 'dx-cardview-overflow-hint', +}; + +export interface FieldProps { + title: string | undefined; + value: unknown; + alignment: 'right' | 'center' | 'left'; + wordWrapEnabled?: boolean; + cellHintEnabled?: boolean; + elementRef?: RefObject; + captionTemplate?: (title: string) => JSX.Element; + valueTemplate?: (value: unknown) => JSX.Element; + + onClick?: (e: MouseEvent) => void; + onDblClick?: (e: MouseEvent) => void; + onHoverChanged?: (hovered: boolean) => void; + onPrepared?: (element: HTMLElement) => void; +} + +export class Field extends Component { + private readonly containerRef: RefObject; + + constructor(props: FieldProps) { + super(props); + this.containerRef = this.props.elementRef || createRef(); + } + + componentDidMount(): void { + this.props.onPrepared?.(this.containerRef.current!); + } + + renderCaption(): JSX.Element { + const { title, captionTemplate } = this.props; + if (captionTemplate && title) { + return captionTemplate(title); + } + return {title}:; + } + + renderValue(): JSX.Element { + const { + value, valueTemplate, wordWrapEnabled, alignment, cellHintEnabled, + } = this.props; + + if (valueTemplate && value) { + return valueTemplate(value); + } + + const valueStyle = { + textAlign: alignment, + whiteSpace: wordWrapEnabled ? 'normal' : 'nowrap', + }; + + return ( + + {value} + + ); + } + + render(): JSX.Element { + return ( +
this.props.onHoverChanged?.(true)} + onMouseLeave={(): void => this.props.onHoverChanged?.(false)} + onClick={this.props.onClick} + onDblClick={this.props.onDblClick} + ref={this.containerRef} + tabIndex={0} + className={CLASSES.field} + > + {this.renderCaption()} + {this.renderValue()} +
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx new file mode 100644 index 000000000000..7b11faed033a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.test.tsx @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable import/no-extraneous-dependencies */ +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { CardHeader, CLASSES } from './header'; + +describe('CardHeader', () => { + it('should render with default properties', () => { + const container = document.createElement('div'); + render(, container); + + // Verify the rendered element + const header = container.querySelector(`.${CLASSES.cardHeader}`); + expect(header).not.toBeNull(); + + // Verify the item text + const headerItem = container.querySelector('.dx-toolbar-item'); + expect(headerItem).not.toBeNull(); + expect(headerItem?.textContent).toBe('Test Header'); + }); + + it('should not render when visible is false', () => { + const container = document.createElement('div'); + render(, container); + + // Verify the header is not rendered + const header = container.querySelector(CLASSES.cardHeader); + expect(header).toBeNull(); + }); + + it('should render with caption from captionExpr', () => { + const container = document.createElement('div'); + render( + , + container, + ); + + // Verify the caption text + const captionItem = container.querySelector('.dx-toolbar-item'); + expect(captionItem).not.toBeNull(); + expect(captionItem?.textContent).toBe('Card Title'); + }); + + it('should render with a custom template', () => { + const container = document.createElement('div'); + const CustomTemplate = (items: any[]) => ( +
{items[0].text}
+ ); + + render( + , + container, + ); + + // Verify the custom template + const customHeader = container.querySelector('.custom-header'); + expect(customHeader).not.toBeNull(); + expect(customHeader?.textContent).toBe('Custom Header'); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx new file mode 100644 index 000000000000..7cce7a366b11 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/card/header.tsx @@ -0,0 +1,56 @@ +import type { dxToolbarItem } from '@js/ui/toolbar'; +import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; +import { Toolbar } from '@ts/grids/new/grid_core/inferno_wrappers/toolbar'; +import { Component } from 'inferno'; + +export const CLASSES = { + cardHeader: 'dx-cardview-card-header', +}; + +export interface CardHeaderItem { + location: 'before' | 'after'; + widget?: string; + text?: string; + options?: dxToolbarItem; +} + +export interface CardHeaderProps { + items?: CardHeaderItem[]; + visible?: boolean; + captionExpr?: string; + template?: (items: CardHeaderItem[]) => JSX.Element; + row?: DataRow; +} + +export class CardHeader extends Component { + render(): JSX.Element | null { + const { + visible = true, + items = [], + captionExpr, + template, + row, + } = this.props; + + if (!visible) { + return null; + } + + const captionItem: CardHeaderItem | null = captionExpr && row?.[captionExpr] + ? { location: 'before', text: row[captionExpr] } + : null; + + const finalItems = captionItem ? [captionItem, ...items] : items; + + if (template) { + return template(finalItems); + } + + return ( +
+ {/* @ts-expect-error */} + +
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx new file mode 100644 index 000000000000..016acae38842 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content/content.tsx @@ -0,0 +1,114 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { RefObject } from 'inferno'; +import { Component, createRef } from 'inferno'; + +import { Card } from './card/card'; +import type { CardHeaderItem } from './card/header'; + +export interface ContentProps { + items: DataRow[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fieldTemplate?: any; + + onRowHeightChange?: (value: number) => void; + + cardsPerRow?: number; + + cardProps?: { + toolbar?: CardHeaderItem[]; + minWidth?: number; + maxWidth?: number; + }; +} + +export const CLASSES = { + content: 'dx-cardview-content', + grid: 'dx-cardview-content-grid', +}; + +function getInfernoCardKey(card: DataRow): undefined | string | number { + if (typeof card.key === 'string' || typeof card.key === 'number') { + return card.key; + } + + return undefined; +} + +export class Content extends Component { + private readonly containerRef = createRef(); + + private cardRefs: RefObject[] = []; + + private getCssVariables(): Record { + const variables = {}; + + if (this.props.cardsPerRow) { + variables['--dx-cardview-cardsperrow'] = this.props.cardsPerRow; + } + + if (this.props.cardProps?.minWidth) { + variables['--dx-cardview-card-min-width'] = `${this.props.cardProps?.minWidth}px`; + } + + if (this.props.cardProps?.maxWidth) { + variables['--dx-cardview-card-max-width'] = `${this.props.cardProps?.maxWidth}px`; + } + + // @ts-expect-error + if (this.props.cardProps?.cover?.maxHeight) { + // @ts-expect-error + variables['--dx-cardview-card-cover-max-height'] = `${this.props.cardProps?.cover?.maxHeight}px`; + } + + // @ts-expect-error + if (this.props.cardProps?.cover?.ratio) { + // @ts-expect-error + variables['--dx-cardview-card-cover-ratio'] = `${this.props.cardProps?.cover?.ratio}`; + } + + return variables; + } + + render(): JSX.Element { + this.cardRefs = new Array(this.props.items.length).fill(undefined).map(() => createRef()); + return ( +
+ {this.props.items.map((item, i) => ( + + ))} +
+ ); + } + + updateSizesInfo(): void { + const firstCard = this.cardRefs[0]; + if (!firstCard) { + return; + } + const cardHeight = firstCard.current!.offsetHeight; + const gapHeight = parseFloat(getComputedStyle(this.containerRef.current!).rowGap); + const rowHeight = cardHeight + gapHeight; + this.props.onRowHeightChange?.(rowHeight); + } + + componentDidMount(): void { + this.updateSizesInfo(); + } + + componentDidUpdate(): void { + this.updateSizesInfo(); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx new file mode 100644 index 000000000000..cec5c649603a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/content_view.tsx @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ContentViewProps as ContentViewBaseProps } from '@ts/grids/new/grid_core/content_view/content_view'; +import { ContentView as ContentViewBase } from '@ts/grids/new/grid_core/content_view/content_view'; +import type { InfernoNode } from 'inferno'; +import { Component } from 'inferno'; + +import type { ContentProps } from './content/content'; +import { Content } from './content/content'; + +export interface ContentViewProps extends ContentViewBaseProps { + contentProps: ContentProps; +} + +export class ContentView extends Component { + render(): InfernoNode { + return ( + + + + ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/index.ts new file mode 100644 index 000000000000..89edfeb2d9f6 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/index.ts @@ -0,0 +1,3 @@ +export * from './options'; +export * from './public_methods'; +export { ContentView as View } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/options.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/options.ts new file mode 100644 index 000000000000..2d2efcd32242 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/options.ts @@ -0,0 +1,22 @@ +import * as Base from '../../grid_core/content_view/options'; +import type { DataObject } from '../../grid_core/data_controller/types'; + +export interface Options extends Base.Options { + cardsPerRow?: number | 'auto'; + cardMinWidth?: number; + cardMaxWidth?: number; + cardCover?: { + imageExpr?: string | ((data: DataObject) => string); + altExpr?: string | ((data: DataObject) => string); + maxHeight?: number; + ratio?: string; + }; +} + +export const defaultOptions = { + cardsPerRow: 3, + cardCover: { + ratio: '1 / 1', + }, + ...Base.defaultOptions, +} satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/public_methods.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/public_methods.ts new file mode 100644 index 000000000000..316555f210b8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/public_methods.ts @@ -0,0 +1,40 @@ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { DxElement } from '@js/core/element'; +import { getPublicElement } from '@js/core/element'; +import $ from '@js/core/renderer'; +import type { DataRow } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { Constructor } from '@ts/grids/new/grid_core/types'; + +import * as Base from '../../grid_core/content_view/public_methods'; +import type { Key } from '../../grid_core/data_controller/types'; +import { ItemsController } from '../../grid_core/items_controller/items_controller'; +import type { CardViewBase } from '../widget'; +import * as cardModule from './content/card/card'; + +export function PublicMethods>(GridCore: T) { + return class CardViewWithContentView extends Base.PublicMethods(GridCore) { + public getCardElement(cardIndex: number): DxElement { + const card = $(this.element()).find(cardModule.CLASSES.card).eq(cardIndex); + + return getPublicElement(card); + } + + public getVisibleCards(): DataRow[] { + const itemsController = this.diContext.get(ItemsController); + return itemsController.items.unreactive_get(); + } + + public getCardIndexByKey(key: Key): number { + const itemsController = this.diContext.get(ItemsController); + const cards = itemsController.items.unreactive_get(); + + return cards.findIndex((card) => card.key === key); + } + + public getKeyByCardIndex(cardIndex: number): Key { + return this.getVisibleCards()[cardIndex]?.key; + } + }; +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.test.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.test.ts new file mode 100644 index 000000000000..367c1d215476 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from '@jest/globals'; + +import { factors } from './utils'; + +describe('factors', () => { + it('should return all factors of given number', () => { + expect(factors(1)).toEqual([1]); + expect(factors(2)).toEqual([1, 2]); + expect(factors(7)).toEqual([1, 7]); + expect(factors(6)).toEqual([1, 2, 3, 6]); + expect(factors(8)).toEqual([1, 2, 4, 8]); + expect(factors(12)).toEqual([1, 2, 3, 4, 6, 12]); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.ts b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.ts new file mode 100644 index 000000000000..1a23a1034c6a --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/utils.ts @@ -0,0 +1,9 @@ +export function factors(n: number): number[] { + const res: number[] = []; + for (let i = 1; i <= n; i += 1) { + if (n % i === 0) { + res.push(i); + } + } + return res; +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx new file mode 100644 index 000000000000..ac12d5ee4440 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/content_view/view.tsx @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { compileGetter } from '@js/core/utils/data'; +import { isDefined } from '@js/core/utils/type'; +import { combined, computed, state } from '@ts/core/reactive/index'; +import type { OptionsController } from '@ts/grids/new/card_view/options_controller'; + +import { ContentView as ContentViewBase } from '../../grid_core/content_view/view'; +import type { DataObject } from '../../grid_core/data_controller/types'; +import type { ContentViewProps } from './content_view'; +import { ContentView as ContentViewComponent } from './content_view'; +import { factors } from './utils'; + +export class ContentView extends ContentViewBase { + // @ts-expect-error + protected options: OptionsController; + + private readonly cardMinWidth = this.options.oneWay('cardMinWidth'); + + private readonly rowHeight = state(0); + + private readonly cardsPerRow = computed( + (width, cardMinWidth, pageSize, cardsPerRowProp) => { + if (cardsPerRowProp !== 'auto') { + return cardsPerRowProp; + } + + const result = factors(pageSize).reverse().find((cardsPerRow) => { + const cardWidth = (width - 6 * (cardsPerRow - 1)) / cardsPerRow; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return cardMinWidth! <= cardWidth; + }); + + return result ?? 1; + }, + [this.width, this.cardMinWidth, this.dataController.pageSize, this.options.oneWay('cardsPerRow')], + ); + + protected override component = ContentViewComponent; + + protected override getProps() { + return combined({ + ...this.getBaseProps(), + contentProps: combined({ + items: this.itemsController.items, + // items: computed((virtualState) => virtualState.virtualItems, [this.virtualState]), + fieldTemplate: this.options.template('fieldTemplate'), + cardsPerRow: this.cardsPerRow, + onRowHeightChange: this.rowHeight.update.bind(this.rowHeight), + cardProps: combined({ + minWidth: this.cardMinWidth, + maxWidth: this.options.oneWay('cardMaxWidth'), + onClick: this.options.action('onCardClick'), + onDblClick: this.options.action('onCardDblClick'), + onHoverChanged: this.options.action('onCardHoverChanged'), + onPrepared: this.options.action('onCardPrepared'), + cover: combined({ + imageExpr: computed( + (imageExpr) => this.processExpr(imageExpr), + [this.options.oneWay('cardCover.imageExpr')], + ), + altExpr: computed( + (altExpr) => this.processExpr(altExpr), + [this.options.oneWay('cardCover.altExpr')], + ), + maxHeight: this.options.oneWay('cardCover.maxHeight'), + ratio: this.options.oneWay('cardCover.ratio'), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toolbar: this.options.oneWay('cardHeader.items') as any, + }), + }), + }); + } + + private processExpr( + expr: T | ((data: DataObject) => T) | undefined, + ): ((data: DataObject) => T) | undefined { + if (!isDefined(expr)) { + return undefined; + } + // @ts-expect-error + return compileGetter(expr); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap new file mode 100644 index 000000000000..ac76d2654bdb --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/item.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Item should render sort icons 1`] = ` +
+
+ Column 1 + + + +
+
+`; + +exports[`Item should use column caption as text 1`] = ` +
+
+ my column caption +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap new file mode 100644 index 000000000000..efd060095c9e --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/__snapshots__/options.test.ts.snap @@ -0,0 +1,156 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColumnProperties headerItemCssClass should override content of headerPanel item 1`] = ` +
+
+
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+ +
+
+`; + +exports[`Options headerPanel itemCssClass should add css class to headerPanel item 1`] = ` +
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+`; + +exports[`Options headerPanel visible when it is false should hide headerPanel 1`] = ` +
+ +
+`; + +exports[`Options headerPanel visible when it is true should show headerPanel 1`] = ` +
+
+
+
+
+
+
+
+ Column 1 +
+
+
+ +
+
+ +
+
+`; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx new file mode 100644 index 000000000000..0ba637d4c77f --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/column_sortable.tsx @@ -0,0 +1,143 @@ +import $ from '@js/core/renderer'; +import type * as SortableTypes from '@js/ui/sortable_types'; +import type { ComponentType, InfernoNode } from 'inferno'; +import { Component, render } from 'inferno'; + +import type { Column } from '../../grid_core/columns_controller/types'; +import type { Props as SortableProps } from '../../grid_core/inferno_wrappers/sortable'; +import { Sortable } from '../../grid_core/inferno_wrappers/sortable'; + +export type Status = 'forbid' | 'show' | 'moving' | 'none'; + +const ALLOWED_DRAGGING_DISTANCE = 64; + +export interface Props extends Omit { + source: string; + + visibleColumns: Column[]; + + allowColumnReordering: boolean; + + onMove: (column: Column, toIndex: number, source: string) => void; + + dragTemplate?: ComponentType<{ column: Column; status: Status }>; +} + +interface State { + status: Status; +} + +export class ColumnSortable extends Component { + status: Status = 'moving'; + + dragItemProps?: { + container: HTMLElement; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: any; + }; + + private readonly onDragStart = (e: SortableTypes.DragStartEvent): void => { + const column = this.props.visibleColumns[e.fromIndex]; + + if (!column.allowReordering) { + e.cancel = true; + return; + } + + e.itemData = { + column, + source: this.props.source, + }; + }; + + private readonly onDragMove = (e: SortableTypes.DragMoveEvent): void => { + const containerCoords = $(e.element).get(0).getBoundingClientRect(); + const dragCoords = { + // @ts-expect-error + x: e.event.clientX, + // @ts-expect-error + y: e.event.clientY, + }; + + const yDistance = Math.min( + Math.abs(dragCoords.y - containerCoords.y), + Math.abs(dragCoords.y - containerCoords.y + containerCoords.height), + ); + + this.status = yDistance <= ALLOWED_DRAGGING_DISTANCE + ? 'moving' + : 'forbid'; + + this.renderDragTemplate(); + }; + + private readonly onDragChange = (e: SortableTypes.DragChangeEvent): void => { + if (this.status === 'forbid') { + e.cancel = true; + } + }; + + private readonly onMove = (e: SortableTypes.AddEvent | SortableTypes.ReorderEvent): void => { + this.props.onMove( + e.itemData.column, + e.toIndex, + e.itemData.source, + ); + }; + + // TODO: move all none-native approaches to sortable wrapper + private readonly renderDragTemplate = (): void => { + if (!this.dragItemProps) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const DragTemplate = this.props.dragTemplate!; + render( + , + this.dragItemProps.container, + ); + }; + + render(): InfernoNode { + if (!this.props.allowColumnReordering) { + return this.props.children; + } + + const { + source, + visibleColumns, + dragTemplate, + dropFeedbackMode, + ...restProps + } = this.props; + + const sortableDragTemplate = dragTemplate ? (e, container): void => { + this.dragItemProps = { + props: e, + // @ts-expect-error + container: $(container).get(0), + }; + this.renderDragTemplate(); + } : undefined; + + return ( + + {this.props.children} + + ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx new file mode 100644 index 000000000000..ba3efebeafd8 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/header_panel.tsx @@ -0,0 +1,77 @@ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import { Scrollable } from '@ts/grids/new/grid_core/inferno_wrappers/scrollable'; +import type { ComponentType } from 'inferno'; +import { Component } from 'inferno'; + +import { ColumnSortable } from './column_sortable'; +import { CLASSES as itemClasses, Item } from './item'; +import type { DraggingOptions } from './options'; + +export const CLASSES = { + headers: 'dx-cardview-headers', + content: 'dx-cardview-headerpanel-content', +}; + +export interface HeaderPanelProps { + columns: Column[]; + + onMove: (column: Column, toIndex: number) => void; + + allowColumnReordering: boolean; + + showSortIndexes: boolean; + + onSortClick: (column: Column) => void; + + itemTemplate?: ComponentType<{ column: Column }>; + + itemCssClass?: string; + + visible: boolean; + + draggingOptions?: DraggingOptions; +} + +export class HeaderPanel extends Component { + public render(): JSX.Element { + if (!this.props.visible) { + return <>; + } + + return ( +
+ this.props.onMove?.(column, index)} + filter={`.${itemClasses.item}`} + dragTemplate={Item} + > + +
+ {this.props.columns.map((column) => ( + { this.props.onSortClick(column); }} + template={this.props.itemTemplate} + cssClass={this.props.itemCssClass} + /> + ))} +
+
+
+
+ ); + } +} diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts new file mode 100644 index 000000000000..66aec0658fc2 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/index.ts @@ -0,0 +1,2 @@ +export * from './options'; +export { HeaderPanelView as View } from './view'; diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx new file mode 100644 index 000000000000..c629938c7f5c --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.test.tsx @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { describe, expect, it } from '@jest/globals'; +import { render } from 'inferno'; + +import { normalizeColumn } from '../../grid_core/columns_controller/columns_controller.mock'; +import type { ItemProps } from './item'; +import { Item } from './item'; + +const setup = (props: ItemProps) => { + const rootElement = document.createElement('div'); + render( + , + rootElement, + ); + + return rootElement; +}; + +describe('Item', () => { + it('should use column caption as text', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'my column data field', + caption: 'my column caption', + }), + }); + + expect(el).toMatchSnapshot(); + }); + + it('should render sort icons', () => { + const el = setup({ + column: normalizeColumn({ + dataField: 'column1', + sortIndex: 0, + sortOrder: 'asc', + }), + }); + + expect(el).toMatchSnapshot(); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx new file mode 100644 index 000000000000..8ad719adc970 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/card_view/header_panel/item.tsx @@ -0,0 +1,85 @@ +import type { Column } from '@ts/grids/new/grid_core/columns_controller/types'; +import type { ComponentType } from 'inferno'; + +import type { Status } from './column_sortable'; + +export const CLASSES = { + item: 'dx-cardview-header-item', + button: 'dx-cardview-header-item-button', + sorting: { + container: 'dx-cardview-header-item-sorting', + order: 'dx-cardview-header-item-sorting-order', + }, +}; + +// TODO: extract icons to separate component +const ICONS = { + // TODO: move to dx-icon once they are updated + forbid: ( + + + + ), + // TODO: move to dx-icon once they are updated + moving: ( + + + + ), + sortUp: , + sortDown: , +}; + +interface SortIconProps { + sortOrder: 'asc' | 'desc'; + sortIndex: number; + showSortIndex: boolean; +} + +function SortIcon(props: SortIconProps): JSX.Element { + return ( + + {props.sortOrder === 'asc' && ICONS.sortUp} + {props.sortOrder === 'desc' && ICONS.sortDown} + { + props.showSortIndex && ( + + {props.sortIndex} + + ) + } + + ); +} + +export interface ItemProps { + column: Column; + status?: Status; + showSortIndexes?: boolean; + onSortClick?: () => void; + template?: ComponentType<{ column: Column }>; + cssClass?: string; +} + +export function Item(props: ItemProps): JSX.Element { + const Template = props.column.headerItemTemplate ?? props.template; + const cssClass = `${CLASSES.item} ${props.column.headerItemCssClass ?? ''} ${props.cssClass ?? ''}`; + + return ( +
+ { props.status && ICONS[props.status]} + { Template &&