Popup menus, nowadays called context menus, were always some kind of challenge, on any platform. A lot of bugs occurred with them, either because the API was unclear, or because they did not work properly. I remember that the AWT PopupMenu of an early LINUX Java could freeze the whole application.
Popup menu sister is the dropdown list, a further relative is the combo box. (Also the Java/Swing JComboBox has an eventful history.) All these show a clickable window that temporarily hovers above the user interface.
Why is that window so problematic? Because it is not contained in the component-tree of the user interface, it is not in the layout, it is invisible and displays just on a certain user gesture, usually a right mouse click, or Shift-F10 key. Then it needs to gain some position where the user really can see and use it, which is more difficult than expected, especially in multi-screen environments. Finally it must be removed by events that may not have been sent to the component that contained it. (Imagine an open context menu, and then you focus another application on your desktop. Would you expect it to still be open when you come back?)
This Blog is about how to manage a popup window in a web page. I will present JavaScript functions that build, open and close it safely. You can find this also on my homepage. There are simpler solutions for popup / dropdown windows, but they do not work always and under all circumstances.
Scroll to any "Popup X" and click it.
For simplicity, this popup shows on left mouse click, other than traditional context menus that show on right mouse click.
In this example, the popup is contained within its visual representation (the JS source below postulates that). The popup is tagged with CSS class popup
. The visual representation renders just the popup's first text line. To do that, it was set to a fixed height, with CSS overflow hidden.
1 | <!DOCTYPE HTML> |
The popup principles are following:
body
, hidden, and its CSS position
must be absolute
getBoundingClientRect()
lets calculate this Put following functions to where /* JS source goes here */
is.
This loops all popups and installs them. A click callback is added on each one, logging the event target. (In real world, you would replace this by some context menu event dispatcher.)
This source code needs to be on bottom of the script
element, all other functions must be somewhere above it.
var popups = document.querySelectorAll(".popup");
for (var i = 0; i < popups.length; i++) {
var popupClone = installPopup(popups[i]);
popupClone.addEventListener("click", function(event) {
var log = document.getElementById("log");
var clickTarget = (event.target || event.srcElement);
log.innerHTML = log.innerHTML+"<p>Click target was: '"+clickTarget.textContent+"'</p>";
});
}
This prepares the given popup for usage. It adds all necessary listeners to open and close it.
var installPopup = function(popup, keepOpenOnScroll) {
var popupClone = createPopupClone(popup);
/* event listener that opens the popup */
popup.addEventListener("click", function(event) {
openPopup(popup, popupClone, event);
});
/* event listeners that close the popup */
var close = function() {
closePopup(popupClone);
};
popupClone.addEventListener("blur", close);
popupClone.addEventListener("click", close);
if ( ! keepOpenOnScroll ) {
var scrollParent = findScrollParent(popup);
if (scrollParent)
scrollParent.addEventListener("scroll", close);
window.addEventListener("scroll", close);
}
return popupClone;
};
This clones the popup element and adds it invisibly to the document's body
root.
var createPopupClone = function(popup) {
var popupClone = popup.cloneNode(true); /* create a deep clone */
closePopup(popupClone); /* set it invisible */
if ( ! popupClone.style["background-color"] )
popupClone.style["background-color"] = "white"; /* don't be transparent */
popupClone.style["position"] = "absolute"; /* position will be calculated */
document.body.appendChild(popupClone); /* add to root level for absolute positioning */
popupClone.setAttribute("tabindex", 0); /* make it focusable for blur-listener */
return popupClone;
};
The open-function calculates the absolute coordinates of the popup and positions it there. (This could be refined to calculate an actually visible rectangle on the screen when being on bottom. See the location()
and smartCoordinate()
functions in my tooltip Blog for improving this.)
The focus()
call sets the input focus onto the popup clone. This is necessary for the blur
listener to work. Setting the clone's tabindex
attribute to 0 made it focusable (see createPopupClone()
above).
var openPopup = function(popup, popupClone, clickEvent) {
var rectangle = popup.getBoundingClientRect();
var scrollOffsetLeft = window.pageXOffset || document.documentElement.scrollLeft;
var scrollOffsetTop = window.pageYOffset || document.documentElement.scrollTop;
popupClone.style.top = Math.round(rectangle.top + scrollOffsetTop)+"px";
popupClone.style.left = Math.round(rectangle.left + scrollOffsetLeft)+"px";
popupClone.style["display"] = "";
popupClone.focus();
};
var closePopup = function(popupClone) {
popupClone.style["display"] = "none";
};
These functions identify and find parent elements that show either an horizontal or a vertical scrollbar. A listener for these would use the "scroll"
event type.
Mind that, in nested scroll panes, when you want to not close the popup on scrolling, the popup would not scroll with the parent. It would stay on its absolute coordinate. In that case you need to implement some kind of re-positioning on each scroll event.
var isScrollPane = function(element) {
var scrolls = (element.offsetHeight < element.scrollHeight || element.offsetWidth < element.scrollWidth);
if ( ! scrolls )
return false;
/* H1 elements cause this to fail when not checking "overflow" */
var style = window.getComputedStyle(element);
return style.overflow === "scroll" || style.overflow === "auto";
};
var findScrollParent = function(element) {
while (element.parentElement && element.parentElement !== document.body) {
if ( isScrollPane(element.parentElement) )
return element.parentElement;
element = element.parentElement;
}
return undefined;
};
Popups on web pages can be managed in many different ways. I tried to show a minimalistic and safe one, pointing out just the necessities.
ɔ⃝ Fritz Ritzberger, 2017-10-07