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