%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /www/varak.net/wiki.varak.net/extensions/MobileFrontend/resources/mobile.search/
Upload File :
Create Path :
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 ) );

Zerion Mini Shell 1.0