Jump to content

MediaWiki:Common.js: Difference between revisions

From Insurer Brain
Content deleted Content added
No edit summary
No edit summary
Line 26: Line 26:


/**
/**
* @source [www.mediawiki.org/wiki/Snippets/Load_JS_and_CSS_by_URL](https://www.mediawiki.org/wiki/Snippets/Load_JS_and_CSS_by_URL)
* @source https://www.mediawiki.org/wiki/Snippets/Load_JS_and_CSS_by_URL
* @rev 6
* @rev 6
*/
*/
Line 355: Line 355:


/* Collapsible inline footnotes ({{footnote}}) — click-triggered POPOVER.
/* Collapsible inline footnotes ({{footnote}}) — click-triggered POPOVER.
Clicking the "note" chip opens the note as a card that floats above the
Clicking the "note" chip opens the note as a card. The card is
position:FIXED and placed here from the chip's rect — that's required so
text (CSS does the positioning); the note text stays in the DOM at all
it escapes the frozen first column's sticky stacking context and the
times, so the bot reads it from the parsed HTML regardless. Behaviour:
table's overflow clip (an absolute card would be hidden behind later
one card open at a time; .ed-fn-flip opens the card leftward when the
sticky cells / clipped by the scroll wrapper). The note text stays in the
chip sits in the right part of the viewport; outside-click and Escape
DOM at all times, so the bot reads it from the parsed HTML regardless.
dismiss; Enter/Space + aria-expanded for keyboard and screen readers. */
Behaviour: one card open at a time; the card flips above / clamps into the
viewport near an edge; mobile (<=768px) uses the CSS bottom sheet; outside
click, Escape, and scroll dismiss; Enter/Space + aria-expanded for
keyboard and screen readers. */
$( function () {
$( function () {
$( '.ed-fn-chip' ).attr( { role: 'button', tabindex: 0, 'aria-expanded': 'false' } );
$( '.ed-fn-chip' ).attr( { role: 'button', tabindex: 0, 'aria-expanded': 'false' } );

function clearCard( fn ) {
var body = fn.querySelector( '.ed-fn-body' );
if ( body ) { body.style.left = body.style.top = ''; }
}


function closeAll( except ) {
function closeAll( except ) {
$( '.ed-fn.ed-fn-open' ).each( function () {
$( '.ed-fn.ed-fn-open' ).each( function () {
if ( this !== except ) {
if ( this !== except ) {
$( this ).removeClass( 'ed-fn-open ed-fn-flip' )
$( this ).removeClass( 'ed-fn-open' )
.children( '.ed-fn-chip' ).attr( 'aria-expanded', 'false' );
.children( '.ed-fn-chip' ).attr( 'aria-expanded', 'false' );
clearCard( this );
}
}
} );
} );
}

function placeCard( fn, chip ) {
// mobile uses the CSS fixed bottom sheet — don't set inline coords
if ( window.innerWidth <= 768 ) { return; }
var card = fn.querySelector( '.ed-fn-body' );
if ( !card ) { return; }
card.style.left = 'auto';
card.style.top = 'auto'; // measure natural size first
var r = chip.getBoundingClientRect(),
cw = card.offsetWidth, ch = card.offsetHeight,
vw = window.innerWidth, vh = window.innerHeight,
left = r.left, top = r.bottom + 4;
if ( left + cw > vw - 8 ) { left = Math.max( 8, vw - 8 - cw ); } // clamp right
if ( top + ch > vh - 8 ) { top = Math.max( 8, r.top - 4 - ch ); } // flip above
card.style.left = left + 'px';
card.style.top = top + 'px';
}
}


Line 383: Line 410:
$fn.toggleClass( 'ed-fn-open', willOpen );
$fn.toggleClass( 'ed-fn-open', willOpen );
if ( willOpen ) {
if ( willOpen ) {
placeCard( $fn[ 0 ], this );
// open the card leftward when the chip is in the right 40% of the viewport
var rect = this.getBoundingClientRect();
$fn.toggleClass( 'ed-fn-flip', rect.left > window.innerWidth * 0.6 );
} else {
} else {
$fn.removeClass( 'ed-fn-flip' );
clearCard( $fn[ 0 ] );
}
}
$( this ).attr( 'aria-expanded', willOpen ? 'true' : 'false' );
$( this ).attr( 'aria-expanded', willOpen ? 'true' : 'false' );
} );
} );


