From Official Arms of God Wiki

bot: hover-infobox tooltips + comparison filter chips (+ library tab/collapsible init)
 
bot: Expand all / Collapse all controls for Help catalogues
 
Line 1: Line 1:
/* WM-BLOCK-BEGIN armsofgod-interactive */
/*
/*
  * MediaWiki:Common.js — baseline JS.
  * MediaWiki:Common.js — baseline JS.
Line 158: Line 157:
  *    common with the active chips of that group. Groups AND together;
  *    common with the active chips of that group. Groups AND together;
  *    chips within a group OR together. Reset clears everything.
  *    chips within a group OR together. Reset clears everything.
*
* 3. Catalogue expand/collapse-all: .wm-expand-all / .wm-collapse-all
*    buttons inside a .wm-catalogue wrapper (Help:Wiki Editing module /
*    template / data-schema catalogues) expand or collapse every
*    jquery.makeCollapsible table inside THAT wrapper only.
  *
  *
  * ES5 only, jQuery + mw provided by MediaWiki. Dark styling lives in
  * ES5 only, jQuery + mw provided by MediaWiki. Dark styling lives in
  * MediaWiki:Common.css (.wm-tipbox / .wm-chip / .wm-row-hidden).
  * MediaWiki:Common.css (.wm-tipbox / .wm-chip / .wm-row-hidden /
* .wm-catalogue-controls).
  */
  */
( function ( $ ) {
( function ( $ ) {
Line 168: Line 173:
initTipPopups();
initTipPopups();
initFilterChips();
initFilterChips();
initCatalogueControls();
} );
} );


Line 381: Line 387:
}
}
}
}
}
/* ------------------------------- catalogue expand/collapse all ---- */
function initCatalogueControls() {
$( document ).on( 'click', '.wm-expand-all, .wm-collapse-all', function ( e ) {
e.preventDefault();
var expand = $( this ).hasClass( 'wm-expand-all' );
var $scope = $( this ).closest( '.wm-catalogue' );
if ( !$scope.length ) {
return;
}
// makeCollapsible ships with core page rendering, but wait for
// the module anyway so the data API is guaranteed initialized.
mw.loader.using( 'jquery.makeCollapsible' ).then( function () {
$scope.find( '.mw-collapsible' ).each( function () {
var $el = $( this );
var api = $el.data( 'mw-collapsible' );
if ( api && api.expand && api.collapse ) {
if ( expand ) {
api.expand();
} else {
api.collapse();
}
return;
}
// Fallback: synthetic toggle click, but only when the
// current collapsed state mismatches the target so it
// never flips a table the wrong way.
var collapsed = $el.hasClass( 'mw-collapsed' );
if ( collapsed === expand ) {
$el.find( '.mw-collapsible-toggle' ).first().trigger( 'click' );
}
} );
} );
} );
}
}


}( jQuery ) );
}( jQuery ) );
/* WM-BLOCK-END armsofgod-interactive */

Latest revision as of 16:21, 10 June 2026

/*
 * MediaWiki:Common.js — baseline JS.
 *
 * Ships unchanged across all games. No substitution slots — this file is fully
 * generic. Per-game customizations layer on top by editing
 * MediaWiki:Common.js/<gameslug>.js on the live wiki (manually, not pipeline-managed).
 *
 * SCOPE DISCIPLINE (mirrors MediaWiki:Common.css):
 *   - Tab init for .wm-tabs (forward-looking — Phase 12 catalogue #1 variant-rarity
 *     tabbed infoboxes will emit elements with these data attributes)
 *   - Collapsible-section toggle for .wm-collapsible (alternative to MediaWiki's
 *     built-in mw-collapsible when we want consistent class names)
 *   - NO business logic, NO third-party libs, NO modal/popup chrome
 *
 * Graceful degradation: every interactive piece must be functional WITHOUT JS
 * (server-rendered content is what readers see; JS adds interactivity, never
 * gates the content).
 *
 * ES5 only — MediaWiki:Common.js runs without transpilation.
 */

