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.