mirror of
https://github.com/StarCitizenTools/mediawiki-extensions-TabberNeue.git
synced 2024-11-27 09:42:48 +00:00
feat: only add empty paragraph for wikitext list elements
We don't need the empty paragraph hack for non-list elements. Related: #151
This commit is contained in:
parent
21ad702ee2
commit
f24ddb58ee
|
@ -14,20 +14,25 @@ declare( strict_types=1 );
|
|||
|
||||
namespace MediaWiki\Extension\TabberNeue;
|
||||
|
||||
use Html;
|
||||
use JsonException;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Parser;
|
||||
use PPFrame;
|
||||
use Sanitizer;
|
||||
|
||||
class Tabber {
|
||||
/**
|
||||
* Flag that checks if this is a nested tabber
|
||||
* @var bool
|
||||
*/
|
||||
|
||||
/** @var int */
|
||||
private static $count = 0;
|
||||
|
||||
/** @var bool Flag that checks if this is a nested tabber */
|
||||
private static $isNested = false;
|
||||
|
||||
/** @var bool */
|
||||
private static $useCodex = false;
|
||||
|
||||
/** @var bool */
|
||||
private static $parseTabName = false;
|
||||
|
||||
/**
|
||||
|
@ -42,8 +47,11 @@ class Tabber {
|
|||
*/
|
||||
public static function parserHook( ?string $input, array $args, Parser $parser, PPFrame $frame ) {
|
||||
$config = MediaWikiServices::getInstance()->getMainConfig();
|
||||
$parserOutput = $parser->getOutput();
|
||||
|
||||
self::$parseTabName = $config->get( 'TabberNeueParseTabName' );
|
||||
self::$useCodex = $config->get( 'TabberNeueUseCodex' );
|
||||
self::$count = count( $parserOutput->getExtensionData( 'tabber-count' ) ?? [] );
|
||||
|
||||
$html = self::render( $input ?? '', $parser, $frame );
|
||||
|
||||
|
@ -51,6 +59,8 @@ class Tabber {
|
|||
return '';
|
||||
}
|
||||
|
||||
$parserOutput->appendExtensionData( 'tabber-count', self::$count++ );
|
||||
|
||||
if ( self::$useCodex === true ) {
|
||||
$parser->getOutput()->addModules( [ 'ext.tabberNeue.codex' ] );
|
||||
} else {
|
||||
|
@ -73,34 +83,36 @@ class Tabber {
|
|||
*/
|
||||
public static function render( string $input, Parser $parser, PPFrame $frame ): string {
|
||||
$arr = explode( '|-|', $input );
|
||||
$htmlTabs = '';
|
||||
$tabs = '';
|
||||
$tabpanels = '';
|
||||
|
||||
foreach ( $arr as $tab ) {
|
||||
$tabData = self::getTabData( $tab, $parser, $frame );
|
||||
if ( $tabData['label'] === '' ) {
|
||||
if ( $tabData === [] ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( self::$useCodex && self::$isNested ) {
|
||||
$htmlTabs .= self::getCodexNestedTabJSON( $tabData );
|
||||
$tabpanels .= self::getCodexNestedTabJSON( $tabData );
|
||||
continue;
|
||||
}
|
||||
|
||||
$htmlTabs .= self::buildTabpanel( $tabData );
|
||||
$tabs .= self::getTabHTML( $tabData );
|
||||
$tabpanels .= self::getTabpanelHTML( $tabData );
|
||||
}
|
||||
|
||||
if ( self::$useCodex && self::$isNested ) {
|
||||
$tab = rtrim( implode( '},', explode( '}', $htmlTabs ) ), ',' );
|
||||
$tab = strip_tags( html_entity_decode( $tab ) );
|
||||
$tab = str_replace( ',,', ',', $tab );
|
||||
$tab = str_replace( ',]', ']', $tab );
|
||||
|
||||
return sprintf( '[%s]', $tab );
|
||||
$tabpanels = rtrim( implode( '},', explode( '}', $tabpanels ) ), ',' );
|
||||
$tabpanels = strip_tags( html_entity_decode( $tab ) );
|
||||
$tabpanels = str_replace( ',,', ',', $tabpanels );
|
||||
$tabpanels = str_replace( ',]', ']', $tabpanels );
|
||||
return sprintf( '[%s]', $tabpanels );
|
||||
}
|
||||
|
||||
return '<div class="tabber">' .
|
||||
return '<div class="tabber tabber--init">' .
|
||||
'<header class="tabber__header"></header>' .
|
||||
'<section class="tabber__section">' . $htmlTabs . '</section></div>';
|
||||
// '<header class="tabber__header"><nav class="tabber__tabs" role="tablist">' . $tabs . '</nav></header>' .
|
||||
'<section class="tabber__section">' . $tabpanels . '</section></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -125,7 +137,6 @@ class Tabber {
|
|||
// Might contains HTML
|
||||
$label = $parser->recursiveTagParseFully( $label );
|
||||
$label = $parser->stripOuterParagraph( $label );
|
||||
$label = htmlentities( $label );
|
||||
}
|
||||
return $label;
|
||||
}
|
||||
|
@ -144,21 +155,26 @@ class Tabber {
|
|||
if ( $content === '' ) {
|
||||
return '';
|
||||
}
|
||||
// Fix #151, some wikitext magic
|
||||
$content = "\n" . $content . "\n";
|
||||
|
||||
if ( !self::$useCodex ) {
|
||||
$content = $parser->recursiveTagParse( $content, $frame );
|
||||
} else {
|
||||
// A nested tabber which should return json in codex
|
||||
if ( strpos( $content, '{{#tag:tabber' ) !== false ) {
|
||||
$wikitextListMarkers = [ '*', '#', ';', ':' ];
|
||||
$isWikitextList = in_array( substr( $content, 0, 1 ), $wikitextListMarkers );
|
||||
if ( $isWikitextList ) {
|
||||
// Fix #151, some wikitext magic
|
||||
// Seems like there is no way to get rid of the mw-empty-elt paragraphs sadly
|
||||
$content = "\n$content\n";
|
||||
}
|
||||
return $parser->recursiveTagParse( $content, $frame );
|
||||
}
|
||||
|
||||
// The outermost tabber that must be parsed fully in codex for correct json
|
||||
if ( strpos( $content, '{{#tag:tabber' ) === false ) {
|
||||
return $parser->recursiveTagParseFully( $content, $frame );
|
||||
}
|
||||
|
||||
self::$isNested = true;
|
||||
$content = $parser->recursiveTagParse( $content, $frame );
|
||||
self::$isNested = false;
|
||||
// The outermost tabber that must be parsed fully in codex for correct json
|
||||
} else {
|
||||
$content = $parser->recursiveTagParseFully( $content, $frame );
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
@ -172,10 +188,7 @@ class Tabber {
|
|||
* @return array<string, string>
|
||||
*/
|
||||
private static function getTabData( string $tab, Parser $parser, PPFrame $frame ): array {
|
||||
$data = [
|
||||
'label' => '',
|
||||
'content' => ''
|
||||
];
|
||||
$data = [];
|
||||
if ( empty( trim( $tab ) ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
@ -189,28 +202,47 @@ class Tabber {
|
|||
}
|
||||
|
||||
$data['content'] = self::getTabContent( $content, $parser, $frame );
|
||||
|
||||
$id = Sanitizer::escapeIdForAttribute( htmlspecialchars( $data['label'] ) ) . '-' . self::$count;
|
||||
$data['id'] = $id;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build individual tabpanel.
|
||||
* Get the HTML for a tab.
|
||||
*
|
||||
* @param array $tabData Tab data
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
private static function buildTabpanel( array $tabData ): string {
|
||||
$label = $tabData['label'];
|
||||
$content = $tabData['content'];
|
||||
private static function getTabHTML( array $tabData ): string {
|
||||
return Html::rawElement( 'a', [
|
||||
'class' => 'tabber__tab',
|
||||
'id' => "tabber-tab-{$tabData['id']}",
|
||||
'href' => "#tabber-tabpanel-{$tabData['id']}",
|
||||
'role' => 'tab',
|
||||
], $tabData['label'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTML for a tabpanel.
|
||||
*
|
||||
* @param array $tabData Tab data
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
private static function getTabpanelHTML( array $tabData ): string {
|
||||
$content = $tabData['content'];
|
||||
$isContentHTML = strpos( $content, '<' ) === 0;
|
||||
if ( $content && !$isContentHTML ) {
|
||||
// If $content does not have any HTML element (i.e. just a text node), wrap it in <p/>
|
||||
$content = '<p>' . $content . '</p>';
|
||||
$content = Html::rawElement( 'p', [], $content );
|
||||
}
|
||||
|
||||
return '<article class="tabber__panel" data-mw-tabber-title="' . $label .
|
||||
'">' . $content . "</article>";
|
||||
return Html::rawElement( 'article', [
|
||||
'class' => 'tabber__panel',
|
||||
// 'id' => "tabber-tabpanel-{$tabData['id']}",
|
||||
'data-mw-tabber-title' => $tabData['label'],
|
||||
], $content );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -39,7 +39,7 @@ class TabberTransclude {
|
|||
}
|
||||
|
||||
$parser->getOutput()->addModuleStyles( [ 'ext.tabberNeue.init.styles' ] );
|
||||
$parser->getOutput()->addModules( [ 'ext.tabberNeue' ] );
|
||||
//$parser->getOutput()->addModules( [ 'ext.tabberNeue' ] );
|
||||
|
||||
$parser->addTrackingCategory( 'tabberneue-tabbertransclude-category' );
|
||||
return $html;
|
||||
|
@ -60,7 +60,7 @@ class TabberTransclude {
|
|||
$htmlTabs = '';
|
||||
foreach ( $arr as $tab ) {
|
||||
$tabData = self::getTabData( $tab );
|
||||
if ( $tabData['label'] === '' ) {
|
||||
if ( $tabData === [] ) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
@ -84,10 +84,7 @@ class TabberTransclude {
|
|||
* @return array<string, string>
|
||||
*/
|
||||
private static function getTabData( string $tab ): array {
|
||||
$data = [
|
||||
'label' => '',
|
||||
'content' => ''
|
||||
];
|
||||
$data = [];
|
||||
if ( empty( trim( $tab ) ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
|
|
@ -1,72 +1,92 @@
|
|||
/**
|
||||
* Critial rendering styles
|
||||
*
|
||||
* Since ext.tabberNeue is loaded a while after page load,
|
||||
* inline styles are needed to create an inital state and
|
||||
* avoid potential layout shifts. This should be kept as
|
||||
* small as possible.
|
||||
*/
|
||||
.tabber {
|
||||
&__header {
|
||||
box-shadow: inset 0 -1px 0 0 var( --border-color-base, #a2a9b1 );
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
&__tabs {
|
||||
display: flex;
|
||||
overflow: auto hidden;
|
||||
}
|
||||
|
||||
/* Only apply skeleton UI when Tabber will be loaded */
|
||||
.client-js {
|
||||
.tabber:not( .tabber--live ) {
|
||||
.tabber__header {
|
||||
height: 2.6em;
|
||||
box-shadow: inset 0 -1px 0 0;
|
||||
opacity: 0.1;
|
||||
&__tab {
|
||||
padding: 0.5em 0.75em;
|
||||
color: var( --color-base, #202122 );
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
width: 16ch;
|
||||
height: 0.5em;
|
||||
border-radius: 40px;
|
||||
margin-top: 1em;
|
||||
margin-left: 0.75em;
|
||||
animation-duration: 10s;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-name: skeletonload;
|
||||
animation-timing-function: linear;
|
||||
background: #000;
|
||||
background: linear-gradient( to right, #202122 8%, #54595d 18%, #202122 33% );
|
||||
/* Use double quote in PHP */
|
||||
content: '';
|
||||
&,
|
||||
&:visited {
|
||||
color: var( --color-base, #202122 );
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@media ( hover: hover ) {
|
||||
color: var( --color-progressive--hover, #447ff5 );
|
||||
box-shadow: inset 0 -2px 0 0 var( --box-shadow-color-progressive-selected--hover, #447ff5 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Avoid layout shift by assigning the grid property early on
|
||||
* Because display:block does not take into account of bottom margin of the content
|
||||
*/
|
||||
.tabber__section {
|
||||
&:active {
|
||||
@media ( hover: hover ) {
|
||||
color: var( --color-progressive--active, #2a4b8d );
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
block-size: 100%;
|
||||
grid-auto-columns: 100%;
|
||||
grid-auto-flow: column;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide all other panels
|
||||
* All panels are stacked vertically initially
|
||||
* then panels are stacked horizontally after Tabber is loaded
|
||||
* Causing lots of layout shift
|
||||
*/
|
||||
.tabber__panel:not( :first-child ) {
|
||||
&__panel {
|
||||
height: max-content;
|
||||
overflow-x: auto;
|
||||
scroll-snap-align: start;
|
||||
|
||||
// Hide edit buttons for non-transclusion tabs since they don't work
|
||||
/* stylelint-disable-next-line selector-class-pattern */
|
||||
&:not( [ data-mw-tabber-page-title ] ) .mw-editsection {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide no script message */
|
||||
.tabber__noscript {
|
||||
display: none;
|
||||
// Set tabber height to the height of first tabpanel by
|
||||
// setting subsequent tabpanels to have 0 height
|
||||
.client-js {
|
||||
.tabber {
|
||||
&__tabs {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeletonload {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
&--init {
|
||||
.tabber__panel ~ .tabber__panel {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100em 0;
|
||||
// Basic nojs support
|
||||
.client-nojs {
|
||||
.tabber__panel {
|
||||
scroll-padding-top: 3rem;
|
||||
height: auto;
|
||||
}
|
||||
}
|
|
@ -655,6 +655,7 @@ class TabberBuilder {
|
|||
const tabberEvent = new TabberEvent( this.tabber, this.tablist );
|
||||
tabberEvent.init();
|
||||
|
||||
this.tabber.classList.remove( 'tabber--init' );
|
||||
this.tabber.classList.add( 'tabber--live' );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,26 +16,6 @@
|
|||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
|
||||
&__tabs {
|
||||
display: flex;
|
||||
overflow: auto hidden;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__section {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
block-size: 100%;
|
||||
grid-auto-columns: 100%;
|
||||
grid-auto-flow: column;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
@ -103,16 +83,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__header,
|
||||
&__section {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
display: none;
|
||||
margin-top: ~'calc( var( --tabber-height-indicator ) * -1 )';
|
||||
|
@ -126,49 +96,17 @@
|
|||
}
|
||||
|
||||
&__tab {
|
||||
padding: 0.5em 0.75em;
|
||||
color: var( --tabber-color );
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:visited {
|
||||
color: var( --tabber-color );
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[ aria-selected='true' ] {
|
||||
box-shadow: 0 -2px 0 var( --tabber-color-progressive ) inset;
|
||||
box-shadow: 0 -2px 0 var( --color-progressive, #36c ) inset;
|
||||
}
|
||||
|
||||
&[ aria-selected='true' ],
|
||||
&[ aria-selected='true' ]:visited {
|
||||
color: var( --tabber-color-progressive );
|
||||
}
|
||||
}
|
||||
|
||||
&__tabs--animate {
|
||||
.tabber__tab[ aria-selected='true' ] {
|
||||
box-shadow: none;
|
||||
color: var( --color-progressive, #36c );
|
||||
}
|
||||
}
|
||||
|
||||
&__panel {
|
||||
height: max-content;
|
||||
overflow-x: auto;
|
||||
scroll-snap-align: start;
|
||||
|
||||
// Hide edit buttons for non-transclusion tabs since they don't work
|
||||
/* stylelint-disable-next-line selector-class-pattern */
|
||||
&:not( [ data-mw-tabber-page-title ] ) .mw-editsection {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
.tabber__transclusion {
|
||||
opacity: 0.1;
|
||||
|
@ -213,10 +151,6 @@
|
|||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
&__indicator {
|
||||
transition: transform 250ms ease, width 250ms ease;
|
||||
}
|
||||
|
||||
&__section,
|
||||
&__tabs {
|
||||
@media ( min-width: 720px ) {
|
||||
|
@ -244,16 +178,6 @@
|
|||
|
||||
@media ( hover: hover ) {
|
||||
.tabber {
|
||||
&__tab {
|
||||
&:hover {
|
||||
color: var( --tabber-color-progressive--hover );
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var( --tabber-color-progressive--active );
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
&__prev,
|
||||
&__next {
|
||||
|
|
Loading…
Reference in a new issue