Skip to content

Commit

Permalink
feat(positioning): refactor positioning service (#5027)
Browse files Browse the repository at this point in the history
* reafctor(positioning): add popper.js and migrate it to typescript

* temporary disable tslint for popper

* feat(position): refactored base functionality

* update

* feat(positioning): flip on overflow added (dirty)

* refactor(positioning): add logic of arrow position correcting

* refactor(positioning): add shift logic. placement: bottom left/bottom right

* refactor(positioning): refactor logic of real-time processing events to avoid memory leaks

* refactor(positioning): add types for all methods(minimal solution) && fix tslint errors

* refactor(positioning): optimize and fix blur text on tooltip/popover

* refactor(positioning): add auto class placement

* refactor(positioning): clean up the code

* refactor(positioning): unit tests are fixed

* refactor(positioning): fix incorrect styles on bootstrap 3 && fix arrow in popovers when auto

* refactor(positioning): fix tests for sauce

* refactor(positioning): fix typeahead dropup issue

* refactor(positioning): fix center for arrow on shift

* refactor(positioning): fix tooltip out of screen on mobile

* refactor(positioning): refactor modifiers flow

* fix(tooltip): back css style

Closes #3303
Closes #2993
Closes #4470
  • Loading branch information
Domainv authored and valorkin committed Feb 8, 2019
1 parent cd13a55 commit 66ae92d
Show file tree
Hide file tree
Showing 46 changed files with 1,266 additions and 274 deletions.
3 changes: 3 additions & 0 deletions src/component-loader/component-loader.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ export class ComponentLoader<T> {
return this;
}

this._posService.deletePositionElement(this._componentRef.location);

this.onBeforeHide.emit(this._componentRef.instance);

const componentEl = this._componentRef.location.nativeElement;
Expand Down Expand Up @@ -350,6 +352,7 @@ export class ComponentLoader<T> {
if (!this._zoneSubscription) {
return;
}

this._zoneSubscription.unsubscribe();
this._zoneSubscription = null;
}
Expand Down
25 changes: 18 additions & 7 deletions src/popover/popover-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ import { isBs3 } from 'ngx-bootstrap/utils';
'[class]':
'"popover in popover-" + placement + " " + "bs-popover-" + placement + " " + placement + " " + containerClass',
'[class.show]': '!isBs3',
'[class.bs3]': 'isBs3',
role: 'tooltip',
style: 'display:block;'
},
styles: [
`
:host.bs-popover-top .arrow, :host.bs-popover-bottom .arrow {
left: 50%;
transform: translateX(-50%);
:host.bs3.popover-top {
margin-bottom: 10px;
}
:host.bs-popover-left .arrow, :host.bs-popover-right .arrow {
top: 50%;
transform: translateY(-50%);
:host.bs3.popover.top>.arrow {
margin-left: -2px;
}
`
:host.bs3.popover.top {
margin-bottom: 10px;
}
:host.popover.bottom>.arrow {
margin-left: -4px;
}
:host.bs3.bs-popover-left {
margin-right: .5rem;
}
:host.bs3.bs-popover-right .arrow, :host.bs3.bs-popover-left .arrow{
margin: .3rem 0;
}
`
],
templateUrl: './popover-container.component.html'
})
Expand Down
26 changes: 26 additions & 0 deletions src/positioning/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface Offsets {
bottom?: number;
height: number;
left?: number;
right?: number;
top?: number;
width: number;
marginTop?: number;
marginLeft?: number;
}

export interface Data {
instance: {
target: HTMLElement;
host: HTMLElement;
arrow: HTMLElement;
};
offsets: {
target: Offsets;
host: Offsets;
arrow: { [key: string]: string | number | HTMLElement };
};
positionFixed: boolean;
placement: string;
placementAuto: boolean;
}
58 changes: 58 additions & 0 deletions src/positioning/modifiers/arrow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getClientRect, getOuterSizes, getStyleComputedProperty } from '../utils';
import { Data } from '../models';

export function arrow(data: Data) {
let targetOffsets = data.offsets.target;
// if arrowElement is a string, suppose it's a CSS selector
const arrowElement: HTMLElement | null = data.instance.target.querySelector('.arrow');

// if arrowElement is not found, don't run the modifier
if (!arrowElement) {
return data;
}

const isVertical = ['left', 'right'].indexOf(data.placement) !== -1;

const len = isVertical ? 'height' : 'width';
const sideCapitalized = isVertical ? 'Top' : 'Left';
const side = sideCapitalized.toLowerCase();
const altSide = isVertical ? 'left' : 'top';
const opSide = isVertical ? 'bottom' : 'right';
const arrowElementSize = getOuterSizes(arrowElement)[len];

// top/left side
if (data.offsets.host[opSide] - arrowElementSize < targetOffsets[side]) {
targetOffsets[side] -=
targetOffsets[side] - (data.offsets.host[opSide] - arrowElementSize);
}
// bottom/right side
if (Number(data.offsets.host[side]) + Number(arrowElementSize) > targetOffsets[opSide]) {
targetOffsets[side] +=
Number(data.offsets.host[side]) + Number(arrowElementSize) - Number(targetOffsets[opSide]);
}
targetOffsets = getClientRect(targetOffsets);

// compute center of the target
const center = Number(data.offsets.host[side]) + Number(data.offsets.host[len] / 2 - arrowElementSize / 2);

// Compute the sideValue using the updated target offsets
// take target margin in account because we don't have this info available
const css = getStyleComputedProperty(data.instance.target);

const targetMarginSide = parseFloat(css[`margin${sideCapitalized}`]);
const targetBorderSide = parseFloat(css[`border${sideCapitalized}Width`]);
let sideValue =
center - targetOffsets[side] - targetMarginSide - targetBorderSide;

// prevent arrowElement from being placed not contiguously to its target
sideValue = Math.max(Math.min(targetOffsets[len] - arrowElementSize, sideValue), 0);

data.offsets.arrow = {
[side]: Math.round(sideValue),
[altSide]: '' // make sure to unset any eventual altSide value from the DOM node
};

data.instance.arrow = arrowElement;

return data;
}
91 changes: 91 additions & 0 deletions src/positioning/modifiers/flip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
computeAutoPlacement,
getBoundaries, getClientRect,
getOppositeVariation,
getTargetOffsets
} from '../utils';

import { Data } from '../models';

export function flip(data: Data): Data {
data.offsets.target = getClientRect(data.offsets.target);

const boundaries = getBoundaries(
data.instance.target,
data.instance.host,
0, // padding
'viewport',
false // positionFixed
);

let placement = data.placement.split(' ')[0];
let variation = data.placement.split(' ')[1] || '';

const autoPosition = computeAutoPlacement(
'auto', data.offsets.host, data.instance.target, data.instance.host, 'viewport', 0
);
const flipOrder = [placement, autoPosition];

/* tslint:disable-next-line: cyclomatic-complexity */
flipOrder.forEach((step, index) => {
if (placement !== step || flipOrder.length === index + 1) {
return data;
}

placement = data.placement.split(' ')[0];

// using floor because the host offsets may contain decimals we are not going to consider here
const overlapsRef =
(placement === 'left' &&
Math.floor(data.offsets.target.right) > Math.floor(data.offsets.host.left)) ||
(placement === 'right' &&
Math.floor(data.offsets.target.left) < Math.floor(data.offsets.host.right)) ||
(placement === 'top' &&
Math.floor(data.offsets.target.bottom) > Math.floor(data.offsets.host.top)) ||
(placement === 'bottom' &&
Math.floor(data.offsets.target.top) < Math.floor(data.offsets.host.bottom));

const overflowsLeft = Math.floor(data.offsets.target.left) < Math.floor(boundaries.left);
const overflowsRight = Math.floor(data.offsets.target.right) > Math.floor(boundaries.right);
const overflowsTop = Math.floor(data.offsets.target.top) < Math.floor(boundaries.top);
const overflowsBottom = Math.floor(data.offsets.target.bottom) > Math.floor(boundaries.bottom);

const overflowsBoundaries =
(placement === 'left' && overflowsLeft) ||
(placement === 'right' && overflowsRight) ||
(placement === 'top' && overflowsTop) ||
(placement === 'bottom' && overflowsBottom);

// flip the variation if required
const isVertical = ['top', 'bottom'].indexOf(placement) !== -1;
const flippedVariation =
((isVertical && variation === 'left' && overflowsLeft) ||
(isVertical && variation === 'right' && overflowsRight) ||
(!isVertical && variation === 'left' && overflowsTop) ||
(!isVertical && variation === 'right' && overflowsBottom));

if (overlapsRef || overflowsBoundaries || flippedVariation) {
// this boolean to detect any flip loop
if (overlapsRef || overflowsBoundaries) {
placement = flipOrder[index + 1];
}

if (flippedVariation) {
variation = getOppositeVariation(variation);
}

data.placement = placement + (variation ? ` ${variation}` : '');

data.offsets.target = {
...data.offsets.target,
...getTargetOffsets(
data.instance.target,
data.offsets.host,
data.placement
)
};
}
});

return data;
}
5 changes: 5 additions & 0 deletions src/positioning/modifiers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { arrow } from './arrow';
export { flip } from './flip';
export { initData } from './initData';
export { preventOverflow } from './preventOverflow';
export { shift } from './shift';
32 changes: 32 additions & 0 deletions src/positioning/modifiers/initData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
computeAutoPlacement,
getReferenceOffsets,
getTargetOffsets
} from '../utils';

import { Data } from '../models';

export function initData(targetElement: HTMLElement, hostElement: HTMLElement, position: string): Data {

const hostElPosition = getReferenceOffsets(targetElement, hostElement);
const targetOffset = getTargetOffsets(targetElement, hostElPosition, position);

const placement = computeAutoPlacement(position, hostElPosition, targetElement, hostElement, 'viewport', 0);
const placementAuto = position.indexOf('auto') !== -1;

return {
instance: {
target: targetElement,
host: hostElement,
arrow: null
},
offsets: {
target: targetOffset,
host: hostElPosition,
arrow: null
},
positionFixed: false,
placement,
placementAuto
};
}
75 changes: 75 additions & 0 deletions src/positioning/modifiers/preventOverflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { getBoundaries } from '../utils';
import { Data } from '../models';

export function preventOverflow(data: Data) {

// NOTE: DOM access here
// resets the targetOffsets's position so that the document size can be calculated excluding
// the size of the targetOffsets element itself
const transformProp = 'transform';
const targetStyles = data.instance.target.style; // assignment to help minification
const { top, left, [transformProp]: transform } = targetStyles;
targetStyles.top = '';
targetStyles.left = '';
targetStyles[transformProp] = '';

const boundaries = getBoundaries(
data.instance.target,
data.instance.host,
0, // padding
'scrollParent',
false // positionFixed
);

// NOTE: DOM access here
// restores the original style properties after the offsets have been computed
targetStyles.top = top;
targetStyles.left = left;
targetStyles[transformProp] = transform;

const order = ['left', 'right', 'top', 'bottom'];

const check = {
primary(placement: string) {
let value = data.offsets.target[placement];
if (
data.offsets.target[placement] < boundaries[placement] &&
!false // options.escapeWithReference
) {
value = Math.max(data.offsets.target[placement], boundaries[placement]);
}

return { [placement]: value };
},
secondary(placement: string) {
const mainSide = placement === 'right' ? 'left' : 'top';
let value = data.offsets.target[mainSide];
if (
data.offsets.target[placement] > boundaries[placement] &&
!false // escapeWithReference
) {
value = Math.min(
data.offsets.target[mainSide],
boundaries[placement] -
(placement === 'right' ? data.offsets.target.width : data.offsets.target.height)
);
}

return { [mainSide]: value };
}
};

let side: string;

order.forEach(placement => {
side = ['left', 'top']
.indexOf(placement) !== -1
? 'primary'
: 'secondary';

data.offsets.target = { ...data.offsets.target, ...check[side](placement) };

});

return data;
}
25 changes: 25 additions & 0 deletions src/positioning/modifiers/shift.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Data } from '../models';

export function shift(data: Data): Data {
const placement = data.placement;
const basePlacement = placement.split(' ')[0];
const shiftvariation = placement.split(' ')[1];

if (shiftvariation) {
const { host, target } = data.offsets;
const isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;
const side = isVertical ? 'left' : 'top';
const measurement = isVertical ? 'width' : 'height';

const shiftOffsets = {
left: { [side]: host[side] },
right: {
[side]: host[side] + host[measurement] - host[measurement]
}
};

data.offsets.target = { ...target, ...shiftOffsets[shiftvariation] };
}

return data;
}
Loading

0 comments on commit 66ae92d

Please sign in to comment.