1/** 2 * @file 3 * External links js file. 4 */ 5 6((Drupal, drupalSettings) => { 7 Drupal.extlink = Drupal.extlink || {}; 8 9 Drupal.extlink.attach = (context, drupalSettings) => { 10 if ( 11 typeof drupalSettings.data === 'undefined' || 12 !drupalSettings.data.hasOwnProperty('extlink') 13 ) { 14 return; 15 } 16 17 // Define the method (either 'append' or 'prepend') of placing the 18 // icon, defaults to 'append'. 19 let extIconPlacement = 'append'; 20 if ( 21 drupalSettings.data.extlink.extIconPlacement && 22 drupalSettings.data.extlink.extIconPlacement !== '0' 23 ) { 24 extIconPlacement = drupalSettings.data.extlink.extIconPlacement; 25 } 26 27 // Strip the host name down, removing ports, subdomains, or www. 28 const pattern = 29 /^(([^:]+?\.)*)([^.:]+)((\.[a-z0-9]{1,253})*)(:[0-9]{1,5})?$/; 30 const host = window.location.host.replace(pattern, '$2$3$6'); 31 const subdomain = window.location.host.replace(host, ''); 32 33 // Determine what subdomains are considered internal. 34 let subdomains; 35 if (drupalSettings.data.extlink.extSubdomains) { 36 subdomains = '([^/]*\\.)?'; 37 } else if (subdomain === 'www.' || subdomain === '') { 38 subdomains = '(www\\.)?'; 39 } else { 40 subdomains = subdomain.replace('.', '\\.'); 41 } 42 43 // Allowed domains. 44 let allowedDomains = false; 45 if (drupalSettings.data.extlink.allowedDomains) { 46 allowedDomains = []; 47 for ( 48 let i = 0; 49 i < drupalSettings.data.extlink.allowedDomains.length; 50 i++ 51 ) { 52 allowedDomains.push( 53 new RegExp( 54 `^https?:\\/\\/${drupalSettings.data.extlink.allowedDomains[i].replace(/(\r\n|\n|\r)/gm, '')}.*$`, 55 'i', 56 ), 57 ); 58 } 59 } 60 61 // Build regular expressions that define an internal link. 62 const internalLink = new RegExp( 63 `^https?://([^@]*@)?${subdomains}${host}`, 64 'i', 65 ); 66 67 // Extra internal link matching. 68 let extInclude = false; 69 if (drupalSettings.data.extlink.extInclude) { 70 extInclude = new RegExp( 71 drupalSettings.data.extlink.extInclude.replace(/\\/, '\\'), 72 'i', 73 ); 74 } 75 76 // Extra external link matching. 77 let extExclude = false; 78 if (drupalSettings.data.extlink.extExclude) { 79 extExclude = new RegExp( 80 drupalSettings.data.extlink.extExclude.replace(/\\/, '\\'), 81 'i', 82 ); 83 } 84 85 // Extra external link matching for excluding noreferrer. 86 let extExcludeNoreferrer = false; 87 if (drupalSettings.data.extlink.extExcludeNoreferrer) { 88 extExcludeNoreferrer = new RegExp( 89 drupalSettings.data.extlink.extExcludeNoreferrer.replace(/\\/, '\\'), 90 'i', 91 ); 92 } 93 94 // Extra external link CSS selector exclusion. 95 let extCssExclude = false; 96 if (drupalSettings.data.extlink.extCssExclude) { 97 extCssExclude = drupalSettings.data.extlink.extCssExclude; 98 } 99 100 // Extra external link CSS selector inclusion. 101 let extCssInclude = false; 102 if (drupalSettings.data.extlink.extCssInclude) { 103 extCssInclude = drupalSettings.data.extlink.extCssInclude; 104 } 105 106 // Extra external link CSS selector explicit. 107 let extCssExplicit = false; 108 if (drupalSettings.data.extlink.extCssExplicit) { 109 extCssExplicit = drupalSettings.data.extlink.extCssExplicit; 110 } 111 112 // Find all links which are NOT internal and begin with http as opposed 113 // to ftp://, javascript:, etc. other kinds of links. 114 // When operating on the 'this' variable, the host has been appended to 115 // all links by the browser, even local ones. 116 const externalLinks = []; 117 const mailtoLinks = []; 118 const telLinks = []; 119 const extlinks = context.querySelectorAll( 120 'a:not([data-extlink]), area:not([data-extlink])', 121 ); 122 extlinks.forEach((el) => { 123 try { 124 let url = ''; 125 if (typeof el.href === 'string') { 126 url = el.href.toLowerCase(); 127 } 128 // Handle SVG links (xlink:href). 129 else if (typeof el.href === 'object') { 130 url = el.href.baseVal; 131 } 132 const isExtCssIncluded = 133 extCssInclude &&
134 (el.matches(extCssInclude) || el.closest(extCssInclude)); 135 if ( 136 url.indexOf('http') === 0 && 137 ((!internalLink.test(url) && !(extExclude && extExclude.test(url))) || 138 (extInclude && extInclude.test(url)) || 139 isExtCssIncluded) && 140 !(extCssExclude && el.matches(extCssExclude)) && 141 !(extCssExclude && el.closest(extCssExclude)) && 142 !(extCssExplicit && !el.closest(extCssExplicit)) 143 ) { 144 let match = false; 145 if (!isExtCssIncluded && allowedDomains) { 146 for (let i = 0; i < allowedDomains.length; i++) { 147 if (allowedDomains[i].test(url)) { 148 match = true; 149 break; 150 } 151 } 152 } 153 if (!match) { 154 externalLinks.push(el); 155 } 156 } 157 // Do not include area tags with begin with mailto: (this prohibits 158 // icons from being added to image-maps). 159 else if ( 160 el.tagName !== 'AREA' && 161 !(extCssExclude && el.closest(extCssExclude)) && 162 !(extCssExplicit && !el.closest(extCssExplicit)) 163 ) { 164 if (url.indexOf('mailto:') === 0) { 165 mailtoLinks.push(el); 166 } else if (url.indexOf('tel:') === 0) { 167 telLinks.push(el); 168 } 169 } 170 } catch (error) { 171 // IE7 throws errors often when dealing with irregular links, such as: 172 // <a href="node/10"></a> Empty tags. 173 // <a href="http://user:[email protected]">example</a> User:pass syntax. 174 return false; 175 } 176 }); 177 178 const hasExtIcon = 179 drupalSettings.data.extlink.extClass !== '0' && 180 drupalSettings.data.extlink.extClass !== ''; 181 const hasAdditionalExtClasses = 182 drupalSettings.data.extlink.extAdditionalLinkClasses !== ''; 183 Drupal.extlink.applyClassAndSpan( 184 externalLinks, 185 'ext', 186 hasExtIcon ? extIconPlacement : null, 187 ); 188 if (hasAdditionalExtClasses) { 189 Drupal.extlink.applyClassAndSpan( 190 externalLinks, 191 drupalSettings.data.extlink.extAdditionalLinkClasses, 192 null, 193 ); 194 } 195 196 const hasMailtoClass = 197 drupalSettings.data.extlink.mailtoClass !== '0' && 198 drupalSettings.data.extlink.mailtoClass !== ''; 199 const hasAdditionalMailtoClasses = 200 drupalSettings.data.extlink.extAdditionalMailtoClasses !== ''; 201 if (hasMailtoClass) { 202 Drupal.extlink.applyClassAndSpan( 203 mailtoLinks, 204 drupalSettings.data.extlink.mailtoClass, 205 extIconPlacement, 206 ); 207 } 208 if (hasAdditionalMailtoClasses) { 209 Drupal.extlink.applyClassAndSpan( 210 mailtoLinks, 211 drupalSettings.data.extlink.extAdditionalMailtoClasses, 212 null, 213 ); 214 } 215 216 const hasTelClass = 217 drupalSettings.data.extlink.telClass !== '0' && 218 drupalSettings.data.extlink.telClass !== ''; 219 const hasAdditionalTelClasses = 220 drupalSettings.data.extlink.extAdditionalTelClasses !== '0' && 221 drupalSettings.data.extlink.extAdditionalTelClasses !== ''; 222 if (hasTelClass) { 223 Drupal.extlink.applyClassAndSpan( 224 telLinks, 225 drupalSettings.data.extlink.telClass, 226 extIconPlacement, 227 ); 228 } 229 if (hasAdditionalTelClasses) { 230 Drupal.extlink.applyClassAndSpan( 231 mailtoLinks, 232 drupalSettings.data.extlink.extAdditionalTelClasses, 233 null, 234 ); 235 } 236 237 if (drupalSettings.data.extlink.extTarget) { 238 // Add target attr to open link in a new tab if not set. 239 externalLinks.forEach((link, i) => { 240 if ( 241 !( 242 drupalSettings.data.extlink.extTargetNoOverride && 243 link.matches('a[target]') 244 ) 245 ) { 246 externalLinks[i].setAttribute('target', '_blank'); 247 } 248 }); 249 250 // Add noopener rel attribute to combat phishing. 251 externalLinks.forEach((link, i) => { 252 const val = link.getAttribute('rel'); 253 // If no rel attribute is present, create one with the value noopener. 254 if (val === null || typeof val === 'undefined') { 255 externalLinks[i].setAttribute('rel', 'noopener'); 256 return; 257 }
258 // Check to see if rel contains noopener. Add what doesn't exist. 259 if (val.indexOf('noopener') > -1) { 260 if (val.indexOf('noopener') === -1) { 261 // Add noopener. 262 externalLinks[i].setAttribute('rel', `${val} noopener`); 263 } else { 264 // Noopener exists. Nothing needs to be added. 265 } 266 } 267 // Else, append noopener to val. 268 else { 269 // Add noopener. 270 externalLinks[i].setAttribute('rel', `${val} noopener`); 271 } 272 }); 273 } 274 275 if (drupalSettings.data.extlink.extNofollow) { 276 externalLinks.forEach((link, i) => { 277 const val = link.getAttribute('rel'); 278 // When the link does not have a rel attribute set it to 'nofollow'. 279 if (val === null || typeof val === 'undefined') { 280 externalLinks[i].setAttribute('rel', 'nofollow'); 281 return; 282 } 283 let target = 'nofollow'; 284 // Change the target, if not overriding follow. 285 if (drupalSettings.data.extlink.extFollowNoOverride) { 286 target = 'follow'; 287 } 288 if (val.indexOf(target) === -1) { 289 // Add nofollow. 290 externalLinks[i].setAttribute('rel', `${val} nofollow`); 291 } 292 }); 293 } 294 295 if (drupalSettings.data.extlink.extTitleNoOverride === false) { 296 // Set the title attribute of all external links that opens in a new window. 297 externalLinks.forEach((link, i) => { 298 const oldTitle = link.getAttribute('title'); 299 let newTitle = ''; 300 301 if ( 302 drupalSettings.data.extlink.extTarget && 303 drupalSettings.data.extlink.extTargetAppendNewWindowDisplay 304 ) { 305 newTitle = drupalSettings.data.extlink.extTargetAppendNewWindowLabel; 306 } 307 // Determine new title based on drupalSettings extTarget configuration. 308 if (oldTitle !== null) { 309 if (Drupal.extlink.hasNewWindowText(oldTitle)) { 310 return; 311 } 312 newTitle = Drupal.extlink.combineLabels(oldTitle, newTitle); 313 } 314 if (newTitle) { 315 externalLinks[i].setAttribute('title', newTitle); 316 } 317 }); 318 } 319 320 if (drupalSettings.data.extlink.extNoreferrer) { 321 externalLinks.forEach((link, i) => { 322 // Don't add 'noreferrer' if exclude noreferrer option is set and the 323 // link matches the entered pattern to exclude 'noreferrer' tag. 324 if ( 325 drupalSettings.data.extlink.extExcludeNoreferrer && 326 extExcludeNoreferrer.test(link.getAttribute('href')) 327 ) { 328 return; 329 } 330 331 const val = link.getAttribute('rel'); 332 // When the link does not have a rel attribute set it to 'noreferrer'. 333 if (val === null || typeof val === 'undefined') { 334 externalLinks[i].setAttribute('rel', 'noreferrer'); 335 return; 336 } 337 // Add noreferrer. 338 externalLinks[i].setAttribute('rel', `${val} noreferrer`); 339 }); 340 } 341 342 /* eslint:disable:no-empty */ 343 Drupal.extlink = Drupal.extlink || {}; 344 345 // Set up default click function for the external links popup. This should be 346 // overridden by modules wanting to alter the popup. 347 Drupal.extlink.popupClickHandler = 348 Drupal.extlink.popupClickHandler || 349 (() => { 350 if (drupalSettings.data.extlink.extAlert) { 351 // eslint-disable-next-line no-restricted-globals 352 return confirm(drupalSettings.data.extlink.extAlertText); 353 } 354 }); 355 356 const _that = this; 357 Drupal.extlink.handleClick = function (event) { 358 const shouldNavigate = Drupal.extlink.popupClickHandler.call( 359 _that, 360 event, 361 ); 362 if (typeof shouldNavigate !== 'undefined' && !shouldNavigate) { 363 // Prevent navigation if the user clicks "Cancel". 364 event.preventDefault(); 365 } 366 }; 367 368 externalLinks.forEach((val, i) => { 369 externalLinks[i].removeEventListener('click', Drupal.extlink.handleClick); 370 externalLinks[i].addEventListener('click', Drupal.extlink.handleClick); 371 }); 372 }; 373 374 /** 375 * Check if the label already has 'new window' text. 376 * 377 * 378 * @returns boolean 379 * @param label 380 */ 381 Drupal.extlink.hasNewWindowText = function (label) { 382 return label.toLowerCase().indexOf(Drupal.t('new window')) !== -1; 383 }; 384 385 /** 386 * Check if element is mostly text like element. 387 * 388 * We consider element to be text like if it starts with text, ends with text 389 * and in between can only have simple html elements - elements that do not 390 * have inner elements. 391 * 392 * @param element 393 * HTML element. 394 * @returns {boolean} 395 * true if this element is mostly text, false other way. 396 */ 397 Drupal.extlink.isTextLikeElement = function (element) { 398 let startsWithText = false;
399 let endsWithText = false; 400 401 for (let i = 0; i < element.childNodes.length; i++) { 402 const child = element.childNodes[i]; 403 404 if (child.nodeType === Node.TEXT_NODE) { 405 const trimmedText = child.textContent.trim(); 406 if (trimmedText.length > 0) { 407 if (!startsWithText) { 408 startsWithText = true; 409 } 410 // Any text node after an element is an ending text. 411 endsWithText = true; 412 } 413 } else if (child.nodeType === Node.ELEMENT_NODE) { 414 endsWithText = false; 415 416 // If the element has any children, itâs not considered a text like 417 // link. 418 if (child.children.length > 0) { 419 return false; 420 } 421 } 422 } 423 424 return startsWithText && endsWithText; 425 }; 426 427 /** 428 * Combine two labels. 429 * 430 * Combine labels in a readable manner, taking into account if the label 431 * uses parenthesis or not. For examples, 432 * 433 * 1. "A" + "B" => "A, B" 434 * 2. "A" + "(B)" => "A (B)" 435 * 3. "(A)" + "B" => "B (A)" 436 * 4. "(A)" + "(B)" => "(A, B)" 437 * 438 * 439 * @returns string 440 * @param labelA 441 * @param labelB 442 */ 443 Drupal.extlink.combineLabels = function (labelA, labelB) { 444 labelA = labelA || ''; 445 labelB = labelB || ''; 446 447 // If either label is empty, return the other without adding a comma 448 if (!labelA.trim()) return labelB; 449 if (!labelB.trim()) return labelA; 450 451 const labelANoParens = labelA.trim().replace('(', '').replace(')', ''); 452 const labelBNoParens = labelB.trim().replace('(', '').replace(')', ''); 453 if (labelA === labelANoParens) { 454 if (labelB === labelBNoParens) { 455 // This is Example 1 above: "A" + "B" => "A, B" 456 return `${labelA}, ${labelB}`; 457 } 458 459 // This is Example 2 above: "A" + "(B)" => "A (B)" 460 return `${labelA} ${labelB}`; 461 } 462 463 if (labelB === labelBNoParens) { 464 // This is Example 3 above: "(A)" + "B" => "B (A)" 465 return `${labelB} ${labelA}`; 466 } 467 468 // This is Example 4 above: "(A)" + "(B)" => "(A, B)" 469 return `(${labelANoParens}, ${labelBNoParens})`; 470 }; 471 472 /** 473 * Apply a class and a trailing <span> to all links not containing images. 474 * 475 * @param {object[]} links 476 * An array of DOM elements representing the links. 477 * @param {string} className 478 * The class to apply to the links. 479 * @param {string} iconPlacement 480 * 'append' or 'prepend' the icon to the link. 481 */ 482 Drupal.extlink.applyClassAndSpan = (links, className, iconPlacement) => { 483 let linksToProcess; 484 if (drupalSettings.data.extlink.extImgClass) { 485 linksToProcess = links; 486 } else { 487 // Only text links. 488 linksToProcess = links.filter((link) => { 489 return link.querySelector('img, svg') === null; 490 }); 491 } 492 493 for (let i = 0; i < linksToProcess.length; i++) { 494 if (className !== '0') { 495 linksToProcess[i].classList.add(className); 496 } 497 498 // Additional classes: 499 if ( 500 className === drupalSettings.data.extlink.mailtoClass && 501 drupalSettings.data.extlink.extAdditionalMailtoClasses 502 ) { 503 // Is mail link: 504 linksToProcess[i].classList.add( 505 drupalSettings.data.extlink.extAdditionalMailtoClasses, 506 ); 507 } else if ( 508 className === drupalSettings.data.extlink.telClass && 509 drupalSettings.data.extlink.extAdditionalTelClasses 510 ) { 511 // Is regular external link: 512 linksToProcess[i].classList.add( 513 drupalSettings.data.extlink.extAdditionalTelClasses, 514 ); 515 } else if (drupalSettings.data.extlink.extAdditionalLinkClasses) { 516 // Is regular external link: 517 linksToProcess[i].classList.add( 518 drupalSettings.data.extlink.extAdditionalLinkClasses, 519 ); 520 } 521 522 // Add data-extlink attribute. 523 linksToProcess[i].setAttribute('data-extlink', ''); 524 525 // Add icon. 526 if (iconPlacement) { 527 let link = linksToProcess[i]; 528 529 // Prevent appended icons from wrapping lines. 530 if ( 531 drupalSettings.data.extlink.extPreventOrphan && 532 iconPlacement === 'append' 533 ) { 534 let preventOrphan = true; 535 536 if ( 537 drupalSettings.data.extlink.extPreventOrphanTextLike && 538 !Drupal.extlink.isTextLikeElement(link) 539 ) { 540 preventOrphan = false;
541 } 542 543 if (preventOrphan) { 544 // Find the last word in the link. 545 let lastTextNode = link.lastChild; 546 let trailingWhitespace = null; 547 let parentNode = link; 548 while (lastTextNode) { 549 if (lastTextNode.lastChild) { 550 // Last node was not text; step down into child node. 551 parentNode = lastTextNode; 552 lastTextNode = lastTextNode.lastChild; 553 } else if ( 554 lastTextNode.nodeName === '#text' && 555 parentNode.lastElementChild && 556 lastTextNode.textContent.trim().length === 0 557 ) { 558 // Last node was text, but it was whitespace. Step back into previous node. 559 trailingWhitespace = lastTextNode; 560 parentNode = parentNode.lastElementChild; 561 lastTextNode = parentNode.lastChild; 562 } else { 563 // Last node was null or valid text. 564 break; 565 } 566 } 567 if ( 568 lastTextNode && 569 lastTextNode.nodeName === '#text' && 570 lastTextNode.textContent.length > 0 571 ) { 572 const lastText = lastTextNode.textContent; 573 const lastWordRegex = new RegExp(/\S+\s*$/, 'g'); 574 const lastWord = lastText.match(lastWordRegex); 575 if (lastWord !== null) { 576 // Wrap the last word in a span. 577 const breakPreventer = document.createElement('span'); 578 breakPreventer.classList.add('extlink-nobreak'); 579 breakPreventer.textContent = lastWord[0]; 580 if (trailingWhitespace) { 581 trailingWhitespace.textContent = ''; 582 breakPreventer.append(trailingWhitespace.textContent); 583 } 584 lastTextNode.textContent = lastText.substring( 585 0, 586 lastText.length - lastWord[0].length, 587 ); 588 lastTextNode.parentNode.append(breakPreventer); 589 // Insert the icon into the span rather than the link. 590 link = breakPreventer; 591 } 592 } 593 } 594 } 595 596 // Create an icon element. 597 let iconElement; 598 if (drupalSettings.data.extlink.extUseFontAwesome) { 599 iconElement = document.createElement('span'); 600 iconElement.setAttribute('class', `fa-${className} extlink`); 601 if (className === drupalSettings.data.extlink.mailtoClass) { 602 if (drupalSettings.data.extlink.mailtoLabel) { 603 link.ariaLabel = drupalSettings.data.extlink.mailtoLabel; 604 } 605 iconElement.innerHTML = Drupal.theme( 606 'extlink_fa_mailto', 607 drupalSettings, 608 iconPlacement, 609 ); 610 } else if (className === drupalSettings.data.extlink.extClass) { 611 if (drupalSettings.data.extlink.extLabel) { 612 link.ariaLabel = drupalSettings.data.extlink.extLabel; 613 } 614 iconElement.innerHTML = Drupal.theme( 615 'extlink_fa_extlink', 616 drupalSettings, 617 iconPlacement, 618 ); 619 } else if (className === drupalSettings.data.extlink.telClass) { 620 if (drupalSettings.data.extlink.telLabel) { 621 link.ariaLabel = drupalSettings.data.extlink.telLabel; 622 } 623 iconElement.innerHTML = Drupal.theme( 624 'extlink_fa_tel', 625 drupalSettings, 626 iconPlacement, 627 ); 628 } 629 } else if (drupalSettings.data.extlink.extUseIcon) { 630 iconElement = document.createElement('span'); 631 iconElement.setAttribute('class', `extlink`); 632 iconElement.setAttribute('data-extlink-placement', iconPlacement); 633 if (className === drupalSettings.data.extlink.mailtoClass) { 634 if (drupalSettings.data.extlink.mailtoLabel) { 635 link.ariaLabel = drupalSettings.data.extlink.mailtoLabel; 636 } 637 iconElement.innerHTML = drupalSettings.data.extlink.extMailIcon; 638 iconElement.setAttribute('data-extlink-icon-type', 'mailto'); 639 } else if (className === drupalSettings.data.extlink.extClass) { 640 if (drupalSettings.data.extlink.extLabel) { 641 link.ariaLabel = drupalSettings.data.extlink.extLabel; 642 } 643 iconElement.innerHTML = drupalSettings.data.extlink.extLinkIcon; 644 iconElement.setAttribute('data-extlink-icon-type', 'link'); 645 } else if (className === drupalSettings.data.extlink.telClass) { 646 if (drupalSettings.data.extlink.telLabel) { 647 link.ariaLabel = drupalSettings.data.extlink.telLabel; 648 } 649 iconElement.innerHTML = drupalSettings.data.extlink.extTelIcon; 650 iconElement.setAttribute('data-extlink-icon-type', 'tel'); 651 } 652 } else { 653 iconElement = document.createElementNS( 654 'http://www.w3.org/2000/svg', 655 'svg', 656 ); 657 iconElement.setAttribute('focusable', 'false'); 658 iconElement.setAttribute('width', '1em'); 659 iconElement.setAttribute('height', '1em'); 660 iconElement.classList.add(className); 661 iconElement.setAttribute('data-extlink-placement', iconPlacement); 662 if (className === drupalSettings.data.extlink.mailtoClass) { 663 iconElement = Drupal.theme( 664 'extlink_mailto', 665 iconElement, 666 drupalSettings, 667 ); 668 } else if (className === drupalSettings.data.extlink.extClass) { 669 iconElement = Drupal.theme( 670 'extlink_extlink', 671 iconElement, 672 drupalSettings, 673 ); 674 } else if (className === drupalSettings.data.extlink.telClass) { 675 iconElement = Drupal.theme( 676 'extlink_tel', 677 iconElement, 678 drupalSettings, 679 ); 680 } 681 } 682 iconElement.setAttribute('role', 'img'); 683 iconElement.setAttribute( 684 'aria-hidden', 685 drupalSettings.data.extlink.extHideIcons, 686 ); 687 link[iconPlacement](iconElement); 688 } 689 } 690 }; 691 692 /** 693 * Theme function for a Font Awesome mailto icon. 694 * 695 * @param {object} drupalSettings 696 * Settings object used to construct the markup. 697 * @param {string} iconPlacement 698 * The class to apply to a link. 699 * 700 * @return {string} 701 * HTML string of the Font AweSome mailto link icon. 702 */ 703 Drupal.theme.extlink_fa_mailto = function (drupalSettings, iconPlacement) { 704 return `<span class="${drupalSettings.data.extlink.extFaMailtoClasse
704s}" data-extlink-placement="${iconPlacement}"></span>`; 705 }; 706 707 /** 708 * Theme function for a Font Awesome external link icon. 709 * 710 * @param {object} drupalSettings 711 * Settings object used to construct the markup. 712 * @param {string} iconPlacement 713 * The class to apply to a link. 714 * 715 * @return {string} 716 * HTML string of the Font AweSome external link icon. 717 */ 718 Drupal.theme.extlink_fa_extlink = function (drupalSettings, iconPlacement) { 719 return `<span class="${drupalSettings.data.extlink.extFaLinkClasses}" data-extlink-placement="${iconPlacement}"></span>`; 720 }; 721 722 /** 723 * Theme function for a Font Awesome telephone icon. 724 * 725 * @param {object} drupalSettings 726 * Settings object used to construct the markup. 727 * @param {string} iconPlacement 728 * The class to apply to a link. 729 * 730 * @return {string} 731 * HTML string of the Font AweSome telephone icon. 732 */ 733 Drupal.theme.extlink_fa_tel = function (drupalSettings, iconPlacement) { 734 return `<span class="${drupalSettings.data.extlink.extFaTelClasses}" data-extlink-placement="${iconPlacement}"></span>`; 735 }; 736 737 /** 738 * Theme function for a mailto icon. 739 * 740 * @param {object} iconElement 741 * The current iconElement being altered. 742 * @param {object} drupalSettings 743 * Settings object used to construct the markup. 744 * 745 * @return {object} 746 * The altered iconElement. 747 */ 748 Drupal.theme.extlink_mailto = function (iconElement, drupalSettings) { 749 iconElement.setAttribute( 750 'aria-label', 751 drupalSettings.data.extlink.mailtoLabel, 752 ); 753 iconElement.setAttribute('viewBox', '0 10 70 20'); 754 iconElement.innerHTML = `<title>${drupalSettings.data.extlink.mailtoLabel}</title><path d="M56 14H8c-1.1 0-2 0.9-2 2v32c0 1.1 0.9 2 2 2h48c1.1 0 2-0.9 2-2V16C58 14.9 57.1 14 56 14zM50.5 18L32 33.4 13.5 18H50.5zM10 46V20.3l20.7 17.3C31.1 37.8 31.5 38 32 38s0.9-0.2 1.3-0.5L54 20.3V46H10z"/>`; 755 return iconElement; 756 }; 757 758 /** 759 * Theme function for an external link icon. 760 * 761 * @param {object} iconElement 762 * The current iconElement being altered. 763 * @param {object} drupalSettings 764 * Settings object used to construct the markup. 765 * 766 * @return {object} 767 * The altered iconElement. 768 */ 769 Drupal.theme.extlink_extlink = function (iconElement, drupalSettings) { 770 iconElement.setAttribute( 771 'aria-label', 772 drupalSettings.data.extlink.extLabel, 773 ); 774 iconElement.setAttribute('viewBox', '0 0 80 40'); 775 iconElement.innerHTML = `<title>${drupalSettings.data.extlink.extLabel}</title><path d="M48 26c-1.1 0-2 0.9-2 2v26H10V18h26c1.1 0 2-0.9 2-2s-0.9-2-2-2H8c-1.1 0-2 0.9-2 2v40c0 1.1 0.9 2 2 2h40c1.1 0 2-0.9 2-2V28C50 26.9 49.1 26 48 26z"/><path d="M56 6H44c-1.1 0-2 0.9-2 2s0.9 2 2 2h7.2L30.6 30.6c-0.8 0.8-0.8 2 0 2.8C31 33.8 31.5 34 32 34s1-0.2 1.4-0.6L54 12.8V20c0 1.1 0.9 2 2 2s2-0.9 2-2V8C58 6.9 57.1 6 56 6z"/>`; 776 return iconElement; 777 }; 778 779 /** 780 * Theme function for an external telephone icon. 781 * 782 * @param {object} iconElement 783 * The current iconElement being altered. 784 * @param {object} drupalSettings 785 * Settings object used to construct the markup. 786 * 787 * @return {object} 788 * The altered iconElement. 789 */ 790 Drupal.theme.extlink_tel = function (iconElement, drupalSettings) { 791 iconElement.setAttribute( 792 'aria-label', 793 drupalSettings.data.extlink.telLabel, 794 ); 795 iconElement.setAttribute('viewBox', '0 0 181.352 181.352'); 796 iconElement.innerHTML = `<title>${drupalSettings.data.extlink.telLabel}</title><path xmlns="http://www.w3.org/2000/svg" d="M169.393,167.37l-14.919,9.848c-9.604,6.614-50.531,14.049-106.211-53.404C-5.415,58.873,9.934,22.86,17.134,14.555L29.523,1.678c2.921-2.491,7.328-2.198,9.839,0.811l32.583,38.543l0.02,0.02c2.384,2.824,2.306,7.22-0.83,9.868v0.029l-14.44,10.415c-5.716,5.667-0.733,14.587,5.11,23.204l27.786,32.808c12.926,12.477,20.009,18.241,26.194,14.118l12.008-13.395c2.941-2.472,7.328-2.169,9.839,0.821l32.603,38.543v0.02C172.607,160.316,172.519,164.703,169.393,167.37z"/>`; 797 return iconElement; 798 }; 799 800 Drupal.behaviors.extlink = Drupal.behaviors.extlink || {}; 801 Drupal.behaviors.extlink.attach = (context, drupalSettings) => { 802 // Backwards compatibility, for the benefit of modules overriding extlink 803 // functionality by defining an "extlinkAttach" global function. 804 if (typeof extlinkAttach === 'function') { 805 // eslint-disable-next-line no-undef 806 extlinkAttach(context); 807 } else { 808 Drupal.extlink.attach(context, drupalSettings); 809 } 810 }; 811})(Drupal, drupalSettings);
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.