Merge "Annotate different parts of the contributions UI with classes"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 28 Mar 2019 00:46:55 +0000 (00:46 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 28 Mar 2019 00:46:55 +0000 (00:46 +0000)
HISTORY
RELEASE-NOTES-1.33
includes/deferred/LinksDeletionUpdate.php
includes/deferred/LinksUpdate.php
includes/htmlform/fields/HTMLDateTimeField.php
includes/libs/objectcache/RedisBagOStuff.php
includes/libs/objectcache/WANObjectCache.php
includes/specials/forms/PreferencesFormOOUI.php
resources/src/mediawiki.special.preferences.ooui/tabs.js
resources/src/mediawiki.special.preferences.styles.ooui.less

diff --git a/HISTORY b/HISTORY
index e87facd..7895316 100644 (file)
--- a/HISTORY
+++ b/HISTORY
@@ -16487,6 +16487,39 @@ place on the development trunk and will appear in the next quarterly release.
 Those wishing to use the latest code instead of a branch release can [[Download
 from SVN|obtain it from source control]].
 
+=== What's new in 1.6 ===
+
+'''User interface:'''
+* The account creation form has been separated from the user login form.
+* Page protection/unprotection uses a new, expanded form
+
+'''Templates:'''
+* Categories and "what links here" now update as expected when adding or
+removing links in a template.
+* Template parameters can now have default values, as <nowiki>{{{name|default
+value}}}</nowiki>
+
+'''Uploads:'''
+* Optional support for rasterizing SVG images to PNG for inline display
+
+'''Feeds:'''
+* Feed generation upgraded to Atom 1.0
+* Diffs in RSS and Atom feeds are now colored for improved readability.
+
+'''Database:'''
+* MySQL 3.23.x support dropped; 4.0 or later required
+* Experimental support for Unicode mode of MySQL 4.1/5.0 (moderately tested)
+* Experimental Oracle support (not well tested!)
+
+'''Anti-spam extension support:'''
+* [[meta:SpamBlacklist extension|SpamBlacklist extension]] now has support for
+automated cleanup.
+* Support for a [[meta:ConfirmEdit extension|captcha extension]] to restrict
+automated spam edits.
+
+Numerous bug fixes and other behind-the-scenes changes have been made; see the
+file HISTORY for a complete change list.
+
 == Changes since 1.5 ==
 
 * (bug 2885) More PHP 5.1 fixes: skin, search, log, undelete
@@ -17231,39 +17264,6 @@ fully support the editing toolbar, but was found to be too confusing.
 * (bug 2139) Show page title in subtitle when viewing "read only" page
 * (bug 5452) Update language name for Cree
 
-=== What's new in 1.6 ===
-
-'''User interface:'''
-* The account creation form has been separated from the user login form.
-* Page protection/unprotection uses a new, expanded form
-
-'''Templates:'''
-* Categories and "what links here" now update as expected when adding or
-removing links in a template.
-* Template parameters can now have default values, as <nowiki>{{{name|default
-value}}}</nowiki>
-
-'''Uploads:'''
-* Optional support for rasterizing SVG images to PNG for inline display
-
-'''Feeds:'''
-* Feed generation upgraded to Atom 1.0
-* Diffs in RSS and Atom feeds are now colored for improved readability.
-
-'''Database:'''
-* MySQL 3.23.x support dropped; 4.0 or later required
-* Experimental support for Unicode mode of MySQL 4.1/5.0 (moderately tested)
-* Experimental Oracle support (not well tested!)
-
-'''Anti-spam extension support:'''
-* [[meta:SpamBlacklist extension|SpamBlacklist extension]] now has support for
-automated cleanup.
-* Support for a [[meta:ConfirmEdit extension|captcha extension]] to restrict
-automated spam edits.
-
-Numerous bug fixes and other behind-the-scenes changes have been made; see the
-file HISTORY for a complete change list.
-
 == Compatibility ==
 
 Older PHP 4.2 and 4.1 releases are no longer supported; PHP 4 users must
@@ -17302,7 +17302,11 @@ Some output, particularly involving user-supplied inline HTML, may not produce
 recommended on live sites. (This must be set for MathML to display properly in
 Mozilla.)
 
-----
+
+= MediaWiki 1.5 =
+
+== MediaWiki 1.5.9 ==
+* (bug 3359) Add hooks on completion of file upload
 
 == MediaWiki 1.5.8 ==
 
index cfaf82d..d3a09c5 100644 (file)
@@ -412,6 +412,11 @@ because of Phabricator reports.
   changed to explicitly cast. Subclasses relying on the base-class
   implementation should check whether they need to override it now.
 * BagOStuff::add is now abstract and must explicitly be defined in subclasses.
+* LinksDeletionUpdate is now a subclass of LinksUpdate. As a consequence,
+  the following hooks will now be triggered upon page deletion in addition
+  to page updates: LinksUpdateConstructed, LinksUpdate, LinksUpdateComplete.
+  LinksUpdateAfterInsert is not triggered since deletions do not cause
+  insertions into links tables.
 
 == Compatibility ==
 MediaWiki 1.33 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
index 4444bac..0e24134 100644 (file)
  */
 use MediaWiki\MediaWikiServices;
 use Wikimedia\ScopedCallback;
-use Wikimedia\Rdbms\IDatabase;
 
 /**
  * Update object handling the cleanup of links tables after a page was deleted.
  */
-class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
+class LinksDeletionUpdate extends LinksUpdate implements EnqueueableDataUpdate {
        /** @var WikiPage */
        protected $page;
-       /** @var int */
-       protected $pageId;
        /** @var string */
        protected $timestamp;
 
-       /** @var IDatabase */
-       private $db;
-
        /**
         * @param WikiPage $page Page we are updating
         * @param int|null $pageId ID of the page we are updating [optional]
@@ -44,63 +38,37 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
         * @throws MWException
         */
        function __construct( WikiPage $page, $pageId = null, $timestamp = null ) {
-               parent::__construct();
-
                $this->page = $page;
                if ( $pageId ) {
-                       $this->pageId = $pageId; // page ID at time of deletion
+                       $this->mId = $pageId; // page ID at time of deletion
                } elseif ( $page->exists() ) {
-                       $this->pageId = $page->getId();
+                       $this->mId = $page->getId();
                } else {
                        throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" );
                }
 
                $this->timestamp = $timestamp ?: wfTimestampNow();
+
+               $fakePO = new ParserOutput();
+               $fakePO->setCacheTime( $timestamp );
+               parent::__construct( $page->getTitle(), $fakePO, false );
        }
 
-       public function doUpdate() {
+       protected function doIncrementalUpdate() {
                $services = MediaWikiServices::getInstance();
                $config = $services->getMainConfig();
                $lbFactory = $services->getDBLoadBalancerFactory();
                $batchSize = $config->get( 'UpdateRowsPerQuery' );
 
-               // Page may already be deleted, so don't just getId()
-               $id = $this->pageId;
-
-               if ( $this->ticket ) {
-                       // Make sure all links update threads see the changes of each other.
-                       // This handles the case when updates have to batched into several COMMITs.
-                       $scopedLock = LinksUpdate::acquirePageLock( $this->getDB(), $id );
-                       if ( !$scopedLock ) {
-                               throw new RuntimeException( "Could not acquire lock for page ID '{$id}'." );
-                       }
-               }
+               $id = $this->mId;
+               $title = $this->mTitle;
 
-               $title = $this->page->getTitle();
                $dbw = $this->getDB(); // convenience
 
-               // Delete restrictions for it
-               $dbw->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
+               parent::doIncrementalUpdate();
 
-               // Fix category table counts
-               $cats = $dbw->selectFieldValues(
-                       'categorylinks',
-                       'cl_to',
-                       [ 'cl_from' => $id ],
-                       __METHOD__
-               );
-               $catBatches = array_chunk( $cats, $batchSize );
-               foreach ( $catBatches as $catBatch ) {
-                       $this->page->updateCategoryCounts( [], $catBatch, $id );
-                       if ( count( $catBatches ) > 1 ) {
-                               // Only sacrifice atomicity if necessary due to size
-                               $lbFactory->commitAndWaitForReplication(
-                                       __METHOD__, $this->ticket, [ 'domain' => $dbw->getDomainID() ]
-                               );
-                       }
-               }
-
-               // Refresh counts on categories that should be empty now
+               // Typically, a category is empty when deleted, so check that we don't leave
+               // spurious row in the category table.
                if ( $title->getNamespace() === NS_CATEGORY ) {
                        // T166757: do the update after the main job DB commit
                        DeferredUpdates::addCallableUpdate( function () use ( $title ) {
@@ -109,52 +77,11 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
                        } );
                }
 
-               $this->batchDeleteByPK(
-                       'pagelinks',
-                       [ 'pl_from' => $id ],
-                       [ 'pl_from', 'pl_namespace', 'pl_title' ],
-                       $batchSize
-               );
-               $this->batchDeleteByPK(
-                       'imagelinks',
-                       [ 'il_from' => $id ],
-                       [ 'il_from', 'il_to' ],
-                       $batchSize
-               );
-               $this->batchDeleteByPK(
-                       'categorylinks',
-                       [ 'cl_from' => $id ],
-                       [ 'cl_from', 'cl_to' ],
-                       $batchSize
-               );
-               $this->batchDeleteByPK(
-                       'templatelinks',
-                       [ 'tl_from' => $id ],
-                       [ 'tl_from', 'tl_namespace', 'tl_title' ],
-                       $batchSize
-               );
-               $this->batchDeleteByPK(
-                       'externallinks',
-                       [ 'el_from' => $id ],
-                       [ 'el_id' ],
-                       $batchSize
-               );
-               $this->batchDeleteByPK(
-                       'langlinks',
-                       [ 'll_from' => $id ],
-                       [ 'll_from', 'll_lang' ],
-                       $batchSize
-               );
-               $this->batchDeleteByPK(
-                       'iwlinks',
-                       [ 'iwl_from' => $id ],
-                       [ 'iwl_from', 'iwl_prefix', 'iwl_title' ],
-                       $batchSize
-               );
+               // Delete restrictions for the deleted page
+               $dbw->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ );
 
-               // Delete any redirect entry or page props entries
+               // Delete any redirect entry
                $dbw->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ );
-               $dbw->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ );
 
                // Find recentchanges entries to clean up...
                $rcIdsForTitle = $dbw->selectFieldValues(
@@ -191,46 +118,14 @@ class LinksDeletionUpdate extends DataUpdate implements EnqueueableDataUpdate {
                ScopedCallback::consume( $scopedLock );
        }
 
-       private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) {
-               $services = MediaWikiServices::getInstance();
-               $lbFactory = $services->getDBLoadBalancerFactory();
-               $dbw = $this->getDB(); // convenience
-
-               $res = $dbw->select( $table, $pk, $conds, __METHOD__ );
-
-               $pkDeleteConds = [];
-               foreach ( $res as $row ) {
-                       $pkDeleteConds[] = $dbw->makeList( (array)$row, LIST_AND );
-                       if ( count( $pkDeleteConds ) >= $bSize ) {
-                               $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
-                               $lbFactory->commitAndWaitForReplication(
-                                       __METHOD__, $this->ticket, [ 'domain' => $dbw->getDomainID() ]
-                               );
-                               $pkDeleteConds = [];
-                       }
-               }
-
-               if ( $pkDeleteConds ) {
-                       $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ );
-               }
-       }
-
-       protected function getDB() {
-               if ( !$this->db ) {
-                       $this->db = wfGetDB( DB_MASTER );
-               }
-
-               return $this->db;
-       }
-
        public function getAsJobSpecification() {
                return [
                        'domain' => $this->getDB()->getDomainID(),
                        'job' => new JobSpecification(
                                'deleteLinks',
-                               [ 'pageId' => $this->pageId, 'timestamp' => $this->timestamp ],
+                               [ 'pageId' => $this->mId, 'timestamp' => $this->timestamp ],
                                [ 'removeDuplicates' => true ],
-                               $this->page->getTitle()
+                               $this->mTitle
                        )
                ];
        }
