PageSourceSearch

https://www.fanshawec.ca/core/misc/position.js?v=11.4.7

js fanshawec.ca collected 2026-09-24 09:18:12 UTC 22,555 bytes, 633 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 === Node.DOCUMENT_NODE) {
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 =
355        !!withinElement[0] && withinElement[0].nodeType === Node.DOCUMENT_NODE;
356      const hasOffset = !isWindow && !isDocument;
357      return {
358        element: withinElement,
359        isWindow,
360        isDocument,
361        offset: hasOffset ? $(element).offset() : { left: 0, top: 0 },
362        scrollLeft: withinElement.scrollLeft(),
363        scrollTop: withinElement.scrollTop(),
364        width: withinElement.outerWidth(),
365        height: withinElement.outerHeight(),
366      };
367    },
368  };
369
370  // eslint-disable-next-line func-names
371  /**
372   * Positions an element relative to another.
373   *
374   * The following documentation is originally from
375   * {@link https://api.jqueryui.com/position/}.
376   *
377   * @param {Object} options - the options object.
378   * @param {string} options.my - Defines which position on the element being
379   *   positioned to align with the target element: "horizontal vertical"
380   *   alignment. A single value such as "right" will be normalized to "right
381   *   center", "top" will be normalized to "center top" (following CSS
382   *   convention). Acceptable horizontal values: "left", "center", "right".
383   *   Acceptable vertical values: "top", "center", "bottom". Example: "left
384   *   top" or "center center". Each dimension can also contain offsets, in
385   *   pixels or percent, e.g., "right+10 top-25%". Percentage offsets are
386   *   relative to the element being positioned. Default value is "center".
387   * @param {string} options.at - Defines which position on the target element
388   *   to align the positioned element against: "horizontal vertical" alignment.
389   *   See the `my` option for full details on possible values. Percentage
390   *   offsets are relative to the target element. Default value is "center".
391   * @param {string|Element|jQuery|Event|null} options.of - Which element to
392   *   position against. If you provide a selector or jQuery object, the first
393   *   matching element will be used. If you provide an event object, the pageX
394   *   and pageY properties will be used. Example: "#top-menu". Default value is
395   *   null.
396   * @param {string} options.collision - When the positioned element overflows
397   *   the window in some direction, move it to an alternative position. Similar
398   *   to `my` and `at`, this accepts a single value or a pair for
399   *   horizontal/vertical, e.g., "flip", "fit", "fit flip", "fit none". Default
400   *   value is "flip". The options work as follows:
401   *   - "flip": Flips the element to the opposite side of the target and the
402   *     collision detection is run again to see if it will fit. Whichever side
403   *     allows more of the element to be visible will be used.
404   *   - "fit": Shift the element away from the edge of the window.
405   *   - "flipfit": First applies the flip logic, placing the element on
406   *     whichever side allows more of the element to be visible. Then the fit
407   *     logic is applied to ensure as much of the element is visible as
408   *     possible.
409   *     "none": Does not apply any collision detection.
410   * @param {function|null} options.using - When specified, the actual property
411   *   setting is delegated to this callback. Receives two parameters: The first
412   *   is a hash of top and left values for the position that should be set and
413   *   can be forwarded to .css() or .animate().The second provides feedback
414   *   about the position and dimensions of both elements, as well as
415   *   calculations to their relative position. Both target and element have
416   *   these properties: element, left, top, width, height. In addition, there's
417   *   horizontal, vertical and important, providing twelve potential directions
418   *   like { horizontal: "center", vertical: "left", important: "horizontal" }.
419   *   Default value is null.
420   * @param {string|Element|jQuery} options.within - Element to position within,
421   *   affecting collision detection. If you provide a selector or jQuery
422   *   object, the first matching element will be used. Default value is window.
423   *
424   * @return {jQuery}
425   *  The jQuery object that called called this function.
426   */
427  $.fn.position = function (options) {
428    if (!options || !options.of) {
429      // eslint-disable-next-line prefer-rest-params
430      return _position.apply(this, arguments);
431    }
432
433    // Make a copy, we don't want to modify arguments
434    options = $.extend({}, options);
435
436    const within = $.position.getWithinInfo(options.within);
437    const scrollInfo = $.position.getScrollInfo(within);
438    const collision = (options.collision || 'flip').split(' ');
439    const offsets = {};
440
441    // Make sure string options are treated as CSS selectors
442    const target =
443      typeof options.of === 'string'
444        ? $(document).find(opti
444ons.of)
445        : $(options.of);
446    const dimensions = getDimensions(target);
447    const targetWidth = dimensions.width;
448    const targetHeight = dimensions.height;
449    const targetOffset = dimensions.offset;
450
451    if (target[0].preventDefault) {
452      // Force left top to allow flipping
453      options.at = 'left top';
454    }
455
456    // Clone to reuse original targetOffset later
457    const basePosition = $.extend({}, targetOffset);
458
459    // Force my and at to have valid horizontal and vertical positions
460    // if a value is missing or invalid, it will be converted to center
461    // eslint-disable-next-line func-names
462    $.each(['my', 'at'], function () {
463      let pos = (options[this] || '').split(' ');
464
465      if (pos.length === 1) {
466        // eslint-disable-next-line no-nested-ternary
467        pos = regexHorizontal.test(pos[0])
468          ? pos.concat(['center'])
469          : regexVertical.test(pos[0])
470            ? ['center'].concat(pos)
471            : ['center', 'center'];
472      }
473      pos[0] = regexHorizontal.test(pos[0]) ? pos[0] : 'center';
474      pos[1] = regexVertical.test(pos[1]) ? pos[1] : 'center';
475
476      // Calculate offsets
477      const horizontalOffset = regexOffset.exec(pos[0]);
478      const verticalOffset = regexOffset.exec(pos[1]);
479      offsets[this] = [
480        horizontalOffset ? horizontalOffset[0] : 0,
481        verticalOffset ? verticalOffset[0] : 0,
482      ];
483
484      // Reduce to just the positions without the offsets
485      options[this] = [
486        regexPosition.exec(pos[0])[0],
487        regexPosition.exec(pos[1])[0],
488      ];
489    });
490
491    // Normalize collision option
492    if (collision.length === 1) {
493      // eslint-disable-next-line prefer-destructuring
494      collision[1] = collision[0];
495    }
496
497    if (options.at[0] === 'right') {
498      basePosition.left += targetWidth;
499    } else if (options.at[0] === 'center') {
500      basePosition.left += targetWidth / 2;
501    }
502
503    if (options.at[1] === 'bottom') {
504      basePosition.top += targetHeight;
505    } else if (options.at[1] === 'center') {
506      basePosition.top += targetHeight / 2;
507    }
508
509    const atOffset = getOffsets(offsets.at, targetWidth, targetHeight);
510    basePosition.left += atOffset[0];
511    basePosition.top += atOffset[1];
512
513    // eslint-disable-next-line func-names
514    return this.each(function () {
515      let using;
516      const elem = $(this);
517      const elemWidth = elem.outerWidth();
518      const elemHeight = elem.outerHeight();
519      const marginLeft = parseCss(this, 'marginLeft');
520      const marginTop = parseCss(this, 'marginTop');
521      const collisionWidth =
522        elemWidth +
523        marginLeft +
524        parseCss(this, 'marginRight') +
525        scrollInfo.width;
526      const collisionHeight =
527        elemHeight +
528        marginTop +
529        parseCss(this, 'marginBottom') +
530        scrollInfo.height;
531      const position = $.extend({}, basePosition);
532      const myOffset = getOffsets(
533        offsets.my,
534        elem.outerWidth(),
535        elem.outerHeight(),
536      );
537
538      if (options.my[0] === 'right') {
539        position.left -= elemWidth;
540      } else if (options.my[0] === 'center') {
541        position.left -= elemWidth / 2;
542      }
543
544      if (options.my[1] === 'bottom') {
545        position.top -= elemHeight;
546      } else if (options.my[1] === 'center') {
547        position.top -= elemHeight / 2;
548      }
549
550      position.left += myOffset[0];
551      position.top += myOffset[1];
552
553      const collisionPosition = {
554        marginLeft,
555        marginTop,
556      };
557
558      // eslint-disable-next-line func-names
559      $.each(['left', 'top'], function (i, dir) {
560        if (collisions[collision[i]]) {
561          collisions[collision[i]][dir](position, {
562            targetWidth,
563            targetHeight,
564            elemWidth,
565            elemHeight,
566            collisionPosition,
567            collisionWidth,
568            collisionHeight,
569            offset: [atOffset[0] + myOffset[0], atOffset[1] + myOffset[1]],
570            my: options.my,
571            at: options.at,
572            within,
573            elem,
574          });
575        }
576      });
577
578      if (options.using) {
579        // Adds feedback as second argument to using callback, if present
580        // eslint-disable-next-line func-names
581        using = function (props) {
582          const left = targetOffset.left - position.left;
583          const right = left + targetWidth - elemWidth;
584          const top = targetOffset.top - position.top;
585          const bottom = top + targetHeight - elemHeight;
586          const feedback = {
587            target: {
588              element: target,
589              left: targetOffset.left,
590              top: targetOffset.top,
591              width: targetWidth,
592              height: targetHeight,
593            },
594            element: {
595              element: elem,
596              left: position.left,
597              top: position.top,
598              width: elemWidth,
599              height: elemHeight,
600            },
601            // eslint-disable-next-line no-nested-ternary
602            horizontal: right < 0 ? 'left' : left > 0 ? 'right' : 'center',
603            // eslint-disable-next-line no-nested-ternary
604            vertical: bottom < 0 ? 'top' : top > 0 ? 'bottom' : 'middle',
605          };
606          if (targetWidth < elemWidth && abs(left + right) < targetWidth) {
607            feedback.horizontal = 'center';
608          }
609          if (targetHeight < elemHeight && abs(top + bottom) < targetHeight) {
610            feedback.vertical = 'middle';
611          }
612          if (max(abs(left), abs(right)) > max(abs(top), abs(bottom))) {
613            feedback.important = 'horizontal';
614          } else {
615            feedback.important = 'vertical';
616          }
617          options.using.call(this, props, feedback);
618        };
619      }
620
621      elem.offset($.extend(position, { using }));
622    });
623  };
624
625  // Although $.ui.position is not built to be called directly, some legacy code
626  // may have checks for the presence of $.ui.position, which can be used to
627  // confirm the presence of jQuery UI position's API, as opposed to the more
628  // limited version provided by jQuery.
629  if (!$.hasOwnProperty('ui')) {
630    $.ui = {};
631  }
632  $.ui.position = collisions;
633})(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.