diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a19e4c9def..f1d6cc2f68e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ All notable changes for each version of this project will be documented in this - The collapse/expand icons have new orientantion to display the action that will be performed when clicked. When an icon points up clicking on it would result in collapsing the related group row and when it points down clicking on it would expand the group row. - The collapse/expand all icons have also been updated to reflect the new group row icons better. - Group rows now can be expanded/collapsed using Alt + Arrow Up/Down to reflect the new icons. + - `filterMode` input added, which determines the filtering ui of the grid. The default value is `quickFilter`. Other possible value is `excelStyle`, which mimics the filtering in Excel with added functionality for column moving, sorting, hiding and pinning. + - `IgxColumnComponent` now has `disablePinning` property, which determines wether the column can be pinned from + the toolbar and whether the column pin will be available in the excel style filter menu. The `disableHiding` input will be used to show/hide the column hiding functionality in the menu. - `igxTreeGrid` - The collapse/expand icons have new orientantion to display the action that will be performed when clicked. When an icon points up clicking on it would result in collapsing the related tree grid level and when it points down clicking on it would expand the tree grid level. - Expanding/collapsing tree levels can now be performed also by using Alt + Arrow Up/Down to reflect the new icons. diff --git a/projects/igniteui-angular/src/lib/calendar/calendar.component.ts b/projects/igniteui-angular/src/lib/calendar/calendar.component.ts index 35d34820746..1c26d6094a8 100644 --- a/projects/igniteui-angular/src/lib/calendar/calendar.component.ts +++ b/projects/igniteui-angular/src/lib/calendar/calendar.component.ts @@ -341,7 +341,9 @@ export class IgxCalendarComponent extends IgxMonthPickerBase { super.activeViewDecade(); requestAnimationFrame(() => { - this.dacadeView.el.nativeElement.focus(); + if (this.dacadeView) { + this.dacadeView.el.nativeElement.focus(); + } }); } diff --git a/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts b/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts index 03ff58b3a3b..2b1371844b3 100644 --- a/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo-dropdown.component.ts @@ -203,7 +203,8 @@ export class IgxComboDropDownComponent extends IgxDropDownComponent implements I this.subscribeNext(vContainer, () => { // children = all items in the DD (including addItemButton) // length - 2 instead of -1, because we do not want to focus the last loaded item (in DOM, but not visible) - super.navigateItem(children[children.length - 2 - extraScroll].itemIndex); // Focus last item (excluding Add Button) + // Focus last item (excluding Add Button) + super.navigateItem(!addedIndex ? children[children.length - 1 - extraScroll].itemIndex : this.items.length - 2); }); vContainer.scrollTo(targetDataIndex); // Perform virtual scroll } diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.ts b/projects/igniteui-angular/src/lib/combo/combo.component.ts index 1f89c95bdf6..389b5b18703 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.component.ts @@ -39,6 +39,7 @@ import { DisplayDensityBase, DisplayDensityToken, IDisplayDensityOptions } from import { IGX_COMBO_COMPONENT, IgxComboBase } from './combo.common'; import { IgxComboAddItemComponent } from './combo-add-item.component'; import { IgxComboAPIService } from './combo.api'; +import { take } from 'rxjs/operators'; /** Custom strategy to provide the combo with callback on initial positioning */ class ComboConnectedPositionStrategy extends ConnectedPositioningStrategy { @@ -1069,12 +1070,27 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas * @hidden */ public handleInputChange(event?: string) { + let cdrFlag = false; + const vContainer = this.dropdown.verticalScrollContainer; if (event !== undefined) { - this.dropdown.verticalScrollContainer.scrollTo(0); + // Do not scroll if not scrollable + if (vContainer.isScrollable()) { + vContainer.scrollTo(0); + } else { + cdrFlag = true; + } this.onSearchInput.emit(event); } if (this.filterable) { this.filter(); + // If there was no scroll before filtering, check if there is after and detect changes + if (cdrFlag) { + vContainer.onChunkLoad.pipe(take(1)).subscribe(() => { + if (vContainer.isScrollable()) { + this.cdr.detectChanges(); + } + }); + } } else { this.checkMatch(); } @@ -1342,7 +1358,7 @@ export class IgxComboComponent extends DisplayDensityBase implements IgxComboBas /** * @hidden */ - public registerOnTouched(fn: any): void {} + public registerOnTouched(fn: any): void { } /** * @hidden diff --git a/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts b/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts index 6b66e5e35cf..20c9a414fe8 100644 --- a/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts +++ b/projects/igniteui-angular/src/lib/core/i18n/grid-resources.ts @@ -38,6 +38,7 @@ export interface IGridResourceStrings { igx_grid_filter_true?: string; igx_grid_filter_false?: string; igx_grid_filter_all?: string; + igx_grid_filter_condition_placeholder?: string; igx_grid_summary_count?: string; igx_grid_summary_min?: string; igx_grid_summary_max?: string; @@ -45,6 +46,28 @@ export interface IGridResourceStrings { igx_grid_summary_average?: string; igx_grid_summary_earliest?: string; igx_grid_summary_latest?: string; + igx_grid_excel_filter_moving_left?: string; + igx_grid_excel_filter_moving_right?: string; + igx_grid_excel_filter_moving_header?: string; + igx_grid_excel_filter_sorting_asc?: string; + igx_grid_excel_filter_sorting_desc?: string; + igx_grid_excel_filter_sorting_header?: string; + igx_grid_excel_custom_dialog_add?: string; + igx_grid_excel_custom_dialog_clear?: string; + igx_grid_excel_custom_dialog_header?: string; + igx_grid_excel_cancel?: string; + igx_grid_excel_apply?: string; + igx_grid_excel_search_placeholder?: string; + igx_grid_excel_select_all?: string; + igx_grid_excel_blanks?: string; + igx_grid_excel_hide?: string; + igx_grid_excel_pin?: string; + igx_grid_excel_unpin?: string; + igx_grid_excel_text_filter?: string; + igx_grid_excel_number_filter?: string; + igx_grid_excel_date_filter?: string; + igx_grid_excel_boolean_filter?: string; + igx_grid_excel_custom_filter?: string; } export const GridResourceStringsEN: IGridResourceStrings = { @@ -87,6 +110,7 @@ export const GridResourceStringsEN: IGridResourceStrings = { igx_grid_filter_true: 'True', igx_grid_filter_false: 'False', igx_grid_filter_all: 'All', + igx_grid_filter_condition_placeholder: 'Select filter', igx_grid_summary_count: 'Count', igx_grid_summary_min: 'Min', igx_grid_summary_max: 'Max', @@ -94,4 +118,26 @@ export const GridResourceStringsEN: IGridResourceStrings = { igx_grid_summary_average: 'Avg', igx_grid_summary_earliest: 'Earliest', igx_grid_summary_latest: 'Latest', + igx_grid_excel_filter_moving_left: 'left', + igx_grid_excel_filter_moving_right: 'right', + igx_grid_excel_filter_moving_header: 'column moving', + igx_grid_excel_filter_sorting_asc: 'ascending', + igx_grid_excel_filter_sorting_desc: 'descending', + igx_grid_excel_filter_sorting_header: 'sorting', + igx_grid_excel_custom_dialog_add: 'add filter', + igx_grid_excel_custom_dialog_clear: 'clear filter', + igx_grid_excel_custom_dialog_header: 'Custom auto-filter on column: ', + igx_grid_excel_cancel: 'cancel', + igx_grid_excel_apply: 'apply', + igx_grid_excel_search_placeholder: 'Search', + igx_grid_excel_select_all: 'Select All', + igx_grid_excel_blanks: '(Blanks)', + igx_grid_excel_hide: 'Hide column', + igx_grid_excel_pin: 'Pin column', + igx_grid_excel_unpin: 'Unpin column', + igx_grid_excel_text_filter: 'Text filter', + igx_grid_excel_number_filter: 'Number filter', + igx_grid_excel_date_filter: 'Date filter', + igx_grid_excel_boolean_filter: 'Boolean filter', + igx_grid_excel_custom_filter: 'Custom filter...' }; diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss new file mode 100644 index 00000000000..a3d8dbf5456 --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-component.scss @@ -0,0 +1,97 @@ +//// +/// @access private +/// @group components +/// @author Simeon Simeonoff +/// @requires {mixin} bem-block +/// @requires {mixin} bem-elem +/// @requires {mixin} bem-mod +//// + +@mixin _excel-filtering-partial { + @include b(igx-excel-filter) { + @extend %grid-excel-filter !optional; + + @include e(menu) { + @extend %grid-excel-menu !optional; + } + + @include e(icon) { + @extend %grid-excel-icon !optional; + } + + @include e(icon, $m: 'filtered') { + @extend %grid-excel-icon !optional; + @extend %grid-excel-icon--filtered !optional; + } + + @include e(menu-header) { + @extend %grid-excel-menu__header !optional; + } + + @include e(menu-main) { + @extend %grid-excel-main !optional; + } + + @include e(menu-footer) { + @extend %grid-excel-menu__footer !optional; + } + + @include e(sort) { + @extend %grid-excel-sort !optional; + } + + @include e(move) { + @extend %grid-excel-move !optional; + } + + @include e(move-buttons) { + @extend %grid-excel-move__buttons !optional; + } + + @include e(actions) { + @extend %grid-excel-actions !optional; + } + + @include e(actions-pin) { + @extend %grid-excel-actions__action !optional; + } + + @include e(actions-unpin) { + @extend %grid-excel-actions__action !optional; + } + + @include e(actions-hide) { + @extend %grid-excel-actions__action !optional; + } + + @include e(actions-filter) { + @extend %grid-excel-actions__action !optional; + } + + @include e(secondary) { + @extend %grid-excel-menu__secondary !optional; + } + + @include e(secondary-header) { + @extend %grid-excel-menu__header !optional; + @extend %grid-excel-menu__secondary-header !optional; + } + + @include e(secondary-main) { + @extend %grid-excel-menu__secondary-main !optional; + } + + @include e(secondary-footer) { + @extend %grid-excel-menu__footer !optional; + @extend %grid-excel-menu__secondary-footer !optional; + } + + @include e(condition) { + @extend %grid-excel-menu__condition !optional; + } + + @include e(add-filter) { + @extend %grid-excel-menu__add-filter !optional; + } + } +} diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss new file mode 100644 index 00000000000..3dd34fba78d --- /dev/null +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_excel-filtering-theme.scss @@ -0,0 +1,212 @@ +//// +/// @group themes +/// @access private +/// @author Simeon Simeonoff +/// @author Marin Popov +//// + +/// @param {Map} $theme - The grid theme used to style the component. +/// @param {Map} $palette - The palette map used as base for the component. +/// @requires igx-color +/// @requires rem +/// @requires --var +@mixin _excel-filtering($theme, $palette) { + %grid-excel-filter { + height: rem(15px); + } + + %grid-excel-icon { + cursor: pointer; + + &.igx-icon { + width: rem(15px); + height: rem(15px); + font-size: rem(15px); + } + } + + %grid-excel-icon--filtered { + &.igx-icon { + color: igx-color($palette, 'secondary'); + } + } + + %grid-excel-menu { + width: 320px; + min-width: 320px; + background: --var($theme, 'filtering-row-background'); + box-shadow: igx-elevation($elevations, 12); + border-radius: 4px; + + @include igx-button-group(igx-button-group-theme( + $shadow: none, + $schema: $dark-schema, + $item-background: --var($theme, 'filtering-row-background'), + $item-hover-background: igx-color($palette, 'grays', 100), + $item-selected-background: igx-color($palette, 'grays', 100), + $item-text-color: igx-color($palette, 'grays', 700), + $item-hover-text-color: igx-color($palette, 'grays', 800), + $item-selected-text-color: igx-color($palette, 'secondary', 500), + $item-border-color: transparent, + $item-selected-border-color: transparent + )); + } + + %grid-excel-menu__header { + padding: rem(16px); + color: igx-color($palette, 'grays', 700); + } + + %grid-excel-menu__footer { + display: flex; + justify-content: space-between; + padding: rem(8px) rem(16px); + + [igxButton] { + flex-grow: 1; + + + [igxButton] { + margin-left: rem(16px); + } + } + } + + %grid-excel-sort { + display: block; + padding: rem(8px) rem(16px); + + header { + color: igx-color($palette, 'grays', 700); + margin-bottom: rem(4px); + } + + igx-icon { + font-size: rem(18px); + width: rem(18px); + height: rem(18px); + margin-right: rem(8px); + } + } + + %grid-excel-actions { + padding: rem(8px) rem(16px); + } + + %grid-excel-move { + margin-bottom: rem(8px); + + header { + color: igx-color($palette, 'grays', 700); + margin-bottom: rem(4px); + } + } + + %grid-excel-move__buttons { + display: flex; + justify-content: space-between; + + [igxButton] { + flex-grow: 1; + + + [igxButton] { + margin-left: rem(8px); + } + } + + igx-icon { + font-size: rem(18px); + width: rem(18px); + height: rem(18px); + } + + igx-icon + span, + span + igx-icon { + margin-left: rem(8px); + } + } + + %grid-excel-actions__action { + display: flex; + align-items: center; + justify-content: space-between; + padding: rem(8px) rem(16px); + margin: 0 -#{rem(16px)}; + cursor: pointer; + color: igx-color($palette, 'grays', 700); + outline-style: none; + + &:hover, + &:focus { + background: igx-color($palette, 'grays', 100); + } + } + + %grid-excel-main { + display: block; + padding: 0 rem(16px); + + igx-list { + margin: rem(8px) -#{rem(16px)} 0; + border-top: 1px dashed igx-color($palette, 'grays', 300); + border-bottom: 1px dashed igx-color($palette, 'grays', 300); + } + } + + %grid-excel-menu__secondary { + width: 520px; + min-width: 520px; + background: --var($theme, 'filtering-row-background'); + box-shadow: igx-elevation($elevations, 12); + border-radius: rem(4px); + } + + %grid-excel-menu__secondary-header { + border-bottom: 1px solid igx-color($palette, 'grays', 300); + } + + %grid-excel-menu__secondary-main { + height: 232px; + overflow: auto; + } + + %grid-excel-menu__secondary-footer { + border-top: 1px dashed igx-color($palette, 'grays', 300); + + [igxButton] { + flex-grow: 0; + } + } + + %grid-excel-menu__condition { + display: flex; + flex-wrap: wrap; + align-items: center; + padding: 0 rem(16px); + + igx-input-group { + flex-grow: 1; + flex-basis: 40%; + margin: rem(16px) 0; + + ~ igx-input-group, + ~ igx-date-picker { + margin-left: rem(16px); + } + } + + [igxButton='icon'] { + margin-left: rem(16px); + } + } + + %grid-excel-menu__add-filter { + margin: 0 rem(16px) rem(16px); + + igx-icon { + width: rem(18px); + height: rem(18px); + font-size: rem(18px); + margin-right: rem(8px); + } + } +} diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss index 7f8a8a5c8f2..e6d80b7a6e5 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-component.scss @@ -1,9 +1,12 @@ +@import './excel-filtering-component'; + //// /// @group components /// @author Simeon Simeonoff /// @requires {mixin} bem-block /// @requires {mixin} bem-elem /// @requires {mixin} bem-mod +/// @requires {mixin} _excel-filtering-partial //// @include b(igx-grid) { $this: bem--selector-to-string(&); @@ -621,6 +624,8 @@ } } } + + @include _excel-filtering-partial(); } @include b(igx-drop-area) { diff --git a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss index 526704576a6..4917246f3ca 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/grid/_grid-theme.scss @@ -1,3 +1,5 @@ +@import './excel-filtering-theme'; + //// /// @group themes /// @access public @@ -417,6 +419,7 @@ /// @param {Map} $theme - The theme used to style the component. /// @requires {mixin} igx-root-css-vars /// @requires {mixin} ellipsis +/// @requires {mixin} _excel-filtering /// @requires igx-color /// @requires igx-contrast-color /// @requires rem @@ -2020,4 +2023,6 @@ margin-right: calc(#{map-get($hierarchical-grid-indent, 'compact')} + 18px); } } + + @include _excel-filtering($theme, $palette); } diff --git a/projects/igniteui-angular/src/lib/core/styles/components/input/_input-group-theme.scss b/projects/igniteui-angular/src/lib/core/styles/components/input/_input-group-theme.scss index e10f1025d12..6353bd0d051 100644 --- a/projects/igniteui-angular/src/lib/core/styles/components/input/_input-group-theme.scss +++ b/projects/igniteui-angular/src/lib/core/styles/components/input/_input-group-theme.scss @@ -691,7 +691,7 @@ display: inline-flex; align-items: center; height: rem(32px, map-get($base-scale-size, 'comfortable')); - transition: all $transition-timing; + transition: color $transition-timing; } %form-group-prefix { diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts index bddf4eccd50..b5b48ccde30 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.ts @@ -14,7 +14,8 @@ import { TemplateRef, Inject, ChangeDetectorRef, - HostListener + HostListener, + NgModuleRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { @@ -392,7 +393,7 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor } constructor(@Inject(IgxOverlayService) private _overlayService: IgxOverlayService, - private cdr: ChangeDetectorRef) { } + private _cdr: ChangeDetectorRef, private _moduleRef: NgModuleRef) { } /** * Gets the input group template. @@ -728,7 +729,7 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor */ public writeValue(value: Date) { this.value = value; - this.cdr.markForCheck(); + this._cdr.markForCheck(); } /** @@ -880,7 +881,7 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor case DatePickerInteractionMode.READONLY: { this.hasHeader = true; const modalOverlay = (this.modalOverlaySettings !== undefined) ? this._modalOverlay : this._modalOverlaySettings; - this._componentID = this._overlayService.attach(IgxCalendarContainerComponent, modalOverlay); + this._componentID = this._overlayService.attach(IgxCalendarContainerComponent, modalOverlay, this._moduleRef); this._overlayService.show(this._componentID, modalOverlay); break; } @@ -889,7 +890,7 @@ export class IgxDatePickerComponent implements IDatePicker, ControlValueAccessor const dropDownOverlay = (this.dropDownOverlaySettings !== undefined) ? this._dropDownOverlay : this._dropDownOverlaySettings; dropDownOverlay.positionStrategy.settings.target = this.editableInputGroup.nativeElement; - this._componentID = this._overlayService.attach(IgxCalendarContainerComponent, dropDownOverlay); + this._componentID = this._overlayService.attach(IgxCalendarContainerComponent, dropDownOverlay, this._moduleRef); this._overlayService.show(this._componentID, dropDownOverlay); break; } diff --git a/projects/igniteui-angular/src/lib/directives/filter/filter.directive.ts b/projects/igniteui-angular/src/lib/directives/filter/filter.directive.ts index 62ed397c59e..59d2a9ef7f5 100644 --- a/projects/igniteui-angular/src/lib/directives/filter/filter.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/filter/filter.directive.ts @@ -31,7 +31,7 @@ export class IgxFilterOptions { public get_value(item: any, key: string): string { let result = ''; - if (key) { + if (key && item[key]) { result = item[key].toString(); } else if (item.element) { if (item.element.nativeElement) { diff --git a/projects/igniteui-angular/src/lib/directives/for-of/virtual.helper.component.ts b/projects/igniteui-angular/src/lib/directives/for-of/virtual.helper.component.ts index 93241048346..50fc1fb1926 100644 --- a/projects/igniteui-angular/src/lib/directives/for-of/virtual.helper.component.ts +++ b/projects/igniteui-angular/src/lib/directives/for-of/virtual.helper.component.ts @@ -5,6 +5,9 @@ import { Component, ElementRef, HostBinding, Input, ViewChild, ViewContainerRef, template: '
' }) export class VirtualHelperComponent implements OnDestroy { + @HostBinding('scrollTop') + public scrollTop; + @ViewChild('container', { read: ViewContainerRef }) public _vcr; @Input() public itemsLength: number; public height: number; diff --git a/projects/igniteui-angular/src/lib/grids/README.md b/projects/igniteui-angular/src/lib/grids/README.md index bddbdb1c4d9..0469f604604 100644 --- a/projects/igniteui-angular/src/lib/grids/README.md +++ b/projects/igniteui-angular/src/lib/grids/README.md @@ -170,6 +170,7 @@ Below is the list of all inputs that the developers may set to configure the gri |`page`| number | The current page index.| |`perPage`|number|Visible items per page, default is 15| |`allowFiltering`| boolean | Enables quick filtering functionality in the grid. | +|`filterMode`| `FilterMode` | Determines the filter mode, default value is `quickFilter`.| |`filteringLogic`| FilteringLogic | The filtering logic of the grid. Defaults to _AND_. | |`filteringExpressionsTree`| IFilteringExpressionsTree | The filtering state of the grid. | |`emptyFilteredGridMessage`| string | The message displayed when there are no records and the grid is filtered.| @@ -329,6 +330,8 @@ Inputs available on the **IgxGridColumnComponent** to define columns: |`pinned`|boolean|Set column to be pinned or not| |`searchable`|boolean|Determines whether the column is included in the search. If set to false, the cell values for this column will not be included in the results of the search API of the grid (defaults to true)| |`groupable`|boolean| Determines whether the column may be grouped via the UI.| +|`disableHiding`|boolean| Enables/disables hiding for the column, default value is `false`.| +|`disablePinning`|boolean| Enables/disables pinning for the column, default value is `false`.| ### Methods diff --git a/projects/igniteui-angular/src/lib/grids/column-pinning.component.ts b/projects/igniteui-angular/src/lib/grids/column-pinning.component.ts index ad744193f6c..28fd0ca562b 100644 --- a/projects/igniteui-angular/src/lib/grids/column-pinning.component.ts +++ b/projects/igniteui-angular/src/lib/grids/column-pinning.component.ts @@ -47,7 +47,7 @@ export class IgxColumnPinningComponent extends ColumnChooserBase { * @hidden */ createColumnItem(container: any, column: any) { - if (column.level !== 0) { + if (column.level !== 0 || column.disablePinning) { return null; } const item = new IgxColumnPinningItemDirective(); diff --git a/projects/igniteui-angular/src/lib/grids/column.component.ts b/projects/igniteui-angular/src/lib/grids/column.component.ts index 4367094a2be..0174d1a327a 100644 --- a/projects/igniteui-angular/src/lib/grids/column.component.ts +++ b/projects/igniteui-angular/src/lib/grids/column.component.ts @@ -231,6 +231,31 @@ export class IgxColumnComponent implements AfterContentInit { this.check(); } } + /** + * Gets whether the pinning is disabled. + * ```typescript + * let isPinningDisabled = this.column.disablePinning; + * ``` + * @memberof IgxColumnComponent + */ + @Input() + get disablePinning(): boolean { + return this._disablePinning; + } + /** + * Enables/disables pinning for the column. + * Default value is `false`. + * ```typescript + * + * ``` + * @memberof IgxColumnComponent + */ + set disablePinning(value: boolean) { + if (this._disablePinning !== value) { + this._disablePinning = value; + this.check(); + } + } /** * Sets/gets whether the column is movable. * Default value is `false`. @@ -852,6 +877,10 @@ export class IgxColumnComponent implements AfterContentInit { *@hidden */ protected _disableHiding = false; + /** + *@hidden + */ + protected _disablePinning = false; /** *@hidden */ @@ -859,7 +888,7 @@ export class IgxColumnComponent implements AfterContentInit { /** *@hidden */ - protected _defaultMinWidth = '64'; + protected _defaultMinWidth = '80'; /** *@hidden */ diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-column-moving.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-column-moving.component.html new file mode 100644 index 00000000000..374b9c227fa --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-column-moving.component.html @@ -0,0 +1,17 @@ +
{{ grid.resourceStrings.igx_grid_excel_filter_moving_header }}
+
+ + +
diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-column-moving.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-column-moving.component.ts new file mode 100644 index 00000000000..e840ae13f6a --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-column-moving.component.ts @@ -0,0 +1,36 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; +import { IgxColumnComponent } from '../../column.component'; +import { IgxGridBaseComponent } from '../../grid'; + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-excel-style-column-moving', + templateUrl: './excel-style-column-moving.component.html' +}) +export class IgxExcelStyleColumnMovingComponent { + + @Input() + public column: IgxColumnComponent; + + @Input() + public grid: IgxGridBaseComponent; + + constructor() {} + + get canMoveLeft() { + return !this.column.movable || this.column.visibleIndex === 0; + } + + get canMoveRight() { + return !this.column.movable || this.column.visibleIndex === this.grid.columns.length - 1; + } + + public onMoveButtonClicked(moveDirection) { + const index = this.column.visibleIndex; + this.grid.moveColumn(this.column, this.grid.columns[index + moveDirection]); + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-custom-dialog.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-custom-dialog.component.html new file mode 100644 index 00000000000..9bdc6997d8e --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-custom-dialog.component.html @@ -0,0 +1,57 @@ +
+
+

+ {{ grid.resourceStrings.igx_grid_excel_custom_dialog_header }}{{ column.field }} +

+
+ +
+ + + + + + + + + + + +
+ +
+ + +
+ + +
+
+
diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-custom-dialog.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-custom-dialog.component.ts new file mode 100644 index 00000000000..db212aa2eab --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-custom-dialog.component.ts @@ -0,0 +1,245 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + ChangeDetectorRef, + ViewChild, + AfterViewInit, + TemplateRef, + ViewChildren, + QueryList +} from '@angular/core'; +import { IgxColumnComponent } from '../../column.component'; +import { IgxFilteringService, ExpressionUI } from '../grid-filtering.service'; +import { FilteringLogic } from '../../../data-operations/filtering-expression.interface'; +import { DataType } from '../../../data-operations/data-util'; +import { + IgxStringFilteringOperand, + IgxBooleanFilteringOperand, + IgxNumberFilteringOperand, + IgxDateFilteringOperand +} from '../../../data-operations/filtering-condition'; +import { IgxToggleDirective } from '../../../directives/toggle/toggle.directive'; +import { + ConnectedPositioningStrategy, + CloseScrollStrategy, + OverlaySettings, + VerticalAlignment, + PositionSettings, + HorizontalAlignment, + IgxOverlayService +} from '../../../services'; +import { ILogicOperatorChangedArgs, IgxExcelStyleDefaultExpressionComponent } from './excel-style-default-expression.component'; +import { KEYS } from '../../../core/utils'; + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-excel-style-custom-dialog', + templateUrl: './excel-style-custom-dialog.component.html' +}) +export class IgxExcelStyleCustomDialogComponent implements AfterViewInit { + + @Input() + public expressionsList = new Array(); + + private _customDialogPositionSettings: PositionSettings = { + verticalDirection: VerticalAlignment.Middle, + horizontalDirection: HorizontalAlignment.Center, + horizontalStartPoint: HorizontalAlignment.Center, + verticalStartPoint: VerticalAlignment.Middle + }; + + private _customDialogOverlaySettings: OverlaySettings = { + closeOnOutsideClick: true, + modal: false, + positionStrategy: new ConnectedPositioningStrategy(this._customDialogPositionSettings), + scrollStrategy: new CloseScrollStrategy() + }; + + @Input() + public column: IgxColumnComponent; + + @Input() + public selectedOperator: string; + + @Input() + public columnData: any[]; + + @Input() + public filteringService: IgxFilteringService; + + @Input() + public overlayComponentId: string; + + @Input() + public overlayService: IgxOverlayService; + + + @ViewChildren(IgxExcelStyleDefaultExpressionComponent) + private expressionComponents: QueryList; + + @ViewChild('toggle', { read: IgxToggleDirective }) + public toggle: IgxToggleDirective; + + @ViewChild('defaultExpressionTemplate', { read: TemplateRef }) + protected defaultExpressionTemplate: TemplateRef; + + @ViewChild('dateExpressionTemplate', { read: TemplateRef }) + protected dateExpressionTemplate: TemplateRef; + + constructor(private cdr: ChangeDetectorRef) {} + + ngAfterViewInit(): void { + this._customDialogOverlaySettings.outlet = this.grid.outletDirective; + } + + get template(): TemplateRef { + if (this.column.dataType === DataType.Date) { + return this.dateExpressionTemplate; + } + + return this.defaultExpressionTemplate; + } + + get grid() { + return this.filteringService.grid; + } + + public onCustomDialogOpening() { + if (!this.column.filteringExpressionsTree) { + this.createInitialExpressionUIElement(); + } + } + + public onCustomDialogOpened() { + if (this.expressionComponents.first) { + this.expressionComponents.first.focus(); + } + } + + public open() { + this._customDialogOverlaySettings.positionStrategy.settings.target = this.grid.nativeElement; + this.toggle.open(this._customDialogOverlaySettings); + } + + public onClearButtonClick() { + this.filteringService.clearFilter(this.column.field); + this.createInitialExpressionUIElement(); + this.cdr.detectChanges(); + } + + public closeDialog() { + if (this.overlayComponentId) { + this.overlayService.hide(this.overlayComponentId); + } + } + + public onApplyButtonClick() { + this.expressionsList = this.expressionsList.filter( + element => element.expression.condition && (element.expression.searchVal || element.expression.condition.isUnary)); + this.filteringService.filter(this.column.field, this.expressionsList); + this.closeDialog(); + } + + public onAddButtonClick() { + const exprUI = new ExpressionUI(); + exprUI.expression = { + condition: null, + fieldName: this.column.field, + ignoreCase: this.column.filteringIgnoreCase, + searchVal: null + }; + + this.expressionsList[this.expressionsList.length - 1].afterOperator = FilteringLogic.And; + exprUI.beforeOperator = this.expressionsList[this.expressionsList.length - 1].afterOperator; + + this.expressionsList.push(exprUI); + + this.markChildrenForCheck(); + } + + public onExpressionRemoved(event: ExpressionUI) { + const indexToRemove = this.expressionsList.indexOf(event); + + if (indexToRemove === 0 && this.expressionsList.length > 1) { + this.expressionsList[1].beforeOperator = null; + } else if (indexToRemove === this.expressionsList.length - 1) { + this.expressionsList[indexToRemove - 1].afterOperator = null; + } else { + this.expressionsList[indexToRemove - 1].afterOperator = this.expressionsList[indexToRemove + 1].beforeOperator; + this.expressionsList[0].beforeOperator = null; + this.expressionsList[this.expressionsList.length - 1].afterOperator = null; + } + + this.expressionsList.splice(indexToRemove, 1); + + this.markChildrenForCheck(); + } + + public onLogicOperatorChanged(event: ILogicOperatorChangedArgs) { + const index = this.expressionsList.indexOf(event.target); + event.target.afterOperator = event.newValue; + if (index + 1 < this.expressionsList.length) { + this.expressionsList[index + 1].beforeOperator = event.newValue; + } + } + + public onKeyDown(eventArgs) { + eventArgs.stopPropagation(); + } + + public onApplyButtonKeyDown(eventArgs) { + if (eventArgs.key === KEYS.TAB && !eventArgs.shiftKey) { + eventArgs.stopPropagation(); + eventArgs.preventDefault(); + } + } + + private createCondition(conditionName: string) { + switch (this.column.dataType) { + case DataType.Boolean: + return IgxBooleanFilteringOperand.instance().condition(conditionName); + case DataType.Number: + return IgxNumberFilteringOperand.instance().condition(conditionName); + case DataType.Date: + return IgxDateFilteringOperand.instance().condition(conditionName); + default: + return IgxStringFilteringOperand.instance().condition(conditionName); + } + } + + private markChildrenForCheck() { + this.expressionComponents.forEach(x => x.cdr.markForCheck()); + } + + private createInitialExpressionUIElement() { + this.expressionsList = []; + const firstExprUI = new ExpressionUI(); + + firstExprUI.expression = { + condition: this.createCondition(this.selectedOperator), + fieldName: this.column.field, + ignoreCase: this.column.filteringIgnoreCase, + searchVal: null + }; + firstExprUI.afterOperator = FilteringLogic.And; + + this.expressionsList.push(firstExprUI); + + const secondExprUI = new ExpressionUI(); + secondExprUI.expression = { + condition: null, + fieldName: this.column.field, + ignoreCase: this.column.filteringIgnoreCase, + searchVal: null + }; + + secondExprUI.beforeOperator = FilteringLogic.And; + + this.expressionsList.push(secondExprUI); + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-date-expression.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-date-expression.component.html new file mode 100644 index 00000000000..a160b9b3bc5 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-date-expression.component.html @@ -0,0 +1,106 @@ + + + + {{ translateCondition(condition) }} + + + + + + + + filter_list + + + + + + +
+ + + {{ column.formatter ? column.formatter(item) : (item | igxdate: column.grid.locale) }} + + +
+
+ + + + + calendar + + + arrow_drop_down + + + + + + + + + + {{ grid.resourceStrings.igx_grid_filter_operator_and }} + + + + {{ grid.resourceStrings.igx_grid_filter_operator_or }} + + diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-date-expression.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-date-expression.component.ts new file mode 100644 index 00000000000..58d5b89d5ae --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-date-expression.component.ts @@ -0,0 +1,43 @@ +import { + Component, + ChangeDetectionStrategy, + ViewChild +} from '@angular/core'; +import { IgxExcelStyleDefaultExpressionComponent } from './excel-style-default-expression.component'; +import { IgxDatePickerComponent } from '../../../date-picker/date-picker.component'; +import { KEYS } from '../../../core/utils'; +import { IgxInputGroupComponent } from '../../../input-group'; + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-excel-style-date-expression', + templateUrl: './excel-style-date-expression.component.html' +}) +export class IgxExcelStyleDateExpressionComponent extends IgxExcelStyleDefaultExpressionComponent { + + @ViewChild('datePicker', { read: IgxDatePickerComponent }) + private datePicker: IgxDatePickerComponent; + + @ViewChild('inputGroupDateValues', { read: IgxInputGroupComponent }) + private inputGroupDateValues: IgxInputGroupComponent; + + protected get inputValuesElement() { + return this.datePicker.getEditElement(); + } + + public openDatePicker(openDialog: Function) { + openDialog(); + } + + public onInputValuesKeydown(event: KeyboardEvent) { + if (event.altKey && (event.key === KEYS.DOWN_ARROW || event.key === KEYS.DOWN_ARROW_IE)) { + this.toggleCustomDialogDropDown(this.inputGroupDateValues, this.dropdownValues); + } + + event.stopPropagation(); + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-default-expression.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-default-expression.component.html new file mode 100644 index 00000000000..569ac001f52 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-default-expression.component.html @@ -0,0 +1,106 @@ + + + + {{ translateCondition(condition) }} + + + + + + + + filter_list + + + + + + +
+ + + + {{ column.formatter ? column.formatter(item) : column.dataType === 'number' ? (item | igxdecimal: column.grid.locale) : item }} + + + +
+ +
+ + + + + + arrow_drop_down + + + + + + + + {{ grid.resourceStrings.igx_grid_filter_operator_and }} + + + + {{ grid.resourceStrings.igx_grid_filter_operator_or }} + + diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-default-expression.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-default-expression.component.ts new file mode 100644 index 00000000000..219e65180e6 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-default-expression.component.ts @@ -0,0 +1,302 @@ +import { + Component, + ChangeDetectionStrategy, + AfterViewInit, + Input, + Output, + EventEmitter, + ChangeDetectorRef, + ViewChild, + OnDestroy +} from '@angular/core'; +import { IgxColumnComponent } from '../../column.component'; +import { ExpressionUI } from '../grid-filtering.service'; +import { IgxButtonGroupComponent } from '../../../buttonGroup/buttonGroup.component'; +import { IgxDropDownItemComponent, IgxDropDownComponent } from '../../../drop-down'; +import { IgxInputGroupComponent, IgxInputDirective } from '../../../input-group'; +import { DataType } from '../../../data-operations/data-util'; +import { IFilteringOperation } from '../../../data-operations/filtering-condition'; +import { OverlaySettings, ConnectedPositioningStrategy, CloseScrollStrategy } from '../../../services'; +import { KEYS } from '../../../core/utils'; +import { FilteringLogic } from '../../../data-operations/filtering-expression.interface'; +import { IgxGridBaseComponent } from '../../grid'; +import { IgxForOfDirective } from '../../../directives/for-of/for_of.directive'; +import { IgxExcelStyleDropDownComponent } from './excel-style-drop-down.component'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +/** + * @hidden + */ +export interface ILogicOperatorChangedArgs { + target: ExpressionUI; + newValue: FilteringLogic; +} + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-excel-style-default-expression', + templateUrl: './excel-style-default-expression.component.html' +}) +export class IgxExcelStyleDefaultExpressionComponent implements AfterViewInit, OnDestroy { + + private _dropDownOverlaySettings: OverlaySettings = { + closeOnOutsideClick: true, + modal: false, + positionStrategy: new ConnectedPositioningStrategy(), + scrollStrategy: new CloseScrollStrategy() + }; + + protected _isDropdownValuesOpening = false; + protected _isDropdownOpened = false; + protected _valuesData: any[]; + protected _scrollTop = 0; + protected destroy$ = new Subject(); + + @Input() + public column: IgxColumnComponent; + + @Input() + public columnData: any[]; + + @Input() + public expressionUI: ExpressionUI; + + @Input() + public expressionsList: Array; + + @Input() + public grid: IgxGridBaseComponent; + + @Output() + public onExpressionRemoved = new EventEmitter(); + + @Output() + public onLogicOperatorChanged = new EventEmitter(); + + @ViewChild('inputGroupValues', { read: IgxInputGroupComponent }) + protected inputGroupValues: IgxInputGroupComponent; + + @ViewChild('inputGroupConditions', { read: IgxInputGroupComponent }) + private inputGroupConditions: IgxInputGroupComponent; + + @ViewChild('inputValues', { read: IgxInputDirective }) + protected inputValuesDirective: IgxInputDirective; + + @ViewChild('dropdownValues', { read: IgxExcelStyleDropDownComponent }) + protected dropdownValues: IgxExcelStyleDropDownComponent; + + @ViewChild('dropdownConditions', { read: IgxDropDownComponent }) + protected dropdownConditions: IgxDropDownComponent; + + @ViewChild('logicOperatorButtonGroup', { read: IgxButtonGroupComponent }) + protected logicOperatorButtonGroup: IgxButtonGroupComponent; + + @ViewChild(IgxForOfDirective) + protected valuesForOfDirective: IgxForOfDirective; + + protected get inputValuesElement() { + return this.inputValuesDirective; + } + + get isLast(): boolean { + return this.expressionsList[this.expressionsList.length - 1] === this.expressionUI; + } + + get isSingle(): boolean { + return this.expressionsList.length === 1; + } + + get inputConditionsPlaceholder(): string { + return this.grid.resourceStrings['igx_grid_filter_condition_placeholder']; + } + + get inputValuePlaceholder(): string { + return this.grid.resourceStrings['igx_grid_filter_row_placeholder']; + } + + get valuesData(): any[] { + if (!this._valuesData) { + this._valuesData = this.columnData.filter(x => x !== null && x !== undefined && x !== ''); + } + + return this._valuesData; + } + + constructor(public cdr: ChangeDetectorRef) {} + + ngAfterViewInit(): void { + this._dropDownOverlaySettings.outlet = this.column.grid.outletDirective; + + this.valuesForOfDirective.onChunkLoad.pipe(takeUntil(this.destroy$)).subscribe(() => { + const isSearchValNumber = typeof (this.valuesForOfDirective.igxForOf[0]) === 'number'; + if (isSearchValNumber) { + const searchVal = parseFloat(this.expressionUI.expression.searchVal); + const selectedItemIndex = this.dropdownValues.items.findIndex(x => x.value === searchVal); + + if (selectedItemIndex !== -1) { + this.dropdownValues.setSelectedItem(selectedItemIndex); + } else if (this.dropdownValues.selectedItem) { + this.dropdownValues.selectedItem.selected = false; + } + } + }); + } + + public ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.complete(); + } + + public focus() { + // use requestAnimationFrame to focus the values input because when initializing the component + // datepicker's input group is not yet fully initialized + requestAnimationFrame(() => this.inputValuesElement.focus()); + } + + public onValuesChanged(eventArgs: any) { + if (!this._isDropdownValuesOpening) { + const value = (eventArgs.newSelection as IgxDropDownItemComponent).value; + this.expressionUI.expression.searchVal = value; + + this.focus(); + } + } + + public onDropdownValuesOpening() { + this._isDropdownValuesOpening = true; + + if (this.expressionUI.expression.searchVal) { + const isSearchValNumber = typeof(this.valuesForOfDirective.igxForOf[0]) === 'number'; + const searchVal = isSearchValNumber ? parseFloat(this.expressionUI.expression.searchVal) + : this.expressionUI.expression.searchVal; + const selectedItemIndex = this.valuesForOfDirective.igxForOf.indexOf(searchVal); + + if (selectedItemIndex > -1) { + this._scrollTop = this.valuesForOfDirective.getScrollForIndex(selectedItemIndex); + } + } + + (this.valuesForOfDirective as any).vh.instance.scrollTop = this._scrollTop; + } + + public onDropdownClosing() { + this._scrollTop = this.valuesForOfDirective.getVerticalScroll().scrollTop; + } + + public onDropdownValuesOpened() { + this._isDropdownValuesOpening = false; + } + + public isConditionSelected(conditionName: string): boolean { + return this.expressionUI.expression.condition && this.expressionUI.expression.condition.name === conditionName; + } + + public getConditionName(condition: IFilteringOperation) { + return condition ? condition.name : null; + } + + public getInputWidth(parent: any) { + return parent ? parent.element.nativeElement.offsetWidth + 'px' : null; + } + + get conditions() { + return this.column.filters.conditionList(); + } + + public translateCondition(value: string): string { + return this.grid.resourceStrings[`igx_grid_filter_${this.getCondition(value).name}`] || value; + } + + public getIconName(): string { + if (this.column.dataType === DataType.Boolean && this.expressionUI.expression.condition === null) { + return this.getCondition(this.conditions[0]).iconName; + } else if (!this.expressionUI.expression.condition) { + return 'filter_list'; + } else { + return this.expressionUI.expression.condition.iconName; + } + } + + public onDropdownClosed() { + this._isDropdownOpened = false; + (this.valuesForOfDirective as any).vh.instance.scrollTop = null; + } + + public toggleCustomDialogDropDown(input: IgxInputGroupComponent, targetDropDown: IgxDropDownComponent) { + if (!this._isDropdownOpened) { + this._dropDownOverlaySettings.positionStrategy.settings.target = input.element.nativeElement; + targetDropDown.toggle(this._dropDownOverlaySettings); + this._isDropdownOpened = true; + } + } + + public getCondition(value: string): IFilteringOperation { + return this.column.filters.condition(value); + } + + public onConditionsChanged(eventArgs: any) { + const value = (eventArgs.newSelection as IgxDropDownItemComponent).value; + this.expressionUI.expression.condition = this.getCondition(value); + + this.focus(); + } + + public isValueSelected(value: string): boolean { + if (this.expressionUI.expression.searchVal) { + return this.expressionUI.expression.searchVal === value; + } else { + return false; + } + } + + public onLogicOperatorButtonClicked(eventArgs, buttonIndex: number) { + if (this.logicOperatorButtonGroup.selectedButtons.length === 0) { + eventArgs.stopPropagation(); + this.logicOperatorButtonGroup.selectButton(buttonIndex); + } else { + this.onLogicOperatorChanged.emit({ + target: this.expressionUI, + newValue: buttonIndex as FilteringLogic + }); + } + } + + public onLogicOperatorKeyDown(eventArgs, buttonIndex: number) { + if (eventArgs.key === KEYS.ENTER) { + this.logicOperatorButtonGroup.selectButton(buttonIndex); + this.onLogicOperatorChanged.emit({ + target: this.expressionUI, + newValue: buttonIndex as FilteringLogic + }); + } + } + + public onRemoveButtonClick() { + this.onExpressionRemoved.emit(this.expressionUI); + } + + public onInputValuesKeydown(event: KeyboardEvent) { + if (event.altKey && (event.key === KEYS.DOWN_ARROW || event.key === KEYS.DOWN_ARROW_IE)) { + this.toggleCustomDialogDropDown(this.inputGroupValues, this.dropdownValues); + } + + event.stopPropagation(); + } + + public onInputKeyDown(eventArgs) { + if (eventArgs.altKey && (eventArgs.key === KEYS.DOWN_ARROW || eventArgs.key === KEYS.DOWN_ARROW_IE)) { + this.toggleCustomDialogDropDown(this.inputGroupConditions, this.dropdownConditions); + } + + if (eventArgs.key === KEYS.TAB && eventArgs.shiftKey && this.expressionsList[0] === this.expressionUI) { + eventArgs.preventDefault(); + } + + event.stopPropagation(); + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-drop-down.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-drop-down.component.ts new file mode 100644 index 00000000000..949204afecc --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-drop-down.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { IgxDropDownComponent, IgxDropDownItemBase } from '../../../drop-down'; +import { IGX_DROPDOWN_BASE } from '../../../drop-down/drop-down.common'; + +/** + * @hidden + */ +@Component({ + selector: 'igx-excel-style-drop-down', + templateUrl: '../../../drop-down/drop-down.component.html', + providers: [{ provide: IGX_DROPDOWN_BASE, useExisting: IgxExcelStyleDropDownComponent }] +}) +export class IgxExcelStyleDropDownComponent extends IgxDropDownComponent { + // TODO: remove this class once the drow down supports igxFor virtualization + protected scrollToItem(item: IgxDropDownItemBase) { + return; + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.html new file mode 100644 index 00000000000..9114d949bd1 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.html @@ -0,0 +1,38 @@ + + search + + + clear + + + + +
+ + + {{ column.formatter ? column.formatter(item.label) : column.dataType === 'number' ? (item.label | igxdecimal: + column.grid.locale) : column.dataType === 'date' ? (item.label | igxdate: column.grid.locale) : item.label }} + + +
+
\ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts new file mode 100644 index 00000000000..3b7f7955038 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-search.component.ts @@ -0,0 +1,67 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + ViewChild +} from '@angular/core'; +import { IgxColumnComponent } from '../../column.component'; +import { IgxFilterOptions } from '../../../directives/filter/filter.directive'; +import { IChangeCheckboxEventArgs } from '../../../checkbox/checkbox.component'; +import { IgxInputDirective } from '../../../directives/input/input.directive'; + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-excel-style-search', + templateUrl: './excel-style-search.component.html' +}) +export class IgxExcelStyleSearchComponent { + + public searchValue: any; + + @Input() + public data: any[]; + + @Input() + public column: IgxColumnComponent; + + @ViewChild('input', { read: IgxInputDirective }) + public searchInput: IgxInputDirective; + + constructor() {} + + get filterOptions() { + const fo = new IgxFilterOptions(); + fo.key = 'value'; + fo.inputValue = this.searchValue; + return fo; + } + + public clearInput() { + this.searchValue = null; + } + + public onCheckboxChange(eventArgs: IChangeCheckboxEventArgs) { + const selectAll = this.column.grid.resourceStrings.igx_grid_excel_select_all; + if (eventArgs.checkbox.value.value === selectAll) { + this.data.forEach(element => { + element.isSelected = eventArgs.checked; + this.data[0].indeterminate = false; + }); + } else { + eventArgs.checkbox.value.isSelected = eventArgs.checked; + if (!this.data.filter(el => el.value !== selectAll).find(el => el.isSelected === false)) { + this.data[0].indeterminate = false; + this.data[0].isSelected = true; + } else if (!this.data.filter(el => el.value !== selectAll).find(el => el.isSelected === true)) { + this.data[0].indeterminate = false; + this.data[0].isSelected = false; + } else { + this.data[0].indeterminate = true; + } + } + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-sorting.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-sorting.component.html new file mode 100644 index 00000000000..3ab28e7de9a --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-sorting.component.html @@ -0,0 +1,12 @@ +
{{ grid.resourceStrings.igx_grid_excel_filter_sorting_header }}
+ + + arrow_upwards + {{ grid.resourceStrings.igx_grid_excel_filter_sorting_desc }} + + + + arrow_downwards + {{ grid.resourceStrings.igx_grid_excel_filter_sorting_asc }} + + \ No newline at end of file diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-sorting.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-sorting.component.ts new file mode 100644 index 00000000000..f91ee785d46 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/excel-style-sorting.component.ts @@ -0,0 +1,48 @@ +import { + Component, + ChangeDetectionStrategy, + ViewChild, + Input +} from '@angular/core'; +import { IgxColumnComponent } from '../../column.component'; +import { IgxButtonGroupComponent } from '../../../buttonGroup/buttonGroup.component'; +import { IgxGridBaseComponent } from '../../grid'; + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-excel-style-sorting', + templateUrl: './excel-style-sorting.component.html' +}) +export class IgxExcelStyleSortingComponent { + + @Input() + public column: IgxColumnComponent; + + @Input() + public grid: IgxGridBaseComponent; + + @ViewChild('sortButtonGroup', { read: IgxButtonGroupComponent }) + public sortButtonGroup: IgxButtonGroupComponent; + + constructor() {} + + public onSortButtonClicked(sortDirection) { + if (this.sortButtonGroup.selectedIndexes.length === 0) { + this.grid.clearSort(this.column.field); + } else { + this.grid.sort({ fieldName: this.column.field, dir: sortDirection, ignoreCase: true }); + } + } + + public selectButton(sortDirection: number) { + if (sortDirection === 1) { + this.sortButtonGroup.selectButton(0); + } else { + this.sortButtonGroup.selectButton(1); + } + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html new file mode 100644 index 00000000000..ebd4f21945d --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.html @@ -0,0 +1,118 @@ +
+ +
+

{{ column.field }}

+
+ + + + + + +
+ +
+ +
+ + + + + + +
+ +
+ + +
+ {{ grid.resourceStrings.igx_grid_excel_pin }} + +
+ +
+ {{ grid.resourceStrings.igx_grid_excel_unpin }} + +
+
+ +
+ +
+ + +
+ {{ grid.resourceStrings.igx_grid_excel_hide }} + visibility_off +
+
+ +
+ +
+ +
+ {{ subMenuText }} + keyboard_arrow_right +
+
+ + + + +
+ + +
+
+ + +
+ + + {{ translateCondition(condition) }} + + + filter_list + {{ grid.resourceStrings.igx_grid_excel_custom_filter }} + +
+
+ + + diff --git a/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts new file mode 100644 index 00000000000..24261898b78 --- /dev/null +++ b/projects/igniteui-angular/src/lib/grids/filtering/excel-style/grid.excel-style-filtering.component.ts @@ -0,0 +1,567 @@ +import { + ChangeDetectorRef, + Component, + ViewChild, + HostBinding, + ChangeDetectionStrategy, + TemplateRef, + Directive, + OnDestroy, + ContentChild, +} from '@angular/core'; +import { IgxColumnComponent } from '../../grid'; +import { IgxDropDownComponent, ISelectionEventArgs } from '../../../drop-down'; +import { + HorizontalAlignment, + VerticalAlignment, + ConnectedPositioningStrategy, + CloseScrollStrategy, + OverlaySettings, + IgxOverlayService +} from '../../../services'; +import { IgxFilteringService, ExpressionUI } from '../grid-filtering.service'; +import { IgxToggleDirective } from '../../../directives/toggle/toggle.directive'; +import { + IFilteringOperation, + IgxStringFilteringOperand, + IgxNumberFilteringOperand, + IgxBooleanFilteringOperand, + IgxDateFilteringOperand +} from '../../../data-operations/filtering-condition'; +import { FilteringExpressionsTree } from '../../../data-operations/filtering-expressions-tree'; +import { FilteringLogic } from '../../../data-operations/filtering-expression.interface'; +import { cloneArray, KEYS } from '../../../core/utils'; +import { DataType } from '../../../data-operations/data-util'; +import { IgxExcelStyleSearchComponent } from './excel-style-search.component'; +import { IgxExcelStyleCustomDialogComponent } from './excel-style-custom-dialog.component'; +import { Subscription, Subject } from 'rxjs'; +import { IgxExcelStyleSortingComponent } from './excel-style-sorting.component'; +import { takeUntil } from 'rxjs/operators'; + +/** + *@hidden + */ +export class FilterListItem { + public value: any; + public label: any; + public isSelected: boolean; + public indeterminate: boolean; +} + +@Directive({ + selector: '[igxExcelStyleSortingTemplate]' +}) +export class IgxExcelStyleSortingTemplateDirective { + constructor(public template: TemplateRef) {} +} + +@Directive({ + selector: '[igxExcelStyleMovingTemplate]' +}) +export class IgxExcelStyleMovingTemplateDirective { + constructor(public template: TemplateRef) {} +} + +@Directive({ + selector: '[igxExcelStyleHidingTemplate]' +}) +export class IgxExcelStyleHidingTemplateDirective { + constructor(public template: TemplateRef) {} +} + +@Directive({ + selector: '[igxExcelStylePinningTemplate]' +}) +export class IgxExcelStylePinningTemplateDirective { + constructor(public template: TemplateRef) {} +} + +/** + * @hidden + */ +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + selector: 'igx-grid-excel-style-filtering', + templateUrl: './grid.excel-style-filtering.component.html' +}) +export class IgxGridExcelStyleFilteringComponent implements OnDestroy { + + private shouldOpenSubMenu = true; + private originalColumnData = new Array(); + private expressionsList = new Array(); + private destroy$ = new Subject(); + private containsNullOrEmpty = false; + private selectAllSelected = true; + private selectAllIndeterminate = false; + private filterValues = []; + + protected chunkLoaded = new Subscription(); + protected columnMoving = new Subscription(); + + public column: IgxColumnComponent; + public filteringService: IgxFilteringService; + public listData = new Array(); + public uniqueValues = []; + public overlayService: IgxOverlayService; + public overlayComponentId: string; + + private _subMenuPositionSettings = { + verticalStartPoint: VerticalAlignment.Top + }; + + private _subMenuOverlaySettings: OverlaySettings = { + closeOnOutsideClick: true, + modal: false, + positionStrategy: new ConnectedPositioningStrategy(this._subMenuPositionSettings), + scrollStrategy: new CloseScrollStrategy() + }; + + @HostBinding('class.igx-excel-filter') + className = 'igx-excel-filter'; + + @ViewChild('dropdown', { read: IgxToggleDirective }) + public mainDropdown: IgxToggleDirective; + + @ViewChild('subMenu', { read: IgxDropDownComponent }) + public subMenu: IgxDropDownComponent; + + @ViewChild('customDialog', { read: IgxExcelStyleCustomDialogComponent }) + public customDialog: IgxExcelStyleCustomDialogComponent; + + @ViewChild('excelStyleSearch', { read: IgxExcelStyleSearchComponent }) + protected excelStyleSearch: IgxExcelStyleSearchComponent; + + @ViewChild('excelStyleSorting', { read: IgxExcelStyleSortingComponent }) + protected excelStyleSorting: IgxExcelStyleSortingComponent; + + @ViewChild('defaultExcelStyleSortingTemplate', { read: TemplateRef }) + protected defaultExcelStyleSortingTemplate: TemplateRef; + + @ViewChild('defaultExcelStyleHidingTemplate', { read: TemplateRef }) + protected defaultExcelStyleHidingTemplate: TemplateRef; + + @ViewChild('defaultExcelStyleMovingTemplate', { read: TemplateRef }) + protected defaultExcelStyleMovingTemplate: TemplateRef; + + @ViewChild('defaultExcelStylePinningTemplate', { read: TemplateRef }) + protected defaultExcelStylePinningTemplate: TemplateRef; + + get grid() { + return this.filteringService.grid; + } + + get conditions() { + return this.column.filters.conditionList(); + } + + get subMenuText() { + switch (this.column.dataType) { + case DataType.Boolean: + return this.grid.resourceStrings.igx_grid_excel_boolean_filter; + case DataType.Number: + return this.grid.resourceStrings.igx_grid_excel_number_filter; + case DataType.Date: + return this.grid.resourceStrings.igx_grid_excel_date_filter; + default: + return this.grid.resourceStrings.igx_grid_excel_text_filter; + } + } + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnDestroy(): void { + this.destroy$.next(true); + this.destroy$.complete(); + } + + public initialize(column: IgxColumnComponent, filteringService: IgxFilteringService, overlayService: IgxOverlayService, + overlayComponentId: string) { + this.column = column; + this.filteringService = filteringService; + this.overlayService = overlayService; + this.overlayComponentId = overlayComponentId; + + this._subMenuOverlaySettings.outlet = this.grid.outletDirective; + + this.chunkLoaded = this.grid.headerContainer.onChunkPreload.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.closeDropdown(); + }); + + this.columnMoving = this.grid.onColumnMoving.pipe(takeUntil(this.destroy$)).subscribe(() => { + this.closeDropdown(); + }); + } + + /** + * Returns the filtering operation condition for a given value. + */ + public getCondition(value: string): IFilteringOperation { + return this.column.filters.condition(value); + } + + /** + * Returns the translated condition name for a given value. + */ + public translateCondition(value: string): string { + return this.grid.resourceStrings[`igx_grid_filter_${this.getCondition(value).name}`] || value; + } + + public onPin() { + this.column.pinned = !this.column.pinned; + this.closeDropdown(); + } + + public onHide() { + this.column.hidden = true; + this.closeDropdown(); + } + + public onTextFilterClick(eventArgs) { + if (this.shouldOpenSubMenu) { + this._subMenuOverlaySettings.positionStrategy.settings.target = eventArgs.currentTarget; + + const gridRect = this.grid.nativeElement.getBoundingClientRect(); + const dropdownRect = this.mainDropdown.element.getBoundingClientRect(); + + let x = dropdownRect.left + dropdownRect.width; + let x1 = gridRect.left + gridRect.width; + x += window.pageXOffset; + x1 += window.pageXOffset; + if (Math.abs(x - x1) < 200) { + this._subMenuOverlaySettings.positionStrategy.settings.horizontalDirection = HorizontalAlignment.Left; + this._subMenuOverlaySettings.positionStrategy.settings.horizontalStartPoint = HorizontalAlignment.Left; + } else { + this._subMenuOverlaySettings.positionStrategy.settings.horizontalDirection = HorizontalAlignment.Right; + this._subMenuOverlaySettings.positionStrategy.settings.horizontalStartPoint = HorizontalAlignment.Right; + } + + this.subMenu.open(this._subMenuOverlaySettings); + this.shouldOpenSubMenu = false; + } + } + + public onTextFilterKeyDown(eventArgs) { + if (eventArgs.key === KEYS.ENTER) { + this.onTextFilterClick(eventArgs); + } + } + + public onSubMenuClosed() { + requestAnimationFrame(() => { + this.shouldOpenSubMenu = true; + }); + } + + public onSubMenuSelection(eventArgs: ISelectionEventArgs) { + this.customDialog.selectedOperator = eventArgs.newSelection.value ? eventArgs.newSelection.value : 'equals'; + eventArgs.cancel = true; + this.mainDropdown.close(); + this.subMenu.close(); + this.customDialog.open(); + } + + private areExpressionsSelectable () { + if (this.expressionsList.length === 1 && + (this.expressionsList[0].expression.condition.name === 'equals' || + this.expressionsList[0].expression.condition.name === 'true' || + this.expressionsList[0].expression.condition.name === 'false' || + this.expressionsList[0].expression.condition.name === 'empty')) { + return true; + } + + const selectableExpressionsCount = this.expressionsList.filter(exp => + (exp.beforeOperator === 1 || exp.afterOperator === 1) && + (this.expressionsList[0].expression.condition.name === 'equals' || + this.expressionsList[0].expression.condition.name === 'true' || + this.expressionsList[0].expression.condition.name === 'false' || + this.expressionsList[0].expression.condition.name === 'empty')).length; + if (selectableExpressionsCount === this.expressionsList.length) { + return true; + } else { + return false; + } + } + + private areExpressionsValuesInTheList() { + if (this.column.dataType === DataType.Boolean) { + return true; + } + let sameElements = 0; + + for (let index = 0; index < this.filterValues.length; index++) { + if (this.uniqueValues.indexOf(this.filterValues[index]) !== -1) { + sameElements ++; + } + } + + if (sameElements === this.filterValues.length) { + return true; + } else { + return false; + } + } + + public populateColumnData() { + if (this.column.dataType === DataType.Date) { + this.uniqueValues = Array.from(new Set(this.grid.data.map(record => + record[this.column.field] ? record[this.column.field].toDateString() : record[this.column.field]))); + this.filterValues = this.expressionsList.map(exp => + exp.expression.searchVal ? exp.expression.searchVal.toDateString() : exp.expression.searchVal); + } else { + this.uniqueValues = Array.from(new Set(this.filteringService.grid.data.map(record => record[this.column.field]))); + this.filterValues = this.expressionsList.map(exp => exp.expression.searchVal); + } + this.listData = new Array(); + + const shouldUpdateSelection = this.areExpressionsSelectable() && this.areExpressionsValuesInTheList(); + + if (this.column.dataType === DataType.Boolean) { + this.addBooleanItems(); + } else { + this.addItems(shouldUpdateSelection); + } + + this.listData.sort((a, b) => this.sortData(a, b)); + + if (this.column.dataType === DataType.Date) { + this.uniqueValues = this.uniqueValues.map(value => new Date(value)); + } + + if (this.containsNullOrEmpty) { + this.addBlanksItem(shouldUpdateSelection); + } + + this.addSelectAllItem(); + + this.cdr.detectChanges(); + } + + private addBooleanItems() { + this.selectAllSelected = true; + this.selectAllIndeterminate = false; + this.uniqueValues.forEach(element => { + const filterListItem = new FilterListItem(); + if (element !== undefined && element !== null && element !== '') { + if (this.column.filteringExpressionsTree) { + if (element === true && this.expressionsList.find(exp => exp.expression.condition.name === 'true' )) { + filterListItem.isSelected = true; + this.selectAllIndeterminate = true; + } else if (element === false && this.expressionsList.find(exp => exp.expression.condition.name === 'false' )) { + filterListItem.isSelected = true; + this.selectAllIndeterminate = true; + } else { + filterListItem.isSelected = false; + } + } else { + filterListItem.isSelected = true; + } + filterListItem.value = element; + filterListItem.label = element; + filterListItem.indeterminate = false; + this.listData.push(filterListItem); + } else { + this.containsNullOrEmpty = true; + } + }); + } + + private addItems(shouldUpdateSelection: boolean) { + this.selectAllSelected = true; + this.selectAllIndeterminate = false; + this.uniqueValues.forEach(element => { + if (element !== undefined && element !== null && element !== '') { + const filterListItem = new FilterListItem(); + if (this.column.filteringExpressionsTree) { + if (shouldUpdateSelection) { + if (this.filterValues.indexOf(element) !== -1) { + filterListItem.isSelected = true; + } else { + filterListItem.isSelected = false; + } + this.selectAllIndeterminate = true; + } else { + filterListItem.isSelected = false; + this.selectAllSelected = false; + } + } else { + filterListItem.isSelected = true; + } + if (this.column.dataType === DataType.Date) { + filterListItem.value = new Date(element); + filterListItem.label = new Date(element); + } else { + filterListItem.value = element; + filterListItem.label = element; + } + filterListItem.indeterminate = false; + this.listData.push(filterListItem); + } else { + this.containsNullOrEmpty = true; + } + }); + } + + private addSelectAllItem() { + const selectAll = new FilterListItem(); + selectAll.isSelected = this.selectAllSelected; + selectAll.value = this.grid.resourceStrings.igx_grid_excel_select_all; + selectAll.label = this.grid.resourceStrings.igx_grid_excel_select_all; + selectAll.indeterminate = this.selectAllIndeterminate; + this.listData.unshift(selectAll); + } + + private addBlanksItem(shouldUpdateSelection) { + const blanks = new FilterListItem(); + if (this.column.filteringExpressionsTree) { + if (shouldUpdateSelection) { + if (this.filterValues.indexOf(null) !== -1) { + blanks.isSelected = true; + } else { + blanks.isSelected = false; + } + } + } else { + blanks.isSelected = true; + } + blanks.value = null; + blanks.label = this.grid.resourceStrings.igx_grid_excel_blanks; + blanks.indeterminate = false; + this.listData.unshift(blanks); + } + + private sortData(a: FilterListItem, b: FilterListItem) { + let valueA = a.value; + let valueB = b.value; + if (typeof(a) === DataType.String) { + valueA = a.value.toUpperCase(); + valueB = b.value.toUpperCase(); + } + if (valueA < valueB) { + return -1; + } else if (valueA > valueB) { + return 1; + } else { + return 0; + } + } + + // TODO: sort members by access modifier + + public toggleDropdown(overlaySettings: OverlaySettings) { + this.mainDropdown.open(overlaySettings); + } + + public onDropDownOpening() { + this.expressionsList = new Array(); + this.filteringService.generateExpressionsList(this.column.filteringExpressionsTree, this.grid.filteringLogic, this.expressionsList); + this.customDialog.expressionsList = this.expressionsList; + this.populateColumnData(); + this.originalColumnData = cloneArray(this.listData, true); + + const se = this.grid.sortingExpressions.find(expr => expr.fieldName === this.column.field); + if (se) { + this.excelStyleSorting.selectButton(se.dir); + } + + requestAnimationFrame(() => { + this.excelStyleSearch.searchInput.nativeElement.focus(); + }); + } + + get sortingTemplate() { + if (this.grid.excelStyleSortingTemplateDirective) { + return this.grid.excelStyleSortingTemplateDirective.template; + } else { + return this.defaultExcelStyleSortingTemplate; + } + } + + get movingTemplate() { + if (this.grid.excelStyleMovingTemplateDirective) { + return this.grid.excelStyleMovingTemplateDirective.template; + } else { + return this.defaultExcelStyleMovingTemplate; + } + } + + get pinningTemplate() { + if (this.grid.excelStylePinningTemplateDirective) { + return this.grid.excelStylePinningTemplateDirective.template; + } else { + return this.defaultExcelStylePinningTemplate; + } + } + + get hidingTemplate() { + if (this.grid.excelStyleHidingTemplateDirective) { + return this.grid.excelStyleHidingTemplateDirective.template; + } else { + return this.defaultExcelStyleHidingTemplate; + } + } + + get applyButtonDisabled() { + return this.listData[0] && !this.listData[0].isSelected && !this.listData[0].indeterminate; + } + + public applyFilter() { + const filterTree = new FilteringExpressionsTree(FilteringLogic.Or, this.column.field); + const selectedItems = this.listData.filter(el => + el.value !== this.grid.resourceStrings.igx_grid_excel_select_all && el.isSelected === true); + const unselectedItem = this.listData.find(el => + el.value !== this.grid.resourceStrings.igx_grid_excel_select_all && el.isSelected === false); + + if (unselectedItem) { + selectedItems.forEach(element => { + let condition = null; + if (element.value !== null && element.value !== undefined) { + if (this.column.dataType === DataType.Boolean) { + condition = this.createCondition(element.value.toString()); + } else { + condition = this.createCondition('equals'); + } + } else { + condition = this.createCondition('empty'); + } + filterTree.filteringOperands.push({ + condition: condition, + fieldName: this.column.field, + ignoreCase: this.column.filteringIgnoreCase, + searchVal: element.value + }); + }); + this.expressionsList = new Array(); + this.grid.filter(this.column.field, null, filterTree); + } else { + this.grid.clearFilter(this.column.field); + } + + this.originalColumnData = new Array(); + this.closeDropdown(); + } + + public closeDropdown() { + if (this.overlayComponentId) { + this.overlayService.hide(this.overlayComponentId); + this.overlayComponentId = null; + } + } + + public onKeyDown(eventArgs) { + eventArgs.stopPropagation(); + } + + private createCondition(conditionName: string) { + switch (this.column.dataType) { + case DataType.Boolean: + return IgxBooleanFilteringOperand.instance().condition(conditionName); + case DataType.Number: + return IgxNumberFilteringOperand.instance().condition(conditionName); + case DataType.Date: + return IgxDateFilteringOperand.instance().condition(conditionName); + default: + return IgxStringFilteringOperand.instance().condition(conditionName); + } + } +} diff --git a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts index 9f2738484d6..1c290c78a8f 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/grid-filtering.service.ts @@ -121,10 +121,10 @@ export class IgxFilteringService implements OnDestroy { /** * Execute filtering on the grid. */ - public filter(field: string): void { + public filter(field: string, expressionUIList = null): void { this.isFiltering = true; - const expressionsTree = this.createSimpleFilteringTree(field); + const expressionsTree = this.createSimpleFilteringTree(field, expressionUIList); this.grid.filter(field, null, expressionsTree); // Wait for the change detection to update filtered data through the pipes and then emit the event. @@ -229,8 +229,8 @@ export class IgxFilteringService implements OnDestroy { /** * Generate filtering tree for a given column from existing ExpressionUIs. */ - public createSimpleFilteringTree(columnId: string): FilteringExpressionsTree { - const expressionsList = this.getExpressions(columnId); + public createSimpleFilteringTree(columnId: string, expressionUIList = null): FilteringExpressionsTree { + const expressionsList = expressionUIList ? expressionUIList : this.getExpressions(columnId); const expressionsTree = new FilteringExpressionsTree(FilteringLogic.Or, columnId); let currAndBranch: FilteringExpressionsTree; let currExpressionUI: ExpressionUI; @@ -405,7 +405,7 @@ export class IgxFilteringService implements OnDestroy { return count; } - private generateExpressionsList(expressions: IFilteringExpressionsTree | IFilteringExpression, + public generateExpressionsList(expressions: IFilteringExpressionsTree | IFilteringExpression, operator: FilteringLogic, expressionsUIs: ExpressionUI[]): void { if (!expressions) { diff --git a/projects/igniteui-angular/src/lib/grids/filtering/svgIcons.ts b/projects/igniteui-angular/src/lib/grids/filtering/svgIcons.ts index 931f67ee9ed..50493d997ce 100644 --- a/projects/igniteui-angular/src/lib/grids/filtering/svgIcons.ts +++ b/projects/igniteui-angular/src/lib/grids/filtering/svgIcons.ts @@ -3,8 +3,9 @@ export default [ { name: 'add_filter', value: ` - -`}, + + ` + }, { name: 'contains', value: ` @@ -204,5 +205,18 @@ export default [ ` + }, + { + name: 'pin', + value: ` + + ` + }, + { + name: 'unpin', + value: ` + + + ` } ]; diff --git a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts index fcb80376231..385072a45d1 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-base.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-base.component.ts @@ -71,6 +71,12 @@ import { IgxGridSummaryService } from './summaries/grid-summary.service'; import { IgxSummaryRowComponent } from './summaries/summary-row.component'; import { DeprecateMethod } from '../core/deprecateDecorators'; import { IViewChangeEventArgs, ICachedViewLoadedEventArgs } from '../directives/template-outlet/template_outlet.directive'; +import { + IgxExcelStyleSortingTemplateDirective, + IgxExcelStylePinningTemplateDirective, + IgxExcelStyleHidingTemplateDirective, + IgxExcelStyleMovingTemplateDirective +} from './filtering/excel-style/grid.excel-style-filtering.component'; const MINIMUM_COLUMN_WIDTH = 136; const FILTER_ROW_HEIGHT = 50; @@ -172,6 +178,11 @@ export enum GridSummaryCalculationMode { rootAndChildLevels = 'rootAndChildLevels' } +export enum FilterMode { + quickFilter = 'quickFilter', + excelStyleFilter = 'excelStyleFilter' +} + export abstract class IgxGridBaseComponent extends DisplayDensityBase implements OnInit, OnDestroy, AfterContentInit, AfterViewInit { private _scrollWidth: number; @@ -824,6 +835,30 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements } } + /** + * Returns the filter mode. + * ```typescript + * let filtering = this.grid.filterMode; + * ``` + * @memberof IgxGridBaseComponent + */ + @Input() + get filterMode() { + return this._filterMode; + } + + /** + * Sets filter mode. + * By default it's set to FilterMode.quickFilter. + * ```html + * + * ``` + * @memberof IgxGridBaseComponent + */ + set filterMode(value) { + this._filterMode = value; + } + /** * Returns the summary position. * ```typescript @@ -1331,6 +1366,30 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements @ContentChildren(IgxColumnComponent, { read: IgxColumnComponent, descendants: true }) public columnList: QueryList; + /** + *@hidden + */ + @ContentChild(IgxExcelStyleSortingTemplateDirective, { read: IgxExcelStyleSortingTemplateDirective }) + public excelStyleSortingTemplateDirective: IgxExcelStyleSortingTemplateDirective; + + /** + *@hidden + */ + @ContentChild(IgxExcelStyleMovingTemplateDirective, { read: IgxExcelStyleMovingTemplateDirective }) + public excelStyleMovingTemplateDirective: IgxExcelStyleMovingTemplateDirective; + + /** + *@hidden + */ + @ContentChild(IgxExcelStyleHidingTemplateDirective, { read: IgxExcelStyleHidingTemplateDirective }) + public excelStyleHidingTemplateDirective: IgxExcelStyleHidingTemplateDirective; + + /** + *@hidden + */ + @ContentChild(IgxExcelStylePinningTemplateDirective, { read: IgxExcelStylePinningTemplateDirective }) + public excelStylePinningTemplateDirective: IgxExcelStylePinningTemplateDirective; + /** * @hidden */ @@ -2221,6 +2280,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements */ protected _wheelListener = null; protected _allowFiltering = false; + protected _filterMode = FilterMode.quickFilter; private resizeHandler; private columnListDiffer; private _hiddenColumnsText = ''; @@ -2437,9 +2497,9 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements return mutation.type === 'childList'; }).length > 0; if (childListHasChanged && this.isAttachedToDom) { - this.reflow(); - observer.disconnect(); - } + this.reflow(); + observer.disconnect(); + } }; observer = new MutationObserver(callback); @@ -3077,7 +3137,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements /** @hidden */ public deleteRowById(rowId: any) { this.gridAPI.deleteRowById(this.id, rowId); - } + } /** * Updates the `IgxGridRowComponent` and the corresponding data record by primary key. @@ -3520,8 +3580,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements * @hidden */ protected get rowBasedHeight() { - return this.dataLength * this.rowHeight; - } + return this.dataLength * this.rowHeight; + } /** * @hidden @@ -3638,8 +3698,8 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements if (this._height && this._height.indexOf('%') !== -1) { /*height in %*/ if (computed.getPropertyValue('height').indexOf('%') === -1 ) { - gridHeight = parseInt(computed.getPropertyValue('height'), 10); - } else { + gridHeight = parseInt(computed.getPropertyValue('height'), 10); + } else { return this.defaultTargetBodyHeight; } } else { @@ -3739,7 +3799,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements if (!width) { - width = this.columnList.reduce((sum, item) => sum + parseInt((item.width || item.defaultWidth), 10), 0); + width = this.columnList.reduce((sum, item) => sum + parseInt((item.width || item.defaultWidth), 10), 0); } if (this.hasVerticalSroll()) { @@ -4756,7 +4816,7 @@ export abstract class IgxGridBaseComponent extends DisplayDensityBase implements this.lastSearchInfo.caseSensitive, this.lastSearchInfo.exactMatch); }); - } +} } if (this.hasHorizontalScroll()) { const tmplId = args.context.templateID; diff --git a/projects/igniteui-angular/src/lib/grids/grid-column-resizing.service.ts b/projects/igniteui-angular/src/lib/grids/grid-column-resizing.service.ts index dc66e7fff5e..f831707eb9a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-column-resizing.service.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-column-resizing.service.ts @@ -59,7 +59,7 @@ export class IgxColumnResizingService { let minWidth = Number.isNaN(actualMinWidth) || actualMinWidth < defaultMinWidth ? defaultMinWidth : actualMinWidth; minWidth = minWidth < parseFloat(this.column.width) ? minWidth : parseFloat(this.column.width); - return minWidth - this.column.headerCell.elementRef.nativeElement.getBoundingClientRect().width; + return this.column.headerCell.elementRef.nativeElement.getBoundingClientRect().width - minWidth; } /** diff --git a/projects/igniteui-angular/src/lib/grids/grid-common.module.ts b/projects/igniteui-angular/src/lib/grids/grid-common.module.ts index f451a880153..1f930234b68 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-common.module.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-common.module.ts @@ -23,6 +23,13 @@ import { IgxGridHeaderComponent } from './grid-header.component'; import { IgxGridToolbarComponent } from './grid-toolbar.component'; import { IgxGridFilteringCellComponent } from './filtering/grid-filtering-cell.component'; import { IgxGridFilteringRowComponent } from './filtering/grid-filtering-row.component'; +import { + IgxGridExcelStyleFilteringComponent, + IgxExcelStyleSortingTemplateDirective, + IgxExcelStyleHidingTemplateDirective, + IgxExcelStyleMovingTemplateDirective, + IgxExcelStylePinningTemplateDirective +} from './filtering/excel-style/grid.excel-style-filtering.component'; import { IgxCellEditorTemplateDirective, IgxCellFooterTemplateDirective, @@ -62,6 +69,16 @@ import { IgxSummaryCellComponent } from './summaries/summary-cell.component'; import { IgxSummaryDataPipe } from './summaries/grid-root-summary.pipe'; import { IgxGridSummaryService } from './summaries/grid-summary.service'; import { IgxProgressBarModule } from '../progressbar/progressbar.component'; +import { IgxListComponent, IgxListModule } from '../list'; +import { IgxFilterModule } from '../directives/filter/filter.directive'; +import { IgxComboModule } from '../combo'; +import { IgxExcelStyleSortingComponent } from './filtering/excel-style/excel-style-sorting.component'; +import { IgxExcelStyleColumnMovingComponent } from './filtering/excel-style/excel-style-column-moving.component'; +import { IgxExcelStyleSearchComponent } from './filtering/excel-style/excel-style-search.component'; +import { IgxExcelStyleCustomDialogComponent } from './filtering/excel-style/excel-style-custom-dialog.component'; +import { IgxExcelStyleDefaultExpressionComponent } from './filtering/excel-style/excel-style-default-expression.component'; +import { IgxExcelStyleDateExpressionComponent } from './filtering/excel-style/excel-style-date-expression.component'; +import { IgxExcelStyleDropDownComponent } from './filtering/excel-style/excel-style-drop-down.component'; /** * @hidden @@ -89,17 +106,30 @@ import { IgxProgressBarModule } from '../progressbar/progressbar.component'; IgxGridTransactionPipe, IgxGridFilteringCellComponent, IgxGridFilteringRowComponent, + IgxGridExcelStyleFilteringComponent, IgxDatePipeComponent, IgxDecimalPipeComponent, IgxSummaryDataPipe, IgxRowComponent, IgxGridHeaderGroupComponent, IgxSummaryRowComponent, - IgxSummaryCellComponent + IgxSummaryCellComponent, + IgxExcelStyleSortingComponent, + IgxExcelStyleColumnMovingComponent, + IgxExcelStyleSearchComponent, + IgxExcelStyleCustomDialogComponent, + IgxExcelStyleDefaultExpressionComponent, + IgxExcelStyleDateExpressionComponent, + IgxExcelStyleSortingTemplateDirective, + IgxExcelStyleHidingTemplateDirective, + IgxExcelStyleMovingTemplateDirective, + IgxExcelStylePinningTemplateDirective, + IgxExcelStyleDropDownComponent ], entryComponents: [ IgxColumnComponent, IgxColumnGroupComponent, + IgxGridExcelStyleFilteringComponent ], exports: [ IgxGridCellComponent, @@ -147,9 +177,16 @@ import { IgxProgressBarModule } from '../progressbar/progressbar.component'; IgxProgressBarModule, IgxGridFilteringCellComponent, IgxGridFilteringRowComponent, + IgxGridExcelStyleFilteringComponent, IgxGridHeaderGroupComponent, IgxSummaryRowComponent, - IgxSummaryCellComponent + IgxSummaryCellComponent, + IgxListComponent, + IgxFilterModule, + IgxExcelStyleSortingTemplateDirective, + IgxExcelStyleHidingTemplateDirective, + IgxExcelStyleMovingTemplateDirective, + IgxExcelStylePinningTemplateDirective ], imports: [ CommonModule, @@ -173,7 +210,10 @@ import { IgxProgressBarModule } from '../progressbar/progressbar.component'; IgxDropDownModule, IgxButtonGroupModule, IgxColumnPinningModule, - IgxProgressBarModule + IgxProgressBarModule, + IgxListModule, + IgxFilterModule, + IgxComboModule ], providers: [ IgxSelectionAPIService, diff --git a/projects/igniteui-angular/src/lib/grids/grid-header-group.component.html b/projects/igniteui-angular/src/lib/grids/grid-header-group.component.html index 42058a79ce7..13b6c1cb2d0 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-header-group.component.html +++ b/projects/igniteui-angular/src/lib/grids/grid-header-group.component.html @@ -21,7 +21,7 @@ - +
- {{sortingIcon}} -
+ + {{sortingIcon}} + + + filter_list + + diff --git a/projects/igniteui-angular/src/lib/grids/grid-header.component.ts b/projects/igniteui-angular/src/lib/grids/grid-header.component.ts index ceacfebc772..789d775ba07 100644 --- a/projects/igniteui-angular/src/lib/grids/grid-header.component.ts +++ b/projects/igniteui-angular/src/lib/grids/grid-header.component.ts @@ -8,7 +8,10 @@ import { HostListener, Input, NgZone, - OnInit + OnInit, + Inject, + OnDestroy, + NgModuleRef } from '@angular/core'; import { DataType } from '../data-operations/data-util'; import { SortingDirection } from '../data-operations/sorting-expression.interface'; @@ -16,8 +19,17 @@ import { GridBaseAPIService } from './api.service'; import { IgxColumnComponent } from './column.component'; import { IgxGridBaseComponent } from './grid-base.component'; import { IgxFilteringService } from './filtering/grid-filtering.service'; -import { IgxGridComponent, IGridDataBindable } from './grid'; +import { IGridDataBindable } from './grid'; import { IgxColumnResizingService } from './grid-column-resizing.service'; +import { IgxOverlayService } from '../services/overlay/overlay'; +import { IgxGridExcelStyleFilteringComponent } from './filtering/excel-style/grid.excel-style-filtering.component'; +import { OverlaySettings, PositionSettings, VerticalAlignment, HorizontalAlignment } from '../services/overlay/utilities'; +import { ConnectedPositioningStrategy } from '../services/overlay/position/connected-positioning-strategy'; +import { CloseScrollStrategy } from '../services/overlay/scroll/close-scroll-strategy'; +import { useAnimation } from '@angular/animations'; +import { fadeIn, fadeOut } from 'igniteui-angular'; +import { filter, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; /** * @hidden @@ -28,7 +40,12 @@ import { IgxColumnResizingService } from './grid-column-resizing.service'; selector: 'igx-grid-header', templateUrl: './grid-header.component.html' }) -export class IgxGridHeaderComponent implements DoCheck { +export class IgxGridHeaderComponent implements DoCheck, OnInit, OnDestroy { + + private _componentOverlayId: string; + private _filterMenuPositionSettings: PositionSettings; + private _filterMenuOverlaySettings: OverlaySettings; + private _destroy$ = new Subject(); @Input() public column: IgxColumnComponent; @@ -94,6 +111,10 @@ export class IgxGridHeaderComponent implements DoCheck { return this.column === this.column.grid.draggedColumn; } + get filterIconClassName() { + return this.column.filteringExpressionsTree ? 'igx-excel-filter__icon--filtered' : 'igx-excel-filter__icon'; + } + @HostBinding('attr.role') public hostRole = 'columnheader'; @@ -113,15 +134,29 @@ export class IgxGridHeaderComponent implements DoCheck { public cdr: ChangeDetectorRef, public elementRef: ElementRef, public zone: NgZone, - public filteringService: IgxFilteringService + private _filteringService: IgxFilteringService, + private _moduleRef: NgModuleRef, + @Inject(IgxOverlayService) private _overlayService: IgxOverlayService ) { } + public ngOnInit() { + this.initFilteringSettings(); + } public ngDoCheck() { this.getSortDirection(); this.cdr.markForCheck(); } + ngOnDestroy(): void { + this._destroy$.next(true); + this._destroy$.complete(); + + if (this._componentOverlayId) { + this._overlayService.hide(this._componentOverlayId); + } + } + @HostListener('click', ['$event']) public onClick(event) { if (!this.colResizingService.isColumnResizing) { @@ -137,6 +172,12 @@ export class IgxGridHeaderComponent implements DoCheck { } } + public onFilteringIconClick(event) { + event.stopPropagation(); + + this.toggleFilterDropdown(); + } + get grid(): any { return this.gridAPI.get(this.gridID); } @@ -163,4 +204,91 @@ export class IgxGridHeaderComponent implements DoCheck { this.grid.sort({ fieldName: this.column.field, dir: this.sortDirection, ignoreCase: this.column.sortingIgnoreCase, strategy: this.column.sortStrategy }); } + + private toggleFilterDropdown() { + if (!this._componentOverlayId) { + const headerTarget = this.elementRef.nativeElement; + + const gridRect = this.grid.nativeElement.getBoundingClientRect(); + const headerRect = headerTarget.getBoundingClientRect(); + + let x = headerRect.left; + let x1 = gridRect.left + gridRect.width; + x += window.pageXOffset; + x1 += window.pageXOffset; + if (Math.abs(x - x1) < 300) { + this._filterMenuOverlaySettings.positionStrategy.settings.horizontalDirection = HorizontalAlignment.Left; + this._filterMenuOverlaySettings.positionStrategy.settings.horizontalStartPoint = HorizontalAlignment.Right; + } else { + this._filterMenuOverlaySettings.positionStrategy.settings.horizontalDirection = HorizontalAlignment.Right; + this._filterMenuOverlaySettings.positionStrategy.settings.horizontalStartPoint = HorizontalAlignment.Left; + } + + this._filterMenuOverlaySettings.positionStrategy.settings.target = headerTarget; + this._filterMenuOverlaySettings.outlet = this.grid.outletDirective; + + this._componentOverlayId = + this._overlayService.attach(IgxGridExcelStyleFilteringComponent, this._filterMenuOverlaySettings, this._moduleRef); + this._overlayService.show(this._componentOverlayId, this._filterMenuOverlaySettings); + } + } + + private initFilteringSettings() { + this._filterMenuPositionSettings = { + verticalStartPoint: VerticalAlignment.Bottom, + openAnimation: useAnimation(fadeIn, { + params: { + duration: '250ms' + } + }), + closeAnimation: useAnimation(fadeOut, { + params: { + duration: '200ms' + } + }) + }; + + this._filterMenuOverlaySettings = { + closeOnOutsideClick: true, + modal: false, + positionStrategy: new ConnectedPositioningStrategy(this._filterMenuPositionSettings), + scrollStrategy: new CloseScrollStrategy() + }; + + this._overlayService.onOpening.pipe( + filter((overlay) => overlay.id === this._componentOverlayId), + takeUntil(this._destroy$)).subscribe((eventArgs) => { + this.onOverlayOpening(eventArgs); + }); + + this._overlayService.onOpened.pipe( + filter((overlay) => overlay.id === this._componentOverlayId), + takeUntil(this._destroy$)).subscribe((eventArgs) => { + this.onOverlayOpened(eventArgs); + }); + + this._overlayService.onClosed.pipe( + filter(overlay => overlay.id === this._componentOverlayId), + takeUntil(this._destroy$)).subscribe(() => { + this.onOverlayClosed(); + }); + } + + private onOverlayOpening(eventArgs) { + const instance = eventArgs.componentRef.instance as IgxGridExcelStyleFilteringComponent; + if (instance) { + instance.initialize(this.column, this._filteringService, this._overlayService, eventArgs.id); + } + } + + private onOverlayOpened(eventArgs) { + const instance = eventArgs.componentRef.instance as IgxGridExcelStyleFilteringComponent; + if (instance) { + instance.toggleDropdown(this._filterMenuOverlaySettings); + } + } + + private onOverlayClosed() { + this._componentOverlayId = null; + } } diff --git a/projects/igniteui-angular/src/lib/grids/grid.common.ts b/projects/igniteui-angular/src/lib/grids/grid.common.ts index 134fe081441..a48646c4313 100644 --- a/projects/igniteui-angular/src/lib/grids/grid.common.ts +++ b/projects/igniteui-angular/src/lib/grids/grid.common.ts @@ -67,14 +67,18 @@ export class IgxColumnResizerDirective implements OnInit, OnDestroy { takeUntil(this._destroy) )) ).subscribe((pos) => { + const left = this._left + pos; - this.left = left < this.restrictHResizeMin ? this.restrictHResizeMin + 'px' : left + 'px'; + const min = this._left - this.restrictHResizeMin; + const max = this._left + this.restrictHResizeMax; + + this.left = left < min ? min : left; - if (left > this.restrictHResizeMax) { - this.left = this.restrictHResizeMax + 'px'; - } else if (left > this.restrictHResizeMin) { - this.left = left + 'px'; + if (left > max) { + this.left = max; + } else if (left > max) { + this.left = left; } }); @@ -101,7 +105,7 @@ export class IgxColumnResizerDirective implements OnInit, OnDestroy { } public set left(val) { - requestAnimationFrame(() => this.element.nativeElement.style.left = val); + requestAnimationFrame(() => this.element.nativeElement.style.left = val + 'px'); } onMouseup(event) { @@ -403,7 +407,7 @@ export class IgxColumnMovingDropDirective extends IgxDropDirective implements On } get isDropTarget(): boolean { - return this._column && this._column.grid.hasMovableColumns && this.cms.column.movable; + return this._column && this._column.grid.hasMovableColumns && this.cms.column.movable && !this.cms.column.disablePinning; } get horizontalScroll(): any { diff --git a/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts index 8551539b3c6..0e1ee0f5aaa 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/column-resizing.spec.ts @@ -83,7 +83,7 @@ describe('IgxGrid - Deferred Column Resizing', () => { tick(); fixture.detectChanges(); - expect(grid.columns[0].width).toEqual('64px'); + expect(grid.columns[0].width).toEqual('80px'); expect(grid.columns[2].cells[0].value).toEqual('Brown'); @@ -161,7 +161,7 @@ describe('IgxGrid - Deferred Column Resizing', () => { tick(); fixture.detectChanges(); - expect(grid.columns[1].width).toEqual('70px'); + expect(grid.columns[1].width).toEqual('80px'); })); it('should resize sortable columns.', fakeAsync(() => { @@ -279,7 +279,7 @@ describe('IgxGrid - Deferred Column Resizing', () => { tick(); fixture.detectChanges(); - expect(grid.columns[0].width).toEqual('70px'); + expect(grid.columns[0].width).toEqual('80px'); headerResArea = headers[1].parent.children[1].nativeElement; UIInteractions.simulateMouseEvent('mousedown', headerResArea, 197, 5); @@ -311,7 +311,7 @@ describe('IgxGrid - Deferred Column Resizing', () => { tick(); fixture.detectChanges(); - expect(grid.columns[1].width).toEqual('64px'); + expect(grid.columns[1].width).toEqual('80px'); })); it('should resize pinned column with preset max width.', fakeAsync(() => { @@ -360,7 +360,7 @@ describe('IgxGrid - Deferred Column Resizing', () => { tick(); fixture.detectChanges(); - expect(grid.columns[0].width).toEqual('78px'); + expect(grid.columns[0].width).toEqual('80px'); resizeArea = headers[1].children[1].nativeElement; UIInteractions.simulateMouseEvent('mouseover', resizeArea, 248, 5); @@ -375,14 +375,14 @@ describe('IgxGrid - Deferred Column Resizing', () => { tick(); fixture.detectChanges(); - expect(grid.columns[2].width).toEqual('78px'); + expect(grid.columns[2].width).toEqual('80px'); resizeArea = headers[3].children[1].nativeElement; UIInteractions.simulateMouseEvent('dblclick', resizeArea, 400, 5); tick(); fixture.detectChanges(); - expect(grid.columns[3].width).toEqual('73px'); + expect(grid.columns[3].width).toEqual('80px'); resizeArea = headers[5].children[1].nativeElement; UIInteractions.simulateMouseEvent('dblclick', resizeArea, 486, 5); diff --git a/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts index 5a754219d23..aec26427896 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/column.spec.ts @@ -8,6 +8,7 @@ import { SampleTestData } from '../../test-utils/sample-test-data.spec'; import { ColumnHiddenFromMarkupComponent, ColumnCellFormatterComponent } from '../../test-utils/grid-samples.spec'; import { wait } from '../../test-utils/ui-interactions.spec'; import { configureTestSuite } from '../../test-utils/configure-suite'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('IgxGrid - Column properties', () => { configureTestSuite(); @@ -25,7 +26,7 @@ describe('IgxGrid - Column properties', () => { ColumnHaederClassesComponent, ColumnHiddenFromMarkupComponent ], - imports: [IgxGridModule.forRoot()] + imports: [IgxGridModule.forRoot(), NoopAnimationsModule] }) .compileComponents(); })); diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts index 64b882d0916..a603754c019 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts @@ -27,6 +27,7 @@ import { IgxGridHeaderGroupComponent } from '../grid-header-group.component'; import { changei18n, getCurrentResourceStrings } from '../../core/i18n/resources'; import { registerLocaleData } from '@angular/common'; import localeDE from '@angular/common/locales/de'; +import { FilterMode } from '../tree-grid'; const FILTER_UI_ROW = 'igx-grid-filtering-row'; @@ -2797,6 +2798,161 @@ describe('IgxGrid - Filtering Row UI actions', () => { })); }); +describe('IgxGrid - Filtering actions - Excel style filtering', () => { + configureTestSuite(); + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + IgxGridFilteringComponent + ], + imports: [ + NoopAnimationsModule, + IgxGridModule.forRoot()] + }) + .compileComponents(); + })); + + afterEach(() => { + UIInteractions.clearOverlay(); + }); + + it('Should sorts the grid properly, when clicking Ascending/Descending buttons.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should toggle correct Ascending/Descending button on opening when sorting is applied.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should move column left/right when clicking buttons.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should pin/unpin column when clicking buttons.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should hide column when click on button.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should activate clear button when a value is entered in the first input.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should not select values in list if two values with And operator are entered.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should not select values in list if two values with Or operator are entered and contains operand.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should select values in list if two values with Or operator are entered and they are in the list below.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should change selection of the list when changing And/Or operator.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should change selection of the list when changing operator.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should populate inputs when deselect all values and then select two.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should clear the filter when select ‘all filters’ item.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should update filter icon when dialog is closed and the filter has been changed.', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + + it('Should open another filter dialog and populates the correct operator when selecting item', fakeAsync(() => { + const fix = TestBed.createComponent(IgxGridFilteringComponent); + const grid = fix.componentInstance.grid; + grid.filterMode = FilterMode.excelStyleFilter; + fix.detectChanges(); + + // TODO + })); + +}); + export class CustomFilter extends IgxFilteringOperand { private static _instance: CustomFilter; diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid.crud.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid.crud.spec.ts index 7a4b1135925..c66c7acdbb3 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid.crud.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid.crud.spec.ts @@ -6,6 +6,7 @@ import { IGridEditEventArgs } from '../grid-base.component'; import { IgxGridModule } from './index'; import { wait } from '../../test-utils/ui-interactions.spec'; import { configureTestSuite } from '../../test-utils/configure-suite'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; const CELL_CSS_CLASS = '.igx-grid__td'; @@ -21,7 +22,7 @@ describe('IgxGrid - CRUD operations', () => { declarations: [ DefaultCRUDGridComponent ], - imports: [IgxGridModule.forRoot()] + imports: [IgxGridModule.forRoot(), NoopAnimationsModule] }).compileComponents(); })); diff --git a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts index 5547edc2e21..c669dfc293d 100644 --- a/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.integration.spec.ts @@ -228,7 +228,7 @@ describe('IgxHierarchicalGrid Integration', () => { expect(fChildCell.value).toBe('09'); const icon = childHeaders[0].query(By.css('.sort-icon')); expect(icon).not.toBeNull(); - expect(icon.nativeElement.textContent.toLowerCase()).toBe('arrow_downward'); + expect(icon.nativeElement.textContent.toLowerCase().trim()).toBe('arrow_downward'); }); }); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-crud.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-crud.spec.ts index ddf3c8224dc..af0805385ae 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-crud.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-crud.spec.ts @@ -8,10 +8,12 @@ import { first } from 'rxjs/operators'; import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec'; import { DropPosition } from '../grid'; import { configureTestSuite } from '../../test-utils/configure-suite'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; const DEBOUNCETIME = 30; const CELL_CSS_CLASS = '.igx-grid__td'; + describe('IgxTreeGrid - CRUD', () => { configureTestSuite(); let fix; @@ -23,7 +25,7 @@ describe('IgxTreeGrid - CRUD', () => { IgxTreeGridSimpleComponent, IgxTreeGridPrimaryForeignKeyComponent ], - imports: [IgxTreeGridModule] + imports: [IgxTreeGridModule, NoopAnimationsModule] }) .compileComponents(); })); diff --git a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts index ec980713cb7..097fd804e6c 100644 --- a/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-sorting.spec.ts @@ -6,6 +6,7 @@ import { IgxTreeGridSortingComponent } from '../../test-utils/tree-grid-componen import { TreeGridFunctions } from '../../test-utils/tree-grid-functions.spec'; import { configureTestSuite } from '../../test-utils/configure-suite'; import { DefaultSortingStrategy } from '../../data-operations/sorting-strategy'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('IgxTreeGrid - Sorting', () => { configureTestSuite(); @@ -17,7 +18,7 @@ describe('IgxTreeGrid - Sorting', () => { declarations: [ IgxTreeGridSortingComponent ], - imports: [IgxTreeGridModule] + imports: [IgxTreeGridModule, NoopAnimationsModule] }) .compileComponents(); })); diff --git a/projects/igniteui-angular/src/lib/select/select.component.spec.ts b/projects/igniteui-angular/src/lib/select/select.component.spec.ts index d156c44a57b..a7533627d3f 100644 --- a/projects/igniteui-angular/src/lib/select/select.component.spec.ts +++ b/projects/igniteui-angular/src/lib/select/select.component.spec.ts @@ -1888,6 +1888,17 @@ describe('igxSelect', () => { })); }); }); + describe('EditorProvider', () => { + beforeEach(async(() => { + fixture = TestBed.createComponent(IgxSelectSimpleComponent); + fixture.detectChanges(); + })); + it('Should return correct edit element', () => { + inputElement = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUT)).nativeElement; + const selectInstance = fixture.componentInstance.select; + expect(selectInstance.getEditElement()).toEqual(inputElement); + }); + }); }); @Component({ diff --git a/projects/igniteui-angular/src/lib/select/select.component.ts b/projects/igniteui-angular/src/lib/select/select.component.ts index 55e0966b0ff..891f858b140 100644 --- a/projects/igniteui-angular/src/lib/select/select.component.ts +++ b/projects/igniteui-angular/src/lib/select/select.component.ts @@ -23,6 +23,7 @@ import { IgxSelectItemNavigationDirective } from './select-navigation.directive' import { CancelableEventArgs } from '../core/utils'; import { IgxLabelDirective } from '../directives/label/label.directive'; import { IgxSelectBase } from './select.common'; +import { EditorProvider } from '../core/edit-provider'; /** @hidden @internal */ @Directive({ @@ -56,7 +57,8 @@ const noop = () => { }; { provide: NG_VALUE_ACCESSOR, useExisting: IgxSelectComponent, multi: true }, { provide: IGX_DROPDOWN_BASE, useExisting: IgxSelectComponent }] }) -export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor, AfterContentInit { +export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelectBase, ControlValueAccessor, AfterContentInit, + EditorProvider { /** @hidden @internal do not use the drop-down container class */ public cssClass = false; @@ -193,6 +195,11 @@ export class IgxSelectComponent extends IgxDropDownComponent implements IgxSelec /** @hidden @internal */ public registerOnTouched(fn: any): void { } + /** @hidden @internal */ + public getEditElement(): HTMLElement { + return this.input.nativeElement; + } + /** @hidden @internal */ public selectItem(newSelection: IgxDropDownItemBase, event?) { const oldSelection = this.selectedItem; diff --git a/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts index 13a091a446b..dc26fc940c7 100644 --- a/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/csv/csv-exporter-grid.spec.ts @@ -17,6 +17,7 @@ import { DefaultSortingStrategy } from '../../data-operations/sorting-strategy'; import { IgxStringFilteringOperand, IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; import { configureTestSuite } from '../../test-utils/configure-suite'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('CSV Grid Exporter', () => { configureTestSuite(); @@ -32,7 +33,7 @@ describe('CSV Grid Exporter', () => { IgxTreeGridPrimaryForeignKeyComponent, ProductsComponent ], - imports: [IgxGridModule.forRoot(), IgxTreeGridModule] + imports: [IgxGridModule.forRoot(), IgxTreeGridModule, NoopAnimationsModule] }) .compileComponents(); })); diff --git a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts index 09e4c278a6c..a3f7d3c9edf 100644 --- a/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts +++ b/projects/igniteui-angular/src/lib/services/excel/excel-exporter-grid.spec.ts @@ -19,6 +19,7 @@ import { IgxTreeGridPrimaryForeignKeyComponent } from '../../test-utils/tree-gri import { IgxTreeGridModule, IgxTreeGridComponent } from '../../grids/tree-grid'; import { IgxNumberFilteringOperand } from '../../data-operations/filtering-condition'; import { wait } from '../../test-utils/ui-interactions.spec'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('Excel Exporter', () => { configureTestSuite(); @@ -34,7 +35,7 @@ describe('Excel Exporter', () => { IgxTreeGridPrimaryForeignKeyComponent, ProductsComponent ], - imports: [IgxGridModule.forRoot(), IgxTreeGridModule] + imports: [IgxGridModule.forRoot(), IgxTreeGridModule, NoopAnimationsModule] }).compileComponents(); })); diff --git a/src/app/grid-column-moving/grid-column-moving.sample.html b/src/app/grid-column-moving/grid-column-moving.sample.html index 4738448f110..771052f7b49 100644 --- a/src/app/grid-column-moving/grid-column-moving.sample.html +++ b/src/app/grid-column-moving/grid-column-moving.sample.html @@ -13,10 +13,11 @@ (onColumnMoving)="onColumnMoving($event)" (onColumnMovingEnd)="onColumnMovingEnd($event)" [rowSelectable]="true" - [rowEditable]="false" + [filterMode]="'excelStyleFilter'" [paging]="false" [width]="'900px'" - [height]="'600px'"> + [height]="'800px'" + [style.zIndex]="'1'"> Fixed Size Rows - +