diff --git a/core/core.libraries.yml b/core/core.libraries.yml index e366dc84c9af1bfe51913a3919fe9148073494a0..551929cd63a18185c2b349ae6dfeee85ac78829b 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -115,7 +115,6 @@ drupal.autocomplete: assets/vendor/jquery.ui/ui/jquery-1-7-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/keycode-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/plugin-min.js: { weight: -11.8, minified: true } - assets/vendor/jquery.ui/ui/position-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/safe-active-element-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/safe-blur-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/scroll-parent-min.js: { weight: -11.8, minified: true } @@ -137,6 +136,8 @@ drupal.autocomplete: - core/drupalSettings - core/drupal.ajax - core/tabbable.jquery.shim + - core/drupal.jquery.position + drupal.array.find: version: VERSION @@ -222,7 +223,6 @@ drupal.dialog: assets/vendor/jquery.ui/ui/ie-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/keycode-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/plugin-min.js: { weight: -11.8, minified: true } - assets/vendor/jquery.ui/ui/position-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/safe-active-element-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/safe-blur-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/widget-min.js: { weight: -11.8, minified: true } @@ -246,6 +246,7 @@ drupal.dialog: - core/drupal.debounce - core/drupal.displace - core/tabbable.jquery.shim + - core/drupal.jquery.position drupal.dialog.ajax: version: VERSION @@ -604,7 +605,6 @@ jquery.ui.dialog: assets/vendor/jquery.ui/ui/jquery-1-7-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/keycode-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/plugin-min.js: { weight: -11.8, minified: true } - assets/vendor/jquery.ui/ui/position-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/safe-active-element-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/safe-blur-min.js: { weight: -11.8, minified: true } assets/vendor/jquery.ui/ui/scroll-parent-min.js: { weight: -11.8, minified: true } @@ -624,6 +624,7 @@ jquery.ui.dialog: dependencies: - core/jquery - core/tabbable.jquery.shim + - core/drupal.jquery.position deprecated: *jquery_ui_unused_deprecated jquery.ui.draggable: @@ -661,13 +662,23 @@ jquery.ui.mouse: - core/jquery.ui.widget deprecated: *jquery_ui_unused_deprecated +drupal.jquery.position: + # For most positioning needs, the core/popperjs library should be used. This + # is a modified version of jQuery UI position for that does not require any + # jQuery UI assets, only jQuery. It is provided by core for use with + # pre-existing libraries that expect the jQuery UI position API. Popperjs is + # recommended for any new uses. + version: VERSION + js: + misc/position.js: {} + dependencies: + - core/jquery + jquery.ui.position: version: *jquery_ui_version license: *jquery_ui_license - js: - assets/vendor/jquery.ui/ui/position-min.js: { weight: -11.8, minified: true } dependencies: - - core/jquery.ui + - core/drupal.jquery.position deprecated: *jquery_ui_unused_deprecated jquery.ui.resizable: diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt index de78c6c615fccbd7949c3f58a3781d85da72d4d4..d660d22c5f1a6398cd071610b068081b72b222b3 100644 --- a/core/misc/cspell/dictionary.txt +++ b/core/misc/cspell/dictionary.txt @@ -595,6 +595,7 @@ fixnull flexbox flexslider flickr +flipfit floatingspace foaf foat diff --git a/core/misc/position.es6.js b/core/misc/position.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..4bd6baa34213b18c3add6049beeac21b24aee592 --- /dev/null +++ b/core/misc/position.es6.js @@ -0,0 +1,625 @@ +/** + * @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. + * + * For most positioning needs, the core/popperjs library should be used instead + * of the functionality provided here. 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 + */ + +/** + * 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 regexPercent = /%$/; + const _position = $.fn.position; + + function getOffsets(offsets, width, height) { + return [ + parseFloat(offsets[0]) * + (regexPercent.test(offsets[0]) ? width / 100 : 1), + parseFloat(offsets[1]) * + (regexPercent.test(offsets[1]) ? height / 100 : 1), + ]; + } + + function parseCss(element, property) { + return parseInt($.css(element, property), 10) || 0; + } + + function getDimensions(elem) { + const raw = elem[0]; + if (raw.nodeType === 9) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: 0, left: 0 }, + }; + } + if ($.isWindow(raw)) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: elem.scrollTop(), left: elem.scrollLeft() }, + }; + } + if (raw.preventDefault) { + return { + width: 0, + height: 0, + offset: { top: raw.pageY, left: raw.pageX }, + }; + } + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset(), + }; + } + + const collisions = { + 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; + + // Element is wider than within + if (data.collisionWidth > outerWidth) { + // Element is initially over the left side of within + if (overLeft > 0 && overRight <= 0) { + newOverRight = + position.left + + overLeft + + data.collisionWidth - + outerWidth - + withinOffset; + position.left += overLeft - newOverRight; + + // Element is initially over right side of within + } else if (overRight > 0 && overLeft <= 0) { + position.left = withinOffset; + + // Element is initially over both left and right sides of within + } else if (overLeft > overRight) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + + // Too far left -> align with left edge + } else if (overLeft > 0) { + position.left += overLeft; + + // Too far right -> align with right edge + } else if (overRight > 0) { + position.left -= overRight; + + // Adjust based on position and margin + } 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; + + // Element is taller than within + if (data.collisionHeight > outerHeight) { + // Element is initially over the top of within + if (overTop > 0 && overBottom <= 0) { + newOverBottom = + position.top + + overTop + + data.collisionHeight - + outerHeight - + withinOffset; + position.top += overTop - newOverBottom; + + // Element is initially over bottom of within + } else if (overBottom > 0 && overTop <= 0) { + position.top = withinOffset; + + // Element is initially over both top and bottom of within + } else if (overTop > overBottom) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + + // Too far up -> align with top + } else if (overTop > 0) { + position.top += overTop; + + // Too far down -> align with bottom edge + } else if (overBottom > 0) { + position.top -= overBottom; + + // Adjust based on position and margin + } else { + position.top = max(position.top - collisionPosTop, position.top); + } + }, + }, + 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; + + if (overLeft < 0) { + newOverRight = + position.left + + myOffset + + atOffset + + offset + + data.collisionWidth - + outerWidth - + withinOffset; + 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; + 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; + if (overTop < 0) { + newOverBottom = + position.top + + myOffset + + atOffset + + offset + + data.collisionHeight - + outerHeight - + withinOffset; + 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; + if (newOverTop > 0 || abs(newOverTop) < overBottom) { + position.top += myOffset + atOffset + offset; + } + } + }, + }, + flipfit: { + left(...args) { + collisions.flip.left.apply(this, args); + collisions.fit.left.apply(this, args); + }, + top(...args) { + collisions.flip.top.apply(this, args); + collisions.fit.top.apply(this, args); + }, + }, + }; + + $.position = { + scrollbarWidth() { + 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]; + + $('body').append(div); + const w1 = innerDiv.offsetWidth; + div.css('overflow', 'scroll'); + + let w2 = innerDiv.offsetWidth; + + if (w1 === w2) { + w2 = div[0].clientWidth; + } + + div.remove(); + cachedScrollbarWidth = w1 - w2; + return cachedScrollbarWidth; + }, + getScrollInfo(within) { + const overflowX = + within.isWindow || within.isDocument + ? '' + : within.element.css('overflow-x'); + const overflowY = + within.isWindow || within.isDocument + ? '' + : within.element.css('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); + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0, + }; + }, + getWithinInfo(element) { + const withinElement = $(element || window); + const isWindow = $.isWindow(withinElement[0]); + const isDocument = !!withinElement[0] && withinElement[0].nodeType === 9; + const hasOffset = !isWindow && !isDocument; + return { + element: withinElement, + isWindow, + isDocument, + offset: hasOffset ? $(element).offset() : { left: 0, top: 0 }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: withinElement.outerWidth(), + height: withinElement.outerHeight(), + }; + }, + }; + + // 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. + */ + $.fn.position = function (options) { + if (!options || !options.of) { + // eslint-disable-next-line prefer-rest-params + return _position.apply(this, arguments); + } + + // Make a copy, we don't want to modify arguments + options = $.extend({}, options); + + const within = $.position.getWithinInfo(options.within); + const scrollInfo = $.position.getScrollInfo(within); + const collision = (options.collision || 'flip').split(' '); + const offsets = {}; + + const target = $(options.of); + const dimensions = getDimensions(target); + const targetWidth = dimensions.width; + const targetHeight = dimensions.height; + const targetOffset = dimensions.offset; + + if (target[0].preventDefault) { + // Force left top to allow flipping + 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 + $.each(['my', 'at'], function () { + let pos = (options[this] || '').split(' '); + + 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']; + } + 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], + ]; + }); + + // Normalize collision option + if (collision.length === 1) { + // eslint-disable-next-line prefer-destructuring + 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); + basePosition.left += atOffset[0]; + basePosition.top += atOffset[1]; + + // eslint-disable-next-line func-names + 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(), + ); + + 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, + marginTop, + }; + + // eslint-disable-next-line func-names + $.each(['left', 'top'], function (i, dir) { + if (collisions[collision[i]]) { + collisions[collision[i]][dir](position, { + targetWidth, + targetHeight, + elemWidth, + elemHeight, + collisionPosition, + collisionWidth, + collisionHeight, + offset: [atOffset[0] + myOffset[0], atOffset[1] + myOffset[1]], + my: options.my, + at: options.at, + within, + elem, + }); + } + }); + + 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 = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight, + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight, + }, + // eslint-disable-next-line no-nested-ternary + horizontal: right < 0 ? 'left' : left > 0 ? 'right' : 'center', + // eslint-disable-next-line no-nested-ternary + vertical: bottom < 0 ? 'top' : top > 0 ? 'bottom' : 'middle', + }; + 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 })); + }); + }; + + // 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. + if (!$.hasOwnProperty('ui')) { + $.ui = {}; + } + $.ui.position = collisions; +})(jQuery); diff --git a/core/misc/position.js b/core/misc/position.js new file mode 100644 index 0000000000000000000000000000000000000000..45d23e00518a50163aa250acb0c2744ae966dfec --- /dev/null +++ b/core/misc/position.js @@ -0,0 +1,417 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($) { + var cachedScrollbarWidth = null; + var max = Math.max, + abs = Math.abs; + var regexHorizontal = /left|center|right/; + var regexVertical = /top|center|bottom/; + var regexOffset = /[+-]\d+(\.[\d]+)?%?/; + var regexPosition = /^\w+/; + var regexPercent = /%$/; + var _position = $.fn.position; + + function getOffsets(offsets, width, height) { + return [parseFloat(offsets[0]) * (regexPercent.test(offsets[0]) ? width / 100 : 1), parseFloat(offsets[1]) * (regexPercent.test(offsets[1]) ? height / 100 : 1)]; + } + + function parseCss(element, property) { + return parseInt($.css(element, property), 10) || 0; + } + + function getDimensions(elem) { + var raw = elem[0]; + + if (raw.nodeType === 9) { + return { + width: elem.width(), + height: elem.height(), + offset: { + top: 0, + left: 0 + } + }; + } + + if ($.isWindow(raw)) { + return { + width: elem.width(), + height: elem.height(), + offset: { + top: elem.scrollTop(), + left: elem.scrollLeft() + } + }; + } + + if (raw.preventDefault) { + return { + width: 0, + height: 0, + offset: { + top: raw.pageY, + left: raw.pageX + } + }; + } + + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset() + }; + } + + var collisions = { + fit: { + left: function left(position, data) { + var within = data.within; + var withinOffset = within.isWindow ? within.scrollLeft : within.offset.left; + var outerWidth = within.width; + var collisionPosLeft = position.left - data.collisionPosition.marginLeft; + var overLeft = withinOffset - collisionPosLeft; + var overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset; + var newOverRight; + + if (data.collisionWidth > outerWidth) { + if (overLeft > 0 && overRight <= 0) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; + position.left += overLeft - newOverRight; + } else if (overRight > 0 && overLeft <= 0) { + position.left = withinOffset; + } else if (overLeft > overRight) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } else if (overLeft > 0) { + position.left += overLeft; + } else if (overRight > 0) { + position.left -= overRight; + } else { + position.left = max(position.left - collisionPosLeft, position.left); + } + }, + top: function top(position, data) { + var within = data.within; + var withinOffset = within.isWindow ? within.scrollTop : within.offset.top; + var outerHeight = data.within.height; + var collisionPosTop = position.top - data.collisionPosition.marginTop; + var overTop = withinOffset - collisionPosTop; + var overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset; + var newOverBottom; + + if (data.collisionHeight > outerHeight) { + if (overTop > 0 && overBottom <= 0) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; + position.top += overTop - newOverBottom; + } else if (overBottom > 0 && overTop <= 0) { + position.top = withinOffset; + } else if (overTop > overBottom) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } else if (overTop > 0) { + position.top += overTop; + } else if (overBottom > 0) { + position.top -= overBottom; + } else { + position.top = max(position.top - collisionPosTop, position.top); + } + } + }, + flip: { + left: function left(position, data) { + var within = data.within; + var withinOffset = within.offset.left + within.scrollLeft; + var outerWidth = within.width; + var offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left; + var collisionPosLeft = position.left - data.collisionPosition.marginLeft; + var overLeft = collisionPosLeft - offsetLeft; + var overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft; + var myOffset = data.my[0] === 'left' ? -data.elemWidth : data.my[0] === 'right' ? data.elemWidth : 0; + var atOffset = data.at[0] === 'left' ? data.targetWidth : data.at[0] === 'right' ? -data.targetWidth : 0; + var offset = -2 * data.offset[0]; + var newOverRight; + var newOverLeft; + + if (overLeft < 0) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; + + 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; + + if (newOverLeft > 0 || abs(newOverLeft) < overRight) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function top(position, data) { + var within = data.within; + var withinOffset = within.offset.top + within.scrollTop; + var outerHeight = within.height; + var offsetTop = within.isWindow ? within.scrollTop : within.offset.top; + var collisionPosTop = position.top - data.collisionPosition.marginTop; + var overTop = collisionPosTop - offsetTop; + var overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop; + var top = data.my[1] === 'top'; + var myOffset = top ? -data.elemHeight : data.my[1] === 'bottom' ? data.elemHeight : 0; + var atOffset = data.at[1] === 'top' ? data.targetHeight : data.at[1] === 'bottom' ? -data.targetHeight : 0; + var offset = -2 * data.offset[1]; + var newOverTop; + var newOverBottom; + + if (overTop < 0) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; + + 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; + + if (newOverTop > 0 || abs(newOverTop) < overBottom) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function left() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + collisions.flip.left.apply(this, args); + collisions.fit.left.apply(this, args); + }, + top: function top() { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + collisions.flip.top.apply(this, args); + collisions.fit.top.apply(this, args); + } + } + }; + $.position = { + scrollbarWidth: function scrollbarWidth() { + if (cachedScrollbarWidth !== undefined) { + return cachedScrollbarWidth; + } + + var div = $('<div ' + "style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'>" + "<div style='height:100px;width:auto;'></div></div>"); + var innerDiv = div.children()[0]; + $('body').append(div); + var w1 = innerDiv.offsetWidth; + div.css('overflow', 'scroll'); + var w2 = innerDiv.offsetWidth; + + if (w1 === w2) { + w2 = div[0].clientWidth; + } + + div.remove(); + cachedScrollbarWidth = w1 - w2; + return cachedScrollbarWidth; + }, + getScrollInfo: function getScrollInfo(within) { + var overflowX = within.isWindow || within.isDocument ? '' : within.element.css('overflow-x'); + var overflowY = within.isWindow || within.isDocument ? '' : within.element.css('overflow-y'); + var hasOverflowX = overflowX === 'scroll' || overflowX === 'auto' && within.width < within.element[0].scrollWidth; + var hasOverflowY = overflowY === 'scroll' || overflowY === 'auto' && within.height < within.element[0].scrollHeight; + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function getWithinInfo(element) { + var withinElement = $(element || window); + var isWindow = $.isWindow(withinElement[0]); + var isDocument = !!withinElement[0] && withinElement[0].nodeType === 9; + var hasOffset = !isWindow && !isDocument; + return { + element: withinElement, + isWindow: isWindow, + isDocument: isDocument, + offset: hasOffset ? $(element).offset() : { + left: 0, + top: 0 + }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: withinElement.outerWidth(), + height: withinElement.outerHeight() + }; + } + }; + + $.fn.position = function (options) { + if (!options || !options.of) { + return _position.apply(this, arguments); + } + + options = $.extend({}, options); + var within = $.position.getWithinInfo(options.within); + var scrollInfo = $.position.getScrollInfo(within); + var collision = (options.collision || 'flip').split(' '); + var offsets = {}; + var target = $(options.of); + var dimensions = getDimensions(target); + var targetWidth = dimensions.width; + var targetHeight = dimensions.height; + var targetOffset = dimensions.offset; + + if (target[0].preventDefault) { + options.at = 'left top'; + } + + var basePosition = $.extend({}, targetOffset); + $.each(['my', 'at'], function () { + var pos = (options[this] || '').split(' '); + + if (pos.length === 1) { + pos = regexHorizontal.test(pos[0]) ? pos.concat(['center']) : regexVertical.test(pos[0]) ? ['center'].concat(pos) : ['center', 'center']; + } + + pos[0] = regexHorizontal.test(pos[0]) ? pos[0] : 'center'; + pos[1] = regexVertical.test(pos[1]) ? pos[1] : 'center'; + var horizontalOffset = regexOffset.exec(pos[0]); + var verticalOffset = regexOffset.exec(pos[1]); + offsets[this] = [horizontalOffset ? horizontalOffset[0] : 0, verticalOffset ? verticalOffset[0] : 0]; + options[this] = [regexPosition.exec(pos[0])[0], regexPosition.exec(pos[1])[0]]; + }); + + if (collision.length === 1) { + 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; + } + + var atOffset = getOffsets(offsets.at, targetWidth, targetHeight); + basePosition.left += atOffset[0]; + basePosition.top += atOffset[1]; + return this.each(function () { + var using; + var elem = $(this); + var elemWidth = elem.outerWidth(); + var elemHeight = elem.outerHeight(); + var marginLeft = parseCss(this, 'marginLeft'); + var marginTop = parseCss(this, 'marginTop'); + var collisionWidth = elemWidth + marginLeft + parseCss(this, 'marginRight') + scrollInfo.width; + var collisionHeight = elemHeight + marginTop + parseCss(this, 'marginBottom') + scrollInfo.height; + var position = $.extend({}, basePosition); + var myOffset = getOffsets(offsets.my, elem.outerWidth(), elem.outerHeight()); + + 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]; + var collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + $.each(['left', 'top'], function (i, dir) { + if (collisions[collision[i]]) { + collisions[collision[i]][dir](position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [atOffset[0] + myOffset[0], atOffset[1] + myOffset[1]], + my: options.my, + at: options.at, + within: within, + elem: elem + }); + } + }); + + if (options.using) { + using = function using(props) { + var left = targetOffset.left - position.left; + var right = left + targetWidth - elemWidth; + var top = targetOffset.top - position.top; + var bottom = top + targetHeight - elemHeight; + var feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? 'left' : left > 0 ? 'right' : 'center', + vertical: bottom < 0 ? 'top' : top > 0 ? 'bottom' : 'middle' + }; + + 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: using + })); + }); + }; + + if (!$.hasOwnProperty('ui')) { + $.ui = {}; + } + + $.ui.position = collisions; +})(jQuery); \ No newline at end of file diff --git a/core/modules/system/tests/modules/position_shim_test/css/position.shim.test.css b/core/modules/system/tests/modules/position_shim_test/css/position.shim.test.css new file mode 100644 index 0000000000000000000000000000000000000000..3ae288e5821e095c41433c585bc563f33465f6ff --- /dev/null +++ b/core/modules/system/tests/modules/position_shim_test/css/position.shim.test.css @@ -0,0 +1,19 @@ +body { + overflow-x: hidden; +} +#position-reference-1 { + width: 200px; + height: 200px; + margin: 75px 100px; + border: 1px solid black; +} + +.test-tip { + width: 75px; + height: 75px; + border: 1px solid blue; +} + +[data-off-canvas-main-canvas] { + min-height: 1200px; +} diff --git a/core/modules/system/tests/modules/position_shim_test/position_shim_test.info.yml b/core/modules/system/tests/modules/position_shim_test/position_shim_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..a8f2cc2228455e9ca1c6deb407f206da949820e5 --- /dev/null +++ b/core/modules/system/tests/modules/position_shim_test/position_shim_test.info.yml @@ -0,0 +1,5 @@ +name: 'Position Shim Test' +type: module +description: 'Module for the testing jQuery UI position shim' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/position_shim_test/position_shim_test.libraries.yml b/core/modules/system/tests/modules/position_shim_test/position_shim_test.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..c4741ecdb62a75d54bf01c21815f99ce72591738 --- /dev/null +++ b/core/modules/system/tests/modules/position_shim_test/position_shim_test.libraries.yml @@ -0,0 +1,5 @@ +position.shim.test: + version: VERSION + css: + component: + css/position.shim.test.css: {} diff --git a/core/modules/system/tests/modules/position_shim_test/position_shim_test.routing.yml b/core/modules/system/tests/modules/position_shim_test/position_shim_test.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..47c9d04fed80f58a44a368da88042d30f10fcfce --- /dev/null +++ b/core/modules/system/tests/modules/position_shim_test/position_shim_test.routing.yml @@ -0,0 +1,15 @@ +position_test_page: + path: '/position-shim-test' + defaults: + _controller: '\Drupal\position_shim_test\Controller\PositionShimTestController::build' + _title: 'position testing' + requirements: + _access: 'TRUE' + +position_test_ported_jqueryui: + path: '/position-shim-test-ported-from-jqueryui' + defaults: + _controller: '\Drupal\position_shim_test\Controller\PositionShimTestPortedJqueryTestsController::build' + _title: 'ported position testing' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/position_shim_test/src/Controller/PositionShimTestController.php b/core/modules/system/tests/modules/position_shim_test/src/Controller/PositionShimTestController.php new file mode 100644 index 0000000000000000000000000000000000000000..da0c438753b3747f26cbdb16fc5268166cb9df29 --- /dev/null +++ b/core/modules/system/tests/modules/position_shim_test/src/Controller/PositionShimTestController.php @@ -0,0 +1,32 @@ +<?php + +namespace Drupal\position_shim_test\Controller; + +use Drupal\Core\Controller\ControllerBase; + +class PositionShimTestController extends ControllerBase { + + /** + * Provides a page with the jQuery UI position library for testing. + * + * @return array + * The render array. + */ + public function build() { + return [ + 'reference1' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'position-reference-1', + ], + ], + '#attached' => [ + 'library' => [ + 'core/jquery.ui.position', + 'position_shim_test/position.shim.test', + ], + ], + ]; + } + +} diff --git a/core/modules/system/tests/modules/position_shim_test/src/Controller/PositionShimTestPortedJqueryTestsController.php b/core/modules/system/tests/modules/position_shim_test/src/Controller/PositionShimTestPortedJqueryTestsController.php new file mode 100644 index 0000000000000000000000000000000000000000..dde7b735202380c17859b152fee24408c8429bfd --- /dev/null +++ b/core/modules/system/tests/modules/position_shim_test/src/Controller/PositionShimTestPortedJqueryTestsController.php @@ -0,0 +1,121 @@ +<?php + +namespace Drupal\position_shim_test\Controller; + +use Drupal\Core\Controller\ControllerBase; + +class PositionShimTestPortedJqueryTestsController extends ControllerBase { + + /** + * Provides a page with the jQuery UI position library for testing. + * + * @return array + * The render array. + */ + public function build() { + return [ + 'el1' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'el1', + 'style' => 'position: absolute; width: 6px; height: 6px;', + ], + ], + 'el2' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'el2', + 'style' => 'position: absolute; width: 6px; height: 6px;', + ], + ], + 'parent' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'parent', + 'style' => 'position: absolute; width: 6px; height: 6px; top: 4px; left: 4px;', + ], + ], + 'within' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'within', + 'style' => 'position: absolute; width: 12px; height: 12px; top: 2px; left: 0px;', + ], + ], + 'scrollX' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'scrollX', + 'style' => 'position: absolute; top: 0px; left: 0px;', + ], + 'elx' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'elx', + 'style' => 'position: absolute; width: 10px; height: 10px;', + ], + ], + 'parentX' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'parentX', + 'style' => 'position: absolute; width: 20px; height: 20px; top: 40px; left: 40px;', + ], + ], + ], + 'largeBox' => [ + '#type' => 'container', + '#attributes' => [ + 'style' => 'position: absolute; height: 5000px; width: 5000px;', + ], + ], + 'fractionsParent' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'fractions-parent', + 'style' => 'position: absolute; left: 10.7432222px; top: 10.532325px; height: 30px; width: 201px;', + ], + 'fractionsElement' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'fractions-element', + ], + ], + ], + 'bug5280' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'bug-5280', + 'style' => 'height: 30px; width: 201px;', + ], + 'child' => [ + '#type' => 'container', + '#attributes' => [ + 'style' => 'width: 50px; height: 10px;', + ], + ], + ], + 'bug8710withinSmaller' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'bug-8710-within-smaller', + 'style' => 'position: absolute; width: 100px; height: 99px; top: 0px; left: 0px;', + ], + ], + 'bug8710withinBigger' => [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'bug-8710-within-bigger', + 'style' => 'position: absolute; width: 100px; height: 101px; top: 0px; left: 0px;', + ], + ], + '#attached' => [ + 'library' => [ + 'core/jquery.ui.position', + 'position_shim_test/position.shim.test', + ], + ], + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Libraries/JqueryUiLibraryAssetsTest.php b/core/tests/Drupal/FunctionalTests/Libraries/JqueryUiLibraryAssetsTest.php index 597dec65c9edb608e13ba5e32922b476eb2c790a..fe2cbcf1a720d3caf4e81b43b2668327bce49f19 100644 --- a/core/tests/Drupal/FunctionalTests/Libraries/JqueryUiLibraryAssetsTest.php +++ b/core/tests/Drupal/FunctionalTests/Libraries/JqueryUiLibraryAssetsTest.php @@ -64,7 +64,6 @@ protected function setUp(): void { 'jquery.ui.dialog', 'jquery.ui.menu', 'jquery.ui.mouse', - 'jquery.ui.position', 'jquery.ui.resizable', 'jquery.ui.widget', ]; @@ -125,7 +124,6 @@ public function testProperlySetWeights() { 'core/assets/vendor/jquery.ui/ui/jquery-1-7-min.js', 'core/assets/vendor/jquery.ui/ui/keycode-min.js', 'core/assets/vendor/jquery.ui/ui/plugin-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/safe-active-element-min.js', 'core/assets/vendor/jquery.ui/ui/safe-blur-min.js', 'core/assets/vendor/jquery.ui/ui/scroll-parent-min.js', @@ -381,7 +379,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/safe-active-element-min.js', 'core/assets/vendor/jquery.ui/ui/safe-blur-min.js', 'core/assets/vendor/jquery.ui/ui/widget-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/menu-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/autocomplete-min.js', ], @@ -416,7 +413,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/ie-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/mouse-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/draggable-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/resizable-min.js', 'core/assets/vendor/jquery.ui/ui/form-reset-mixin-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/checkboxradio-min.js', @@ -474,7 +470,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/safe-active-element-min.js', 'core/assets/vendor/jquery.ui/ui/safe-blur-min.js', 'core/assets/vendor/jquery.ui/ui/widget-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/menu-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/autocomplete-min.js', ], @@ -540,7 +535,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/ie-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/mouse-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/draggable-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/resizable-min.js', 'core/assets/vendor/jquery.ui/ui/form-reset-mixin-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/checkboxradio-min.js', @@ -573,7 +567,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/safe-active-element-min.js', 'core/assets/vendor/jquery.ui/ui/safe-blur-min.js', 'core/assets/vendor/jquery.ui/ui/widget-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/menu-min.js', ], ], @@ -603,31 +596,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/widgets/mouse-min.js', ], ], - 'jquery.ui.position' => [ - 'library' => 'jquery.ui.position', - 'expected_css' => [ - 'core/assets/vendor/jquery.ui/themes/base/core.css', - 'core/assets/vendor/jquery.ui/themes/base/theme.css', - ], - 'expected_js' => [ - 'core/assets/vendor/jquery.ui/ui/data-min.js', - 'core/assets/vendor/jquery.ui/ui/disable-selection-min.js', - 'core/assets/vendor/jquery.ui/ui/form-min.js', - 'core/assets/vendor/jquery.ui/ui/labels-min.js', - 'core/assets/vendor/jquery.ui/ui/jquery-1-7-min.js', - 'core/assets/vendor/jquery.ui/ui/scroll-parent-min.js', - 'core/assets/vendor/jquery.ui/ui/unique-id-min.js', - 'core/assets/vendor/jquery.ui/ui/version-min.js', - 'core/assets/vendor/jquery.ui/ui/escape-selector-min.js', - 'core/assets/vendor/jquery.ui/ui/focusable-min.js', - 'core/assets/vendor/jquery.ui/ui/ie-min.js', - 'core/assets/vendor/jquery.ui/ui/keycode-min.js', - 'core/assets/vendor/jquery.ui/ui/plugin-min.js', - 'core/assets/vendor/jquery.ui/ui/safe-active-element-min.js', - 'core/assets/vendor/jquery.ui/ui/safe-blur-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', - ], - ], 'jquery.ui.resizable' => [ 'library' => 'jquery.ui.resizable', 'expected_css' => [ @@ -738,7 +706,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/safe-active-element-min.js', 'core/assets/vendor/jquery.ui/ui/safe-blur-min.js', 'core/assets/vendor/jquery.ui/ui/widget-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/menu-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/autocomplete-min.js', 'core/assets/vendor/jquery.ui/ui/ie-min.js', @@ -752,8 +719,8 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/widgets/dialog-min.js', ], ], - 'jquery.ui.widget|jquery.ui.resizable|jquery.ui.position|jquery.ui.mouse|jquery.ui.menu|jquery.ui.dialog|jquery.ui.button|jquery.ui.autocomplete|jquery.ui|drupal.dialog|drupal.autocomplete' => [ - 'library' => 'jquery.ui.widget|jquery.ui.resizable|jquery.ui.position|jquery.ui.mouse|jquery.ui.menu|jquery.ui.dialog|jquery.ui.button|jquery.ui.autocomplete|jquery.ui|drupal.dialog|drupal.autocomplete', + 'jquery.ui.widget|jquery.ui.resizable|jquery.ui.mouse|jquery.ui.menu|jquery.ui.dialog|jquery.ui.button|jquery.ui.autocomplete|jquery.ui|drupal.dialog|drupal.autocomplete' => [ + 'library' => 'jquery.ui.widget|jquery.ui.resizable|jquery.ui.mouse|jquery.ui.menu|jquery.ui.dialog|jquery.ui.button|jquery.ui.autocomplete|jquery.ui|drupal.dialog|drupal.autocomplete', 'expected_css' => [ 'core/assets/vendor/jquery.ui/themes/base/core.css', 'core/assets/vendor/jquery.ui/themes/base/resizable.css', @@ -784,7 +751,6 @@ public function providerTestAssetLoading() { 'core/assets/vendor/jquery.ui/ui/ie-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/mouse-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/resizable-min.js', - 'core/assets/vendor/jquery.ui/ui/position-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/menu-min.js', 'core/assets/vendor/jquery.ui/ui/widgets/draggable-min.js', 'core/assets/vendor/jquery.ui/ui/form-reset-mixin-min.js', diff --git a/core/tests/Drupal/KernelTests/Core/Asset/DeprecatedJqueryUiAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/DeprecatedJqueryUiAssetsTest.php index 87fbd9432308f7f9ce69035766868f7a999297f3..9ee318f6e949a19da8a02fd83851703673aed0b9 100644 --- a/core/tests/Drupal/KernelTests/Core/Asset/DeprecatedJqueryUiAssetsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Asset/DeprecatedJqueryUiAssetsTest.php @@ -23,11 +23,11 @@ public function testDeprecatedJqueryUi() { 'jquery.ui' => '291c28f873a71cd6b3116218d1f5da22', 'jquery.ui.autocomplete' => '153f2836f8f2da39767208b6e09cb5b4', 'jquery.ui.button' => 'ad23e5de0fa1de1f511d10ba2e10d2dd', - 'jquery.ui.dialog' => '729090e5ddcd8563ddade80c3dabc87c', + 'jquery.ui.dialog' => '6521b8917536afe00f35055da4ec466c', 'jquery.ui.draggable' => 'af0f2bdc8aa4ade1e3de8042f31a9312', 'jquery.ui.menu' => '7d0c4d57f43d2f881d2cd5e5b79effbb', 'jquery.ui.mouse' => '626bb203807fa2cdc62510412685df4a', - 'jquery.ui.position' => '6d1759c7d3eb94accbed78416487469b', + 'jquery.ui.position' => 'fec1ca376f2b1cb9b0ca3db36be848c2', 'jquery.ui.resizable' => 'a2448fa87071a17a9756f39c9becb70d', 'jquery.ui.widget' => 'eacd675de09572383b58e52309ba2245', ]; @@ -41,7 +41,7 @@ public function testDeprecatedJqueryUi() { // Confirm that the libraries extending jQuery UI functionality depend on // core/jquery.ui directly or via a dependency on core/jquery.ui.widget. - if ($library !== 'jquery.ui' && $library !== 'jquery.ui.dialog') { + if (!in_array($library, ['jquery.ui', 'jquery.ui.dialog', 'jquery.ui.position'])) { $has_main_or_widget = (in_array('core/jquery.ui', $library_definition['dependencies']) || in_array('core/jquery.ui.widget', $library_definition['dependencies'])); $this->assertTrue($has_main_or_widget, "$library must depend on core/jquery.ui or core/jquery.ui.widget"); } diff --git a/core/tests/Drupal/Nightwatch/Tests/jQueryUIPositionShimTest.js b/core/tests/Drupal/Nightwatch/Tests/jQueryUIPositionShimTest.js new file mode 100644 index 0000000000000000000000000000000000000000..5cb08eee361603a611042b09338e3c498533ef99 --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Tests/jQueryUIPositionShimTest.js @@ -0,0 +1,2403 @@ +/** + * The testScenarios object is for testing a wide range of jQuery UI position + * configuration options. The object properties are: + * { + * - How the `of:` option will be used. This option determines the element the + * positioned element will attach to. This can be a selector, window, a + * jQuery object, or a vanilla JS element. + * - `my`: Sets the 'my' option for position(). + * - `at`: Sets the 'at' option for position(). + * - `x`: The expected X position of the element being positioned. + * - `y`: The expected Y position of the element being positioned. + * } + * This covers every possible combination of `my:` and `at:` using fixed amounts + * (left, right, center, top, bottom), with additional scenarios that include + * offsets. + */ +/* cSpell:disable */ +const testScenarios = { + window: { + centerbottomcenterbottom: { + at: 'center bottom', + my: 'center bottom', + x: 38.5, + y: 77, + }, + centerbottomcentercenter: { + at: 'center center', + my: 'center bottom', + x: 38.5, + y: 77, + }, + centerbottomcentertop: { + at: 'center top', + my: 'center bottom', + x: 38.5, + y: -76.984375, + }, + centerbottomleftbottom: { + at: 'left bottom', + my: 'center bottom', + x: -38.5, + y: 77, + }, + centerbottomleftcenter: { + at: 'left center', + my: 'center bottom', + x: -38.5, + y: 77, + }, + centerbottomlefttop: { + at: 'left top', + my: 'center bottom', + x: -38.5, + y: -76.984375, + }, + centerbottomrightbottom: { + at: 'right bottom', + my: 'center bottom', + x: 38.5, + y: 77, + }, + centerbottomrightcenter: { + at: 'right center', + my: 'center bottom', + x: 38.5, + y: 77, + }, + centerbottomrightminus80bottomminus40: { + at: 'right-80 bottom-40', + my: 'center bottom', + x: 118.5, + y: 117, + }, + centerbottomrighttop: { + at: 'right top', + my: 'center bottom', + x: 38.5, + y: -76.984375, + }, + centerminus40topplus40leftplus20ptop: { + at: 'left+20 top', + my: 'center-40 top+40', + x: -58.5, + y: 40, + }, + centerplus10perpbottomcenterminus10pertop: { + at: 'center+110 top', + my: 'center+150 bottom', + x: -221.5, + y: -76.984375, + }, + centerplus20ptopplus20pcenterbottom: { + at: 'center bottom', + my: 'center+100 top-200', + x: -61.5, + y: 200, + }, + centerplus40topminus15pcentercenterplus40: { + at: 'center center+40', + my: 'center+40 top+15', + x: -1.5, + y: -55, + }, + centerplus80bottomminus90leftbottom: { + at: 'left bottom', + my: 'center+80 bottom-90', + x: 41.5, + y: 167, + }, + centertopcenterbottom: { + at: 'center bottom', + my: 'center top', + x: 38.5, + y: 0, + }, + centertopcentercenter: { + at: 'center center', + my: 'center top', + x: 38.5, + y: 0, + }, + centertopcenterplus20ptopplus20p: { + at: 'center+70 top+60', + my: 'center top', + x: -31.5, + y: 60, + }, + centertopcentertop: { at: 'center top', my: 'center top', x: 38.5, y: 0 }, + centertopleftbottom: { + at: 'left bottom', + my: 'center top', + x: -38.5, + y: 0, + }, + centertopleftcenter: { + at: 'left center', + my: 'center top', + x: -38.5, + y: 0, + }, + centertoplefttop: { at: 'left top', my: 'center top', x: -38.5, y: 0 }, + centertoprightbottom: { + at: 'right bottom', + my: 'center top', + x: 38.5, + y: 0, + }, + centertoprightcenter: { + at: 'right center', + my: 'center top', + x: 38.5, + y: 0, + }, + centertoprighttop: { at: 'right top', my: 'center top', x: 38.5, y: 0 }, + leftbottomcenterbottom: { + at: 'center bottom', + my: 'left bottom', + x: 0, + y: 77, + }, + leftbottomcentercenter: { + at: 'center center', + my: 'left bottom', + x: 0, + y: 77, + }, + leftbottomcentertop: { + at: 'center top', + my: 'left bottom', + x: 0, + y: -76.984375, + }, + leftbottomleftbottom: { at: 'left bottom', my: 'left bottom', x: 0, y: 77 }, + leftbottomleftcenter: { at: 'left center', my: 'left bottom', x: 0, y: 77 }, + leftbottomlefttop: { + at: 'left top', + my: 'left bottom', + x: 0, + y: -76.984375, + }, + leftbottomrightbottom: { + at: 'right bottom', + my: 'left bottom', + x: 0, + y: 77, + }, + leftbottomrightcenter: { + at: 'right center', + my: 'left bottom', + x: 0, + y: 77, + }, + leftbottomrighttop: { + at: 'right top', + my: 'left bottom', + x: 0, + y: -76.984375, + }, + leftcentercenterbottom: { + at: 'center bottom', + my: 'left center', + x: 0, + y: 38.5, + }, + leftcentercentercenter: { + at: 'center center', + my: 'left center', + x: 0, + y: 38.5, + }, + leftcentercentertop: { + at: 'center top', + my: 'left center', + x: 0, + y: -38.484375, + }, + leftcenterleftbottom: { + at: 'left bottom', + my: 'left center', + x: 0, + y: 38.5, + }, + leftcenterleftcenter: { + at: 'left center', + my: 'left center', + x: 0, + y: 38.5, + }, + leftcenterlefttop: { + at: 'left top', + my: 'left center', + x: 0, + y: -38.484375, + }, + leftcenterrightbottom: { + at: 'right bottom', + my: 'left center', + x: 0, + y: 38.5, + }, + leftcenterrightcenter: { + at: 'right center', + my: 'left center', + x: 0, + y: 38.5, + }, + leftcenterrighttop: { + at: 'right top', + my: 'left center', + x: 0, + y: -38.484375, + }, + lefttopcenterbottom: { at: 'center bottom', my: 'left top', x: 0, y: 0 }, + lefttopcentercenter: { at: 'center center', my: 'left top', x: 0, y: 0 }, + lefttopcentertop: { at: 'center top', my: 'left top', x: 0, y: 0 }, + lefttopleftbottom: { at: 'left bottom', my: 'left top', x: 0, y: 0 }, + lefttopleftcenter: { at: 'left center', my: 'left top', x: 0, y: 0 }, + lefttoplefttop: { at: 'left top', my: 'left top', x: 0, y: 0 }, + lefttoprightbottom: { at: 'right bottom', my: 'left top', x: 0, y: 0 }, + lefttoprightcenter: { at: 'right center', my: 'left top', x: 0, y: 0 }, + lefttoprighttop: { at: 'right top', my: 'left top', x: 0, y: 0 }, + rightbottomcenterbottom: { + at: 'center bottom', + my: 'right bottom', + x: 77, + y: 77, + }, + rightbottomcentercenter: { + at: 'center center', + my: 'right bottom', + x: 77, + y: 77, + }, + rightbottomcentertop: { + at: 'center top', + my: 'right bottom', + x: 77, + y: -76.984375, + }, + rightbottomleftbottom: { + at: 'left bottom', + my: 'right bottom', + x: -77, + y: 77, + }, + rightbottomleftcenter: { + at: 'left center', + my: 'right bottom', + x: -77, + y: 77, + }, + rightbottomlefttop: { + at: 'left top', + my: 'right bottom', + x: -77, + y: -76.984375, + }, + rightbottomrightbottom: { + at: 'right bottom', + my: 'right bottom', + x: 77, + y: 77, + }, + rightbottomrightcenter: { + at: 'right center', + my: 'right bottom', + x: 77, + y: 77, + }, + rightbottomrighttop: { + at: 'right top', + my: 'right bottom', + x: 77, + y: -76.984375, + }, + rightcentercenterbottom: { + at: 'center bottom', + my: 'right center', + x: 77, + y: 38.5, + }, + rightcentercentercenter: { + at: 'center center', + my: 'right center', + x: 77, + y: 38.5, + }, + rightcentercentertop: { + at: 'center top', + my: 'right center', + x: 77, + y: -38.484375, + }, + rightcenterleftbottom: { + at: 'left bottom', + my: 'right center', + x: -77, + y: 38.5, + }, + rightcenterleftcenter: { + at: 'left center', + my: 'right center', + x: -77, + y: 38.5, + }, + rightcenterlefttop: { + at: 'left top', + my: 'right center', + x: -77, + y: -38.484375, + }, + rightcenterrightbottom: { + at: 'right bottom', + my: 'right center', + x: 77, + y: 38.5, + }, + rightcenterrightcenter: { + at: 'right center', + my: 'right center', + x: 77, + y: 38.5, + }, + rightcenterrighttop: { + at: 'right top', + my: 'right center', + x: 77, + y: -38.484375, + }, + righttopcenterbottom: { at: 'center bottom', my: 'right top', x: 77, y: 0 }, + righttopcentercenter: { at: 'center center', my: 'right top', x: 77, y: 0 }, + righttopcentertop: { at: 'center top', my: 'right top', x: 77, y: 0 }, + righttopleftbottom: { at: 'left bottom', my: 'right top', x: -77, y: 0 }, + righttopleftcenter: { at: 'left center', my: 'right top', x: -77, y: 0 }, + righttoplefttop: { at: 'left top', my: 'right top', x: -77, y: 0 }, + righttoprightbottom: { at: 'right bottom', my: 'right top', x: 77, y: 0 }, + righttoprightcenter: { at: 'right center', my: 'right top', x: 77, y: 0 }, + righttoprighttop: { at: 'right top', my: 'right top', x: 77, y: 0 }, + }, + selector: { + centerbottomcenterbottom: { + at: 'center bottom', + my: 'center bottom', + x: 62.5, + y: 125, + }, + centerbottomcentercenter: { + at: 'center center', + my: 'center bottom', + x: 62.5, + y: 24, + }, + centerbottomcentertop: { + at: 'center top', + my: 'center bottom', + x: 62.5, + y: -77, + }, + centerbottomleftbottom: { + at: 'left bottom', + my: 'center bottom', + x: -38.5, + y: 125, + }, + centerbottomleftcenter: { + at: 'left center', + my: 'center bottom', + x: -38.5, + y: 24, + }, + centerbottomlefttop: { + at: 'left top', + my: 'center bottom', + x: -38.5, + y: -77, + }, + centerbottomrightbottom: { + at: 'right bottom', + my: 'center bottom', + x: 163.5, + y: 125, + }, + centerbottomrightcenter: { + at: 'right center', + my: 'center bottom', + x: 163.5, + y: 24, + }, + centerbottomrightplus40bottomminus40: { + at: 'right+40 bottom-40', + my: 'center bottom', + x: 203.5, + y: 85, + }, + centerbottomrighttop: { + at: 'right top', + my: 'center bottom', + x: 163.5, + y: -77, + }, + centerminus40topplus40leftminus20ptop: { + at: 'left-20% top', + my: 'center-40 top+40', + x: -118.890625, + y: 40, + }, + centerplus10perpbottomcenterminus10pertop: { + at: 'center-20% top', + my: 'center+20% bottom', + x: 37.5, + y: -77, + }, + centerplus40bottomminus40leftbottom: { + at: 'left bottom', + my: 'center+40 bottom-40', + x: 1.5, + y: 85, + }, + centerplus40topminus15pcentercenterplus40: { + at: 'center center+40', + my: 'center+40 top-15%', + x: 102.5, + y: 129.4375, + }, + centertopcenterbottom: { + at: 'center bottom', + my: 'center top', + x: 62.5, + y: 202, + }, + centertopcentercenter: { + at: 'center center', + my: 'center top', + x: 62.5, + y: 101, + }, + centertopcenterplus20ptopplus20p: { + at: 'center+20% top+20%', + my: 'center top', + x: 102.890625, + y: 40.390625, + }, + centertopcentertop: { at: 'center top', my: 'center top', x: 62.5, y: 0 }, + centertopleftbottom: { + at: 'left bottom', + my: 'center top', + x: -38.5, + y: 202, + }, + centertopleftcenter: { + at: 'left center', + my: 'center top', + x: -38.5, + y: 101, + }, + centertoplefttop: { at: 'left top', my: 'center top', x: -38.5, y: 0 }, + centertoprightbottom: { + at: 'right bottom', + my: 'center top', + x: 163.5, + y: 202, + }, + centertoprightcenter: { + at: 'right center', + my: 'center top', + x: 163.5, + y: 101, + }, + centertoprighttop: { at: 'right top', my: 'center top', x: 163.5, y: 0 }, + leftbottomcenterbottom: { + at: 'center bottom', + my: 'left bottom', + x: 101, + y: 125, + }, + leftbottomcentercenter: { + at: 'center center', + my: 'left bottom', + x: 101, + y: 24, + }, + leftbottomcentertop: { + at: 'center top', + my: 'left bottom', + x: 101, + y: -77, + }, + leftbottomleftbottom: { + at: 'left bottom', + my: 'left bottom', + x: 0, + y: 125, + }, + leftbottomleftcenter: { at: 'left center', my: 'left bottom', x: 0, y: 24 }, + leftbottomlefttop: { at: 'left top', my: 'left bottom', x: 0, y: -77 }, + leftbottomrightbottom: { + at: 'right bottom', + my: 'left bottom', + x: 202, + y: 125, + }, + leftbottomrightcenter: { + at: 'right center', + my: 'left bottom', + x: 202, + y: 24, + }, + leftbottomrighttop: { at: 'right top', my: 'left bottom', x: 202, y: -77 }, + leftcentercenterbottom: { + at: 'center bottom', + my: 'left center', + x: 101, + y: 163.5, + }, + leftcentercentercenter: { + at: 'center center', + my: 'left center', + x: 101, + y: 62.5, + }, + leftcentercentertop: { + at: 'center top', + my: 'left center', + x: 101, + y: -38.5, + }, + leftcenterleftbottom: { + at: 'left bottom', + my: 'left center', + x: 0, + y: 163.5, + }, + leftcenterleftcenter: { + at: 'left center', + my: 'left center', + x: 0, + y: 62.5, + }, + leftcenterlefttop: { at: 'left top', my: 'left center', x: 0, y: -38.5 }, + leftcenterrightbottom: { + at: 'right bottom', + my: 'left center', + x: 202, + y: 163.5, + }, + leftcenterrightcenter: { + at: 'right center', + my: 'left center', + x: 202, + y: 62.5, + }, + leftcenterrighttop: { + at: 'right top', + my: 'left center', + x: 202, + y: -38.5, + }, + lefttopcenterbottom: { + at: 'center bottom', + my: 'left top', + x: 101, + y: 202, + }, + lefttopcentercenter: { + at: 'center center', + my: 'left top', + x: 101, + y: 101, + }, + lefttopcentertop: { at: 'center top', my: 'left top', x: 101, y: 0 }, + lefttopleftbottom: { at: 'left bottom', my: 'left top', x: 0, y: 202 }, + lefttopleftcenter: { at: 'left center', my: 'left top', x: 0, y: 101 }, + lefttoplefttop: { at: 'left top', my: 'left top', x: 0, y: 0 }, + lefttoprightbottom: { at: 'right bottom', my: 'left top', x: 202, y: 202 }, + lefttoprightcenter: { at: 'right center', my: 'left top', x: 202, y: 101 }, + lefttoprighttop: { at: 'right top', my: 'left top', x: 202, y: 0 }, + rightbottomcenterbottom: { + at: 'center bottom', + my: 'right bottom', + x: 24, + y: 125, + }, + rightbottomcentercenter: { + at: 'center center', + my: 'right bottom', + x: 24, + y: 24, + }, + rightbottomcentertop: { + at: 'center top', + my: 'right bottom', + x: 24, + y: -77, + }, + rightbottomleftbottom: { + at: 'left bottom', + my: 'right bottom', + x: -77, + y: 125, + }, + rightbottomleftcenter: { + at: 'left center', + my: 'right bottom', + x: -77, + y: 24, + }, + rightbottomlefttop: { at: 'left top', my: 'right bottom', x: -77, y: -77 }, + rightbottomrightbottom: { + at: 'right bottom', + my: 'right bottom', + x: 125, + y: 125, + }, + rightbottomrightcenter: { + at: 'right center', + my: 'right bottom', + x: 125, + y: 24, + }, + rightbottomrighttop: { + at: 'right top', + my: 'right bottom', + x: 125, + y: -77, + }, + rightcentercenterbottom: { + at: 'center bottom', + my: 'right center', + x: 24, + y: 163.5, + }, + rightcentercentercenter: { + at: 'center center', + my: 'right center', + x: 24, + y: 62.5, + }, + rightcentercentertop: { + at: 'center top', + my: 'right center', + x: 24, + y: -38.5, + }, + rightcenterleftbottom: { + at: 'left bottom', + my: 'right center', + x: -77, + y: 163.5, + }, + rightcenterleftcenter: { + at: 'left center', + my: 'right center', + x: -77, + y: 62.5, + }, + rightcenterlefttop: { + at: 'left top', + my: 'right center', + x: -77, + y: -38.5, + }, + rightcenterrightbottom: { + at: 'right bottom', + my: 'right center', + x: 125, + y: 163.5, + }, + rightcenterrightcenter: { + at: 'right center', + my: 'right center', + x: 125, + y: 62.5, + }, + rightcenterrighttop: { + at: 'right top', + my: 'right center', + x: 125, + y: -38.5, + }, + righttopcenterbottom: { + at: 'center bottom', + my: 'right top', + x: 24, + y: 202, + }, + righttopcentercenter: { + at: 'center center', + my: 'right top', + x: 24, + y: 101, + }, + righttopcentertop: { at: 'center top', my: 'right top', x: 24, y: 0 }, + righttopleftbottom: { at: 'left bottom', my: 'right top', x: -77, y: 202 }, + righttopleftcenter: { at: 'left center', my: 'right top', x: -77, y: 101 }, + righttoplefttop: { at: 'left top', my: 'right top', x: -77, y: 0 }, + righttoprightbottom: { + at: 'right bottom', + my: 'right top', + x: 125, + y: 202, + }, + righttoprightcenter: { + at: 'right center', + my: 'right top', + x: 125, + y: 101, + }, + righttoprighttop: { at: 'right top', my: 'right top', x: 125, y: 0 }, + }, +}; +/* cSpell:enable */ + +// Testing `of:` using jQuery or vanilla JS elements can use the same test +// scenarios and expected values as those using a selector. +testScenarios.jQuery = testScenarios.selector; +testScenarios.element = testScenarios.selector; + +module.exports = { + '@tags': ['core'], + before(browser) { + browser.drupalInstall().drupalLoginAsAdmin(() => { + browser + .drupalRelativeURL('/admin/modules') + .setValue('input[type="search"]', 'position Shim Test') + .waitForElementVisible( + 'input[name="modules[position_shim_test][enable]"]', + 1000, + ) + .click('input[name="modules[position_shim_test][enable]"]') + .click('input[type="submit"]'); + }); + }, + after(browser) { + browser.drupalUninstall(); + }, + beforeEach(browser) { + if (browser.currentTest.name !== 'test position') { + browser + .drupalRelativeURL('/position-shim-test-ported-from-jqueryui') + .waitForElementVisible('#el1', 1000); + } + }, + 'test position': (browser) => { + browser + .resizeWindow(1200, 600) + .drupalRelativeURL('/position-shim-test') + .waitForElementPresent('#position-reference-1', 1000) + .executeAsync( + // eslint-disable-next-line func-names, prefer-arrow-callback + function (testIterations, done) { + const $ = jQuery; + const toReturn = {}; + + /** + * Confirms a coordinate is acceptably close to the expected value. + * + * @param {number} actual + * The actual coordinate value. + * @param {number} expected + * The expected coordinate value. + * @return {boolean} + * True if the actual is within 3px of the expected. + */ + const withinRange = (actual, expected) => { + return actual <= expected + 3 && actual >= expected - 3; + }; + + /** + * Parses a jQuery UI position config string for `at:` or `my:`. + * + * A position config string can contain both alignment and offset + * configuration. This string is parsed and returned as an object that + * separates horizontal and vertical alignment and their respective + * offsets into distinct object properties. + * + * This is a copy of the parseOffset function from the jQuery position + * API. + * + * @param {string} offset + * Offset configuration in jQuery UI Position format. + * @param {Element} element + * The element being positioned. + * @return {{horizontal: (*|string), verticalOffset: number, vertical: (*|string), horizontalOffset: number}} + * The horizontal and vertical alignment and offset values for the element. + * + * @see core/misc/position.es6.js + */ + const parseOffset = (offset, element) => { + const regexHorizontal = /left|center|right/; + const regexVertical = /top|center|bottom/; + const regexOffset = /[+-]\d+(\.[\d]+)?%?/; + const regexPosition = /^\w+/; + const regexPercent = /%$/; + let positions = offset.split(' '); + if (positions.length === 1) { + if (regexHorizontal.test(positions[0])) { + positions.push('center'); + } else if (regexVertical.test(positions[0])) { + positions = ['center'].concat(positions); + } + } + + const horizontalOffset = regexOffset.exec(positions[0]); + const verticalOffset = regexOffset.exec(positions[1]); + positions = positions.map((pos) => regexPosition.exec(pos)[0]); + + return { + horizontalOffset: horizontalOffset + ? parseFloat(horizontalOffset[0]) * + (regexPercent.test(horizontalOffset[0]) + ? element.offsetWidth / 100 + : 1) + : 0, + verticalOffset: verticalOffset + ? parseFloat(verticalOffset[0]) * + (regexPercent.test(verticalOffset[0]) + ? element.offsetWidth / 100 + : 1) + : 0, + horizontal: positions[0], + vertical: positions[1], + }; + }; + + /** + * Checks the position of an element. + * + * The position values of an element are based on their distance + * relative to the element they're positioned against. + * + * @param {jQuery} tip + * The element being positioned. + * @param {Object} options + * The position options. + * @param {string} attachToType + * A string representing the data type used for the value of the `of` + * option. This could be 'selector', 'window', 'jQuery', 'element'. + * + * @param {string} idKey + * The unique id of the element indicating the use case scenario. + * + * @return {Promise} + * Resolve after the tip position is calculated. + */ + const checkPosition = (tip, options, attachToType, idKey) => + new Promise((resolve) => { + setTimeout(() => { + const box = tip[0].getBoundingClientRect(); + let { x, y } = box; + // If the tip is attaching to the window, X and Y are measured + // based on their distance from the closest window boundary. + if (attachToType === 'window') { + // Parse options.at to get the configured the horizontal and + // vertical positioning within the window. This will be used + // to get the tip distance relative to the configured position + // within the window. This provides a reliable way of + // getting position info that doesn't rely on an exact + // viewport width. + const atOffsets = parseOffset(options.at, tip[0]); + + if (atOffsets.horizontal === 'center') { + x = document.documentElement.clientWidth / 2 - x; + } else if (atOffsets.horizontal === 'right') { + x = document.documentElement.clientWidth - x; + } + if (atOffsets.vertical === 'center') { + y = document.documentElement.clientHeight / 2 - y; + } else if (atOffsets.vertical === 'bottom') { + y = document.documentElement.clientHeight - y; + } else { + y += window.pageYOffset; + } + } else { + // Measure the distance of the tip from the reference element. + const refRect = document + .querySelector('#position-reference-1') + .getBoundingClientRect(); + x -= refRect.x; + y -= refRect.y; + } + if (!withinRange(x, options.x) || !withinRange(y, options.y)) { + toReturn[ + idKey + ] = `${idKey} EXPECTED x:${options.x} y:${options.y} ACTUAL x:${x} y:${y}`; + } else { + toReturn[idKey] = true; + } + + resolve(); + }, 25); + }); + + const attachScenarios = { + selector: '#position-reference-1', + window, + jQuery: $('#position-reference-1'), + element: document.querySelector('#position-reference-1'), + }; + + // Loop through testScenarios and attachScenarios to get config for a + // positioned tip. + (async function iterate() { + const attachToTypes = Object.keys(attachScenarios); + for (let i = 0; i < attachToTypes.length; i++) { + const attachToType = attachToTypes[i]; + const scenarios = Object.keys(testIterations[attachToType]); + for (let j = 0; j < scenarios.length; j++) { + const key = scenarios[j]; + const options = testIterations[attachToType][key]; + options.of = attachScenarios[attachToType]; + options.collision = 'none'; + const idKey = `${attachToType}${key}`; + + // eslint-disable-next-line no-await-in-loop + const tip = await new Promise((resolve) => { + const addedTip = $( + `<div class="test-tip" style="position:${ + attachToType === 'window' ? 'fixed' : 'absolute' + }" id="${idKey}">${idKey}</div>`, + ).appendTo('main'); + addedTip.position(options); + setTimeout(() => { + resolve(addedTip); + }); + }); + // eslint-disable-next-line no-await-in-loop + await checkPosition(tip, options, attachToType, idKey); + tip.remove(); + } + } + done(toReturn); + })(); + }, + [testScenarios], + (result) => { + let numberOfScenarios = 0; + Object.keys(testScenarios).forEach((scenario) => { + numberOfScenarios += Object.keys(testScenarios[scenario]).length; + }); + const valueKeys = Object.keys(result.value); + browser.assert.equal(valueKeys.length, numberOfScenarios); + valueKeys.forEach((item) => { + browser.assert.equal( + result.value[item], + true, + `expected position: ${item}`, + ); + }); + }, + ); + }, + // The remaining tests are ported from jQuery UI's QUnit tests. + 'my, at, of': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'left top', + at: 'left top', + of: '#parentX', + collision: 'none', + }); + toReturn['left top, left top'] = { + actual: $elx.offset(), + expected: { top: 40, left: 40 }, + }; + $elx.position({ + my: 'left top', + at: 'left bottom', + of: '#parentX', + collision: 'none', + }); + toReturn['left top, left bottom'] = { + actual: $elx.offset(), + expected: { top: 60, left: 40 }, + }; + $elx.position({ + my: 'left', + at: 'bottom', + of: '#parentX', + collision: 'none', + }); + toReturn['left, bottom'] = { + actual: $elx.offset(), + expected: { top: 55, left: 50 }, + }; + $elx.position({ + my: 'left foo', + at: 'bar baz', + of: '#parentX', + collision: 'none', + }); + toReturn['left foo, bar baz'] = { + actual: $elx.offset(), + expected: { top: 45, left: 50 }, + }; + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 4); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'multiple elements': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const elements = $('#el1, #el2'); + const result = elements.position({ + my: 'left top', + at: 'left bottom', + of: '#parent', + collision: 'none', + }); + toReturn['elements return'] = { + actual: result, + expected: elements, + }; + // eslint-disable-next-line func-names + elements.each(function (index) { + toReturn[`element${index}`] = { + actual: $(this).offset(), + expected: { top: 10, left: 4 }, + }; + }); + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 3); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + positions: (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + + const offsets = { + left: 0, + center: 3, + right: 6, + top: 0, + bottom: 6, + }; + const start = { left: 4, top: 4 }; + const el = $('#el1'); + + $.each([0, 1], (my) => { + $.each(['top', 'center', 'bottom'], (vIndex, vertical) => { + // eslint-disable-next-line max-nested-callbacks + $.each(['left', 'center', 'right'], (hIndex, horizontal) => { + const _my = my ? `${horizontal} ${vertical}` : 'left top'; + const _at = !my ? `${horizontal} ${vertical}` : 'left top'; + el.position({ + my: _my, + at: _at, + of: '#parent', + collision: 'none', + }); + toReturn[`my: ${_my} at: ${_at}`] = { + actual: el.offset(), + expected: { + top: start.top + offsets[vertical] * (my ? -1 : 1), + left: start.left + offsets[horizontal] * (my ? -1 : 1), + }, + }; + }); + }); + }); + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 17); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + of: (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + const $parentX = $('#parentX'); + const win = $(window); + let event; + + // eslint-disable-next-line func-names + let scrollTopSupport = function () { + const support = win.scrollTop(1).scrollTop() === 1; + win.scrollTop(0); + // eslint-disable-next-line func-names + scrollTopSupport = function () { + return support; + }; + return support; + }; + + $elx.position({ + my: 'left top', + at: 'left top', + of: '#parentX', + collision: 'none', + }); + toReturn.selector = { + actual: $elx.offset(), + expected: { top: 40, left: 40 }, + }; + + $elx.position({ + my: 'left top', + at: 'left bottom', + of: $parentX, + collision: 'none', + }); + toReturn['jQuery object'] = { + actual: $elx.offset(), + expected: { top: 60, left: 40 }, + }; + + $elx.position({ + my: 'left top', + at: 'left top', + of: $parentX[0], + collision: 'none', + }); + toReturn['DOM element'] = { + actual: $elx.offset(), + expected: { top: 40, left: 40 }, + }; + + $elx.position({ + my: 'right bottom', + at: 'right bottom', + of: document, + collision: 'none', + }); + toReturn.document = { + actual: $elx.offset(), + expected: { + top: $(document).height() - 10, + left: $(document).width() - 10, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'right bottom', + of: $(document), + collision: 'none', + }); + toReturn['document as jQuery object'] = { + actual: $elx.offset(), + expected: { + top: $(document).height() - 10, + left: $(document).width() - 10, + }, + }; + + win.scrollTop(0); + + $elx.position({ + my: 'right bottom', + at: 'right bottom', + of: window, + collision: 'none', + }); + + toReturn.window = { + actual: $elx.offset(), + expected: { + top: win.height() - 10, + left: win.width() - 10, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'right bottom', + of: win, + collision: 'none', + }); + toReturn['window as jQuery object'] = { + actual: $elx.offset(), + expected: { + top: win.height() - 10, + left: win.width() - 10, + }, + }; + + if (scrollTopSupport()) { + win.scrollTop(500).scrollLeft(200); + $elx.position({ + my: 'right bottom', + at: 'right bottom', + of: window, + collision: 'none', + }); + + toReturn['window, scrolled'] = { + actual: $elx.offset(), + expected: { + top: win.height() + 500 - 10, + left: win.width() + 200 - 10, + }, + }; + + win.scrollTop(0).scrollLeft(0); + } + + event = $.extend($.Event('someEvent'), { pageX: 200, pageY: 300 }); + $elx.position({ + my: 'left top', + at: 'left top', + of: event, + collision: 'none', + }); + toReturn['event - left top, left top'] = { + actual: $elx.offset(), + expected: { + top: 300, + left: 200, + }, + }; + + event = $.extend($.Event('someEvent'), { pageX: 400, pageY: 600 }); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: event, + collision: 'none', + }); + toReturn['event - left top, right bottom'] = { + actual: $elx.offset(), + expected: { + top: 600, + left: 400, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 10); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + offsets: (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = { + deepEquals: {}, + trues: {}, + }; + const $elx = $('#elx'); + let offset; + + $elx.position({ + my: 'left top', + at: 'left+10 bottom+10', + of: '#parentX', + collision: 'none', + }); + toReturn.deepEquals['offsets in at'] = { + actual: $elx.offset(), + expected: { top: 70, left: 50 }, + }; + + $elx.position({ + my: 'left+10 top-10', + at: 'left bottom', + of: '#parentX', + collision: 'none', + }); + toReturn.deepEquals['offsets in my'] = { + actual: $elx.offset(), + expected: { top: 50, left: 50 }, + }; + + $elx.position({ + my: 'left top', + at: 'left+50% bottom-10%', + of: '#parentX', + collision: 'none', + }); + toReturn.deepEquals['percentage offsets in at'] = { + actual: $elx.offset(), + expected: { top: 58, left: 50 }, + }; + + $elx.position({ + my: 'left-30% top+50%', + at: 'left bottom', + of: '#parentX', + collision: 'none', + }); + toReturn.deepEquals['percentage offsets in my'] = { + actual: $elx.offset(), + expected: { top: 65, left: 37 }, + }; + + $elx.position({ + my: 'left-30.001% top+50.0%', + at: 'left bottom', + of: '#parentX', + collision: 'none', + }); + offset = $elx.offset(); + toReturn.trues['decimal percentage top offsets in my'] = + Math.round(offset.top) === 65; + toReturn.trues['decimal percentage left offsets in my'] = + Math.round(offset.left) === 37; + + $elx.position({ + my: 'left+10.4 top-10.6', + at: 'left bottom', + of: '#parentX', + collision: 'none', + }); + offset = $elx.offset(); + toReturn.trues['decimal top offsets in my'] = + Math.round(offset.top) === 49; + toReturn.trues['decimal left offsets in my'] = + Math.round(offset.left) === 50; + + $elx.position({ + my: 'left+right top-left', + at: 'left-top bottom-bottom', + of: '#parentX', + collision: 'none', + }); + toReturn.deepEquals['invalid offsets'] = { + actual: $elx.offset(), + expected: { top: 60, left: 40 }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value.trues).length, 4); + browser.assert.equal(Object.keys(result.value.deepEquals).length, 5); + Object.entries(result.value.deepEquals).forEach(([key, value]) => { + browser.assert.deepEqual(value.actual, value.expected, key); + }); + Object.entries(result.value.trues).forEach(([key, value]) => { + browser.assert.equal(value, true, key); + }); + }, + ); + }, + using: (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + let count = 0; + const elems = $('#el1, #el2'); + const of = $('#parentX'); + const expectedPosition = { top: 60, left: 60 }; + const expectedFeedback = { + target: { + element: of, + width: 20, + height: 20, + left: 40, + top: 40, + }, + element: { + width: 6, + height: 6, + left: 60, + top: 60, + }, + horizontal: 'left', + vertical: 'top', + important: 'vertical', + }; + const originalPosition = elems + .position({ + my: 'right bottom', + at: 'right bottom', + of: '#parentX', + collision: 'none', + }) + .offset(); + + elems.position({ + my: 'left top', + at: 'center+10 bottom', + of: '#parentX', + using(position, feedback) { + toReturn[`correct context for call #${count}`] = { + actual: this, + expected: elems[count], + }; + toReturn[`correct position for call #${count}`] = { + actual: position, + expected: expectedPosition, + }; + toReturn[`feedback and element match for call #${count}`] = { + actual: feedback.element.element[0], + expected: elems[count], + }; + // assert.deepEqual(feedback.element.element[0], elems[count]); + delete feedback.element.element; + toReturn[`expected feedback after delete for call #${count}`] = { + actual: feedback, + expected: expectedFeedback, + }; + count += 1; + }, + }); + + // eslint-disable-next-line func-names + elems.each(function (index) { + toReturn[`elements not moved: ${index}`] = { + actual: $(this).offset(), + expected: originalPosition, + }; + }); + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 10); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: fit, no collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'fit', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right+2 bottom+3', + of: '#parent', + collision: 'fit', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 13, + left: 12, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: fit, collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + const win = $(window); + // eslint-disable-next-line func-names + let scrollTopSupport = function () { + const support = win.scrollTop(1).scrollTop() === 1; + win.scrollTop(0); + // eslint-disable-next-line func-names + scrollTopSupport = function () { + return support; + }; + return support; + }; + + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'fit', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 0, + left: 0, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left+2 top+3', + of: '#parent', + collision: 'fit', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 0, + left: 0, + }, + }; + + if (scrollTopSupport()) { + win.scrollTop(300).scrollLeft(200); + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'fit', + }); + toReturn['window scrolled'] = { + actual: $elx.offset(), + expected: { + top: 300, + left: 200, + }, + }; + + win.scrollTop(0).scrollLeft(0); + } + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 3); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: flip, no collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'flip', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right+2 bottom+3', + of: '#parent', + collision: 'flip', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 13, + left: 12, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: flip, collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'flip', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left+2 top+3', + of: '#parent', + collision: 'flip', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 7, + left: 8, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: flipfit, no collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'flipfit', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right+2 bottom+3', + of: '#parent', + collision: 'flipfit', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 13, + left: 12, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: flipfit, collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'flipfit', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left+2 top+3', + of: '#parent', + collision: 'flipfit', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 7, + left: 8, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: none, no collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'none', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right+2 bottom+3', + of: '#parent', + collision: 'none', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: 13, + left: 12, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: none, collision': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'none', + }); + + toReturn['no offset'] = { + actual: $elx.offset(), + expected: { + top: -6, + left: -6, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left+2 top+3', + of: '#parent', + collision: 'none', + }); + + toReturn['with offset'] = { + actual: $elx.offset(), + expected: { + top: -3, + left: -4, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: fit, with margin': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx').css({ + marginTop: 6, + marginLeft: 4, + }); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'fit', + }); + + toReturn['right bottom'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'fit', + }); + + toReturn['left top'] = { + actual: $elx.offset(), + expected: { + top: 6, + left: 4, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 2); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'collision: flip, with margin': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx').css({ + marginTop: 6, + marginLeft: 4, + }); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'flip', + }); + + toReturn['left top'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + collision: 'flip', + }); + + toReturn['right bottom'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'left top', + at: 'left top', + of: '#parent', + collision: 'flip', + }); + + toReturn['left top left top'] = { + actual: $elx.offset(), + expected: { + top: 0, + left: 4, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 3); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + within: (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + within: document, + }); + + toReturn['within document'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: 10, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + collision: 'fit', + + within: '#within', + }); + + toReturn['fit - right bottom'] = { + actual: $elx.offset(), + expected: { + top: 4, + left: 2, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + within: '#within', + collision: 'fit', + }); + + toReturn['fit - left top'] = { + actual: $elx.offset(), + expected: { + top: 2, + left: 0, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + within: '#within', + collision: 'flip', + }); + + toReturn['flip - right bottom'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: -6, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + within: '#within', + collision: 'flip', + }); + + toReturn['flip - left top'] = { + actual: $elx.offset(), + expected: { + top: 10, + left: -6, + }, + }; + + $elx.position({ + my: 'left top', + at: 'right bottom', + of: '#parent', + within: '#within', + collision: 'flipfit', + }); + + toReturn['flipfit - right bottom'] = { + actual: $elx.offset(), + expected: { + top: 4, + left: 0, + }, + }; + + $elx.position({ + my: 'right bottom', + at: 'left top', + of: '#parent', + within: '#within', + collision: 'flipfit', + }); + + toReturn['flipfit - left top'] = { + actual: $elx.offset(), + expected: { + top: 4, + left: 0, + }, + }; + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 7); + Object.entries(result.value).forEach(([key, value]) => { + browser.assert.equal(typeof value, 'object'); + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'with scrollbars': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + + const $scrollX = $('#scrollX'); + $scrollX.css({ + width: 100, + height: 100, + left: 0, + top: 0, + }); + + const $elx = $('#elx').position({ + my: 'left top', + at: 'right bottom', + of: '#scrollX', + within: '#scrollX', + collision: 'fit', + }); + + toReturn.visible = { + actual: $elx.offset(), + expected: { + top: 90, + left: 90, + }, + }; + + const scrollbarInfo = $.position.getScrollInfo( + $.position.getWithinInfo($('#scrollX')), + ); + + $elx.position({ + of: '#scrollX', + collision: 'fit', + within: '#scrollX', + my: 'left top', + at: 'right bottom', + }); + + toReturn.scroll = { + actual: $elx.offset(), + expected: { + top: 90 - scrollbarInfo.height, + left: 90 - scrollbarInfo.width, + }, + }; + + $scrollX.css({ + overflow: 'auto', + }); + + toReturn['auto, no scroll"'] = { + actual: $elx.offset(), + expected: { + top: 90, + left: 90, + }, + }; + + $scrollX + .css({ + overflow: 'auto', + }) + .append($('<div>').height(300).width(300)); + + $elx.position({ + of: '#scrollX', + collision: 'fit', + within: '#scrollX', + my: 'left top', + at: 'right bottom', + }); + + toReturn['auto, with scroll'] = { + actual: $elx.offset(), + expected: { + top: 90 - scrollbarInfo.height, + left: 90 - scrollbarInfo.width, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 4); + Object.entries(result.value).forEach((key, value) => { + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + fractions: (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $fractionElement = $('#fractions-element').position({ + my: 'left top', + at: 'left top', + of: '#fractions-parent', + collision: 'none', + }); + toReturn['left top, left top'] = { + actual: $fractionElement.offset(), + expected: $('#fractions-parent').offset(), + }; + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 1); + Object.entries(result.value).forEach((key, value) => { + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'bug #5280: consistent results (avoid fractional values)': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const wrapper = $('#bug-5280'); + const elem = wrapper.children(); + const offset1 = elem + .position({ + my: 'center', + at: 'center', + of: wrapper, + collision: 'none', + }) + .offset(); + const offset2 = elem + .position({ + my: 'center', + at: 'center', + of: wrapper, + collision: 'none', + }) + .offset(); + toReturn['offsets consistent'] = { + actual: offset1, + expected: offset2, + }; + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 1); + Object.entries(result.value).forEach((key, value) => { + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, + 'bug #8710: flip if flipped position fits more': (browser) => { + browser.execute( + // eslint-disable-next-line func-names + function () { + const $ = jQuery; + const toReturn = {}; + const $elx = $('#elx'); + $elx.position({ + my: 'left top', + within: '#bug-8710-within-smaller', + of: '#parentX', + collision: 'flip', + at: 'right bottom+30', + }); + + toReturn['flip - top fits all'] = { + actual: $elx.offset(), + expected: { + top: 0, + left: 60, + }, + }; + + $elx.position({ + my: 'left top', + within: '#bug-8710-within-smaller', + of: '#parentX', + collision: 'flip', + at: 'right bottom+32', + }); + toReturn['flip - top fits more'] = { + actual: $elx.offset(), + expected: { + top: -2, + left: 60, + }, + }; + + $elx.position({ + my: 'left top', + within: '#bug-8710-within-bigger', + of: '#parentX', + collision: 'flip', + at: 'right bottom+32', + }); + toReturn['no flip - top fits less'] = { + actual: $elx.offset(), + expected: { + top: 92, + left: 60, + }, + }; + + return toReturn; + }, + [], + (result) => { + browser.assert.equal(Object.keys(result.value).length, 3); + Object.entries(result.value).forEach((key, value) => { + browser.assert.deepEqual(value.actual, value.expected, key); + }); + }, + ); + }, +};