1/** 2 * @package Regular Labs Library 3 * @version 26.9.10756 4 * 5 * @author Peter van Westen <[email protected]> 6 * @link https://regularlabs.com 7 * @copyright Copyright © 2026 Regular Labs All Rights Reserved 8 * @license GNU General Public License version 2 or later 9 */ 10 11(function() { 12 'use strict'; 13 14 window.RegularLabs = window.RegularLabs || {}; 15 16 window.RegularLabs.Scripts = window.RegularLabs.Scripts || { 17 version: '26.9.10756', 18 19 ajax_list : [], 20 started_ajax_list : false, 21 ajax_list_timer : null, 22 editor_api : null, 23 editor_api_loading : null, 24 legacy_editors : null, 25 editor_selections : {}, 26 27 escapeHtml: function(value) { 28 return String(value) 29 .replace(/&/g, '&') 30 .replace(/</g, '<') 31 .replace(/>/g, '>'); 32 }, 33 34 escapeAttributeValue: function(value) { 35 return this.escapeHtml(value) 36 .replace(/"/g, '"'); 37 }, 38 39 escapeTagAttributeValue: function(value, use_backslash_quotes = false) { 40 const quote = use_backslash_quotes ? '\\"' : '"'; 41 42 return this.escapeHtml(value) 43 .replace(/"/g, quote); 44 }, 45 46 getOptions: function(key, default_value = {}) { 47 if (typeof Joomla !== 'undefined' && typeof Joomla.getOptions !== 'undefined') { 48 return Joomla.getOptions(key, default_value); 49 } 50 51 if (typeof Joomla === 'undefined' || typeof Joomla.optionsStorage === 'undefined') { 52 return default_value; 53 } 54 55 return typeof Joomla.optionsStorage[key] === 'undefined' 56 ? default_value 57 : Joomla.optionsStorage[key]; 58 }, 59 60 parseJson: function(value, default_value = null) { 61 if (typeof value !== 'string' || value === '') { 62 return default_value; 63 } 64 65 try { 66 return JSON.parse(value); 67 } catch (error) { 68 return default_value; 69 } 70 }, 71 72 parseJsonObject: function(value, default_value = {}) { 73 const parsed = this.parseJson(value, default_value); 74 75 if ( ! parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { 76 return default_value; 77 } 78 79 return parsed; 80 }, 81 82 getEditor: function(editor) { 83 if (typeof editor !== 'string') { 84 return editor; 85 } 86 87 if (this.editor_api) { 88 const api_editor = this.editor_api.get(editor); 89 90 if (api_editor) { 91 return api_editor; 92 } 93 } 94 95 if (this.legacy_editors) { 96 return this.legacy_editors[editor] || null; 97 } 98 99 return typeof Joomla !== 'undefined' 100 ? Joomla.editors?.instances?.[editor] || null 101 : null; 102 }, 103 104 getEditors: function() { 105 if ( ! this.editor_api) { 106 return this.legacy_editors || {}; 107 } 108 109 const editors = {}; 110 111 document.querySelectorAll('textarea[id]').forEach((element) => { 112 const editor = this.editor_api.get(element.id); 113 114 if (editor) { 115 editors[element.id] = editor; 116 } 117 }); 118 119 const active_editor = this.editor_api.getActive(); 120 121 if (active_editor) { 122 editors[this.getEditorId(active_editor)] = active_editor; 123 } 124 125 return editors; 126 }, 127 128 initEditorApi: function() { 129 if (this.editor_api_loading || typeof Joomla === 'undefined') { 130 return; 131 } 132 133 this.editor_api_loading = import('editor-api') 134 .then(({JoomlaEditor}) => { 135 this.editor_api = JoomlaEditor; 136 }) 137 .catch(() => { 138 this.legacy_editors = Joomla.editors && Joomla.editors.instances 139 ? Joomla.editors.instances 140 : {}; 141 }); 142 }, 143 144 getEditorValue: function(editor) { 145 return this.getEditor(editor)?.getValue(); 146 }, 147 148 getEditorFieldValue: function(element) { 149 const editor_value = this.getEditorValue(element.name || element.i
149d); 150 151 if (typeof editor_value !== 'undefined') { 152 return editor_value; 153 } 154 155 const code_mirror = element.closest('joomla-editor-codemirror')?.querySelector('.CodeMirror'); 156 157 return code_mirror?.CodeMirror?.getValue(); 158 }, 159 160 setEditorValue: function(editor, value) { 161 this.getEditor(editor)?.setValue(value); 162 }, 163 164 isEditorSourceMode: function(editor) { 165 editor = this.getEditor(editor); 166 167 if ( ! editor) { 168 return false; 169 } 170 171 const source_instance = editor.instance?.plugins?.source; 172 173 return Boolean( 174 source_instance 175 && typeof source_instance.isHidden === 'function' 176 && ! source_instance.isHidden() 177 ); 178 }, 179 180 isJceEditor: function(editor) { 181 editor = this.getEditor(editor); 182 183 if ( ! editor) { 184 return false; 185 } 186 187 const source_instance = editor.instance?.plugins?.source; 188 189 return Boolean(source_instance && typeof source_instance.isHidden === 'function'); 190 }, 191 192 getEditorId: function(editor) { 193 editor = this.getEditor(editor); 194 195 if ( ! editor) { 196 return ''; 197 } 198 199 if (typeof editor.id === 'function') { 200 return editor.id(); 201 } 202 203 return editor.id || editor.instance?.id || editor.instance?.editor?.id || ''; 204 }, 205 206 getEditorSourceElement: function(editor) { 207 editor = this.getEditor(editor); 208 209 if ( ! this.isEditorSourceMode(editor)) { 210 return null; 211 } 212 213 return editor.instance.getElement().ownerDocument.getElementById( 214 editor.instance.id + '_editor_source_textarea' 215 ); 216 }, 217 218 getEditorTextarea: function(editor) { 219 editor = this.getEditor(editor); 220 221 if ( ! editor) { 222 return null; 223 } 224 225 const source_element = this.getEditorSourceElement(editor); 226 227 if (source_element) { 228 return source_element; 229 } 230 231 const instance_element = editor.instance?.editor; 232 233 if (instance_element && typeof instance_element.selectionStart !== 'undefined') { 234 return instance_element; 235 } 236 237 if (typeof editor.instance?.getElement !== 'function') { 238 return null; 239 } 240 241 const editor_element = editor.instance.getElement(); 242 243 return editor_element?.classList?.contains('wf-no-editor') 244 ? editor_element 245 : null; 246 }, 247 248 captureEditorSelection: function(editor) { 249 const editor_name = typeof editor === 'string' ? editor : this.getEditorId(editor); 250 editor = this.getEditor(editor); 251 252 if ( ! editor) { 253 return; 254 } 255 256 const editor_id = this.getEditorId(editor) || editor_name; 257 258 if ( ! editor_id) { 259 return; 260 } 261 262 if (this.editor_selections[editor_id]?.preserve) { 263 return; 264 } 265 266 const textarea = this.getEditorTextarea(editor); 267 268 if (textarea) { 269 this.editor_selections[editor_id] = { 270 end : textarea.selectionEnd, 271 start: textarea.selectionStart, 272 value: textarea.value.substring( 273 textarea.selectionStart, 274 textarea.selectionEnd 275 ), 276 }; 277 return; 278 } 279 280 if (typeof editor.instance?.selection !== 'undefined') { 281 this.editor_selections[editor_id] = { 282 bookmark: editor.instance.selection.getBookmark(2, true), 283 html : editor.instance.selection.getContent(), 284 range : editor.instance.selection.getRng().cloneRange(), 285 value : editor.instance.selection.getContent({format: 'text'}), 286 }; 287 return; 288 } 289 290 const fallback_textarea = document.getElementById(editor_name); 291 292 if (fallback_textarea && typeof fallback_textarea.selectionStart !== 'undefined') { 293 this.editor_selections[editor_id] = { 294 end : fallback_textarea.selectionEnd, 295 start: fallback_textarea.selectionStart, 296 value: fallback_textarea.value.substring( 297 fallback_textarea.selectionStart, 298 fallback_textarea.selectionEnd 299 ), 300 }; 301 } 302 }, 303 304 selectEditorText: function(editor, text) { 305 const editor_name = typeof editor === 'string' ? editor : this.getEditorId(editor); 306 editor = this.getEditor(editor); 307 308 if ( ! editor) { 309 return ''; 310 } 311 312 const body = editor.instance?.getBody?.(); 313 const selection_element = this.getEditorTextarea(editor) 314 || (body ? null : document.getElementById(editor_name)); 315 316 if (selection_element && typeof selection_element.selectionStart !== 'undefined') { 317 const start = selection_element.value.indexOf(text); 318 319 if (start === -1) { 320 return ''; 321 } 322 323 selection_element.focus(); 324 selection_element.setSelectionRange(start, start + text.length); 325 this.editor_selections[this.getEditorId(editor) || editor_name] = { 326 end : start + text.length, 327 preserve: true, 328 start : start, 329 value : text, 330 }; 331 332 return selection_element.value.substring( 333 selection_element.selectionStart, 334 selection_element.selectionEnd 335 ); 336 } 337
338 if ( ! body || typeof editor.instance?.selection === 'undefined') { 339 return ''; 340 } 341 342 const editor_document = body.ownerDocument; 343 const walker = editor_document.createTreeWalker( 344 body, 345 editor_document.defaultView.NodeFilter.SHOW_TEXT 346 ); 347 let text_node = null; 348 349 while (walker.nextNode()) { 350 if (walker.currentNode.data.includes(text)) { 351 text_node = walker.currentNode; 352 break; 353 } 354 } 355 356 if ( ! text_node) { 357 return ''; 358 } 359 360 const start = text_node.data.indexOf(text); 361 const range = editor_document.createRange(); 362 range.setStart(text_node, start); 363 range.setEnd(text_node, start + text.length); 364 editor.instance.focus(); 365 editor.instance.selection.setRng(range); 366 const html_container = editor_document.createElement('div'); 367 html_container.appendChild(range.cloneContents()); 368 this.editor_selections[this.getEditorId(editor) || editor_name] = { 369 html : html_container.innerHTML, 370 preserve: true, 371 range : range.cloneRange(), 372 value : text, 373 }; 374 375 return text; 376 }, 377 378 isEditorActive: function(editor) { 379 editor = this.getEditor(editor); 380 381 if ( ! editor) { 382 return false; 383 } 384 385 const textarea = this.getEditorTextarea(editor); 386 387 if (textarea) { 388 return textarea.ownerDocument.activeElement === textarea; 389 } 390 391 if (typeof editor.instance?.hasFocus === 'function') { 392 if (editor.instance.hasFocus()) { 393 return true; 394 } 395 } 396 397 if (editor.instance?.focused) { 398 return true; 399 } 400 401 if (typeof editor.instance?.getDoc === 'function' && editor.instance.getDoc()?.hasFocus()) { 402 return true; 403 } 404 405 if (typeof editor.instance?.getContentAreaContainer === 'function') { 406 const iframe = editor.instance.getContentAreaContainer()?.querySelector('iframe'); 407 408 if (iframe && iframe.ownerDocument.activeElement === iframe) { 409 return true; 410 } 411 } 412 413 const fallback_textarea = editor.instance?.editor; 414 415 return Boolean( 416 fallback_textarea 417 && fallback_textarea.ownerDocument.activeElement === fallback_textarea 418 ); 419 }, 420 421 captureActiveEditorSelections: function() { 422 if (document.querySelector( 423 '.joomla-modal.show iframe[src*="Plugin.EditorButton."], ' 424 + 'dialog[open] iframe[src*="Plugin.EditorButton."]' 425 )) { 426 return; 427 } 428 429 if (this.editor_api) { 430 const editor = this.editor_api.getActive(); 431 432 if (editor) { 433 this.captureEditorSelection(editor); 434 } 435 436 return; 437 } 438 439 Object.values(this.legacy_editors || {}).forEach((editor) => { 440 if (this.isEditorActive(editor)) { 441 this.captureEditorSelection(editor); 442 } 443 }); 444 }, 445 446 captureEditorSelections: function(only_untracked = false) { 447 Object.values(this.getEditors()).forEach((editor) => { 448 if (only_untracked && this.editor_selections[this.getEditorId(editor)]) { 449 return; 450 } 451 452 this.captureEditorSelection(editor); 453 }); 454 }, 455 456 initEditorSelectionTracking: function() { 457 if (document.documentElement.dataset.rlEditorSelectionTracking === 'true') { 458 return; 459 } 460 461 document.documentElement.dataset.rlEditorSelectionTracking = 'true'; 462 this.initEditorApi(); 463 document.addEventListener('pointerdown', (event) => { 464 this.captureEditorSelectionFromEvent(event); 465 }, true); 466 document.addEventListener('keydown', (event) => { 467 if (event.key !== 'Enter' && event.key !== ' ') { 468 return; 469 } 470 471 this.captureEditorSelectionFromEvent(event); 472 }, true); 473 }, 474 475 captureEditorSelectionFromEvent: function(event) { 476 if ( ! this.isEditorButtonElement(event.target)) { 477 return; 478 } 479 480 this.captureActiveEditorSelections(); 481 this.captureEditorSelections(true); 482 }, 483 484 isEditorButtonElement: function(target) { 485 const element = target instanceof Element ? target.closest('button, [role="menuitem"]') : null; 486 487 if ( ! element) { 488 return false;
489 } 490 491 const is_regular_labs_button = element.matches( 492 '[data-joomla-editor-button-options*="Plugin.EditorButton."]' 493 ); 494 const is_marked_editor_button = element.matches('[data-rl-editor-button]'); 495 const is_legacy_editor_button = Boolean(element.closest('.editor-xtd-buttons')); 496 const is_editor_button_menu = element.getAttribute('data-mce-name') === 'jxtdbuttons' 497 || element.getAttribute('aria-label') === 'CMS Content'; 498 499 if ( 500 ! is_regular_labs_button 501 && ! is_marked_editor_button 502 && ! is_legacy_editor_button 503 && ! is_editor_button_menu 504 ) { 505 return false; 506 } 507 508 return true; 509 }, 510 511 getEditorSelection: function(editor, preserve_html = false) { 512 editor = this.getEditor(editor); 513 514 if ( ! editor) { 515 return ''; 516 } 517 518 const textarea = this.getEditorTextarea(editor); 519 520 if (this.isEditorActive(editor)) { 521 if (textarea) { 522 return textarea.value.substring( 523 textarea.selectionStart, 524 textarea.selectionEnd 525 ); 526 } 527 528 if (preserve_html && typeof editor.instance?.selection !== 'undefined') { 529 return editor.instance.selection.getContent(); 530 } 531 532 return editor.getSelection(); 533 } 534 535 const selection = this.editor_selections[this.getEditorId(editor)]; 536 if (selection) { 537 if (preserve_html && typeof selection.html !== 'undefined') { 538 return selection.html; 539 } 540 541 if (typeof selection.value !== 'undefined') { 542 return selection.value; 543 } 544 } 545 546 if (textarea) { 547 return textarea.value.substring( 548 textarea.selectionStart, 549 textarea.selectionEnd 550 ); 551 } 552 553 if (preserve_html && typeof editor.instance?.selection !== 'undefined') { 554 return editor.instance.selection.getContent(); 555 } 556 557 return editor.getSelection(); 558 }, 559 560 replaceEditorSelection: function(editor, value) { 561 editor = this.getEditor(editor); 562 563 if ( ! editor) { 564 return; 565 } 566 567 const editor_id = this.getEditorId(editor); 568 const selection = this.editor_selections[editor_id]; 569 const selection_element = this.getEditorTextarea(editor); 570 571 if ( 572 selection?.range 573 && ! selection_element 574 && typeof editor.instance?.selection !== 'undefined' 575 ) { 576 editor.instance.focus(); 577 editor.instance.selection.setRng(selection.range); 578 } else if ( 579 selection?.bookmark 580 && ! selection_element 581 && typeof editor.instance?.selection !== 'undefined' 582 ) { 583 editor.instance.focus(); 584 editor.instance.selection.moveToBookmark(selection.bookmark); 585 } else if (typeof selection?.start !== 'undefined') { 586 if (selection_element) { 587 selection_element.focus(); 588 selection_element.setSelectionRange(selection.start, selection.end); 589 } 590 } 591 592 if ( 593 selection?.range 594 && ! selection_element 595 && this.isJceEditor(editor) 596 && ! this.isEditorSourceMode(editor) 597 ) { 598 editor.instance.execCommand('mceInsertContent', false, value); 599 delete this.editor_selections[editor_id]; 600 return; 601 } 602 603 editor.replaceSelection(value); 604 delete this.editor_selections[editor_id]; 605 }, 606 607 isEditorReady: function(editor) { 608 editor = this.getEditor(editor); 609 610 if ( ! editor || typeof editor.getValue !== 'function') { 611 return false;
612 } 613 614 if (typeof editor.instance?.initialized === 'boolean' && ! editor.instance.initialized) { 615 return false; 616 } 617 618 try { 619 editor.getValue(); 620 return true; 621 } catch (error) { 622 return false; 623 } 624 }, 625 626 normalizeEditorInsertionValue: function(editor, value) { 627 editor = this.getEditor(editor); 628 629 if ( ! editor) { 630 return value; 631 } 632 633 if ( 634 editor.className !== 'mce_editable' 635 || value.substring(0, 3) !== '<p>' 636 || value.substring(value.length - 4) !== '</p>' 637 ) { 638 return value; 639 } 640 641 return value.substring(3, value.length - 4); 642 }, 643 644 applyEditorFormatting: function(editor_name) { 645 if (typeof tinyMCE === 'undefined') { 646 return; 647 } 648 649 tinyMCE.get(editor_name)?.formatter?.apply(); 650 }, 651 652 prepareOutputForEditor: function(string, editor, remove_all_p_tags = false) { 653 editor = this.getEditor(editor); 654 655 if ( ! editor) { 656 return string; 657 } 658 659 const editor_content = editor.getValue(); 660 const editor_selection = this.getEditorSelection(editor); 661 662 // If the editor is CodeMirror 663 if (editor_content === '' || editor_content[0] !== '<') { 664 return remove_all_p_tags ? string.replace(/<\/?p>/g, '') : string; 665 } 666 667 // If selection is empty or code is replacing a selection not starting with a html tag 668 if (editor_selection.indexOf('<') !== 0) { 669 // remove surrounding p tags 670 return string.replace(/^<p>(.*)<\/p>$/g, '$1'); 671 } 672 673 return string; 674 }, 675 676 getFormValue: function(form, id, default_value = '', getEditorContent = null) { 677 let elements = form.querySelectorAll('[name="' + id + '"]'); 678 679 if ( ! elements.length) { 680 elements = form.querySelectorAll('[name="' + id + '[]"]'); 681 } 682 683 if ( ! elements.length) { 684 return default_value; 685 } 686 687 const element = elements[0]; 688 689 if (element.type === 'textarea') { 690 const value = getEditorContent ? getEditorContent(element) : element.value; 691 692 return this.fixValueType(value); 693 } 694 695 let value = element.value ? element.value : default_value; 696 697 if (element.type === 'select-one') { 698 if (element.type === 'checkbox' && ! element.checked) { 699 return default_value; 700 } 701 702 return this.fixValueType(value); 703 } 704 705 if (element.type === 'select-multiple') { 706 value = []; 707 708 for (let i = 0; i < element.options.length; i++) { 709 if (element.options[i].selected && element.options[i].value !== '') { 710 value.push(element.options[i].value); 711 } 712 } 713 714 return this.fixValueType(value); 715 } 716 717 if (elements.length > 1) { 718 value = []; 719 720 for (let i = 0; i < elements.length; i++) { 721 if ((elements[i].selected || elements[i].checked) && elements[i].value !== '') { 722 value.push(elements[i].value); 723 } 724 } 725 726 if (element.type === 'radio') { 727 return this.fixValueType(value[0]); 728 } 729 730 return this.fixValueType(value); 731 } 732 733 return this.fixValueType(value); 734 }, 735 736 fixValueType: function(value) { 737 if (Array.isArray(value)) { 738 return value.map((val) => this.fixValueType(val)); 739 } 740 741 if (isNaN(value) || isNaN(parseInt(value))) { 742 return value; 743 } 744 745 return Number(value); 746 }, 747 748 loadAjax: function(url, success, fail, query, timeout, dataType, cache) { 749 if (url.indexOf('index.php') !== 0 && url.indexOf('administrator/index.php') !== 0) { 750 url = url.replace('http://', ''); 751 url = `index.php?rl_qp=1&url=${encodeURIComponent(url)}`; 752 if (timeout) { 753 url += `&timeout=${timeout}`; 754 } 755 if (cache) { 756 url += `&cache=${cache}`; 757 } 758 } 759 760 let base = window.location.pathname; 761 762 base = base.substring(0, base.lastIndexOf('/')); 763 764 if ( 765 typeof Joomla !== 'undefined' 766 && typeof Joomla.getOptions !== 'undefined' 767 && Joomla.getOptions('system.paths') 768 ) { 769 base = Joomla.getOptions('system.paths').base; 770 } 771 772 const token = typeof Joomla !== 'undefined' && typeof Joomla.getOptions === 'function' 773 ? Joomla.getOptions('csrf.token', '') 774 : ''; 775 const headers = token ? {'X-CSRF-Token': token} : {}; 776 777 // console.log(url); 778 // console.log(`${base}/${url}`); 779 780 this.loadUrl( 781 `${base}/${url}`, 782 query, 783 (function(data) { 784 if (success) { 785 success = `data = data ? data : ''; ${success};`.replace(/;\s*;/g, ';'); 786 eval(success); 787 } 788 }), 789 (function(data) { 790 if (fail) { 791 fail = `data = data ? data : ''; ${fail};`.replace(/;\s*;/g, ';'); 792 eval(fail); 793 } 794 }), 795 headers 796 ); 797 }, 798 799 /**
800 * Loads a url with optional POST data and optionally calls a function on success or fail. 801 * 802 * @param url String containing the url to load. 803 * @param data Optional string representing the POST data to send along. 804 * @param success Optional callback function to execute when the url loads successfully (status 200). 805 * @param fail Optional callback function to execute when the url fails to load. 806 * @param headers Optional request headers keyed by name. 807 */ 808 loadUrl: function(url, data, success, fail, headers = {}) { 809 return new Promise((resolve) => { 810 const request = new XMLHttpRequest(); 811 812 request.open("POST", url, true); 813 814 request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 815 816 for (const [headerName, headerValue] of Object.entries(headers)) { 817 request.setRequestHeader(headerName, headerValue); 818 } 819 820 request.onreadystatechange = function() { 821 if (this.readyState !== 4) { 822 return; 823 } 824 825 if (this.status !== 200) { 826 fail && fail.call(null, this.responseText, this.status, this); 827 resolve(this); 828 return; 829 } 830 831 success && success.call(null, this.responseText, this.status, this); 832 resolve(this); 833 }; 834 835 request.send(data); 836 }); 837 }, 838 839 addToLoadAjaxList: function(url, success, error, query = '') { 840 // wrap inside the loadajax function (and escape string values) 841 url = url.replace(/'/g, "\\'"); 842 success = success.replace(/'/g, "\\'"); 843 error = error.replace(/'/g, "\\'"); 844 query = query.replace(/'/g, "\\'"); 845 846 const action = `RegularLabs.Scripts.loadAjax( 847 '${url}', 848 '${success};RegularLabs.Scripts.ajaxRun();', 849 '${error};RegularLabs.Scripts.ajaxRun();', 850 '${query}' 851 )`; 852 853 this.addToAjaxList(action); 854 }, 855 856 addToAjaxList: function(action) { 857 this.ajax_list.push(action); 858 859 if ( ! this.started_ajax_list) { 860 this.ajaxRun(); 861 } 862 }, 863 864 ajaxRun: function() { 865 if ( ! this.ajax_list.length) { 866 return; 867 } 868 869 clearTimeout(this.ajax_list_timer); 870 871 this.started_ajax_list = true; 872 873 const action = this.ajax_list.shift(); 874 875 eval(`${action};`); 876 877 if ( ! this.ajax_list.length) { 878 this.started_ajax_list = false; 879 return; 880 } 881 882 // Re-trigger this ajaxRun function just in case it hangs somewhere 883 this.ajax_list_timer = setTimeout( 884 function() { 885 RegularLabs.Scripts.ajaxRun(); 886 }, 887 5000 888 ); 889 }, 890 }; 891 892 RegularLabs.Scripts.initEditorSelectionTracking(); 893})();
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.