PageSourceSearch

https://owsd.net/core/misc/position.js?v=10.6.9

js owsd.net collected 2026-09-24 19:07:46 UTC 22,513 bytes, 632 lines download raw bytes

1/**
2 * @file
3 * A modified version of jQuery UI position.
4 *
5 * Per jQuery UI's public domain license, it is permissible to run modified
6 * versions of their code. This file offers the same functionality as what is
7 * provided by jQuery UI position, but refactored to meet Drupal coding
8 * standards, and restructured so it extends jQuery core instead of jQuery UI.
9 *
10 * This is provided to support pre-existing code that expects the jQuery
11 * position API.
12 *
13 * @see https://github.com/jquery/jquery-ui/blob/1.12.1/LICENSE.txt
14 * @see https://raw.githubusercontent.com/jquery/jquery-ui/1.12.1/ui/position.js
15 */
16
17/**
18 * This provides ported version of jQuery UI position, refactored to not depend
19 * on jQuery UI and to meet Drupal JavaScript coding standards. Functionality
20 * and usage is identical. It positions an element relative to another. The
21 * `position()` function can be called by any jQuery object. Additional details
22 * on using `position()` are provided in this file in the docblock for
23 * $.fn.position.
24 */
25(($) => {
26  let cachedScrollbarWidth = null;
27  const { max, abs } = Math;
28  const regexHorizontal = /left|center|right/;
29  const regexVertical = /top|center|bottom/;
30  const regexOffset = /[+-]\d+(\.[\d]+)?%?/;
31  const regexPosition = /^\w+/;
32  const _position = $.fn.position;
33
34  function getOffsets(offsets, width, height) {
35    return [
36      parseFloat(offsets[0]) *
37        (typeof offsets[0] === 'string' && offsets[0].endsWith('%')
38          ? width / 100
39          : 1),
40      parseFloat(offsets[1]) *
41        (typeof offsets[1] === 'string' && offsets[1].endsWith('%')
42          ? height / 100
43          : 1),
44    ];
45  }
46
47  function parseCss(element, property) {
48    return parseInt(window.getComputedStyle(element)[property], 10) || 0;
49  }
50
51  function getDimensions(elem) {
52    const raw = elem[0];
53    if (raw.nodeType === 9) {
54      return {
55        width: elem.width(),
56        height: elem.height(),
57        offset: { top: 0, left: 0 },
58      };
59    }
60    if (!!raw && raw === raw.window) {
61      return {
62        width: elem.width(),
63        height: elem.height(),
64        offset: { top: elem.scrollTop(), left: elem.scrollLeft() },
65      };
66    }
67    if (raw.preventDefault) {
68      return {
69        width: 0,
70        height: 0,
71        offset: { top: raw.pageY, left: raw.pageX },
72      };
73    }
74    return {
75      width: elem.outerWidth(),
76      height: elem.outerHeight(),
77      offset: elem.offset(),
78    };
79  }
80
81  const collisions = {
82    fit: {
83      left(position, data) {
84        const { within } = data;
85        const withinOffset = within.isWindow
86          ? within.scrollLeft
87          : within.offset.left;
88        const outerWidth = within.width;
89        const collisionPosLeft =
90          position.left - data.collisionPosition.marginLeft;
91        const overLeft = withinOffset - collisionPosLeft;
92        const overRight =
93          collisionPosLeft + data.collisionWidth - outerWidth - withinOffset;
94        let newOverRight;
95
96        // Element is wider than within
97        if (data.collisionWidth > outerWidth) {
98          // Element is initially over the left side of within
99          if (overLeft > 0 && overRight <= 0) {
100            newOverRight =
101              position.left +
102              overLeft +
103              data.collisionWidth -
104              outerWidth -
105              withinOffset;
106            position.left += overLeft - newOverRight;
107
108            // Element is initially over right side of within
109          } else if (overRight > 0 && overLeft <= 0) {
110            position.left = withinOffset;
111
112            // Element is initially over both left and right sides of within
113          } else if (overLeft > overRight) {
114            position.left = withinOffset + outerWidth - data.collisionWidth;
115          } else {
116            position.left = withinOffset;
117          }
118
119          // Too far left -> align with left edge
120        } else if (overLeft > 0) {
121          position.left += overLeft;
122
123          // Too far right -> align with right edge
124        } else if (overRight > 0) {
125          position.left -= overRight;
126
127          // Adjust based on position and margin
128        } else {
129          position.left = max(position.left - collisionPosLeft, position.left);
130        }
131      },
132      top(position, data) {
133        const { within } = data;
134        const withinOffset = within.isWindow
135          ? within.scrollTop
136          : within.offset.top;
137        const outerHeight = data.within.height;
138        const collisionPosTop = position.top - data.collisionPosition.marginTop;
139        const overTop = withinOffset - collisionPosTop;
140        const overBottom =
141          collisionPosTop + data.collisionHeight - outerHeight - withinOffset;
142        let newOverBottom;
143
144        // Element is taller than within
145        if (data.collisionHeight > outerHeight) {
146          // Element is initially over the top of within
147          if (overTop > 0 && overBottom <= 0) {
148            newOverBottom =
149              position.top +
150              overTop +
151              data.collisionHeight -
152              outerHeight -
153              withinOffset;
154            position.top += overTop - newOverBottom;
155
156            // Element is initially over bottom of within
157          } else if (overBottom > 0 && overTop <= 0) {
158            position.top = withinOffset;
159
160            // Element is initially over both top and bottom of within
161          } else if (overTop > overBottom) {
162            position.top = withinOffset + outerHeight - data.collisionHeight;
163          } else {
164            position.top = withinOffset;
165          }
166
167          // Too far up -> align with top
168        } else if (overTop > 0) {
169          position.top += overTop;
170
171          // Too far down -> align with bottom edge
172        } else if (overBottom > 0) {
173          position.top -= overBottom;
174
175          // Adjust based on position and margin
176        } else {
177          position.top = max(position.top - collisionPosTop, position.top);
178        }
179      },
180    },
181    flip: {
182      left(position, data) {
183        const { within } = data;
184        const withinOffset = within.offset.left + within.scrollLeft;
185        const outerWidth = within.width;
186        const offsetLeft = within.isWindow
187          ? within.scrollLeft
188          : within.offset.left;
189        const collisionPosLeft =
190          position.left - data.collisionPosition.marginLeft;
191        const overLeft = collisionPosLeft - offsetLeft;
192        const overRight =
193          collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft;
194        const myOffset =
195          // eslint-disable-next-line no-nested-ternary
196          data.my[0] === 'left'
197            ? -data.elemWidth
198            : data.my[0] === 'right'
199              ? data.elemWidth
200              : 0;
201        const atOffset =
202          // eslint-disable-next-line no-nested-ternary
203          data.at[0] === 'left'
204            ? data.targetWidth
205            : data.at[0] === 'right'
206              ? -data.targetWidth
207              : 0;
208        const offset = -2 * data.offset[0];
209        let newOverRight;
210        let newOverLeft;
211
212        if (overLeft < 0) {
213          newOverRight =
214            position.left +
215            myOffset +
216            atOffset +
217            offset +
218            data.collisionWidth -
219            outerWidth -
220            withinOffset;
221          if (newOverRight < 0 || newOverRight < abs(overLeft)) {
222            position.left += myOffset + atOffset + offset;
223          }
224        } else if (overRight > 0) {
225          newOverLeft =
226            position.left -
227            data.collisionPosition.marginLeft +
228            myOffset +
229            atOffset +
230            offset -
231            offsetLeft;
232          if (newOverLeft > 0 || abs(newOverLeft) < overRight) {
233            position.left += myOffset + atOffset + offset;
234          }
235        }
236      },
237      top(position, data) {
238        const { within } = data;
239        const withinOffset = within.offset.top + within.scrollTop;
240        const outerHeight = within.height;
241        const offsetTop = within.isWindow
242          ? within.scrollTop
243          : within.offset.top;
244        const collisionPosTop = position.top - data.collisionPosition.marginTop;
245        const overTop = collisionPosTop - offsetTop;
246        const overBottom =
247          collisionPosTop + data.collisionHeight - outerHeight - offsetTop;
248        const top = data.my[1] === 'top';
249        // eslint-disable-next-line no-nested-ternary
250        const myOffset = top
251          ? -data.elemHeight
252          : data.my[1] === 'bottom'
253            ? data.elemHeight
254            : 0;
255        const atOffset =
256          // eslint-disable-next-line no-nested-ternary
257          data.at[1] === 'top'
258            ? data.targetHeight
259            : data.at[1] === 'bottom'
260              ? -data.targetHeight
261              : 0;
262        const offset = -2 * data.offset[1];
263        let newOverTop;
264        let newOverBottom;
265        if (overTop < 0) {
266          newOverBottom =
267            position.top +
268            myOffset +
269            atOffset +
270            offset +
271            data.collisionHeight -
272            outerHeight -
273            withinOffset;
274          if (newOverBottom < 0 || newOverBottom < abs(overTop)) {
275            position.top += myOffset + atOffset + offset;
276          }
277        } else if (overBottom > 0) {
278          newOverTop =
279            position.top -
280            data.collisionPosition.marginTop +
281            myOffset +
282            atOffset +
283            offset -
284            offsetTop;
285          if (newOverTop > 0 || abs(newOverTop) < overBottom) {
286            position.top += myOffset + atOffset + offset;
287          }
288        }
289      },
290    },
291    flipfit: {
292      left(...args) {
293        collisions.flip.left.apply(this, args);
294        collisions.fit.left.apply(this, args);
295      },
296      top(...args) {
297        collisions.flip.top.apply(this, args);
298        collisions.fit.top.apply(this, args);
299      },
300    },
301  };
302
303  $.position = {
304    scrollbarWidth() {
305      if (cachedScrollbarWidth !== undefined) {
306        return cachedScrollbarWidth;
307      }
308      const div = $(
309        '<div ' +
310          "style='display:block;position:absolute;width:50px;height:50px;overflow:hidden;'>" +
311          "<div style='height:100px;width:auto;'></div></div>",
312      );
313      const innerDiv = div.children()[0];
314
315      $('body').append(div);
316      const w1 = innerDiv.offsetWidth;
317      div[0].style.overflow = 'scroll';
318
319      let w2 = innerDiv.offsetWidth;
320
321      if (w1 === w2) {
322        w2 = div[0].clientWidth;
323      }
324
325      div.remove();
326      cachedScrollbarWidth = w1 - w2;
327      return cachedScrollbarWidth;
328    },
329    getScrollInfo(within) {
330      const overflowX =
331        within.isWindow || within.isDocument
332          ? ''
333          : window.getComputedStyle(within.element[0])['overflow-x'];
334      const overflowY =
335        within.isWindow || within.isDocument
336          ? ''
337          : window.getComputedStyle(within.element[0])['overflow-y'];
338      const hasOverflowX =
339        overflowX === 'scroll' ||
340        (overflowX === 'auto' && within.width < within.element[0].scrollWidth);
341      const hasOverflowY =
342        overflowY === 'scroll' ||
343        (overflowY === 'auto' &&
344          within.height < within.element[0].scrollHeight);
345      return {
346        width: hasOverflowY ? $.position.scrollbarWidth() : 0,
347        height: hasOverflowX ? $.position.scrollbarWidth() : 0,
348      };
349    },
350    getWithinInfo(element) {
351      const withinElement = $(element || window);
352      const isWindow =
353        !!withinElement[0] && withinElement[0] === withinElement[0].window;
354      const isDocument = !!withinElement[0] && withinElement[0].nodeType === 9;
355      const hasOffset = !isWindow && !isDocument;
356      return {
357        element: withinElement,
358        isWindow,
359        isDocument,
360        offset: hasOffset ? $(element).offset() : { left: 0, top: 0 },
361        scrollLeft: withinElement.scrollLeft(),
362        scrollTop: withinElement.scrollTop(),
363        width: withinElement.outerWidth(),
364        height: withinElement.outerHeight(),
365      };
366    },
367  };
368
369  // eslint-disable-next-line func-names
370  /**
371   * Positions an element relative to another.
372   *
373   * The following documentation is originally from
374   * {@link https://api.jqueryui.com/position/}.
375   *
376   * @param {Object} options - the options object.
377   * @param {string} options.my - Defines which position on the element being
378   *   positioned to align with the target element: "horizontal vertical"
379   *   alignment. A single value such as "right" will be normalized to "right
380   *   center", "top" will be normalized to "center top" (following CSS
381   *   convention). Acceptable horizontal values: "left", "center", "right".
382   *   Acceptable vertical values: "top", "center", "bottom". Example: "left
383   *   top" or "center center". Each dimension can also contain offsets, in
384   *   pixels or percent, e.g., "right+10 top-25%". Percentage offsets are
385   *   relative to the element being positioned. Default value is "center".
386   * @param {string} options.at - Defines which position on the target element
387   *   to align the positioned element against: "horizontal vertical" alignment.
388   *   See the `my` option for full details on possible values. Percentage
389   *   offsets are relative to the target element. Default value is "center".
390   * @param {string|Element|jQuery|Event|null} options.of - Which element to
391   *   position against. If you provide a selector or jQuery object, the first
392   *   matching element will be used. If you provide an event object, the pageX
393   *   and pageY properties will be used. Example: "#top-menu". Default value is
394   *   null.
395   * @param {string} options.collision - When the positioned element overflows
396   *   the window in some direction, move it to an alternative position. Similar
397   *   to `my` and `at`, this accepts a single value or a pair for
398   *   horizontal/vertical, e.g., "flip", "fit", "fit flip", "fit none". Default
399   *   value is "flip". The options work as follows:
400   *   - "flip": Flips the element to the opposite side of the target and the
401   *     collision detection is run again to see if it will fit. Whichever side
402   *     allows more of the element to be visible will be used.
403   *   - "fit": Shift the element away from the edge of the window.
404   *   - "flipfit": First applies the flip logic, placing the element on
405   *     whichever side allows more of the element to be visible. Then the fit
406   *     logic is applied to ensure as much of the element is visible as
407   *     possible.
408   *     "none": Does not apply any collision detection.
409   * @param {function|null} options.using - When specified, the actual property
410   *   setting is delegated to this callback. Receives two parameters: The first
411   *   is a hash of top and left values for the position that should be set and
412   *   can be forwarded to .css() or .animate().The second provides feedback
413   *   about the position and dimensions of both elements, as well as
414   *   calculations to their relative position. Both target and element have
415   *   these properties: element, left, top, width, height. In addition, there's
416   *   horizontal, vertical and important, providing twelve potential directions
417   *   like { horizontal: "center", vertical: "left", important: "horizontal" }.
418   *   Default value is null.
419   * @param {string|Element|jQuery} options.within - Element to position within,
420   *   affecting collision detection. If you provide a selector or jQuery
421   *   object, the first matching element will be used. Default value is window.
422   *
423   * @return {jQuery}
424   *  The jQuery object that called called this function.
425   */
426  $.fn.position = function (options) {
427    if (!options || !options.of) {
428      // eslint-disable-next-line prefer-rest-params
429      return _position.apply(this, arguments);
430    }
431
432    // Make a copy, we don't want to modify arguments
433    options = $.extend({}, options);
434
435    const within = $.position.getWithinInfo(options.within);
436    const scrollInfo = $.position.getScrollInfo(within);
437    const collision = (options.collision || 'flip').split(' ');
438    const offsets = {};
439
440    // Make sure string options are treated as CSS selectors
441    const target =
442      typeof options.of === 'string'
443        ? $(document).find(options.of)
444        : $(options.of);
445    const
445 dimensions = getDimensions(target);
446    const targetWidth = dimensions.width;
447    const targetHeight = dimensions.height;
448    const targetOffset = dimensions.offset;
449
450    if (target[0].preventDefault) {
451      // Force left top to allow flipping
452      options.at = 'left top';
453    }
454
455    // Clone to reuse original targetOffset later
456    const basePosition = $.extend({}, targetOffset);
457
458    // Force my and at to have valid horizontal and vertical positions
459    // if a value is missing or invalid, it will be converted to center
460    // eslint-disable-next-line func-names
461    $.each(['my', 'at'], function () {
462      let pos = (options[this] || '').split(' ');
463
464      if (pos.length === 1) {
465        // eslint-disable-next-line no-nested-ternary
466        pos = regexHorizontal.test(pos[0])
467          ? pos.concat(['center'])
468          : regexVertical.test(pos[0])
469            ? ['center'].concat(pos)
470            : ['center', 'center'];
471      }
472      pos[0] = regexHorizontal.test(pos[0]) ? pos[0] : 'center';
473      pos[1] = regexVertical.test(pos[1]) ? pos[1] : 'center';
474
475      // Calculate offsets
476      const horizontalOffset = regexOffset.exec(pos[0]);
477      const verticalOffset = regexOffset.exec(pos[1]);
478      offsets[this] = [
479        horizontalOffset ? horizontalOffset[0] : 0,
480        verticalOffset ? verticalOffset[0] : 0,
481      ];
482
483      // Reduce to just the positions without the offsets
484      options[this] = [
485        regexPosition.exec(pos[0])[0],
486        regexPosition.exec(pos[1])[0],
487      ];
488    });
489
490    // Normalize collision option
491    if (collision.length === 1) {
492      // eslint-disable-next-line prefer-destructuring
493      collision[1] = collision[0];
494    }
495
496    if (options.at[0] === 'right') {
497      basePosition.left += targetWidth;
498    } else if (options.at[0] === 'center') {
499      basePosition.left += targetWidth / 2;
500    }
501
502    if (options.at[1] === 'bottom') {
503      basePosition.top += targetHeight;
504    } else if (options.at[1] === 'center') {
505      basePosition.top += targetHeight / 2;
506    }
507
508    const atOffset = getOffsets(offsets.at, targetWidth, targetHeight);
509    basePosition.left += atOffset[0];
510    basePosition.top += atOffset[1];
511
512    // eslint-disable-next-line func-names
513    return this.each(function () {
514      let using;
515      const elem = $(this);
516      const elemWidth = elem.outerWidth();
517      const elemHeight = elem.outerHeight();
518      const marginLeft = parseCss(this, 'marginLeft');
519      const marginTop = parseCss(this, 'marginTop');
520      const collisionWidth =
521        elemWidth +
522        marginLeft +
523        parseCss(this, 'marginRight') +
524        scrollInfo.width;
525      const collisionHeight =
526        elemHeight +
527        marginTop +
528        parseCss(this, 'marginBottom') +
529        scrollInfo.height;
530      const position = $.extend({}, basePosition);
531      const myOffset = getOffsets(
532        offsets.my,
533        elem.outerWidth(),
534        elem.outerHeight(),
535      );
536
537      if (options.my[0] === 'right') {
538        position.left -= elemWidth;
539      } else if (options.my[0] === 'center') {
540        position.left -= elemWidth / 2;
541      }
542
543      if (options.my[1] === 'bottom') {
544        position.top -= elemHeight;
545      } else if (options.my[1] === 'center') {
546        position.top -= elemHeight / 2;
547      }
548
549      position.left += myOffset[0];
550      position.top += myOffset[1];
551
552      const collisionPosition = {
553        marginLeft,
554        marginTop,
555      };
556
557      // eslint-disable-next-line func-names
558      $.each(['left', 'top'], function (i, dir) {
559        if (collisions[collision[i]]) {
560          collisions[collision[i]][dir](position, {
561            targetWidth,
562            targetHeight,
563            elemWidth,
564            elemHeight,
565            collisionPosition,
566            collisionWidth,
567            collisionHeight,
568            offset: [atOffset[0] + myOffset[0], atOffset[1] + myOffset[1]],
569            my: options.my,
570            at: options.at,
571            within,
572            elem,
573          });
574        }
575      });
576
577      if (options.using) {
578        // Adds feedback as second argument to using callback, if present
579        // eslint-disable-next-line func-names
580        using = function (props) {
581          const left = targetOffset.left - position.left;
582          const right = left + targetWidth - elemWidth;
583          const top = targetOffset.top - position.top;
584          const bottom = top + targetHeight - elemHeight;
585          const feedback = {
586            target: {
587              element: target,
588              left: targetOffset.left,
589              top: targetOffset.top,
590              width: targetWidth,
591              height: targetHeight,
592            },
593            element: {
594              element: elem,
595              left: position.left,
596              top: position.top,
597              width: elemWidth,
598              height: elemHeight,
599            },
600            // eslint-disable-next-line no-nested-ternary
601            horizontal: right < 0 ? 'left' : left > 0 ? 'right' : 'center',
602            // eslint-disable-next-line no-nested-ternary
603            vertical: bottom < 0 ? 'top' : top > 0 ? 'bottom' : 'middle',
604          };
605          if (targetWidth < elemWidth && abs(left + right) < targetWidth) {
606            feedback.horizontal = 'center';
607          }
608          if (targetHeight < elemHeight && abs(top + bottom) < targetHeight) {
609            feedback.vertical = 'middle';
610          }
611          if (max(abs(left), abs(right)) > max(abs(top), abs(bottom))) {
612            feedback.important = 'horizontal';
613          } else {
614            feedback.important = 'vertical';
615          }
616          options.using.call(this, props, feedback);
617        };
618      }
619
620      elem.offset($.extend(position, { using }));
621    });
622  };
623
624  // Although $.ui.position is not built to be called directly, some legacy code
625  // may have checks for the presence of $.ui.position, which can be used to
626  // confirm the presence of jQuery UI position's API, as opposed to the more
627  // limited version provided by jQuery.
628  if (!$.hasOwnProperty('ui')) {
629    $.ui = {};
630  }
631  $.ui.position = collisions;
632})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.