'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',
$userlink = '';
}
+ $flags = [];
if ( $rev->getParentId() === 0 ) {
- $nflag = ChangesList::flag( 'newpage' );
- } else {
- $nflag = '';
+ $flags[] = ChangesList::flag( 'newpage' );
}
if ( $rev->isMinor() ) {
- $mflag = ChangesList::flag( 'minor' );
- } else {
- $mflag = '';
+ $flags[] = ChangesList::flag( 'minor' );
}
$del = Linker::getRevDeleteLink( $user, $rev, $page );
$diffHistLinks = $this->msg( 'parentheses' )
->rawParams( $difftext . $this->messages['pipe-separator'] . $histlink )
->escaped();
- $ret = "{$del}{$d} {$diffHistLinks}{$chardiff}{$nflag}{$mflag} ";
- $ret .= "{$link}{$userlink} {$comment} {$topmarktext}";
-
- # Denote if username is redacted for this edit
- if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
- $ret .= " <strong>" .
- $this->msg( 'rev-deleted-user-contribs' )->escaped() .
- "</strong>";
- }
# Tags, if any.
list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow(
$this->getContext()
);
$classes = array_merge( $classes, $newClasses );
- $ret .= " $tagSummary";
+
+ $templateParams = [
+ 'articleLink' => $link,
+ 'charDifference' => $chardiff,
+ 'classes' => $classes,
+ 'diffHistLinks' => $diffHistLinks,
+ 'flags' => $flags,
+ 'logText' => $comment,
+ 'revDeleteLink' => $del,
+ 'tagSummary' => $tagSummary,
+ 'timestamp' => $d,
+ 'topmarktext' => $topmarktext,
+ 'userlink' => $userlink,
+ ];
+
+ # Denote if username is redacted for this edit
+ if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ $templateParams['rev-deleted-user-contribs'] =
+ $this->msg( 'rev-deleted-user-contribs' )->escaped();
+ }
+
+ $templateParser = new TemplateParser();
+ $ret = $templateParser->processTemplate(
+ 'SpecialContributionsLine',
+ $templateParams
+ );
}
// Let extensions add data
- Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes ] );
+ Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$templateParams['classes'] ] );
- if ( $classes === [] && $ret === '' ) {
+ // TODO: Handle exceptions in the catch block above. Do any extensions rely on
+ // receiving empty rows?
+
+ if ( $templateParams['classes'] === [] && $ret === '' ) {
wfDebug( "Dropping Special:Contribution row that could not be formatted\n" );
- $ret = "<!-- Could not format Special:Contribution row. -->\n";
- } else {
- $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n";
+ return "<!-- Could not format Special:Contribution row. -->\n";
}
- return $ret;
+ // FIXME: The signature of the ContributionsLineEnding hook makes it
+ // very awkward to move this LI wrapper into the template.
+ return Html::rawElement( 'li', [ 'class' => $templateParams['classes'] ], $ret ) . "\n";
}
/**
--- /dev/null
+{{{ del }}}{{{ timestamp }}}
+{{{ diffHistLinks }}}{{{ charDifference }}}{{# flags }}{{{ . }}}{{/ flags }}
+{{{ articleLink }}}{{{ userlink }}}
+{{{ logText }}}
+{{{ topmarktext }}}{{# rev-deleted-user-contribs }} <strong>{{{ . }}}</strong>{{/ rev-deleted-user-contribs }}
+{{{ tagSummary }}}
--- /dev/null
+<?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 );
+ }
+}
"mw.util",
"mw.plugin.*",
"mw.cookie",
- "mw.experiments"
+ "mw.experiments",
+ "mw.viewport"
]
},
{
'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',
],
'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' => [
-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;
+}
--- /dev/null
+.mw-widget-searchInputWidget {
+ display: inline-block;
+}
--- /dev/null
+/*!
+ * 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 ) );
* @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 ) {
--- /dev/null
+( 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 ) );
'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',
'mediawiki.template.mustache',
'mediawiki.template',
'mediawiki.util',
+ 'mediawiki.viewport',
'mediawiki.special.recentchanges',
'mediawiki.language',
'mediawiki.cldr',
--- /dev/null
+( 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 ) );