( function ( $ ) {
	'use strict';

	$( document ).ready( function () {
		initTabs();
		initCollapsibles();
		initCarousels();
	} );

	/**
	 * Tabbed-panel init. Activates any container with:
	 *
	 *   <div class="wm-tabs" data-wm-tabs>
	 *     <div class="wm-tabs__buttons" data-wm-tabs-btns>
	 *       <div class="wm-tabs__button" data-wm-tabs-btn="1">I</div>
	 *       <div class="wm-tabs__button" data-wm-tabs-btn="2">II</div>
	 *     </div>
	 *     <div class="wm-tabs__panels" data-wm-tabs-panels>
	 *       <div class="wm-tabs__panel" data-wm-tabs-panel="1">…</div>
	 *       <div class="wm-tabs__panel wm-tabs__panel--hidden" data-wm-tabs-panel="2">…</div>
	 *     </div>
	 *   </div>
	 *
	 * Click on a button hides every panel except the matching one. First panel
	 * is visible by default (server-rendered); JS only swaps on click.
	 *
	 * If JS is off, the first panel shows and others stay hidden — readers
	 * see SOMETHING (the most common variant), just not the tabs.
	 */
	function initTabs() {
		var $containers = $( '[data-wm-tabs]' );
		if ( !$containers.length ) {
			return;
		}
		$containers.each( function () {
			var $container = $( this );
			var $btns = $container.find( '[data-wm-tabs-btn]' );
			var $panels = $container.find( '[data-wm-tabs-panel]' );

			$btns.on( 'click', function () {
				var key = $( this ).attr( 'data-wm-tabs-btn' );
				$btns.removeClass( 'wm-tabs__button--active' );
				$( this ).addClass( 'wm-tabs__button--active' );
				$panels.addClass( 'wm-tabs__panel--hidden' );
				$panels.filter( '[data-wm-tabs-panel="' + key + '"]' )
					.removeClass( 'wm-tabs__panel--hidden' );
			} );
		} );
	}

	/**
	 * Collapsible-section toggle. Activates any element with:
	 *
	 *   <div class="wm-collapsible" data-wm-collapsible>
	 *     <div class="wm-collapsible__header" data-wm-collapsible-toggle>Section header (click to collapse)</div>
	 *     <div class="wm-collapsible__body">…</div>
	 *   </div>
	 *
	 * Click on the header toggles a `wm-collapsible--collapsed` class on the
	 * container. CSS in MediaWiki:Common.css handles the show/hide via
	 * `.wm-collapsible--collapsed .wm-collapsible__body { display: none; }`.
	 *
	 * If JS is off, sections stay expanded (the readable default).
	 *
	 * MediaWiki ships a built-in `mw-collapsible` that does similar work; this
	 * exists for the cases where we want a consistent wm-* class namespace
	 * across our templates (so MediaWiki:Common.css selectors stay simple).
	 */
	function initCollapsibles() {
		$( '[data-wm-collapsible-toggle]' ).on( 'click', function () {
			$( this ).closest( '[data-wm-collapsible]' )
				.toggleClass( 'wm-collapsible--collapsed' );
		} );
	}

	/**
	 * Carousel init. Paired with Widget:Carousel emitting.
	 *
	 * Indexing convention: data-* attributes are 0-indexed (DOM convention);
	 * aria-labels are 1-indexed ("Slide 1", "Slide 2" — human convention).
	 * Don't "fix" this — matches WAI-ARIA carousel patterns.
	 *
	 *
	 *   <div class="wm-carousel" data-wm-carousel>
	 *     <div data-wm-carousel-slides>
	 *       <div data-wm-carousel-slide="0" class="wm-carousel__slide--active">…</div>
	 *       <div data-wm-carousel-slide="1">…</div>
	 *     </div>
	 *     <div data-wm-carousel-btns>
	 *       <button data-wm-carousel-btn="0" class="wm-carousel__btn--active">…</button>
	 *       <button data-wm-carousel-btn="1">…</button>
	 *     </div>
	 *   </div>
	 *
	 * Click a button → opacity-fade the matching slide in, others out. First
	 * slide is server-rendered visible (opacity:1); JS-off readers see only that.
	 *
	 * Active-state CSS lives in MediaWiki:Common.css. JS toggles class names +
	 * inline opacity (the inline rule is for graceful degradation when CSS
	 * hasn't loaded yet).
	 */
	function initCarousels() {
		$( '[data-wm-carousel]' ).each( function () {
			var $car = $( this );
			var $slides = $car.find( '[data-wm-carousel-slide]' );
			var $btns = $car.find( '[data-wm-carousel-btn]' );

			$btns.on( 'click', function () {
				var idx = $( this ).attr( 'data-wm-carousel-btn' );
				$btns.removeClass( 'wm-carousel__btn--active' ).css( 'background', 'transparent' );
				$( this ).addClass( 'wm-carousel__btn--active' ).css( 'background', '#888' );
				$slides.removeClass( 'wm-carousel__slide--active' ).css( 'opacity', '0' );
				$slides.filter( '[data-wm-carousel-slide="' + idx + '"]' )
					.addClass( 'wm-carousel__slide--active' ).css( 'opacity', '1' );
			} );
		} );
	}
}( jQuery ) );

