diff --git a/includes/Tabber.php b/includes/Tabber.php
index 543c7b3..c6abcb6 100644
--- a/includes/Tabber.php
+++ b/includes/Tabber.php
@@ -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 '
' .
+ return '
' .
'' .
- '
';
+ // '' .
+ '
';
}
/**
@@ -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 ) {
- 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 );
+ $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;
return $content;
}
@@ -172,10 +188,7 @@ class Tabber {
* @return array
*/
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
- $content = '' . $content . '
';
+ $content = Html::rawElement( 'p', [], $content );
}
-
- return '' . $content . "";
+ return Html::rawElement( 'article', [
+ 'class' => 'tabber__panel',
+ // 'id' => "tabber-tabpanel-{$tabData['id']}",
+ 'data-mw-tabber-title' => $tabData['label'],
+ ], $content );
}
/**
diff --git a/includes/TabberTransclude.php b/includes/TabberTransclude.php
index d1be364..50a1c18 100644
--- a/includes/TabberTransclude.php
+++ b/includes/TabberTransclude.php
@@ -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
*/
private static function getTabData( string $tab ): array {
- $data = [
- 'label' => '',
- 'content' => ''
- ];
+ $data = [];
if ( empty( trim( $tab ) ) ) {
return $data;
}
diff --git a/modules/ext.tabberNeue.init/ext.tabberNeue.init.less b/modules/ext.tabberNeue.init/ext.tabberNeue.init.less
index 5b84b6f..18c424f 100644
--- a/modules/ext.tabberNeue.init/ext.tabberNeue.init.less
+++ b/modules/ext.tabberNeue.init/ext.tabberNeue.init.less
@@ -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 {
- display: grid;
+ &:active {
+ @media ( hover: hover ) {
+ color: var( --color-progressive--active, #2a4b8d );
+ }
}
- /**
- * 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 ) {
+ &,
+ &: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;
+ }
+
+ &__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;
+ }
+ }
+
+ &--init {
+ .tabber__panel ~ .tabber__panel {
+ height: 0;
+ }
+ }
}
}
-@keyframes skeletonload {
- 0% {
- background-position: 0 0;
+// Basic nojs support
+.client-nojs {
+ .tabber__panel {
+ scroll-padding-top: 3rem;
+ height: auto;
}
-
- 100% {
- background-position: 100em 0;
- }
-}
+}
\ No newline at end of file
diff --git a/modules/ext.tabberNeue/ext.tabberNeue.js b/modules/ext.tabberNeue/ext.tabberNeue.js
index 0de7b22..5a38a1d 100644
--- a/modules/ext.tabberNeue/ext.tabberNeue.js
+++ b/modules/ext.tabberNeue/ext.tabberNeue.js
@@ -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' );
}
}
diff --git a/modules/ext.tabberNeue/ext.tabberNeue.less b/modules/ext.tabberNeue/ext.tabberNeue.less
index 243ef34..4f90da9 100644
--- a/modules/ext.tabberNeue/ext.tabberNeue.less
+++ b/modules/ext.tabberNeue/ext.tabberNeue.less
@@ -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 {