1/** 2 * Drupal initializer. 3 * Launch as behavior and pull variables from config. 4 */ 5 6((Drupal, drupalSettings) => { 7 // Prevent multiple inits in modules that re-trigger the document context. 8 let linkPurposeOnce; 9 10 Drupal.behaviors.linkPurpose = { 11 attach: (context) => { 12 if ( 13 context === document && 14 !linkPurposeOnce && 15 CSS.supports('selector(:is(body))') 16 ) { 17 // "Supports" selector drops old browsers. 18 linkPurposeOnce = true; 19 20 const invalidClass = /([^A-Za-z0-9\-_])+/g; 21 const invalidClasses = /([^A-Za-z0-9,\-_])+/g; 22 23 const sanitizeClass = (string, defaultString) => { 24 const cleanClass = string 25 ? string.replaceAll(invalidClass, '') 26 : false; 27 return cleanClass || defaultString; 28 }; 29 30 const classArray = (source, defaults) => { 31 if (!source) { 32 return defaults; 33 } 34 35 // Normalize spaces to commas. 36 source = source.replaceAll(/(,*\s+)/g, ','); 37 38 // Sanitize 39 source = source.replaceAll(invalidClasses, ''); 40 41 // Split and remove empties. 42 source = source.split(',').filter(Boolean); 43 44 return source.length > 0 ? source : defaults; 45 }; 46 47 const queryCheck = document.createDocumentFragment(); 48 const validateSelector = (string, fallback, selector) => { 49 if (string && string.trim()) { 50 // Selector exists 51 try { 52 queryCheck.querySelector(string); 53 } catch (e) { 54 console.warn( 55 `Invalid selector in Link Purpose Icons for "${selector}."`, 56 ); 57 return fallback; 58 } 59 return string; 60 } 61 return fallback; 62 }; 63 64 const ignores = validateSelector( 65 drupalSettings.linkpurpose.ignore, 66 '', 67 'Ignore elements', 68 ); 69 let options = { 70 domain: drupalSettings.linkpurpose.domain, 71 roots: validateSelector( 72 drupalSettings.linkpurpose.roots, 73 'body', 74 'Check roots', 75 ), 76 shadowComponents: validateSelector( 77 drupalSettings.linkpurpose.shadowComponents, 78 '', 79 'Shadow components', 80 ), 81 shadowCSS: drupalSettings.linkpurpose.css_url 82 ? [ 83 `${drupalSettings.linkpurpose.css_url}/library/css/linkpurpose.css`, 84 ] 85 : false, 86 ignore: ignores 87 ? `${ignores}, .ck-editor a, .form-textarea-wrapper a` 88 : '.ck-editor, .form-textarea-wrapper', 89 hideIcon: validateSelector( 90 drupalSettings.linkpurpose.hideIcon, 91 false, 92 'Hide icon on element', 93 ), 94 noRunIfPresent: validateSelector( 95 drupalSettings.linkpurpose.noRunIfPresent, 96 '#quickedit-entity-toolbar, .layout-builder-form', 97 'No run if present', 98 ), 99 noRunIfAbsent: validateSelector( 100 drupalSettings.linkpurpose.noRunIfAbsent, 101 '', 102 'No run if absent', 103 ), 104 baseSelector: validateSelector( 105 drupalSettings.linkpurpose.baseSelector, 106 'a[href]', 107 'Base selector', 108 ), 109 baseLinkClass: sanitizeClass( 110 drupalSettings.linkpurpose.baseLinkClass, 111 'link-purpose', 112 ), 113 baseIconWrapperClass: sanitizeClass( 114 drupalSettings.linkpurpose.baseIconWrapperClass, 115 'link-purpose-icon', 116 ), 117 noBreakClass: sanitizeClass( 118 drupalSettings.linkpurpose.noBreakClass, 119 'link-purpose-nobreak', 120 ), 121 noReferrer: !!drupalSettings.linkpurpose.purposeExternalNoReferrer, 122 noIconOnImages: !!drupalSettings.linkpurpose.noIconOnImages, 123 suppressNoBreak: validateSelector( 124 drupalSettings.linkpurpose.suppressNoBreak, 125 false, 126 'Suppress no-break', 127 ), 128 insertIconOutsideHiddenSpan: validateSelector( 129 drupalSettings.linkpurpose.insertIconOutsideHiddenSpan, 130 '.sr-only, .visually-hidden', 131 'Insert icon outside hidden span', 132 ), 133 134 purposes: { 135 external: { 136 selector: drupalSettings.linkpurpose.purposeExternal 137 ? '[href^="https://"], [href^="http://"], [href^="//"]' 138 : false, 139 additionalSelector: validateSelector( 140 drupalSettings.linkpurpose.purposeExternalSelector, 141 '', 142 'External link selector', 143 ), 144 newWindow: !!drupalSettings.linkpurpose.purposeExternalNewWindow, 145 message: drupalSettings.linkpurpose.purposeExternalMessage 146 ? drupalSettings.linkpurpose.purposeExternalMessage 147 : 'Link is external', 148 linkClass: sanitizeClass( 149 drupalSettings.linkpurpose.purposeExternalClass, 150 'link-purpose-external', 151 ), 152 iconWrapperClass: sanitizeClass( 153 drupalSettings.linkpurpose.purposeExternalIconWrapperClass, 154 'link-purpose-external-icon', 155 ), 156 iconType: drupalSettings.linkpurpose.purposeExternalIconType 157 ? drupalSettings.linkpurpose.purposeExternalIconType 158 : 'html', 159 iconPosition: drupalSettings.linkpurpose 160 .purposeExternalIconPosition 161 ? drupalSettings.linkpurpose.purposeExternalIconPosition 162 : 'beforeend', 163 iconClasses: classArray( 164 drupalSettings.linkpurpose.purposeExternalIconClasses, 165 ['fa-solid', 'fa-up-right-from-square'], 166 ), 167 }, 168 169 document: { 170 selector: drupalSettings.linkpurpose.purposeDocument 171 ? validateSelector( 172 drupalSettings.linkpurpose.purposeDocumentSelector, 173 "[href^='/document/'], [href$='.pdf'], [href*='.pdf?'], [href$='.doc'], [href$='.docx'], [href*='.doc?'], [href*='.docx?'], [href$='.ppt'], [href$='.pptx'], [href*='.ppt?'], [href*='.pptx?'], [href^='https://docs.google']", 174 'Document selector', 175 ) 176 : false, 177 message: drupalSettings.linkpurpose.purposeDocumentMessage 178 ? drupalSettings.linkpurpose.purposeDocumentMessage 179 : 'Link downloads document', 180 linkClass: sanitizeClass( 181 drupalSettings.linkpurpose.purposeDocumentClass, 182 'link-purpose-document', 183 ), 184 iconWrapperClass: sanitizeClass( 185 drupalSettings.linkpurpose.purposeDocumentIconWrapperClass, 186 'link-purpose-document-icon', 187 ), 188 iconType: drupalSettings.linkpurpose.purposeDocumentIconType 189 ? drupalSettings.linkpurpose.purposeDocumentIconType 190 : 'html', 191 iconPosition: drupalSettings.linkpurpose 192 .purposeDocumentIconPosition 193 ? drupalSettings.linkpurpose.purposeDocumentIconPosition 194 : 'beforeend', 195 iconClasses: classArray( 196 drupalSettings.linkpurpose.purposeDocumentIconClasses, 197 ['fa-regular', 'fa-file-lines'], 198 ), 199 }, 200 201 download: { 202 selector: drupalSettings.linkpurpose.purposeDownload 203 ? validateSelector( 204 drupalSettings.linkpurpose.purposeDownloadSelector, 205 '[download]', 206 'Download selector', 207 ) 208 : false, 209 message: drupalSettings.linkpurpose.purposeDownloadMessage 210 ? drupalSettings.linkpurpose.purposeDownloadMessage 211 : 'Link downloads file', 212 linkClass: sanitizeClass( 213 drupalSettings.linkpurpose.purposeDownloadClass, 214 'link-purpose-download', 215 ), 216 iconWrapperClass: sanitizeClass( 217 drupalSettings.linkpurpose.purposeDownloadIconWrapperClass, 218 'link-purpose-download-icon', 219 ), 220 iconType: drupalSettings.linkpurpose.purposeDownloadIconType 221 ? drupalSettings.linkpurpose.purposeDownloadIconType 222 : 'html', 223 iconPosition: drupalSettings.linkpurpose 224 .purposeDownloadIconPosition 225 ? drupalSettings.linkpurpose.purposeDownloadIconPosition 226 : 'beforeend', 227 iconClasses: classArray( 228 drupalSettings.linkpurpose.purposeDownloadIconClasses, 229 ['fa-regular', 'fa-download'], 230 ), 231 }, 232 233 app: { 234 selector: drupalSettings.linkpurpose.purposeApp 235 ? validateSelector( 236 drupalSettings.linkpurpose.purposeAppSelector, 237 ':is([href*="://"]):not([href^="/"], [href^="https:"], [href^="http:"], [href^="file:"])', 238 'App selector', 239 ) 240 : false, 241 message: drupalSettings.linkpurpose.purposeAppMessage 242 ? drupalSettings.linkpurpose.purposeAppMessage 243 : 'Link opens app', 244 linkClass: sanitizeClass( 245 drupalSettings.linkpurpose.purposeAppClass, 246 'link-purpose-app', 247 ), 248 iconWrapperClass: sanitizeClass( 249 drupalSettings.linkpurpose.purposeAppIconWrapperClass, 250 'link-purpose-app-icon', 251 ), 252 iconType: drupalSettings.linkpurpose.purposeAppIconType 253 ? drupalSettings.linkpurpose.purposeAppIconType 254 : 'html', 255 iconPosition: drupalSettings.linkpurpose.purposeAppIconPosition 256 ? drupalSettings.linkpurpose.purposeAppIconPosition 257 : 'beforeend', 258 iconClasses: classArray( 259 drupalSettings.linkpurpose.purposeAppIconClasses, 260 ['fa-regular', 'fa-window-restore'], 261 ), 262 }, 263 264 mail: { 265 selector: drupalSettings.linkpurpose.purposeMail 266 ? '[href^="mailto:"]' 267 : false, 268 message: drupalSettings.linkpurpose.purposeMailMessage 269 ? drupalSettings.linkpurpose.purposeMailMessage 270 : 'Link opens email app', 271 linkClass: sanitizeClass( 272 drupalSettings.linkpurpose.purposeMailClass, 273 'link-purpose-mailto', 274 ), 275 iconWrapperClass: sanitizeClass( 276 drupalSettings.linkpurpose.purposeMailIconWrapperClass, 277 'link-purpose-mail-icon', 278 ), 279 iconType: drupalSettings.linkpurpose.purposeMailIconType 280 ? drupalSettings.linkpurpose.purposeMailIconType 281 : 'html', 282 iconPosition: drupalSettings.linkpurpose.purposeMailIconPosition 283 ? drupalSettings.linkpurpose.purposeMailIconPosition 284 : 'beforeend', 285 iconClasses: classArray( 286 drupalSettings.linkpurpose.purposeMailIconClasses, 287 ['fa-regular', 'fa-envelope'], 288 ), 289 }, 290 291 newWindow: { 292 selector: drupalSettings.linkpurpose.purposeNewWindow 293 ? '[target="_blank"]' 294 : false, 295 message: drupalSettings.linkpurpose.purposeNewWindowMessage 296 ? drupalSettings.linkpurpose.purposeNewWindowMessage 297 : 'Link opens phone app', 298 linkClass: sanitizeClass( 299 drupalSettings.linkpurpose.purposeNewWindowClass, 300 'link-purpose-window', 301 ), 302 iconWrapperClass: sanitizeClass( 303 drupalSettings.linkpurpose.purposeNewWindowIconWrapperClass, 304 'link-purpose-window-icon', 305 ), 306 iconType: drupalSettings.linkpurpose.purposeNewWindowIconType 307 ? drupalSettings.linkpurpose.purposeNewWindowIconType 308 : 'html', // html, src or classes 309 iconPosition: drupalSettings.linkpurpose 310 .purposeNewWindowIconPosition 311 ? drupalSettings.linkpurpose.purposeNewWindowIconPosition 312 : 'beforeend', 313 iconClasses: classArray( 314 drupalSettings.linkpurpose.purposeNewWindowIconClasses, 315 ['fa-regular', 'fa-window-restore'], 316 ), 317 }, 318 319 tel: { 320 selector: drupalSettings.linkpurpose.purposeTel 321 ? '[href^="tel:"]' 322 : false, 323 message: drupalSettings.linkpurpose.purposeTelMessage 324 ? drupalSettings.linkpurpose.purposeTelMessage 325 : 'Link opens in new window', 326 linkClass: sanitizeClass( 327 drupalSettings.linkpurpose.purposeTelClass, 328 'link-purpose-tel', 329 ), 330 iconWrapperClass: sanitizeClass( 331 drupalSettings.linkpurpose.purposeTelIconWrapperClass, 332 'link-purpose-tel-icon', 333 ), 334 iconType: drupalSettings.linkpurpose.purposeTelIconType 335 ? drupalSettings.linkpurpose.purposeTelIconType 336 : 'html', // html, src or classes 337 iconPosition: drupalSettings.linkpurpose.purposeTelIconPosition 338 ? drupalSettings.linkpurpose.purposeTelIconPosition 339 : 'beforeend', 340 iconClasses: classArray( 341 drupalSettings.linkpurpose.purposeTelIconClasses, 342 ['fa-solid', 'fa-square-phone'], 343 ), 344 }, 345 }, 346 }; 347 348 if (typeof linkPurposeOptionsOverride !== 'undefined') { // eslint-disable-line 349 options = linkPurposeOptions(options); // eslint-disable-line 350 } 351 352 const linkPurpose = new LinkPurpose(options); //eslint-disable-line 353 } 354 }, 355 }; 356})(Drupal, drupalSettings); // eslint-disable-line no-undef
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.