// click anywhere outside an .ed-fn, or press Escape, to dismiss
// outside click, Escape, or scroll dismiss (a fixed card can't follow the page)
$( document ).on( 'click', function ( e ) {
$( document ).on( 'click', function ( e ) {
if ( !$( e.target ).closest( '.ed-fn' ).length ) {
if ( !$( e.target ).closest( '.ed-fn' ).length ) {
Line 403: Line 428:
}
}
} );
} );
window.addEventListener( 'scroll', function () {
closeAll( null );
}, true );
} );
} );

Revision as of 17:03, 13 June 2026

/* Any JavaScript here will be loaded for all users on every page load. */
/**
 * Keep code in MediaWiki:Common.js to a minimum as it is unconditionally
 * loaded for all users on every wiki page. If possible create a gadget that is
 * enabled by default instead of adding it here (since gadgets are fully
 * optimized ResourceLoader modules with possibility to add dependencies etc.)
 *
 * Since Common.js isn't a gadget, there is no place to declare its
 * dependencies, so we have to lazy load them with mw.loader.using on demand and
 * then execute the rest in the callback. In most cases these dependencies will
 * be loaded (or loading) already and the callback will not be delayed. In case a
 * dependency hasn't arrived yet it'll make sure those are loaded before this.
 */

/* global mw, $ */
/* jshint strict:false, browser:true */

