Newer
Older

Lauri Timmanee
committed
/**
* @file
* A modified version of jQuery UI position.
*
* Per jQuery UI's public domain license, it is permissible to run modified
* versions of their code. This file offers the same functionality as what is
* provided by jQuery UI position, but refactored to meet Drupal coding
* standards, and restructured so it extends jQuery core instead of jQuery UI.
*
* This is provided to support pre-existing code that expects the jQuery
* position API.
*
* @see https://github.com/jquery/jquery-ui/blob/1.12.1/LICENSE.txt
* @see https://raw.githubusercontent.com/jquery/jquery-ui/1.12.1/ui/position.js
*/

Lauri Timmanee
committed
/**
* This provides ported version of jQuery UI position, refactored to not depend
* on jQuery UI and to meet Drupal JavaScript coding standards. Functionality
* and usage is identical. It positions an element relative to another. The
* `position()` function can be called by any jQuery object. Additional details
* on using `position()` are provided in this file in the docblock for
* $.fn.position.
*/
(($) => {
let cachedScrollbarWidth = null;
const { max, abs } = Math;
const regexHorizontal = /left|center|right/;
const regexVertical = /top|center|bottom/;
const regexOffset = /[+-]\d+(\.[\d]+)?%?/;
const regexPosition = /^\w+/;
const _position = $.fn.position;

Lauri Timmanee
committed
function getOffsets(offsets, width, height) {
return [
parseFloat(offsets[0]) *

Théodore Biadala
committed
(typeof offsets[0] === 'string' && offsets[0].endsWith('%')
? width / 100
: 1),
parseFloat(offsets[1]) *

Théodore Biadala
committed
(typeof offsets[1] === 'string' && offsets[1].endsWith('%')
? height / 100
: 1),

Lauri Timmanee
committed
}
function parseCss(element, property) {

Lee Rowlands
committed
return parseInt(window.getComputedStyle(element)[property], 10) || 0;

Lauri Timmanee
committed
}
function getDimensions(elem) {
const raw = elem[0];

Théodore Biadala
committed
if (raw.nodeType === Node.DOCUMENT_NODE) {

Lauri Timmanee
committed
return {
width: elem.width(),
height: elem.height(),
offset: { top: 0, left: 0 },

Lauri Timmanee
committed
};
}

Théodore Biadala
committed
if (!!raw && raw === raw.window) {

Lauri Timmanee
committed
return {
width: elem.width(),
height: elem.height(),
offset: { top: elem.scrollTop(), left: elem.scrollLeft() },

Lauri Timmanee
committed
};
}
if (raw.preventDefault) {
return {
width: 0,
height: 0,
offset: { top: raw.pageY, left: raw.pageX },

Lauri Timmanee
committed
};
}
return {
width: elem.outerWidth(),
height: elem.outerHeight(),
offset: elem.offset(),

Lauri Timmanee
committed
};
}
const collisions = {

Lauri Timmanee
committed
fit: {
left(position, data) {
const { within } = data;
const withinOffset = within.isWindow
? within.scrollLeft
: within.offset.left;
const outerWidth = within.width;
const collisionPosLeft =
position.left - data.collisionPosition.marginLeft;
const overLeft = withinOffset - collisionPosLeft;
const overRight =
collisionPosLeft + data.collisionWidth - outerWidth - withinOffset;
let newOverRight;

Lauri Timmanee
committed
// Element is wider than within

Lauri Timmanee
committed
if (data.collisionWidth > outerWidth) {
// Element is initially over the left side of within

Lauri Timmanee
committed
if (overLeft > 0 && overRight <= 0) {
newOverRight =
position.left +
overLeft +
data.collisionWidth -
outerWidth -
withinOffset;

Lauri Timmanee
committed
position.left += overLeft - newOverRight;
// Element is initially over right side of within

Lauri Timmanee
committed
} else if (overRight > 0 && overLeft <= 0) {
position.left = withinOffset;
// Element is initially over both left and right sides of within

Lauri Timmanee
committed
} else if (overLeft > overRight) {
position.left = withinOffset + outerWidth - data.collisionWidth;
} else {
position.left = withinOffset;
}
// Too far left -> align with left edge

Lauri Timmanee
committed
} else if (overLeft > 0) {
position.left += overLeft;
// Too far right -> align with right edge

Lauri Timmanee
committed
} else if (overRight > 0) {
position.left -= overRight;
// Adjust based on position and margin

Lauri Timmanee
committed
} else {
position.left = max(position.left - collisionPosLeft, position.left);
}
},
top(position, data) {
const { within } = data;
const withinOffset = within.isWindow
? within.scrollTop
: within.offset.top;
const outerHeight = data.within.height;
const collisionPosTop = position.top - data.collisionPosition.marginTop;
const overTop = withinOffset - collisionPosTop;
const overBottom =
collisionPosTop + data.collisionHeight - outerHeight - withinOffset;
let newOverBottom;

Lauri Timmanee
committed
// Element is taller than within

Lauri Timmanee
committed
if (data.collisionHeight > outerHeight) {
// Element is initially over the top of within

Lauri Timmanee
committed
if (overTop > 0 && overBottom <= 0) {
newOverBottom =
position.top +
overTop +
data.collisionHeight -
outerHeight -
withinOffset;

Lauri Timmanee
committed
position.top += overTop - newOverBottom;
// Element is initially over bottom of within

Lauri Timmanee
committed
} else if (overBottom > 0 && overTop <= 0) {
position.top = withinOffset;
// Element is initially over both top and bottom of within

Lauri Timmanee
committed
} else if (overTop > overBottom) {
position.top = withinOffset + outerHeight - data.collisionHeight;
} else {
position.top = withinOffset;
}
// Too far up -> align with top

Lauri Timmanee
committed
} else if (overTop > 0) {
position.top += overTop;
// Too far down -> align with bottom edge

Lauri Timmanee
committed
} else if (overBottom > 0) {
position.top -= overBottom;
// Adjust based on position and margin

Lauri Timmanee
committed
} else {
position.top = max(position.top - collisionPosTop, position.top);
}

Lauri Timmanee
committed
},
flip: {
left(position, data) {
const { within } = data;
const withinOffset = within.offset.left + within.scrollLeft;
const outerWidth = within.width;
const offsetLeft = within.isWindow
? within.scrollLeft
: within.offset.left;
const collisionPosLeft =
position.left - data.collisionPosition.marginLeft;
const overLeft = collisionPosLeft - offsetLeft;
const overRight =
collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft;
const myOffset =
// eslint-disable-next-line no-nested-ternary
data.my[0] === 'left'
? -data.elemWidth
: data.my[0] === 'right'
? data.elemWidth
: 0;
const atOffset =
// eslint-disable-next-line no-nested-ternary
data.at[0] === 'left'
? data.targetWidth
: data.at[0] === 'right'
? -data.targetWidth
: 0;
const offset = -2 * data.offset[0];
let newOverRight;
let newOverLeft;

Lauri Timmanee
committed
if (overLeft < 0) {
newOverRight =
position.left +
myOffset +
atOffset +
offset +
data.collisionWidth -
outerWidth -
withinOffset;

Lauri Timmanee
committed
if (newOverRight < 0 || newOverRight < abs(overLeft)) {
position.left += myOffset + atOffset + offset;
}
} else if (overRight > 0) {
newOverLeft =
position.left -
data.collisionPosition.marginLeft +
myOffset +
atOffset +
offset -
offsetLeft;

Lauri Timmanee
committed
if (newOverLeft > 0 || abs(newOverLeft) < overRight) {
position.left += myOffset + atOffset + offset;
}
}
},
top(position, data) {
const { within } = data;
const withinOffset = within.offset.top + within.scrollTop;
const outerHeight = within.height;
const offsetTop = within.isWindow
? within.scrollTop
: within.offset.top;
const collisionPosTop = position.top - data.collisionPosition.marginTop;
const overTop = collisionPosTop - offsetTop;
const overBottom =
collisionPosTop + data.collisionHeight - outerHeight - offsetTop;
const top = data.my[1] === 'top';
// eslint-disable-next-line no-nested-ternary
const myOffset = top
? -data.elemHeight
: data.my[1] === 'bottom'
? data.elemHeight
: 0;
const atOffset =
// eslint-disable-next-line no-nested-ternary
data.at[1] === 'top'
? data.targetHeight
: data.at[1] === 'bottom'
? -data.targetHeight
: 0;
const offset = -2 * data.offset[1];
let newOverTop;
let newOverBottom;

Lauri Timmanee
committed
if (overTop < 0) {
newOverBottom =
position.top +
myOffset +
atOffset +
offset +
data.collisionHeight -
outerHeight -
withinOffset;

Lauri Timmanee
committed
if (newOverBottom < 0 || newOverBottom < abs(overTop)) {
position.top += myOffset + atOffset + offset;
}
} else if (overBottom > 0) {
newOverTop =
position.top -
data.collisionPosition.marginTop +
myOffset +
atOffset +
offset -
offsetTop;

Lauri Timmanee
committed
if (newOverTop > 0 || abs(newOverTop) < overBottom) {
position.top += myOffset + atOffset + offset;
}
}

Lauri Timmanee
committed
},
flipfit: {
left(...args) {

Lauri Timmanee
committed
collisions.flip.left.apply(this, args);
collisions.fit.left.apply(this, args);
},
top(...args) {

Lauri Timmanee
committed
collisions.flip.top.apply(this, args);
collisions.fit.top.apply(this, args);

Lauri Timmanee
committed
};

Lauri Timmanee
committed
$.position = {
scrollbarWidth() {

Lauri Timmanee
committed
if (cachedScrollbarWidth !== undefined) {
return cachedScrollbarWidth;
}
const div = $(
'<div ' +
"style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'>" +
"<div style='height:100px;width:auto;'></div></div>",
);
const innerDiv = div.children()[0];

Lauri Timmanee
committed
$('body').append(div);
const w1 = innerDiv.offsetWidth;

Lee Rowlands
committed
div[0].style.overflow = 'scroll';
let w2 = innerDiv.offsetWidth;

Lauri Timmanee
committed
if (w1 === w2) {
w2 = div[0].clientWidth;
}
div.remove();
cachedScrollbarWidth = w1 - w2;
return cachedScrollbarWidth;
},
getScrollInfo(within) {
const overflowX =
within.isWindow || within.isDocument
? ''

Lee Rowlands
committed
: window.getComputedStyle(within.element[0])['overflow-x'];
const overflowY =
within.isWindow || within.isDocument
? ''

Lee Rowlands
committed
: window.getComputedStyle(within.element[0])['overflow-y'];
const hasOverflowX =
overflowX === 'scroll' ||
(overflowX === 'auto' && within.width < within.element[0].scrollWidth);
const hasOverflowY =
overflowY === 'scroll' ||
(overflowY === 'auto' &&
within.height < within.element[0].scrollHeight);

Lauri Timmanee
committed
return {
width: hasOverflowY ? $.position.scrollbarWidth() : 0,
height: hasOverflowX ? $.position.scrollbarWidth() : 0,

Lauri Timmanee
committed
};
},
getWithinInfo(element) {
const withinElement = $(element || window);

Théodore Biadala
committed
const isWindow =
!!withinElement[0] && withinElement[0] === withinElement[0].window;

Théodore Biadala
committed
const isDocument =
!!withinElement[0] && withinElement[0].nodeType === Node.DOCUMENT_NODE;
const hasOffset = !isWindow && !isDocument;

Lauri Timmanee
committed
return {
element: withinElement,
isWindow,
isDocument,
offset: hasOffset ? $(element).offset() : { left: 0, top: 0 },

Lauri Timmanee
committed
scrollLeft: withinElement.scrollLeft(),
scrollTop: withinElement.scrollTop(),
width: withinElement.outerWidth(),
height: withinElement.outerHeight(),

Lauri Timmanee
committed
};

Lauri Timmanee
committed
};
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
// eslint-disable-next-line func-names
/**
* Positions an element relative to another.
*
* The following documentation is originally from
* {@link https://api.jqueryui.com/position/}.
*
* @param {Object} options - the options object.
* @param {string} options.my - Defines which position on the element being
* positioned to align with the target element: "horizontal vertical"
* alignment. A single value such as "right" will be normalized to "right
* center", "top" will be normalized to "center top" (following CSS
* convention). Acceptable horizontal values: "left", "center", "right".
* Acceptable vertical values: "top", "center", "bottom". Example: "left
* top" or "center center". Each dimension can also contain offsets, in
* pixels or percent, e.g., "right+10 top-25%". Percentage offsets are
* relative to the element being positioned. Default value is "center".
* @param {string} options.at - Defines which position on the target element
* to align the positioned element against: "horizontal vertical" alignment.
* See the `my` option for full details on possible values. Percentage
* offsets are relative to the target element. Default value is "center".
* @param {string|Element|jQuery|Event|null} options.of - Which element to
* position against. If you provide a selector or jQuery object, the first
* matching element will be used. If you provide an event object, the pageX
* and pageY properties will be used. Example: "#top-menu". Default value is
* null.
* @param {string} options.collision - When the positioned element overflows
* the window in some direction, move it to an alternative position. Similar
* to `my` and `at`, this accepts a single value or a pair for
* horizontal/vertical, e.g., "flip", "fit", "fit flip", "fit none". Default
* value is "flip". The options work as follows:
* - "flip": Flips the element to the opposite side of the target and the
* collision detection is run again to see if it will fit. Whichever side
* allows more of the element to be visible will be used.
* - "fit": Shift the element away from the edge of the window.
* - "flipfit": First applies the flip logic, placing the element on
* whichever side allows more of the element to be visible. Then the fit
* logic is applied to ensure as much of the element is visible as
* possible.
* "none": Does not apply any collision detection.
* @param {function|null} options.using - When specified, the actual property
* setting is delegated to this callback. Receives two parameters: The first
* is a hash of top and left values for the position that should be set and
* can be forwarded to .css() or .animate().The second provides feedback
* about the position and dimensions of both elements, as well as
* calculations to their relative position. Both target and element have
* these properties: element, left, top, width, height. In addition, there's
* horizontal, vertical and important, providing twelve potential directions
* like { horizontal: "center", vertical: "left", important: "horizontal" }.
* Default value is null.
* @param {string|Element|jQuery} options.within - Element to position within,
* affecting collision detection. If you provide a selector or jQuery
* object, the first matching element will be used. Default value is window.
*
* @return {jQuery}
* The jQuery object that called called this function.
*/

Lauri Timmanee
committed
$.fn.position = function (options) {
if (!options || !options.of) {
// eslint-disable-next-line prefer-rest-params

Lauri Timmanee
committed
return _position.apply(this, arguments);
}
// Make a copy, we don't want to modify arguments

Lauri Timmanee
committed
options = $.extend({}, options);
const within = $.position.getWithinInfo(options.within);
const scrollInfo = $.position.getScrollInfo(within);
const collision = (options.collision || 'flip').split(' ');
const offsets = {};
// Make sure string options are treated as CSS selectors
const target =
typeof options.of === 'string'
? $(document).find(options.of)
: $(options.of);
const dimensions = getDimensions(target);
const targetWidth = dimensions.width;
const targetHeight = dimensions.height;
const targetOffset = dimensions.offset;

Lauri Timmanee
committed
if (target[0].preventDefault) {
// Force left top to allow flipping

Lauri Timmanee
committed
options.at = 'left top';
}
// Clone to reuse original targetOffset later
const basePosition = $.extend({}, targetOffset);
// Force my and at to have valid horizontal and vertical positions
// if a value is missing or invalid, it will be converted to center
// eslint-disable-next-line func-names

Lauri Timmanee
committed
$.each(['my', 'at'], function () {
let pos = (options[this] || '').split(' ');

Lauri Timmanee
committed
if (pos.length === 1) {
// eslint-disable-next-line no-nested-ternary
pos = regexHorizontal.test(pos[0])
? pos.concat(['center'])
: regexVertical.test(pos[0])
? ['center'].concat(pos)
: ['center', 'center'];

Lauri Timmanee
committed
}
pos[0] = regexHorizontal.test(pos[0]) ? pos[0] : 'center';
pos[1] = regexVertical.test(pos[1]) ? pos[1] : 'center';
// Calculate offsets
const horizontalOffset = regexOffset.exec(pos[0]);
const verticalOffset = regexOffset.exec(pos[1]);
offsets[this] = [
horizontalOffset ? horizontalOffset[0] : 0,
verticalOffset ? verticalOffset[0] : 0,
];
// Reduce to just the positions without the offsets
options[this] = [
regexPosition.exec(pos[0])[0],
regexPosition.exec(pos[1])[0],
];

Lauri Timmanee
committed
});
// Normalize collision option

Lauri Timmanee
committed
if (collision.length === 1) {
// eslint-disable-next-line prefer-destructuring

Lauri Timmanee
committed
collision[1] = collision[0];
}
if (options.at[0] === 'right') {
basePosition.left += targetWidth;
} else if (options.at[0] === 'center') {
basePosition.left += targetWidth / 2;
}
if (options.at[1] === 'bottom') {
basePosition.top += targetHeight;
} else if (options.at[1] === 'center') {
basePosition.top += targetHeight / 2;
}
const atOffset = getOffsets(offsets.at, targetWidth, targetHeight);

Lauri Timmanee
committed
basePosition.left += atOffset[0];
basePosition.top += atOffset[1];
// eslint-disable-next-line func-names

Lauri Timmanee
committed
return this.each(function () {
let using;
const elem = $(this);
const elemWidth = elem.outerWidth();
const elemHeight = elem.outerHeight();
const marginLeft = parseCss(this, 'marginLeft');
const marginTop = parseCss(this, 'marginTop');
const collisionWidth =
elemWidth +
marginLeft +
parseCss(this, 'marginRight') +
scrollInfo.width;
const collisionHeight =
elemHeight +
marginTop +
parseCss(this, 'marginBottom') +
scrollInfo.height;
const position = $.extend({}, basePosition);
const myOffset = getOffsets(
offsets.my,
elem.outerWidth(),
elem.outerHeight(),
);

Lauri Timmanee
committed
if (options.my[0] === 'right') {
position.left -= elemWidth;
} else if (options.my[0] === 'center') {
position.left -= elemWidth / 2;
}
if (options.my[1] === 'bottom') {
position.top -= elemHeight;
} else if (options.my[1] === 'center') {
position.top -= elemHeight / 2;
}
position.left += myOffset[0];
position.top += myOffset[1];
const collisionPosition = {
marginLeft,

Lauri Timmanee
committed
};
// eslint-disable-next-line func-names

Lauri Timmanee
committed
$.each(['left', 'top'], function (i, dir) {
if (collisions[collision[i]]) {
collisions[collision[i]][dir](position, {
targetWidth,
targetHeight,
elemWidth,
elemHeight,
collisionPosition,
collisionWidth,
collisionHeight,

Lauri Timmanee
committed
offset: [atOffset[0] + myOffset[0], atOffset[1] + myOffset[1]],
my: options.my,
at: options.at,
within,

Lauri Timmanee
committed
});
}
});
if (options.using) {
// Adds feedback as second argument to using callback, if present
// eslint-disable-next-line func-names
using = function (props) {
const left = targetOffset.left - position.left;
const right = left + targetWidth - elemWidth;
const top = targetOffset.top - position.top;
const bottom = top + targetHeight - elemHeight;
const feedback = {

Lauri Timmanee
committed
target: {
element: target,
left: targetOffset.left,
top: targetOffset.top,
width: targetWidth,
height: targetHeight,

Lauri Timmanee
committed
},
element: {
element: elem,
left: position.left,
top: position.top,
width: elemWidth,
height: elemHeight,

Lauri Timmanee
committed
},
// eslint-disable-next-line no-nested-ternary

Lauri Timmanee
committed
horizontal: right < 0 ? 'left' : left > 0 ? 'right' : 'center',
// eslint-disable-next-line no-nested-ternary
vertical: bottom < 0 ? 'top' : top > 0 ? 'bottom' : 'middle',

Lauri Timmanee
committed
};
if (targetWidth < elemWidth && abs(left + right) < targetWidth) {
feedback.horizontal = 'center';
}
if (targetHeight < elemHeight && abs(top + bottom) < targetHeight) {
feedback.vertical = 'middle';
}
if (max(abs(left), abs(right)) > max(abs(top), abs(bottom))) {
feedback.important = 'horizontal';
} else {
feedback.important = 'vertical';
}
options.using.call(this, props, feedback);
};
}
elem.offset($.extend(position, { using }));

Lauri Timmanee
committed
});
};
// Although $.ui.position is not built to be called directly, some legacy code
// may have checks for the presence of $.ui.position, which can be used to
// confirm the presence of jQuery UI position's API, as opposed to the more
// limited version provided by jQuery.

Lauri Timmanee
committed
if (!$.hasOwnProperty('ui')) {
$.ui = {};
}
$.ui.position = collisions;
})(jQuery);