diff --git a/src/app/components/api/primengconfig.ts b/src/app/components/api/primengconfig.ts index b4282cb0c7e..c4858748e86 100644 --- a/src/app/components/api/primengconfig.ts +++ b/src/app/components/api/primengconfig.ts @@ -3,11 +3,17 @@ import { Subject } from 'rxjs'; import { FilterMatchMode } from './filtermatchmode'; import { Translation } from './translation'; +interface OverlayOptions { + breakpoint: number; +} + @Injectable({providedIn: 'root'}) export class PrimeNGConfig { ripple: boolean = false; + overlayOptions: OverlayOptions; + filterMatchModeOptions = { text: [ FilterMatchMode.STARTS_WITH, diff --git a/src/app/components/dropdown/dropdown.css b/src/app/components/dropdown/dropdown.css index b0b02d7c3cf..5f529c53717 100755 --- a/src/app/components/dropdown/dropdown.css +++ b/src/app/components/dropdown/dropdown.css @@ -85,4 +85,4 @@ input.p-dropdown-label { .p-fluid .p-dropdown .p-dropdown-label { width: 1%; -} +} \ No newline at end of file diff --git a/src/app/components/dropdown/dropdown.ts b/src/app/components/dropdown/dropdown.ts index dd56728b40f..8f252f24fc8 100755 --- a/src/app/components/dropdown/dropdown.ts +++ b/src/app/components/dropdown/dropdown.ts @@ -1,16 +1,17 @@ import {NgModule,Component,ElementRef,OnInit,AfterViewInit,AfterContentInit,AfterViewChecked,OnDestroy,Input,Output,Renderer2,EventEmitter,ContentChildren, QueryList,ViewChild,TemplateRef,forwardRef,ChangeDetectorRef,NgZone,ViewRef,ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; -import {trigger,style,transition,animate,AnimationEvent} from '@angular/animations'; +import {trigger,transition,AnimationEvent, query, animateChild} from '@angular/animations'; import {CommonModule} from '@angular/common'; -import {OverlayService, PrimeNGConfig, SelectItem, TranslationKeys} from 'primeng/api'; +import {PrimeNGConfig, SelectItem, TranslationKeys} from 'primeng/api'; import {SharedModule,PrimeTemplate, FilterService} from 'primeng/api'; import {DomHandler, ConnectedOverlayScrollHandler} from 'primeng/dom'; -import {ObjectUtils,UniqueComponentId,ZIndexUtils} from 'primeng/utils'; +import {ObjectUtils,UniqueComponentId} from 'primeng/utils'; import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; import {TooltipModule} from 'primeng/tooltip'; import {Scroller, ScrollerModule, ScrollerOptions} from 'primeng/scroller'; import {RippleModule} from 'primeng/ripple'; import {AutoFocusModule} from 'primeng/autofocus'; +import {Overlay, OverlayModule} from '../overlay/overlay'; export const DROPDOWN_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, @@ -86,87 +87,79 @@ export class DropdownItem {
-
- -
- - - - -
- - -
-
-
-
- - - + +
+ +
+ + + + +
+ + +
- - - +
+
+ + + - - - - - - - -
    - - -
  • - {{getOptionGroupLabel(optgroup)||'empty'}} - -
  • - + + + - - - - - - - - -
  • - - {{emptyFilterMessageLabel}} + + + + + + +
      + + +
    • + {{getOptionGroupLabel(optgroup)||'empty'}} + +
    • + +
      - - -
    • - - {{emptyMessageLabel}} + + - -
    • -
    -
    + + + + + +
  • + + {{emptyFilterMessageLabel}} + + +
  • +
  • + + {{emptyMessageLabel}} + + +
  • +
+
+
+
- -
+
`, - animations: [ - trigger('overlayAnimation', [ - transition(':enter', [ - style({opacity: 0, transform: 'scaleY(0.8)'}), - animate('{{showTransitionParams}}') - ]), - transition(':leave', [ - animate('{{hideTransitionParams}}', style({ opacity: 0 })) - ]) - ]) - ], + host: { 'class': 'p-element p-inputwrapper', '[class.p-inputwrapper-filled]': 'filled', @@ -279,6 +272,8 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView @Input() autofocusFilter: boolean = true; + @Input() overlayDirection: string = 'end'; + @Output() onChange: EventEmitter = new EventEmitter(); @Output() onFilter: EventEmitter = new EventEmitter(); @@ -309,6 +304,8 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView @ViewChild('scroller') scroller: Scroller; + @ViewChild('overlay') overlayViewChild: Overlay; + @ContentChildren(PrimeTemplate) templates: QueryList; private _disabled: boolean; @@ -341,8 +338,6 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView console.warn("The itemSize property is deprecated, use virtualScrollItemSize property instead."); } - overlay: HTMLDivElement; - itemsWrapper: HTMLDivElement; itemTemplate: TemplateRef; @@ -421,7 +416,7 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView listId: string; - constructor(public el: ElementRef, public renderer: Renderer2, public cd: ChangeDetectorRef, public zone: NgZone, public filterService: FilterService, public config: PrimeNGConfig, public overlayService: OverlayService) {} + constructor(public el: ElementRef, public renderer: Renderer2, public cd: ChangeDetectorRef, public zone: NgZone, public filterService: FilterService, public config: PrimeNGConfig) {} ngAfterContentInit() { this.templates.forEach((item) => { @@ -601,15 +596,15 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView this.zone.runOutsideAngular(() => { setTimeout(() => { - this.alignOverlay(); + this.overlayViewChild.alignOverlay(); }, 1); }); } if (this.selectedOptionUpdated && this.itemsWrapper) { - let selectedItem = DomHandler.findSingle(this.overlay, 'li.p-highlight'); + let selectedItem = DomHandler.findSingle(this.overlayViewChild.el.nativeElement, 'li.p-highlight'); if (selectedItem) { - DomHandler.scrollInView(this.itemsWrapper, DomHandler.findSingle(this.overlay, 'li.p-highlight')); + DomHandler.scrollInView(this.itemsWrapper, DomHandler.findSingle(this.overlayViewChild.el.nativeElement, 'li.p-highlight')); } this.selectedOptionUpdated = false; } @@ -682,13 +677,6 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView this.cd.detectChanges(); } - onOverlayClick(event) { - this.overlayService.add({ - originalEvent: event, - target: this.el.nativeElement - }); - } - isInputClick(event): boolean { return DomHandler.hasClass(event.target, 'p-dropdown-clear-icon') || event.target.isSameNode(this.accessibleViewChild.nativeElement) || @@ -696,7 +684,7 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView } isOutsideClicked(event: Event): boolean { - return !(this.el.nativeElement.isSameNode(event.target) || this.el.nativeElement.contains(event.target) || (this.overlay && this.overlay.contains( event.target))); + return !(this.el.nativeElement.isSameNode(event.target) || this.el.nativeElement.contains(event.target) || (this.overlayViewChild && this.overlayViewChild.el.nativeElement.contains( event.target))); } isEmpty() { @@ -730,78 +718,49 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView } onOverlayAnimationStart(event: AnimationEvent) { - switch (event.toState) { - case 'visible': - this.overlay = event.element; - this.itemsWrapper = DomHandler.findSingle(this.overlay, this.virtualScroll ? '.p-scroller' : '.p-dropdown-items-wrapper'); - this.virtualScroll && this.scroller.setContentEl(this.itemsViewChild.nativeElement); - this.appendOverlay(); - if (this.autoZIndex) { - ZIndexUtils.set('overlay', this.overlay, this.baseZIndex + this.config.zIndex.overlay); - } - this.alignOverlay(); - this.bindDocumentClickListener(); - this.bindDocumentResizeListener(); - this.bindScrollListener(); - - if (this.options && this.options.length) { - if (this.virtualScroll) { - const selectedIndex = this.selectedOption ? this.findOptionIndex(this.getOptionValue(this.selectedOption), this.optionsToDisplay) : -1; - if (selectedIndex !== -1) { - this.scroller.scrollToIndex(selectedIndex); - } - } - else { - let selectedListItem = DomHandler.findSingle(this.itemsWrapper, '.p-dropdown-item.p-highlight'); - - if (selectedListItem) { - selectedListItem.scrollIntoView({ block: 'nearest', inline: 'center' }); - } + if (event.toState === 'visible') { + this.itemsWrapper = DomHandler.findSingle(this.overlayViewChild.el.nativeElement, this.virtualScroll ? '.p-scroller' : '.p-dropdown-items-wrapper'); + this.virtualScroll && this.scroller.setContentEl(this.itemsViewChild.nativeElement); + this.overlayViewChild.appendOverlay(); + this.overlayViewChild.alignOverlay(); + this.bindDocumentClickListener(); + this.bindScrollListener(); + + if (this.options && this.options.length) { + if (this.virtualScroll) { + const selectedIndex = this.selectedOption ? this.findOptionIndex(this.getOptionValue(this.selectedOption), this.optionsToDisplay) : -1; + if (selectedIndex !== -1) { + this.scroller.scrollToIndex(selectedIndex); } } + else { + let selectedListItem = DomHandler.findSingle(this.itemsWrapper, '.p-dropdown-item.p-highlight'); - if (this.filterViewChild && this.filterViewChild.nativeElement) { - this.preventModelTouched = true; - - if (this.autofocusFilter) { - this.filterViewChild.nativeElement.focus(); + if (selectedListItem) { + selectedListItem.scrollIntoView({ block: 'nearest', inline: 'center' }); } } + } - this.onShow.emit(event); - break; + if (this.filterViewChild && this.filterViewChild.nativeElement) { + this.preventModelTouched = true; - case 'void': - this.onOverlayHide(); - this.onHide.emit(event); - break; - } - } + if (this.autofocusFilter) { + this.filterViewChild.nativeElement.focus(); + } + } - onOverlayAnimationEnd(event: AnimationEvent) { - switch (event.toState) { - case 'void': - ZIndexUtils.clear(event.element); - break; + this.onShow.emit(event); } - } - - appendOverlay() { - if (this.appendTo) { - if (this.appendTo === 'body') - document.body.appendChild(this.overlay); - else - DomHandler.appendChild(this.overlay, this.appendTo); - - if (!this.overlay.style.minWidth) { - this.overlay.style.minWidth = DomHandler.getWidth(this.containerViewChild.nativeElement) + 'px'; - } + if (event.toState === 'void') { + this.onOverlayHide(); + this.onHide.emit(event); } } restoreOverlayAppend() { - if (this.overlay && this.appendTo) { - this.el.nativeElement.appendChild(this.overlay); + if (this.overlayViewChild && this.appendTo) { + this.el.nativeElement.appendChild(this.overlayViewChild.el.nativeElement); } } @@ -811,19 +770,9 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView if (this.filter && this.resetFilterOnHide) { this.resetFilter(); } - this.cd.markForCheck(); } - alignOverlay() { - if (this.overlay) { - if (this.appendTo) - DomHandler.absolutePosition(this.overlay, this.containerViewChild.nativeElement); - else - DomHandler.relativePosition(this.overlay, this.containerViewChild.nativeElement); - } - } - onInputFocus(event) { this.focused = true; this.onFocus.emit(event); @@ -1288,7 +1237,6 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView this.unbindDocumentClickListener(); this.unbindDocumentResizeListener(); this.unbindScrollListener(); - this.overlay = null; this.itemsWrapper = null; this.onModelTouched(); } @@ -1298,19 +1246,15 @@ export class Dropdown implements OnInit,AfterViewInit,AfterContentInit,AfterView this.scrollHandler.destroy(); this.scrollHandler = null; } - - if (this.overlay) { - ZIndexUtils.clear(this.overlay); - } - this.restoreOverlayAppend(); this.onOverlayHide(); } } @NgModule({ - imports: [CommonModule,SharedModule,TooltipModule,RippleModule,ScrollerModule, AutoFocusModule], + imports: [CommonModule,OverlayModule,SharedModule,TooltipModule,RippleModule,ScrollerModule, AutoFocusModule], exports: [Dropdown,SharedModule,ScrollerModule], - declarations: [Dropdown,DropdownItem] + declarations: [Dropdown,DropdownItem], + entryComponents: [Overlay] }) export class DropdownModule { } diff --git a/src/app/components/overlay/ng-package.json b/src/app/components/overlay/ng-package.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/components/overlay/overlay.css b/src/app/components/overlay/overlay.css new file mode 100644 index 00000000000..a7fc4f67c0d --- /dev/null +++ b/src/app/components/overlay/overlay.css @@ -0,0 +1,37 @@ +.p-overlay-mask { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.p-overlay-responsive { + overflow: hidden; + position: static; + height: fit-content; + width: inherit; +} + +.p-overlay-panel-start { + align-items: flex-start; +} + +.p-overlay-panel-center { + align-items: center; +} + +.p-overlay-panel-end { + align-items: flex-end; +} + +.p-overlay { + position: absolute; +} + +.p-overlay-panel-static { + position: static; +} \ No newline at end of file diff --git a/src/app/components/overlay/overlay.ts b/src/app/components/overlay/overlay.ts new file mode 100644 index 00000000000..df3ffb421a7 --- /dev/null +++ b/src/app/components/overlay/overlay.ts @@ -0,0 +1,266 @@ +import {NgModule,Component,Input,OnDestroy,Renderer2,ElementRef,ChangeDetectionStrategy, ViewEncapsulation, ViewChild, Inject, Output, EventEmitter, ChangeDetectorRef} from '@angular/core'; +import {CommonModule, DOCUMENT} from '@angular/common'; +import {DomHandler} from 'primeng/dom'; +import {SharedModule, PrimeNGConfig, OverlayService} from 'primeng/api'; +import {RippleModule} from 'primeng/ripple'; +import {trigger,style,transition,animate, animation, useAnimation} from '@angular/animations'; +import {ZIndexUtils} from 'primeng/utils'; + +const showAnimation = animation([ + style({ transform: '{{transform}}', opacity: 0 }), + animate('{{showTransitionParams}}') +]); + +const hideAnimation = animation([ + animate('{{hideTransitionParams}}', style({ transform: '{{transform}}', opacity: 0 })) +]); + +@Component({ + selector: 'p-overlay', + template: ` +
+
+
+ +
+
+
+ `, + animations: [ + trigger('overlayAnimation', [ + transition(':enter', [ + useAnimation(showAnimation) + ]), + transition(':leave', [ + useAnimation(hideAnimation) + ]) + ]) + ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./overlay.css'], + host: { + 'class': 'p-element' + } +}) +export class Overlay implements OnDestroy { + + @Input() baseZIndex: number = 0; + + @Input() showTransitionOptions: string = '.12s cubic-bezier(0, 0, 0.2, 1)'; + + @Input() hideTransitionOptions: string = '.1s linear'; + + @Input() container: ElementRef; + + @Input() autoZIndex: boolean; + + @Output() onAnimationStart: EventEmitter = new EventEmitter(); + + @Output() onAnimationEnd: EventEmitter = new EventEmitter(); + + @Output() onOverlayHide: EventEmitter = new EventEmitter(); + + @ViewChild('overlay') overlayViewChild: ElementRef; + + @ViewChild('content') contentViewChild: ElementRef; + + @ViewChild('mask') maskViewChild: ElementRef; + + @Input() set overlayDirection(value: string) { + if (value && this.viewport.width < this.overlayBreakpoints) { + this._overlayDirection = value; + + switch (value) { + case 'start': + this.transformOptions = "translate3d(0px, -100%, 0px)"; + break; + case 'end': + this.transformOptions = "translate3d(0px, 100%, 0px)"; + break; + case 'center': + this.transformOptions = "scale(0.8)"; + break; + } + } + else { + this.transformOptions = "scaleY(0.8)"; + } + } + + get overlayDirection(): string { + return this._overlayDirection; + } + + @Input() set appendTo(val) { + this._appendTo = val; + } + + get appendTo(): any { + return this._appendTo; + } + + @Input() set visible(value: boolean) { + this._visible = value; + } + + get visible(): boolean { + return this._visible; + } + + get overlayBreakpoints(): any { + return this.config.overlayOptions ? this.config.overlayOptions.breakpoint : null; + } + + get viewport(): any { + this._viewport = DomHandler.getViewport(); + return this._viewport; + } + + _visible: boolean; + + _overlayDirection: string; + + _viewport: any; + + _overlayBreakpoints: number; + + _appendTo: any; + + transformOptions: string = "scaleY(0.8)"; + + documentResizeListener: any; + + responsive: boolean; + + constructor(@Inject(DOCUMENT) private document: Document, public el: ElementRef, public renderer: Renderer2, private config: PrimeNGConfig, public overlayService: OverlayService, private cd: ChangeDetectorRef) {} + + ngOnInit() { + this.bindDocumentResizeListener(); + } + + onOverlayClick(event: MouseEvent) { + this.overlayService.add({ + originalEvent: event, + target: this.el.nativeElement + }); + } + + onOverlayAnimationStart(event) { + this.onAnimationStart.emit(event); + } + + onOverlayAnimationEnd(event) { + if (event.toState === 'void') { + this.destroyOverlay(); + } + this.onAnimationEnd.emit(event); + } + + appendOverlay() { + if (this.autoZIndex) { + ZIndexUtils.set('modal', this.el.nativeElement, this.baseZIndex + this.config.zIndex.modal); + } + if (this.viewport.width < this.overlayBreakpoints) { + this.responsive = true; + DomHandler.addClass(this.document.body, 'p-overflow-hidden'); + DomHandler.addClass(this.overlayViewChild.nativeElement, 'p-overlay-responsive'); + DomHandler.addClass(this.contentViewChild.nativeElement.firstChild, 'p-overlay-panel-static'); + } + else { + this.responsive = false; + if (this.appendTo) { + if (this.appendTo === 'body') { + this.document.body.appendChild(this.overlayViewChild.nativeElement); + } + else { + DomHandler.appendChild(this.overlayViewChild.nativeElement, this.appendTo); + } + } + } + if (!this.contentViewChild.nativeElement.style.minWidth) { + this.contentViewChild.nativeElement.style.minWidth = DomHandler.getWidth(this.container) + 'px'; + } + } + + alignOverlay() { + if(this.overlayViewChild) { + if (this.viewport.width < this.overlayBreakpoints) { + switch (this.overlayDirection) { + case 'start': + DomHandler.addClass(this.maskViewChild.nativeElement, 'p-overlay-panel-start') + break; + + case 'center': + DomHandler.addClass(this.maskViewChild.nativeElement, 'p-overlay-panel-center') + break; + + case 'end': + DomHandler.addClass(this.maskViewChild.nativeElement, 'p-overlay-panel-end') + break; + } + } + else { + if (this.appendTo) { + DomHandler.absolutePosition(this.overlayViewChild.nativeElement, this.container); + } + else { + DomHandler.relativePosition(this.overlayViewChild.nativeElement, this.container); + } + } + } + } + + blockScroll() { + DomHandler.addClass(this.document.body, 'p-overflow-hidden'); + } + + unblockScroll() { + DomHandler.removeClass(this.document.body, 'p-overflow-hidden'); + } + + bindDocumentResizeListener() { + if (!this.documentResizeListener) { + this.documentResizeListener = this.renderer.listen(window, 'resize', this.onWindowResize.bind(this)); + } + } + + unbindDocumentResizeListener() { + if (this.documentResizeListener) { + this.documentResizeListener(); + this.documentResizeListener = null; + } + } + + onWindowResize() { + if (this.visible) { + this.visible = false; + this.onOverlayHide.emit({visible: this.visible}); + this.cd.markForCheck(); + } + } + + destroyOverlay() { + this.unblockScroll(); + this.unbindDocumentResizeListener(); + + if (this.overlayViewChild && this.overlayViewChild.nativeElement) { + ZIndexUtils.clear(this.el.nativeElement); + this.overlayViewChild = null; + } + + this.onOverlayHide.emit({visible: this.visible}); + } + + ngOnDestroy() { + this.destroyOverlay(); + } + +} + +@NgModule({ + imports: [CommonModule,RippleModule, SharedModule], + exports: [Overlay, SharedModule], + declarations: [Overlay] +}) +export class OverlayModule { } diff --git a/src/app/components/overlay/public_api.ts b/src/app/components/overlay/public_api.ts new file mode 100644 index 00000000000..5d4bcbb5787 --- /dev/null +++ b/src/app/components/overlay/public_api.ts @@ -0,0 +1 @@ +export * from './overlay'; \ No newline at end of file