Jump to content

MediaWiki:Common.js: Difference between revisions

From Insurer Brain
Content deleted Content added
No edit summary
No edit summary
Line 354: Line 354:
});
});


/* Collapsible inline footnotes ({{footnote}}) — click-triggered POPOVER.
/* Collapsible inline footnotes ({{footnote}}) — click-triggered card.
Clicking the "note" chip opens the note as a card. The card is
Clicking the "note" chip toggles .ed-fn-open; ALL positioning/visibility
is in Common.css (a fixed bottom-centre card), so the JS just flips the
position:FIXED and placed here from the chip's rect — that's required so
class — there is nothing to mis-place, so the card can never open to
it escapes the frozen first column's sticky stacking context and the
"nothing". One card open at a time; outside-click and Escape dismiss;
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
Enter/Space + aria-expanded for keyboard and screen readers. The note text
DOM at all times, so the bot reads it from the parsed HTML regardless.
stays in the DOM at all times, so the bot reads it from the parsed HTML. */
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 ) {
Line 378: Line 369:
$( this ).removeClass( 'ed-fn-open' )
$( 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 409: Line 382:
closeAll( $fn[ 0 ] );
closeAll( $fn[ 0 ] );
$fn.toggleClass( 'ed-fn-open', willOpen );
$fn.toggleClass( 'ed-fn-open', willOpen );
if ( willOpen ) {
placeCard( $fn[ 0 ], this );
} else {
clearCard( $fn[ 0 ] );
}
$( this ).attr( 'aria-expanded', willOpen ? 'true' : 'false' );
$( this ).attr( 'aria-expanded', willOpen ? 'true' : 'false' );
} );
} );


// outside click, Escape, or scroll dismiss (a fixed card can't follow the page)
// click outside any .ed-fn, or press Escape, to dismiss
$( 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 428: Line 396:
}
}
} );
} );
window.addEventListener( 'scroll', function () {
closeAll( null );
}, true );
} );
} );

Revision as of 17:14, 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 card.
   Clicking the "note" chip toggles .ed-fn-open; ALL positioning/visibility
   is in Common.css (a fixed bottom-centre card), so the JS just flips the
   class — there is nothing to mis-place, so the card can never open to
   "nothing". One card open at a time; outside-click and Escape dismiss;
   Enter/Space + aria-expanded for keyboard and screen readers. The note text
   stays in the DOM at all times, so the bot reads it from the parsed HTML. */
$( function () {
	$( '.ed-fn-chip' ).attr( { role: 'button', tabindex: 0, 'aria-expanded': 'false' } );

	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' );
			}
		} );
	}

	$( 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 );
		$( this ).attr( 'aria-expanded', willOpen ? 'true' : 'false' );
	} );

	// click outside any .ed-fn, or press Escape, to dismiss
	$( 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 );
		}
	} );
} );