From: jenkins-bot Date: Wed, 8 Mar 2017 19:21:09 +0000 (+0000) Subject: Merge "Add some translations for Western Punjabi (pnb)" X-Git-Tag: 1.31.0-rc.0~3840 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=a4c1f2ee49b9425eb4cfada96b3e666c8832115c;hp=0bc81d74c0840e484709784d47f54d44cea1e30a Merge "Add some translations for Western Punjabi (pnb)" --- diff --git a/RELEASE-NOTES-1.29 b/RELEASE-NOTES-1.29 index 98834745f1..5bc66fd79e 100644 --- a/RELEASE-NOTES-1.29 +++ b/RELEASE-NOTES-1.29 @@ -63,6 +63,7 @@ production. * Updated cssjanus from v1.1.2 to 1.1.3. * Updated psr/log from v1.0.0 to v1.0.2. * Update Moment.js from v2.8.4 to v2.15.0. +* Updated oyejorge/less.php from v1.7.0.10 to v1.7.0.13. ==== New external libraries ==== diff --git a/autoload.php b/autoload.php index a16451d7a5..f96d898663 100644 --- a/autoload.php +++ b/autoload.php @@ -918,6 +918,10 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Tidy\\RaggettInternalHHVM' => __DIR__ . '/includes/tidy/RaggettInternalHHVM.php', 'MediaWiki\\Tidy\\RaggettInternalPHP' => __DIR__ . '/includes/tidy/RaggettInternalPHP.php', 'MediaWiki\\Tidy\\RaggettWrapper' => __DIR__ . '/includes/tidy/RaggettWrapper.php', + 'MediaWiki\\Tidy\\RemexCompatFormatter' => __DIR__ . '/includes/tidy/RemexCompatFormatter.php', + 'MediaWiki\\Tidy\\RemexCompatMunger' => __DIR__ . '/includes/tidy/RemexCompatMunger.php', + 'MediaWiki\\Tidy\\RemexDriver' => __DIR__ . '/includes/tidy/RemexDriver.php', + 'MediaWiki\\Tidy\\RemexMungerData' => __DIR__ . '/includes/tidy/RemexMungerData.php', 'MediaWiki\\Tidy\\TidyDriverBase' => __DIR__ . '/includes/tidy/TidyDriverBase.php', 'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php', 'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php', diff --git a/composer.json b/composer.json index fe68a61178..ce38914f3e 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,8 @@ "ext-xml": "*", "liuggio/statsd-php-client": "1.0.18", "mediawiki/at-ease": "1.1.0", - "oojs/oojs-ui": "0.19.4", - "oyejorge/less.php": "1.7.0.10", + "oojs/oojs-ui": "0.19.5", + "oyejorge/less.php": "1.7.0.13", "php": ">=5.5.9", "psr/log": "1.0.2", "wikimedia/assert": "0.2.2", @@ -38,6 +38,7 @@ "wikimedia/ip-set": "1.1.0", "wikimedia/php-session-serializer": "1.0.4", "wikimedia/relpath": "1.0.3", + "wikimedia/remex-html": "1.0.0", "wikimedia/running-stat": "1.1.0", "wikimedia/scoped-callback": "1.0.0", "wikimedia/utfnormal": "1.1.0", diff --git a/includes/collation/IcuCollation.php b/includes/collation/IcuCollation.php index bf1fe74d5e..e0eb1c13c8 100644 --- a/includes/collation/IcuCollation.php +++ b/includes/collation/IcuCollation.php @@ -330,7 +330,7 @@ class IcuCollation extends Collation { $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING ); $cacheKey = $cache->makeKey( 'first-letters', - get_class( $this ), + static::class, $this->locale, $this->digitTransformLanguage->getCode(), self::getICUVersion(), diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php index 829dd73f0b..ea5278fc06 100644 --- a/includes/context/ContextSource.php +++ b/includes/context/ContextSource.php @@ -39,7 +39,7 @@ abstract class ContextSource implements IContextSource { */ public function getContext() { if ( $this->context === null ) { - $class = get_class( $this ); + $class = static::class; wfDebug( __METHOD__ . " ($class): called and \$context is null. " . "Using RequestContext::getMain() for sanity\n" ); $this->context = RequestContext::getMain(); diff --git a/includes/exception/MWException.php b/includes/exception/MWException.php index e958c9449f..4ff8636e2e 100644 --- a/includes/exception/MWException.php +++ b/includes/exception/MWException.php @@ -112,7 +112,7 @@ class MWException extends Exception { "

\n"; } else { $logId = WebRequest::getRequestId(); - $type = get_class( $this ); + $type = static::class; return "
" . '[' . $logId . '] ' . gmdate( 'Y-m-d H:i:s' ) . ": " . @@ -164,7 +164,7 @@ class MWException extends Exception { if ( $this->useOutputPage() ) { $wgOut->prepareErrorPage( $this->getPageTitle() ); - $hookResult = $this->runHooks( get_class( $this ) ); + $hookResult = $this->runHooks( static::class ); if ( $hookResult ) { $wgOut->addHTML( $hookResult ); } else { @@ -183,7 +183,7 @@ class MWException extends Exception { '' . "\n"; - $hookResult = $this->runHooks( get_class( $this ) . 'Raw' ); + $hookResult = $this->runHooks( static::class . 'Raw' ); if ( $hookResult ) { echo $hookResult; } else { @@ -203,7 +203,7 @@ class MWException extends Exception { if ( defined( 'MW_API' ) ) { // Unhandled API exception, we can't be sure that format printer is alive - self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $this ) ); + self::header( 'MediaWiki-API-Error: internal_api_error_' . static::class ); wfHttpError( 500, 'Internal Server Error', $this->getText() ); } elseif ( self::isCommandLine() ) { $message = $this->getText(); diff --git a/includes/export/XmlDumpWriter.php b/includes/export/XmlDumpWriter.php index 52bf0f0910..5a1f92c4cc 100644 --- a/includes/export/XmlDumpWriter.php +++ b/includes/export/XmlDumpWriter.php @@ -433,6 +433,9 @@ class XmlDumpWriter { global $wgContLang; $prefix = $wgContLang->getFormattedNsText( $title->getNamespace() ); + // @todo Emit some kind of warning to the user if $title->getNamespace() !== + // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace) + if ( $prefix !== '' ) { $prefix .= ':'; } diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index ca417187e0..43f1d211b3 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -571,7 +571,7 @@ class ForeignAPIRepo extends FileRepo { $cache = ObjectCache::getMainWANInstance(); return $cache->getWithSetCallback( - $this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) ), + $this->getLocalCacheKey( static::class, $target, md5( $url ) ), $cacheTTL, function ( $curValue, &$ttl ) use ( $url, $cache ) { $html = self::httpGet( $url, 'default', [], $mtime ); @@ -593,13 +593,13 @@ class ForeignAPIRepo extends FileRepo { * @throws MWException */ function enumFiles( $callback ) { - throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) ); + throw new MWException( 'enumFiles is not supported by ' . static::class ); } /** * @throws MWException */ protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index f49ef88c5c..3e8850823e 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -138,7 +138,7 @@ class ForeignDBRepo extends LocalRepo { } protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } /** diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index a9cd030869..f83fd1c813 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -100,7 +100,7 @@ class ForeignDBViaLBRepo extends LocalRepo { } protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } public function getInfo() { diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index f2b7395c7b..1c12e0274a 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -33,6 +33,6 @@ class NullRepo extends FileRepo { } protected function assertWritableRepo() { - throw new MWException( get_class( $this ) . ': write operations are not supported.' ); + throw new MWException( static::class . ': write operations are not supported.' ); } } diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index 3b873eada1..f7e85a8a40 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -1766,7 +1766,7 @@ abstract class File implements IDBAccessObject { * @throws MWException */ function readOnlyError() { - throw new MWException( get_class( $this ) . ': write operations are not supported' ); + throw new MWException( static::class . ': write operations are not supported' ); } /** diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index a1cb0a2473..a633fd2f8d 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -391,7 +391,7 @@ class LocalFile extends File { * @param int $flags */ function loadFromDB( $flags = 0 ) { - $fname = get_class( $this ) . '::' . __FUNCTION__; + $fname = static::class . '::' . __FUNCTION__; # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->dataLoaded = true; @@ -416,7 +416,7 @@ class LocalFile extends File { * This covers fields that are sometimes not cached. */ protected function loadExtraFromDB() { - $fname = get_class( $this ) . '::' . __FUNCTION__; + $fname = static::class . '::' . __FUNCTION__; # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->extraDataLoaded = true; @@ -1100,7 +1100,7 @@ class LocalFile extends File { */ public function nextHistoryLine() { # Polymorphic function name to distinguish foreign and local fetches - $fname = get_class( $this ) . '::' . __FUNCTION__; + $fname = static::class . '::' . __FUNCTION__; $dbr = $this->repo->getReplicaDB(); diff --git a/includes/htmlform/HTMLFormField.php b/includes/htmlform/HTMLFormField.php index 487d6f647b..3a3146bc22 100644 --- a/includes/htmlform/HTMLFormField.php +++ b/includes/htmlform/HTMLFormField.php @@ -475,7 +475,7 @@ abstract class HTMLFormField { public function getTableRow( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); + $fieldType = static::class; $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = []; $rowAttributes = []; @@ -533,7 +533,7 @@ abstract class HTMLFormField { public function getDiv( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); + $fieldType = static::class; $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() ); $cellAttributes = []; $label = $this->getLabelHtml( $cellAttributes ); @@ -601,7 +601,7 @@ abstract class HTMLFormField { $infusable = false; } - $fieldType = get_class( $this ); + $fieldType = static::class; $help = $this->getHelpText(); $errors = $this->getErrorsRaw( $value ); foreach ( $errors as &$error ) { diff --git a/includes/htmlform/fields/HTMLCheckMatrix.php b/includes/htmlform/fields/HTMLCheckMatrix.php index 46172a581b..fa18a3cdad 100644 --- a/includes/htmlform/fields/HTMLCheckMatrix.php +++ b/includes/htmlform/fields/HTMLCheckMatrix.php @@ -189,7 +189,7 @@ class HTMLCheckMatrix extends HTMLFormField implements HTMLNestedFilterable { public function getTableRow( $value ) { list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value ); $inputHtml = $this->getInputHTML( $value ); - $fieldType = get_class( $this ); + $fieldType = static::class; $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); $cellAttributes = [ 'colspan' => 2 ]; diff --git a/includes/htmlform/fields/HTMLFormFieldCloner.php b/includes/htmlform/fields/HTMLFormFieldCloner.php index 8fb840a136..dd9184bf33 100644 --- a/includes/htmlform/fields/HTMLFormFieldCloner.php +++ b/includes/htmlform/fields/HTMLFormFieldCloner.php @@ -46,7 +46,7 @@ class HTMLFormFieldCloner extends HTMLFormField { protected $uniqueId; public function __construct( $params ) { - $this->uniqueId = get_class( $this ) . ++self::$counter . 'x'; + $this->uniqueId = static::class . ++self::$counter . 'x'; parent::__construct( $params ); if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) { diff --git a/includes/htmlform/fields/HTMLMultiSelectField.php b/includes/htmlform/fields/HTMLMultiSelectField.php index 23044bd6ff..2b6e0665d5 100644 --- a/includes/htmlform/fields/HTMLMultiSelectField.php +++ b/includes/htmlform/fields/HTMLMultiSelectField.php @@ -17,6 +17,11 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable public function __construct( $params ) { parent::__construct( $params ); + // If the disabled-options parameter is not provided, use an empty array + if ( isset( $this->mParams['disabled-options'] ) === false ) { + $this->mParams['disabled-options'] = []; + } + // For backwards compatibility, also handle the old way with 'cssclass' => 'mw-chosen' if ( isset( $params['dropdown'] ) || strpos( $this->mClass, 'mw-chosen' ) !== false ) { $this->mClass .= ' mw-htmlform-dropdown'; @@ -75,6 +80,9 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable 'id' => "{$this->mID}-$info", 'value' => $info, ]; + if ( in_array( $info, $this->mParams['disabled-options'], true ) ) { + $thisAttribs['disabled'] = 'disabled'; + } $checked = in_array( $info, $value, true ); $checkbox = $this->getOneCheckbox( $checked, $attribs + $thisAttribs, $label ); @@ -112,6 +120,18 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable } } + /** + * Get options and make them into arrays suitable for OOUI. + * @return array Options for inclusion in a select or whatever. + */ + public function getOptionsOOUI() { + $options = parent::getOptionsOOUI(); + foreach ( $options as &$option ) { + $option['disabled'] = in_array( $option['data'], $this->mParams['disabled-options'], true ); + } + return $options; + } + /** * Get the OOUI version of this field. * diff --git a/includes/installer/WebInstallerPage.php b/includes/installer/WebInstallerPage.php index 7a41cebe65..3aad6f8793 100644 --- a/includes/installer/WebInstallerPage.php +++ b/includes/installer/WebInstallerPage.php @@ -133,7 +133,7 @@ abstract class WebInstallerPage { * @return string */ public function getName() { - return str_replace( 'WebInstaller', '', get_class( $this ) ); + return str_replace( 'WebInstaller', '', static::class ); } /** diff --git a/includes/jobqueue/JobSpecification.php b/includes/jobqueue/JobSpecification.php index d636dc6516..d844795143 100644 --- a/includes/jobqueue/JobSpecification.php +++ b/includes/jobqueue/JobSpecification.php @@ -128,7 +128,7 @@ class JobSpecification implements IJobSpecification { $this->type = $type; $this->params = $params; - $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . get_class( $this ) ); + $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . static::class ); $this->opts = $opts; } diff --git a/includes/media/TransformationalImageHandler.php b/includes/media/TransformationalImageHandler.php index 60aec45729..1ab0f369db 100644 --- a/includes/media/TransformationalImageHandler.php +++ b/includes/media/TransformationalImageHandler.php @@ -569,7 +569,7 @@ abstract class TransformationalImageHandler extends ImageHandler { */ public function rotate( $file, $params ) { return new MediaTransformError( 'thumbnail_error', 0, 0, - get_class( $this ) . ' rotation not implemented' ); + static::class . ' rotation not implemented' ); } /** diff --git a/includes/pager/IndexPager.php b/includes/pager/IndexPager.php index dc302a2d62..46948909c6 100644 --- a/includes/pager/IndexPager.php +++ b/includes/pager/IndexPager.php @@ -197,7 +197,7 @@ abstract class IndexPager extends ContextSource implements Pager { */ public function doQuery() { # Use the child class name for profiling - $fname = __METHOD__ . ' (' . get_class( $this ) . ')'; + $fname = __METHOD__ . ' (' . static::class . ')'; $section = Profiler::instance()->scopedProfileIn( $fname ); // @todo This should probably compare to DIR_DESCENDING and DIR_ASCENDING constants @@ -348,7 +348,7 @@ abstract class IndexPager extends ContextSource implements Pager { * @return string */ function getSqlComment() { - return get_class( $this ); + return static::class; } /** diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index d4dabe7d71..a2b4b1d67f 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -843,7 +843,7 @@ abstract class ResourceLoaderModule implements LoggerAwareInterface { */ public function getDefinitionSummary( ResourceLoaderContext $context ) { return [ - '_class' => get_class( $this ), + '_class' => static::class, '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ), ]; } diff --git a/includes/session/SessionProvider.php b/includes/session/SessionProvider.php index e8d81cbfba..3cf69b7b33 100644 --- a/includes/session/SessionProvider.php +++ b/includes/session/SessionProvider.php @@ -455,7 +455,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI * @return string */ public function __toString() { - return get_class( $this ); + return static::class; } /** @@ -475,7 +475,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI */ protected function describeMessage() { return wfMessage( - 'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) ) + 'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) ) ); } diff --git a/includes/skins/Skin.php b/includes/skins/Skin.php index a7740a093d..3ef646aec9 100644 --- a/includes/skins/Skin.php +++ b/includes/skins/Skin.php @@ -1038,7 +1038,7 @@ abstract class Skin extends ContextSource { global $wgStylePath, $wgStyleVersion; if ( $this->stylename === null ) { - $class = get_class( $this ); + $class = static::class; throw new MWException( "$class::\$stylename must be set to use getSkinStylePath()" ); } diff --git a/includes/skins/SkinTemplate.php b/includes/skins/SkinTemplate.php index 780fac4964..61dbf2b1c1 100644 --- a/includes/skins/SkinTemplate.php +++ b/includes/skins/SkinTemplate.php @@ -344,7 +344,7 @@ class SkinTemplate extends Skin { $tpl->set( 'charset', 'UTF-8' ); $tpl->setRef( 'wgScript', $wgScript ); $tpl->setRef( 'skinname', $this->skinname ); - $tpl->set( 'skinclass', get_class( $this ) ); + $tpl->set( 'skinclass', static::class ); $tpl->setRef( 'skin', $this ); $tpl->setRef( 'stylename', $this->stylename ); $tpl->set( 'printable', $out->isPrintable() ); diff --git a/includes/specialpage/QueryPage.php b/includes/specialpage/QueryPage.php index 40f82f5588..65e82e86f7 100644 --- a/includes/specialpage/QueryPage.php +++ b/includes/specialpage/QueryPage.php @@ -304,7 +304,7 @@ abstract class QueryPage extends SpecialPage { return 0; } - $fname = get_class( $this ) . '::recache'; + $fname = static::class . '::recache'; $dbw = wfGetDB( DB_MASTER ); if ( !$dbw ) { return false; @@ -389,7 +389,7 @@ abstract class QueryPage extends SpecialPage { * @since 1.18 */ public function reallyDoQuery( $limit, $offset = false ) { - $fname = get_class( $this ) . "::reallyDoQuery"; + $fname = static::class . '::reallyDoQuery'; $dbr = $this->getRecacheDB(); $query = $this->getQueryInfo(); $order = $this->getOrderFields(); @@ -480,7 +480,7 @@ abstract class QueryPage extends SpecialPage { public function getCachedTimestamp() { if ( is_null( $this->cachedTimestamp ) ) { $dbr = wfGetDB( DB_REPLICA ); - $fname = get_class( $this ) . '::getCachedTimestamp'; + $fname = static::class . '::getCachedTimestamp'; $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp', [ 'qci_type' => $this->getName() ], $fname ); } diff --git a/includes/specialpage/RedirectSpecialPage.php b/includes/specialpage/RedirectSpecialPage.php index ea7d783148..b1ddacfb2f 100644 --- a/includes/specialpage/RedirectSpecialPage.php +++ b/includes/specialpage/RedirectSpecialPage.php @@ -52,7 +52,7 @@ abstract class RedirectSpecialPage extends UnlistedSpecialPage { return $redirect; } else { - $class = get_class( $this ); + $class = static::class; throw new MWException( "RedirectSpecialPage $class doesn't redirect!" ); } } diff --git a/includes/tidy/RemexCompatFormatter.php b/includes/tidy/RemexCompatFormatter.php new file mode 100644 index 0000000000..3dc727bc89 --- /dev/null +++ b/includes/tidy/RemexCompatFormatter.php @@ -0,0 +1,71 @@ + true, + 'p' => true, + 'tr' => true, + ]; + + public function __construct( $options = [] ) { + parent::__construct( $options ); + $this->attributeEscapes["\xc2\xa0"] = ' '; + unset( $this->attributeEscapes["&"] ); + $this->textEscapes["\xc2\xa0"] = ' '; + unset( $this->textEscapes["&"] ); + } + + public function startDocument( $fragmentNamespace, $fragmentName ) { + return ''; + } + + public function element( SerializerNode $parent, SerializerNode $node, $contents ) { + $data = $node->snData; + if ( $data && $data->isPWrapper ) { + if ( $data->nonblankNodeCount ) { + return "

