diff --git a/packages/mosaic-dev/popover/module.ts b/packages/mosaic-dev/popover/module.ts index 1a28716e6..9b6418225 100644 --- a/packages/mosaic-dev/popover/module.ts +++ b/packages/mosaic-dev/popover/module.ts @@ -1,10 +1,16 @@ import { A11yModule } from '@angular/cdk/a11y'; import { Component, NgModule, ViewEncapsulation } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { McButtonModule } from '@ptsecurity/mosaic/button'; +import { McCheckboxModule } from '@ptsecurity/mosaic/checkbox'; +import { McFormFieldModule } from '@ptsecurity/mosaic/form-field'; +import { McInputModule } from '@ptsecurity/mosaic/input'; import { McPopoverModule } from '@ptsecurity/mosaic/popover'; +import { McSelectModule } from '@ptsecurity/mosaic/select'; +import { McSplitterModule } from '@ptsecurity/mosaic/splitter'; import { McIconModule } from '../../mosaic/icon/'; @@ -18,9 +24,39 @@ import { McIconModule } from '../../mosaic/icon/'; }) export class DemoComponent { popoverActiveStage: number; + selectedOrder: boolean; isPopoverVisibleLeft: boolean = false; + activatedPosition: string = ''; + + ELEMENTS = { + BUTTON: 'button', + INPUT: 'input', + ICON: 'icon' + }; + + TRIGGERS = { + CLICK: 'click', + FOCUS: 'focus', + HOVER: 'hover' + }; + + SIZE = { + LARGE: 'large', + NORMAL: 'normal', + SMALL: 'small' + }; + + selectedElement: string = 'button'; + selectedPlacement: string = 'left'; + selectedTrigger: string = 'click'; + selectedSize: string = 'normal'; + layoutClass: string = 'flex-65 layout-row layout-align-center-center'; + content: string = 'button text'; + userDefinedPlacementPriority: string[] = ['bottom', 'right']; + multipleSelected: string[] = []; + constructor() { this.popoverActiveStage = 1; } @@ -34,16 +70,43 @@ export class DemoComponent { this.popoverActiveStage += direction; } - changePopoverVisibilityLeft() { - this.isPopoverVisibleLeft = !this.isPopoverVisibleLeft; + onPopoverVisibleChange($event) { + if (!$event) { + this.activatedPosition = ''; + } + } + + onStrategyPlacementChange(event) { + this.activatedPosition = event; + } + + setPlacement(placement: string) { + this.selectedPlacement = placement; + } + + showElement(): string { + return this.selectedElement; + } + + activated(value: string): boolean { + return this.selectedPlacement === value; + } + + isActual(value: string): boolean { + return this.activatedPosition === value && this.selectedPlacement !== this.activatedPosition; } - onPopoverVisibleChangeLeft(update: boolean) { - this.isPopoverVisibleLeft = update; + getOrder(forElement: string) { + if (forElement === 'config') { + return this.selectedOrder ? {order: 2} : {order: 1}; + } + if (forElement === 'result') { + return this.selectedOrder ? {order: 1} : {order: 2}; + } } - onPopoverVisibleChange() { - console.log('onPopoverVisibleChange'); // tslint:disable-line:no-console + get isFallbackActivated(): boolean { + return this.selectedPlacement !== this.activatedPosition && this.activatedPosition !== ''; } } @@ -55,9 +118,15 @@ export class DemoComponent { BrowserModule, BrowserAnimationsModule, A11yModule, + FormsModule, + McFormFieldModule, + McSelectModule, McPopoverModule, McButtonModule, - McIconModule + McIconModule, + McInputModule, + McSplitterModule, + McCheckboxModule ], bootstrap: [ DemoComponent diff --git a/packages/mosaic-dev/popover/styles.scss b/packages/mosaic-dev/popover/styles.scss index 183989cc8..8fc277d7f 100644 --- a/packages/mosaic-dev/popover/styles.scss +++ b/packages/mosaic-dev/popover/styles.scss @@ -16,12 +16,127 @@ body, html { app { height: 100%; width: 100%; - display: flex; - flex: 1 1 100%; - flex-flow: column; - justify-content: center; - align-items: stretch; - padding: 0 10%; + + .actual { + animation: blink-animation 1s steps(5, start) infinite; + -webkit-animation: blink-animation 1s steps(5, start) infinite; + } + @keyframes blink-animation { + to { + visibility: hidden; + } + } + @-webkit-keyframes blink-animation { + to { + visibility: hidden; + } + } + + .red { + color: red; + } + + .container { + width: 100%; + height: 100%; + padding: 1%; + } + + .config { + height: 50%; + width: 100%; + } + + .result { + border-top: solid 2px #56d1ff; + border-bottom: solid 2px #56d1ff; + height: 50%; + width: 100%; + } + + .padding-32 { + padding: 8px; + } + + .visual-box { + height: 100px; + width: 240px; + border: 1px solid #575757; + position: relative; + margin: 50px; + + .visual-box--clickable { + height: 12px; + width: 12px; + position: absolute; + background: #4dc3ff; + cursor: pointer; + } + + .active { + background: red; + } + + &_placement-top-left { + top: -6px; + left: 10px; + } + + &_placement-top { + top: -6px; + left: calc(50% - 6px); + } + + &_placement-top-right { + top: -6px; + right: 10px; + } + + &_placement-left-top { + top: 10px; + left: -6px; + } + + &_placement-left { + top: calc(50% - 6px); + left: -6px; + } + + &_placement-left-bottom { + bottom: 10px; + left: -6px; + } + + &_placement-bottom-right { + bottom: -6px; + right: 10px; + } + + &_placement-bottom { + bottom: -6px; + right: calc(50% - 6px); + } + + &_placement-bottom-left { + bottom: -6px; + left: 10px; + } + + &_placement-right-bottom { + right: -6px; + bottom: 10px; + } + + &_placement-right { + right: -6px; + bottom: calc(50% - 6px); + } + + &_placement-right-top { + right: -6px; + top: 10px; + } + } } .popover-485 { diff --git a/packages/mosaic-dev/popover/template.html b/packages/mosaic-dev/popover/template.html index 36538387b..d8f318424 100644 --- a/packages/mosaic-dev/popover/template.html +++ b/packages/mosaic-dev/popover/template.html @@ -1,168 +1,252 @@ - - - - В западной традиции рыбой выступает фрагмент латинского текста из философского трактата Цицерона «О пределах добра и зла», написанного в 45 году до нашей эры. Впервые этот текст был применен для набора шрифтовых образцов неизвестным печатником еще в XVI веке. +
+ + + + В западной традиции рыбой выступает фрагмент латинского текста из философского трактата Цицерона «О пределах добра и зла», написанного в 45 году до нашей эры. Впервые этот текст был применен для набора шрифтовых образцов неизвестным печатником еще в XVI веке. + + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. + + + Сегодня этот текст используют практически все дизайнеры, набирающие рыбу латиницей. Абзац считается каноническим во всех справочниках по типографике и предлагается к использованию в статьях, посвященных изготовлению макета верстки при отсутствии финальных текстов. В руководствах по работе с фирменным стилем крупных международных компаний именно с этих слов начинаются образцы верстки. Существуют даже издания с названием Lorem ipsum. + - - Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. - - - Сегодня этот текст используют практически все дизайнеры, набирающие рыбу латиницей. Абзац считается каноническим во всех справочниках по типографике и предлагается к использованию в статьях, посвященных изготовлению макета верстки при отсутствии финальных текстов. В руководствах по работе с фирменным стилем крупных международных компаний именно с этих слов начинаются образцы верстки. Существуют даже издания с названием Lorem ipsum. - - - + - -
- - -
-
+ +
+ + +
+
+ +
+
+ + Yes/No +
+ + + + Button + Icon + Input + + -
- - - -
+ + + + Click + Hover + Focus + + -
-
- - - - rightBottom - - -
+ + + + Small + Normal + Large + + -
- - - -
-
+ + + + Start start + Start center + Start end + Center start + Center center + Center end + End start + End center + End end + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + Top + TopLeft + TopRight + Bottom + BottomLeft + BottomRight + Right + RightTop + RightBottom + Left + LeftTop + LeftBottom + + +
+
+

Configuration:

+
    +
  • Popover size: {{selectedSize}}
  • +
  • Popover trigger: {{selectedTrigger}}
  • +
  • Popover anchor placement: {{selectedPlacement}}
  • +
  • Popover anchored element: {{selectedElement}}
  • +
  • Popover activated fallback position: {{activatedPosition}}
  • +
  • Layout positioning: {{layoutClass}}
  • +
  • Placement priority order: {{multipleSelected}}
  • +
+ +
+
+ +
+
+ + + + +
+ +
+
+
-
- - - - - -
+
\ No newline at end of file diff --git a/packages/mosaic/core/overlay/overlay-position-map.ts b/packages/mosaic/core/overlay/overlay-position-map.ts index 37ee58ffb..0c465e5ce 100644 --- a/packages/mosaic/core/overlay/overlay-position-map.ts +++ b/packages/mosaic/core/overlay/overlay-position-map.ts @@ -98,6 +98,133 @@ export const EXTENDED_OVERLAY_POSITIONS = objectValues([ POSITION_MAP.left, POSITION_MAP.leftTop, POSITION_MAP.leftBottom ]); +export const TOP_POSITION_PRIORITY = objectValues([ + POSITION_MAP.top, + POSITION_MAP.bottom, + POSITION_MAP.rightBottom, + POSITION_MAP.leftBottom, + POSITION_MAP.bottomLeft, + POSITION_MAP.bottomRight +]); + +export const BOTTOM_POSITION_PRIORITY = objectValues([ + POSITION_MAP.bottom, + POSITION_MAP.top, + POSITION_MAP.topLeft, + POSITION_MAP.topRight, + POSITION_MAP.rightBottom, + POSITION_MAP.leftBottom +]); + +export const RIGHT_POSITION_PRIORITY = objectValues([ + POSITION_MAP.right, + POSITION_MAP.left, + POSITION_MAP.leftTop, + POSITION_MAP.leftBottom, + POSITION_MAP.top, + POSITION_MAP.bottom +]); + +export const LEFT_POSITION_PRIORITY = objectValues([ + POSITION_MAP.left, + POSITION_MAP.right, + POSITION_MAP.rightTop, + POSITION_MAP.rightBottom, + POSITION_MAP.top, + POSITION_MAP.bottom +]); + +export const RIGHT_TOP_POSITION_PRIORITY = objectValues([ + POSITION_MAP.rightTop, + POSITION_MAP.leftTop, + POSITION_MAP.left, + POSITION_MAP.leftBottom, + POSITION_MAP.topLeft, + POSITION_MAP.bottomLeft +]); + +export const RIGHT_BOTTOM_POSITION_PRIORITY = objectValues([ + POSITION_MAP.rightBottom, + POSITION_MAP.leftBottom, + POSITION_MAP.left, + POSITION_MAP.leftTop, + POSITION_MAP.topLeft, + POSITION_MAP.bottomLeft +]); + +export const LEFT_TOP_POSITION_PRIORITY = objectValues([ + POSITION_MAP.leftTop, + POSITION_MAP.rightTop, + POSITION_MAP.right, + POSITION_MAP.rightBottom, + POSITION_MAP.topRight, + POSITION_MAP.bottomRight +]); + +export const LEFT_BOTTOM_POSITION_PRIORITY = objectValues([ + POSITION_MAP.leftBottom, + POSITION_MAP.rightBottom, + POSITION_MAP.right, + POSITION_MAP.rightTop, + POSITION_MAP.topRight, + POSITION_MAP.bottomRight +]); + +export const TOP_LEFT_POSITION_PRIORITY = objectValues([ + POSITION_MAP.topLeft, + POSITION_MAP.topRight, + POSITION_MAP.bottomLeft, + POSITION_MAP.bottom, + POSITION_MAP.bottomRight, + POSITION_MAP.leftBottom, + POSITION_MAP.rightBottom +]); + +export const TOP_RIGHT_POSITION_PRIORITY = objectValues([ + POSITION_MAP.topRight, + POSITION_MAP.topLeft, + POSITION_MAP.bottomRight, + POSITION_MAP.bottom, + POSITION_MAP.bottomLeft, + POSITION_MAP.leftBottom, + POSITION_MAP.rightBottom +]); + +export const BOTTOM_RIGHT_POSITION_PRIORITY = objectValues([ + POSITION_MAP.bottomRight, + POSITION_MAP.bottomLeft, + POSITION_MAP.topRight, + POSITION_MAP.top, + POSITION_MAP.topLeft, + POSITION_MAP.leftTop, + POSITION_MAP.rightTop +]); + +export const BOTTOM_LEFT_POSITION_PRIORITY = objectValues([ + POSITION_MAP.bottomLeft, + POSITION_MAP.bottomRight, + POSITION_MAP.topLeft, + POSITION_MAP.top, + POSITION_MAP.topRight, + POSITION_MAP.rightTop, + POSITION_MAP.leftTop +]); + +export const POSITION_PRIORITY_STRATEGY = { + top: TOP_POSITION_PRIORITY, + topLeft: TOP_LEFT_POSITION_PRIORITY, + topRight: TOP_RIGHT_POSITION_PRIORITY, + bottom: BOTTOM_POSITION_PRIORITY, + bottomLeft: BOTTOM_LEFT_POSITION_PRIORITY, + bottomRight: BOTTOM_RIGHT_POSITION_PRIORITY, + left: LEFT_POSITION_PRIORITY, + leftTop: LEFT_TOP_POSITION_PRIORITY, + leftBottom: LEFT_BOTTOM_POSITION_PRIORITY, + right: RIGHT_POSITION_PRIORITY, + rightTop: RIGHT_TOP_POSITION_PRIORITY, + rightBottom: RIGHT_BOTTOM_POSITION_PRIORITY +}; + export const POSITION_TO_CSS_MAP: {[key: string]: string} = { top: 'top', topLeft: 'top-left', @@ -113,6 +240,13 @@ export const POSITION_TO_CSS_MAP: {[key: string]: string} = { bottomRight: 'bottom-right' }; +export const DEFAULT_4_POSITIONS_TO_CSS_MAP: {[key: string]: string} = { + top: 'top', + bottom: 'bottom', + right: 'right', + left: 'left' +}; + function arrayMap(array: T[], iteratee: (item: T, index: number, arr: T[]) => S): S[] { let index = -1; const length = array == null ? 0 : array.length; diff --git a/packages/mosaic/popover/popover.component.ts b/packages/mosaic/popover/popover.component.ts index dec8b9a0f..cad7e33df 100644 --- a/packages/mosaic/popover/popover.component.ts +++ b/packages/mosaic/popover/popover.component.ts @@ -5,14 +5,10 @@ import { ConnectedOverlayPositionChange, ConnectionPositionPair, FlexibleConnectedPositionStrategy, - HorizontalConnectionPos, - OriginConnectionPosition, Overlay, - OverlayConnectionPosition, OverlayRef, ScrollDispatcher, - ScrollStrategy, - VerticalConnectionPos + ScrollStrategy } from '@angular/cdk/overlay'; import { ComponentPortal } from '@angular/cdk/portal'; import { @@ -35,8 +31,13 @@ import { ViewEncapsulation } from '@angular/core'; import { ESCAPE } from '@ptsecurity/cdk/keycodes'; -import { EXTENDED_OVERLAY_POSITIONS, POSITION_MAP, POSITION_TO_CSS_MAP } from '@ptsecurity/mosaic/core'; -import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { + DEFAULT_4_POSITIONS_TO_CSS_MAP, + EXTENDED_OVERLAY_POSITIONS, + POSITION_MAP, POSITION_PRIORITY_STRATEGY, + POSITION_TO_CSS_MAP +} from '@ptsecurity/mosaic/core'; +import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { mcPopoverAnimations } from './popover-animations'; @@ -181,6 +182,8 @@ export class McPopoverComponent { if (this.isNonEmptyContent()) { this.closeOnInteraction = true; this.popoverVisibility = PopoverVisibility.Visible; + this._mcVisible.next(true); + this.mcVisibleChange.emit(true); // Mark for check so if any parent component has set the // ChangeDetectionStrategy to OnPush it will be checked anyways this.markForCheck(); @@ -189,6 +192,7 @@ export class McPopoverComponent { hide(): void { this.popoverVisibility = PopoverVisibility.Hidden; + this._mcVisible.next(false); this.mcVisibleChange.emit(false); // Mark for check so if any parent component has set the @@ -263,11 +267,7 @@ export function getMcPopoverInvalidPositionError(position: string) { } const VIEWPORT_MARGIN: number = 8; -/** @docs-private - * Minimal width of anchor element should be equal or greater than popover arrow width plus arrow offset right/left - * MIN_ANCHOR_ELEMENT_WIDTH used for positioning update inside handlePositionUpdate() - */ -const MIN_ANCHOR_ELEMENT_WIDTH: number = 40; +const POPOVER_ARROW_BORDER_DISTANCE: number = 20; // tslint:disable-line @Directive({ selector: '[mcPopover]', @@ -283,11 +283,15 @@ export class McPopover implements OnInit, OnDestroy { isDynamicPopover = false; overlayRef: OverlayRef | null; portal: ComponentPortal; - availablePositions: any; + availablePositions: { [key: string]: ConnectionPositionPair }; + defaultPositionsMap: { [key: string]: string}; popover: McPopoverComponent | null; @Output('mcPopoverVisibleChange') mcVisibleChange = new EventEmitter(); + @Output('mcPopoverPositionStrategyPlacementChange') + mcPositionStrategyPlacementChange: EventEmitter = new EventEmitter(); + @Input('mcPopoverHeader') get mcHeader(): string | TemplateRef { return this._mcHeader; @@ -373,6 +377,7 @@ export class McPopover implements OnInit, OnDestroy { } else { this._mcTrigger = PopoverTriggers.Click; } + this.resetListeners(); } private _mcTrigger: string = PopoverTriggers.Click; @@ -391,6 +396,19 @@ export class McPopover implements OnInit, OnDestroy { } private popoverSize: string = 'normal'; + @Input('mcPopoverPlacementPriority') + get mcPlacementPriority() { + return this._mcPlacementPriority; + } + set mcPlacementPriority(value) { + if (value && value.length > 0) { + this._mcPlacementPriority = value; + } else { + this._mcPlacementPriority = null; + } + } + private _mcPlacementPriority: string | string[] | null = null; + @Input('mcPopoverPlacement') get mcPlacement(): string { return this._mcPlacement; @@ -422,13 +440,16 @@ export class McPopover implements OnInit, OnDestroy { set mcVisible(externalValue: boolean) { const value = coerceBooleanProperty(externalValue); - this._mcVisible = value; - this.updateCompValue('mcVisible', value); - if (value) { - this.show(); - } else { - this.hide(); + if (this._mcVisible !== value) { + this._mcVisible = value; + this.updateCompValue('mcVisible', value); + + if (value) { + this.show(); + } else { + this.hide(); + } } } @@ -440,6 +461,7 @@ export class McPopover implements OnInit, OnDestroy { private manualListeners = new Map(); private readonly destroyed = new Subject(); + private backDropSubscription: Subscription; constructor( private overlay: Overlay, @@ -448,14 +470,16 @@ export class McPopover implements OnInit, OnDestroy { private scrollDispatcher: ScrollDispatcher, private hostView: ViewContainerRef, @Inject(MC_POPOVER_SCROLL_STRATEGY) private scrollStrategy, - @Optional() private direction: Directionality) { - this.availablePositions = POSITION_MAP; - } + @Optional() private direction: Directionality + ) { + this.availablePositions = POSITION_MAP; + this.defaultPositionsMap = DEFAULT_4_POSITIONS_TO_CSS_MAP; + } /** Create the overlay config and position strategy */ createOverlay(): OverlayRef { if (this.overlayRef) { - return this.overlayRef; + this.overlayRef.dispose(); } // Create connected position strategy that listens for scroll events to reposition. @@ -471,7 +495,9 @@ export class McPopover implements OnInit, OnDestroy { strategy.withScrollableContainers(scrollableAncestors); - strategy.positionChanges.pipe(takeUntil(this.destroyed)).subscribe((change) => { + strategy.positionChanges + .pipe(takeUntil(this.destroyed)) + .subscribe((change) => { if (this.popover) { this.onPositionChange(change); if (change.scrollableViewProperties.isOverlayClipped && this.popover.mcVisible) { @@ -491,14 +517,7 @@ export class McPopover implements OnInit, OnDestroy { backdropClass: 'no-class' }); - if (this.mcTrigger === PopoverTriggers.Click) { - this.overlayRef.backdropClick() - .subscribe(() => { - if (!this.popover) { return; } - - this.popover.hide(); - }); - } + this.updateOverlayBackdropClick(); this.updatePosition(); @@ -510,10 +529,10 @@ export class McPopover implements OnInit, OnDestroy { } detach() { - if (this.overlayRef && this.overlayRef.hasAttached() && this.popover) { + if (this.overlayRef && this.overlayRef.hasAttached()) { this.overlayRef.detach(); - this.popover = null; } + this.popover = null; } onPositionChange($event: ConnectedOverlayPositionChange): void { @@ -530,49 +549,46 @@ export class McPopover implements OnInit, OnDestroy { return false; }); + this.updateCompValue('mcPlacement', updatedPlacement); + this.mcPositionStrategyPlacementChange.emit(updatedPlacement); if (this.popover) { this.updateCompValue('classList', this.classList); this.popover.markForCheck(); } - this.handlePositionUpdate(); + if (!this.defaultPositionsMap[updatedPlacement]) { + this.handlePositionUpdate(updatedPlacement); + } } - handlePositionUpdate() { + handlePositionUpdate(updatedPlacement: string) { if (!this.overlayRef) { this.overlayRef = this.createOverlay(); } - const verticalOffset = this.hostView.element.nativeElement.clientHeight / 2; // tslint:disable-line - const anchorElementWidth = this.hostView.element.nativeElement.clientWidth; // tslint:disable-line + const currentContainer = this.overlayRef.overlayElement.style; + const elementHeight = this.hostView.element.nativeElement.clientHeight; + const elementWidth = this.hostView.element.nativeElement.clientWidth; + const verticalOffset: number = Math.floor(elementHeight / 2); // tslint:disable-line + const horizontalOffset: number = Math.floor(elementWidth / 2 - 6); // tslint:disable-line + const offsets: { [key: string]: number} = { + top: verticalOffset, + bottom: verticalOffset, + right: horizontalOffset, + left: horizontalOffset + }; - if (this.mcPlacement === 'rightTop' || this.mcPlacement === 'leftTop') { - const currentContainer = this.overlayRef.overlayElement.style.top || '0px'; - this.overlayRef.overlayElement.style.top = - `${parseInt(currentContainer.split('px')[0], 10) + verticalOffset - 20}px`; // tslint:disable-line - } + const styleProperty = updatedPlacement.split(/(?=[A-Z])/)[1].toLowerCase(); - if (this.mcPlacement === 'rightBottom' || this.mcPlacement === 'leftBottom') { - const currentContainer = this.overlayRef.overlayElement.style.bottom || '0px'; - this.overlayRef.overlayElement.style.bottom = - `${parseInt(currentContainer.split('px')[0], 10) + verticalOffset - 22}px`; // tslint:disable-line + if (!this.overlayRef.overlayElement.style[styleProperty]) { + this.overlayRef.overlayElement.style[styleProperty] = '0px'; } - if ((this.mcPlacement === 'topRight' || this.mcPlacement === 'bottomRight') && - anchorElementWidth < MIN_ANCHOR_ELEMENT_WIDTH) { - const currentContainer = this.overlayRef.overlayElement.style.right || '0px'; - this.overlayRef.overlayElement.style.right = - `${parseInt(currentContainer.split('px')[0], 10) - 18}px`; // tslint:disable-line - } - - if ((this.mcPlacement === 'topLeft' || this.mcPlacement === 'bottomLeft') && - anchorElementWidth < MIN_ANCHOR_ELEMENT_WIDTH) { - const currentContainer = this.overlayRef.overlayElement.style.left || '0px'; - this.overlayRef.overlayElement.style.left = - `${parseInt(currentContainer.split('px')[0], 10) - 20}px`; // tslint:disable-line - } + this.overlayRef.overlayElement.style[styleProperty] = + `${parseInt(currentContainer[styleProperty].split('px')[0], 10) + + offsets[styleProperty] - POPOVER_ARROW_BORDER_DISTANCE}px`; } // tslint:disable-next-line:no-any @@ -638,15 +654,41 @@ export class McPopover implements OnInit, OnDestroy { } } + registerResizeHandler() { + // The resize handler is currently responsible for detecting slider dimension + // changes and therefore doesn't cause a value change that needs to be propagated. + this.ngZone.runOutsideAngular(() => { + window.addEventListener('resize', this.resizeListener); + }); + } + + deregisterResizeHandler() { + window.removeEventListener('resize', this.resizeListener); + } + + resetListeners() { + if (this.manualListeners.size) { + this.manualListeners.forEach((listener, event) => { + this.elementRef.nativeElement.removeEventListener(event, listener); + }); + this.manualListeners.clear(); + this.initElementRefListeners(); + } + } + show(): void { if (!this.disabled) { if (!this.popover) { - const overlayRef = this.createOverlay(); this.detach(); + const overlayRef = this.createOverlay(); this.portal = this.portal || new ComponentPortal(McPopoverComponent, this.hostView); this.popover = overlayRef.attach(this.portal).instance; + this.popover.afterHidden() + .pipe(takeUntil(this.destroyed)) + .subscribe(() => this.detach()); + this.isDynamicPopover = true; const properties = [ 'mcPlacement', @@ -670,15 +712,8 @@ export class McPopover implements OnInit, OnDestroy { this.mcVisibleChange.emit(data); this.isPopoverOpen = data; }); - - this.mcVisibleChange.emit(this.popover.mcVisible); - - this.popover.afterHidden() - .pipe(takeUntil(this.destroyed)) - .subscribe(() => this.detach()); } - this.updatePosition(); this.popover.show(); } } @@ -689,6 +724,20 @@ export class McPopover implements OnInit, OnDestroy { } } + updateOverlayBackdropClick() { + if (this.mcTrigger === PopoverTriggers.Click && this.overlayRef) { + this.backDropSubscription = this.overlayRef.backdropClick() + .subscribe(() => { + if (!this.popover) { return; } + + this.popover.hide(); + }); + } else if (this.backDropSubscription && this.overlayRef) { + this.backDropSubscription.unsubscribe(); + this.overlayRef.detachBackdrop(); + } + } + /** Updates the position of the current popover. */ updatePosition(reapplyPosition: boolean = false) { if (!this.overlayRef) { @@ -696,17 +745,8 @@ export class McPopover implements OnInit, OnDestroy { } const position = this.overlayRef.getConfig().positionStrategy as FlexibleConnectedPositionStrategy; - const origin = this.getOrigin(); - const overlay = this.getOverlayPosition(); - - position.withPositions([ - {...origin.main, ...overlay.main}, - {...origin.fallback, ...overlay.fallback} - ]); + position.withPositions(this.getPrioritizedPositions()).withPush(true); - // - // FIXME: Необходимо в некоторых моментах форсировать позиционировать только после рендеринга всего контента - // if (reapplyPosition) { setTimeout(() => { position.reapplyLastPosition(); @@ -714,98 +754,29 @@ export class McPopover implements OnInit, OnDestroy { } } - /** - * Returns the origin position and a fallback position based on the user's position preference. - * The fallback position is the inverse of the origin (e.g. `'below' -> 'above'`). - */ - getOrigin(): {main: OriginConnectionPosition; fallback: OriginConnectionPosition} { - let originPosition: OriginConnectionPosition; - const originXPosition = this.getOriginXaxis(); - const originYPosition = this.getOriginYaxis(); - originPosition = {originX: originXPosition, originY: originYPosition}; - - const {x, y} = this.invertPosition(originPosition.originX, originPosition.originY); - - return { - main: originPosition, - fallback: {originX: x, originY: y} - }; - } - - getOriginXaxis(): HorizontalConnectionPos { - const position = this.mcPlacement; - let origX: HorizontalConnectionPos; - const isLtr = !this.direction || this.direction.value === 'ltr'; - if (position === 'top' || position === 'bottom') { - origX = 'center'; - } else if (position.toLowerCase().includes('right') && !isLtr || - position.toLowerCase().includes('left') && isLtr) { - origX = 'start'; - } else if (position.toLowerCase().includes('right') && isLtr || - position.toLowerCase().includes('left') && !isLtr) { - origX = 'end'; - } else { - throw getMcPopoverInvalidPositionError(position); - } - - return origX; - } - - getOriginYaxis(): VerticalConnectionPos { - const position = this.mcPlacement; - let origY: VerticalConnectionPos; - if (position === 'right' || position === 'left') { - origY = 'center'; - } else if (position.toLowerCase().includes('top')) { - origY = 'top'; - } else if (position.toLowerCase().includes('bottom')) { - origY = 'bottom'; - } else { - throw getMcPopoverInvalidPositionError(position); + private getPriorityPlacementStrategy(value: string | string[]): ConnectionPositionPair[] { + const result: ConnectionPositionPair[] = []; + const possiblePositions = Object.keys(this.availablePositions); + if (Array.isArray(value)) { + value.forEach((position: string) => { + if (possiblePositions.includes(position)) { + result.push(this.availablePositions[position]); + } + }); + } else if (possiblePositions.includes(value)) { + result.push(this.availablePositions[value]); } - return origY; + return result; } - /** Returns the overlay position and a fallback position based on the user's preference */ - getOverlayPosition(): {main: OverlayConnectionPosition; fallback: OverlayConnectionPosition} { - const position = this.mcPlacement; - let overlayPosition: OverlayConnectionPosition; - if (this.availablePositions[position]) { - overlayPosition = { - overlayX : this.availablePositions[position].overlayX, - overlayY: this.availablePositions[position].overlayY - }; - } else { - throw getMcPopoverInvalidPositionError(position); + private getPrioritizedPositions() { + if (this.mcPlacementPriority) { + return this.getPriorityPlacementStrategy(this.mcPlacementPriority); } - const {x, y} = this.invertPosition(overlayPosition.overlayX, overlayPosition.overlayY); - - return { - main: overlayPosition, - fallback: {overlayX: x, overlayY: y} - }; + return POSITION_PRIORITY_STRATEGY[this.mcPlacement]; } - /** Inverts an overlay position. */ - private invertPosition(x: HorizontalConnectionPos, y: VerticalConnectionPos) { - let newX: HorizontalConnectionPos = x; - let newY: VerticalConnectionPos = y; - if (this.mcPlacement === 'top' || this.mcPlacement === 'bottom') { - if (y === 'top') { - newY = 'bottom'; - } else if (y === 'bottom') { - newY = 'top'; - } - } else { - if (x === 'end') { - newX = 'start'; - } else if (x === 'start') { - newX = 'end'; - } - } - - return {x: newX, y: newY}; - } + private resizeListener = () => this.updatePosition(); } diff --git a/packages/mosaic/popover/popover.scss b/packages/mosaic/popover/popover.scss index 8c4f58ff0..b7f4d7506 100644 --- a/packages/mosaic/popover/popover.scss +++ b/packages/mosaic/popover/popover.scss @@ -8,7 +8,7 @@ $z-index-popover: 1060; $popover-max-width: 240px; $border-radius-base: 4px; $popover-arrow-width: 6px; -$popover-distance: $popover-arrow-width + 4px; +$popover-distance: $popover-arrow-width * 2; $popover-arrow-distance: -($popover-arrow-width + 2px); $border-size: 1px;