%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/resources/mobile.search/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/MobileFrontend/resources/mobile.search/SearchOverlay.js |
( function ( M ) {
var
Overlay = M.require( 'mobile.startup/Overlay' ),
util = M.require( 'mobile.startup/util' ),
Anchor = M.require( 'mobile.startup/Anchor' ),
Icon = M.require( 'mobile.startup/Icon' ),
WatchstarPageList = M.require( 'mobile.pagelist.scripts/WatchstarPageList' ),
SEARCH_DELAY = 300,
SEARCH_SPINNER_DELAY = 2000,
feedbackLink = mw.config.get( 'wgCirrusSearchFeedbackLink' );
/**
* Overlay displaying search results
* @class SearchOverlay
* @extends Overlay
* @uses SearchGateway
* @uses Icon
*
* @param {Object} options Configuration options
* @fires SearchOverlay#search-show
* @fires SearchOverlay#search-start
* @fires SearchOverlay#search-results
* @fires SearchOverlay#search-result-click
*/
function SearchOverlay( options ) {
Overlay.call( this, options );
this.api = options.api;
// eslint-disable-next-line new-cap
this.gateway = new options.gatewayClass( this.api );
this.router = options.router;
}
OO.mfExtend( SearchOverlay, Overlay, {
/**
* @memberof SearchOverlay
* @instance
*/
isBorderBox: false,
/**
* @memberof SearchOverlay
* @instance
*/
templatePartials: util.extend( {}, Overlay.prototype.templatePartials, {
header: mw.template.get( 'mobile.search', 'header.hogan' ),
content: mw.template.get( 'mobile.search', 'content.hogan' ),
icon: Icon.prototype.template
} ),
/**
* @memberof SearchOverlay
* @instance
*/
className: 'overlay search-overlay',
/**
* @memberof SearchOverlay
* @instance
* @mixes Overlay#defaults
* @property {Object} defaults Default options hash.
* @property {SearchGateway} defaults.gatewayClass The class to use to setup an API gateway.
* FIXME: Should be removed when wikidata descriptions in stable (T101719)
* @property {Router} defaults.router instance
* @property {Object} defaults.clearIcon options for the button that clears the search text.
* @property {Object} defaults.searchContentIcon options for the button that allows you to
* search within content
* @property {string} defaults.searchTerm Search text.
* @property {string} defaults.placeholderMsg Search input placeholder text.
* @property {string} defaults.clearMsg Tooltip for clear button that appears when you type
* into search box.
* @property {string} defaults.searchContentMsg Caption for a button performing full text
* search of a given search query.
* @property {string} defaults.noResultsMsg Message informing user that no pages were found
* for a given query.
* @property {string} defaults.searchContentNoResultsMsg Used when no pages with matching
* titles were found.
* @property {string} defaults.action The value of wgScript
* @property {Object} defaults.feedback options for the feedback link
* below the search results
*/
defaults: util.extend( {}, Overlay.prototype.defaults, {
headerChrome: true,
clearIcon: new Icon( {
tagName: 'button',
name: 'clear',
isSmall: true,
label: mw.msg( 'mobile-frontend-clear-search' ),
additionalClassNames: 'clear'
} ).options,
searchContentIcon: new Icon( {
tagName: 'a',
// When this icon is clicked we want to reset the hash for subsequent views
href: '#',
name: 'search-content',
label: mw.msg( 'mobile-frontend-search-content' )
} ).options,
searchTerm: '',
placeholderMsg: '',
noResultsMsg: mw.msg( 'mobile-frontend-search-no-results' ),
searchContentNoResultsMsg: mw.message( 'mobile-frontend-search-content-no-results' ).parse(),
action: mw.config.get( 'wgScript' ),
feedback: !feedbackLink ? false : {
feedback: new Anchor( {
label: mw.msg( 'mobile-frontend-search-feedback-link-text' ),
href: feedbackLink
} ).options,
prompt: mw.msg( 'mobile-frontend-search-feedback-prompt' )
}
} ),
/**
* @inheritdoc
* @memberof SearchOverlay
* @instance
*/
events: util.extend( {}, Overlay.prototype.events, {
'input input': 'onInputInput',
'click .clear': 'onClickClear',
'click .search-content': 'onClickSearchContent',
'click .overlay-content': 'onClickOverlayContent',
'click .overlay-content > div': 'onClickOverlayContentDiv',
'touchstart .results': 'hideKeyboardOnScroll',
'mousedown .results': 'hideKeyboardOnScroll',
'click .results a': 'onClickResult'
} ),
/**
* Make sure search header is docked to the top of the screen when the
* user begins typing so that there is adequate space for search results
* above the keyboard. (This is only a potential issue when sitenotices
* are displayed.)
* @memberof SearchOverlay
* @instance
*/
onInputInput: function () {
this.performSearch();
this.$clear.toggle( this.$input.val() !== '' );
},
/**
* Initialize the button that clears the search field
* @memberof SearchOverlay
* @instance
* @return {boolean} False to cancel the native event
*/
onClickClear: function () {
this.$input.val( '' ).focus();
this.performSearch();
this.$clear.hide();
// In beta the clear button is on top of the search input.
// Stop propagation so that the input doesn't receive the click.
return false;
},
/**
* Initialize 'search within pages' functionality
* @memberof SearchOverlay
* @instance
*/
onClickSearchContent: function () {
var $el = util.getDocument().find( 'body' ),
$form = this.$( 'form' );
// Add fulltext input to force fulltext search
this.parseHTML( '<input>' )
.attr( {
type: 'hidden',
name: 'fulltext',
value: 'search'
} )
.appendTo( $form );
// history.back queues a task so might run after this call. Thus we use setTimeout
// http://www.w3.org/TR/2011/WD-html5-20110113/webappapis.html#queue-a-task
setTimeout( function () {
// Firefox doesn't allow submission of a form not in the DOM
// so temporarily re-add it
$form.appendTo( $el );
$form.submit();
}, 0 );
},
/**
* Tapping on background only should hide the overlay
* @memberof SearchOverlay
* @instance
*/
onClickOverlayContent: function () {
this.$( '.cancel' ).trigger( 'click' );
},
/**
* Stop propagation
* @memberof SearchOverlay
* @instance
* @param {jQuery.Event} ev
*/
onClickOverlayContentDiv: function ( ev ) {
ev.stopPropagation();
},
/**
* Hide the keyboard when scrolling starts (avoid weird situation when
* user taps on an item, the keyboard hides and wrong item is clicked).
* @memberof SearchOverlay
* @instance
*/
hideKeyboardOnScroll: function () {
this.$input.blur();
},
/**
* Handle the user clicking a result.
*
* @memberof SearchOverlay
* @instance
* @param {jQuery.Event} ev
*/
onClickResult: function ( ev ) {
var $link = this.$( ev.currentTarget ),
$result = $link.closest( 'li' );
/**
* Fired when the user clicks a search result
* @event SearchOverlay#search-result-click
* @type {Object}
* @property {jQuery.Object} result The jQuery-wrapped DOM element that
* the user clicked
* @property {number} resultIndex The zero-based index of the
* result in the set of results
* @property {jQuery.Event} originalEvent The original event
*/
this.emit( 'search-result-click', {
result: $result,
resultIndex: this.$results.index( $result ),
originalEvent: ev
} );
// FIXME: ugly hack that removes search from browser history
// when navigating to search results
ev.preventDefault();
this.router.back().then( function () {
// Router.navigate does not support changing href.
// FIXME: Needs upstream change T189173
// eslint-disable-next-line no-restricted-properties
window.location.href = $link.attr( 'href' );
} );
},
/**
* @inheritdoc
* @memberof SearchOverlay
* @instance
*/
postRender: function () {
var self = this,
timer;
Overlay.prototype.postRender.call( this );
this.$input = this.$( 'input' );
this.$clear = this.$( '.clear' );
this.$searchContent = this.$( '.search-content' ).hide();
this.$searchFeedback = this.$( '.search-feedback' ).hide();
this.$resultContainer = this.$( '.results-list-container' );
/**
* Hide the spinner and abort timed spinner shows.
*/
function clearSearch() {
self.$spinner.hide();
clearTimeout( timer );
}
// Show a spinner on top of search results
this.$spinner = this.$( '.spinner-container' );
this.on( 'search-start', function ( searchData ) {
if ( timer ) {
clearSearch();
}
timer = setTimeout( function () {
self.$spinner.show();
}, SEARCH_SPINNER_DELAY - searchData.delay );
} );
this.on( 'search-results', clearSearch );
// Hide the clear button if the search input is empty
if ( self.$input.val() === '' ) {
this.$clear.hide();
}
},
/**
* Trigger a focus() event on search input in order to
* bring up the virtual keyboard.
* @memberof SearchOverlay
* @instance
*/
showKeyboard: function () {
var len = this.$input.val().length;
this.$input.focus();
// Cursor to the end of the input
if ( this.$input[0].setSelectionRange ) {
this.$input[0].setSelectionRange( len, len );
}
},
/**
* @inheritdoc
* @memberof SearchOverlay
* @instance
*/
show: function () {
// Overlay#show defines the actual overlay visibility.
Overlay.prototype.show.apply( this, arguments );
this.showKeyboard();
/**
* Fired after the search overlay is shown
* @event SearchOverlay#search-show
*/
this.emit( 'search-show' );
},
/**
* Fade out if the browser supports animations
* @inheritdoc
* @memberof SearchOverlay
* @instance
*/
hide: function () {
var self = this,
$html = util.getDocument();
if ( $html.hasClass( 'animations' ) ) {
self.$el.addClass( 'fade-out' );
setTimeout( function () {
Overlay.prototype.hide.apply( self, arguments );
}, 500 );
} else {
Overlay.prototype.hide.apply( self, arguments );
}
return true;
},
/**
* Perform search and render results inside current view.
* FIXME: Much of the logic for caching and pending queries inside this function should
* actually live in SearchGateway, please move out.
* @memberof SearchOverlay
* @instance
*/
performSearch: function () {
var
self = this,
api = this.api,
query = this.$input.val(),
delay = this.gateway.isCached( query ) ? 0 : SEARCH_DELAY;
// it seems the input event can be fired when virtual keyboard is closed
// (Chrome for Android)
if ( query !== this.lastQuery ) {
if ( self._pendingQuery ) {
self._pendingQuery.abort();
}
clearTimeout( this.timer );
if ( query.length ) {
this.timer = setTimeout( function () {
var xhr;
/**
* Fired immediately before the search API request is sent
* @event SearchOverlay#search-start
* @property {Object} data related to the current search
*/
self.emit( 'search-start', {
query: query,
delay: delay
} );
xhr = self.gateway.search( query );
self._pendingQuery = xhr.then( 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 && data.query === self.$input.val() ) {
self.$el.toggleClass( 'no-results', data.results.length === 0 );
self.$searchContent
.show()
.find( 'p' )
.hide()
.filter( data.results.length ? '.with-results' : '.without-results' )
.show();
// eslint-disable-next-line no-new
new WatchstarPageList( {
api: api,
funnel: 'search',
pages: data.results,
el: self.$resultContainer
} );
self.$results = self.$resultContainer.find( 'li' );
/**
* Fired when search API returns results
* @event SearchOverlay#search-results
* @type {Object}
* @property {Object[]} results The results returned by the search
* API
*/
self.emit( 'search-results', {
results: data.results
} );
}
} ).promise( { abort: function () { xhr.abort(); } } );
}, delay );
} else {
self.resetSearch();
}
this.lastQuery = query;
}
},
/**
* Clear results
*
* @private
*/
resetSearch: function () {
this.$spinner.hide();
this.$searchContent.hide();
this.$searchFeedback.hide();
this.$resultContainer.empty();
}
} );
M.define( 'mobile.search/SearchOverlay', SearchOverlay ); // resource-modules-disable-line
}( mw.mobileFrontend ) );