mirror of
https://github.com/StarCitizenTools/mediawiki-skins-Citizen.git
synced 2024-11-28 08:10:45 +00:00
Merge pull request #281 from StarCitizenTools/dev
Implement collapsible section
This commit is contained in:
commit
1b7109b064
|
@ -34,6 +34,7 @@ Note that:
|
|||
Name | Description | Values | Default
|
||||
:--- | :--- | :--- | :---
|
||||
`$wgCitizenThemeDefault` | The default theme of the skin | `auto` - switch between light and dark according to OS/browser settings; `light`; `dark` | `auto`
|
||||
`$wgCitizenEnableCollapsibleSections` | Enables or disable collapsible sections on content pages | `true` - enable; `false` - disable | `true`
|
||||
`$wgCitizenShowPageTools` | The condition of page tools visibility | `true` - always visible; `login` - visible to logged-in users; `permission` - visible to users with the right permissions | `true`
|
||||
`$wgCitizenEnableDrawerSubSearch` | Enables the drawer search box for menu entries | `true` - enable; `false` - disable | `false`
|
||||
`$wgCitizenPortalAttach` | Label of the portal to attach links to upload and special pages to | string | `first`
|
||||
|
|
244
includes/Partials/BodyContent.php
Normal file
244
includes/Partials/BodyContent.php
Normal file
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
/**
|
||||
* Citizen - A responsive skin developed for the Star Citizen Wiki
|
||||
*
|
||||
* This file is part of Citizen.
|
||||
*
|
||||
* Citizen is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Citizen is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Citizen. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup Skins
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Citizen\Partials;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMXpath;
|
||||
use Html;
|
||||
|
||||
final class BodyContent extends Partial {
|
||||
|
||||
/**
|
||||
* The code below is largely based on the extension MobileFrontend
|
||||
* All credits go to the author and contributors of the project
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class name for collapsible section wrappers
|
||||
*/
|
||||
public const STYLE_COLLAPSIBLE_SECTION_CLASS = 'section-collapsible';
|
||||
|
||||
/**
|
||||
* List of tags that could be considered as section headers.
|
||||
* @var array
|
||||
*/
|
||||
private $topHeadingTags = [ "h1", "h2", "h3", "h4", "h5", "h6 " ];
|
||||
|
||||
/**
|
||||
* Rebuild the body content
|
||||
*
|
||||
* @param string $out OutputPage
|
||||
* @return string html
|
||||
*/
|
||||
public function buildBodyContent( $out ) {
|
||||
$printSource = Html::rawElement( 'div', [ 'class' => 'printfooter' ], $this->skin->printSource() );
|
||||
$htmlBodyContent = $bodyContent = $out->getHTML() . "\n" . $printSource;
|
||||
$title = $out->getTitle();
|
||||
|
||||
// Check if it is not the main page
|
||||
if ( !$title->isMainPage() ) {
|
||||
$doc = new DOMDocument;
|
||||
$doc->loadHTML( $bodyContent );
|
||||
|
||||
// Make top level sections
|
||||
$this->makeSections( $doc, $this->getTopHeadings( $doc ) );
|
||||
// Mark subheadings
|
||||
$this->markSubHeadings( $this->getSubHeadings( $doc ) );
|
||||
|
||||
$htmlBodyContent = $doc->saveHTML();
|
||||
}
|
||||
|
||||
$newBodyContent = $this->skin->wrapHTMLPublic( $title, $htmlBodyContent );
|
||||
|
||||
return $newBodyContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually splits splits the body of the document into sections
|
||||
*
|
||||
* @param DOMElement $doc representing the HTML of the current article. In the HTML the sections
|
||||
* should not be wrapped.
|
||||
* @param DOMElement[] $headings The headings returned by
|
||||
* @return DOMDocument
|
||||
*/
|
||||
private function makeSections( DOMDocument $doc, array $headings ) {
|
||||
$xpath = new DOMXpath( $doc );
|
||||
$containers = $xpath->query( 'body/div[@class="mw-parser-output"][1]' );
|
||||
|
||||
// Return if no parser output is found
|
||||
if ( !$containers->length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$container = $containers->item( 0 );
|
||||
$containerChild = $container->firstChild;
|
||||
$firstHeading = reset( $headings );
|
||||
$firstHeadingName = $firstHeading ? $firstHeading->nodeName : false;
|
||||
$sectionNumber = 0;
|
||||
$sectionBody = $this->createSectionBodyElement( $doc, $sectionNumber, false );
|
||||
|
||||
while ( $containerChild ) {
|
||||
$node = $containerChild;
|
||||
$containerChild = $containerChild->nextSibling;
|
||||
|
||||
// If we've found a top level heading, insert the previous section if
|
||||
// necessary and clear the container div.
|
||||
// Note well the use of DOMNode#nodeName here. Only DOMElement defines
|
||||
// DOMElement#tagName. So, if there's trailing text - represented by
|
||||
// DOMText - then accessing #tagName will trigger an error.
|
||||
if ( $node->nodeName === $firstHeadingName ) {
|
||||
// The heading we are transforming is always 1 section ahead of the
|
||||
// section we are currently processing
|
||||
/** @phan-suppress-next-line PhanTypeMismatchArgument DOMNode vs. DOMElement */
|
||||
$this->prepareHeading( $doc, $node, $sectionNumber + 1 );
|
||||
// Insert the previous section body and reset it for the new section
|
||||
$container->insertBefore( $sectionBody, $node );
|
||||
|
||||
$sectionNumber += 1;
|
||||
$sectionBody = $this->createSectionBodyElement( $doc, $sectionNumber );
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it is not a top level heading, keep appending the nodes to the
|
||||
// section body container.
|
||||
$sectionBody->appendChild( $node );
|
||||
}
|
||||
|
||||
// Append the last section body.
|
||||
$container->appendChild( $sectionBody );
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare section headings, add required classes
|
||||
*
|
||||
* @param DOMDocument $doc
|
||||
* @param DOMElement $heading
|
||||
* @param int $sectionNumber
|
||||
*/
|
||||
private function prepareHeading( DOMDocument $doc, DOMElement $heading, $sectionNumber ) {
|
||||
$className = $heading->hasAttribute( 'class' ) ? $heading->getAttribute( 'class' ) . ' ' : '';
|
||||
$heading->setAttribute( 'class', $className . 'section-heading' );
|
||||
|
||||
// prepend indicator - this avoids a reflow by creating a placeholder for a toggling indicator
|
||||
$indicator = $doc->createElement( 'div' );
|
||||
$indicator->setAttribute( 'class', 'section-toggle' );
|
||||
$indicator->setAttribute( 'role', 'button' );
|
||||
$heading->insertBefore( $indicator, $heading->firstChild );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Section body element
|
||||
*
|
||||
* @param DOMDocument $doc
|
||||
* @param int $sectionNumber
|
||||
*
|
||||
* @return DOMElement
|
||||
*/
|
||||
private function createSectionBodyElement( DOMDocument $doc, $sectionNumber ) {
|
||||
$sectionBody = $doc->createElement( 'section' );
|
||||
$sectionBody->setAttribute( 'class', self::STYLE_COLLAPSIBLE_SECTION_CLASS );
|
||||
$sectionBody->setAttribute( 'id', 'section-collapsible-' . $sectionNumber );
|
||||
return $sectionBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets top headings in the document.
|
||||
*
|
||||
* @param DOMDocument $doc
|
||||
* @return array An array first is the highest rank headings
|
||||
*/
|
||||
private function getTopHeadings( DOMDocument $doc ): array {
|
||||
$headings = [];
|
||||
|
||||
foreach ( $this->topHeadingTags as $tagName ) {
|
||||
$allTags = $doc->getElementsByTagName( $tagName );
|
||||
|
||||
foreach ( $allTags as $el ) {
|
||||
if ( $el->parentNode->getAttribute( 'class' ) !== 'toctitle' ) {
|
||||
$headings[] = $el;
|
||||
}
|
||||
}
|
||||
if ( $headings ) {
|
||||
return $headings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $headings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the subheadings for the approiate styles by adding
|
||||
* the <code>section-subheading</code> class to each of them, if it
|
||||
* hasn't already been added.
|
||||
*
|
||||
* @param DOMElement[] $headings Heading elements
|
||||
*/
|
||||
protected function markSubHeadings( array $headings ) {
|
||||
foreach ( $headings as $heading ) {
|
||||
$class = $heading->getAttribute( 'class' );
|
||||
if ( strpos( $class, 'section-subheading' ) === false ) {
|
||||
$heading->setAttribute(
|
||||
'class',
|
||||
ltrim( $class . ' section-subheading' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all subheadings in the document in rank order.
|
||||
*
|
||||
* @param DOMDocument $doc
|
||||
* @return DOMElement[]
|
||||
*/
|
||||
private function getSubHeadings( DOMDocument $doc ): array {
|
||||
$found = false;
|
||||
$subheadings = [];
|
||||
foreach ( $this->topHeadingTags as $tagName ) {
|
||||
$allTags = $doc->getElementsByTagName( $tagName );
|
||||
$elements = [];
|
||||
foreach ( $allTags as $el ) {
|
||||
if ( $el->parentNode->getAttribute( 'class' ) !== 'toctitle' ) {
|
||||
$elements[] = $el;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $elements ) {
|
||||
if ( !$found ) {
|
||||
$found = true;
|
||||
} else {
|
||||
$subheadings = array_merge( $subheadings, $elements );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $subheadings;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
*/
|
||||
|
||||
use Citizen\GetConfigTrait;
|
||||
use Citizen\Partials\BodyContent;
|
||||
use Citizen\Partials\Drawer;
|
||||
use Citizen\Partials\Footer;
|
||||
use Citizen\Partials\Header;
|
||||
|
@ -51,6 +52,7 @@ class SkinCitizen extends SkinMustache {
|
|||
public function __construct( $options = [] ) {
|
||||
$skin = $this;
|
||||
$out = $skin->getOutput();
|
||||
$title = $out->getTitle();
|
||||
|
||||
$metadata = new Metadata( $this );
|
||||
$skinTheme = new Theme( $this );
|
||||
|
@ -60,6 +62,35 @@ class SkinCitizen extends SkinMustache {
|
|||
// Theme handler
|
||||
$skinTheme->setSkinTheme( $options );
|
||||
|
||||
// Only load in content pages
|
||||
if ( $title->isContentPage() ) {
|
||||
// Load Citizen collapsible sections modules if enabled
|
||||
if ( $this->getConfigValue( 'CitizenEnableCollapsibleSections' ) === true ) {
|
||||
$options['scripts'] = array_merge(
|
||||
$options['scripts'],
|
||||
[ 'skins.citizen.scripts.sections' ]
|
||||
);
|
||||
$options['styles'] = array_merge(
|
||||
$options['styles'],
|
||||
[
|
||||
'skins.citizen.styles.sections',
|
||||
'skins.citizen.icons.sections'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Load table of content script if ToC presents
|
||||
if ( $out->isTOCEnabled() ) {
|
||||
// Add class to body that notifies the page has TOC
|
||||
$out->addBodyClasses( 'skin-citizen-has-toc' );
|
||||
// Disabled style condition loading due to pop in
|
||||
$options['scripts'] = array_merge(
|
||||
$options['scripts'],
|
||||
[ 'skins.citizen.scripts.toc' ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Citizen search suggestion styles if enabled
|
||||
if ( $this->getConfigValue( 'CitizenEnableSearch' ) === true ) {
|
||||
$options['styles'] = array_merge(
|
||||
|
@ -91,17 +122,6 @@ class SkinCitizen extends SkinMustache {
|
|||
);
|
||||
}
|
||||
|
||||
// Load table of content script if ToC presents
|
||||
if ( $out->isTOCEnabled() ) {
|
||||
// Add class to body that notifies the page has TOC
|
||||
$out->addBodyClasses( 'skin-citizen-has-toc' );
|
||||
// Disabled style condition loading due to pop in
|
||||
$options['scripts'] = array_merge(
|
||||
$options['scripts'],
|
||||
[ 'skins.citizen.scripts.toc' ]
|
||||
);
|
||||
}
|
||||
|
||||
$options['templateDirectory'] = __DIR__ . '/templates';
|
||||
parent::__construct( $options );
|
||||
}
|
||||
|
@ -116,6 +136,7 @@ class SkinCitizen extends SkinMustache {
|
|||
|
||||
$header = new Header( $this );
|
||||
$drawer = new Drawer( $this );
|
||||
$bodycontent = new BodyContent( $this );
|
||||
$footer = new Footer( $this );
|
||||
$tools = new PageTools( $this );
|
||||
|
||||
|
@ -161,6 +182,8 @@ class SkinCitizen extends SkinMustache {
|
|||
|
||||
'msg-tagline' => $this->msg( 'tagline' )->text(),
|
||||
|
||||
'html-body-content--formatted' => $bodycontent->buildBodyContent( $out ),
|
||||
|
||||
'data-footer' => $footer->getFooterData(),
|
||||
];
|
||||
}
|
||||
|
@ -201,6 +224,17 @@ class SkinCitizen extends SkinMustache {
|
|||
return parent::buildContentNavigationUrls();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change access to public, as it is used in partials
|
||||
*
|
||||
* @param Title $title
|
||||
* @param string $html body text
|
||||
* @return array
|
||||
*/
|
||||
final public function wrapHTMLPublic( $title, $html ) {
|
||||
return parent::wrapHTML( $title, $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $label to be used to derive the id and human readable label of the menu
|
||||
* If the key has an entry in the constant MENU_LABEL_KEYS then that message will be used for the
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<div id="bodyContent" class="mw-body-content">
|
||||
<div id="contentSub"{{{html-user-language-attributes}}}>{{{html-subtitle}}}</div>
|
||||
{{#html-undelete-link}}<div id="contentSub2">{{{html-undelete-link}}}</div>{{/html-undelete-link}}
|
||||
{{{html-body-content}}}
|
||||
{{{html-body-content--formatted}}}
|
||||
{{{html-categories}}}
|
||||
</div>
|
||||
</main>
|
||||
|
|
7
resources/skins.citizen.icons/shared/collapse.svg
Normal file
7
resources/skins.citizen.icons/shared/collapse.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<title>
|
||||
collapse
|
||||
</title>
|
||||
<path d="M2.5 15.25l7.5-7.5 7.5 7.5 1.5-1.5-9-9-9 9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 217 B |
19
resources/skins.citizen.scripts.sections/sections.js
Normal file
19
resources/skins.citizen.scripts.sections/sections.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
function bindClick( collToggle, collSection, i, j ) {
|
||||
return function () {
|
||||
j = i + 1;
|
||||
this.classList.toggle( 'section-toggle--collapsed' );
|
||||
collSection[ j ].classList.toggle( 'section-collapsible--collapsed' );
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
var collSection = document.getElementsByClassName( 'section-collapsible' ),
|
||||
collToggle = document.getElementsByClassName( 'section-toggle' ),
|
||||
i, j;
|
||||
|
||||
for ( i = 0; i < collToggle.length; i++ ) {
|
||||
collToggle[ i ].addEventListener( 'click', bindClick( collToggle, collSection, i, j ) );
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,64 @@
|
|||
@import '../variables.less';
|
||||
|
||||
.section {
|
||||
&-toggle {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 5px;
|
||||
margin-right: 10px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 18px;
|
||||
cursor: pointer;
|
||||
opacity: var( --opacity-icon-base );
|
||||
transition: @transition-opacity-quick, @transition-transform-quick;
|
||||
|
||||
&:hover {
|
||||
opacity: var( --opacity-icon-base--hover );
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: var( --opacity-icon-base--active );
|
||||
}
|
||||
|
||||
&--collapsed {
|
||||
transform: rotate3d( 1, 0, 0, 180deg );
|
||||
|
||||
~ .mw-headline {
|
||||
color: var( --color-base );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-heading {
|
||||
.mw-headline {
|
||||
transition: @transition-color-quick;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix button alignment
|
||||
&-heading,
|
||||
&-subheading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-collapsible {
|
||||
&--collapsed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skin-citizen-dark {
|
||||
.section-toggle {
|
||||
filter: invert( 1 );
|
||||
}
|
||||
}
|
||||
|
||||
// Hide toggle when client is noscript
|
||||
.client-nojs {
|
||||
.section-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
.mw-editsection {
|
||||
display: flex;
|
||||
margin-left: 10px;
|
||||
margin-left: auto;
|
||||
|
||||
a {
|
||||
.resource-loader-icon-link-small;
|
||||
|
|
30
skin.json
30
skin.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Citizen",
|
||||
"version": "1.3.5",
|
||||
"version": "1.4.0",
|
||||
"author": [
|
||||
"[https://www.mediawiki.org/wiki/User:Alistair3149 Alistair3149]",
|
||||
"[https://www.mediawiki.org/wiki/User:Octfx Octfx]"
|
||||
|
@ -139,6 +139,15 @@
|
|||
"features": [],
|
||||
"styles": [ "resources/skins.citizen.styles.toc/skins.citizen.styles.toc.less" ]
|
||||
},
|
||||
"skins.citizen.styles.sections": {
|
||||
"class": "ResourceLoaderSkinModule",
|
||||
"targets": [
|
||||
"desktop",
|
||||
"mobile"
|
||||
],
|
||||
"features": [],
|
||||
"styles": [ "resources/skins.citizen.styles.sections/skins.citizen.styles.sections.less" ]
|
||||
},
|
||||
"skins.citizen.scripts": {
|
||||
"packageFiles": [
|
||||
"resources/skins.citizen.scripts/skin.js",
|
||||
|
@ -189,6 +198,11 @@
|
|||
"resources/skins.citizen.scripts.drawer/drawerSubSearch.js"
|
||||
]
|
||||
},
|
||||
"skins.citizen.scripts.sections": {
|
||||
"scripts": [
|
||||
"resources/skins.citizen.scripts.sections/Sections.js"
|
||||
]
|
||||
},
|
||||
"skins.citizen.icons": {
|
||||
"class": "ResourceLoaderImageModule",
|
||||
"selector": "{name}",
|
||||
|
@ -306,6 +320,14 @@
|
|||
"light": "resources/skins.citizen.icons/shared/moon.svg",
|
||||
"dark": "resources/skins.citizen.icons/shared/bright.svg"
|
||||
}
|
||||
},
|
||||
"skins.citizen.icons.sections": {
|
||||
"class": "ResourceLoaderImageModule",
|
||||
"selector": ".section-toggle",
|
||||
"useDataURI": false,
|
||||
"images": {
|
||||
"collapse": "resources/skins.citizen.icons/shared/collapse.svg"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ResourceFileModulePaths": {
|
||||
|
@ -619,6 +641,12 @@
|
|||
"description": "Enables to search the drawer for menu entries",
|
||||
"descriptionmsg": "citizen-config-enabledrawersubsearch",
|
||||
"public": true
|
||||
},
|
||||
"EnableCollapsibleSections": {
|
||||
"value": true,
|
||||
"description": "Enables or disable collapsible sections on content pages",
|
||||
"descriptionmsg": "citizen-config-enablecollapsiblesections",
|
||||
"public": true
|
||||
}
|
||||
},
|
||||
"manifest_version": 2
|
||||
|
|
Loading…
Reference in a new issue