Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

fix paper menu positioning #1144

Merged
merged 1 commit into from
May 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 5 additions & 132 deletions addon/components/paper-menu/component.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import { layout, tagName } from '@ember-decorators/component';
import Component from '@ember/component';
import { action, computed } from '@ember/object';
import MenuPositionCalculator from 'ember-paper/utils/menu-position-calculator';
import template from './template';

import clamp from 'ember-paper/utils/clamp';
import { assert } from '@ember/debug';
import { computed, action } from '@ember/object';

import { tagName, layout } from '@ember-decorators/component';

function firstVisibleChild(node) {
for (let i = 0; i < node.children.length; ++i) {
if (window.getComputedStyle(node.children[i]).display !== 'none') {
return node.children[i];
}
}
}

const MENU_EDGE_MARGIN = 8;

@tagName('')
@layout(template)
class PaperMenu extends Component {
Expand Down Expand Up @@ -57,125 +44,11 @@ class PaperMenu extends Component {

@action
calculatePosition(trigger, content) {
let containerNode = content;
let openMenuNode = content.firstElementChild;
let openMenuNodeRect = openMenuNode.getBoundingClientRect();
let boundryNode = document.body;
let boundryNodeRect = boundryNode.getBoundingClientRect();

let menuStyle = window.getComputedStyle(openMenuNode);

let originNode = trigger.querySelector('.md-menu-origin') || trigger.querySelector('md-icon') || trigger;
let originNodeRect = originNode.getBoundingClientRect();

let bounds = {
left: boundryNodeRect.left + MENU_EDGE_MARGIN,
top: Math.max(boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
bottom: Math.max(boundryNodeRect.bottom, Math.max(boundryNodeRect.top, 0) + boundryNodeRect.height) - MENU_EDGE_MARGIN,
right: boundryNodeRect.right - MENU_EDGE_MARGIN
};

let alignTarget;
let alignTargetRect = { top: 0, left: 0, right: 0, bottom: 0 };
let positionMode = this.positionMode;

if (positionMode.top === 'target' || positionMode.left === 'target' || positionMode.left === 'target-right') {
alignTarget = firstVisibleChild(openMenuNode);
if (alignTarget) {
// TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
alignTarget = alignTarget.firstElementChild || alignTarget;
alignTarget = alignTarget.querySelector('md-icon') || alignTarget.querySelector('.md-menu-align-target') || alignTarget;
alignTargetRect = alignTarget.getBoundingClientRect();
}
}

let position = {};
let transformOrigin = 'top ';

switch (positionMode.top) {
case 'target':
position.top = originNodeRect.top - alignTarget.offsetTop;
break;
case 'cascade':
position.top = originNodeRect.top - parseFloat(menuStyle.paddingTop) - originNode.style.top;
break;
case 'bottom':
position.top = originNodeRect.top + originNodeRect.height;
break;
default:
assert(`Invalid target mode '${positionMode.top}' specified for paper-menu on Y axis.`);
}

switch (positionMode.left) {
case 'target': {
position.left = originNodeRect.left - alignTarget.offsetLeft;
transformOrigin += 'left';
break;
}
case 'target-left': {
position.left = originNodeRect.left;
transformOrigin += 'left';
break;
}
case 'target-right': {
position.left = originNodeRect.right - openMenuNodeRect.width + (openMenuNodeRect.right - alignTargetRect.right);
transformOrigin += 'right';
break;
}
case 'cascade': {
let willFitRight = (originNodeRect.right + openMenuNodeRect.width) < bounds.right;
position.left = willFitRight ? originNodeRect.right - originNode.style.left : originNodeRect.left - originNode.style.left - openMenuNodeRect.width;
transformOrigin += willFitRight ? 'left' : 'right';
break;
}
case 'right': {
position.left = originNodeRect.right - openMenuNodeRect.width;
transformOrigin += 'right';
break;
}
case 'left': {
position.left = originNodeRect.left;
transformOrigin += 'left';
break;
}
default: {
assert(`Invalid target mode '${positionMode.left}' specified for paper-menu on X axis.`);
}
}

// sum offsets
let offsets = this.offsets;
position.top += offsets.top;
position.left += offsets.left;

clamp(position, bounds, containerNode);

let dropdownTop = Math.round(position.top);
let dropdownLeft = Math.round(position.left);

let scaleX = Math.round(100 * Math.min(originNodeRect.width / containerNode.offsetWidth, 1.0)) / 100;
let scaleY = Math.round(100 * Math.min(originNodeRect.height / containerNode.offsetHeight, 1.0)) / 100;

let style = {
top: dropdownTop,
left: dropdownLeft,
// Animate a scale out if we aren't just repositioning
transform: !this.didAnimateScale ? `scale(${scaleX}, ${scaleY})` : undefined,
'transform-origin': transformOrigin
};

/*
dropdown.setProperties({
transform: !dropdown.didAnimateScale ? `scale(${scaleX}, ${scaleY})` : undefined,
transformOrigin
});
*/

let positionCalculator = new MenuPositionCalculator(trigger, content, this.positionMode, this.offsets, this.didAnimateScale);
this.didAnimateScale = true;

return { style, horizontalPosition: '', verticalPosition: '' };
return positionCalculator.drowdownPosition;
}

}

export default PaperMenu;
238 changes: 238 additions & 0 deletions addon/utils/menu-position-calculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
const MENU_EDGE_MARGIN = 8;

function firstVisibleChild(node) {
for (let i = 0; i < node.children.length; ++i) {
if (window.getComputedStyle(node.children[i]).display !== 'none') {
return node.children[i];
}
}
}

/**
* Calculates the menu position
*/
class MenuPositionCalculator {

/**
* Creates an instance of menu position calculator.
* @param trigger HTMLElement of menu's trigger
* @param content HTMLElement of the menu's content
* @param positionMode Mode to use when calculating position {top: string, left: string}
* @param offsets Top and left offsets {top: number, left: number}
* @param didAnimateScale True when the menu has already animated.
*/
constructor(trigger, content, positionMode, offsets, didAnimateScale) {
this.trigger = trigger;
this.content = content;
this.positionMode = positionMode;
this.didAnimateScale = didAnimateScale;
this.offsets = offsets;
this.position = {};
this.verticalTransformOrigin = "top"
this.horizontalTransformOrigin = "";

this.calcTopPositionMode();
this.calcLeftPositionMode();
this.addOffsets();
this.clampPosition();
}

/**
* Element of the menu originiation
*/
get originNode() {
return this.trigger;
}

/**
* Client rec of origin node
*/
get originNodeRect() {
return this.originNode.getBoundingClientRect();
}

/**
* Element of menu container
*/
get containerNode() {
return this.content;
}

/**
* Element of open menu
*/
get openMenuNode() {
return this.content.firstElementChild;
}

/**
* Client rect of open menu
*/
get openMenuNodeRect() {
return this.openMenuNode.getBoundingClientRect();
}

/**
* Element in open menu to use for aligning positiong
*/
get alignTarget() {
let alignTarget;

if (this.positionMode.top === 'target' || this.positionMode.left === 'target' || this.positionMode.left === 'target-right') {
alignTarget = firstVisibleChild(this.openMenuNode);
if (alignTarget) {
// TODO: Allow centering on an arbitrary node, for now center on first menu-item's child
alignTarget = alignTarget.firstElementChild || alignTarget;
alignTarget = alignTarget.querySelector('md-icon') || alignTarget.querySelector('.md-menu-align-target') || alignTarget;
}
}

return alignTarget;
}

/**
* Client rect
*/
get alignTargetRect() {
return this.alignTarget ? this.alignTarget.getBoundingClientRect() : { top: 0, left: 0, right: 0, bottom: 0 };
}

/**
* Element for calculating boundry
*/
get boundryNode() {
return document.body;
}

/**
* Client rect of boundry node
*/
get boundryNodeRect() {
return this.boundryNode.getBoundingClientRect();
}

/**
* Bounds for menu position
*/
get bounds() {
return {
left: this.boundryNodeRect.left + MENU_EDGE_MARGIN,
top: Math.max(this.boundryNodeRect.top, 0) + MENU_EDGE_MARGIN,
bottom: Math.max(this.boundryNodeRect.bottom, Math.max(this.boundryNodeRect.top, 0) + this.boundryNodeRect.height) - MENU_EDGE_MARGIN,
right: this.boundryNodeRect.right - MENU_EDGE_MARGIN
};
}

/**
* Transform origin for dropdown position
*/
get transformOrigin() {
return this.verticalTransformOrigin + " " + this.horizontalTransformOrigin;
}

/**
* Transofrm for dropdown position
*/
get transform() {
let scaleX = Math.round(100 * Math.min(this.originNodeRect.width / this.containerNode.offsetWidth, 1.0)) / 100;
let scaleY = Math.round(100 * Math.min(this.originNodeRect.height / this.containerNode.offsetHeight, 1.0)) / 100;

return !this.didAnimateScale ? `scale(${scaleX}, ${scaleY})` : undefined;
}

/**
* Returns the dropdown position used by ember basic dropdown
*/
get drowdownPosition() {
let style = {
top: Math.round(this.position.top),
left: Math.round(this.position.left),
// Animate a scale out if we aren't just repositioning
transform: this.transform,
'transform-origin': this.transformOrigin
};

return { style, horizontalPosition: '', verticalPosition: '' };
}

/**
* Calcs top position based on the top position mode
*/
calcTopPositionMode() {
let menuStyle = window.getComputedStyle(this.openMenuNode);

switch (this.positionMode.top) {
case 'target':
this.position.top = this.originNodeRect.top - this.alignTarget.offsetTop;
break;
case 'cascade':
this.position.top = this.originNodeRect.top - parseFloat(menuStyle.paddingTop) - this.originNode.style.top;
break;
case 'bottom':
this.position.top = this.originNodeRect.top + this.originNodeRect.height;
break;
default:
assert(`Invalid target mode '${this.positionMode.top}' specified for paper-menu on Y axis.`);
}
}

/**
* Calcs left position based on the left position mode
*/
calcLeftPositionMode() {
switch (this.positionMode.left) {
case 'target': {
this.position.left = this.originNodeRect.left - this.alignTarget.offsetLeft;
this.horizontalTransformOrigin += 'left';
break;
}
case 'target-left': {
this.position.left = this.originNodeRect.left;
this.horizontalTransformOrigin += 'left';
break;
}
case 'target-right': {
this.position.left = this.originNodeRect.right - this.openMenuNodeRect.width + (this.openMenuNodeRect.right - this.alignTargetRect.right);
this.horizontalTransformOrigin += 'right';
break;
}
case 'cascade': {
let willFitRight = (this.originNodeRect.right + this.openMenuNodeRect.width) < this.bounds.right;
this.position.left = willFitRight ? this.originNodeRect.right - this.originNode.style.left : this.originNodeRect.left - this.originNode.style.left - this.openMenuNodeRect.width;
this.horizontalTransformOrigin += willFitRight ? 'left' : 'right';
break;
}
case 'right': {
this.position.left = this.originNodeRect.right - this.openMenuNodeRect.width;
this.horizontalTransformOrigin += 'right';
break;
}
case 'left': {
this.position.left = this.originNodeRect.left;
this.horizontalTransformOrigin += 'left';
break;
}
default: {
assert(`Invalid target mode '${positionMode.left}' specified for paper-menu on X axis.`);
}
}
}

/**
* Adds offsets to position
*/
addOffsets() {
this.position.top += this.offsets.top;
this.position.left + this.offsets.left
}

/**
* Clamp position to bounds to handle when menu doesn't fit on the screen
*/
clampPosition() {
this.position.top = Math.max(Math.min(this.position.top, this.bounds.bottom - this.containerNode.offsetHeight), this.bounds.top);
this.position.left = Math.max(Math.min(this.position.left, this.bounds.right - this.containerNode.offsetWidth), this.bounds.left);
}
}

export default MenuPositionCalculator;