jenkins-bot has submitted this change and it was merged.

Change subject: Story 1099: Better search UX
......................................................................


Story 1099: Better search UX

Bonus fixes:
* search in progress indication
* aborting old requests if search query changed
* double check if response is the response to the latest query
* clean up OverlayNew CSS

Change-Id: I842ff097814f66cb7cca4949b5ccadff834029c4
---
M MobileFrontend.i18n.php
M includes/Resources.php
M javascripts/common/OverlayNew.js
M javascripts/common/history-alpha.js
M javascripts/modules/lazyload.js
A javascripts/modules/searchNew/SearchApi.js
A javascripts/modules/searchNew/SearchOverlay.js
M javascripts/modules/searchNew/search.js
M javascripts/modules/uploadsNew/PhotoUploadOverlay.js
M less/common/OverlayNew.less
A less/common/images/clear.png
M less/common/pagelist.less
M less/common/ui.less
A less/modules/images/searchContent.png
A less/modules/searchNew/SearchOverlay.less
M less/modules/watchstar.less
M less/variables.less
M templates/OverlayNew.html
A templates/modules/searchNew/SearchOverlay.html
A tests/javascripts/modules/searchNew/test_SearchApi.js
20 files changed, 391 insertions(+), 182 deletions(-)

Approvals:
  Jdlrobson: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/MobileFrontend.i18n.php b/MobileFrontend.i18n.php
index 5eac62a..c7bd670 100644
--- a/MobileFrontend.i18n.php
+++ b/MobileFrontend.i18n.php
@@ -113,6 +113,10 @@
        'mobile-frontend-search-help' => 'Type search term above and matching 
page titles will appear here.',
        'mobile-frontend-dismiss-notification' => 'dismiss this notification',
        'mobile-frontend-clear-search' => 'Clear',
