Merge "Revert "Templatize Special:Contributions lines""
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Fri, 4 Mar 2016 15:29:30 +0000 (15:29 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 4 Mar 2016 15:29:30 +0000 (15:29 +0000)
15 files changed:
autoload.php
includes/context/RequestContext.php
includes/widget/SearchInputWidget.php [new file with mode: 0644]
maintenance/jsduck/categories.json
resources/Resources.php
resources/src/mediawiki.less/mediawiki.mixins.less
resources/src/mediawiki.less/mediawiki.ui/mixins.less
resources/src/mediawiki.ui/components/buttons.less
resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.css [new file with mode: 0644]
resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js [new file with mode: 0644]
resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js
resources/src/mediawiki/mediawiki.viewport.js [new file with mode: 0644]
tests/parserTests.php
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js [new file with mode: 0644]

index e882547..e74df0a 100644 (file)
@@ -821,6 +821,7 @@ $wgAutoloadLocalClasses = [
        'MediaWiki\\Widget\\ComplexNamespaceInputWidget' => __DIR__ . '/includes/widget/ComplexNamespaceInputWidget.php',
        'MediaWiki\\Widget\\ComplexTitleInputWidget' => __DIR__ . '/includes/widget/ComplexTitleInputWidget.php',
        'MediaWiki\\Widget\\NamespaceInputWidget' => __DIR__ . '/includes/widget/NamespaceInputWidget.php',
+       'MediaWiki\\Widget\\SearchInputWidget' => __DIR__ . '/includes/widget/SearchInputWidget.php',
        'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php',
        'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
        'MemCachedClientforWiki' => __DIR__ . '/includes/compat/MemcachedClientCompat.php',
index 63b4f71..35ee1b7 100644 (file)
@@ -496,7 +496,7 @@ class RequestContext implements IContextSource, MutableContext {
         * Resets singleton returned by getMain(). Should be called only from unit tests.
         */
        public static function resetMain() {
-               if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
+               if ( !( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_PARSER_TEST' ) ) ) {
                        throw new MWException( __METHOD__ . '() should be called only from unit tests!' );
                }
                self::$instance = null;
diff --git a/includes/widget/SearchInputWidget.php b/includes/widget/SearchInputWidget.php
new file mode 100644 (file)
index 0000000..7b3de4a
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * MediaWiki Widgets – SearchInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+namespace MediaWiki\Widget;
+
+/**
+ * Search input widget.
+ */
+class SearchInputWidget extends TitleInputWidget {
+
+       protected $pushPending = false;
+       protected $validateTitle = false;
+       protected $highlightFirst = false;
+
+       /**
+        * @param array $config Configuration options
+        * @param int|null $config['pushPending'] Whether the input should be visually marked as
+        *  "pending", while requesting suggestions (default: true)
+        */
+       public function __construct( array $config = [] ) {
+               // Parent constructor
+               parent::__construct(
+                       array_merge( [
+                               'infusable' => true,
+                               'maxLength' => null,
+                               'type' => 'search',
+                               'icon' => 'search'
+                       ], $config )
+               );
+
+               // Properties, which are ignored in PHP and just shipped back to JS
+               if ( isset( $config['pushPending'] ) ) {
+                       $this->pushPending = $config['pushPending'];
+               }
+
+               // Initialization
+               $this->addClasses( [ 'mw-widget-searchInputWidget' ] );
+       }
+
+       protected function getJavaScriptClassName() {
+               return 'mw.widgets.SearchInputWidget';
+       }
+
+       public function getConfig( &$config ) {
+               $config['pushPending'] = $this->pushPending;
+               return parent::getConfig( $config );
+       }
+}
index 41b56f6..d9e2c50 100644 (file)
@@ -32,7 +32,8 @@
                                        "mw.util",
                                        "mw.plugin.*",
                                        "mw.cookie",
-                                       "mw.experiments"
+                                       "mw.experiments",
+                                       "mw.viewport"
                                ]
                        },
                        {
index dcc02b7..bdf95a7 100644 (file)
@@ -1335,6 +1335,11 @@ return [
                'position' => 'top', // For $wgPreloadJavaScriptMwUtil
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'mediawiki.viewport' => [
+               'scripts' => 'resources/src/mediawiki/mediawiki.viewport.js',
+               'position' => 'top',
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
        'mediawiki.checkboxtoggle' => [
                'scripts' => 'resources/src/mediawiki/mediawiki.checkboxtoggle.js',
                'position' => 'top',
@@ -2213,6 +2218,25 @@ return [
                ],
                'targets' => [ 'desktop', 'mobile' ],
        ],
+       'mediawiki.widgets.SearchInputWidget' => [
+               'scripts' => [
+                       'resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js',
+               ],
+               'dependencies' => [
+                       'mediawiki.searchSuggest',
+                       // FIXME: Needs TitleInputWidget only
+                       'mediawiki.widgets',
+               ],
+       ],
+       'mediawiki.widgets.SearchInputWidget.styles' => [
+               'skinStyles' => [
+                       'default' => [
+                               'resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.css',
+                       ],
+               ],
+               'position' => 'top',
+               'targets' => [ 'desktop', 'mobile' ],
+       ],
 
        /* es5-shim */
        'es5-shim' => [
index a4dca02..3535be8 100644 (file)
        -ms-flex-order: @order; // IE 10
        order: @order;
 }
+
+
+// Screen Reader Helper Mixin
+.mixin-screen-reader-text() {
+       display: block;
+       position: absolute !important;
+       clip: rect( 1px, 1px, 1px, 1px );
+       width: 1px;
+       height: 1px;
+       margin: -1px;
+       border: 0;
+       padding: 0;
+       overflow: hidden;
+}
index 1b31956..93b7265 100644 (file)
@@ -53,6 +53,7 @@
        }
 
        &:active,
+       &.is-on,
        &.mw-ui-checked {
                background: @activeColor;
                box-shadow: none;
index 71febe3..40a67c5 100644 (file)
@@ -253,29 +253,35 @@ a.mw-ui-button {
 //
 // Markup:
 // <div class="mw-ui-button-group">
-//   <div class="mw-ui-button">A</div>
+//   <div class="mw-ui-button is-on">A</div>
 //   <div class="mw-ui-button">B</div>
 //   <div class="mw-ui-button">C</div>
 //   <div class="mw-ui-button">D</div>
 // </div><div style="clear:both"></div>
 //
 // Styleguide 2.2.
-.mw-ui-button-group > * {
-       min-width: 48px;
-       border-radius: 0;
-       float: left;
+.mw-ui-button-group {
+       & > * {
+               min-width: 48px;
+               border-radius: 0;
+               float: left;
 
-       &:first-child {
-               border-top-left-radius: @borderRadius;
-               border-bottom-left-radius: @borderRadius;
-       }
+               &:first-child {
+                       border-top-left-radius: @borderRadius;
+                       border-bottom-left-radius: @borderRadius;
+               }
+
+               &:not( :first-child ) {
+                       border-left: 0;
+               }
 
-       &:not( :first-child ) {
-               border-left: 0;
+               &:last-child {
+                       border-top-right-radius: @borderRadius;
+                       border-bottom-right-radius: @borderRadius;
+               }
        }
 
-       &:last-child{
-               border-top-right-radius: @borderRadius;
-               border-bottom-right-radius: @borderRadius;
+       & .is-on .button {
+               cursor: default;
        }
 }
diff --git a/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.css b/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.css
new file mode 100644 (file)
index 0000000..3da5d31
--- /dev/null
@@ -0,0 +1,3 @@
+.mw-widget-searchInputWidget {
+       display: inline-block;
+}
diff --git a/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js
new file mode 100644 (file)
index 0000000..1f526e2
--- /dev/null
@@ -0,0 +1,111 @@
+/*!
+ * MediaWiki Widgets - SearchInputWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+       /**
+        * Creates a mw.widgets.SearchInputWidget object.
+        *
+        * @class
+        * @extends mw.widgets.TitleInputWidget
+        *
+        * @constructor
+        * @cfg {boolean} [pushPending=true] Visually mark the input field as "pending", while
+        *  requesting suggestions.
+        */
+       mw.widgets.SearchInputWidget = function MwWidgetsSearchInputWidget( config ) {
+               config = $.extend( {
+                       type: 'search',
+                       icon: 'search',
+                       maxLength: undefined
+               }, config );
+
+               // Parent constructor
+               mw.widgets.SearchInputWidget.parent.call( this, config );
+
+               // Initialization
+               this.$element.addClass( 'mw-widget-searchInputWidget' );
+               this.lookupMenu.$element.addClass( 'mw-widget-searchWidget-menu' );
+               if ( !config.pushPending ) {
+                       this.pushPending = false;
+               }
+               this.setLookupsDisabled( !this.suggestions );
+       };
+
+       /* Setup */
+
+       OO.inheritClass( mw.widgets.SearchInputWidget, mw.widgets.TitleInputWidget );
+
+       /* Methods */
+
+       /**
+        * @inheritdoc mw.widgets.TitleWidget
+        */
+       mw.widgets.SearchInputWidget.prototype.getSuggestionsPromise = function () {
+               var api = new mw.Api();
+
+               // reuse the searchSuggest function from mw.searchSuggest
+               return mw.searchSuggest.request( api, this.getQueryValue(), $.noop, this.limit );
+       };
+
+       /**
+        * @inheritdoc mw.widgets.TitleInputWidget
+        */
+       mw.widgets.SearchInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
+               // mw.widgets.TitleInputWidget uses response.query, which doesn't exist for opensearch,
+               // so return the whole response (titles only, and links)
+               return response || {};
+       };
+
+       /**
+        * @inheritdoc mw.widgets.TitleWidget
+        */
+       mw.widgets.SearchInputWidget.prototype.getOptionsFromData = function ( data ) {
+               var items = [],
+                       self = this;
+
+               // mw.widgets.TitleWidget does a lot more work here, because the TitleOptionWidgets can
+               // differ a lot, depending on the returned data from the request. With the request used here
+               // we get only the search results.
+               $.each( data[ 1 ], function ( i, result ) {
+                       items.push( new mw.widgets.TitleOptionWidget(
+                               // data[ 3 ][ i ] is the link for this result
+                               self.getOptionWidgetData( result, null, data[ 3 ][ i ] )
+                       ) );
+               } );
+
+               mw.track( 'mw.widgets.SearchInputWidget', {
+                       action: 'impression-results',
+                       numberOfResults: items.length,
+                       resultSetType: mw.searchSuggest.type
+               } );
+
+               return items;
+       };
+
+       /**
+        * @inheritdoc mw.widgets.TitleWidget
+        *
+        * @param {string} title
+        * @param {Object} data
+        * @param {string} url The Url to the result
+        */
+       mw.widgets.SearchInputWidget.prototype.getOptionWidgetData = function ( title, data, url ) {
+               // the values used in mw.widgets-TitleWidget doesn't exist here, that's why
+               // the values are hard-coded here
+               return {
+                       data: title,
+                       url: url,
+                       imageUrl: null,
+                       description: null,
+                       missing: false,
+                       redirect: false,
+                       disambiguation: false,
+                       query: this.getQueryValue()
+               };
+       };
+
+}( jQuery, mediaWiki ) );
index abe1228..b805e65 100644 (file)
@@ -23,7 +23,8 @@
         * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
         * @cfg {boolean} [showImages] Show page images
         * @cfg {boolean} [showDescriptions] Show page descriptions
-        * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title
+        * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
+        *  the widget will marks itself red for invalid inputs, including an empty query).
         * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
         */
        mw.widgets.TitleWidget = function MwWidgetsTitleWidget( config ) {
diff --git a/resources/src/mediawiki/mediawiki.viewport.js b/resources/src/mediawiki/mediawiki.viewport.js
new file mode 100644 (file)
index 0000000..aa9dd05
--- /dev/null
@@ -0,0 +1,89 @@
+( function ( mw, $ ) {
+       'use strict';
+
+       /**
+        * Utility library for viewport-related functions
+        *
+        * Notable references:
+        * - https://github.com/tuupola/jquery_lazyload
+        * - https://github.com/luis-almeida/unveil
+        *
+        * @class mw.viewport
+        * @singleton
+        */
+       var viewport = {
+
+               /**
+                * This is a private method pulled inside the module for testing purposes.
+                *
+                * @ignore
+                * @private
+                */
+               makeViewportFromWindow: function () {
+                       var $window = $( window ),
+                               scrollTop = $window.scrollTop(),
+                               scrollLeft = $window.scrollLeft();
+
+                       return {
+                               top: scrollTop,
+                               left: scrollLeft,
+                               right: scrollLeft + $window.width(),
+                               bottom: ( window.innerHeight ? window.innerHeight : $window.height() ) + scrollTop
+                       };
+               },
+
+               /**
+                * Check if any part of a given element is in a given viewport
+                *
+                * @method
+                * @param {HTMLElement} el Element that's being tested
+                * @param {Object} [rectangle] Viewport to test against; structured as such:
+                *
+                *      var rectangle = {
+                *              top: topEdge,
+                *              left: leftEdge,
+                *              right: rightEdge,
+                *              bottom: bottomEdge
+                *      }
+                *      Defaults to viewport made from `window`.
+                *
+                * @return {boolean}
+                */
+               isElementInViewport: function ( el, rectangle ) {
+                       var elRect = el.getBoundingClientRect(),
+                               viewport = rectangle || this.makeViewportFromWindow();
+
+                       return (
+                               ( viewport.bottom >= elRect.top ) &&
+                               ( viewport.right >= elRect.left ) &&
+                               ( viewport.top <= elRect.top + elRect.height ) &&
+                               ( viewport.left <= elRect.left + elRect.width )
+                       );
+               },
+
+               /**
+                * Check if an element is a given threshold away in any direction from a given viewport
+                *
+                * @method
+                * @param {HTMLElement} el Element that's being tested
+                * @param {number} [threshold] Pixel distance considered "close". Must be a positive number.
+                *  Defaults to 50.
+                * @param {Object} [rectangle] Viewport to test against.
+                *  Defaults to viewport made from `window`.
+                * @return {boolean}
+                */
+               isElementCloseToViewport: function ( el, threshold, rectangle ) {
+                       var viewport = rectangle ? $.extend( {}, rectangle ) : this.makeViewportFromWindow();
+                       threshold = threshold || 50 ;
+
+                       viewport.top -= threshold;
+                       viewport.left -= threshold;
+                       viewport.right += threshold;
+                       viewport.bottom += threshold;
+                       return this.isElementInViewport( el, viewport );
+               }
+
+       };
+
+       mw.viewport = viewport;
+}( mediaWiki, jQuery ) );
index 7e6f68c..b3cb89a 100644 (file)
@@ -24,6 +24,8 @@
  * @ingroup Testing
  */
 
+define( 'MW_PARSER_TEST', true );
+
 $options = [ 'quick', 'color', 'quiet', 'help', 'show-output',
        'record', 'run-disabled', 'run-parsoid' ];
 $optionsWithArgs = [ 'regex', 'filter', 'seed', 'setversion', 'file' ];
index a2dead6..310268f 100644 (file)
@@ -80,6 +80,7 @@ return [
                        'tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js',
                        'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js',
                        'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js',
                        'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.messages.test.js',
@@ -130,6 +131,7 @@ return [
                        'mediawiki.template.mustache',
                        'mediawiki.template',
                        'mediawiki.util',
+                       'mediawiki.viewport',
                        'mediawiki.special.recentchanges',
                        'mediawiki.language',
                        'mediawiki.cldr',
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.viewport.test.js
new file mode 100644 (file)
index 0000000..61391d8
--- /dev/null
@@ -0,0 +1,89 @@
+( function ( mw, $ ) {
+
+       // Simulate square element with 20px long edges placed at (20, 20) on the page
+       var
+               DEFAULT_VIEWPORT = {
+                       top: 0,
+                       left: 0,
+                       right: 100,
+                       bottom: 100
+               };
+
+       QUnit.module( 'mediawiki.viewport', QUnit.newMwEnvironment( {
+               setup: function () {
+                       this.el = $( '<div />' )
+                               .appendTo( '#qunit-fixture' )
+                               .width( 20 )
+                               .height( 20 )
+                               .offset( {
+                                       top: 20,
+                                       left: 20
+                               } )
+                               .get( 0 );
+                       this.sandbox.stub( mw.viewport, 'makeViewportFromWindow' )
+                               .returns( DEFAULT_VIEWPORT );
+               }
+       } ) );
+
+       QUnit.test( 'isElementInViewport', 6, function ( assert ) {
+               var viewport = $.extend( {}, DEFAULT_VIEWPORT );
+               assert.ok( mw.viewport.isElementInViewport( this.el, viewport ),
+                       'It should return true when the element is fully enclosed in the viewport' );
+
+               viewport.right = 20;
+               viewport.bottom = 20;
+               assert.ok( mw.viewport.isElementInViewport( this.el, viewport ),
+                       'It should return true when only the top-left of the element is within the viewport' );
+
+               viewport.top = 40;
+               viewport.left = 40;
+               viewport.right = 50;
+               viewport.bottom = 50;
+               assert.ok( mw.viewport.isElementInViewport( this.el, viewport ),
+                       'It should return true when only the bottom-right is within the viewport' );
+
+               viewport.top = 30;
+               viewport.left = 30;
+               viewport.right = 35;
+               viewport.bottom = 35;
+               assert.ok( mw.viewport.isElementInViewport( this.el, viewport ),
+                       'It should return true when the element encapsulates the viewport' );
+
+               viewport.top = 0;
+               viewport.left = 0;
+               viewport.right = 19;
+               viewport.bottom = 19;
+               assert.notOk( mw.viewport.isElementInViewport( this.el, viewport ),
+                       'It should return false when the element is not within the viewport' );
+
+               assert.ok( mw.viewport.isElementInViewport( this.el ),
+                       'It should default to the window object if no viewport is given' );
+       } );
+
+       QUnit.test( 'isElementCloseToViewport', 3, function ( assert ) {
+               var
+                       viewport = {
+                               top: 90,
+                               left: 90,
+                               right: 100,
+                               bottom: 100
+                       },
+                       distantElement = $( '<div />' )
+                               .appendTo( '#qunit-fixture' )
+                               .width( 20 )
+                               .height( 20 )
+                               .offset( {
+                                       top: 220,
+                                       left: 20
+                               } )
+                               .get( 0 );
+
+               assert.ok( mw.viewport.isElementCloseToViewport( this.el, 60, viewport ),
+                       'It should return true when the element is within the given threshold away' );
+               assert.notOk( mw.viewport.isElementCloseToViewport( this.el, 20, viewport ),
+                       'It should return false when the element is further than the given threshold away' );
+               assert.notOk( mw.viewport.isElementCloseToViewport( distantElement ),
+                       'It should default to a threshold of 50px and the window\'s viewport' );
+       } );
+
+}( mediaWiki, jQuery ) );