ares-ide
Version:
A browser-based code editor and UI designer for Enyo 2 projects
513 lines (468 loc) • 20.2 kB
JavaScript
/**
_onyx.ContextualPopup_ is a subkind of <a href="#enyo.Popup">enyo.Popup</a>.
Contextual popups serve as child windows that appear near the point of
initiation. Use them for prompting users to select from a defined set of
options; for conducting other quick, single-step interactions in which
context should be maintained; and for presenting simple views, such as
previews.
A contextual popup is meant to be used in conjunction with a decorator, such
as an <a href="#onyx.MenuDecorator">onyx.MenuDecorator</a>. The decorator
couples the popup with an activating control, which may be a button or any
other control with an _onActivate_ event. When the control is activated, the
popup shows itself in the correct position relative to the activator.
Note that, by default, the popup is not floating, so toolbars and controls
with high z-index values may obscure it. You may set the _floating_ property
to true to have the popup always appear on top; however, the popup will not
be in the containing document's flow and so will not scroll with the
document.
In addition, while contextual popups have their own positioning logic, they
do not currently have their own sizing logic, so be sure to take this into
account when using them.
{kind: "onyx.MenuDecorator", components: [
{content: "Show Popup"},
{kind: "onyx.ContextualPopup",
title: "Sample Popup",
actionButtons: [
{content:"Button 1", classes: "onyx-button-warning"},
{content:"Button 2"}
],
components: [
{content:"Sample component in popup"}
]
}
]}
*/
enyo.kind({
name: "onyx.ContextualPopup",
kind: "enyo.Popup",
modal: true,
autoDismiss: true,
floating:false,
classes: "onyx-contextual-popup enyo-unselectable",
published: {
//* Maximum height of the menu
maxHeight: 100,
//* Toggle scrolling
scrolling: true,
//* Popup title content
title: undefined,
//* Buttons at bottom of popup
actionButtons: []
},
//layout parameters
vertFlushMargin:60, //vertical flush layout margin
horizFlushMargin:50, //horizontal flush layout margin
widePopup:200, //popups wider than this value are considered wide (for layout purposes)
longPopup:200, //popups longer than this value are considered long (for layout purposes)
horizBuffer:16, //do not allow horizontal flush popups past spec'd amount of buffer space on left/right screen edge
events: {
onTap: ""
},
handlers: {
onRequestShowMenu: "requestShow",
onRequestHideMenu: "requestHide"
},
components: [
{name:"title", classes:"onyx-contextual-popup-title"},
{classes:"onyx-contextual-popup-scroller", components:[
{name:"client", kind: "enyo.Scroller", vertical:"auto", classes:"enyo-unselectable", thumb:false, strategyKind: "TouchScrollStrategy"}
]},
{name:"actionButtons", classes:"onyx-contextual-popup-action-buttons"}
],
scrollerName: "client",
create: function() {
this.inherited(arguments);
this.maxHeightChanged();
this.titleChanged();
this.actionButtonsChanged();
},
getScroller: function() {
return this.$[this.scrollerName];
},
titleChanged: function(){
this.$.title.setContent(this.title);
},
actionButtonsChanged: function(){
for (var i=0;i<this.actionButtons.length;i++){
this.$.actionButtons.createComponent({
kind:"onyx.Button",
content:this.actionButtons[i].content,
classes: this.actionButtons[i].classes + " onyx-contextual-popup-action-button",
name: this.actionButtons[i].name ?
this.actionButtons[i].name :
"ActionButton" + i,
index: i,
tap: this.actionButtons[i].ontap ?
this.owner.bindSafely(this.actionButtons[i].ontap) :
this.bindSafely(this.tapHandler)
});
}
},
tapHandler: function(inSender, inEvent){
//Populate event fields with origination info
inEvent.actionButton = true;
inEvent.popup = this;
this.bubble("ontap",inEvent);
return true;
},
maxHeightChanged: function() {
if (this.scrolling) {
this.getScroller().setMaxHeight(this.maxHeight + "px");
}
},
showingChanged: function() {
this.inherited(arguments);
if (this.scrolling) {
this.getScroller().setShowing(this.showing);
}
this.adjustPosition();
},
requestShow: function(inSender, inEvent) {
var n = inEvent.activator.hasNode();
if (n) {
this.activatorOffset = this.getPageOffset(n);
}
this.show();
return true;
},
applyPosition: function(inRect) {
var s = "";
for (var n in inRect) {
s += (n + ":" + inRect[n] + (isNaN(inRect[n]) ? "; " : "px; "));
}
this.addStyles(s);
},
getPageOffset: function(inNode) {
var r = this.getBoundingRect(inNode);
var pageYOffset = (window.pageYOffset === undefined) ? document.documentElement.scrollTop : window.pageYOffset;
var pageXOffset = (window.pageXOffset === undefined) ? document.documentElement.scrollLeft : window.pageXOffset;
var rHeight = (r.height === undefined) ? (r.bottom - r.top) : r.height;
var rWidth = (r.width === undefined) ? (r.right - r.left) : r.width;
return {top: r.top + pageYOffset, left: r.left + pageXOffset, height: rHeight, width: rWidth};
},
//* @protected
/* Adjusts the popup position + nub location & direction
*/
adjustPosition: function() {
if (this.showing && this.hasNode()) {
/****ContextualPopup positioning rules:
1. Activator Location:
a. If activator is located in a corner then position using a flush style.
i. Attempt vertical first.
ii. Horizontal if vertical doesn't fit.
b. If not in a corner then check if the activator is located in one of the 4 "edges" of the view & position the
following way if so:
i. Activator is in top edge, position popup below it.
ii. Activator is in bottom edge, position popup above it.
iii. Activator is in left edge, position popup to the right of it.
iv. Activator is in right edge, position popup to the left of it.
2. Screen Size - the pop-up should generally extend in the direction where there’s room for it.
Note: no specific logic below for this rule since it is built into the positioning functions, ie we attempt to never
position a popup where there isn't enough room for it.
3. Popup Size:
i. If popup content is wide, use top or bottom positioning.
ii. If popup content is long, use horizontal positioning.
4. Favor top or bottom:
If all the above rules have been followed and location can still vary then favor top or bottom positioning.
5. If top or bottom will work, favor bottom.
Note: no specific logic below for this rule since it is built into the vertical position functions, ie we attempt to
use a bottom position for the popup as much possible. Additionally within the vetical position function we center the
popup if the activator is at the vertical center of the view.
****/
this.resetPositioning();
var innerWidth = this.getViewWidth();
var innerHeight = this.getViewHeight();
//These are the view "flush boundaries"
var topFlushPt = this.vertFlushMargin;
var bottomFlushPt = innerHeight - this.vertFlushMargin;
var leftFlushPt = this.horizFlushMargin;
var rightFlushPt = innerWidth - this.horizFlushMargin;
//Rule 1 - Activator Location based positioning
//if the activator is in the top or bottom edges of the view, check if the popup needs flush positioning
if ((this.activatorOffset.top + this.activatorOffset.height) < topFlushPt || this.activatorOffset.top > bottomFlushPt) {
//check/try vertical flush positioning (rule 1.a.i)
if (this.applyVerticalFlushPositioning(leftFlushPt, rightFlushPt)) {
return;
}
//if vertical doesn't fit then check/try horizontal flush (rule 1.a.ii)
if (this.applyHorizontalFlushPositioning(leftFlushPt, rightFlushPt)) {
return;
}
//if flush positioning didn't work then try just positioning vertically (rule 1.b.i & rule 1.b.ii)
if (this.applyVerticalPositioning()){
return;
}
//otherwise check if the activator is in the left or right edges of the view & if so try horizontal positioning
} else if ((this.activatorOffset.left + this.activatorOffset.width) < leftFlushPt || this.activatorOffset.left > rightFlushPt) {
//if flush positioning didn't work then try just positioning horizontally (rule 1.b.iii & rule 1.b.iv)
if (this.applyHorizontalPositioning()){
return;
}
}
//Rule 2 - no specific logic below for this rule since it is inheritent to the positioning functions, ie we attempt to never
//position a popup where there isn't enough room for it.
//Rule 3 - Popup Size based positioning
var clientRect = this.getBoundingRect(this.node);
//if the popup is wide then use vertical positioning
if (clientRect.width > this.widePopup) {
if (this.applyVerticalPositioning()){
return;
}
}
//if the popup is long then use horizontal positioning
else if (clientRect.height > this.longPopup) {
if (this.applyHorizontalPositioning()){
return;
}
}
//Rule 4 - Favor top or bottom positioning
if (this.applyVerticalPositioning()) {
return;
}
//but if thats not possible try horizontal
else if (this.applyHorizontalPositioning()){
return;
}
//Rule 5 - no specific logic below for this rule since it is built into the vertical position functions, ie we attempt to
// use a bottom position for the popup as much possible.
}
},
//move the popup below or above the activator & verify that it fits on screen
initVerticalPositioning: function() {
this.resetPositioning();
this.addClass("vertical");
var clientRect = this.getBoundingRect(this.node);
var innerHeight = this.getViewHeight();
if (this.floating){
if (this.activatorOffset.top < (innerHeight / 2)) {
this.applyPosition({top: this.activatorOffset.top + this.activatorOffset.height, bottom: "auto"});
this.addClass("below");
} else {
this.applyPosition({top: this.activatorOffset.top - clientRect.height, bottom: "auto"});
this.addClass("above");
}
} else {
//if the popup's bottom goes off the screen then put it on the top of the invoking control
if ((clientRect.top + clientRect.height > innerHeight) && ((innerHeight - clientRect.bottom) < (clientRect.top - clientRect.height))){
this.addClass("above");
} else {
this.addClass("below");
}
}
//if moving the popup above or below the activator puts it past the edge of the screen then vertical doesn't work
clientRect = this.getBoundingRect(this.node);
if ((clientRect.top + clientRect.height) > innerHeight || clientRect.top < 0){
return false;
}
return true;
},
applyVerticalPositioning: function() {
//if we can't fit the popup above or below the activator then forget vertical positioning
if (!this.initVerticalPositioning()) {
return false;
}
var clientRect = this.getBoundingRect(this.node);
var innerWidth = this.getViewWidth();
if (this.floating){
//Get the left edge delta to horizontally center the popup
var centeredLeft = this.activatorOffset.left + this.activatorOffset.width/2 - clientRect.width/2;
if (centeredLeft + clientRect.width > innerWidth) {//popup goes off right edge of the screen if centered
this.applyPosition({left: this.activatorOffset.left + this.activatorOffset.width - clientRect.width});
this.addClass("left");
} else if (centeredLeft < 0) {//popup goes off left edge of the screen if centered
this.applyPosition({left:this.activatorOffset.left});
this.addClass("right");
} else {//center the popup
this.applyPosition({left: centeredLeft});
}
} else {
//Get the left edge delta to horizontally center the popup
var centeredLeftDelta = this.activatorOffset.left + this.activatorOffset.width/2 - clientRect.left - clientRect.width/2;
if (clientRect.right + centeredLeftDelta > innerWidth) {//popup goes off right edge of the screen if centered
this.applyPosition({left: this.activatorOffset.left + this.activatorOffset.width - clientRect.right});
this.addRemoveClass("left", true);
} else if (clientRect.left + centeredLeftDelta < 0) {//popup goes off left edge of the screen if centered
this.addRemoveClass("right", true);
} else {//center the popup
this.applyPosition({left: centeredLeftDelta});
}
}
return true;
},
applyVerticalFlushPositioning: function(leftFlushPt, rightFlushPt) {
//if we can't fit the popup above or below the activator then forget vertical positioning
if (!this.initVerticalPositioning()) {
return false;
}
var clientRect = this.getBoundingRect(this.node);
var innerWidth = this.getViewWidth();
//If the activator's right side is within our left side cut off use flush positioning
if ((this.activatorOffset.left + this.activatorOffset.width/2) < leftFlushPt){
//if the activator's left edge is too close or past the screen left edge
if (this.activatorOffset.left + this.activatorOffset.width/2 < this.horizBuffer){
this.applyPosition({left:this.horizBuffer + (this.floating ? 0 : -clientRect.left)});
} else {
this.applyPosition({left:this.activatorOffset.width/2 + (this.floating ? this.activatorOffset.left : 0)});
}
this.addClass("right");
this.addClass("corner");
return true;
}
//If the activator's left side is within our right side cut off use flush positioning
else if (this.activatorOffset.left + this.activatorOffset.width/2 > rightFlushPt) {
if ((this.activatorOffset.left+this.activatorOffset.width/2) > (innerWidth-this.horizBuffer)){
this.applyPosition({left:innerWidth - this.horizBuffer - clientRect.right});
} else {
this.applyPosition({left: (this.activatorOffset.left + this.activatorOffset.width/2) - clientRect.right});
}
this.addClass("left");
this.addClass("corner");
return true;
}
return false;
},
//move the popup left or right of the activator & verify that it fits on screen
initHorizontalPositioning: function() {
this.resetPositioning();
var clientRect = this.getBoundingRect(this.node);
var innerWidth = this.getViewWidth();
//adjust horizontal positioning of the popup & nub vertical positioning
if (this.floating){
if ((this.activatorOffset.left + this.activatorOffset.width) < innerWidth/2) {
this.applyPosition({left: this.activatorOffset.left + this.activatorOffset.width});
this.addRemoveClass("left", true);
} else {
this.applyPosition({left: this.activatorOffset.left - clientRect.width});
this.addRemoveClass("right", true);
}
} else {
if (this.activatorOffset.left - clientRect.width > 0) {
this.applyPosition({left: this.activatorOffset.left - clientRect.left - clientRect.width});
this.addRemoveClass("right", true);
} else {
this.applyPosition({left: this.activatorOffset.width});
this.addRemoveClass("left", true);
}
}
this.addRemoveClass("horizontal", true);
//if moving the popup left or right of the activator puts it past the edge of the screen then horizontal won't work
clientRect = this.getBoundingRect(this.node);
if (clientRect.left < 0 || (clientRect.left + clientRect.width) > innerWidth){
return false;
}
return true;
},
applyHorizontalPositioning: function() {
//if we can't fit the popup left or right of the activator then forget horizontal positioning
if (!this.initHorizontalPositioning()) {
return false;
}
var clientRect = this.getBoundingRect(this.node);
var innerHeight = this.getViewHeight();
var activatorCenter = this.activatorOffset.top + this.activatorOffset.height/2;
if (this.floating){
//if the activator's center is within 10% of the center of the view, vertically center the popup
if ((activatorCenter >= (innerHeight/2 - 0.05 * innerHeight)) && (activatorCenter <= (innerHeight/2 + 0.05 * innerHeight))) {
this.applyPosition({top: this.activatorOffset.top + this.activatorOffset.height/2 - clientRect.height/2, bottom: "auto"});
} else if (this.activatorOffset.top + this.activatorOffset.height < innerHeight/2) { //the activator is in the top 1/2 of the screen
this.applyPosition({top: this.activatorOffset.top - this.activatorOffset.height, bottom: "auto"});
this.addRemoveClass("high", true);
} else { //otherwise the popup will be positioned in the bottom 1/2 of the screen
this.applyPosition({top: this.activatorOffset.top - clientRect.height + this.activatorOffset.height*2, bottom: "auto"});
this.addRemoveClass("low", true);
}
} else {
//if the activator's center is within 10% of the center of the view, vertically center the popup
if ((activatorCenter >= (innerHeight/2 - 0.05 * innerHeight)) && (activatorCenter <= (innerHeight/2 + 0.05 * innerHeight))) {
this.applyPosition({top: (this.activatorOffset.height - clientRect.height)/2});
} else if (this.activatorOffset.top + this.activatorOffset.height < innerHeight/2) { //the activator is in the top 1/2 of the screen
this.applyPosition({top: -this.activatorOffset.height});
this.addRemoveClass("high", true);
} else { //otherwise the popup will be positioned in the bottom 1/2 of the screen
this.applyPosition({top: clientRect.top - clientRect.height - this.activatorOffset.top + this.activatorOffset.height});
this.addRemoveClass("low", true);
}
}
return true;
},
applyHorizontalFlushPositioning: function(leftFlushPt, rightFlushPt) {
//if we can't fit the popup left or right of the activator then forget vertical positioning
if (!this.initHorizontalPositioning()) {
return false;
}
var clientRect = this.getBoundingRect(this.node);
var innerHeight = this.getViewHeight();
//adjust vertical positioning (high or low nub & popup position)
if (this.floating){
if (this.activatorOffset.top < (innerHeight/2)){
this.applyPosition({top: this.activatorOffset.top + this.activatorOffset.height/2});
this.addRemoveClass("high", true);
} else {
this.applyPosition({top:this.activatorOffset.top + this.activatorOffset.height/2 - clientRect.height});
this.addRemoveClass("low", true);
}
} else {
if (((clientRect.top + clientRect.height) > innerHeight) && ((innerHeight - clientRect.bottom) < (clientRect.top - clientRect.height))) {
this.applyPosition({top: clientRect.top - clientRect.height - this.activatorOffset.top - this.activatorOffset.height/2});
this.addRemoveClass("low", true);
} else {
this.applyPosition({top: this.activatorOffset.height/2});
this.addRemoveClass("high", true);
}
}
//If the activator's right side is within our left side cut off use flush positioning
if ((this.activatorOffset.left + this.activatorOffset.width) < leftFlushPt){
this.addClass("left");
this.addClass("corner");
return true;
}
//If the activator's left side is within our right side cut off use flush positioning
else if (this.activatorOffset.left > rightFlushPt) {
this.addClass("right");
this.addClass("corner");
return true;
}
return false;
},
getBoundingRect: function(inNode){
// getBoundingClientRect returns top/left values which are relative to the viewport and not absolute
var o = inNode.getBoundingClientRect();
if (!o.width || !o.height) {
return {
left: o.left,
right: o.right,
top: o.top,
bottom: o.bottom,
width: o.right - o.left,
height: o.bottom - o.top
};
}
return o;
},
getViewHeight: function() {
return (window.innerHeight === undefined) ? document.documentElement.clientHeight : window.innerHeight;
},
getViewWidth: function() {
return (window.innerWidth === undefined) ? document.documentElement.clientWidth : window.innerWidth;
},
resetPositioning: function() {
this.removeClass("right");
this.removeClass("left");
this.removeClass("high");
this.removeClass("low");
this.removeClass("corner");
this.removeClass("below");
this.removeClass("above");
this.removeClass("vertical");
this.removeClass("horizontal");
this.applyPosition({left: "auto"});
this.applyPosition({top: "auto"});
},
resizeHandler: function() {
this.inherited(arguments);
this.adjustPosition();
},
requestHide: function(){
this.setShowing(false);
}
});