+       'mobile-frontend-search-content' => 'Search within articles',
+       'mobile-frontend-search-no-results' => "No article with this title.",
+       'mobile-frontend-search-content-no-results' => "'''Search within 
articles''' to see if this phrase appears anywhere.",
+
        'mobile-frontend-privacy-link-text' => 'Privacy',
        'mobile-frontend-footer-sitename' => '{{SITENAME}}',
        'mobile-frontend-footer-license' => 'Text is available under 
[//en.m.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License?useformat=mobile
 CC BY-SA 3.0]; additional terms may apply.',
@@ -599,6 +603,9 @@
        'mobile-frontend-dismiss-notification' => 'Phrase used to dismiss the 
top banner notification',
        'mobile-frontend-clear-search' => 'Tooltip for clear button that 
appears when you type into search box.
 {{Identical|Clear}}',
+       'mobile-frontend-search-content' => 'Caption for a button performing 
full text search of a given search query.',
+       'mobile-frontend-search-no-results' => 'Message informing user that no 
articles were found for a given query.',
+       'mobile-frontend-search-content-no-results' => 'Caption for a button 
performing full text search of a given search query when no pages with matching 
titles were found.',
        'mobile-frontend-privacy-link-text' => 'Custom version of "Privacy 
policy" link text for mobile footer, intended to be as brief as possible to 
take up as little screen real estate as possible.
 {{Identical|Privacy}}',
        'mobile-frontend-footer-sitename' => 'Name of site',
diff --git a/includes/Resources.php b/includes/Resources.php
index 425bc2c..7df31e5 100644
--- a/includes/Resources.php
+++ b/includes/Resources.php
@@ -474,19 +474,23 @@
                        'mobile.beta'
                ),
                'styles' => array(
-                       'less/modules/search.less',
+                       'less/modules/searchNew/SearchOverlay.less',
                ),
                'scripts' => array(
+                       'javascripts/modules/searchNew/SearchApi.js',
+                       'javascripts/modules/searchNew/SearchOverlay.js',
                        'javascripts/modules/searchNew/search.js',
                        'javascripts/modules/searchNew/pageImages.js',
                ),
                'templates' => array(
-                       'overlays/search/search',
+                       'modules/searchNew/SearchOverlay',
                ),
                'messages' => array(
                        // for search.js
-                       'mobile-frontend-search-help',
-                       'mobile-frontend-search-noresults',
+                       'mobile-frontend-clear-search',
+                       'mobile-frontend-search-content',
+                       'mobile-frontend-search-no-results',
+                       'mobile-frontend-search-content-no-results' => array( 
'parse' ),
                ),
        ),
 
diff --git a/javascripts/common/OverlayNew.js b/javascripts/common/OverlayNew.js
index 2fac641..492fbee 100644
--- a/javascripts/common/OverlayNew.js
+++ b/javascripts/common/OverlayNew.js
@@ -4,7 +4,10 @@
        var Overlay = M.require( 'Overlay' ),
        OverlayNew = Overlay.extend( {
                className: 'overlay',
-               template: M.template.get( 'OverlayNew' )
+               template: M.template.get( 'OverlayNew' ),
+               defaults: {
+                       headerButtonsListClassName: 'v-border bottom-border'
+               }
        } );
 
        M.define( 'OverlayNew', OverlayNew );
diff --git a/javascripts/common/history-alpha.js 
b/javascripts/common/history-alpha.js
index 43ac99f..a7672ea 100644
--- a/javascripts/common/history-alpha.js
+++ b/javascripts/common/history-alpha.js
@@ -45,9 +45,11 @@
                 * Trigger navigation to a page via javascript
                 *
                 * @param {String} pageTitle String representing the title of a 
page that should be loaded in the browser
+                * @param {Boolean} replaceState Replace current state instead 
of pushing
+                * a new one.
                 */
-               function navigateToPage( title ) {
-                       History.pushState( null, title, mw.util.getUrl( title ) 
);
+               function navigateToPage( title, replaceState ) {
+                       History[replaceState ? 'replaceState' : 'pushState']( 
null, title, mw.util.getUrl( title ) );
                }
 
                /**
@@ -56,11 +58,13 @@
                 *
                 * @param {jQuery.object} $container A container to hijack links
                 * @param {Boolean} useFuzzyHijacking When set any link any 
links missing a data-title attribute are hijacked if they might be links
+                * @param {Boolean} replaceState Replace current state instead 
of pushing
+                * a new one.
                 */
-               function hijackLinks( $container, useFuzzyHijacking ) {
+               function hijackLinks( $container, useFuzzyHijacking, 
replaceState ) {
                        function lazyLoad( ev ) {
                                ev.preventDefault();
-                               navigateToPage( $( this ).data( 'title' ) );
+                               navigateToPage( $( this ).data( 'title' ), 
replaceState );
                        }
 
                        function hijackLink( $a ) {
diff --git a/javascripts/modules/lazyload.js b/javascripts/modules/lazyload.js
index 2ce1c8b..a6507dc 100644
--- a/javascripts/modules/lazyload.js
+++ b/javascripts/modules/lazyload.js
@@ -23,8 +23,8 @@
                } );
 
                M.on( 'search-results', function( overlay ) {
-                       var $results = overlay.$( 'ul' );
-                       history.hijackLinks( $results );
+                       var $results = overlay.$( '.results' );
+                       history.hijackLinks( $results, false, true );
                        $results.find( 'a' ).on( 'click', function() {
                                overlay.hide();
                        } );
diff --git a/javascripts/modules/searchNew/SearchApi.js 
b/javascripts/modules/searchNew/SearchApi.js
new file mode 100644
index 0000000..a31dca9
--- /dev/null
+++ b/javascripts/modules/searchNew/SearchApi.js
@@ -0,0 +1,57 @@
+( function( M, $ ) {
+
+       var Api = M.require( 'api' ).Api, SearchApi;
+
+       /**
+        * Escapes regular expression wildcards (metacharacters) by adding a \\ 
prefix
+        * @param {String} str a string
+        * @return {String} a regular expression that can be used to search for 
that str
+        */
+       function createSearchRegEx( str ) {
+               str = str.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&' );
+               return new RegExp( '^(' + str + ')' , 'ig' );
+       }
+
+       /**
+        * Takes a label potentially beginning with term
+        * and highlights term if it is present with strong
+        *
+        * @param {String} label a piece of text
+        * @param {String} term a string to search for from the start
+        * @return {String} safe html string with matched terms encapsulated in 
strong tags
+        */
+       function highlightSearchTerm( label, term ) {
+               label = $( '<span>' ).text( label ).html();
+               term = $( '<span>' ).text( term ).html();
+
+               return label.replace( createSearchRegEx( term 
),'<strong>$1</strong>' );
+       }
+
+       SearchApi = Api.extend( {
+               search: function( query ) {
+                       return this.get( {
+                               search: query,
+                               action: 'opensearch',
+                               namespace: 0,
+                               limit: 15
+                       } ).then( function( data ) {
+                               return {
+                                       query: data[0],
+                                       results: $.map( data[1], function( 
title ) {
+                                               return {
+                                                       heading: 
highlightSearchTerm( title, query ),
+                                                       title: title,
+                                                       url: mw.util.getUrl( 
title )
+                                               };
+                                       } )
+                               };
+                       } );
+               }
+       } );
+
+       // for tests
+       SearchApi._highlightSearchTerm = highlightSearchTerm;
+
+       M.define( 'modules/searchNew/SearchApi', SearchApi );
+
+}( mw.mobileFrontend, jQuery ));
diff --git a/javascripts/modules/searchNew/SearchOverlay.js 
b/javascripts/modules/searchNew/SearchOverlay.js
new file mode 100644
index 0000000..be4f3d8
--- /dev/null
+++ b/javascripts/modules/searchNew/SearchOverlay.js
@@ -0,0 +1,114 @@
+( function( M, $ ) {
+
+       var
+               OverlayNew = M.require( 'OverlayNew' ),
+               SearchApi = M.require( 'modules/searchNew/SearchApi' ),
+               KEY_ENTER = 13,
+               SEARCH_DELAY = 500,
+               SearchOverlay;
+
+       SearchOverlay = OverlayNew.extend( {
+               className: 'overlay search-overlay',
+               template: M.template.get( 'modules/searchNew/SearchOverlay' ),
+               defaults: {
+                       placeholderMsg: $( '#searchInput' ).attr( 'placeholder' 
),
+                       clearMsg: mw.msg( 'mobile-frontend-clear-search' ),
+                       searchContentMsg: mw.msg( 
'mobile-frontend-search-content' ),
+                       noResultsMsg: mw.msg( 
'mobile-frontend-search-no-results' ),
+                       searchContentNoResultsMsg: mw.msg( 
'mobile-frontend-search-content-no-results' ),
+                       action: mw.config.get( 'wgScript' )
+               },
+               closeOnBack: true,
+
+               initialize: function( options ) {
+                       this._super( options );
+                       this.api = new SearchApi();
+               },
+
+               postRender: function( options ) {
+                       var
+                               self = this,
+                               $clear = this.$( '.clear' ),
+                               $form = this.$( 'form' );
+
+                       this._super( options );
+
+                       this.$input = this.$( 'input' ).on( 'input', function( 
ev ) {
+                               if ( ev.which === KEY_ENTER ) {
+                                       $form.submit();
+                               } else {
+                                       self.performSearch();
+                               }
+
+                               $clear.toggle( self.$input.val() !== '' );
+                       } );
+
+                       $clear.hide().on( M.tapEvent( 'click' ), function() {
+                               self.$input.val( '' ).focus();
+                               self.performSearch();
+                               $clear.hide();
+                       } );
+
+                       this.$searchContent = this.$( '.search-content' ).
+                               hide().
+                               // can't use $.proxy because it would pass ev 
to submit() which would
+                               // be treated as alternative form data
+                               on( M.tapEvent( 'click' ), function() {
+                                       $form.submit();
+                               } );
+
+                       // tapping on background only should hide the overlay
+                       this.$el.on( M.tapEvent( 'click' ), function() {
+                               window.history.back();
+                       } );
+                       this.$( '> div' ).on( M.tapEvent( 'click' ), function( 
ev ) {
+                               ev.stopPropagation();
+                       } );
+               },
+
+               show: function() {
+                       this._super();
+                       this.$input.focus();
+               },
+
+               performSearch: function() {
+                       var
+                               self = this,
+                               query = this.$input.val(),
+                               $results = this.$( '.results' );
+
+                       this.api.abort();
+                       clearTimeout( this.timer );
+                       self.$searchContent.hide();
+                       $results.empty();
+
+                       if ( query.length ) {
+                               $results.addClass( 'loading' );
+
+                               this.timer = setTimeout( function() {
+                                       self.api.search( query ).done( 
function( data ) {
+                                               // check if we're getting the 
rights response in case of out of
+                                               // order responses (need to get 
the current value of the input)
+                                               if ( data.query === 
self.$input.val() ) {
+                                                       self.$searchContent.
+                                                               show().
+                                                               find( 'p' ).
+                                                               hide().
+                                                               filter( 
data.results.length ? '.with-results' : '.without-results' ).
+                                                               show();
+                                                       $results.
+                                                               removeClass( 
'loading' ).
+                                                               html( 
M.template.get( 'articleList' ).render( { pages: data.results } ) );
+                                                       M.emit( 
'search-results', self, data.results );
+                                               }
+                                       } );
+                               }, SEARCH_DELAY );
+                       } else {
+                               $results.removeClass( 'loading' );
+                       }
+               }
+       } );
+
+       M.define( 'modules/searchNew/SearchOverlay', SearchOverlay );
+
+}( mw.mobileFrontend, jQuery ));
diff --git a/javascripts/modules/searchNew/search.js 
b/javascripts/modules/searchNew/search.js
index 30d0a95..c8c5865 100644
--- a/javascripts/modules/searchNew/search.js
+++ b/javascripts/modules/searchNew/search.js
@@ -1,126 +1,33 @@
 ( function( M, $ ) {
 
-var Overlay = M.require( 'Overlay' ), SearchOverlay,
-       api = M.require( 'api' ),
-       searchOverlay;
+       var SearchOverlay = M.require( 'modules/searchNew/SearchOverlay' );
 
-/**
- * Escapes regular expression wildcards (metacharacters) by adding a \\ prefix
- * @param {String} str a string
- * @return {String} a regular expression that can be used to search for that 
str
- */
-function createSearchRegEx( str ) {
-       str = str.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&' );
-       return new RegExp( '^(' + str + ')' , 'ig' );
-}
-
-/**
- * Takes a label potentially beginning with term
- * and highlights term if it is present with strong
- *
- * @param {String} label a piece of text
- * @param {String} term a string to search for from the start
- * @return {String} safe html string with matched terms encapsulated in strong 
tags
- */
-function highlightSearchTerm( label, term ) {
-       label = $( '<span>' ).text( label ).html();
-       term = $( '<span>' ).text( term ).html();
-
-       return label.replace( createSearchRegEx( term ),'<strong>$1</strong>' );
-}
-
-SearchOverlay = Overlay.extend( {
-       template: M.template.get( 'overlays/search/search' ),
-       defaults: {
-               explanation: mw.msg( 'mobile-frontend-search-help' ),
-               noresults: mw.msg( 'mobile-frontend-search-noresults' ),
-               action: mw.config.get( 'wgScript' )
-       },
-       className: 'mw-mf-overlay',
-       postRender: function( options ) {
-               var self = this;
-
-               this._super( options );
-
-               this.data = this.defaults;
-
-               this.$( 'input' ).on( 'keyup', function( ev ) {
-                       if ( ev.keyCode && ev.keyCode === 13 ) {
-                               $( this ).parents( 'form' ).submit();
-                       } else {
-                               self.performSearch();
-                       }
-               } );
-               this.results = [];
-               this.$( '.clear' ).on( 'click', function() {
-                       self.$( 'input' ).val( '' ).focus();
-               } );
-       },
-       /**
-        * Renders a list of results
-        *
-        * @param {Array} results list of search results with label and url 
properties set
-        */
-       writeResults: function( results ) {
-               var $content = this.$( 'div.suggestions-results' ),
-                       data = {
-                               pages: results || []
-                       };
-               if ( results.length === 0 ) {
-                       data.error = {
-                               guidance: mw.msg( 
'mobile-frontend-search-noresults' )
-                       };
-               }
-               $content.html( M.template.get( 'articleList' ).render( data ) );
-               M.emit( 'search-results', this, results );
-       },
-
-       performSearch: function() {
-               var self = this,
-                       term = this.$( 'input' ).val();
-
-               function completeSearch( data ) {
-                       data = $.map( data[ 1 ], function( item ) {
-                               return {
-                                       heading: highlightSearchTerm( item, 
term ),
-                                       title: item,
-                                       url: mw.util.wikiGetlink( item )
-                               };
-                       } );
-
-                       self.writeResults( data );
-
-                       self.$( 'input' ).removeClass( 'searching' );
-               }
-
-               clearTimeout( this.timer );
-               if ( term.length > 0 ) {
-                       this.timer = setTimeout( function () {
-                               self.$( 'input' ).addClass( 'searching' );
-                               api.get( {
-                                       search: term,
-                                       action: 'opensearch',
-                                       namespace: 0,
-                                       limit: 15
-                               } ).done( completeSearch );
-                       }, 500 );
-               }
-       },
-       showAndFocus: function() {
-               this.show();
-               this.$( 'input' ).focus().select();
-       }
-} );
-
-searchOverlay = new SearchOverlay();
-
-function init() {
        // FIXME change when micro.tap.js in stable
        // don't use focus event 
(https://bugzilla.wikimedia.org/show_bug.cgi?id=47499)
        $( '#searchInput' ).on( M.tapEvent( 'touchend mouseup' ), function() {
-               searchOverlay.showAndFocus();
+               M.router.navigate( 'search' );
        } );
-}
-init();
+
+       // FIXME: ugly hack that removes search from browser history when 
navigating
+       // to search results (we can't rely on History API yet)
+       // alpha does it differently in lazyload.js
+       if ( mw.config.get( 'wgMFMode' ) !== 'alpha' ) {
+               M.on( 'search-results', function( overlay ) {
+                       overlay.$( '.results a' ).on( 'click', function( ev ) {
+                               var href = $( this ).attr( 'href' );
+                               ev.preventDefault();
+                               window.history.back();
+
+                               // give browser a tick to update its history 
and redirect
+                               setTimeout( function() {
+                                       window.location.href = href;
+                               }, 0 );
+                       } );
+               } );
+       }
+
+       M.router.route( /^search$/, function() {
+               new SearchOverlay().show();
+       } );
 
 }( mw.mobileFrontend, jQuery ));
diff --git a/javascripts/modules/uploadsNew/PhotoUploadOverlay.js 
b/javascripts/modules/uploadsNew/PhotoUploadOverlay.js
index 6e0f1b2..259a3fa 100644
--- a/javascripts/modules/uploadsNew/PhotoUploadOverlay.js
+++ b/javascripts/modules/uploadsNew/PhotoUploadOverlay.js
@@ -12,6 +12,7 @@
                        help: mw.msg( 'mobile-frontend-photo-ownership-help' ),
                        ownerStatement: ownershipMessage,
                        heading: mw.msg( 
'mobile-frontend-image-heading-describe' ),
+                       headerButtonsListClassName: '',
                        headerButtons: [
                                { className: 'submit icon', msg: mw.msg( 
'mobile-frontend-photo-submit' ) }
                        ]
diff --git a/less/common/OverlayNew.less b/less/common/OverlayNew.less
index 8aef893..0c924a1 100644
--- a/less/common/OverlayNew.less
+++ b/less/common/OverlayNew.less
@@ -1,13 +1,14 @@
 // FIXME: merge with overlays.less when OverlayNew gets to stable
 @import "../mixins.less";
 
-@headerMargin: .4em;
-@headingMarginV: 1em;
-@headingFontSize: 1em;
-@buttonSize: @headingFontSize + @headingMarginV * 2 + @headerMargin;
-
 .overlay-enabled {
-       .overlay {
+       #mw-mf-page-center {
+               overflow: hidden;
+               // FIXME: override old overlay styles, remove when old overlays 
gone
+               display: block;
+       }
+
+       .overlay, #mw-mf-page-center {
                // use height instead of bottom 0 so that overlay expands when 
there's
                // more content, don't use min-height because of
                // 
http://stackoverflow.com/questions/3808512/the-inherited-height-of-a-child-div-from-a-parent-with-a-min-height-attribute
@@ -24,54 +25,69 @@
        z-index: 4;
 
        .overlay-header {
-               padding: @headerMargin @headerMargin 0;
                display: table;
                width: 100%;
                .box-sizing(border-box);
 
-               > ul, h2 {
+               > ul, > div {
                        display: table-cell;
-                       vertical-align: bottom;
+                       vertical-align: middle;
+               }
+
+               .bottom-border {
                        border-bottom: 1px solid #cacaca;
                }
 
-               button, h2 {
-                       line-height: 1;
-                       padding: @headingMarginV .6em;
+               > div {
+                       padding: @overlayHeadingMarginV @overlayHeadingMarginH;
+               }
+
+               h2 {
                        // FIXME: should not be necessary, scope other h2s to 
.content
-                       font-size: @headingFontSize;
+                       font-size: @overlayHeadingFontSize;
                }
 
                > ul, button {
-                       width: @buttonSize - @headerMargin;
+                       width: @overlayButtonSize - @overlayHeaderMargin;
                        white-space: nowrap;
                }
 
                button {
-                       width: @buttonSize;
+                       padding: @overlayHeadingMarginV @overlayHeadingMarginH;
+                       width: @overlayButtonSize;
                        border-radius: 0;
-                       min-height: @buttonSize;
-                       font-weight: bold;
-                       margin-top: -@headerMargin;
+                       min-height: @overlayButtonSize;
 
                        &.cancel {
                                color: #d11d13;
                        }
                }
 
-               > ul:first-child li:first-child {
+               .v-border:first-child {
                        border-right: 1px solid #cacaca;
-
-                       button {
-                               margin-left: -@headerMargin;
-                       }
                }
 
-               > ul:last-child li:last-child {
+               .v-border:last-child li:last-child {
                        border-left: 1px solid #cacaca;
+               }
+
+               &.with-margins {
+                       padding: @overlayHeaderMargin @overlayHeaderMargin 0;
+
+                       > div {
+                               padding: (@overlayHeadingMarginV - 
@overlayHeaderMargin) @overlayHeadingMarginH;
+                       }
 
                        button {
-                               margin-right: -@headerMargin;
+                               margin-top: -@overlayHeaderMargin;
+                       }
+
+                       > ul:first-child li:first-child button {
+                               margin-left: -@overlayHeaderMargin;
+                       }
+
+                       > ul:last-child li:last-child button {
+                               margin-right: -@overlayHeaderMargin;
                        }
                }
 
@@ -89,10 +105,11 @@
                                background-image: url(images/cancel.png);
                        }
 
+                       &.clear {
+                               background-image: url(images/clear.png);
+                       }
+
                        &.submit {
-                               // following 2 rules to hide the border
-                               margin-left: -1px;
-                               margin-bottom: -1px;
                                background-image: url(images/check.png);
                                background-color: #00af8b;
                        }
diff --git a/less/common/images/clear.png b/less/common/images/clear.png
new file mode 100644
index 0000000..185b722
--- /dev/null
+++ b/less/common/images/clear.png
Binary files differ
diff --git a/less/common/pagelist.less b/less/common/pagelist.less
index 1df8b50..5bee0e4 100644
--- a/less/common/pagelist.less
+++ b/less/common/pagelist.less
@@ -26,6 +26,8 @@
 
 .page-list {
        cursor: pointer;
+       // needed for rotated watchstars to avoid horizontal scrollbar
+       overflow: hidden;
 
        li {
                color: #666;
@@ -103,6 +105,7 @@
 }
 
 // FIXME: there is no reason for styling in overlays to be different!
+// FIXME: remove when new overlays in stable
 .mw-mf-overlay {
        .page-list {
                font-size: .9em;
@@ -111,32 +114,14 @@
                        padding-left: @searchBarPaddingLeft;
                }
 
-               &.thumbs li {
-                       padding-left: @thumbOverlaySpace;
-               }
-
                h2 {
                        font: inherit;
-               }
-
-               .listThumb {
-                       width: @thumbOverlayWidth;
                }
        }
 
        .actionable {
                li:hover {
                        background-color: #ACD1E9;
-               }
-       }
-}
-
-// search without page images
-// FIXME: remove when page images are in stable
-.stable {
-       .suggestions-results {
-               li {
-                       padding-left: @searchBarPaddingLeft !important;
                }
        }
 }
diff --git a/less/common/ui.less b/less/common/ui.less
index ba4aefc..5bd4045 100644
--- a/less/common/ui.less
+++ b/less/common/ui.less
@@ -109,7 +109,6 @@
 input.search {
        outline: none;
        width: 100%;
-       height: @headerElementHeight;
        // FIXME: unable to check but the important may not be needed for Nokia 
S60;
        background-color: #fff !important; /* remove fennec default background 
also see bug 36490 */
        .box-sizing( border-box );
@@ -121,6 +120,11 @@
        }
 }
 
+// FIXME: why is this needed?
+.stable input.search {
+       height: @headerElementHeight;
+}
+
 .searchSubmit {
        // FIXME: do all buttons in header need to have padding by default?
        padding: 0 !important;
diff --git a/less/modules/images/searchContent.png 
b/less/modules/images/searchContent.png
new file mode 100644
index 0000000..273f6df
--- /dev/null
+++ b/less/modules/images/searchContent.png
Binary files differ
diff --git a/less/modules/searchNew/SearchOverlay.less 
b/less/modules/searchNew/SearchOverlay.less
new file mode 100644
index 0000000..7729f5f
--- /dev/null
+++ b/less/modules/searchNew/SearchOverlay.less
@@ -0,0 +1,42 @@
+@import "../../variables";
+
+.search-overlay {
+       background: rgba(255, 255, 255, .8);
+
+       .overlay-header, .results {
+               // don't use background to preserve .loading properties
+               background-color: #fff;
+       }
+
+       input {
+               border: none;
+               padding: 0;
+       }
+
+       .search-content {
+               cursor: pointer;
+
+               button {
+                       background-image: url(../images/searchContent.png);
+               }
+       }
+
+       .results {
+               box-shadow: 0 1px 5px 0 rgba(117, 117, 117, .5);
+
+               li {
+                       padding-left: @overlayButtonSize + 
@overlayHeadingMarginH;
+               }
+
+               .listThumb {
+                       // for @overlayButtonSize to work properly
+                       // FIXME: why is this needed?
+                       font-size: 1em;
+                       width: @overlayButtonSize;
+               }
+
+               h2 {
+                       font: inherit;
+               }
+       }
+}
diff --git a/less/modules/watchstar.less b/less/modules/watchstar.less
index a701a56..0259bf5 100644
--- a/less/modules/watchstar.less
+++ b/less/modules/watchstar.less
@@ -1,16 +1,16 @@
 @import "../mixins.less";
 
-@watchThisArticleSize: 22px;
+@watchThisArticleSize: 48px;
 
 // watchlist and search results
 .page-list .watch-this-article {
        position: absolute;
-       right: 8px;
+       right: 0;
        top: 50%;
        margin-top: -(@watchThisArticleSize / 2);
        width: @watchThisArticleSize;
        height: @watchThisArticleSize;
-       .background-size( auto, @watchThisArticleSize );
+       .background-size( auto, (@watchThisArticleSize / 2) );
 }
 
 .animations {
diff --git a/less/variables.less b/less/variables.less
index b96f8ba..60b558c 100644
--- a/less/variables.less
+++ b/less/variables.less
@@ -7,6 +7,13 @@
 @headerBorderBottom: #e3e3e3;
 @menuWidth: 16em;
 
+// new overlays
+@overlayHeaderMargin: .4em;
+@overlayHeadingMarginV: 1em;
+@overlayHeadingMarginH: .6em;
+@overlayHeadingFontSize: 1em;
+@overlayButtonSize: @overlayHeadingFontSize + @overlayHeadingMarginV * 2 + 
@overlayHeaderMargin;
+
 @grayDark: #252525;
 @grayMediumDark: #565656;
 @grayMedium: #777;
diff --git a/templates/OverlayNew.html b/templates/OverlayNew.html
index 58ecea0..87a61d2 100644
--- a/templates/OverlayNew.html
+++ b/templates/OverlayNew.html
@@ -1,9 +1,11 @@
-<div class="overlay-header">
-       <ul>
+<div class="overlay-header with-margins">
+       <ul class="v-border bottom-border">
                <li><button class="cancel icon">{{closeMsg}}</button></li>
        </ul>
-       <h2>{{{heading}}}</h2>
-       <ul>
+       <div class="bottom-border">
+               <h2>{{{heading}}}</h2>
+       </div>
+       <ul class="{{headerButtonsListClassName}}">
                {{#headerButtons}}
                <li><button class="{{className}}">{{msg}}</button></li>
                {{/headerButtons}}
diff --git a/templates/modules/searchNew/SearchOverlay.html 
b/templates/modules/searchNew/SearchOverlay.html
new file mode 100644
index 0000000..25a9081
--- /dev/null
+++ b/templates/modules/searchNew/SearchOverlay.html
@@ -0,0 +1,24 @@
+<div class="overlay-header with-margins">
+       <ul class="v-border bottom-border">
+               <li><button class="cancel icon">{{closeMsg}}</button></li>
+       </ul>
+       <div class="bottom-border">
+               <form method="get" action="{{action}}">
+                       <input class="search" type="search" name="search" 
autocomplete="off" placeholder="{{placeholderMsg}}">
+               </form>
+       </div>
+       <ul class="bottom-border">
+               <li><button class="clear icon">{{clearMsg}}</button></li>
+       </ul>
+</div>
+<div class="search-content overlay-header">
+       <ul class="v-border bottom-border">
+               <li><button class="icon">{{searchContentMsg}}</button></li>
+       </ul>
+       <div class="caption bottom-border">
+               <p class="with-results">{{searchContentMsg}}</p>
+               <p class="without-results">{{noResultsMsg}}</p>
+               <p class="without-results">{{{searchContentNoResultsMsg}}}</p>
+       </div>
+</div>
+<div class="results spinner"></div>
diff --git a/tests/javascripts/modules/searchNew/test_SearchApi.js 
b/tests/javascripts/modules/searchNew/test_SearchApi.js
new file mode 100644
index 0000000..7cc679e
--- /dev/null
+++ b/tests/javascripts/modules/searchNew/test_SearchApi.js
@@ -0,0 +1,31 @@
+( function ( $, M ) {
+
+       var SearchApi = M.require( 'modules/searchNew/SearchApi' );
+
+       QUnit.module( 'MobileFrontend: SearchApi' );
+
+       QUnit.test( '._highlightSearchTerm', 14, function( assert ) {
+               var data = [
+                       [ 'Hello World', 'Hel', '<strong>Hel</strong>lo World' 
],
+                       [ 'Hello kitty', 'el', 'Hello kitty' ], // not at start
+                       [ 'Hello worl', 'hel', '<strong>Hel</strong>lo worl' ],
+                       [ 'Belle & Sebastian', 'Belle & S', '<strong>Belle 
&amp; S</strong>ebastian' ],
+                       [ 'Belle & the Beast', 'Belle &amp;', 'Belle &amp; the 
Beast' ],
+                       [ 'with ? in it', 'with ?', '<strong>with ?</strong> in 
it' ], // not at start
+                       [ 'Title with ? in it', 'with ?', 'Title with ? in it' 
], // not at start
+                       [ 'AT&T', 'a', '<strong>A</strong>T&amp;T'],
+                       [ 'AT&T', 'at&', '<strong>AT&amp;</strong>T'],
+                       [ '<tag', '&lt;tag', '&lt;tag' ],
+                       [ '& this is a weird title', '&', 
'<strong>&amp;</strong> this is a weird title' ],
+                       [ '& this is a weird title', '&a', '&amp; this is a 
weird title' ],
+                       [ '&lt;t', '<t', '&amp;lt;t' ],
+                       [ "<script>alert('FAIL')</script> should be safe",
+                               "<script>alert('FAIL'", 
"<strong>&lt;script&gt;alert('FAIL'</strong>)&lt;/script&gt; should be safe" ]
+               ];
+
+               $.each( data, function( i, item ) {
+                       assert.strictEqual( SearchApi._highlightSearchTerm( 
item[0], item[1] ), item[2], 'highlightSearchTerm test ' + i );
+               } );
+       } );
+
+}( jQuery, mw.mobileFrontend ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/95318
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I842ff097814f66cb7cca4949b5ccadff834029c4
Gerrit-PatchSet: 4
Gerrit-Project: mediawiki/extensions/MobileFrontend
Gerrit-Branch: master
Gerrit-Owner: JGonera <[email protected]>
Gerrit-Reviewer: JGonera <[email protected]>
Gerrit-Reviewer: Jdlrobson <[email protected]>
Gerrit-Reviewer: jenkins-bot

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to