$contents

"; + } else { + return $contents; + } + } + + $name = $node->name; + $attrs = $node->attrs; + if ( isset( self::$markedEmptyElements[$name] ) && $attrs->count() === 0 ) { + if ( strspn( $contents, "\t\n\f\r " ) === strlen( $contents ) ) { + return "<{$name} class=\"mw-empty-elt\">$contents"; + } + } + + $s = "<$name"; + foreach ( $attrs->getValues() as $attrName => $attrValue ) { + $encValue = strtr( $attrValue, $this->attributeEscapes ); + $s .= " $attrName=\"$encValue\""; + } + if ( $node->namespace === HTMLData::NS_HTML && isset( $this->voidElements[$name] ) ) { + $s .= ' />'; + return $s; + } + + $s .= '>'; + if ( $node->namespace === HTMLData::NS_HTML + && isset( $contents[0] ) && $contents[0] === "\n" + && isset( $this->prefixLfElements[$name] ) + ) { + $s .= "\n$contents"; + } else { + $s .= "$contents"; + } + return $s; + } +} diff --git a/includes/tidy/RemexCompatMunger.php b/includes/tidy/RemexCompatMunger.php new file mode 100644 index 0000000000..d5f5c281c2 --- /dev/null +++ b/includes/tidy/RemexCompatMunger.php @@ -0,0 +1,468 @@ + true, + "abbr" => true, + "acronym" => true, + "applet" => true, + "b" => true, + "basefont" => true, + "bdo" => true, + "big" => true, + "br" => true, + "button" => true, + "cite" => true, + "code" => true, + "dfn" => true, + "em" => true, + "font" => true, + "i" => true, + "iframe" => true, + "img" => true, + "input" => true, + "kbd" => true, + "label" => true, + "legend" => true, + "map" => true, + "object" => true, + "param" => true, + "q" => true, + "rb" => true, + "rbc" => true, + "rp" => true, + "rt" => true, + "rtc" => true, + "ruby" => true, + "s" => true, + "samp" => true, + "select" => true, + "small" => true, + "span" => true, + "strike" => true, + "strong" => true, + "sub" => true, + "sup" => true, + "textarea" => true, + "tt" => true, + "u" => true, + "var" => true, + ]; + + private static $formattingElements = [ + 'a' => true, + 'b' => true, + 'big' => true, + 'code' => true, + 'em' => true, + 'font' => true, + 'i' => true, + 'nobr' => true, + 's' => true, + 'small' => true, + 'strike' => true, + 'strong' => true, + 'tt' => true, + 'u' => true, + ]; + + /** + * Constructor + * + * @param Serializer $serializer + */ + public function __construct( Serializer $serializer ) { + $this->serializer = $serializer; + } + + public function startDocument( $fragmentNamespace, $fragmentName ) { + $this->serializer->startDocument( $fragmentNamespace, $fragmentName ); + $root = $this->serializer->getRootNode(); + $root->snData = new RemexMungerData; + $root->snData->needsPWrapping = true; + } + + public function endDocument( $pos ) { + $this->serializer->endDocument( $pos ); + } + + private function getParentForInsert( $preposition, $refElement ) { + if ( $preposition === TreeBuilder::ROOT ) { + return [ $this->serializer->getRootNode(), null ]; + } elseif ( $preposition === TreeBuilder::BEFORE ) { + $refNode = $refElement->userData; + return [ $this->serializer->getParentNode( $refNode ), $refNode ]; + } else { + $refNode = $refElement->userData; + $refData = $refNode->snData; + if ( $refData->currentCloneElement ) { + // Follow a chain of clone links if necessary + $origRefData = $refData; + while ( $refData->currentCloneElement ) { + $refElement = $refData->currentCloneElement; + $refNode = $refElement->userData; + $refData = $refNode->snData; + } + // Cache the end of the chain in the requested element + $origRefData->currentCloneElement = $refElement; + } elseif ( $refData->childPElement ) { + $refElement = $refData->childPElement; + $refNode = $refElement->userData; + } + return [ $refNode, $refNode ]; + } + } + + /** + * Insert a p-wrapper + * + * @param SerializerNode $parent + * @param integer $sourceStart + * @return SerializerNode + */ + private function insertPWrapper( SerializerNode $parent, $sourceStart ) { + $pWrap = new Element( HTMLData::NS_HTML, 'mw:p-wrap', new PlainAttributes ); + $this->serializer->insertElement( TreeBuilder::UNDER, $parent, $pWrap, false, + $sourceStart, 0 ); + $data = new RemexMungerData; + $data->isPWrapper = true; + $data->wrapBaseNode = $parent; + $pWrap->userData->snData = $data; + $parent->snData->childPElement = $pWrap; + return $pWrap->userData; + } + + public function characters( $preposition, $refElement, $text, $start, $length, + $sourceStart, $sourceLength + ) { + $isBlank = strspn( $text, "\t\n\f\r ", $start, $length ) === $length; + + list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement ); + $parentData = $parent->snData; + + if ( $preposition === TreeBuilder::UNDER ) { + if ( $parentData->needsPWrapping && !$isBlank ) { + // Add a p-wrapper for bare text under body/blockquote + $refNode = $this->insertPWrapper( $refNode, $sourceStart ); + $parent = $refNode; + $parentData = $parent->snData; + } elseif ( $parentData->isSplittable && !$parentData->ancestorPNode ) { + // The parent is splittable and in block mode, so split the tag stack + $refNode = $this->splitTagStack( $refNode, true, $sourceStart ); + $parent = $refNode; + $parentData = $parent->snData; + } + } + + if ( !$isBlank ) { + // Non-whitespace characters detected + $parentData->nonblankNodeCount++; + } + $this->serializer->characters( $preposition, $refNode, $text, $start, + $length, $sourceStart, $sourceLength ); + } + + /** + * Insert or reparent an element. Create p-wrappers or split the tag stack + * as necessary. + * + * Consider the following insertion locations. The parent may be: + * + * - A: A body or blockquote (!!needsPWrapping) + * - B: A p-wrapper (!!isPWrapper) + * - C: A descendant of a p-wrapper (!!ancestorPNode) + * - CS: With splittable formatting elements in the stack region up to + * the p-wrapper + * - CU: With one or more unsplittable elements in the stack region up + * to the p-wrapper + * - D: Not a descendant of a p-wrapper (!ancestorNode) + * - DS: With splittable formatting elements in the stack region up to + * the body or blockquote + * - DU: With one or more unsplittable elements in the stack region up + * to the body or blockquote + * + * And consider that we may insert two types of element: + * - b: block + * - i: inline + * + * We handle the insertion as follows: + * + * - A/i: Create a p-wrapper, insert under it + * - A/b: Insert as normal + * - B/i: Insert as normal + * - B/b: Close the p-wrapper, insert under the body/blockquote (wrap + * base) instead) + * - C/i: Insert as normal + * - CS/b: Split the tag stack, insert the block under cloned formatting + * elements which have the wrap base (the parent of the p-wrap) as + * their ultimate parent. + * - CU/b: Disable the p-wrap, by reparenting the currently open child + * of the p-wrap under the p-wrap's parent. Then insert the block as + * normal. + * - D/b: Insert as normal + * - DS/i: Split the tag stack, creating a new p-wrapper as the ultimate + * parent of the formatting elements thus cloned. The parent of the + * p-wrapper is the body or blockquote. + * - DU/i: Insert as normal + * + * FIXME: fostering ($preposition == BEFORE) is mostly done by inserting as + * normal, the full algorithm is not followed. + * + * @param integer $preposition + * @param Element|SerializerNode|null $refElement + * @param Element $element + * @param bool $void + * @param integer $sourceStart + * @param integer $sourceLength + */ + + public function insertElement( $preposition, $refElement, Element $element, $void, + $sourceStart, $sourceLength + ) { + list( $parent, $newRef ) = $this->getParentForInsert( $preposition, $refElement ); + $parentData = $parent->snData; + $parentNs = $parent->namespace; + $parentName = $parent->name; + $elementName = $element->htmlName; + + $inline = isset( self::$onlyInlineElements[$elementName] ); + $under = $preposition === TreeBuilder::UNDER; + + if ( $under && $parentData->isPWrapper && !$inline ) { + // [B/b] The element is non-inline and the parent is a p-wrapper, + // close the parent and insert into its parent instead + $newParent = $this->serializer->getParentNode( $parent ); + $parent = $newParent; + $parentData = $parent->snData; + $parentData->childPElement = null; + $newRef = $refElement->userData; + // FIXME cannot call endTag() since we don't have an Element + } elseif ( $under && $parentData->isSplittable + && (bool)$parentData->ancestorPNode !== $inline + ) { + // [CS/b, DS/i] The parent is splittable and the current element is + // inline in block context, or if the current element is a block + // under a p-wrapper, split the tag stack. + $newRef = $this->splitTagStack( $newRef, $inline, $sourceStart ); + $parent = $newRef; + $parentData = $parent->snData; + } elseif ( $under && $parentData->needsPWrapping && $inline ) { + // [A/i] If the element is inline and we are in body/blockquote, + // we need to create a p-wrapper + $newRef = $this->insertPWrapper( $newRef, $sourceStart ); + $parent = $newRef; + $parentData = $parent->snData; + } elseif ( $parentData->ancestorPNode && !$inline ) { + // [CU/b] If the element is non-inline and (despite attempting to + // split above) there is still an ancestor p-wrap, disable that + // p-wrap + $this->disablePWrapper( $parent, $sourceStart ); + } + // else [A/b, B/i, C/i, D/b, DU/i] insert as normal + + // An element with element children is a non-blank element + $parentData->nonblankNodeCount++; + + // Insert the element downstream and so initialise its userData + $this->serializer->insertElement( $preposition, $newRef, + $element, $void, $sourceStart, $sourceLength ); + + // Initialise snData + if ( !$element->userData->snData ) { + $elementData = $element->userData->snData = new RemexMungerData; + } else { + $elementData = $element->userData->snData; + } + if ( ( $parentData->isPWrapper || $parentData->isSplittable ) + && isset( self::$formattingElements[$elementName] ) + ) { + $elementData->isSplittable = true; + } + if ( $parentData->isPWrapper ) { + $elementData->ancestorPNode = $parent; + } elseif ( $parentData->ancestorPNode ) { + $elementData->ancestorPNode = $parentData->ancestorPNode; + } + if ( $parentData->wrapBaseNode ) { + $elementData->wrapBaseNode = $parentData->wrapBaseNode; + } elseif ( $parentData->needsPWrapping ) { + $elementData->wrapBaseNode = $parent; + } + if ( $elementName === 'body' + || $elementName === 'blockquote' + || $elementName === 'html' + ) { + $elementData->needsPWrapping = true; + } + } + + /** + * Clone nodes in a stack range and return the new parent + * + * @param SerializerNode $parentNode + * @param bool $inline + * @param integer $pos The source position + * @return SerializerNode + */ + private function splitTagStack( SerializerNode $parentNode, $inline, $pos ) { + $parentData = $parentNode->snData; + $wrapBase = $parentData->wrapBaseNode; + $pWrap = $parentData->ancestorPNode; + if ( !$pWrap ) { + $cloneEnd = $wrapBase; + } else { + $cloneEnd = $parentData->ancestorPNode; + } + + $serializer = $this->serializer; + $node = $parentNode; + $root = $serializer->getRootNode(); + $nodes = []; + $removableNodes = []; + $haveContent = false; + while ( $node !== $cloneEnd ) { + $nextParent = $serializer->getParentNode( $node ); + if ( $nextParent === $root ) { + throw new \Exception( 'Did not find end of clone range' ); + } + $nodes[] = $node; + if ( $node->snData->nonblankNodeCount === 0 ) { + $removableNodes[] = $node; + $nextParent->snData->nonblankNodeCount--; + } + $node = $nextParent; + } + + if ( $inline ) { + $pWrap = $this->insertPWrapper( $wrapBase, $pos ); + $node = $pWrap; + } else { + if ( $pWrap ) { + // End the p-wrap which was open, cancel the diversion + $wrapBase->snData->childPElement = null; + } + $pWrap = null; + $node = $wrapBase; + } + + for ( $i = count( $nodes ) - 1; $i >= 0; $i-- ) { + $oldNode = $nodes[$i]; + $oldData = $oldNode->snData; + $nodeParent = $node; + $element = new Element( $oldNode->namespace, $oldNode->name, $oldNode->attrs ); + $this->serializer->insertElement( TreeBuilder::UNDER, $nodeParent, + $element, false, $pos, 0 ); + $oldData->currentCloneElement = $element; + + $newNode = $element->userData; + $newData = $newNode->snData = new RemexMungerData; + if ( $pWrap ) { + $newData->ancestorPNode = $pWrap; + } + $newData->isSplittable = true; + $newData->wrapBaseNode = $wrapBase; + $newData->isPWrapper = $oldData->isPWrapper; + + $nodeParent->snData->nonblankNodeCount++; + + $node = $newNode; + } + foreach ( $removableNodes as $rNode ) { + $fakeElement = new Element( $rNode->namespace, $rNode->name, $rNode->attrs ); + $fakeElement->userData = $rNode; + $this->serializer->removeNode( $fakeElement, $pos ); + } + return $node; + } + + /** + * Find the ancestor of $node which is a child of a p-wrapper, and + * reparent that node so that it is placed after the end of the p-wrapper + */ + private function disablePWrapper( SerializerNode $node, $sourceStart ) { + $nodeData = $node->snData; + $pWrapNode = $nodeData->ancestorPNode; + $newParent = $this->serializer->getParentNode( $pWrapNode ); + if ( $pWrapNode !== $this->serializer->getLastChild( $newParent ) ) { + // Fostering or something? Abort! + return; + } + + $nextParent = $node; + do { + $victim = $nextParent; + $victim->snData->ancestorPNode = null; + $nextParent = $this->serializer->getParentNode( $victim ); + } while ( $nextParent !== $pWrapNode ); + + // Make a fake Element to use in a reparenting operation + $victimElement = new Element( $victim->namespace, $victim->name, $victim->attrs ); + $victimElement->userData = $victim; + + // Reparent + $this->serializer->insertElement( TreeBuilder::UNDER, $newParent, $victimElement, + false, $sourceStart, 0 ); + + // Decrement nonblank node count + $pWrapNode->snData->nonblankNodeCount--; + + // Cancel the diversion so that no more elements are inserted under this p-wrap + $newParent->snData->childPElement = null; + } + + public function endTag( Element $element, $sourceStart, $sourceLength ) { + $this->serializer->endTag( $element, $sourceStart, $sourceLength ); + } + + public function doctype( $name, $public, $system, $quirks, $sourceStart, $sourceLength ) { + $this->serializer->doctype( $name, $public, $system, $quirks, + $sourceStart, $sourceLength ); + } + + public function comment( $preposition, $refElement, $text, $sourceStart, $sourceLength ) { + list( $parent, $refNode ) = $this->getParentForInsert( $preposition, $refElement ); + $this->serializer->comment( $preposition, $refNode, $text, + $sourceStart, $sourceLength ); + } + + public function error( $text, $pos ) { + $this->serializer->error( $text, $pos ); + } + + public function mergeAttributes( Element $element, Attributes $attrs, $sourceStart ) { + $this->serializer->mergeAttributes( $element, $attrs, $sourceStart ); + } + + public function removeNode( Element $element, $sourceStart ) { + $this->serializer->removeNode( $element, $sourceStart ); + } + + public function reparentChildren( Element $element, Element $newParent, $sourceStart ) { + $self = $element->userData; + $children = $self->children; + $self->children = []; + $this->insertElement( TreeBuilder::UNDER, $element, $newParent, false, $sourceStart, 0 ); + $newParentNode = $newParent->userData; + $newParentId = $newParentNode->id; + foreach ( $children as $child ) { + if ( is_object( $child ) ) { + $child->parentId = $newParentId; + } + } + $newParentNode->children = $children; + } +} diff --git a/includes/tidy/RemexDriver.php b/includes/tidy/RemexDriver.php new file mode 100644 index 0000000000..e02af88fd9 --- /dev/null +++ b/includes/tidy/RemexDriver.php @@ -0,0 +1,57 @@ + false, + 'pwrap' => true + ]; + $this->trace = $config['treeMutationTrace']; + $this->pwrap = $config['pwrap']; + parent::__construct( $config ); + } + + public function tidy( $text ) { + $formatter = new RemexCompatFormatter; + $serializer = new Serializer( $formatter ); + if ( $this->pwrap ) { + $munger = new RemexCompatMunger( $serializer ); + } else { + $munger = $serializer; + } + if ( $this->trace ) { + $tracer = new TreeMutationTracer( $munger, function ( $msg ) { + wfDebug( "RemexHtml: $msg" ); + } ); + } else { + $tracer = $munger; + } + $treeBuilder = new TreeBuilder( $tracer, [ + 'ignoreErrors' => true, + 'ignoreNulls' => true, + ] ); + $dispatcher = new Dispatcher( $treeBuilder ); + $tokenizer = new Tokenizer( $dispatcher, $text, [ + 'ignoreErrors' => true, + 'ignoreCharRefs' => true, + 'ignoreNulls' => true, + 'skipPreprocess' => true, + ] ); + $tokenizer->execute( [ + 'fragmentNamespace' => \RemexHtml\HTMLData::NS_HTML, + 'fragmentName' => 'body' + ] ); + return $serializer->getResult(); + } +} diff --git a/includes/tidy/RemexMungerData.php b/includes/tidy/RemexMungerData.php new file mode 100644 index 0000000000..d614a38183 --- /dev/null +++ b/includes/tidy/RemexMungerData.php @@ -0,0 +1,78 @@ +foreignNamespaces[$ns] ) ) { $namespaceName = $this->foreignNamespaces[$ns]; } else { - $namespaceName = $ns == '0' ? '' : $pieces[0]; + // If the foreign wiki is misconfigured, XML dumps can contain a page with + // a non-zero namespace ID, but whose title doesn't contain a colon + // (T114115). In those cases, output a made-up namespace name to avoid + // collisions. The ImportTitleFactory might replace this with something + // more appropriate. + $namespaceName = $titleIncludesNamespace ? $pieces[0] : "Ns$ns"; } // We assume that the portion of the page title before the colon is the - // namespace name, except in the case of namespace 0 - if ( $ns != '0' ) { + // namespace name, except in the case of namespace 0. + if ( $titleIncludesNamespace ) { $pageName = $pieces[1]; } else { $pageName = $title; diff --git a/languages/Language.php b/languages/Language.php index 2ec2d54e73..067231557b 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -415,10 +415,10 @@ class Language { function __construct() { $this->mConverter = new FakeConverter( $this ); // Set the code to the name of the descendant - if ( get_class( $this ) == 'Language' ) { + if ( static::class === 'Language' ) { $this->mCode = 'en'; } else { - $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) ); + $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) ); } self::getLocalisationCache(); } diff --git a/languages/LanguageConverter.php b/languages/LanguageConverter.php index 6a426e5671..77210155fc 100644 --- a/languages/LanguageConverter.php +++ b/languages/LanguageConverter.php @@ -845,9 +845,8 @@ class LanguageConverter { * @throws MWException */ function loadDefaultTables() { - $name = get_class( $this ); - - throw new MWException( "Must implement loadDefaultTables() method in class $name" ); + $class = static::class; + throw new MWException( "Must implement loadDefaultTables() method in class $class" ); } /** diff --git a/resources/Resources.php b/resources/Resources.php index 5c6b26243f..0c3d27d59c 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1853,6 +1853,7 @@ return [ 'mediawiki.rcfilters.filters.dm', 'oojs-ui.styles.icons-moderation', 'oojs-ui.styles.icons-editing-core', + 'oojs-ui.styles.icons-editing-styling', 'oojs-ui.styles.icons-interactions', ], ], diff --git a/resources/lib/oojs-ui/oojs-ui-apex.js b/resources/lib/oojs-ui/oojs-ui-apex.js index 541462f0ba..53ce966f67 100644 --- a/resources/lib/oojs-ui/oojs-ui-apex.js +++ b/resources/lib/oojs-ui/oojs-ui-apex.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:40Z + * Date: 2017-03-07T22:57:01Z */ ( function ( OO ) { diff --git a/resources/lib/oojs-ui/oojs-ui-core-apex.css b/resources/lib/oojs-ui/oojs-ui-core-apex.css index 08187820e5..369cf096ba 100644 --- a/resources/lib/oojs-ui/oojs-ui-core-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-core-apex.css @@ -1,18 +1,15 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-element-hidden { display: none !important; - /* stylelint-disable-line declaration-no-important */ } .oo-ui-buttonElement { display: inline-block; @@ -667,9 +664,49 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { .oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor { display: block; position: absolute; - /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */ background-repeat: no-repeat; } +.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-style: solid; + border-color: transparent; +} +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor { + left: 0; + /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after { + border-top: 0; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { + left: 0; + /* `bottom` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after { + border-bottom: 0; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor { + top: 0; + /* `left` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after { + border-left: 0; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor { + top: 0; + /* `right` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after { + border-right: 0; +} .oo-ui-popupWidget-head { -webkit-touch-callout: none; -webkit-user-select: none; @@ -696,34 +733,78 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { border-radius: 0.25em; box-shadow: 0 0.15em 0.5em 0 rgba(0, 0, 0, 0.2); } -.oo-ui-popupWidget-anchored { +.oo-ui-popupWidget-anchored-top { margin-top: 6px; } -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor { +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor { top: -6px; } -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before, -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-color: transparent; - border-top: 0; -} -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before { +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before { bottom: -7px; left: -6px; border-bottom-color: #aaa; border-width: 7px; } -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after { +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after { bottom: -7px; left: -5px; border-bottom-color: #fff; border-width: 6px; } +.oo-ui-popupWidget-anchored-bottom { + margin-bottom: 6px; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { + bottom: -6px; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before { + top: -7px; + left: -6px; + border-top-color: #aaa; + border-width: 7px; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after { + top: -7px; + left: -5px; + border-top-color: #fff; + border-width: 6px; +} +.oo-ui-popupWidget-anchored-start { + margin-left: 6px; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor { + left: -6px; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before { + right: -7px; + top: -6px; + border-right-color: #aaa; + border-width: 7px; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after { + right: -7px; + top: -5px; + border-right-color: #fff; + border-width: 6px; +} +.oo-ui-popupWidget-anchored-end { + margin-right: 6px; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor { + right: -6px; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before { + left: -7px; + top: -6px; + border-left-color: #aaa; + border-width: 7px; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after { + left: -7px; + top: -5px; + border-left-color: #fff; + border-width: 6px; +} .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup { -webkit-transition: width 100ms ease, height 100ms ease, left 100ms ease; -moz-transition: width 100ms ease, height 100ms ease, left 100ms ease; @@ -750,10 +831,12 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { .oo-ui-popupButtonWidget .oo-ui-popupWidget { cursor: auto; } -.oo-ui-popupWidget.oo-ui-popupButtonWidget-frameless-popup { +.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor, +.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { margin-left: 0.9375em; } -.oo-ui-popupWidget.oo-ui-popupButtonWidget-framed-popup { +.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor, +.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { margin-left: 1.2375em; } .oo-ui-inputWidget { @@ -931,8 +1014,6 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { -webkit-transition: border-color 250ms ease, box-shadow 250ms ease; -moz-transition: border-color 250ms ease, box-shadow 250ms ease; transition: border-color 250ms ease, box-shadow 250ms ease; - /* stylelint-disable indentation */ - /* stylelint-enable indentation */ } .oo-ui-textInputWidget input.oo-ui-pendingElement-pending, .oo-ui-textInputWidget textarea.oo-ui-pendingElement-pending { diff --git a/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css index f468d17478..b041ef4ab4 100644 --- a/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-core-mediawiki.css @@ -1,18 +1,15 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-element-hidden { display: none !important; - /* stylelint-disable-line declaration-no-important */ } .oo-ui-buttonElement { display: inline-block; @@ -826,9 +823,49 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { .oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor { display: block; position: absolute; - /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */ background-repeat: no-repeat; } +.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after { + content: ''; + position: absolute; + width: 0; + height: 0; + border-style: solid; + border-color: transparent; +} +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor { + left: 0; + /* `top` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after { + border-top: 0; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { + left: 0; + /* `bottom` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after { + border-bottom: 0; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor { + top: 0; + /* `left` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after { + border-left: 0; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor { + top: 0; + /* `right` property is to be set in theme's selector due to specific `@size-anchor` values */ +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before, +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after { + border-right: 0; +} .oo-ui-popupWidget-head { -webkit-touch-callout: none; -webkit-user-select: none; @@ -855,34 +892,78 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { border-radius: 2px; box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.25); } -.oo-ui-popupWidget-anchored { +.oo-ui-popupWidget-anchored-top { margin-top: 9px; } -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor { +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor { top: -9px; } -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before, -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-style: solid; - border-color: transparent; - border-top: 0; -} -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:before { +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:before { bottom: -10px; left: -9px; border-bottom-color: #a2a9b1; border-width: 10px; } -.oo-ui-popupWidget-anchored .oo-ui-popupWidget-anchor:after { +.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor:after { bottom: -10px; left: -8px; border-bottom-color: #fff; border-width: 9px; } +.oo-ui-popupWidget-anchored-bottom { + margin-bottom: 9px; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { + bottom: -9px; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:before { + top: -10px; + left: -9px; + border-top-color: #a2a9b1; + border-width: 10px; +} +.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor:after { + top: -10px; + left: -8px; + border-top-color: #fff; + border-width: 9px; +} +.oo-ui-popupWidget-anchored-start { + margin-left: 9px; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor { + left: -9px; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:before { + right: -10px; + top: -9px; + border-right-color: #a2a9b1; + border-width: 10px; +} +.oo-ui-popupWidget-anchored-start .oo-ui-popupWidget-anchor:after { + right: -10px; + top: -8px; + border-right-color: #fff; + border-width: 9px; +} +.oo-ui-popupWidget-anchored-end { + margin-right: 9px; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor { + right: -9px; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:before { + left: -10px; + top: -9px; + border-left-color: #a2a9b1; + border-width: 10px; +} +.oo-ui-popupWidget-anchored-end .oo-ui-popupWidget-anchor:after { + left: -10px; + top: -8px; + border-left-color: #fff; + border-width: 9px; +} .oo-ui-popupWidget-transitioning .oo-ui-popupWidget-popup { -webkit-transition: width 100ms, height 100ms, left 100ms; -moz-transition: width 100ms, height 100ms, left 100ms; @@ -909,10 +990,12 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { .oo-ui-popupButtonWidget .oo-ui-popupWidget { cursor: auto; } -.oo-ui-popupWidget.oo-ui-popupButtonWidget-frameless-popup { +.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor, +.oo-ui-popupButtonWidget-frameless-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { margin-left: 0.9375em; } -.oo-ui-popupWidget.oo-ui-popupButtonWidget-framed-popup { +.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor, +.oo-ui-popupButtonWidget-framed-popup.oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { margin-left: 1.5em; } .oo-ui-inputWidget { @@ -1343,8 +1426,6 @@ body:not( :-moz-handler-blocked ) .oo-ui-fieldsetLayout { -webkit-transition: border-color 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1); -moz-transition: border-color 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1); transition: border-color 200ms cubic-bezier(0.39, 0.575, 0.565, 1), box-shadow 200ms cubic-bezier(0.39, 0.575, 0.565, 1); - /* stylelint-disable indentation */ - /* stylelint-enable indentation */ } .oo-ui-textInputWidget.oo-ui-widget-enabled input:hover, .oo-ui-textInputWidget.oo-ui-widget-enabled textarea:hover { diff --git a/resources/lib/oojs-ui/oojs-ui-core.js b/resources/lib/oojs-ui/oojs-ui-core.js index 9eb8716356..f10bdfa710 100644 --- a/resources/lib/oojs-ui/oojs-ui-core.js +++ b/resources/lib/oojs-ui/oojs-ui-core.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:40Z + * Date: 2017-03-07T22:57:01Z */ ( function ( OO ) { @@ -4079,6 +4079,8 @@ OO.ui.mixin.PendingElement.prototype.popPending = function () { * 'start': Align the start (left in LTR, right in RTL) edge with $floatableContainer's start edge * 'end': Align the end (right in LTR, left in RTL) edge with $floatableContainer's end edge * 'center': Horizontally align the center with $floatableContainer's center + * @cfg {boolean} [hideWhenOutOfView=true] Whether to hide the floatable element if the container + * is out of view */ OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) { // Configuration initialization @@ -4097,6 +4099,7 @@ OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) { this.setFloatableElement( config.$floatable || this.$element ); this.setVerticalPosition( config.verticalPosition || 'below' ); this.setHorizontalPosition( config.horizontalPosition || 'start' ); + this.hideWhenOutOfView = config.hideWhenOutOfView === undefined ? true : !!config.hideWhenOutOfView; }; /* Methods */ @@ -4141,9 +4144,11 @@ OO.ui.mixin.FloatableElement.prototype.setVerticalPosition = function ( position if ( [ 'below', 'above', 'top', 'bottom', 'center' ].indexOf( position ) === -1 ) { throw new Error( 'Invalid value for vertical position: ' + position ); } - this.verticalPosition = position; - if ( this.$floatable ) { - this.position(); + if ( this.verticalPosition !== position ) { + this.verticalPosition = position; + if ( this.$floatable ) { + this.position(); + } } }; @@ -4156,9 +4161,11 @@ OO.ui.mixin.FloatableElement.prototype.setHorizontalPosition = function ( positi if ( [ 'before', 'after', 'start', 'end', 'center' ].indexOf( position ) === -1 ) { throw new Error( 'Invalid value for horizontal position: ' + position ); } - this.horizontalPosition = position; - if ( this.$floatable ) { - this.position(); + if ( this.horizontalPosition !== position ) { + this.horizontalPosition = position; + if ( this.$floatable ) { + this.position(); + } } }; @@ -4290,27 +4297,49 @@ OO.ui.mixin.FloatableElement.prototype.isElementInViewport = function ( $element * @chainable */ OO.ui.mixin.FloatableElement.prototype.position = function () { - var containerPos, direction, $offsetParent, isBody, scrollableX, scrollableY, - horizScrollbarHeight, vertScrollbarWidth, scrollTop, scrollLeft, - newPos = { top: '', left: '', bottom: '', right: '' }; - if ( !this.positioning ) { return this; } - if ( !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) { + if ( this.hideWhenOutOfView && !this.isElementInViewport( this.$floatableContainer, this.$floatableClosestScrollable ) ) { this.$floatable.addClass( 'oo-ui-element-hidden' ); - return; + return this; } else { this.$floatable.removeClass( 'oo-ui-element-hidden' ); } if ( !this.needsCustomPosition ) { - return; + return this; } - direction = this.$floatableContainer.css( 'direction' ); - $offsetParent = this.$floatable.offsetParent(); + this.$floatable.css( this.computePosition() ); + + // We updated the position, so re-evaluate the clipping state. + // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so + // will not notice the need to update itself.) + // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does + // it not listen to the right events in the right places? + if ( this.clip ) { + this.clip(); + } + + return this; +}; + +/** + * Compute how #$floatable should be positioned based on the position of #$floatableContainer + * and the positioning settings. This is a helper for #position that shouldn't be called directly, + * but may be overridden by subclasses if they want to change or add to the positioning logic. + * + * @return {Object} New position to apply with .css(). Keys are 'top', 'left', 'bottom' and 'right'. + */ +OO.ui.mixin.FloatableElement.prototype.computePosition = function () { + var isBody, scrollableX, scrollableY, containerPos, + horizScrollbarHeight, vertScrollbarWidth, scrollTop, scrollLeft, + newPos = { top: '', left: '', bottom: '', right: '' }, + direction = this.$floatableContainer.css( 'direction' ), + $offsetParent = this.$floatable.offsetParent(); + if ( $offsetParent.is( 'html' ) ) { // The innerHeight/Width and clientHeight/Width calculations don't work well on the // element, but they do work on the @@ -4408,18 +4437,7 @@ OO.ui.mixin.FloatableElement.prototype.position = function () { } } - this.$floatable.css( newPos ); - - // We updated the position, so re-evaluate the clipping state. - // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so - // will not notice the need to update itself.) - // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does - // it not listen to the right events in the right places? - if ( this.clip ) { - this.clip(); - } - - return this; + return newPos; }; /** @@ -4745,13 +4763,24 @@ OO.ui.mixin.ClippableElement.prototype.clip = function () { * @cfg {number} [width=320] Width of popup in pixels * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height. * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup - * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`. - * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the - * popup is leaning towards the right of the screen. - * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence - * in the given language, which means it will flip to the correct positioning in right-to-left languages. - * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the - * sentence in the given language. + * @cfg {string} [position='below'] Where to position the popup relative to $floatableContainer + * 'above': Put popup above $floatableContainer; anchor points down to the start edge of $floatableContainer + * 'below': Put popup below $floatableContainer; anchor points up to the start edge of $floatableContainer + * 'before': Put popup to the left (LTR) / right (RTL) of $floatableContainer; anchor points + * endwards (right/left) to the vertical center of $floatableContainer + * 'after': Put popup to the right (LTR) / left (RTL) of $floatableContainer; anchor points + * startwards (left/right) to the vertical center of $floatableContainer + * @cfg {string} [align='center'] How to align the popup to $floatableContainer + * 'forwards': If position is above/below, move the popup as far endwards (right in LTR, left in RTL) + * as possible while still keeping the anchor within the popup; + * if position is before/after, move the popup as far downwards as possible. + * 'backwards': If position is above/below, move the popup as far startwards (left in LTR, right in RTL) + * as possible while still keeping the anchor within the popup; + * if position in before/after, move the popup as far upwards as possible. + * 'center': Horizontally (if position is above/below) or vertically (before/after) align the center + * of the popup with the center of $floatableContainer. + * 'force-left': Alias for 'forwards' in LTR and 'backwards' in RTL + * 'force-right': Alias for 'backwards' in RTL and 'forwards' in LTR * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container. * See the [OOjs UI docs on MediaWiki][3] for an example. * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample @@ -4794,15 +4823,16 @@ OO.ui.PopupWidget = function OoUiPopupWidget( config ) { this.autoClose = !!config.autoClose; this.$autoCloseIgnore = config.$autoCloseIgnore; this.transitionTimeout = null; - this.anchor = null; + this.anchored = false; this.width = config.width !== undefined ? config.width : 320; this.height = config.height !== undefined ? config.height : null; - this.setAlignment( config.align ); this.onMouseDownHandler = this.onMouseDown.bind( this ); this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this ); // Initialization this.toggleAnchor( config.anchor === undefined || config.anchor ); + this.setAlignment( config.align || 'center' ); + this.setPosition( config.position || 'below' ); this.$body.addClass( 'oo-ui-popupWidget-body' ); this.$anchor.addClass( 'oo-ui-popupWidget-anchor' ); this.$popup @@ -4950,6 +4980,21 @@ OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) { this.anchored = show; } }; +/** + * Change which edge the anchor appears on. + * + * @param {string} edge 'top', 'bottom', 'start' or 'end' + */ +OO.ui.PopupWidget.prototype.setAnchorEdge = function ( edge ) { + if ( [ 'top', 'bottom', 'start', 'end' ].indexOf( edge ) === -1 ) { + throw new Error( 'Invalid value for edge: ' + edge ); + } + if ( this.anchorEdge !== null ) { + this.$element.removeClass( 'oo-ui-popupWidget-anchored-' + this.anchorEdge ); + } + this.anchorEdge = edge; + this.$element.addClass( 'oo-ui-popupWidget-anchored-' + edge ); +}; /** * Check if the anchor is visible. @@ -4957,7 +5002,7 @@ OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) { * @return {boolean} Anchor is visible */ OO.ui.PopupWidget.prototype.hasAnchor = function () { - return this.anchor; + return this.anchored; }; /** @@ -4981,6 +5026,10 @@ OO.ui.PopupWidget.prototype.toggle = function ( show ) { OO.ui.warnDeprecation( 'PopupWidget#toggle: Before calling this method, the popup must be attached to the DOM.' ); this.warnedUnattached = true; } + if ( show && !this.$floatableContainer && this.isElementAttached() ) { + // Fall back to the parent node if the floatableContainer is not set + this.setFloatableContainer( this.$element.parent() ); + } // Parent method OO.ui.PopupWidget.parent.prototype.toggle.call( this, show ); @@ -5035,9 +5084,37 @@ OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) { * @chainable */ OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) { - var popupOffset, originOffset, containerLeft, containerWidth, containerRight, - popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth, direction, - dirFactor, align, + var widget = this; + + // Prevent transition from being interrupted + clearTimeout( this.transitionTimeout ); + if ( transition ) { + // Enable transition + this.$element.addClass( 'oo-ui-popupWidget-transitioning' ); + } + + this.position(); + + if ( transition ) { + // Prevent transitioning after transition is complete + this.transitionTimeout = setTimeout( function () { + widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); + }, 200 ); + } else { + // Prevent transitioning immediately + this.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); + } +}; + +/** + * @inheritdoc + */ +OO.ui.PopupWidget.prototype.computePosition = function () { + var direction, align, vertical, start, end, near, far, sizeProp, popupSize, anchorSize, anchorPos, + anchorOffset, anchorMargin, parentPosition, positionProp, positionAdjustment, floatablePos, + offsetParentPos, containerPos, + popupPos = {}, + anchorCss = { left: '', right: '', top: '', bottom: '' }, alignMap = { ltr: { 'force-left': 'backwards', @@ -5048,77 +5125,119 @@ OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) { 'force-right': 'backwards' } }, - widget = this; + anchorEdgeMap = { + above: 'bottom', + below: 'top', + before: 'end', + after: 'start' + }, + hPosMap = { + forwards: 'start', + center: 'center', + backwards: 'before' + }, + vPosMap = { + forwards: 'top', + center: 'center', + backwards: 'bottom' + }; if ( !this.$container ) { // Lazy-initialize $container if not specified in constructor this.$container = $( this.getClosestScrollableElementContainer() ); } direction = this.$container.css( 'direction' ); - dirFactor = direction === 'rtl' ? -1 : 1; - align = alignMap[ direction ][ this.align ] || this.align; - // Set height and width before measuring things, since it might cause our measurements - // to change (e.g. due to scrollbars appearing or disappearing) + // Set height and width before we do anything else, since it might cause our measurements + // to change (e.g. due to scrollbars appearing or disappearing), and it also affects centering this.$popup.css( { width: this.width, height: this.height !== null ? this.height : 'auto' } ); - // Compute initial popupOffset based on alignment - popupOffset = this.width * ( { backwards: -1, center: -0.5, forwards: 0 } )[ align ]; - - // Figure out if this will cause the popup to go beyond the edge of the container - originOffset = this.$element.offset().left; - containerLeft = this.$container.offset().left; - containerWidth = this.$container.innerWidth(); - containerRight = containerLeft + containerWidth; - popupLeft = dirFactor * popupOffset - this.containerPadding; - popupRight = dirFactor * popupOffset + this.containerPadding + this.width + this.containerPadding; - overlapLeft = ( originOffset + popupLeft ) - containerLeft; - overlapRight = containerRight - ( originOffset + popupRight ); - - // Adjust offset to make the popup not go beyond the edge, if needed - if ( overlapRight < 0 ) { - popupOffset += dirFactor * overlapRight; - } else if ( overlapLeft < 0 ) { - popupOffset -= dirFactor * overlapLeft; - } + align = alignMap[ direction ][ this.align ] || this.align; + // If the popup is positioned before or after, then the anchor positioning is vertical, otherwise horizontal + vertical = this.popupPosition === 'before' || this.popupPosition === 'after'; + start = vertical ? 'top' : ( direction === 'rtl' ? 'right' : 'left' ); + end = vertical ? 'bottom' : ( direction === 'rtl' ? 'left' : 'right' ); + near = vertical ? 'top' : 'left'; + far = vertical ? 'bottom' : 'right'; + sizeProp = vertical ? 'Height' : 'Width'; + popupSize = vertical ? ( this.height || this.$popup.height() ) : this.width; + + this.setAnchorEdge( anchorEdgeMap[ this.popupPosition ] ); + this.horizontalPosition = vertical ? this.popupPosition : hPosMap[ align ]; + this.verticalPosition = vertical ? vPosMap[ align ] : this.popupPosition; - // Adjust offset to avoid anchor being rendered too close to the edge - // $anchor.width() doesn't work with the pure CSS anchor (returns 0) - // TODO: Find a measurement that works for CSS anchors and image anchors - anchorWidth = this.$anchor[ 0 ].scrollWidth * 2; - if ( popupOffset + this.width < anchorWidth ) { - popupOffset = anchorWidth - this.width; - } else if ( -popupOffset < anchorWidth ) { - popupOffset = -anchorWidth; + // Parent method + parentPosition = OO.ui.mixin.FloatableElement.prototype.computePosition.call( this ); + // Find out which property FloatableElement used for positioning, and adjust that value + positionProp = vertical ? + ( parentPosition.top !== '' ? 'top' : 'bottom' ) : + ( parentPosition.left !== '' ? 'left' : 'right' ); + + // Figure out where the near and far edges of the popup and $floatableContainer are + floatablePos = this.$floatableContainer.offset(); + floatablePos[ far ] = floatablePos[ near ] + this.$floatableContainer[ 'outer' + sizeProp ](); + // Measure where the offsetParent is and compute our position based on that and parentPosition + offsetParentPos = this.$element.offsetParent().offset(); + + if ( positionProp === near ) { + popupPos[ near ] = offsetParentPos[ near ] + parentPosition[ near ]; + popupPos[ far ] = popupPos[ near ] + popupSize; + } else { + popupPos[ far ] = offsetParentPos[ near ] + + this.$element.offsetParent()[ 'inner' + sizeProp ]() - parentPosition[ far ]; + popupPos[ near ] = popupPos[ far ] - popupSize; + } + + // Position the anchor (which is positioned relative to the popup) to point to $floatableContainer + // For popups above/below, we point to the start edge; for popups before/after, we point to the center + anchorPos = vertical ? ( floatablePos[ start ] + floatablePos[ end ] ) / 2 : floatablePos[ start ]; + anchorOffset = ( start === far ? -1 : 1 ) * ( anchorPos - popupPos[ start ] ); + + // If the anchor is less than 2*anchorSize from either edge, move the popup to make more space + // this.$anchor.width()/height() returns 0 because of the CSS trickery we use, so use scrollWidth/Height + anchorSize = this.$anchor[ 0 ][ 'scroll' + sizeProp ]; + anchorMargin = parseFloat( this.$anchor.css( 'margin-' + start ) ); + if ( anchorOffset + anchorMargin < 2 * anchorSize ) { + // Not enough space for the anchor on the start side; pull the popup startwards + positionAdjustment = ( positionProp === start ? -1 : 1 ) * + ( 2 * anchorSize - ( anchorOffset + anchorMargin ) ); + } else if ( anchorOffset + anchorMargin > popupSize - 2 * anchorSize ) { + // Not enough space for the anchor on the end side; pull the popup endwards + positionAdjustment = ( positionProp === end ? -1 : 1 ) * + ( anchorOffset + anchorMargin - ( popupSize - 2 * anchorSize ) ); + } else { + positionAdjustment = 0; } - // Prevent transition from being interrupted - clearTimeout( this.transitionTimeout ); - if ( transition ) { - // Enable transition - this.$element.addClass( 'oo-ui-popupWidget-transitioning' ); + // Check if the popup will go beyond the edge of this.$container + containerPos = this.$container.offset(); + containerPos[ far ] = containerPos[ near ] + this.$container[ 'inner' + sizeProp ](); + // Take into account how much the popup will move because of the adjustments we're going to make + popupPos[ near ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment; + popupPos[ far ] += ( positionProp === near ? 1 : -1 ) * positionAdjustment; + if ( containerPos[ near ] + this.containerPadding > popupPos[ near ] ) { + // Popup goes beyond the near (left/top) edge, move it to the right/bottom + positionAdjustment += ( positionProp === near ? 1 : -1 ) * + ( containerPos[ near ] + this.containerPadding - popupPos[ near ] ); + } else if ( containerPos[ far ] - this.containerPadding < popupPos[ far ] ) { + // Popup goes beyond the far (right/bottom) edge, move it to the left/top + positionAdjustment += ( positionProp === far ? 1 : -1 ) * + ( popupPos[ far ] - ( containerPos[ far ] - this.containerPadding ) ); } - // Position body relative to anchor - this.$popup.css( direction === 'rtl' ? 'margin-right' : 'margin-left', popupOffset ); - - if ( transition ) { - // Prevent transitioning after transition is complete - this.transitionTimeout = setTimeout( function () { - widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); - }, 200 ); - } else { - // Prevent transitioning immediately - this.$element.removeClass( 'oo-ui-popupWidget-transitioning' ); - } + // Adjust anchorOffset for positionAdjustment + anchorOffset += ( positionProp === start ? -1 : 1 ) * positionAdjustment; - // Reevaluate clipping state since we've relocated and resized the popup - this.clip(); + // Position the anchor + anchorCss[ start ] = anchorOffset; + this.$anchor.css( anchorCss ); + // Move the popup if needed + parentPosition[ positionProp ] += positionAdjustment; - return this; + return parentPosition; }; /** @@ -5140,18 +5259,41 @@ OO.ui.PopupWidget.prototype.setAlignment = function ( align ) { } else { this.align = 'center'; } + this.position(); }; /** * Get popup alignment * - * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`, + * @return {string} Alignment of the popup, `center`, `force-left`, `force-right`, * `backwards` or `forwards`. */ OO.ui.PopupWidget.prototype.getAlignment = function () { return this.align; }; +/** + * Change the positioning of the popup. + * + * @param {string} position 'above', 'below', 'before' or 'after' + */ +OO.ui.PopupWidget.prototype.setPosition = function ( position ) { + if ( [ 'above', 'below', 'before', 'after' ].indexOf( position ) === -1 ) { + position = 'below'; + } + this.popupPosition = position; + this.position(); +}; + +/** + * Get popup positioning. + * + * @return {string} 'above', 'below', 'before' or 'after' + */ +OO.ui.PopupWidget.prototype.getPosition = function () { + return this.popupPosition; +}; + /** * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}. * A popup is a container for content. It is overlaid and positioned absolutely. By default, each @@ -5172,9 +5314,14 @@ OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) { // Properties this.popup = new OO.ui.PopupWidget( $.extend( - { autoClose: true }, + { + autoClose: true, + $floatableContainer: this.$element + }, config.popup, - { $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) } + { + $autoCloseIgnore: this.$element.add( config.popup && config.popup.$autoCloseIgnore ) + } ) ); }; @@ -5222,11 +5369,7 @@ OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) { OO.ui.PopupButtonWidget.parent.call( this, config ); // Mixin constructors - OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, { - popup: { - $floatableContainer: this.$element - } - } ) ); + OO.ui.mixin.PopupElement.call( this, config ); // Properties this.$overlay = config.$overlay || this.$element; @@ -6736,20 +6879,34 @@ OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) { * @protected */ OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () { - var i, item, visible, + var i, item, visible, section, sectionEmpty, anyVisible = false, len = this.items.length, showAll = !this.isVisible(), filter = showAll ? null : this.getItemMatcher( this.$input.val() ); + // Hide non-matching options, and also hide section headers if all options + // in their section are hidden. for ( i = 0; i < len; i++ ) { item = this.items[ i ]; - if ( item instanceof OO.ui.OptionWidget ) { + if ( item instanceof OO.ui.MenuSectionOptionWidget ) { + if ( section ) { + // If the previous section was empty, hide its header + section.toggle( showAll || !sectionEmpty ); + } + section = item; + sectionEmpty = true; + } else if ( item instanceof OO.ui.OptionWidget ) { visible = showAll || filter( item ); anyVisible = anyVisible || visible; + sectionEmpty = sectionEmpty && !visible; item.toggle( visible ); } } + // Process the final section + if ( section ) { + section.toggle( showAll || !sectionEmpty ); + } this.$element.toggleClass( 'oo-ui-menuSelectWidget-invisible', !anyVisible ); @@ -9125,7 +9282,7 @@ OO.ui.CheckboxMultiselectInputWidget.prototype.setOptions = function ( options ) * specifies minimum number of rows to display. * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content. * Use the #maxRows config to specify a maximum number of displayed rows. - * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true. + * @cfg {number} [maxRows] Maximum number of rows to display when #autosize is set to true. * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided. * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of * the value or placeholder text: `'before'` or `'after'` @@ -10081,6 +10238,10 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) { * - by choosing a value from the menu. The value of the chosen option will then appear in the text * input field. * + * After the user chooses an option, its `data` will be used as a new value for the widget. + * A `label` also can be specified for each option: if given, it will be shown instead of the + * `data` in the dropdown menu. + * * This widget can be used inside an HTML form, such as a OO.ui.FormLayout. * * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1]. @@ -10088,32 +10249,33 @@ OO.ui.SearchInputWidget.prototype.setReadOnly = function ( state ) { * @example * // Example: A ComboBoxInputWidget. * var comboBox = new OO.ui.ComboBoxInputWidget( { - * label: 'ComboBoxInputWidget', * value: 'Option 1', - * menu: { - * items: [ - * new OO.ui.MenuOptionWidget( { - * data: 'Option 1', - * label: 'Option One' - * } ), - * new OO.ui.MenuOptionWidget( { - * data: 'Option 2', - * label: 'Option Two' - * } ), - * new OO.ui.MenuOptionWidget( { - * data: 'Option 3', - * label: 'Option Three' - * } ), - * new OO.ui.MenuOptionWidget( { - * data: 'Option 4', - * label: 'Option Four' - * } ), - * new OO.ui.MenuOptionWidget( { - * data: 'Option 5', - * label: 'Option Five' - * } ) - * ] - * } + * options: [ + * { data: 'Option 1' }, + * { data: 'Option 2' }, + * { data: 'Option 3' } + * ] + * } ); + * $( 'body' ).append( comboBox.$element ); + * + * @example + * // Example: A ComboBoxInputWidget with additional option labels. + * var comboBox = new OO.ui.ComboBoxInputWidget( { + * value: 'Option 1', + * options: [ + * { + * data: 'Option 1', + * label: 'Option One' + * }, + * { + * data: 'Option 2', + * label: 'Option Two' + * }, + * { + * data: 'Option 3', + * label: 'Option Three' + * } + * ] * } ); * $( 'body' ).append( comboBox.$element ); * diff --git a/resources/lib/oojs-ui/oojs-ui-mediawiki.js b/resources/lib/oojs-ui/oojs-ui-mediawiki.js index b39010c45b..7b1c099925 100644 --- a/resources/lib/oojs-ui/oojs-ui-mediawiki.js +++ b/resources/lib/oojs-ui/oojs-ui-mediawiki.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:40Z + * Date: 2017-03-07T22:57:01Z */ ( function ( OO ) { diff --git a/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css b/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css index 4d7f9d7546..760458952a 100644 --- a/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-toolbars-apex.css @@ -1,21 +1,19 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-popupTool .oo-ui-popupWidget-popup, .oo-ui-popupTool .oo-ui-popupWidget-anchor { z-index: 4; } -.oo-ui-popupTool .oo-ui-popupWidget { - /* @noflip */ +.oo-ui-popupTool .oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor, +.oo-ui-popupTool .oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { margin-left: 1.25em; } .oo-ui-toolGroupTool > .oo-ui-popupToolGroup { diff --git a/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css index 59de29baa2..ad1746d9dd 100644 --- a/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-toolbars-mediawiki.css @@ -1,15 +1,13 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-tool.oo-ui-widget-enabled { -webkit-transition: background-color 100ms; -moz-transition: background-color 100ms; @@ -24,8 +22,8 @@ .oo-ui-popupTool .oo-ui-popupWidget-anchor { z-index: 4; } -.oo-ui-popupTool .oo-ui-popupWidget { - /* @noflip */ +.oo-ui-popupTool .oo-ui-popupWidget-anchored-top .oo-ui-popupWidget-anchor, +.oo-ui-popupTool .oo-ui-popupWidget-anchored-bottom .oo-ui-popupWidget-anchor { margin-left: 1.25em; } .oo-ui-toolGroupTool > .oo-ui-toolGroup { diff --git a/resources/lib/oojs-ui/oojs-ui-toolbars.js b/resources/lib/oojs-ui/oojs-ui-toolbars.js index 777debfb6f..1574f6c452 100644 --- a/resources/lib/oojs-ui/oojs-ui-toolbars.js +++ b/resources/lib/oojs-ui/oojs-ui-toolbars.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:40Z + * Date: 2017-03-07T22:57:01Z */ ( function ( OO ) { @@ -1469,6 +1469,7 @@ OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) { OO.ui.mixin.PopupElement.call( this, config ); // Initialization + this.popup.setPosition( toolGroup.getToolbar().position === 'bottom' ? 'above' : 'below' ); this.$element .addClass( 'oo-ui-popupTool' ) .append( this.popup.$element ); @@ -2194,6 +2195,9 @@ OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () { for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) { this.collapsibleTools[ i ].toggle( this.expanded ); } + + // Re-evaluate clipping, because our height has changed + this.clip(); }; /** diff --git a/resources/lib/oojs-ui/oojs-ui-widgets-apex.css b/resources/lib/oojs-ui/oojs-ui-widgets-apex.css index a1064bb855..4ec3d1c189 100644 --- a/resources/lib/oojs-ui/oojs-ui-widgets-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-widgets-apex.css @@ -1,15 +1,13 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-draggableElement-handle, .oo-ui-draggableElement-handle.oo-ui-widget { cursor: move; @@ -81,8 +79,6 @@ left: 0; right: 0; bottom: 0; - /* stylelint-disable declaration-no-important */ - /* stylelint-enable declaration-no-important */ } .oo-ui-menuLayout-menu, .oo-ui-menuLayout-content { @@ -593,23 +589,26 @@ .oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator { opacity: 0.5; } -.oo-ui-outlineOptionWidget-level-0 { - padding-left: 3.5em; -} -.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon { - left: 1em; +.oo-ui-outlineOptionWidget-level-0.oo-ui-iconElement { + padding-left: 2.5em; } .oo-ui-outlineOptionWidget-level-1 { - padding-left: 5em; + padding-left: 2.5em; +} +.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement { + padding-left: 4.5em; } -.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon { +.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement .oo-ui-iconElement-icon { left: 2.5em; } .oo-ui-outlineOptionWidget-level-2 { - padding-left: 6.5em; + padding-left: 5em; +} +.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement { + padding-left: 7em; } -.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon { - left: 4em; +.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement .oo-ui-iconElement-icon { + left: 5em; } .oo-ui-selectWidget-depressed .oo-ui-outlineOptionWidget.oo-ui-optionWidget-selected { background-color: #a7dcff; @@ -754,8 +753,6 @@ background-color: transparent; color: #000; vertical-align: middle; - /* stylelint-disable indentation */ - /* stylelint-enable indentation */ } .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content > input::-webkit-input-placeholder { color: #72777d; diff --git a/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css index 495cbfe5a7..f64a619f2f 100644 --- a/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-widgets-mediawiki.css @@ -1,15 +1,13 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-draggableElement-handle, .oo-ui-draggableElement-handle.oo-ui-widget { cursor: move; @@ -92,8 +90,6 @@ left: 0; right: 0; bottom: 0; - /* stylelint-disable declaration-no-important */ - /* stylelint-enable declaration-no-important */ } .oo-ui-menuLayout-menu, .oo-ui-menuLayout-content { @@ -656,6 +652,12 @@ .oo-ui-widget-disabled .oo-ui-selectFileWidget-dropLabel { display: none; } +.oo-ui-outlineSelectWidget { + height: 100%; +} +.oo-ui-outlineSelectWidget:focus { + box-shadow: inset 0 0 0 2px #36c; +} .oo-ui-outlineOptionWidget { -webkit-touch-callout: none; -webkit-user-select: none; @@ -689,23 +691,26 @@ .oo-ui-outlineOptionWidget.oo-ui-indicatorElement .oo-ui-indicatorElement-indicator { opacity: 0.5; } -.oo-ui-outlineOptionWidget-level-0 { - padding-left: 3.5em; -} -.oo-ui-outlineOptionWidget-level-0 .oo-ui-iconElement-icon { - left: 1em; +.oo-ui-outlineOptionWidget-level-0.oo-ui-iconElement { + padding-left: 2.571em; } .oo-ui-outlineOptionWidget-level-1 { - padding-left: 5em; + padding-left: 2.571em; +} +.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement { + padding-left: 4.429em; } -.oo-ui-outlineOptionWidget-level-1 .oo-ui-iconElement-icon { - left: 2.5em; +.oo-ui-outlineOptionWidget-level-1.oo-ui-iconElement .oo-ui-iconElement-icon { + left: 2.571em; } .oo-ui-outlineOptionWidget-level-2 { - padding-left: 6.5em; + padding-left: 5.142em; +} +.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement { + padding-left: 6.857em; } -.oo-ui-outlineOptionWidget-level-2 .oo-ui-iconElement-icon { - left: 4em; +.oo-ui-outlineOptionWidget-level-2.oo-ui-iconElement .oo-ui-iconElement-icon { + left: 4.429em; } .oo-ui-outlineControlsWidget { height: 3em; @@ -842,8 +847,6 @@ background-color: transparent; color: #000; vertical-align: middle; - /* stylelint-disable indentation */ - /* stylelint-enable indentation */ } .oo-ui-capsuleMultiselectWidget-handle > .oo-ui-capsuleMultiselectWidget-content > input::-webkit-input-placeholder { color: #72777d; diff --git a/resources/lib/oojs-ui/oojs-ui-widgets.js b/resources/lib/oojs-ui/oojs-ui-widgets.js index b89262dc83..d1fbe0df48 100644 --- a/resources/lib/oojs-ui/oojs-ui-widgets.js +++ b/resources/lib/oojs-ui/oojs-ui-widgets.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:40Z + * Date: 2017-03-07T22:57:01Z */ ( function ( OO ) { @@ -3643,11 +3643,7 @@ OO.ui.CapsuleMultiselectWidget = function OoUiCapsuleMultiselectWidget( config ) align: 'forwards', anchor: false } ); - OO.ui.mixin.PopupElement.call( this, $.extend( true, {}, config, { - popup: { - $floatableContainer: this.$element - } - } ) ); + OO.ui.mixin.PopupElement.call( this, config ); $tabFocus = $( '' ); OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) ); } else { @@ -4273,6 +4269,9 @@ OO.ui.CapsuleMultiselectWidget.prototype.updateIfHeightChanged = function () { if ( height !== this.height ) { this.height = height; this.menu.position(); + if ( this.popup ) { + this.popup.updateDimensions(); + } this.emit( 'resize' ); } }; diff --git a/resources/lib/oojs-ui/oojs-ui-windows-apex.css b/resources/lib/oojs-ui/oojs-ui-windows-apex.css index b546dd114b..4353b43362 100644 --- a/resources/lib/oojs-ui/oojs-ui-windows-apex.css +++ b/resources/lib/oojs-ui/oojs-ui-windows-apex.css @@ -1,15 +1,13 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-actionWidget.oo-ui-pendingElement-pending { background-image: /* @embed */ url(themes/apex/images/textures/pending.gif); } diff --git a/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css b/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css index 701c058240..823b1a1db0 100644 --- a/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css +++ b/resources/lib/oojs-ui/oojs-ui-windows-mediawiki.css @@ -1,15 +1,13 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:44Z + * Date: 2017-03-07T22:57:06Z */ -/* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ -/* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ .oo-ui-window { background: transparent; } diff --git a/resources/lib/oojs-ui/oojs-ui-windows.js b/resources/lib/oojs-ui/oojs-ui-windows.js index ad3c226c9e..586efb0d72 100644 --- a/resources/lib/oojs-ui/oojs-ui-windows.js +++ b/resources/lib/oojs-ui/oojs-ui-windows.js @@ -1,12 +1,12 @@ /*! - * OOjs UI v0.19.4 + * OOjs UI v0.19.5 * https://www.mediawiki.org/wiki/OOjs_UI * * Copyright 2011–2017 OOjs UI Team and other contributors. * Released under the MIT license * http://oojs.mit-license.org * - * Date: 2017-02-28T23:19:40Z + * Date: 2017-03-07T22:57:01Z */ ( function ( OO ) { diff --git a/resources/lib/oojs-ui/themes/apex/icons-content.json b/resources/lib/oojs-ui/themes/apex/icons-content.json index 394ec85ea7..0cd901f62b 100644 --- a/resources/lib/oojs-ui/themes/apex/icons-content.json +++ b/resources/lib/oojs-ui/themes/apex/icons-content.json @@ -10,6 +10,10 @@ "ltr": "images/icons/articleRedirect-ltr.svg", "rtl": "images/icons/articleRedirect-rtl.svg" } }, + "journal": { "file": { + "ltr": "images/icons/journal-ltr.svg", + "rtl": "images/icons/journal-rtl.svg" + } }, "upload": { "file": { "ltr": "images/icons/upload-ltr.svg", "rtl": "images/icons/upload-rtl.svg" diff --git a/resources/lib/oojs-ui/themes/apex/icons-editing-styling.json b/resources/lib/oojs-ui/themes/apex/icons-editing-styling.json index 5b35e74a60..718f9ffa68 100644 --- a/resources/lib/oojs-ui/themes/apex/icons-editing-styling.json +++ b/resources/lib/oojs-ui/themes/apex/icons-editing-styling.json @@ -36,6 +36,10 @@ "os": "images/icons/bold-cyrl-be.svg" } } }, + "highlight": { "file": { + "ltr": "images/icons/highlight-ltr.svg", + "rtl": "images/icons/highlight-rtl.svg" + } }, "italic": { "file": { "default": "images/icons/italic-a.svg", "lang": { diff --git a/resources/lib/oojs-ui/themes/apex/icons-interactions.json b/resources/lib/oojs-ui/themes/apex/icons-interactions.json index d68e70c426..ec18f29ddb 100644 --- a/resources/lib/oojs-ui/themes/apex/icons-interactions.json +++ b/resources/lib/oojs-ui/themes/apex/icons-interactions.json @@ -8,8 +8,9 @@ } }, "images": { - "beta": { "file": "images/icons/beta.svg" }, - "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg" }, + "add": { "file": "images/icons/add.svg" }, + "beta": { "file": "images/icons/beta.svg", "deprecated": "Deprecated since v0.18.3, don't use." }, + "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg", "deprecated": "Moved since v0.18.3, use 'logoWikimediaDiscovery' from the 'Wikimedia' pack instead." }, "bookmark": { "file": { "ltr": "images/icons/bookmark-ltr.svg", "rtl": "images/icons/bookmark-rtl.svg" @@ -49,7 +50,8 @@ "ltr": "images/icons/printer-ltr.svg", "rtl": "images/icons/printer-rtl.svg" } }, - "ribbonPrize": { "file": "images/icons/ribbonPrize.svg" }, + "ribbonPrize": { "file": "images/icons/ribbonPrize.svg", "deprecated": "Deprecated since v0.18.3, don't use." }, + "subtract": { "file": "images/icons/subtract.svg" }, "sun": { "file": { "ltr": "images/icons/sun-ltr.svg", "rtl": "images/icons/sun-rtl.svg" diff --git a/resources/lib/oojs-ui/themes/apex/icons-moderation.json b/resources/lib/oojs-ui/themes/apex/icons-moderation.json index 15c0251bf4..c8385e4dc2 100644 --- a/resources/lib/oojs-ui/themes/apex/icons-moderation.json +++ b/resources/lib/oojs-ui/themes/apex/icons-moderation.json @@ -6,7 +6,7 @@ "blockUndo": { "file": { "ltr": "images/icons/unBlock-ltr.svg", "rtl": "images/icons/unBlock-rtl.svg" - } }, + }, "deprecated": "Renamed since v0.18.3, use 'unBlock' instead." }, "unBlock": { "file": { "ltr": "images/icons/unBlock-ltr.svg", "rtl": "images/icons/unBlock-rtl.svg" @@ -18,7 +18,7 @@ "flagUndo": { "file": { "ltr": "images/icons/unFlag-ltr.svg", "rtl": "images/icons/unFlag-rtl.svg" - } }, + }, "deprecated": "Renamed since v0.18.3, use 'unFlag' instead." }, "unFlag": { "file": { "ltr": "images/icons/unFlag-ltr.svg", "rtl": "images/icons/unFlag-rtl.svg" @@ -42,7 +42,7 @@ "trashUndo": { "file": { "ltr": "images/icons/unTrash-ltr.svg", "rtl": "images/icons/unTrash-rtl.svg" - } }, + }, "deprecated": "Renamed since v0.18.3, use 'unTrash' instead." }, "ongoingConversation": { "file": { "ltr": "images/icons/ongoingConversation-ltr.svg", diff --git a/resources/lib/oojs-ui/themes/apex/icons.json b/resources/lib/oojs-ui/themes/apex/icons.json index b5fbbed0cd..f4a2dc9269 100644 --- a/resources/lib/oojs-ui/themes/apex/icons.json +++ b/resources/lib/oojs-ui/themes/apex/icons.json @@ -2,7 +2,7 @@ "prefix": "oo-ui-icon", "intro": "@import '../../../../src/styles/common';", "images": { - "add": { "file": "images/icons/add.svg" }, + "add": { "file": "images/icons/add.svg", "deprecated": "Moved since v0.19.5, use from the 'interactive' pack instead." }, "advanced": { "file": "images/icons/advanced.svg" }, "alert": { "file": "images/icons/alert.svg" }, "cancel": { "file": "images/icons/cancel.svg" }, diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png new file mode 100644 index 0000000000..8deeddf49e Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.svg b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.svg new file mode 100644 index 0000000000..a2d49a101f --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/add-invert.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/close.svg b/resources/lib/oojs-ui/themes/apex/images/icons/close.svg index d058d65b31..b379e18573 100644 --- a/resources/lib/oojs-ui/themes/apex/images/icons/close.svg +++ b/resources/lib/oojs-ui/themes/apex/images/icons/close.svg @@ -1,6 +1,4 @@ - - - + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png new file mode 100644 index 0000000000..73dd6b9c91 Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.svg new file mode 100644 index 0000000000..eb42923d68 --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-ltr.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png new file mode 100644 index 0000000000..2ea298305f Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.svg new file mode 100644 index 0000000000..9b1940ebef --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/highlight-rtl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png new file mode 100644 index 0000000000..2af4eb0e08 Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.svg b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.svg new file mode 100644 index 0000000000..8a83e4179e --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/journal-ltr.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png new file mode 100644 index 0000000000..033ede1388 Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.svg b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.svg new file mode 100644 index 0000000000..2a07a44aeb --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/journal-rtl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png new file mode 100644 index 0000000000..4e58b70a24 Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.svg b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.svg new file mode 100644 index 0000000000..9595ac67c7 --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/subtract-invert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract.png b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.png new file mode 100644 index 0000000000..bd719cb675 Binary files /dev/null and b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.png differ diff --git a/resources/lib/oojs-ui/themes/apex/images/icons/subtract.svg b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.svg new file mode 100644 index 0000000000..a79f0b508e --- /dev/null +++ b/resources/lib/oojs-ui/themes/apex/images/icons/subtract.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json index c97d770273..85e47ee9b2 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-editing-styling.json @@ -56,6 +56,10 @@ "os": "images/icons/bold-cyrl-be.svg" } } }, + "highlight": { "file": { + "ltr": "images/icons/highlight-ltr.svg", + "rtl": "images/icons/highlight-rtl.svg" + } }, "italic": { "file": { "default": "images/icons/italic-a.svg", "lang": { diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json b/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json index 3436446e04..827a72e157 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-interactions.json @@ -22,8 +22,9 @@ } }, "images": { - "beta": { "file": "images/icons/beta.svg" }, - "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg" }, + "add": { "file": "images/icons/add.svg", "variants": [ "constructive", "progressive" ] }, + "beta": { "file": "images/icons/beta.svg", "deprecated": "Deprecated since v0.18.3, don't use." }, + "betaLaunch": { "file": "images/icons/logo-wikimediaDiscovery.svg", "deprecated": "Moved since v0.18.3, use 'logoWikimediaDiscovery' from the 'Wikimedia' pack instead." }, "bookmark": { "file": { "ltr": "images/icons/bookmark-ltr.svg", "rtl": "images/icons/bookmark-rtl.svg" @@ -66,7 +67,8 @@ "ltr": "images/icons/printer-ltr.svg", "rtl": "images/icons/printer-rtl.svg" } }, - "ribbonPrize": { "file": "images/icons/ribbonPrize.svg" }, + "ribbonPrize": { "file": "images/icons/ribbonPrize.svg", "deprecated": "Deprecated since v0.18.3, don't use." }, + "subtract": { "file": "images/icons/subtract.svg" }, "sun": { "file": { "ltr": "images/icons/sun-ltr.svg", "rtl": "images/icons/sun-rtl.svg" diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json b/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json index 6a472676d8..b4acff1ce8 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons-moderation.json @@ -26,7 +26,7 @@ "blockUndo": { "file": { "ltr": "images/icons/unBlock-ltr.svg", "rtl": "images/icons/unBlock-rtl.svg" - } }, + }, "deprecated": "Renamed since v0.18.3, use 'unBlock' instead." }, "unBlock": { "file": { "ltr": "images/icons/unBlock-ltr.svg", "rtl": "images/icons/unBlock-rtl.svg" @@ -42,7 +42,7 @@ "flagUndo": { "file": { "ltr": "images/icons/unFlag-ltr.svg", "rtl": "images/icons/unFlag-rtl.svg" - } }, + }, "deprecated": "Renamed since v0.18.3, use 'unFlag' instead." }, "lock": { "file": { "ltr": "images/icons/lock-ltr.svg", "rtl": "images/icons/lock-rtl.svg" @@ -65,7 +65,7 @@ "trashUndo": { "file": { "ltr": "images/icons/unTrash-ltr.svg", "rtl": "images/icons/unTrash-rtl.svg" - } }, + }, "deprecated": "Renamed since v0.18.3, use 'unTrash' instead." }, "ongoingConversation": { "file": { "ltr": "images/icons/ongoingConversation-ltr.svg", diff --git a/resources/lib/oojs-ui/themes/mediawiki/icons.json b/resources/lib/oojs-ui/themes/mediawiki/icons.json index 4666fd1a59..60c05f3e9a 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/icons.json +++ b/resources/lib/oojs-ui/themes/mediawiki/icons.json @@ -22,16 +22,13 @@ } }, "images": { - "add": { "file": "images/icons/add.svg", "variants": [ "constructive", "progressive" ] }, + "add": { "file": "images/icons/add.svg", "variants": [ "constructive", "progressive" ], "deprecated": "Moved since v0.19.5, use from the 'interactive' pack instead." }, "advanced": { "file": "images/icons/advanced.svg" }, "alert": { "file": "images/icons/alert.svg", "variants": [ "warning" ] }, "cancel": { "file": "images/icons/cancel.svg", "variants": [ "destructive" ] }, "check": { "file": "images/icons/check.svg", "variants": [ "constructive", "progressive", "destructive" ] }, "circle": { "file": "images/icons/circle.svg", "variants": [ "constructive", "progressive" ] }, - "close": { "file": { - "ltr": "images/icons/close-ltr.svg", - "rtl": "images/icons/close-rtl.svg" - } }, + "close": { "file": "images/icons/close.svg" }, "code": { "file": "images/icons/code.svg" }, "collapse": { "file": "images/icons/collapse.svg" }, "comment": { "file": "images/icons/comment.svg" }, diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png new file mode 100644 index 0000000000..92af7b6fa0 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.svg new file mode 100644 index 0000000000..8027fffd5e --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-invert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png deleted file mode 100644 index a35e9d12d2..0000000000 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.png and /dev/null differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.svg deleted file mode 100644 index 6de3e300c1..0000000000 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-invert.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png deleted file mode 100644 index 69f07873e3..0000000000 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.png and /dev/null differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.svg deleted file mode 100644 index 122340eca3..0000000000 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr-progressive.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png deleted file mode 100644 index b6a42b8054..0000000000 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.png and /dev/null differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.svg deleted file mode 100644 index c2693163e4..0000000000 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-ltr.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png new file mode 100644 index 0000000000..06fcd5d413 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.svg new file mode 100644 index 0000000000..37c960e45c --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-progressive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png deleted file mode 100644 index 16462a2683..0000000000 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.png and /dev/null differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.svg deleted file mode 100644 index 67dc06c8b4..0000000000 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-invert.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png deleted file mode 100644 index 2c88596096..0000000000 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.png and /dev/null differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.svg deleted file mode 100644 index c6494de536..0000000000 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl-progressive.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png deleted file mode 100644 index e3e24176b5..0000000000 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.png and /dev/null differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.svg deleted file mode 100644 index 36e58ec2f0..0000000000 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close-rtl.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png new file mode 100644 index 0000000000..cdb037ad53 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.svg new file mode 100644 index 0000000000..88ffb56d89 --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png new file mode 100644 index 0000000000..314d7acb85 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.svg new file mode 100644 index 0000000000..e301deaf1d --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-invert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png new file mode 100644 index 0000000000..14bd7becfc Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.svg new file mode 100644 index 0000000000..4cc90b523a --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr-progressive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png new file mode 100644 index 0000000000..73dd6b9c91 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.svg new file mode 100644 index 0000000000..eb42923d68 --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-ltr.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png new file mode 100644 index 0000000000..024595e2f8 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.svg new file mode 100644 index 0000000000..46d61c2258 --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-invert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png new file mode 100644 index 0000000000..039ccbe720 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.svg new file mode 100644 index 0000000000..4f3997a944 --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl-progressive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png new file mode 100644 index 0000000000..2ea298305f Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.svg new file mode 100644 index 0000000000..9b1940ebef --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/highlight-rtl.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png index e7dc90ebc5..bc2e9cc6ff 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.svg index e04818fa7c..370d0a374b 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-invert.svg @@ -1,4 +1,4 @@ - + - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png index 444d48b579..19f17bdb9d 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.svg index 0232e22c33..baf8a4a626 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr-progressive.svg @@ -1,4 +1,4 @@ - + - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png index f30b5ff3a1..2af4eb0e08 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.svg index c9fa5536c8..8a83e4179e 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-ltr.svg @@ -1,4 +1,4 @@ - + - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png index deb4f76aca..60b5f473e6 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.svg index 3487511e43..4660285938 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-invert.svg @@ -1,4 +1,4 @@ - + - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png index e5a4fa1208..b16badc620 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.svg index f2eb554994..17ccb82c73 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl-progressive.svg @@ -1,4 +1,4 @@ - + - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png index 7826fa80f8..033ede1388 100644 Binary files a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.svg index 84da9fa60f..2a07a44aeb 100644 --- a/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.svg +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/journal-rtl.svg @@ -1,4 +1,4 @@ - + - + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png new file mode 100644 index 0000000000..4e58b70a24 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.svg new file mode 100644 index 0000000000..d76eb3c768 --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-invert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png new file mode 100644 index 0000000000..173297207c Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.svg new file mode 100644 index 0000000000..24795ac20d --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract-progressive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png new file mode 100644 index 0000000000..bd719cb675 Binary files /dev/null and b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.png differ diff --git a/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.svg b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.svg new file mode 100644 index 0000000000..a79f0b508e --- /dev/null +++ b/resources/lib/oojs-ui/themes/mediawiki/images/icons/subtract.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js index 18f1299377..0df34f8f16 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FilterItem.js @@ -378,4 +378,13 @@ mw.rcfilters.dm.FilterItem.prototype.isHighlightSupported = function () { return !!this.getCssClass(); }; + + /** + * Check if the filter is currently highlighted + * + * @return {boolean} + */ + mw.rcfilters.dm.FilterItem.prototype.isHighlighted = function () { + return this.isHighlightEnabled() && !!this.getHighlightColor(); + }; }( mediaWiki ) ); diff --git a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js index 2afe286095..5be3656988 100644 --- a/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js +++ b/resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.FiltersViewModel.js @@ -359,7 +359,7 @@ mw.rcfilters.dm.FiltersViewModel.prototype.setFiltersToDefaults = function () { var defaultFilterStates = this.getFiltersFromParameters( this.getDefaultParams() ); - this.updateFilters( defaultFilterStates ); + this.toggleFiltersSelected( defaultFilterStates ); }; /** @@ -418,7 +418,7 @@ * are the selected highlight colors. */ mw.rcfilters.dm.FiltersViewModel.prototype.getHighlightParameters = function () { - var result = { highlight: this.isHighlightEnabled() }; + var result = { highlight: Number( this.isHighlightEnabled() ) }; this.getItems().forEach( function ( filterItem ) { result[ filterItem.getName() + '_color' ] = filterItem.getHighlightColor(); @@ -472,15 +472,10 @@ * @return {boolean} Current filters are all empty */ mw.rcfilters.dm.FiltersViewModel.prototype.areCurrentFiltersEmpty = function () { - var model = this; - // Check if there are either any selected items or any items // that have highlight enabled return !this.getItems().some( function ( filterItem ) { - return ( - filterItem.isSelected() || - ( model.isHighlightEnabled() && filterItem.getHighlightColor() ) - ); + return filterItem.isSelected() || filterItem.isHighlighted(); } ); }; @@ -602,14 +597,19 @@ * This is equivalent to display all. */ mw.rcfilters.dm.FiltersViewModel.prototype.emptyAllFilters = function () { - var filters = {}; - this.getItems().forEach( function ( filterItem ) { - filters[ filterItem.getName() ] = false; - } ); + this.toggleFilterSelected( filterItem.getName(), false ); + }.bind( this ) ); + }; - // Update filters - this.updateFilters( filters ); + /** + * Toggle selected state of one item + * + * @param {string} name Name of the filter item + * @param {boolean} [isSelected] Filter selected state + */ + mw.rcfilters.dm.FiltersViewModel.prototype.toggleFilterSelected = function ( name, isSelected ) { + this.getItemByName( name ).toggleSelected( isSelected ); }; /** @@ -617,13 +617,10 @@ * * @param {Object} filterDef Filter definitions */ - mw.rcfilters.dm.FiltersViewModel.prototype.updateFilters = function ( filterDef ) { - var name, filterItem; - - for ( name in filterDef ) { - filterItem = this.getItemByName( name ); - filterItem.toggleSelected( filterDef[ name ] ); - } + mw.rcfilters.dm.FiltersViewModel.prototype.toggleFiltersSelected = function ( filterDef ) { + Object.keys( filterDef ).forEach( function ( name ) { + this.toggleFilterSelected( name, filterDef[ name ] ); + }.bind( this ) ); }; /** @@ -726,7 +723,7 @@ * @return {boolean} */ mw.rcfilters.dm.FiltersViewModel.prototype.isHighlightEnabled = function () { - return this.highlightEnabled; + return !!this.highlightEnabled; }; /** diff --git a/resources/src/mediawiki.rcfilters/images/marker-ltr.svg b/resources/src/mediawiki.rcfilters/images/marker-ltr.svg deleted file mode 100644 index eb42923d68..0000000000 --- a/resources/src/mediawiki.rcfilters/images/marker-ltr.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/src/mediawiki.rcfilters/images/marker-rtl.svg b/resources/src/mediawiki.rcfilters/images/marker-rtl.svg deleted file mode 100644 index 9b1940ebef..0000000000 --- a/resources/src/mediawiki.rcfilters/images/marker-rtl.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js index 1c0590901d..e562057845 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js @@ -20,13 +20,20 @@ * @param {Object} filterStructure Filter definition and structure for the model */ mw.rcfilters.Controller.prototype.initialize = function ( filterStructure ) { - var uri = new mw.Uri(); - // Initialize the model this.filtersModel.initializeFilters( filterStructure ); + this.updateStateBasedOnUrl(); + }; + + /** + * Update filter state (selection and highlighting) based + * on current URL and default values. + */ + mw.rcfilters.Controller.prototype.updateStateBasedOnUrl = function () { + var uri = new mw.Uri(); // Set filter states based on defaults and URL params - this.filtersModel.updateFilters( + this.filtersModel.toggleFiltersSelected( this.filtersModel.getFiltersFromParameters( // Merge defaults with URL params for initialization $.extend( @@ -43,11 +50,11 @@ this.filtersModel.toggleHighlight( !!uri.query.highlight ); this.filtersModel.getItems().forEach( function ( filterItem ) { var color = uri.query[ filterItem.getName() + '_color' ]; - if ( !color ) { - return; + if ( color ) { + filterItem.setHighlightColor( color ); + } else { + filterItem.clearHighlightColor(); } - - filterItem.setHighlightColor( color ); } ); // Check all filter interactions @@ -59,6 +66,7 @@ */ mw.rcfilters.Controller.prototype.resetToDefaults = function () { this.filtersModel.setFiltersToDefaults(); + this.filtersModel.clearAllHighlightColors(); // Check all filter interactions this.filtersModel.reassessFilterInteractions(); @@ -84,19 +92,17 @@ * @param {boolean} [isSelected] Filter selected state */ mw.rcfilters.Controller.prototype.toggleFilterSelect = function ( filterName, isSelected ) { - var obj = {}, - filterItem = this.filtersModel.getItemByName( filterName ); + var filterItem = this.filtersModel.getItemByName( filterName ); isSelected = isSelected === undefined ? !filterItem.isSelected() : isSelected; if ( filterItem.isSelected() !== isSelected ) { - obj[ filterName ] = isSelected; - this.filtersModel.updateFilters( obj ); + this.filtersModel.toggleFilterSelected( filterName, isSelected ); this.updateChangesList(); // Check filter interactions - this.filtersModel.reassessFilterInteractions( this.filtersModel.getItemByName( filterName ) ); + this.filtersModel.reassessFilterInteractions( filterItem ); } }; @@ -112,14 +118,22 @@ * @param {Object} [params] Extra parameters to add to the API call */ mw.rcfilters.Controller.prototype.updateURL = function ( params ) { - var uri; + var updatedUri, + notEquivalent = function ( obj1, obj2 ) { + var keys = Object.keys( obj1 ).concat( Object.keys( obj2 ) ); + return keys.some( function ( key ) { + return obj1[ key ] != obj2[ key ]; // eslint-disable-line eqeqeq + } ); + }; params = params || {}; - uri = this.getUpdatedUri(); - uri.extend( params ); + updatedUri = this.getUpdatedUri(); + updatedUri.extend( params ); - window.history.pushState( { tag: 'rcfilters' }, document.title, uri.toString() ); + if ( notEquivalent( updatedUri.query, new mw.Uri().query ) ) { + window.history.pushState( { tag: 'rcfilters' }, document.title, updatedUri.toString() ); + } }; /** @@ -247,4 +261,32 @@ this.filtersModel.clearHighlightColor( filterName ); this.updateURL(); }; + + /** + * Clear both highlight and selection of a filter + * + * @param {string} filterName Name of the filter item + */ + mw.rcfilters.Controller.prototype.clearFilter = function ( filterName ) { + var filterItem = this.filtersModel.getItemByName( filterName ); + + if ( filterItem.isSelected() || filterItem.isHighlighted() ) { + this.filtersModel.clearHighlightColor( filterName ); + this.filtersModel.toggleFilterSelected( filterName, false ); + this.updateChangesList(); + this.filtersModel.reassessFilterInteractions( filterItem ); + } + }; + + /** + * Synchronize the URL with the current state of the filters + * without adding an history entry. + */ + mw.rcfilters.Controller.prototype.replaceUrl = function () { + window.history.replaceState( + { tag: 'rcfilters' }, + document.title, + this.getUpdatedUri().toString() + ); + }; }( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js index 255d93b692..a0b785d32c 100644 --- a/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js +++ b/resources/src/mediawiki.rcfilters/mw.rcfilters.init.js @@ -193,6 +193,7 @@ $( '.rcfilters-head' ).addClass( 'mw-rcfilters-ui-ready' ); window.addEventListener( 'popstate', function () { + controller.updateStateBasedOnUrl(); controller.updateChangesList(); } ); @@ -200,6 +201,8 @@ 'href', 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:New_filters_for_edit_review' ); + + controller.replaceUrl(); } }; diff --git a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less index 0f30137bab..198f599715 100644 --- a/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less +++ b/resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.FilterItemHighlightButton.less @@ -1,11 +1,6 @@ @import 'mw.rcfilters.mixins'; .mw-rcfilters-ui-filterItemHighlightButton { - .oo-ui-iconElement-icon.oo-ui-icon-highlight { - /* @embed */ - background-image: url( ../images/marker-ltr.svg ); - } - .oo-ui-buttonWidget.oo-ui-popupButtonWidget .oo-ui-buttonElement-button > &-circle { display: inline-block; vertical-align: middle; diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js index 05f2f66639..f28523a765 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.CapsuleItemWidget.js @@ -38,6 +38,7 @@ popup: { padded: false, align: 'center', + position: 'above', $content: $popupContent .append( descLabelWidget.$element ), $floatableContainer: this.$element, @@ -111,8 +112,7 @@ } // Respond to user removing the filter - this.controller.toggleFilterSelect( this.model.getName(), false ); - this.controller.clearHighlightColor( this.model.getName() ); + this.controller.clearFilter( this.model.getName() ); }; mw.rcfilters.ui.CapsuleItemWidget.prototype.setHighlightColor = function () { @@ -149,8 +149,8 @@ mw.rcfilters.ui.CapsuleItemWidget.prototype.onMouseEnter = function () { if ( this.model.getDescription() ) { if ( !this.positioned ) { - // Recalculate position to be center of the capsule item - this.popup.$element.css( 'margin-left', ( this.$element.width() / 2 ) ); + // Recalculate anchor position to be center of the capsule item + this.popup.$anchor.css( 'margin-left', ( this.$element.width() / 2 ) ); this.positioned = true; } diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js index 17aad51ce1..889ba08cf4 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js @@ -19,7 +19,11 @@ icon: 'highlight', indicator: 'down', popup: { - anchor: false, + // TODO: There is a bug in non-anchored popups in + // OOUI, so we set this popup to "anchored" until + // the bug is fixed. + // See: https://phabricator.wikimedia.org/T159906 + anchor: true, padded: true, align: 'backwards', horizontalPosition: 'end', diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js index 3f67da4539..e64a4c055b 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js @@ -56,7 +56,8 @@ $content: this.filterPopup.$element, $footer: $footer, classes: [ 'mw-rcfilters-ui-filterWrapperWidget-popup' ], - width: 650 + width: 650, + hideWhenOutOfView: false } } ); diff --git a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js index cefe7492dd..4011e6d5d2 100644 --- a/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js +++ b/resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FiltersListWidget.js @@ -29,7 +29,8 @@ this.groups = {}; this.selected = null; - this.highlightButton = new OO.ui.ButtonWidget( { + this.highlightButton = new OO.ui.ToggleButtonWidget( { + icon: 'highlight', label: mw.message( 'rcfilters-highlightbutton-title' ).text(), classes: [ 'mw-rcfilters-ui-filtersListWidget-hightlightButton' ] } ); @@ -43,7 +44,7 @@ this.highlightButton.connect( this, { click: 'onHighlightButtonClick' } ); this.model.connect( this, { initialize: 'onModelInitialize', - highlightChange: 'onHighlightChange' + highlightChange: 'onModelHighlightChange' } ); // Initialize @@ -109,7 +110,12 @@ ); }; - mw.rcfilters.ui.FiltersListWidget.prototype.onHighlightChange = function ( highlightEnabled ) { + /** + * Respond to model highlight change event + * + * @param {boolean} highlightEnabled Highlight is enabled + */ + mw.rcfilters.ui.FiltersListWidget.prototype.onModelHighlightChange = function ( highlightEnabled ) { this.highlightButton.setActive( highlightEnabled ); }; diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index 37f76ff4d1..419ff00f85 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -439,7 +439,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { * @return string Absolute name of the temporary file */ protected function getNewTempFile() { - $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . get_class( $this ) . '_' ); + $fileName = tempnam( wfTempDir(), 'MW_PHPUnit_' . static::class . '_' ); $this->tmpFiles[] = $fileName; return $fileName; @@ -1304,8 +1304,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase { if ( isset( $compatibility[$func] ) ) { return call_user_func_array( [ $this, $compatibility[$func] ], $args ); } else { - throw new MWException( "Called non-existent $func method on " - . get_class( $this ) ); + throw new MWException( "Called non-existent $func method on " . static::class ); } } diff --git a/tests/phpunit/includes/api/ApiTestCase.php b/tests/phpunit/includes/api/ApiTestCase.php index 6b299c98c8..abef1c9206 100644 --- a/tests/phpunit/includes/api/ApiTestCase.php +++ b/tests/phpunit/includes/api/ApiTestCase.php @@ -219,7 +219,7 @@ abstract class ApiTestCase extends MediaWikiLangTestCase { } public function testApiTestGroup() { - $groups = PHPUnit_Util_Test::getGroups( get_class( $this ) ); + $groups = PHPUnit_Util_Test::getGroups( static::class ); $constraint = PHPUnit_Framework_Assert::logicalOr( $this->contains( 'medium' ), $this->contains( 'large' ) diff --git a/tests/phpunit/includes/tidy/RemexDriverTest.php b/tests/phpunit/includes/tidy/RemexDriverTest.php new file mode 100644 index 0000000000..6b16cbf695 --- /dev/null +++ b/tests/phpunit/includes/tidy/RemexDriverTest.php @@ -0,0 +1,297 @@ +x

" + ], + [ + 'No p-wrap of blank node', + " ", + " " + ], + [ + 'p-wrap terminated by div', + "x
", + "

x

" + ], + [ + 'p-wrap not terminated by span', + "x", + "

x

" + ], + [ + 'An element is non-blank and so gets p-wrapped', + "", + "

" + ], + [ + 'The blank flag is set after a block-level element', + "
", + "
" + ], + [ + 'Blank detection between two block-level elements', + "
", + "
" + ], + [ + 'But p-wrapping of non-blank content works after an element', + "
x", + "

x

" + ], + [ + 'p-wrapping between two block-level elements', + "
x
", + "

x

" + ], + [ + 'p-wrap inside blockquote', + "
x
", + "

x

" + ], + [ + 'A comment is blank for p-wrapping purposes', + "", + "" + ], + [ + 'A comment is blank even when a p-wrap was opened by a text node', + " ", + " " + ], + [ + 'A comment does not open a p-wrap', + "x", + "

x

" + ], + [ + 'A comment does not close a p-wrap', + "x", + "

x

" + ], + [ + 'Empty li', + "
", + "
" + ], + [ + 'li with element', + "
", + "
" + ], + [ + 'li with text', + "
  • x
", + "
  • x
" + ], + [ + 'Empty tr', + "
", + "
" + ], + [ + 'Empty p', + "

\n

", + "

\n

" + ], + [ + 'No p-wrapping of an inline element which contains a block element (T150317)', + "
x
", + "
x
" + ], + [ + 'p-wrapping of an inline element which contains an inline element', + "x", + "

x

" + ], + [ + 'p-wrapping is enabled in a blockquote in an inline element', + "
x
", + "

x

" + ], + [ + 'All bare text should be p-wrapped even when surrounded by block tags', + "
x
y
z", + "

x

y

z

" + ], + [ + 'Split tag stack 1', + "x
y
z
", + "

x

y

z

" + ], + [ + 'Split tag stack 2', + "
y
z
", + "
y

z

" + ], + [ + 'Split tag stack 3', + "x
y
", + "

x

y
" + ], + [ + 'Split tag stack 4 (modified to use splittable tag)', + "abc
d
e
", + "

abc

d

e

" + ], + [ + "Split tag stack regression check 1", + "x
y
", + "

x

y
" + ], + [ + "Split tag stack regression check 2 (modified to use splittable tag)", + "a
d
e
", + "

a

d

e

" + ], + // Simple tests from pwrap.js + [ + 'Simple pwrap test 1', + 'a', + '

a

' + ], + [ + ' is not a splittable tag, but gets p-wrapped in simple wrapping scenarios', + 'a', + '

a

' + ], + [ + 'Simple pwrap test 3', + 'x
a
b
y', + '

x

a
b

y

' + ], + [ + 'Simple pwrap test 4', + 'x
a
b
y', + '

x

a
b

y

' + ], + // Complex tests from pwrap.js + [ + 'Complex pwrap test 1', + 'x
a
y
', + '

x

a

y

' + ], + [ + 'Complex pwrap test 2', + 'abc
d
e
f', + '

abc

d

ef

' + ], + [ + 'Complex pwrap test 3', + 'abc
d
e
', + '

abc

d

e

' + ], + [ + 'Complex pwrap test 4', + 'x
y
', + '

x

y
' + ], + [ + 'Complex pwrap test 5', + 'a
d
e
', + '

a

d

e

' + ], + [ + 'Complex pwrap test 6', + 'a
b
cd
e
f
g
', + // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong + // PHP 5 does not allow concatenation in initialisation of a class static variable + '

a

b

cd

e

fg

' + // @codingStandardsIgnoreEnd + ], + /* FIXME the second causes a stack split which clones the even + * though no

is actually generated + [ + 'Complex pwrap test 7', + '

x
y
z
', + '
x
y
z
' + ], + */ + // New local tests + [ + 'Blank text node after block end', + 'x
y
z
', + '

x

y

z

' + ], + [ + 'Text node fostering (FIXME: wrap missing)', + 'x
', + 'x
' + ], + [ + 'Blockquote fostering', + '
x
', + '

x

' + ], + [ + 'Block element fostering', + '
x', + '
x
' + ], + [ + 'Formatting element fostering (FIXME: wrap missing)', + 'x', + 'x
' + ], + [ + 'AAA clone of p-wrapped element (FIXME: empty b)', + 'x

yz

', + '

x

yz

', + ], + [ + 'AAA with fostering (FIXME: wrap missing)', + '1

23

', + '1

23

' + ], + ]; + + public function provider() { + return self::$remexTidyTestData; + } + + /** + * @dataProvider provider + * @covers MediaWiki\Tidy\RemexCompatFormatter + * @covers MediaWiki\Tidy\RemexCompatMunger + * @covers MediaWiki\Tidy\RemexDriver + * @covers MediaWiki\Tidy\RemexMungerData + */ + public function testTidy( $desc, $input, $expected ) { + $r = new MediaWiki\Tidy\RemexDriver( [] ); + $result = $r->tidy( $input ); + $this->assertEquals( $expected, $result, $desc ); + } + + public function html5libProvider() { + $files = json_decode( file_get_contents( __DIR__ . '/html5lib-tests.json' ), true ); + $tests = []; + foreach ( $files as $file => $fileTests ) { + foreach ( $fileTests as $i => $test ) { + $tests[] = [ "$file:$i", $test['data'] ]; + } + } + return $tests; + } + + /** + * This is a quick and dirty test to make sure none of the html5lib tests + * generate exceptions. We don't really know what the expected output is. + * + * @dataProvider html5libProvider + * @coversNothing + */ + public function testHtml5Lib( $desc, $input ) { + $r = new MediaWiki\Tidy\RemexDriver( [] ); + $result = $r->tidy( $input ); + $this->assertTrue( true, $desc ); + } +} diff --git a/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php b/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php index 76cedc60ea..520108aac1 100644 --- a/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php +++ b/tests/phpunit/includes/title/NamespaceAwareForeignTitleFactoryTest.php @@ -36,10 +36,18 @@ class NamespaceAwareForeignTitleFactoryTest extends MediaWikiTestCase { 'MainNamespaceArticle', null, new ForeignTitle( 0, '', 'MainNamespaceArticle' ), ], + [ + 'Magic:_The_Gathering', 0, + new ForeignTitle( 0, '', 'Magic:_The_Gathering' ), + ], [ 'Talk:Nice_talk', 1, new ForeignTitle( 1, 'Talk', 'Nice_talk' ), ], + [ + 'Talk:Magic:_The_Gathering', 1, + new ForeignTitle( 1, 'Talk', 'Magic:_The_Gathering' ), + ], [ 'Bogus:Nice_talk', 0, new ForeignTitle( 0, '', 'Bogus:Nice_talk' ), @@ -56,6 +64,11 @@ class NamespaceAwareForeignTitleFactoryTest extends MediaWikiTestCase { 'Bogus:Nice_talk', 1, new ForeignTitle( 1, 'Talk', 'Nice_talk' ), ], + // Misconfigured wiki with unregistered namespace (T114115) + [ + 'Nice_talk', 1234, + new ForeignTitle( 1234, 'Ns1234', 'Nice_talk' ), + ], ]; } diff --git a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js index 3a940d081e..ad0ed54484 100644 --- a/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js +++ b/tests/qunit/suites/resources/mediawiki.rcfilters/dm.FiltersViewModel.test.js @@ -79,7 +79,7 @@ 'Initial state of filters' ); - model.updateFilters( { + model.toggleFiltersSelected( { group1filter1: true, group2filter2: true, group3filter1: true @@ -276,7 +276,7 @@ ); // Select 1 filter - model.updateFilters( { + model.toggleFiltersSelected( { hidefilter1: true, hidefilter2: false, hidefilter3: false, @@ -302,7 +302,7 @@ ); // Select 2 filters - model.updateFilters( { + model.toggleFiltersSelected( { hidefilter1: true, hidefilter2: true, hidefilter3: false, @@ -328,7 +328,7 @@ ); // Select 3 filters - model.updateFilters( { + model.toggleFiltersSelected( { hidefilter1: true, hidefilter2: true, hidefilter3: true, @@ -354,7 +354,7 @@ ); // Select 1 filter from string_options - model.updateFilters( { + model.toggleFiltersSelected( { filter7: true, filter8: false, filter9: false @@ -377,7 +377,7 @@ ); // Select 2 filters from string_options - model.updateFilters( { + model.toggleFiltersSelected( { filter7: true, filter8: true, filter9: false @@ -400,7 +400,7 @@ ); // Select 3 filters from string_options - model.updateFilters( { + model.toggleFiltersSelected( { filter7: true, filter8: true, filter9: true @@ -550,23 +550,23 @@ // This test is demonstrating wrong usage of the method; // We should be aware that getFiltersFromParameters is stateless, // so each call gives us a filter state that only reflects the query given. - // This means that the two calls to updateFilters() below collide. + // This means that the two calls to toggleFiltersSelected() below collide. // The result of the first is overridden by the result of the second, // since both get a full state object from getFiltersFromParameters that **only** relates // to the input it receives. - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { hidefilter1: '1' } ) ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { hidefilter6: '1' } ) ); - // The result here is ignoring the first updateFilters call + // The result here is ignoring the first toggleFiltersSelected call // We should receive default values + hidefilter6 as false assert.deepEqual( model.getSelectedState(), @@ -581,12 +581,12 @@ model = new mw.rcfilters.dm.FiltersViewModel(); model.initializeFilters( definition ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { hidefilter1: '0' } ) ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { hidefilter1: '1' } ) @@ -600,7 +600,7 @@ 'After checking and then unchecking a \'send_unselected_if_any\' filter (without touching other filters in that group), results are default' ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { group3: 'filter7' } ) @@ -615,7 +615,7 @@ 'A \'string_options\' parameter containing 1 value, results in the corresponding filter as checked' ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { group3: 'filter7,filter8' } ) @@ -630,7 +630,7 @@ 'A \'string_options\' parameter containing 2 values, results in both corresponding filters as checked' ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { group3: 'filter7,filter8,filter9' } ) @@ -645,7 +645,7 @@ 'A \'string_options\' parameter containing all values, results in all filters of the group as unchecked.' ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { group3: 'filter7,all,filter9' } ) @@ -660,7 +660,7 @@ 'A \'string_options\' parameter containing the value \'all\', results in all filters of the group as unchecked.' ); - model.updateFilters( + model.toggleFiltersSelected( model.getFiltersFromParameters( { group3: 'filter7,foo,filter9' } ) @@ -797,7 +797,7 @@ 'Initial state: default filters are not selected (controller selects defaults explicitly).' ); - model.updateFilters( { + model.toggleFiltersSelected( { hidefilter1: false, hidefilter3: false } ); @@ -870,7 +870,7 @@ model.initializeFilters( definition ); // Select a filter that has subset with another filter - model.updateFilters( { + model.toggleFiltersSelected( { filter1: true } ); @@ -886,7 +886,7 @@ ); // Select another filter that has a subset with the same previous filter - model.updateFilters( { + model.toggleFiltersSelected( { filter4: true } ); model.reassessFilterInteractions( model.getItemByName( 'filter4' ) ); @@ -903,7 +903,7 @@ ); // Remove one filter (but leave the other) that affects filter2 - model.updateFilters( { + model.toggleFiltersSelected( { filter1: false } ); model.reassessFilterInteractions( model.getItemByName( 'filter1' ) ); @@ -918,7 +918,7 @@ 'Removing a filter only un-includes its subset if there is no other filter affecting.' ); - model.updateFilters( { + model.toggleFiltersSelected( { filter4: false } ); model.reassessFilterInteractions( model.getItemByName( 'filter4' ) ); @@ -994,7 +994,7 @@ ); // Select most (but not all) items in each group - model.updateFilters( { + model.toggleFiltersSelected( { filter1: true, filter2: true, filter4: true, @@ -1009,7 +1009,7 @@ ); // Select all items in 'fullCoverage' group (group2) - model.updateFilters( { + model.toggleFiltersSelected( { filter6: true } ); @@ -1025,7 +1025,7 @@ ); // Select all items in non 'fullCoverage' group (group1) - model.updateFilters( { + model.toggleFiltersSelected( { filter3: true } ); @@ -1041,7 +1041,7 @@ ); // Uncheck an item from each group - model.updateFilters( { + model.toggleFiltersSelected( { filter3: false, filter5: false } ); @@ -1107,7 +1107,7 @@ ); // Select a filter that has a conflict with another - model.updateFilters( { + model.toggleFiltersSelected( { filter1: true // conflicts: filter2, filter4 } ); @@ -1124,7 +1124,7 @@ ); // Select one of the conflicts (both filters are now conflicted and selected) - model.updateFilters( { + model.toggleFiltersSelected( { filter4: true // conflicts: filter 1 } ); model.reassessFilterInteractions( model.getItemByName( 'filter4' ) ); @@ -1141,7 +1141,7 @@ // Select another filter from filter4 group, meaning: // now filter1 no longer conflicts with filter4 - model.updateFilters( { + model.toggleFiltersSelected( { filter6: true // conflicts: filter2 } ); model.reassessFilterInteractions( model.getItemByName( 'filter6' ) );