mw.loader.using( [ 'mediawiki.util' ] ).done( function () {
	/* Begin of mw.loader.using callback */

	/**
	 * Map addPortletLink to mw.util
	 * @deprecated: Use mw.util.addPortletLink instead.
	 */
	mw.log.deprecate( window, 'addPortletLink', mw.util.addPortletLink, 'Use mw.util.addPortletLink instead' );

	/**
	 * @source https://www.mediawiki.org/wiki/Snippets/Load_JS_and_CSS_by_URL
	 * @rev 6
	 */
	var extraCSS = mw.util.getParamValue( 'withCSS' ),
		extraJS = mw.util.getParamValue( 'withJS' );

	if ( extraCSS ) {
		if ( extraCSS.match( /^MediaWiki:[^&<>=%#]*\.css$/ ) ) {
			mw.loader.load( '/w/index.php?title=' + extraCSS + '&action=raw&ctype=text/css', 'text/css' );
		} else {
			mw.notify( 'Only pages from the MediaWiki namespace are allowed.', { title: 'Invalid withCSS value' } );
		}
	}

	if ( extraJS ) {
		if ( extraJS.match( /^MediaWiki:[^&<>=%#]*\.js$/ ) ) {
			mw.loader.load( '/w/index.php?title=' + extraJS + '&action=raw&ctype=text/javascript' );
		} else {
			mw.notify( 'Only pages from the MediaWiki namespace are allowed.', { title: 'Invalid withJS value' } );
		}
	}

	/**
	 * Collapsible tables; reimplemented with mw-collapsible
	 * Styling is also in place to avoid FOUC
	 *
	 * Allows tables to be collapsed, showing only the header. See [[Help:Collapsing]].
	 * @version 3.0.0 (2018-05-20)
	 * @source https://www.mediawiki.org/wiki/MediaWiki:Gadget-collapsibleTables.js
	 * @author [[User:R. Koot]]
	 * @author [[User:Krinkle]]
	 * @author [[User:TheDJ]]
	 * @deprecated Since MediaWiki 1.20: Use class="mw-collapsible" instead which
	 * is supported in MediaWiki core. Shimmable since MediaWiki 1.32
	 *
	 * @param {jQuery} $content
	 */
	function makeCollapsibleMwCollapsible( $content ) {
		var $tables = $content
			.find( 'table.collapsible:not(.mw-collapsible)' )
			.addClass( 'mw-collapsible' );

		$.each( $tables, function ( index, table ) {
			// mw.log.warn( 'This page is using the deprecated class collapsible. Please replace it with mw-collapsible.');
			if ( $( table ).hasClass( 'collapsed' ) ) {
				$( table ).addClass( 'mw-collapsed' );
				// mw.log.warn( 'This page is using the deprecated class collapsed. Please replace it with mw-collapsed.');
			}
		} );
		if ( $tables.length > 0 ) {
			mw.loader.using( 'jquery.makeCollapsible' ).then( function () {
				$tables.makeCollapsible();
			} );
		}
	}
	mw.hook( 'wikipage.content' ).add( makeCollapsibleMwCollapsible );

	/**
	 * Add support to mw-collapsible for autocollapse, innercollapse and outercollapse
	 *
	 * Maintainers: TheDJ
	 */
	function mwCollapsibleSetup( $collapsibleContent ) {
		var $element,
			$toggle,
			autoCollapseThreshold = 2;
		$.each( $collapsibleContent, function ( index, element ) {
			$element = $( element );
			if ( $element.hasClass( 'collapsible' ) ) {
				$element.find( 'tr:first > th:first' ).prepend( $element.find( 'tr:first > * > .mw-collapsible-toggle' ) );
			}
			if ( $collapsibleContent.length >= autoCollapseThreshold && $element.hasClass( 'autocollapse' ) ) {
				$element.data( 'mw-collapsible' ).collapse();
			} else if ( $element.hasClass( 'innercollapse' ) ) {
				if ( $element.parents( '.outercollapse' ).length > 0 ) {
					$element.data( 'mw-collapsible' ).collapse();
				}
			}
			// because of colored backgrounds, style the link in the text color
			// to ensure accessible contrast
			$toggle = $element.find( '.mw-collapsible-toggle' );
			if ( $toggle.length ) {
				// Make the toggle inherit text color (Updated for T333357 2023-04-29)
				if ( $toggle.parent()[ 0 ].style.color ) {
					$toggle.css( 'color', 'inherit' );
					$toggle.find( '.mw-collapsible-text' ).css( 'color', 'inherit' );
				}
			}
		} );
	}

	mw.hook( 'wikipage.collapsibleContent' ).add( mwCollapsibleSetup );

	/* End of mw.loader.using callback */
} );

// CapSach — Sticky TOC overlay (UNRESTRICTED: Works on iPad/Desktop/Mobile)
(function () {

  // 1. REMOVED the "min-width: 768px" check. Now runs everywhere.

  // Only run on pages where it makes sense (Articles/MainPage)
  if (window.mw && mw.config && mw.config.get) {
    var isAllowed = mw.config.get('wgIsArticle') || mw.config.get('wgIsMainPage');
    if (!isAllowed) return;
  }

  // Find the content root; MobileFrontend restructures DOM, so be flexible
  var root =
    document.querySelector('#mw-content-text .mw-parser-output') ||
    document.querySelector('.mw-parser-output') ||
    document.getElementById('mw-content-text') ||
    document.querySelector('#content') ||
    document.body;

  // Collect headings (H2–H6). Prefer spans with .mw-headline (stable anchor ids)
  var items = [];
  var headings = root.querySelectorAll('h2, h3, h4, h5, h6');
  headings.forEach(function (h) {
    var level = parseInt(h.tagName.slice(1), 10);
    if (level < 2 || level > 6) return;
    var headline = h.querySelector('.mw-headline') || h;
    var id = headline.id || h.id;
    var text = (headline.textContent || h.textContent || '').trim();
    if (!id || !text) return;
    items.push({ id: id, text: text, level: level });
  });

  // Show only if there are enough headings to be useful
  // CHANGED: Lowered requirement to 1 heading so it always shows if there is any structure
  if (items.length < 1) return;

  // Create trigger button (bottom-left; avoids “Back to top” on bottom-right)
  var btn = document.createElement('button');
  btn.id = 'cps-open-toc';
  btn.type = 'button';
  btn.setAttribute('aria-label', 'Open table of contents');
  btn.innerHTML = '<span class="icon" aria-hidden="true">≡</span><span class="label">TOC</span>';
  document.body.appendChild(btn);

  // Overlay + panel
  var overlay = document.createElement('div');
  overlay.id = 'cps-toc-overlay';
  overlay.setAttribute('aria-hidden', 'true');

  var panel = document.createElement('div');
  panel.id = 'cps-toc-panel';
  panel.setAttribute('role', 'dialog');
  panel.setAttribute('aria-modal', 'true');
  panel.setAttribute('aria-label', 'Table of contents');

  var header = document.createElement('div');
  header.id = 'cps-toc-header';
  header.innerHTML =
    '<h2 id="cps-toc-title">Contents</h2>' +
    '<button id="cps-toc-close" type="button" aria-label="Close">×</button>';

  var list = document.createElement('ul');
  list.id = 'cps-toc-list';

  items.forEach(function (it) {
    var li = document.createElement('li');
    li.setAttribute('data-level', String(it.level));
    var a = document.createElement('a');
    a.href = '#' + it.id;
    a.textContent = it.text;
    li.appendChild(a);
    list.appendChild(li);
  });

  panel.appendChild(header);
  panel.appendChild(list);
  overlay.appendChild(panel);
  document.body.appendChild(overlay);

  // Focus handling
  var lastFocus = null;
  function openOverlay() {
    lastFocus = document.activeElement;
    overlay.classList.add('is-open');
    overlay.setAttribute('aria-hidden', 'false');
    document.body.style.overflow = 'hidden';
    // Focus first link for accessibility
    var firstLink = list.querySelector('a');
    if (firstLink) firstLink.focus({ preventScroll: true });
  }
  function closeOverlay() {
    overlay.classList.remove('is-open');
    overlay.setAttribute('aria-hidden', 'true');
    document.body.style.overflow = '';
    if (lastFocus && lastFocus.focus) lastFocus.focus({ preventScroll: true });
  }

  // Force button display immediately
  btn.style.display = 'flex';
  btn.addEventListener('click', openOverlay);

  overlay.addEventListener('click', function (e) {
    // Click outside the bottom sheet closes
    if (e.target === overlay) closeOverlay();
  });
  overlay.querySelector('#cps-toc-close').addEventListener('click', closeOverlay);

  overlay.addEventListener('keydown', function (e) {
    if (e.key === 'Escape') closeOverlay();
  });

  // Navigate and try to ensure mobile-collapsed sections are visible
// Navigate and try to ensure mobile-collapsed sections are visible
  list.addEventListener('click', function (e) {
    var a = e.target.closest('a');
    if (!a) return;
    e.preventDefault();

    var targetId = a.getAttribute('href').slice(1);

    // === NEW LOGIC START: Scroll to Top for "Contents" ===
    // If the user clicks the "Contents" header (id="mw-toc-heading"), scroll to top (0,0)
// === FIXED CODE ===
if (targetId === 'mw-toc-heading') {
   closeOverlay();
   // Delay scroll to let iOS Safari process the overflow change
   setTimeout(function() {
       try {
           window.scrollTo({ top: 0, behavior: 'smooth' });
       } catch (e) {
           window.scrollTo(0, 0);
       }
       // Fallback for older iOS Safari
       document.documentElement.scrollTop = 0;
       document.body.scrollTop = 0;
   }, 100);
   if (history.replaceState) {
       history.replaceState(null, '', window.location.pathname + window.location.search);
   }
   return;
}
    // === NEW LOGIC END ===

    var target = document.getElementById(targetId);
    closeOverlay();

    if (target) {
      try {
        target.scrollIntoView({ behavior: 'smooth', block: 'start' });
      } catch (_) {
        target.scrollIntoView(true);
      }

      // Update URL hash after a tick (so browser back works)
      setTimeout(function () {
        if (history && history.replaceState) {
          history.replaceState(null, '', '#' + targetId);
        } else {
          location.hash = targetId;
        }
      }, 200);

      // MobileFrontend: headings may be inside collapsed sections.
      // Heuristic: click the nearest toggle if present.
      var maybeToggle = target.closest('.collapsible-block, .mf-section') ||
                        target.closest('section');
      if (maybeToggle && maybeToggle.classList.contains('collapsed')) {
        // Try to open; fallback by clicking the first heading inside
        var headingToggle = maybeToggle.querySelector('.section-heading, h2, h3, h4, h5, h6');
        if (headingToggle) headingToggle.click();
      }
    }
  });

  // 2. REMOVED the "resize" event listener that was hiding the button.
  // The button now persists on all screen sizes.
})();

/* Script for Inline Expandable Template */
$(function() {
    $('.inline-expand-trigger').on('click', function() {
        // 1. Toggle the content visibility
        $(this).next('.inline-expand-content').toggle();

        // 2. Toggle the arrow icon
        const currentText = $(this).text();
        $(this).text(
            currentText.includes('▸') ? currentText.replace('▸', '◂') : currentText.replace('◂', '▸')
        );
    });
});

$(document).ready(function() {
    // Check if the button already exists to prevent duplicates
    if ($('#custom-email-btn').length === 0) {

        // Create the email button element
        var emailBtn = $('<a>', {
            id: 'custom-email-btn',
            href: 'mailto:bananabot@axabrain.com',
            // Simple accessible title
            title: 'Contact AXA BRAIN Services'
        });

        // Add it to the body of the page
        $('body').append(emailBtn);
    }
});

/* Open AXA BRAIN AI Assistant when clicking the logo */
$(document).ready(function() {
    $('.fullscreen-logo').css('cursor', 'pointer').click(function(e) {
        e.preventDefault();

        // Method 1: Click the AI Assistant floating icon
        var $aiButton = $('img[src*="ai-icon.png"]').closest('div, button, a');
        if ($aiButton.length > 0) {
            $aiButton.trigger('click');
            return;
        }

        // Method 2: Try the extension's trigger class
        var $trigger = $('.ext-aiassistant-trigger, .ext-aiassistant');
        if ($trigger.length > 0) {
            $trigger.first().trigger('click');
            return;
        }

        console.log("AXA BRAIN Assistant button not found on this page.");
    });
});

/* Collapsible inline footnotes ({{footnote}}) — click-triggered POPOVER.
   Clicking the "note" chip opens the note as a card. The card is
   position:FIXED and placed here from the chip's rect — that's required so
   it escapes the frozen first column's sticky stacking context and the
   table's overflow clip (an absolute card would be hidden behind later
   sticky cells / clipped by the scroll wrapper). The note text stays in the
   DOM at all times, so the bot reads it from the parsed HTML regardless.
   Behaviour: one card open at a time; the card flips above / clamps into the
   viewport near an edge; mobile (<=768px) uses the CSS bottom sheet; outside
   click, Escape, and scroll dismiss; Enter/Space + aria-expanded for
   keyboard and screen readers. */
$( function () {
	$( '.ed-fn-chip' ).attr( { role: 'button', tabindex: 0, 'aria-expanded': 'false' } );

	function clearCard( fn ) {
		var body = fn.querySelector( '.ed-fn-body' );
		if ( body ) { body.style.left = body.style.top = ''; }
	}

	function closeAll( except ) {
		$( '.ed-fn.ed-fn-open' ).each( function () {
			if ( this !== except ) {
				$( this ).removeClass( 'ed-fn-open' )
					.children( '.ed-fn-chip' ).attr( 'aria-expanded', 'false' );
				clearCard( this );
			}
		} );
	}

	function placeCard( fn, chip ) {
		// mobile uses the CSS fixed bottom sheet — don't set inline coords
		if ( window.innerWidth <= 768 ) { return; }
		var card = fn.querySelector( '.ed-fn-body' );
		if ( !card ) { return; }
		card.style.left = 'auto';
		card.style.top = 'auto';                 // measure natural size first
		var r = chip.getBoundingClientRect(),
			cw = card.offsetWidth, ch = card.offsetHeight,
			vw = window.innerWidth, vh = window.innerHeight,
			left = r.left, top = r.bottom + 4;
		if ( left + cw > vw - 8 ) { left = Math.max( 8, vw - 8 - cw ); }  // clamp right
		if ( top + ch > vh - 8 ) { top = Math.max( 8, r.top - 4 - ch ); } // flip above
		card.style.left = left + 'px';
		card.style.top = top + 'px';
	}

	$( document ).on( 'click keydown', '.ed-fn-chip', function ( e ) {
		if ( e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ' ) {
			return;
		}
		e.preventDefault();
		var $fn = $( this ).closest( '.ed-fn' ),
			willOpen = !$fn.hasClass( 'ed-fn-open' );
		closeAll( $fn[ 0 ] );
		$fn.toggleClass( 'ed-fn-open', willOpen );
		if ( willOpen ) {
			placeCard( $fn[ 0 ], this );
		} else {
			clearCard( $fn[ 0 ] );
		}
		$( this ).attr( 'aria-expanded', willOpen ? 'true' : 'false' );
	} );

	// outside click, Escape, or scroll dismiss (a fixed card can't follow the page)
	$( document ).on( 'click', function ( e ) {
		if ( !$( e.target ).closest( '.ed-fn' ).length ) {
			closeAll( null );
		}
	} );
	$( document ).on( 'keydown', function ( e ) {
		if ( e.key === 'Escape' ) {
			closeAll( null );
		}
	} );
	window.addEventListener( 'scroll', function () {
		closeAll( null );
	}, true );
} );