MediaWiki:Gadget-slideshows.js

From The Binding of Isaac: Rebirth Wiki
Jump to navigation Jump to search

In other languages: Français


Note: After saving, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Clear the cache in Tools → Preferences
( function ( mw, array ) {

/**
 * Delay between two automatic cycling frames.
 * @type {number}
 */
const autoInterval = 1500;

/**
 * Whether automatic cycling has been enabled, i.e. at least one automatic
 * slideshow is enabled or was enabled before the last slide update.
 * @type {boolean}
 */
var cyclingEnabled = false;

/**
 * The click events added to elements in a slideshow.
 * @typedef ClickEvents
 * @property {EventListener}                   [slide] On all slides.
 * @property {Map<HTMLElement, EventListener>} titles  On each slide title.
 */

/**
 * The click events added to each slideshow.
 * @type {Map<HTMLElement, ClickEvents>}
 */
const clickEvents = new Map();

/**
 * The list of enabled slideshows cycling automatically.
 * @type {HTMLElement[]}
 */
const auto = [];

/**
 * Handles an "impossible" case, supposedly caused by other scripts breaking the
 * expected DOM elements.
 * @returns {never}
 */
function domPanic() {
	throw "Something went wrong, either DOM elements have been modified in " +
	      "an unexpected way, or they have been disconnected from the document.";
}

/**
 * Called when some text should be processed.
 * @param {JQuery} $content The content element to process.
 */
function onContentLoaded( $content ) {
	const content = $content[ 0 ];
	if ( !content ) {
		return;
	}

	init( content );
}

/**
 * Enables all slideshows in a container.
 * @param {HTMLElement} container The container.
 * @returns {HTMLElement[]} The enabled slideshows in the container.
 */
function init( container ) {
	return array.filter.call(
		container.getElementsByClassName( 'infobox2-slideshow' ),
		enable
	);
}

/**
 * Enables a slideshow.
 * @param {HTMLElement} slideshow The slideshow to enable.
 * @returns {boolean} True if the slideshow is now enabled,
 *                    false if it is now disabled.
 */
function enable( slideshow ) {
	if ( slideshow.classList.contains( 'infobox2-slideshow-disabled' ) ) {
		return false;
	}

	if ( isEnabled( slideshow ) ) {
		return true;
	}

	const slides = getSlides( slideshow );
	if ( slides.length < 2 ) {
		disable( slideshow );
		return false;
	}

	/** @type {ClickEvents} */
	const newClickEvents = { titles: new Map() };

	slideshow.classList.add( 'infobox2-slideshow-enabled' );

	const titlebar = document.createElement( 'div' );
	titlebar.classList.add( 'infobox2-slideshow-titlebar' );

	const titles = slides.map( enableTitle, newClickEvents );

	if ( newClickEvents.titles.size > 0 ) {
		const as = titlebar.getElementsByTagName( 'a' );
		while ( as[ 0 ] ) {
			unwrap( as[ 0 ] );
		}
	}

	const activeIndex = slideshow.classList.contains( 'dlc-slideshow' ) ? slides.length - 1 : 0;
	const activeSlide = slides[ activeIndex ];
	const activeTitle = titles[ activeIndex ];
	if ( !activeSlide || !activeTitle ) {
		domPanic();
	}

	activeSlide.classList.add( 'infobox2-slide-active' );
	if ( activeTitle.classList.contains( 'infobox2-slide-title' ) ) {
		activeTitle.classList.add( 'infobox2-slide-title-active' );
	}

	if (
		!slideshow.classList.contains( 'infobox2-slideshow-hidden' ) &&
		!slideshow.getElementsByClassName( 'infobox2-slideshow' )[ 0 ]
	) {
		newClickEvents.slide = function () { cycle( slideshow ); };
		slides.forEach( setSlideClickEvent, newClickEvents.slide );
	}

	titles.forEach( appendTitle, titlebar );

	if ( slideshow.classList.contains( 'infobox2-slideshow-auto' ) ) {
		makeAuto( slideshow );
	}

	clickEvents.set( slideshow, newClickEvents );

	if ( newClickEvents.titles.size > 0 ) {
		slideshow.insertBefore( titlebar, slideshow.firstChild );
	}

	setMinHeight( slideshow );

	return true;
}

/**
 * Adds a title to a title bar of a slideshow.
 * @this {HTMLElement} The title bar.
 * @param {HTMLElement?} title The title to add.
 */
function appendTitle( title ) {
	if ( !title ) {
		this.appendChild( document.createElement( 'span' ) );
		return;
	}

	const titlePlaceholder = document.createElement( 'span' );
	titlePlaceholder.classList.add( 'infobox2-slide-title-placeholder' );
	title.replaceWith( titlePlaceholder );
	this.appendChild( title );
}

/**
 * Sets the event handler when clicking on a slide.
 * @this {EventListener} The event handler.
 * @param {HTMLElement} slide The slide whose click event should be handled.
 */
function setSlideClickEvent( slide ) {
	slide.addEventListener( 'click', this );
}

/**
 * Makes a slideshow cycle slides automatically.
 * @param {HTMLElement} slideshow The slideshow.
 */
function makeAuto( slideshow ) {
	slideshow.classList.add( 'infobox2-slideshow-auto' );
	if ( !isEnabled( slideshow ) || auto.indexOf( slideshow ) !== -1 ) {
		return;
	}

	auto.push( slideshow );
	if ( cyclingEnabled ) {
		return;
	}

	cyclingEnabled = true;
	setTimeout( runAutoInterval, autoInterval );
}

/**
 * Disables a slideshow.
 * @param {HTMLElement} slideshow The slideshow to disable.
 */
function disable( slideshow ) {
	if ( slideshow.classList.contains( 'infobox2-slideshow-disabled' ) ) {
		return;
	}

	const slides = getSlides( slideshow );

	slideshow.classList.add( 'infobox2-slideshow-disabled' );

	if ( !isEnabled( slideshow ) ) {
		slides.forEach( function ( slide ) {
			const title = getSlideTitle( slide );
			if ( title ) {
				title.remove();
			}
		} );
		return;
	}

	const titleBar         = getTitleBar( slideshow );
	const localClickEvents = clickEvents.get( slideshow );
	if ( !localClickEvents ) {
		domPanic();
	}

	slideshow.classList.remove( 'infobox2-slideshow-enabled' );

	slides.forEach( function ( slide ) {
		slide.classList.remove( 'infobox2-slide-active' );
		if ( localClickEvents.slide ) {
			slide.removeEventListener( 'click', localClickEvents.slide );
		}

		if ( !titleBar ) {
			return;
		}

		const title = getSlideTitle( slide );
		if ( !title ) {
			domPanic();
		}

		title.classList.remove( 'infobox2-slide-title-active' );

		const titlePlaceholder = slide.getElementsByClassName( 'infobox2-slide-title-placeholder' )[ 0 ];
		if ( titlePlaceholder ) {
			titlePlaceholder.replaceWith( title );
		}

		const titleEvent = localClickEvents.titles.get( title );
		if ( titleEvent ) {
			title.removeEventListener( 'click', titleEvent );
		}
	} );

	if ( titleBar ) {
		titleBar.remove();
	}

	clickEvents.delete( slideshow );
}

/**
 * Sets a slide as the active one of a slideshow.
 * @param {HTMLElement} slide   The slide to set as active one.
 * @param {HTMLElement} [title] The title of the slide.
 */
function setActiveSlide( slide, title ) {
	const slideshow = slide.parentElement;
	if ( !slideshow ) {
		domPanic();
	}

	const activeSlide = getActiveSlide( slideshow );
	if ( !activeSlide ) {
		domPanic();
	}

	activeSlide.classList.remove( 'infobox2-slide-active' );
	slide.classList.add( 'infobox2-slide-active' );

	const titlebar = getTitleBar( slideshow );
	if ( !titlebar ) {
		return;
	}

	var activeTitle = getSlideTitle( activeSlide );
	if ( activeTitle ) {
		activeTitle.classList.remove( 'infobox2-slide-title-active' );
	}

	const slideTitle = title || getSlideTitle( slide );
	if ( slideTitle && slideTitle.classList.contains( 'infobox2-slide-title' ) ) {
		slideTitle.classList.add( 'infobox2-slide-title-active' );
	}
}

/**
 * Sets the next slide as the active one of a slideshow.
 * @param {HTMLElement} slideshow The slideshow.
 */
function cycle( slideshow ) {
	const activeSlide = getActiveSlide( slideshow );
	if ( !activeSlide ) {
		domPanic();
	}

	setActiveSlide(
		getNextSiblingByClassName( activeSlide, 'infobox2-slide' ) ||
		getChildByClassName( slideshow, 'infobox2-slide' )
	);
}

/**
 * Removes a slide from a slideshow.
 * @param {HTMLElement} slide The slide to remove.
 */
function removeSlide( slide ) {
	const title = getSlideTitle( slide );
	slide.remove();

	if ( title ) {
		title.remove();
	}

	const slideshow = slide.parentElement;
	if ( slideshow && getSlides( slideshow ).length < 2 ) {
		disable( slideshow );
	}
}

/**
 * Indicates whether a slideshow is enabled.
 * @param {HTMLElement} slideshow The slideshow.
 * @returns {boolean} True if the slideshow is enabled, false otherwise.
 */
function isEnabled( slideshow ) {
	return slideshow.classList.contains( 'infobox2-slideshow-enabled' );
}

/**
 * Gets all slides of a slideshow.
 * @param {HTMLElement} slideshow The slideshow to enable.
 * @returns {HTMLElement[]} An array of all slides of the slideshow.
 */
function getSlides( slideshow ) {
	return array.filter.call( slideshow.children, isSlide );
}

/**
 * Gets the currently active slide of a slideshow.
 * @param {HTMLElement} slideshow The slideshow to enable.
 * @returns {HTMLElement?} The currently active slide of the slideshow,
 *                         null if there is not any.
 */
function getActiveSlide( slideshow ) {
	return array.find.call( slideshow.children, isActiveSlide ) || null;
}

/**
 * Gets the title bar of a slideshow.
 * @param {HTMLElement} slideshow The slideshow.
 * @returns {HTMLElement?} The title bar of the slideshow,
 *                         null if it does not have any.
 */
function getTitleBar( slideshow ) {
	return array.find.call( slideshow.children, isTitleBar ) || null;
}

/**
 * Indicates whether an element is a slide.
 * @param {HTMLElement} element The element.
 * @returns {boolean} True if the element is a slide, false otherwise.
 */
function isSlide( element ) {
	return element.classList.contains( 'infobox2-slide' );
}

/**
 * Indicates whether an element is the active slide of a slideshow.
 * @param {HTMLElement} element The element.
 * @returns {boolean} True if the element is a slide and the active one of its
 *                    slideshow, false otherwise.
 */
function isActiveSlide( element ) {
	return element.classList.contains( 'infobox2-slide-active' );
}

/**
 * Indicates whether an element is the title bar of a slideshow.
 * @param {HTMLElement} element The element.
 * @returns {boolean} True if the element is the title bar of a slideshow,
 *                    false otherwise.
 */
function isTitleBar( element ) {
	return element.classList.contains( 'infobox2-slideshow-titlebar' );
}

/**
 * Gets the title of a slide.
 * @param {HTMLElement} slide The slide.
 * @returns {HTMLElement?} The title of the slide, null if it does not have any.
 */
function getSlideTitle( slide ) {
	const slideshow = slide.parentElement;
	if ( !slideshow ) {
		return domPanic();
	}

	const titlebar = getTitleBar( slideshow );
	if ( titlebar ) {
		return titlebar.children[ getSlides( slideshow ).indexOf( slide ) ] || domPanic();
	}

	return slide.getElementsByClassName( 'infobox2-slide-title' )[ 0 ] || null;
}

/**
 * Gets a slide from its title.
 * @param {HTMLElement} title The title.
 * @returns {HTMLElement?} The associated slide, null if there is not any.
 */
function getTitleSlide( title ) {
	for ( var parent = title.parentElement; parent; parent = parent.parentElement ) {
		if ( parent.classList.contains( 'infobox2-slideshow-titlebar' ) ) {
			const slideshow = parent.parentElement;
			if ( !slideshow ) {
				domPanic();
			}
	
			const index = array.slice.call( parent.children ).indexOf( title );
			return getSlides( slideshow )[ index ] || null;
		}
	
		if ( parent.classList.contains( 'infobox2-slide' ) ) {
			return parent;
		}
	}

	return null;
}

/**
 * Enables the click action from a title to its slide.
 * @this {ClickEvents} The click events of the slideshow.
 * @param {HTMLElement} slide The slide.
 * @returns {HTMLElement} The title of the slide, null if there is not any.
 */
function enableTitle( slide ) {
	const title = getSlideTitle( slide );
	if ( !title ) {
		domPanic();
	}

	const click = function () {
		if ( title.classList.contains( 'infobox2-slide-title-active' ) ) {
			return;
		}

		const slide = getTitleSlide( title );
		if ( !slide ) {
			domPanic();
		}

		setActiveSlide( slide, title );
	};

	this.titles.set( title, click );
	title.addEventListener( 'click', click );

	return title;
}

/**
 * Cycles slides of all enabled slideshows.
 */
function runAutoInterval() {
	if ( !auto.length ) {
		cyclingEnabled = false;
		return;
	}

	auto.forEach( cycle );

	setTimeout( runAutoInterval, autoInterval );
}

/**
 * Sets a minimum height to a slideshow to prevent other elements from
 * moving on the page while switching slides.
 * @param {HTMLElement} slideshow The slideshow.
 */
function setMinHeight( slideshow ) {
	const activeSlide = getActiveSlide( slideshow );
	if ( !activeSlide ) {
		domPanic();
	}

	activeSlide.classList.remove( 'infobox2-slide-active' );

	const minHeight = getSlides( slideshow ).reduce( function ( maxHeight, slide ) {
		slide.classList.add( 'infobox2-slide-active' );
		maxHeight = Math.max(
			maxHeight,
			slideshow.getBoundingClientRect().height
		);
		slide.classList.remove( 'infobox2-slide-active' );
		return maxHeight;
	}, 0 );

	activeSlide.classList.add( 'infobox2-slide-active' );

	slideshow.style.minHeight = minHeight + 'px';
}

/**
 * Gets the first element following an element which has a given class.
 * @param {HTMLElement} element   The element.
 * @param {string}      className The class name.
 * @returns {HTMLElement?} An element following the given element which has
 *                         the given class, null if there is not any.
 */
function getNextSiblingByClassName( element, className ) {
	for ( var sibling = element.nextElementSibling; sibling; sibling = sibling.nextElementSibling ) {
		if ( sibling.classList.contains( className ) ) {
			return sibling;
		}
	}

	return null;
}

/**
 * Gets the first element within a container which has a given class.
 * @param {HTMLElement} container The container.
 * @param {string}      className The class name.
 * @returns {HTMLElement?} An element from the container which has the given class,
 *                         null if there is not any.
 */
function getChildByClassName( container, className ) {
	return array.find.call( container.getElementsByClassName( className ), isChild, container );
}

/**
 * Indicates whether an element is a direct child of another.
 * @this {HTMLElement} The parent element.
 * @param {HTMLElement} element The element to check.
 * @returns {boolean} True if the given element is a child of the other one,
 *                    false otherwise.
 */
function isChild( element ) {
	return element.parentElement === this;
}

/**
 * Removes an element, leaving its content in place.
 * @param {HTMLElement} element The element to remove.
 */
function unwrap( element ) {
	const parent = element.parentElement;
	if ( !parent ) {
		domPanic();
	}

	var childNode = element.firstChild;
	while ( childNode ) {
		parent.insertBefore( childNode, element );
		childNode = element.firstChild;
	}

	element.remove();
}

module.exports = {
	init: init,
	enable: enable,
	makeAuto: makeAuto,
	disable: disable,
	setActiveSlide: setActiveSlide,
	cycle: cycle,
	removeSlide: removeSlide,
	isEnabled: isEnabled,
	getSlides: getSlides,
	getActiveSlide: getActiveSlide,
	getTitleBar: getTitleBar,
	getSlideTitle: getSlideTitle,
	getTitleSlide: getTitleSlide
};

mw.hook( 'wikipage.content' ).add( onContentLoaded );

} )( mediaWiki, Array.prototype );