mirror of
https://github.com/StarCitizenTools/mediawiki-extensions-TabberNeue.git
synced 2024-11-24 00:13:28 +00:00
feat: allow tab content to be transclusions of other pages
* Initial merge of the TabberTransclude extension from Ciencia * Tab content can now be fetched from other pages on the wiki. The content is lazy-loaded when the tab is selected through a XHR request. * Add a config option to disable setting URL hash on tab change
This commit is contained in:
parent
91105aa338
commit
d8c3db4e59
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "TabberNeue",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"author": [
|
||||
"alistair3149",
|
||||
"Eric Fortin",
|
||||
|
@ -25,7 +25,17 @@
|
|||
"ResourceModules": {
|
||||
"ext.tabberNeue": {
|
||||
"packageFiles": [
|
||||
"ext.tabberNeue.js"
|
||||
"ext.tabberNeue.js",
|
||||
{
|
||||
"name": "config.json",
|
||||
"config": {
|
||||
"updateLocationOnTabChange": "TabberNeueUpdateLocationOnTabChange"
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
"tabberneue-loading",
|
||||
"tabberneue-error"
|
||||
],
|
||||
"styles": [
|
||||
"ext.tabberNeue.less"
|
||||
|
@ -62,6 +72,14 @@
|
|||
"localBasePath": "modules",
|
||||
"remoteExtPath": "TabberNeue/modules"
|
||||
},
|
||||
"config_prefix": "wg",
|
||||
"config": {
|
||||
"TabberNeueUpdateLocationOnTabChange": {
|
||||
"value": true,
|
||||
"description": "If enabled, when a tab is selected, the URL displayed on the browser changes. Opening this URL makes that tab initially selected.",
|
||||
"public": true
|
||||
}
|
||||
},
|
||||
"Hooks": {
|
||||
"ParserFirstCallInit": [
|
||||
"TabberNeue\\TabberNeueHooks::onParserFirstCallInit"
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
"@metadata": {
|
||||
"authors": [
|
||||
"alistair3149",
|
||||
"Eric Fortin"
|
||||
"Eric Fortin",
|
||||
"Ciencia Al Poder"
|
||||
]
|
||||
},
|
||||
"tabberneue-desc": "Allows to create tabs within a page. Forked from [https://www.mediawiki.org/wiki/Extension:Tabber Extension:Tabber]."
|
||||
"tabberneue-desc": "Allows to create tabs within a page. Forked from [https://www.mediawiki.org/wiki/Extension:Tabber Extension:Tabber].",
|
||||
"tabberneue-loading": "Loading...",
|
||||
"tabberneue-error": "Error."
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Armando-Martin"
|
||||
"Ciencia Al Poder"
|
||||
]
|
||||
},
|
||||
"tabberneue-desc": "Permite para fichas dentro de una página"
|
||||
"tabberneue-desc": "Permite usar pestañas dentro de una página.",
|
||||
"tabberneue-loading": "Cargando...",
|
||||
"tabberneue-error": "Error."
|
||||
}
|
|
@ -4,5 +4,7 @@
|
|||
"Shirayuki"
|
||||
]
|
||||
},
|
||||
"tabberneue-desc": "{{desc|name=TabberNeue|url=http://www.mediawiki.org/wiki/Extension:TabberNeue}}"
|
||||
"tabberneue-desc": "{{desc|name=TabberNeue|url=http://www.mediawiki.org/wiki/Extension:TabberNeue}}",
|
||||
"tabberneue-loading": "Placeholder loading message for the tab content",
|
||||
"tabberneue-error": "Error message shown loading tab content"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* TabberNeue Hooks Class
|
||||
*
|
||||
* @package TabberNeue
|
||||
* @author alistair3149, Eric Fortin, Alexia E. Smith
|
||||
* @author alistair3149, Eric Fortin, Alexia E. Smith, Ciencia Al Poder
|
||||
* @license GPL-3.0-or-later
|
||||
* @link https://www.mediawiki.org/wiki/Extension:TabberNeue
|
||||
*/
|
||||
|
@ -13,8 +13,11 @@ declare( strict_types=1 );
|
|||
|
||||
namespace TabberNeue;
|
||||
|
||||
use Hooks;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Parser;
|
||||
use PPFrame;
|
||||
use Title;
|
||||
|
||||
class TabberNeueHooks {
|
||||
/**
|
||||
|
@ -24,6 +27,7 @@ class TabberNeueHooks {
|
|||
*/
|
||||
public static function onParserFirstCallInit( Parser $parser ) {
|
||||
$parser->setHook( 'tabber', [ __CLASS__, 'renderTabber' ] );
|
||||
$parser->setHook( 'tabbertransclude', [ __CLASS__, 'renderTabberTransclude' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,4 +81,110 @@ class TabberNeueHooks {
|
|||
|
||||
return $tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the necessary HTML for a <tabbertransclude> tag.
|
||||
*
|
||||
* @param string $input The input URL between the beginning and ending tags.
|
||||
* @param array $args Array of attribute arguments on that beginning tag.
|
||||
* @param Parser $parser Mediawiki Parser Object
|
||||
* @param PPFrame $frame Mediawiki PPFrame Object
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
public static function renderTabberTransclude( $input, array $args, Parser $parser, PPFrame $frame ) {
|
||||
$parser->getOutput()->addModules( [ 'ext.tabberNeue' ] );
|
||||
$selected = true;
|
||||
|
||||
$arr = explode( "\n", $input );
|
||||
$htmlTabs = '';
|
||||
foreach ( $arr as $tab ) {
|
||||
$htmlTabs .= self::buildTabTransclude( $tab, $parser, $frame, $selected );
|
||||
}
|
||||
|
||||
$html = '<div class="tabber">' .
|
||||
'<section class="tabber__section">' . $htmlTabs . "</section></div>";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build individual tab.
|
||||
*
|
||||
* @param string $tab Tab information
|
||||
* @param Parser $parser Mediawiki Parser Object
|
||||
* @param PPFrame $frame Mediawiki PPFrame Object
|
||||
* @param bool $selected The tab is the selected one
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
private static function buildTabTransclude( $tab, Parser $parser, PPFrame $frame, &$selected ) {
|
||||
$tab = trim( $tab );
|
||||
if ( empty( $tab ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tabBody = '';
|
||||
$dataProps = [];
|
||||
// Use array_pad to make sure at least 2 array values are always returned
|
||||
list( $pageName, $tabName ) = array_pad( explode( '|', $tab, 2 ), 2, '' );
|
||||
$title = Title::newFromText( trim( $pageName ) );
|
||||
if ( !$title ) {
|
||||
if ( empty( $tabName ) ) {
|
||||
$tabName = $pageName;
|
||||
}
|
||||
$tabBody = sprintf( '<div class="error">Invalid title: %s</div>', $pageName );
|
||||
$pageName = '';
|
||||
} else {
|
||||
$pageName = $title->getPrefixedText();
|
||||
if ( empty( $tabName ) ) {
|
||||
$tabName = $pageName;
|
||||
}
|
||||
$dataProps['page-title'] = $pageName;
|
||||
if ( $selected ) {
|
||||
$tabBody = $parser->recursiveTagParseFully(
|
||||
sprintf( '{{:%s}}', $pageName ),
|
||||
$frame
|
||||
);
|
||||
} else {
|
||||
// Add a link placeholder, as a fallback if JavaScript doesn't execute
|
||||
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
|
||||
$tabBody = sprintf(
|
||||
'<div class="tabber__ajaxplaceholder">%s</div>',
|
||||
$linkRenderer->makeLink( $title, null, [ 'rel' => 'nofollow' ] )
|
||||
);
|
||||
$dataProps['pending-load'] = '1';
|
||||
// 1.37: $currentTitle = $parser->getPage();
|
||||
$currentTitle = $parser->getTitle();
|
||||
$query = sprintf(
|
||||
'?action=parse&format=json&formatversion=2&title=%s&text={{:%s}}&redirects=1&prop=text&disablelimitreport=1&disabletoc=1&wrapoutputclass=',
|
||||
urlencode( $currentTitle->getPrefixedText() ),
|
||||
urlencode( $pageName )
|
||||
);
|
||||
$dataProps['load-url'] = wfExpandUrl( wfScript( 'api' ) . $query, PROTO_CANONICAL );
|
||||
$oldTabBody = $tabBody;
|
||||
// Allow extensions to update the lazy loaded tab
|
||||
Hooks::run( 'TabberTranscludeRenderLazyLoadedTab', [ &$tabBody, &$dataProps, $parser, $frame ] );
|
||||
if ( $oldTabBody != $tabBody ) {
|
||||
$parser->getOutput()->recordOption( 'tabbertranscludelazyupdated' );
|
||||
}
|
||||
}
|
||||
// Register as a template
|
||||
$revRecord = $parser->fetchCurrentRevisionRecordOfTitle( $title );
|
||||
$parser->getOutput()->addTemplate(
|
||||
$title,
|
||||
$title->getArticleId(),
|
||||
$revRecord ? $revRecord->getId() : null
|
||||
);
|
||||
}
|
||||
|
||||
$tab = '<article class="tabber__panel" title="' . htmlspecialchars( $tabName ) . '"';
|
||||
$tab .= implode( array_map( static function ( $prop, $value ) {
|
||||
return sprintf( ' data-tabber-%s="%s"', $prop, htmlspecialchars( $value ) );
|
||||
}, array_keys( $dataProps ), $dataProps ) );
|
||||
$tab .= '>' . $tabBody . '</article>';
|
||||
$selected = false;
|
||||
|
||||
return $tab;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
function initTabber( tabber, count ) {
|
||||
var tabPanels = tabber.querySelectorAll( ':scope > .tabber__section > .tabber__panel' );
|
||||
|
||||
var container = document.createElement( 'header' ),
|
||||
var config = require( './config.json' ),
|
||||
container = document.createElement( 'header' ),
|
||||
tabList = document.createElement( 'nav' ),
|
||||
prevButton = document.createElement( 'div' ),
|
||||
nextButton = document.createElement( 'div' );
|
||||
|
@ -149,22 +150,62 @@ function initTabber( tabber, count ) {
|
|||
updateButtons();
|
||||
} );
|
||||
|
||||
// Listen for window resize
|
||||
window.addEventListener( 'resize', mw.util.debounce( 250, setupButtons ) );
|
||||
// Listen for element resize
|
||||
if ( window.ResizeObserver ) {
|
||||
var tabListResizeObserver = new ResizeObserver( mw.util.debounce( 250, setupButtons ) );
|
||||
tabListResizeObserver.observe( tabList );
|
||||
}
|
||||
};
|
||||
|
||||
// NOTE: Are there better ways to scope them?
|
||||
var xhr = new XMLHttpRequest();
|
||||
var currentRequest = null, nextRequest = null;
|
||||
|
||||
/**
|
||||
* Loads page contents into tab
|
||||
*
|
||||
* @param {HTMLElement} tab panel
|
||||
* @param {string} api URL
|
||||
*/
|
||||
function loadPage( targetPanel, url ) {
|
||||
var requestData = {
|
||||
url: url,
|
||||
targetPanel: targetPanel
|
||||
};
|
||||
if ( currentRequest ) {
|
||||
if ( currentRequest.url != requestData.url ) {
|
||||
nextRequest = requestData;
|
||||
}
|
||||
// busy
|
||||
return;
|
||||
}
|
||||
xhr.open( 'GET', url );
|
||||
currentRequest = requestData;
|
||||
xhr.send( null );
|
||||
}
|
||||
|
||||
/**
|
||||
* Show panel based on target hash
|
||||
*
|
||||
* @param {string} targetHash
|
||||
*/
|
||||
function showPanel( targetHash ) {
|
||||
function showPanel( targetHash, allowRemoteLoad ) {
|
||||
var ACTIVETABCLASS = 'tabber__tab--active',
|
||||
ACTIVEPANELCLASS = 'tabber__panel--active',
|
||||
targetPanel = document.getElementById( targetHash ),
|
||||
targetTab = document.getElementById( 'tab-' + targetHash ),
|
||||
section = targetPanel.parentElement,
|
||||
activePanel = section.querySelector( ':scope > .' + ACTIVEPANELCLASS );
|
||||
activePanel = section.querySelector( ':scope > .' + ACTIVEPANELCLASS ),
|
||||
parentPanel, parentSection;
|
||||
|
||||
if ( allowRemoteLoad && targetPanel.dataset.tabberPendingLoad && targetPanel.dataset.tabberLoadUrl ) {
|
||||
var loading = document.createElement( 'div' );
|
||||
loading.setAttribute( 'class', 'tabber__loading' );
|
||||
loading.appendChild( document.createTextNode( mw.message( 'tabberneue-loading' ).text() ) );
|
||||
targetPanel.textContent = '';
|
||||
targetPanel.appendChild( loading );
|
||||
loadPage( targetPanel, targetPanel.dataset.tabberLoadUrl );
|
||||
}
|
||||
|
||||
/* eslint-disable mediawiki/class-doc */
|
||||
if ( activePanel ) {
|
||||
|
@ -212,6 +253,48 @@ function initTabber( tabber, count ) {
|
|||
/* eslint-enable mediawiki/class-doc */
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for XMLHttpRequest where ends loading
|
||||
*/
|
||||
function onLoadEndPage() {
|
||||
var targetPanel = currentRequest.targetPanel;
|
||||
if ( xhr.status != 200 ) {
|
||||
var err = document.createElement( 'div' );
|
||||
err.setAttribute( 'class', 'tabber__error' );
|
||||
err.appendChild( document.createTextNode( mw.message( 'tabberneue-error' ).text() ) );
|
||||
targetPanel.textContent = '';
|
||||
targetPanel.appendChild( err );
|
||||
} else {
|
||||
var result = JSON.parse( xhr.responseText );
|
||||
targetPanel.innerHTML = result.parse.text;
|
||||
// wikipage.content hook requires a jQuery object
|
||||
mw.hook( 'wikipage.content' ).fire( $( targetPanel ) );
|
||||
delete targetPanel.dataset.tabberPendingLoad;
|
||||
delete targetPanel.dataset.tabberLoadUrl;
|
||||
}
|
||||
|
||||
var ACTIVEPANELCLASS = 'tabber__panel--active',
|
||||
targetHash = targetPanel.getAttribute( 'id' ),
|
||||
section = targetPanel.parentElement,
|
||||
activePanel = section.querySelector( ':scope > .' + ACTIVEPANELCLASS );
|
||||
|
||||
if ( nextRequest ) {
|
||||
currentRequest = nextRequest;
|
||||
nextRequest = null;
|
||||
xhr.open( 'GET', currentRequest.url );
|
||||
xhr.send( null );
|
||||
} else {
|
||||
currentRequest = null;
|
||||
}
|
||||
if ( activePanel ) {
|
||||
// Refresh height
|
||||
showPanel( targetHash, false );
|
||||
}
|
||||
}
|
||||
|
||||
xhr.timeout = 20000;
|
||||
xhr.addEventListener( 'loadend', onLoadEndPage );
|
||||
|
||||
/**
|
||||
* Retrieve target hash and trigger show panel
|
||||
* If no targetHash is invalid, use the first panel
|
||||
|
@ -244,9 +327,11 @@ function initTabber( tabber, count ) {
|
|||
tab.addEventListener( 'click', function( event ) {
|
||||
var targetHash = tab.getAttribute( 'href' ).substring( 1 );
|
||||
event.preventDefault();
|
||||
if ( !config || config.updateLocationOnTabChange ) {
|
||||
// Add hash to the end of the URL
|
||||
history.replaceState( null, null, '#' + targetHash );
|
||||
showPanel( targetHash );
|
||||
}
|
||||
showPanel( targetHash, true );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
|
Loading…
Reference in a new issue