/*
 * Arms of God — per-game interactivity block (appended after the generic
 * library block by 5-build-pages/code/emit_pages.py::emit_css_js).
 *
 * 1. Hover-infobox tooltips: any element with class="wm-tip"
 *    data-tip-title="<page>" (emitted by Module:Iconbox, Module:CrossRef,
 *    Module:Index, Module:LinkGrid, Module:StatIndex and the comparison
 *    datasets) shows the target page's infobox in a dark floating popup on
 *    hover. The infobox HTML is fetched once per title via api.php
 *    action=parse and cached for the page view. No JS / touch-only device /
 *    page without an infobox -> plain link, no popup.
 *
 * 2. Comparison filter chips: .wm-filterbar bars (shipped display:none so
 *    no-JS readers never see inert controls) are revealed; clicking a
 *    .wm-chip toggles it and hides table rows whose data-<group> attribute
 *    (data-element / data-class / data-tier / data-type) has no value in
 *    common with the active chips of that group. Groups AND together;
 *    chips within a group OR together. Reset clears everything.
 *
 * 3. Catalogue expand/collapse-all: .wm-expand-all / .wm-collapse-all
 *    buttons inside a .wm-catalogue wrapper (Help:Wiki Editing module /
 *    template / data-schema catalogues) expand or collapse every
 *    jquery.makeCollapsible table inside THAT wrapper only.
 *
 * ES5 only, jQuery + mw provided by MediaWiki. Dark styling lives in
 * MediaWiki:Common.css (.wm-tipbox / .wm-chip / .wm-row-hidden /
 * .wm-catalogue-controls).
 */