index 045795e..14f86b7 100644 (file)
@@ -122,7 +122,11 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
                parent::__construct();
 
                $this->mTitle = $title;
-               $this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
+
+               if ( !$this->mId ) {
+                       // NOTE: subclasses may initialize mId before calling this constructor!
+                       $this->mId = $title->getArticleID( Title::GAID_FOR_UPDATE );
+               }
 
                if ( !$this->mId ) {
                        throw new InvalidArgumentException(
@@ -1180,7 +1184,7 @@ class LinksUpdate extends DataUpdate implements EnqueueableDataUpdate {
        /**
         * @return IDatabase
         */
-       private function getDB() {
+       protected function getDB() {
                if ( !$this->db ) {
                        $this->db = wfGetDB( DB_MASTER );
                }
index 7b59a1d..ffdf5f8 100644 (file)
@@ -171,11 +171,19 @@ class HTMLDateTimeField extends HTMLTextField {
                        }
                }
 
-               return new MediaWiki\Widget\DateTimeInputWidget( $params );
+               if ( $this->mType === 'date' ) {
+                       return new MediaWiki\Widget\DateInputWidget( $params );
+               } else {
+                       return new MediaWiki\Widget\DateTimeInputWidget( $params );
+               }
        }
 
        protected function getOOUIModules() {
-               return [ 'mediawiki.widgets.datetime' ];
+               if ( $this->mType === 'date' ) {
+                       return [ 'mediawiki.widgets.DateInputWidget' ];
+               } else {
+                       return [ 'mediawiki.widgets.datetime' ];
+               }
        }
 
        protected function shouldInfuseOOUI() {
index 79859db..2c74d45 100644 (file)
@@ -325,21 +325,29 @@ class RedisBagOStuff extends BagOStuff {
                return $result;
        }
 
-       public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
+       public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
                list( $server, $conn ) = $this->getConnection( $key );
                if ( !$conn ) {
                        return false;
                }
 
-               $expiry = $this->convertToRelative( $expiry );
+               $relative = $this->expiryIsRelative( $exptime );
                try {
-                       $result = $conn->expire( $key, $expiry );
+                       if ( $exptime == 0 ) {
+                               $result = $conn->persist( $key );
+                               $this->logRequest( 'persist', $key, $server, $result );
+                       } elseif ( $relative ) {
+                               $result = $conn->expire( $key, $this->convertToRelative( $exptime ) );
+                               $this->logRequest( 'expire', $key, $server, $result );
+                       } else {
+                               $result = $conn->expireAt( $key, $this->convertToExpiry( $exptime ) );
+                               $this->logRequest( 'expireAt', $key, $server, $result );
+                       }
                } catch ( RedisException $e ) {
                        $result = false;
                        $this->handleException( $conn, $e );
                }
 
-               $this->logRequest( 'expire', $key, $server, $result );
                return $result;
        }
 
index ba21156..87bccc5 100644 (file)
@@ -1150,10 +1150,17 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
         *      It is generally preferable to use a class constant when setting this value.
         *      This has no effect unless pcTTL is used.
         *      Default: WANObjectCache::PC_PRIMARY.
-        *   - version: Integer version number. This allows for callers to make breaking changes to
-        *      how values are stored while maintaining compatability and correct cache purges. New
-        *      versions are stored alongside older versions concurrently. Avoid storing class objects
-        *      however, as this reduces compatibility (due to serialization).
+        *   - version: Integer version number. This lets callers make breaking changes to the format
+        *      of cached values without causing problems for sites that use non-instantaneous code
+        *      deployments. Old and new code will recognize incompatible versions and purges from
+        *      both old and new code will been seen by each other. When this method encounters an
+        *      incompatibly versioned value at the provided key, a "variant key" will be used for
+        *      reading from and saving to cache. The variant key is specific to the key and version
+        *      number provided to this method. If the variant key value is older than that of the
+        *      provided key, or the provided key is non-existant, then the variant key will be seen
+        *      as non-existant. Therefore, delete() calls invalidate the provided key's variant keys.
+        *      The "checkKeys" and "touchedCallback" options still apply to variant keys as usual.
+        *      Avoid storing class objects, as this reduces compatibility (due to serialization).
         *      Default: null.
         *   - minAsOf: Reject values if they were generated before this UNIX timestamp.
         *      This is useful if the source of a key is suspected of having possibly changed
index 81abf1c..fd98dcb 100644 (file)
@@ -117,45 +117,12 @@ class PreferencesFormOOUI extends OOUIHTMLForm {
        }
 
        protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
-               // to get a user visible effect, wrap the fieldset into a framed panel layout
-               if ( $isRoot ) {
-                       // Mimic TabPanelLayout
-                       $wrapper = new OOUI\PanelLayout( [
-                               'expanded' => false,
-                               'scrollable' => true,
-                               // Framed and padded for no-JS, frame hidden with CSS
-                               'framed' => true,
-                               'infusable' => false,
-                               'classes' => [ 'oo-ui-stackLayout oo-ui-indexLayout-stackLayout' ]
-                       ] );
-                       $layout = new OOUI\PanelLayout( [
-                               'expanded' => false,
-                               'scrollable' => true,
-                               'infusable' => false,
-                               'classes' => [ 'oo-ui-tabPanelLayout' ]
-                       ] );
-                       $wrapper->appendContent( $layout );
-               } else {
-                       $wrapper = $layout = new OOUI\PanelLayout( [
-                               'expanded' => false,
-                               'padded' => true,
-                               'framed' => true,
-                               'infusable' => false,
-                       ] );
-               }
+               $layout = parent::wrapFieldSetSection( $legend, $section, $attributes, $isRoot );
 
-               $layout->appendContent(
-                       new OOUI\FieldsetLayout( [
-                               'label' => $legend,
-                               'infusable' => false,
-                               'items' => [
-                                       new OOUI\Widget( [
-                                               'content' => new OOUI\HtmlSnippet( $section )
-                                       ] ),
-                               ],
-                       ] + $attributes )
-               );
-               return $wrapper;
+               $layout->addClasses( [ 'mw-prefs-fieldset-wrapper' ] );
+               $layout->removeClasses( [ 'oo-ui-panelLayout-framed' ] );
+
+               return $layout;
        }
 
        /**
@@ -163,61 +130,50 @@ class PreferencesFormOOUI extends OOUIHTMLForm {
         * @return string
         */
        function getBody() {
-               // Construct fake tabs to avoid FOUC. The structure mimics OOUI's tabPanelLayout.
-               // TODO: Consider creating an infusable TabPanelLayout in OOUI-PHP.
-               $fakeTabs = [];
-               foreach ( $this->getPreferenceSections() as $i => $key ) {
-                       $fakeTabs[] =
-                               Html::rawElement(
-                                       'div',
-                                       [
-                                               'class' =>
-                                                       'oo-ui-widget oo-ui-widget-enabled oo-ui-optionWidget ' .
-                                                       'oo-ui-tabOptionWidget oo-ui-labelElement' .
-                                                       ( $i === 0 ? ' oo-ui-optionWidget-selected' : '' )
+               $tabPanels = [];
+               foreach ( $this->mFieldTree as $key => $val ) {
+                       if ( !is_array( $val ) ) {
+                               wfDebug( __METHOD__ . " encountered a field not attached to a section: '$key'" );
+                               continue;
+                       }
+                       $label = $this->getLegend( $key );
+                       $content =
+                               $this->getHeaderText( $key ) .
+                               $this->displaySection( $this->mFieldTree[$key] ) .
+                               $this->getFooterText( $key );
+
+                       $tabPanels[] = new OOUI\TabPanelLayout( [
+                               'classes' => [ 'mw-htmlform-autoinfuse-lazy' ],
+                               'name' => 'mw-prefsection-' . $key,
+                               'label' => $label,
+                               'content' => new OOUI\FieldsetLayout( [
+                                       'classes' => [ 'mw-prefs-section-fieldset' ],
+                                       'label' => $label,
+                                       'items' => [
+                                               new OOUI\Widget( [
+                                                       'content' => new OOUI\HtmlSnippet( $content )
+                                               ] ),
                                        ],
-                                       Html::element(
-                                               'a',
-                                               [
-                                                       'class' => 'oo-ui-labelElement-label',
-                                                       // Make this a usable link instead of a span so the tabs
-                                                       // can be used before JS runs
-                                                       'href' => '#mw-prefsection-' . $key
-                                               ],
-                                               $this->getLegend( $key )
-                                       )
-                               );
+                               ] ),
+                               'expanded' => false,
+                               'framed' => true,
+                       ] );
                }
-               $fakeTabsHtml = Html::rawElement(
-                       'div',
-                       [ 'class' => 'oo-ui-layout oo-ui-panelLayout oo-ui-indexLayout-tabPanel' ],
-                       Html::rawElement(
-                               'div',
-                               [ 'class' => 'oo-ui-widget oo-ui-widget-enabled oo-ui-selectWidget ' .
-                                       'oo-ui-selectWidget-depressed oo-ui-tabSelectWidget' ],
-                               implode( $fakeTabs )
-                       )
-               );
-
-               return Html::rawElement(
-                       'div',
-                       [ 'class' => 'oo-ui-layout oo-ui-panelLayout oo-ui-panelLayout-framed mw-prefs-faketabs' ],
-                       Html::rawElement(
-                               'div',
-                               [ 'class' => 'oo-ui-layout oo-ui-menuLayout oo-ui-menuLayout-static ' .
-                                       'oo-ui-menuLayout-top oo-ui-menuLayout-showMenu oo-ui-indexLayout' ],
-                               Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'oo-ui-menuLayout-menu' ],
-                                       $fakeTabsHtml
-                               ) .
-                               Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'oo-ui-menuLayout-content mw-htmlform-autoinfuse-lazy' ],
-                                       $this->displaySection( $this->mFieldTree, '', 'mw-prefsection-' )
-                               )
-                       )
-               );
+
+               $indexLayout = new OOUI\IndexLayout( [
+                       'infusable' => true,
+                       'expanded' => false,
+                       'autoFocus' => false,
+                       'classes' => [ 'mw-prefs-tabs' ],
+               ] );
+               $indexLayout->addTabPanels( $tabPanels );
+
+               return new OOUI\PanelLayout( [
+                       'framed' => true,
+                       'expanded' => false,
+                       'classes' => [ 'mw-prefs-tabs-wrapper' ],
+                       'content' => $indexLayout
+               ] );
        }
 
        /**
index ffa9e42..71b343c 100644 (file)
@@ -3,9 +3,7 @@
  */
 ( function () {
        $( function () {
-               var $preferences, tabs, wrapper, previousTab, switchingNoHash;
-
-               $preferences = $( '#preferences' );
+               var tabs, previousTab, switchingNoHash;
 
                // Make sure the accessibility tip is focussable so that keyboard users take notice,
                // but hide it by default to reduce visual clutter.
                        } )
                        .insertBefore( '.mw-htmlform-ooui-wrapper' );
 
-               tabs = new OO.ui.IndexLayout( {
-                       expanded: false,
-                       // Do not remove focus from the tabs menu after choosing a tab
-                       autoFocus: false
-               } );
-
-               mw.config.get( 'wgPreferencesTabs' ).forEach( function ( tabConfig ) {
-                       var panel, $panelContents;
-
-                       panel = new OO.ui.TabPanelLayout( tabConfig.name, {
-                               expanded: false,
-                               label: tabConfig.label
-                       } );
-                       $panelContents = $( '#mw-prefsection-' + tabConfig.name );
-
-                       // Hide the unnecessary PHP PanelLayouts
-                       // (Do not use .remove(), as that would remove event handlers for everything inside them)
-                       $panelContents.parent().detach();
+               tabs = OO.ui.infuse( $( '.mw-prefs-tabs' ) );
 
-                       panel.$element.append( $panelContents );
-                       tabs.addTabPanels( [ panel ] );
-
-                       // Remove duplicate labels
-                       // (This must be after .addTabPanels(), otherwise the tab item doesn't exist yet)
-                       $panelContents.children( 'legend' ).remove();
-                       $panelContents.attr( 'aria-labelledby', panel.getTabItem().getElementId() );
-               } );
-
-               wrapper = new OO.ui.PanelLayout( {
-                       expanded: false,
-                       padded: false,
-                       framed: true
-               } );
-               wrapper.$element.append( tabs.$element );
-               $preferences.prepend( wrapper.$element );
-               $( '.mw-prefs-faketabs' ).remove();
+               tabs.$element.addClass( 'mw-prefs-tabs-infused' );
 
                function enhancePanel( panel ) {
                        if ( !panel.$element.data( 'mw-section-infused' ) ) {
-                               // mw-htmlform-autoinfuse-lazy class has been removed by replacing faketabs
+                               panel.$element.removeClass( 'mw-htmlform-autoinfuse-lazy' );
                                mw.hook( 'htmlform.enhance' ).fire( panel.$element );
                                panel.$element.data( 'mw-section-infused', true );
                        }
@@ -75,7 +40,7 @@
                        // Changing the hash apparently causes keyboard focus to be lost?
                        // Save and restore it. This makes no sense though.
                        active = document.activeElement;
-                       location.hash = '#mw-prefsection-' + panel.getName();
+                       location.hash = '#' + panel.getName();
                        if ( active ) {
                                active.focus();
                        }
@@ -86,7 +51,7 @@
 
                /**
                 * @ignore
-                * @param {string} name the name of a tab without the prefix ("mw-prefsection-")
+                * @param {string} name The name of a tab
                 * @param {boolean} [noHash] A hash will be set according to the current
                 *  open section. Use this flag to suppress this.
                 */
                                matchedElement, parentSection;
                        if ( hash.match( /^#mw-prefsection-[\w]+$/ ) ) {
                                mw.storage.session.remove( 'mwpreferences-prevTab' );
-                               switchPrefTab( hash.replace( '#mw-prefsection-', '' ) );
+                               switchPrefTab( hash.slice( 1 ) );
                        } else if ( hash.match( /^#mw-[\w-]+$/ ) ) {
                                matchedElement = document.getElementById( hash.slice( 1 ) );
                                parentSection = $( matchedElement ).parent().closest( '[id^="mw-prefsection-"]' );
                                if ( parentSection.length ) {
                                        mw.storage.session.remove( 'mwpreferences-prevTab' );
                                        // Switch to proper tab and scroll to selected item.
-                                       switchPrefTab( parentSection.attr( 'id' ).replace( 'mw-prefsection-', '' ), true );
+                                       switchPrefTab( parentSection.attr( 'id' ), true );
                                        matchedElement.scrollIntoView();
                                }
                        }
                        if ( hash.match( /^#mw-[\w-]+/ ) ) {
                                detectHash();
                        } else if ( hash === '' ) {
-                               switchPrefTab( 'personal', true );
+                               switchPrefTab( 'mw-prefsection-personal', true );
                        }
                } )
                        // Run the function immediately to select the proper tab on startup.
index b1931f4..532b9ca 100644 (file)
        overflow: hidden;
 }
 
-/* Most outer Panellayout:
- * Decrease contrast of `border` slightly as padding/border combination is sufficient
- * accessibility wise and focus of content is more important here. */
-#preferences .oo-ui-panelLayout-framed {
-       border-color: #c8ccd1;
-}
+.mw-prefs-tabs {
+       .mw-prefs-fieldset-wrapper {
+               padding-left: 0;
+               padding-right: 0;
+
+               &:first-child {
+                       padding-top: 0;
+               }
 
-#preferences .oo-ui-menuLayout .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
-       border-width: 0;
-       border-radius: 0;
-       padding-left: 0;
-       padding-right: 0;
-       box-shadow: none;
+               &:last-child {
+                       padding-bottom: 0;
+               }
+       }
 }
 
-.mw-prefs-faketabs > .oo-ui-menuLayout > .oo-ui-menuLayout-menu a {
-       color: inherit;
-       text-decoration: none;
+.mw-prefs-tabs-wrapper.oo-ui-panelLayout-framed,
+.mw-prefs-tabs > .oo-ui-menuLayout-content > .oo-ui-indexLayout-stackLayout > .oo-ui-tabPanelLayout {
+       /* Decrease contrast of `border` slightly as padding/border combination is sufficient
+        * accessibility wise and focus of content is more important here. */
+       border-color: #c8ccd1;
 }
 
-/* Disabled JavaScript */
+/* JavaScript disabled */
 .client-nojs {
-       /* Adjust the borders: frame each prefsection instead of the
-        * whole tabLayout wrapper */
-       #preferences .oo-ui-menuLayout .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed:first-child {
-               border-color: #c8ccd1;
-               border-width: 1px 0 0;
-       }
-
-       #preferences .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed:last-child {
-               padding-bottom: 0;
-               margin-bottom: 0;
-       }
-
-       /* Fake Tabs to address reflow */
-       .mw-prefs-faketabs {
+       // Disable .oo-ui-panelLayout-framed on outer wrapper
+       .mw-prefs-tabs-wrapper {
                border-width: 0;
                border-radius: 0;
-               .box-shadow( none );
+       }
 
-               > .oo-ui-menuLayout > .oo-ui-menuLayout-content > .oo-ui-stackLayout {
-                       margin-bottom: 1em;
+       .mw-prefs-tabs {
+               // Hide the tab menu when JS is disabled as we can't use this feature
+               > .oo-ui-menuLayout-menu {
+                       display: none;
                }
 
-               /* Hide the tab menu when JS is disabled as we can't use this feature */
-               > .oo-ui-menuLayout > .oo-ui-menuLayout-menu {
-                       display: none;
+               .mw-prefs-section-fieldset {
+                       // <legend> is hard to style, so apply border to top of group
+                       > .oo-ui-fieldsetLayout-group {
+                               padding-top: 1.5em;
+                               border-top: 1px solid #c8ccd1;
+                       }
+
+                       // Remove spacing between legend and underline
+                       &.oo-ui-labelElement > .oo-ui-fieldsetLayout-header > .oo-ui-labelElement-label {
+                               margin-bottom: 0;
+                       }
+               }
+
+               // Spacing between sections
+               > .oo-ui-menuLayout-content > .oo-ui-indexLayout-stackLayout > .oo-ui-tabPanelLayout {
+                       margin-bottom: 1em;
                }
        }
 }
 
-/* Enabled JavaScript
- * Hide top level legends when JS is enabled, as they will not be visible
- * when the real tabLayout is built */
-.client-js #preferences {
+/* JavaScript enabled */
+.client-js .mw-prefs-tabs {
        .oo-ui-tabPanelLayout {
-               padding-top: 0.5em;
+               // Panels don't need borders as the IndexLayout is inside a framed wrapper.
+               border: 0;
 
-               & > fieldset > legend {
+               // Hide section legend, only used in nojs mode
+               > fieldset > legend {
                        display: none;
                }
        }
 
-       .oo-ui-panelLayout-framed .oo-ui-panelLayout-framed {
-               margin-top: 2.286em; /* equals `32px` at `font-size: 14px;` */
-               margin-bottom: 0;
-               border-width: 0;
-               border-radius: 0;
-               padding: 0;
-               box-shadow: none;
-
-               &:first-child {
-                       margin-top: 0.85714286em;
-               }
-
-               .oo-ui-panelLayout-framed:first-child {
-                       margin-top: 0;
+       // Hide all but the first panel before infusion
+       &:not( .mw-prefs-tabs-infused ) {
+               .oo-ui-tabPanelLayout:not( :first-child ) {
+                       display: none;
                }
        }
-
-       > .oo-ui-panelLayout > .oo-ui-fieldsetLayout > .oo-ui-fieldsetLayout-header {
-               margin-bottom: 1em;
-       }
 }
 
 /* Make the "Basic information" section more compact */