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.