( function ( $ ) {
	'use strict';

	$( document ).ready( function () {
		initTipPopups();
		initFilterChips();
		initCatalogueControls();
	} );

	/* ---------------------------------------------- hover tooltips ---- */

	function initTipPopups() {
		// Hover-driven feature: skip entirely on touch-only devices.
		if ( window.matchMedia && window.matchMedia( '(hover: none)' ).matches ) {
			return;
		}
		var cache = {};   // title -> html string, or false (= no popup)
		var timer = null;
		var $box = null;
		var current = null;

		function apiUrl() {
			return mw.config.get( 'wgScriptPath' ) + '/api.php';
		}

		function ensureBox() {
			if ( !$box ) {
				$box = $( '<div>' ).addClass( 'wm-tipbox' )
					.css( {
						position: 'absolute',
						zIndex: 1000,
						maxWidth: '360px',
						background: '#1c1d22',
						color: '#e6e6e6',
						border: '1px solid #3a3c44',
						borderRadius: '5px',
						boxShadow: '0 6px 18px rgba(0,0,0,0.65)',
						padding: '6px',
						fontSize: '0.9em',
						pointerEvents: 'none',
						display: 'none'
					} )
					.appendTo( document.body );
			}
			return $box;
		}

		function position( $b, x, y ) {
			var w = $b.outerWidth() || 360;
			var winRight = $( window ).scrollLeft() + $( window ).width();
			var left = ( x + 18 + w > winRight ) ? Math.max( 4, x - w - 12 ) : x + 18;
			$b.css( { left: left + 'px', top: ( y + 14 ) + 'px' } );
		}

		function extract( html ) {
			var $root = $( '<div>' ).html( html );
			var $ib = $root.find( '.infobox' ).first();
			if ( $ib.length ) {
				// neutralise the on-page float/width inline styles
				$ib.css( { float: 'none', width: 'auto', maxWidth: '340px', margin: 0 } );
				return $( '<div>' ).append( $ib ).html();
			}
			var $p = $root.find( 'p' ).filter( function () {
				return $.trim( $( this ).text() ).length > 0;
			} ).first();
			if ( $p.length ) {
				var text = $.trim( $p.text() );
				if ( text.length > 320 ) {
					text = text.slice( 0, 317 ) + '…';
				}
				return $( '<div>' ).text( text ).html();
			}
			return false;
		}

		function show( title, x, y ) {
			if ( cache[ title ] === false ) {
				return;
			}
			if ( typeof cache[ title ] === 'string' ) {
				var $b = ensureBox();
				$b.html( cache[ title ] ).show();
				position( $b, x, y );
				return;
			}
			$.getJSON( apiUrl(), {
				action: 'parse',
				page: title,
				prop: 'text',
				redirects: 1,
				disablelimitreport: 1,
				formatversion: 2,
				format: 'json'
			} ).done( function ( data ) {
				var html = data && data.parse && data.parse.text;
				cache[ title ] = html ? extract( html ) : false;
				// only show if the cursor is still on the same link
				if ( current === title && cache[ title ] ) {
					var $b = ensureBox();
					$b.html( cache[ title ] ).show();
					position( $b, x, y );
				}
			} ).fail( function () {
				cache[ title ] = false;
			} );
		}

		function hide() {
			current = null;
			if ( timer ) {
				clearTimeout( timer );
				timer = null;
			}
			if ( $box ) {
				$box.hide();
			}
		}

		$( document ).on( 'mouseenter', '.wm-tip', function ( e ) {
			// no popups for links rendered inside a popup
			if ( $( this ).closest( '.wm-tipbox' ).length ) {
				return;
			}
			var title = $( this ).attr( 'data-tip-title' );
			if ( !title ) {
				return;
			}
			var x = e.pageX, y = e.pageY;
			current = title;
			if ( timer ) {
				clearTimeout( timer );
			}
			timer = setTimeout( function () {
				if ( current === title ) {
					show( title, x, y );
				}
			}, 160 );
		} ).on( 'mouseleave', '.wm-tip', hide );
	}

	/* ---------------------------------------------- filter chips ------ */

	function initFilterChips() {
		var $bars = $( '.wm-filterbar' );
		if ( !$bars.length ) {
			return;
		}
		var GROUPS = [ 'element', 'class', 'tier', 'type' ];

		function rowSelector() {
			var sel = [];
			for ( var i = 0; i < GROUPS.length; i++ ) {
				sel.push( 'tr[data-' + GROUPS[ i ] + ']' );
			}
			return sel.join( ',' );
		}

		function apply() {
			var active = {};
			$( '.wm-chip-on' ).each( function () {
				var g = $( this ).attr( 'data-group' );
				var v = $( this ).attr( 'data-val' );
				if ( !g || !v ) {
					return;
				}
				( active[ g ] = active[ g ] || [] ).push( v );
			} );
			$( rowSelector() ).each( function () {
				var row = this;
				var hidden = false;
				for ( var g in active ) {
					var rv = row.getAttribute( 'data-' + g );
					if ( rv === null ) {
						continue; // row not classified on this axis
					}
					var tokens = rv.split( ' ' );
					var hit = false;
					for ( var i = 0; i < tokens.length && !hit; i++ ) {
						for ( var j = 0; j < active[ g ].length && !hit; j++ ) {
							if ( tokens[ i ] === active[ g ][ j ] ) {
								hit = true;
							}
						}
					}
					if ( !hit ) {
						hidden = true;
						break;
					}
				}
				$( row ).toggleClass( 'wm-row-hidden', hidden )
					.css( 'display', hidden ? 'none' : '' );
			} );
		}

		$bars.show(); // bars ship display:none; only JS users see them

		$( document ).on( 'click', '.wm-filterbar .wm-chip', function () {
			var $chip = $( this );
			if ( $chip.hasClass( 'wm-chip-clear' ) ) {
				$chip.closest( '.wm-filterbar' ).find( '.wm-chip-on' )
					.removeClass( 'wm-chip-on' )
					.each( restyleChip );
				apply();
				return;
			}
			$chip.toggleClass( 'wm-chip-on' );
			restyleChip.call( this );
			apply();
		} );

		// Inline-style fallback so the active state is visible even if the
		// site CSS is stale (every shipped surface styles inline-first).
		function restyleChip() {
			var $chip = $( this );
			if ( $chip.hasClass( 'wm-chip-on' ) ) {
				$chip.css( { background: '#f1e9d2', color: '#111', fontWeight: 'bold' } );
			} else {
				$chip.css( { background: '', color: '', fontWeight: '' } );
			}
		}
	}

	/* ------------------------------- catalogue expand/collapse all ---- */

	function initCatalogueControls() {
		$( document ).on( 'click', '.wm-expand-all, .wm-collapse-all', function ( e ) {
			e.preventDefault();
			var expand = $( this ).hasClass( 'wm-expand-all' );
			var $scope = $( this ).closest( '.wm-catalogue' );
			if ( !$scope.length ) {
				return;
			}
			// makeCollapsible ships with core page rendering, but wait for
			// the module anyway so the data API is guaranteed initialized.
			mw.loader.using( 'jquery.makeCollapsible' ).then( function () {
				$scope.find( '.mw-collapsible' ).each( function () {
					var $el = $( this );
					var api = $el.data( 'mw-collapsible' );
					if ( api && api.expand && api.collapse ) {
						if ( expand ) {
							api.expand();
						} else {
							api.collapse();
						}
						return;
					}
					// Fallback: synthetic toggle click, but only when the
					// current collapsed state mismatches the target so it
					// never flips a table the wrong way.
					var collapsed = $el.hasClass( 'mw-collapsed' );
					if ( collapsed === expand ) {
						$el.find( '.mw-collapsible-toggle' ).first().trigger( 'click' );
					}
				} );
			} );
		} );
	}

}( jQuery ) );