contain-by-screen
Version:
Position a dropdown element near a button in a way that fits on the screen.
283 lines (278 loc) • 11.3 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.containByScreen = containByScreen;
exports.getContainByScreenResults = getContainByScreenResults;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _flatten = _interopRequireDefault(require("lodash/flatten"));
var _uniq = _interopRequireDefault(require("lodash/uniq"));
var _isNotNil = require("./isNotNil");
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/**
* This option is used when position is "cover", "top", or "bottom".
* It controls the horizontal alignment of the element relative to the anchor.
* "center" means the element's center is aligned with the anchor's center.
* "left" and "right" means that edge of the element is aligned with the same edge of the anchor.
* "unaligned" means the element may not be aligned with the anchor at all. Currently
* this works by doing the same thing as "center" and then adjusting the result to fit on screen.
*/
/**
* Similar to {@link HAlignOption}, except this controls the vertical alignment of the element
* relative to the anchor when the position is "cover", "left", or "right".
*/
function containByScreen(element, anchorPoint, options) {
var choiceAndCoord = getContainByScreenResults(element, anchorPoint, options);
element.style.top = "".concat(choiceAndCoord.coordinates.top, "px");
element.style.left = "".concat(choiceAndCoord.coordinates.left, "px");
return choiceAndCoord.choice;
}
function getContainByScreenResults(element, anchorPoint, options) {
if (process.env.NODE_ENV !== "production" && window.getComputedStyle) {
var style = window.getComputedStyle(element);
if (style.position !== "fixed") {
// eslint-disable-next-line no-console
console.error("containByScreen only works on fixed position elements", element);
}
}
var elRect = getBoundingClientRect(element);
var anchorRect = getBoundingClientRect(anchorPoint);
var buffers = {
all: options.buffer || 0,
top: options.topBuffer || 0,
bottom: options.bottomBuffer || 0,
left: options.leftBuffer || 0,
right: options.rightBuffer || 0
};
var optionPositions = Array.isArray(options.position) ? options.position : [options.position].filter(_isNotNil.isNotNil);
var optionHAligns = Array.isArray(options.hAlign) ? options.hAlign : [options.hAlign].filter(_isNotNil.isNotNil);
var optionVAligns = Array.isArray(options.vAlign) ? options.vAlign : [options.vAlign].filter(_isNotNil.isNotNil);
var positions = optionPositions.length > 0 && options.forcePosition ? optionPositions : (0, _uniq["default"])(optionPositions.concat(["top", "bottom", "left", "right"]));
var hAligns = optionHAligns.length > 0 && options.forceHAlign ? optionHAligns : (0, _uniq["default"])(optionHAligns.concat(["center", "left", "right"]));
var vAligns = optionVAligns.length > 0 && options.forceVAlign ? optionVAligns : (0, _uniq["default"])(optionVAligns.concat(["center", "top", "bottom"]));
var allPossibleChoices = (0, _flatten["default"])(positions.map(function (position) {
return position === "cover" ? (0, _flatten["default"])(hAligns.map(function (hAlign) {
return vAligns.map(function (vAlign) {
return {
position: position,
hAlign: hAlign,
vAlign: vAlign
};
});
})) : position === "top" || position === "bottom" ? hAligns.map(function (hAlign) {
return {
position: position,
hAlign: hAlign,
vAlign: "center"
};
}) : vAligns.map(function (vAlign) {
return {
position: position,
hAlign: "center",
vAlign: vAlign
};
});
}));
// Try unaligned versions at the end
if (!hAligns.includes("unaligned")) {
allPossibleChoices.push.apply(allPossibleChoices, (0, _toConsumableArray2["default"])((0, _flatten["default"])(positions.map(function (position) {
return !["cover", "top", "bottom"].includes(position) ? [] : vAligns.map(function (vAlign) {
return {
position: position,
hAlign: "unaligned",
vAlign: vAlign
};
});
}))));
}
if (!vAligns.includes("unaligned")) {
allPossibleChoices.push.apply(allPossibleChoices, (0, _toConsumableArray2["default"])((0, _flatten["default"])(positions.map(function (position) {
return !["cover", "left", "right"].includes(position) ? [] : hAligns.map(function (hAlign) {
return {
position: position,
hAlign: hAlign,
vAlign: "unaligned"
};
});
}))));
}
var choiceAndCoord = null;
for (var i = 0; i < allPossibleChoices.length; i++) {
var choice = allPossibleChoices[i];
var coordinates = positionAndAlign(elRect, anchorRect, choice, buffers);
var top = coordinates.top,
left = coordinates.left;
var ignoreHorizontalConstraints = choice.hAlign === "unaligned" && ["cover", "top", "bottom"].includes(choice.position);
var ignoreVerticalConstraints = choice.vAlign === "unaligned" && ["cover", "left", "right"].includes(choice.position);
var hasHorizontalFit = ignoreHorizontalConstraints || left - buffers.all - buffers.left >= 0 && left + elRect.width + buffers.all + buffers.right <= window.innerWidth;
var hasVerticalFit = ignoreVerticalConstraints || top - buffers.all - buffers.top >= 0 && top + elRect.height + buffers.all + buffers.bottom <= window.innerHeight;
if (hasHorizontalFit && hasVerticalFit) {
choiceAndCoord = {
choice: choice,
coordinates: coordinates
};
break;
}
}
// Fallback if we failed to find a choice that fit on the screen.
if (!choiceAndCoord) {
var _choice = {
position: "cover",
hAlign: "unaligned",
vAlign: "unaligned"
};
choiceAndCoord = {
choice: _choice,
coordinates: positionAndAlign(elRect, anchorRect, _choice, buffers)
};
}
return choiceAndCoord;
}
function getBoundingClientRect(el) {
var rect = el.getBoundingClientRect();
if (!("width" in rect)) {
// IE <9 support
rect = _objectSpread({
width: rect.right - rect.left,
height: rect.bottom - rect.top
}, rect);
}
return rect;
}
function positionAndAlign(elRect, anchorRect, _ref, buffers) {
var position = _ref.position,
hAlign = _ref.hAlign,
vAlign = _ref.vAlign;
var top = 0,
left = 0;
if (position === "cover") {
switch (hAlign) {
case "center":
left = Math.round((anchorRect.left + anchorRect.right - elRect.width) / 2);
break;
case "left":
left = Math.floor(anchorRect.left);
break;
case "right":
left = Math.ceil(anchorRect.right - elRect.width);
break;
case "unaligned":
{
left = Math.round((anchorRect.left + anchorRect.right - elRect.width) / 2);
var overhang = Math.ceil(left + elRect.width + buffers.all + buffers.right - window.innerWidth);
if (overhang > 0) {
left -= overhang;
}
left = Math.max(buffers.all + buffers.left, left);
break;
}
default:
hAlign;
throw new Error("Should not happen");
}
switch (vAlign) {
case "center":
top = Math.round((anchorRect.top + anchorRect.bottom - elRect.height) / 2);
break;
case "top":
top = Math.floor(anchorRect.top);
break;
case "bottom":
top = Math.ceil(anchorRect.bottom - elRect.height);
break;
case "unaligned":
{
top = Math.round((anchorRect.top + anchorRect.bottom - elRect.height) / 2);
var _overhang = Math.ceil(top + elRect.height + buffers.all + buffers.bottom - window.innerHeight);
if (_overhang > 0) {
top -= _overhang;
}
top = Math.max(buffers.all + buffers.top, top);
break;
}
default:
vAlign;
throw new Error("Should not happen");
}
} else if (position === "top" || position === "bottom") {
switch (position) {
case "top":
top = Math.floor(anchorRect.top - elRect.height - buffers.all - buffers.bottom);
break;
case "bottom":
top = Math.ceil(anchorRect.bottom + buffers.all + buffers.top);
break;
default:
position;
throw new Error("Should not happen");
}
switch (hAlign) {
case "center":
left = Math.round((anchorRect.left + anchorRect.right - elRect.width) / 2);
break;
case "left":
left = Math.round(anchorRect.left);
break;
case "right":
left = Math.round(anchorRect.right - elRect.width);
break;
case "unaligned":
{
left = Math.round((anchorRect.left + anchorRect.right - elRect.width) / 2);
var _overhang2 = Math.ceil(left + elRect.width + buffers.all + buffers.right - window.innerWidth);
if (_overhang2 > 0) {
left -= _overhang2;
}
left = Math.max(buffers.all + buffers.left, left);
break;
}
default:
hAlign;
throw new Error("Should not happen");
}
} else {
switch (position) {
case "left":
left = Math.floor(anchorRect.left - elRect.width - buffers.all - buffers.right);
break;
case "right":
left = Math.ceil(anchorRect.right + buffers.all + buffers.left);
break;
default:
position;
throw new Error("Should not happen");
}
switch (vAlign) {
case "center":
top = Math.round((anchorRect.top + anchorRect.bottom - elRect.height) / 2);
break;
case "top":
top = Math.round(anchorRect.top);
break;
case "bottom":
top = Math.round(anchorRect.bottom - elRect.height);
break;
case "unaligned":
{
top = Math.round((anchorRect.top + anchorRect.bottom - elRect.height) / 2);
var _overhang3 = Math.ceil(top + elRect.height + buffers.all + buffers.bottom - window.innerHeight);
if (_overhang3 > 0) {
top -= _overhang3;
}
top = Math.max(buffers.all + buffers.top, top);
break;
}
default:
vAlign;
throw new Error("Should not happen");
}
}
return {
top: top,
left: left
};
}
//# sourceMappingURL=index.js.map