diff --git a/RELEASE-NOTES-1.20 b/RELEASE-NOTES-1.20 index 3e68abd06d5..03e3062d8c9 100644 --- a/RELEASE-NOTES-1.20 +++ b/RELEASE-NOTES-1.20 @@ -149,6 +149,8 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki. * (bug 40072) Add CSS classes to items in output of ChangesList pages. * Added $wgCopyUploadProxy global to define which proxy to use for copy uploads. +* (bug 40448) mediawiki.legacy.mwsuggest has been replaced with a new module, + mediawiki.searchSuggest, based on SimpleSeach from Extension:Vector. === Bug fixes in 1.20 === * (bug 30245) Use the correct way to construct a log page title. @@ -323,6 +325,11 @@ changes to languages because of Bugzilla reports. * Use of __DIR__ instead of dirname( __FILE__ ). * OutputPage::wrapWikiMsg() no longer supports the 'options' parameter. It was not used and complicated migration to Message class. +* (bug 40448) Removed mediawiki.legacy.mwsuggest module, and removed the + following that has become obsolete: + - globals $wgEnableMWSuggest and $wgMWSuggestTemplate. + - mw.config.values wgMWSuggestTemplate and wgSearchNamespaces. + - method SearchEngine::getMWSuggestTemplate(). == Compatibility == diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index c2574dd09e8..94b24a31b0f 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2720,7 +2720,7 @@ $wgFooterIcons = array( $wgUseCombinedLoginLink = false; /** - * Search form behavior for Vector skin only. + * Search form look for Vector skin only. * - true = use an icon search button * - false = use Go & Search buttons */ @@ -4694,17 +4694,11 @@ $wgCountTotalSearchHits = false; */ $wgOpenSearchTemplate = false; -/** - * Enable suggestions while typing in search boxes - * (results are passed around in OpenSearch format) - * Requires $wgEnableOpenSearchSuggest = true; - */ -$wgEnableMWSuggest = false; - /** * Enable OpenSearch suggestions requested by MediaWiki. Set this to - * false if you've disabled MWSuggest or another suggestion script and - * want reduce load caused by cached scripts pulling suggestions. + * false if you've disabled scripts that use api?action=opensearch and + * want reduce load caused by cached scripts still pulling suggestions. + * It will let the API fallback by responding with an empty array. */ $wgEnableOpenSearchSuggest = true; @@ -4713,14 +4707,6 @@ $wgEnableOpenSearchSuggest = true; */ $wgSearchSuggestCacheExpiry = 1200; -/** - * Template for internal MediaWiki suggestion engine, defaults to API action=opensearch - * - * Placeholders: {searchTerms}, {namespaces}, {dbname} - * - */ -$wgMWSuggestTemplate = false; - /** * If you've disabled search semi-permanently, this also disables updates to the * table. If you ever re-enable, be sure to rebuild the search table. diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 981637c97b8..a2e26709bbe 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -2459,7 +2459,7 @@ $templates */ private function addDefaultModules() { global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax, - $wgAjaxWatch, $wgEnableMWSuggest; + $wgAjaxWatch; // Add base resources $this->addModules( array( @@ -2487,8 +2487,8 @@ $templates $this->addModules( 'mediawiki.page.watch.ajax' ); } - if ( $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) { - $this->addModules( 'mediawiki.legacy.mwsuggest' ); + if ( !$this->getUser()->getOption( 'disablesuggest', false ) ) { + $this->addModules( 'mediawiki.searchSuggest' ); } } @@ -2912,7 +2912,7 @@ $templates * @return array */ public function getJSVars() { - global $wgUseAjax, $wgEnableMWSuggest, $wgContLang; + global $wgUseAjax, $wgContLang; $latestRevID = 0; $pageID = 0; @@ -2978,9 +2978,6 @@ $templates foreach ( $title->getRestrictionTypes() as $type ) { $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type ); } - if ( $wgUseAjax && $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) { - $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $this->getUser() ); - } if ( $title->isMainPage() ) { $vars['wgIsMainPage'] = true; } diff --git a/includes/Preferences.php b/includes/Preferences.php index eaf781bb7f2..216ba48c8f9 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -978,7 +978,7 @@ class Preferences { * @param $defaultPreferences Array */ static function searchPreferences( $user, IContextSource $context, &$defaultPreferences ) { - global $wgContLang, $wgEnableMWSuggest, $wgVectorUseSimpleSearch; + global $wgContLang, $wgVectorUseSimpleSearch; ## Search ##################################### $defaultPreferences['searchlimit'] = array( @@ -988,22 +988,21 @@ class Preferences { 'min' => 0, ); - if ( $wgEnableMWSuggest ) { - $defaultPreferences['disablesuggest'] = array( - 'type' => 'toggle', - 'label-message' => 'mwsuggest-disable', - 'section' => 'searchoptions/displaysearchoptions', - ); - } if ( $wgVectorUseSimpleSearch ) { $defaultPreferences['vector-simplesearch'] = array( 'type' => 'toggle', 'label-message' => 'vector-simplesearch-preference', - 'section' => 'searchoptions/displaysearchoptions' + 'section' => 'searchoptions/displaysearchoptions', ); } + $defaultPreferences['disablesuggest'] = array( + 'type' => 'toggle', + 'label-message' => 'mwsuggest-disable', + 'section' => 'searchoptions/displaysearchoptions', + ); + $defaultPreferences['searcheverything'] = array( 'type' => 'toggle', 'label-message' => 'searcheverything-enable', diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 5a7835412cc..ef56274197b 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -45,7 +45,7 @@ class ApiOpenSearch extends ApiBase { $namespaces = $params['namespace']; $suggest = $params['suggest']; - // MWSuggest or similar hit + // Some script that was loaded regardless of wgEnableOpenSearchSuggest, likely cached. if ( $suggest && !$wgEnableOpenSearchSuggest ) { $searches = array(); } else { diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index c86ed1d7562..20ee83f9c77 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -37,8 +37,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { protected function getConfig( $context ) { global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, - $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion, - $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, + $wgVariantArticlePath, $wgActionPaths, $wgVersion, + $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgSitename, $wgFileExtensions, $wgExtensionAssetsPath, $wgCookiePrefix, $wgResourceLoaderMaxQueryLength; @@ -95,9 +95,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength, 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, ); - if ( $wgUseAjax && $wgEnableMWSuggest ) { - $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate(); - } wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) ); diff --git a/includes/search/SearchEngine.php b/includes/search/SearchEngine.php index 2ccb6d3164d..27a321ac167 100644 --- a/includes/search/SearchEngine.php +++ b/includes/search/SearchEngine.php @@ -505,19 +505,6 @@ class SearchEngine { return $wgCanonicalServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns; } } - - /** - * Get internal MediaWiki Suggest template - * - * @return String - */ - public static function getMWSuggestTemplate() { - global $wgMWSuggestTemplate, $wgServer; - if ( $wgMWSuggestTemplate ) - return $wgMWSuggestTemplate; - else - return $wgServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace={namespaces}&suggest'; - } } /** diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index d7d3beb55e6..e75567c9245 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -799,7 +799,7 @@ XHTML id names. 'vector-action-protect' => 'Protect', 'vector-action-undelete' => 'Undelete', 'vector-action-unprotect' => 'Change protection', -'vector-simplesearch-preference' => 'Enable enhanced search suggestions (Vector skin only)', +'vector-simplesearch-preference' => 'Enable simplified search bar (Vector skin only)', 'vector-view-create' => 'Create', 'vector-view-edit' => 'Edit', 'vector-view-history' => 'View history', @@ -1752,8 +1752,6 @@ Details can be found in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENA 'search-interwiki-default' => '$1 results:', 'search-interwiki-custom' => '', # do not translate or duplicate this message to other languages 'search-interwiki-more' => '(more)', -'search-mwsuggest-enabled' => 'with suggestions', -'search-mwsuggest-disabled' => 'no suggestions', 'search-relatedarticle' => 'Related', 'mwsuggest-disable' => 'Disable AJAX suggestions', 'searcheverything-enable' => 'Search in all namespaces', @@ -4889,6 +4887,10 @@ Otherwise, you can use the easy form below. Your comment will be added to the pa 'feedback-bugcheck' => 'Great! Just check that it is not already one of the [$1 known bugs].', 'feedback-bugnew' => 'I checked. Report a new bug', +# Search suggestions +'searchsuggest-search' => 'Search', +'searchsuggest-containing' => 'containing...', + # API errors 'api-error-badaccess-groups' => 'You are not permitted to upload files to this wiki.', 'api-error-badtoken' => 'Internal error: Bad token.', diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index 9b0c5b4ceeb..8d3d1eebe4d 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -4837,6 +4837,12 @@ $4 is the gender of the target user.', 'feedback-bugcheck' => 'Message that appears before the user submits a bug, reminding them to check for known bugs.', 'feedback-bugnew' => 'Button label - asserts that the user has checked for existing bugs. When clicked will launch a bugzilla form to add a new bug in a new tab or window', +# SimpleSearch +'simplesearch-search' => 'Greyed out default text in the simple search box (it disappears and lets the user enter the requested search terms when the search box receives focus.) + +{{Identical|Search}}', +'simplesearch-containing' => 'Label used in the special item of the search suggestions list which gives the user an option to perform a full text search for the term.', + # API errors 'api-error-badaccess-groups' => 'API error message that can be used for client side localisation of API errors.', 'api-error-badtoken' => 'API error message that can be used for client side localisation of API errors.', diff --git a/resources/Resources.php b/resources/Resources.php index 17a5904157d..1910672e7d4 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -614,6 +614,19 @@ return array( 'mediawiki.notify' => array( 'scripts' => 'resources/mediawiki/mediawiki.notify.js', ), + 'mediawiki.searchSuggest' => array( + 'scripts' => 'resources/mediawiki/mediawiki.searchSuggest.js', + 'messages' => array( + 'searchsuggest-search', + 'searchsuggest-containing', + ), + 'dependencies' => array( + 'jquery.autoEllipsis', + 'jquery.client', + 'jquery.placeholder', + 'jquery.suggestions', + ), + ), 'mediawiki.Title' => array( 'scripts' => 'resources/mediawiki/mediawiki.Title.js', 'dependencies' => 'mediawiki.util', @@ -901,13 +914,6 @@ return array( 'localBasePath' => $GLOBALS['wgStyleDirectory'], 'dependencies' => 'mediawiki.legacy.wikibits', ), - 'mediawiki.legacy.mwsuggest' => array( - 'scripts' => 'common/mwsuggest.js', - 'remoteBasePath' => $GLOBALS['wgStylePath'], - 'localBasePath' => $GLOBALS['wgStyleDirectory'], - 'dependencies' => 'mediawiki.legacy.wikibits', - 'messages' => array( 'search-mwsuggest-enabled', 'search-mwsuggest-disabled' ), - ), 'mediawiki.legacy.protect' => array( 'scripts' => 'common/protect.js', 'remoteBasePath' => $GLOBALS['wgStylePath'], diff --git a/resources/jquery/jquery.suggestions.css b/resources/jquery/jquery.suggestions.css index 3cbdad22985..e0ba647b9d2 100644 --- a/resources/jquery/jquery.suggestions.css +++ b/resources/jquery/jquery.suggestions.css @@ -11,14 +11,15 @@ padding: 0; margin: -1px -1px 0 0; } + /* IGNORED BY IE6 */ html > body .suggestions { margin: -1px 0 0 0; } + .suggestions-special { position: relative; background-color: white; - font-size: 0.8em; cursor: pointer; border: solid 1px #aaaaaa; padding: 0; @@ -28,14 +29,15 @@ html > body .suggestions { padding: 0.25em 0.25em; line-height: 1.25em; } + .suggestions-results { background-color: white; - font-size: 0.8em; cursor: pointer; border: solid 1px #aaaaaa; padding: 0; margin: 0; } + .suggestions-result { color: black; margin: 0; @@ -43,28 +45,33 @@ html > body .suggestions { padding: 0.01em 0.25em; text-align: left; } + .suggestions-result-current { background-color: #4C59A6; color: white; } + .suggestions-special .special-label { - font-size: 0.8em; color: gray; text-align: left; } + .suggestions-special .special-query { color: black; font-style: italic; text-align: left; } + .suggestions-special .special-hover { background-color: silver; } + .suggestions-result-current .special-label, .suggestions-result-current .special-query { color: white; } + .autoellipsis-matched, .highlight { font-weight: bold; -} \ No newline at end of file +} diff --git a/resources/jquery/jquery.suggestions.js b/resources/jquery/jquery.suggestions.js index dff5535f8c3..d80680fcfc5 100644 --- a/resources/jquery/jquery.suggestions.js +++ b/resources/jquery/jquery.suggestions.js @@ -36,7 +36,11 @@ * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set to e.g. 2, the suggestions box * will never be grown beyond 2 times the width of the textbox. * Type: Number, Range: 1 - infinity, Default: 3 - * positionFromLeft: Whether to position the suggestion box with the left attribute or the right + * expandFrom: Which direction to offset the suggestion box from. + * Values 'start' and 'end' translate to left and right respectively depending on the directionality + * of the current document, according to $( 'html' ).css( 'direction' ). + * Type: String, default: 'auto', options: 'left', 'right', 'start', 'end', 'auto'. + * positionFromLeft: Sets expandFrom=left, for backwards compatibility * Type: Boolean, Default: true * highlightInput: Whether to hightlight matched portions of the input or not * Type: Boolean, Default: false @@ -114,6 +118,7 @@ $.suggestions = { * @param value Mixed Value to set property with */ configure: function ( context, property, value ) { + var newCSS; // Validate creation using fallback values switch( property ) { case 'fetch': @@ -121,6 +126,7 @@ $.suggestions = { case 'special': case 'result': case '$region': + case 'expandFrom': context.config[property] = value; break; case 'suggestions': @@ -134,19 +140,77 @@ $.suggestions = { // Rebuild the suggestions list context.data.$container.show(); // Update the size and position of the list - var newCSS = { + newCSS = { top: context.config.$region.offset().top + context.config.$region.outerHeight(), bottom: 'auto', width: context.config.$region.outerWidth(), height: 'auto' }; - if ( context.config.positionFromLeft ) { + + // Process expandFrom, after this it is set to left or right. + context.config.expandFrom = ( function ( expandFrom ) { + var regionWidth, docWidth, regionCenter, docCenter, + docDir = $( document.documentElement ).css( 'direction' ), + $region = context.config.$region; + + // Backwards compatible + if ( context.config.positionFromLeft ) { + expandFrom = 'left'; + + // Catch invalid values, default to 'auto' + } else if ( $.inArray( expandFrom, ['left', 'right', 'start', 'end', 'auto'] ) === -1 ) { + expandFrom = 'auto'; + } + + if ( expandFrom === 'auto' ) { + if ( $region.data( 'searchsuggest-expand-dir' ) ) { + // If the markup explicitly contains a direction, use it. + expandFrom = $region.data( 'searchsuggest-expand-dir' ); + } else { + regionWidth = $region.outerWidth(); + docWidth = $( document ).width(); + if ( ( regionWidth / docWidth ) > 0.85 ) { + // If the input size takes up more than 85% of the document horizontally + // expand the suggestions to the writing direction's native end. + expandFrom = 'start'; + } else { + // Calculate the center points of the input and document + regionCenter = $region.offset().left + regionWidth / 2; + docCenter = docWidth / 2; + if ( Math.abs( regionCenter - docCenter ) / docCenter < 0.10 ) { + // If the input's center is within 10% of the document center + // use the writing direction's native end. + expandFrom = 'start'; + } else { + // Otherwise expand the input from the closest side of the page, + // towards the side of the page with the most free open space + expandFrom = regionCenter > docCenter ? 'right' : 'left'; + } + } + } + } + + if ( expandFrom === 'start' ) { + expandFrom = docDir === 'rtl' ? 'right': 'left'; + + } else if ( expandFrom === 'end' ) { + expandFrom = docDir === 'rtl' ? 'left': 'right'; + } + + return expandFrom; + + }( context.config.expandFrom ) ); + + if ( context.config.expandFrom === 'left' ) { + // Expand from left newCSS.left = context.config.$region.offset().left; newCSS.right = 'auto'; } else { + // Expand from right newCSS.left = 'auto'; newCSS.right = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() ); } + context.data.$container.css( newCSS ); var $results = context.data.$container.children( '.suggestions-results' ); $results.empty(); @@ -344,14 +408,15 @@ $.suggestions = { $.fn.suggestions = function () { // Multi-context fields - var returnValue; - var args = arguments; + var returnValue, + args = arguments; $(this).each( function () { + var context, key; /* Construction / Loading */ - var context = $(this).data( 'suggestions-context' ); + context = $(this).data( 'suggestions-context' ); if ( context === undefined || context === null ) { context = { config: { @@ -365,7 +430,7 @@ $.fn.suggestions = function () { 'delay': 120, 'submitOnClick': false, 'maxExpandFactor': 3, - 'positionFromLeft': true, + 'expandFrom': 'auto', 'highlightInput': false } }; @@ -377,7 +442,7 @@ $.fn.suggestions = function () { if ( args.length > 0 ) { if ( typeof args[0] === 'object' ) { // Apply set of properties - for ( var key in args[0] ) { + for ( key in args[0] ) { $.suggestions.configure( context, key, args[0][key] ); } } else if ( typeof args[0] === 'string' ) { @@ -409,22 +474,9 @@ $.fn.suggestions = function () { $textbox: $(this), selectedWithMouse: false }; - // Setup the css for positioning the results box - var newCSS = { - top: Math.round( context.data.$textbox.offset().top + context.data.$textbox.outerHeight() ), - width: context.data.$textbox.outerWidth(), - display: 'none' - }; - if ( context.config.positionFromLeft ) { - newCSS.left = context.config.$region.offset().left; - newCSS.right = 'auto'; - } else { - newCSS.left = 'auto'; - newCSS.right = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() ); - } context.data.$container = $( '
' ) - .css( newCSS ) + .css( 'display', 'none' ) .addClass( 'suggestions' ) .append( $( '
' ).addClass( 'suggestions-results' ) @@ -476,6 +528,7 @@ $.fn.suggestions = function () { } ) ) .appendTo( $( 'body' ) ); + $(this) // Stop browser autocomplete from interfering .attr( 'autocomplete', 'off') @@ -521,6 +574,7 @@ $.fn.suggestions = function () { $.suggestions.cancel( context ); } ); } + // Store the context for next time $(this).data( 'suggestions-context', context ); } ); diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js new file mode 100644 index 00000000000..42c839c71e3 --- /dev/null +++ b/resources/mediawiki/mediawiki.searchSuggest.js @@ -0,0 +1,161 @@ +/** + * Add search suggestions to the search form. + */ +( function ( mw, $ ) { + $( document ).ready( function ( $ ) { + var map, + // Region where the suggestions box will appear directly below + // (using the same width). Can be a container element or the input + // itself, depending on what suits best in the environment. + // For Vector the suggestion box should align with the simpleSearch + // container's borders, in other skins it should align with the input + // element (not the search form, as that would leave the buttons + // vertically between the input and the suggestions). + $searchRegion = $( '#simpleSearch, #searchInput' ).first(), + $searchInput = $( '#searchInput' ); + + // Ensure that the thing is actually present! + if ( $searchRegion.length === 0 ) { + // Don't try to set anything up if simpleSearch is disabled sitewide. + // The loader code loads us if the option is present, even if we're + // not actually enabled (anymore). + return; + } + + // Compatibility map + map = { + browsers: { + // Left-to-right languages + ltr: { + // SimpleSearch is broken in Opera < 9.6 + opera: [['>=', 9.6]], + docomo: false, + blackberry: false, + ipod: false, + iphone: false + }, + // Right-to-left languages + rtl: { + opera: [['>=', 9.6]], + docomo: false, + blackberry: false, + ipod: false, + iphone: false + } + } + }; + + if ( !$.client.test( map ) ) { + return; + } + + // Placeholder text for search box + $searchInput + .attr( 'placeholder', mw.msg( 'searchsuggest-search' ) ) + .placeholder(); + + // General suggestions functionality for all search boxes + $( '#searchInput, #searchInput2, #powerSearchText, #searchText' ) + .suggestions( { + fetch: function ( query ) { + var $el, jqXhr; + + if ( query.length !== 0 ) { + $el = $(this); + jqXhr = $.ajax( { + url: mw.util.wikiScript( 'api' ), + data: { + format: 'json', + action: 'opensearch', + search: query, + namespace: 0, + suggest: '' + }, + dataType: 'json', + success: function ( data ) { + if ( $.isArray( data ) && data.length ) { + $el.suggestions( 'suggestions', data[1] ); + } + } + }); + $el.data( 'request', jqXhr ); + } + }, + cancel: function () { + var jqXhr = $(this).data( 'request' ); + // If the delay setting has caused the fetch to have not even happened + // yet, the jqXHR object will have never been set. + if ( jqXhr && $.isFunction ( jqXhr.abort ) ) { + jqXhr.abort(); + $(this).removeData( 'request' ); + } + }, + result: { + select: function ( $input ) { + $input.closest( 'form' ).submit(); + } + }, + delay: 120, + highlightInput: true + } ) + .bind( 'paste cut drop', function () { + // make sure paste and cut events from the mouse and drag&drop events + // trigger the keypress handler and cause the suggestions to update + $( this ).trigger( 'keypress' ); + } ); + + // Special suggestions functionality for skin-provided search box + $searchInput.suggestions( { + result: { + select: function ( $input ) { + $input.closest( 'form' ).submit(); + } + }, + special: { + render: function ( query ) { + var $el = this; + if ( $el.children().length === 0 ) { + $el + .append( + $( '
' ) + .addClass( 'special-label' ) + .text( mw.msg( 'searchsuggest-containing' ) ) + ) + .append( + $( '
' ) + .addClass( 'special-query' ) + .text( query ) + .autoEllipsis() + ) + .show(); + } else { + $el.find( '.special-query' ) + .empty() + .text( query ) + .autoEllipsis(); + } + }, + select: function ( $input ) { + $input.closest( 'form' ).append( + $( '', { + name: 'fulltext', + val: '1' + }) + ); + $input.closest( 'form' ).submit(); + } + }, + $region: $searchRegion + } ); + + // In most skins (at least Monobook and Vector), the font-size is messed up in . + // (they use 2 elements to get a sane font-height). So, instead of making exceptions for + // each skin or adding more stylesheets, just copy it from the active element so auto-fit. + $searchInput + .data( 'suggestions-context' ) + .data.$container + .css( 'fontSize', $searchInput.css( 'fontSize' ) ); + + } ); + +}( mediaWiki, jQuery ) ); \ No newline at end of file diff --git a/skins/common/mwsuggest.js b/skins/common/mwsuggest.js deleted file mode 100644 index dac59546ec7..00000000000 --- a/skins/common/mwsuggest.js +++ /dev/null @@ -1,1063 +0,0 @@ -/* - * OpenSearch ajax suggestion engine for MediaWiki - * - * uses core MediaWiki open search support to fetch suggestions - * and show them below search boxes and other inputs - * - * by Robert Stojnic (April 2008) - */ - -// Make sure wgMWSuggestTemplate is defined -if ( !mw.config.exists( 'wgMWSuggestTemplate' ) ) { - mw.config.set( 'wgMWSuggestTemplate', mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) - + "/api.php?action=opensearch\x26search={searchTerms}\x26namespace={namespaces}\x26suggest" ); -} - -// search_box_id -> Results object -window.os_map = {}; -// cached data, url -> json_text -window.os_cache = {}; -// global variables for suggest_keypress -window.os_cur_keypressed = 0; -window.os_keypressed_count = 0; -// type: Timer -window.os_timer = null; -// tie mousedown/up events -window.os_mouse_pressed = false; -window.os_mouse_num = -1; -// if true, the last change was made by mouse (and not keyboard) -window.os_mouse_moved = false; -// delay between keypress and suggestion (in ms) -window.os_search_timeout = 250; -// these pairs of inputs/forms will be autoloaded at startup -window.os_autoload_inputs = ['searchInput', 'searchInput2', 'powerSearchText', 'searchText']; -window.os_autoload_forms = ['searchform', 'searchform2', 'powersearch', 'search']; -// if we stopped the service -window.os_is_stopped = false; -// max lines to show in suggest table -window.os_max_lines_per_suggest = 7; -// number of steps to animate expansion/contraction of container width -window.os_animation_steps = 6; -// num of pixels of smallest step -window.os_animation_min_step = 2; -// delay between steps (in ms) -window.os_animation_delay = 30; -// max width of container in percent of normal size (1 == 100%) -window.os_container_max_width = 2; -// currently active animation timer -window.os_animation_timer = null; -// whether MWSuggest is enabled. Set to false when os_MWSuggestDisable() is called -window.os_enabled = true; - -/** - * is a new HTML5 element that allows you to manually - * supply suggestion lists and have them rendered according to the - * right platform conventions. Opera as of version 11 has a fatal - * problem: the suggestion lags behind what the user types by one - * keypress. (Reported as DSK-276870 to Opera's secret bug tracker.) - * There are also problems with other browsers, including Firefox and - * Safari: See bug 31602 for details. - */ -window.os_use_datalist = false; - -/** Timeout timer class that will fetch the results */ -window.os_Timer = function( id, r, query ) { - this.id = id; - this.r = r; - this.query = query; -}; - -/** Property class for single search box */ -window.os_Results = function( name, formname ) { - this.searchform = formname; // id of the searchform - this.searchbox = name; // id of the searchbox - this.container = name + 'Suggest'; // div that holds results - this.resultTable = name + 'Result'; // id base for the result table (+num = table row) - this.resultText = name + 'ResultText'; // id base for the spans within result tables (+num) - this.toggle = name + 'Toggle'; // div that has the toggle (enable/disable) link - this.query = null; // last processed query - this.results = null; // parsed titles - this.resultCount = 0; // number of results - this.original = null; // query that user entered - this.selected = -1; // which result is selected - this.containerCount = 0; // number of results visible in container - this.containerRow = 0; // height of result field in the container - this.containerTotal = 0; // total height of the container will all results - this.visible = false; // if container is visible - this.stayHidden = false; // don't try to show if lost focus -}; - -/** Timer user to animate expansion/contraction of container width */ -window.os_AnimationTimer = function( r, target ) { - this.r = r; - var current = document.getElementById(r.container).offsetWidth; - this.inc = Math.round( ( target - current ) / os_animation_steps ); - if( this.inc < os_animation_min_step && this.inc >=0 ) { - this.inc = os_animation_min_step; // minimal animation step - } - if( this.inc > -os_animation_min_step && this.inc < 0 ) { - this.inc = -os_animation_min_step; - } - this.target = target; -}; - -/****************** - * Initialization - ******************/ - -/** Initialization, call upon page onload */ -window.os_MWSuggestInit = function() { - if ( !window.os_enabled ) { - return; - } - - for( var i = 0; i < os_autoload_inputs.length; i++ ) { - var id = os_autoload_inputs[i]; - var form = os_autoload_forms[i]; - element = document.getElementById( id ); - if( element != null ) { - os_initHandlers( id, form, element ); - } - } -}; - -/* Teardown, called when things like SimpleSearch need to disable MWSuggest */ -window.os_MWSuggestTeardown = function() { - for( var i = 0; i < os_autoload_inputs.length; i++ ) { - var id = os_autoload_inputs[i]; - var form = os_autoload_forms[i]; - element = document.getElementById( id ); - if( element != null ) { - os_teardownHandlers( id, form, element ); - } - } -}; - -/* Call this to disable MWSuggest. Works regardless of whether MWSuggest has been initialized already. */ -window.os_MWSuggestDisable = function() { - window.os_MWSuggestTeardown(); - window.os_enabled = false; -} - - -/** Init Result objects and event handlers */ -window.os_initHandlers = function( name, formname, element ) { - var r = new os_Results( name, formname ); - var formElement = document.getElementById( formname ); - if( !formElement ) { - // Older browsers (Opera 8) cannot get form elements - return; - } - // event handler - os_hookEvent( element, 'keyup', os_eventKeyup ); - os_hookEvent( element, 'keydown', os_eventKeydown ); - os_hookEvent( element, 'keypress', os_eventKeypress ); - if ( !os_use_datalist ) { - // These are needed for the div hack to hide it if the user blurs. - os_hookEvent( element, 'blur', os_eventBlur ); - os_hookEvent( element, 'focus', os_eventFocus ); - // We don't want browser auto-suggestions interfering with our div, but - // autocomplete must be on for datalist to work (at least in Opera - // 10.10). - element.setAttribute( 'autocomplete', 'off' ); - } - // stopping handler - os_hookEvent( formElement, 'submit', os_eventOnsubmit ); - os_map[name] = r; - // toggle link - if( document.getElementById( r.toggle ) == null ) { - // TODO: disable this while we figure out a way for this to work in all browsers - /* if( name == 'searchInput' ) { - // special case: place above the main search box - var t = os_createToggle( r, 'os-suggest-toggle' ); - var searchBody = document.getElementById( 'searchBody' ); - var first = searchBody.parentNode.firstChild.nextSibling.appendChild(t); - } else { - // default: place below search box to the right - var t = os_createToggle( r, 'os-suggest-toggle-def' ); - var top = element.offsetTop + element.offsetHeight; - var left = element.offsetLeft + element.offsetWidth; - t.style.position = 'absolute'; - t.style.top = top + 'px'; - t.style.left = left + 'px'; - element.parentNode.appendChild( t ); - // only now width gets calculated, shift right - left -= t.offsetWidth; - t.style.left = left + 'px'; - t.style.visibility = 'visible'; - } */ - } - -}; - -window.os_teardownHandlers = function( name, formname, element ) { - var formElement = document.getElementById( formname ); - if( !formElement ) { - // Older browsers (Opera 8) cannot get form elements - return; - } - - os_unhookEvent( element, 'keyup', os_eventKeyup ); - os_unhookEvent( element, 'keydown', os_eventKeydown ); - os_unhookEvent( element, 'keypress', os_eventKeypress ); - if ( !os_use_datalist ) { - // These are needed for the div hack to hide it if the user blurs. - os_unhookEvent( element, 'blur', os_eventBlur ); - os_unhookEvent( element, 'focus', os_eventFocus ); - // We don't want browser auto-suggestions interfering with our div, but - // autocomplete must be on for datalist to work (at least in Opera - // 10.10). - element.removeAttribute( 'autocomplete' ); - } - // stopping handler - os_unhookEvent( formElement, 'submit', os_eventOnsubmit ); -}; - - -window.os_hookEvent = function( element, hookName, hookFunct ) { - if ( element.addEventListener ) { - element.addEventListener( hookName, hookFunct, false ); - } else if ( window.attachEvent ) { - element.attachEvent( 'on' + hookName, hookFunct ); - } -}; - -window.os_unhookEvent = function( element, hookName, hookFunct ) { - if ( element.removeEventListener ) { - element.removeEventListener( hookName, hookFunct, false ); - } else if ( element.detachEvent ) { - element.detachEvent( 'on' + hookName, hookFunct ); - } -} - -/******************** - * Keyboard events - ********************/ - -/** Event handler that will fetch results on keyup */ -window.os_eventKeyup = function( e ) { - var targ = os_getTarget( e ); - var r = os_map[targ.id]; - if( r == null ) { - return; // not our event - } - - // some browsers won't generate keypressed for arrow keys, catch it - if( os_keypressed_count == 0 ) { - os_processKey( r, os_cur_keypressed, targ ); - } - var query = targ.value; - os_fetchResults( r, query, os_search_timeout ); -}; - -/** catch arrows up/down and escape to hide the suggestions */ -window.os_processKey = function( r, keypressed, targ ) { - if ( keypressed == 40 && !r.visible && os_timer == null ) { - // If the user hits the down arrow, fetch results immediately if none - // are already displayed. - r.query = ''; - os_fetchResults( r, targ.value, 0 ); - } - // Otherwise, if we're not using datalist, we need to handle scrolling and - // so on. - if ( os_use_datalist ) { - return; - } - if ( keypressed == 40 ) { // Arrow Down - if ( r.visible ) { - os_changeHighlight( r, r.selected, r.selected + 1, true ); - } - } else if ( keypressed == 38 ) { // Arrow Up - if ( r.visible ) { - os_changeHighlight( r, r.selected, r.selected - 1, true ); - } - } else if( keypressed == 27 ) { // Escape - document.getElementById( r.searchbox ).value = r.original; - r.query = r.original; - os_hideResults( r ); - } else if( r.query != document.getElementById( r.searchbox ).value ) { - // os_hideResults( r ); // don't show old suggestions - } -}; - -/** When keys is held down use a timer to output regular events */ -window.os_eventKeypress = function( e ) { - var targ = os_getTarget( e ); - var r = os_map[targ.id]; - if( r == null ) { - return; // not our event - } - - var keypressed = os_cur_keypressed; - - os_keypressed_count++; - os_processKey( r, keypressed, targ ); -}; - -/** Catch the key code (Firefox bug) */ -window.os_eventKeydown = function( e ) { - if ( !e ) { - e = window.event; - } - var targ = os_getTarget( e ); - var r = os_map[targ.id]; - if( r == null ) { - return; // not our event - } - - os_mouse_moved = false; - - os_cur_keypressed = ( e.keyCode == undefined ) ? e.which : e.keyCode; - os_keypressed_count = 0; -}; - - -/** When the form is submitted hide everything, cancel updates... */ -window.os_eventOnsubmit = function( e ) { - var targ = os_getTarget( e ); - - os_is_stopped = true; - // kill timed requests - if( os_timer != null && os_timer.id != null ) { - clearTimeout( os_timer.id ); - os_timer = null; - } - // Hide all suggestions - for( i = 0; i < os_autoload_inputs.length; i++ ) { - var r = os_map[os_autoload_inputs[i]]; - if( r != null ) { - var b = document.getElementById( r.searchform ); - if( b != null && b == targ ) { - // set query value so the handler won't try to fetch additional results - r.query = document.getElementById( r.searchbox ).value; - } - os_hideResults( r ); - } - } - return true; -}; - - - -/** Hide results from the user, either making the div visibility=hidden or - * detaching the datalist from the input. */ -window.os_hideResults = function( r ) { - if ( os_use_datalist ) { - document.getElementById( r.searchbox ).setAttribute( 'list', '' ); - } else { - var c = document.getElementById( r.container ); - if ( c != null ) { - c.style.visibility = 'hidden'; - } - } - r.visible = false; - r.selected = -1; -}; - -window.os_decodeValue = function( value ) { - if ( decodeURIComponent ) { - return decodeURIComponent( value ); - } - if( unescape ) { - return unescape( value ); - } - return null; -}; - -window.os_encodeQuery = function( value ) { - if ( encodeURIComponent ) { - return encodeURIComponent( value ); - } - if( escape ) { - return escape( value ); - } - return null; -}; - -/** Handles data from XMLHttpRequest, and updates the suggest results */ -window.os_updateResults = function( r, query, text, cacheKey ) { - os_cache[cacheKey] = text; - r.query = query; - r.original = query; - if( text == '' ) { - r.results = null; - r.resultCount = 0; - os_hideResults( r ); - } else { - try { - var p = eval( '(' + text + ')' ); // simple json parse, could do a safer one - if( p.length < 2 || p[1].length == 0 ) { - r.results = null; - r.resultCount = 0; - os_hideResults( r ); - return; - } - if ( os_use_datalist ) { - os_setupDatalist( r, p[1] ); - } else { - os_setupDiv( r, p[1] ); - } - } catch( e ) { - // bad response from server or such - os_hideResults( r ); - os_cache[cacheKey] = null; - } - } -}; - -/** - * Create and populate a . - * - * @param r os_Result object - * @param results Array of the new results to replace existing ones - */ -window.os_setupDatalist = function( r, results ) { - var s = document.getElementById( r.searchbox ); - var c = document.getElementById( r.container ); - if ( c == null ) { - c = document.createElement( 'datalist' ); - c.setAttribute( 'id', r.container ); - document.body.appendChild( c ); - } else { - c.innerHTML = ''; - } - s.setAttribute( 'list', r.container ); - - r.results = []; - r.resultCount = results.length; - r.visible = true; - for ( i = 0; i < results.length; i++ ) { - var title = os_decodeValue( results[i] ); - var opt = document.createElement( 'option' ); - opt.value = title; - r.results[i] = title; - c.appendChild( opt ); - } -}; - -/** Fetch namespaces from checkboxes or hidden fields in the search form, - if none defined use wgSearchNamespaces */ -window.os_getNamespaces = function( r ) { - var namespaces = ''; - var elements = document.forms[r.searchform].elements; - for( i = 0; i < elements.length; i++ ) { - var name = elements[i].name; - if( typeof name != 'undefined' && name.length > 2 && name[0] == 'n' && - name[1] == 's' && ( - ( elements[i].type == 'checkbox' && elements[i].checked ) || - ( elements[i].type == 'hidden' && elements[i].value == '1' ) - ) - ) { - if( namespaces != '' ) { - namespaces += '|'; - } - namespaces += name.substring( 2 ); - } - } - if( namespaces == '' ) { - namespaces = mw.config.get( 'wgSearchNamespaces' ).join('|'); - } - return namespaces; -}; - -/** Update results if user hasn't already typed something else */ -window.os_updateIfRelevant = function( r, query, text, cacheKey ) { - var t = document.getElementById( r.searchbox ); - if( t != null && t.value == query ) { // check if response is still relevant - os_updateResults( r, query, text, cacheKey ); - } - r.query = query; -}; - -/** Fetch results after some timeout */ -window.os_delayedFetch = function() { - if( os_timer == null ) { - return; - } - var r = os_timer.r; - var query = os_timer.query; - os_timer = null; - var path = mw.config.get( 'wgMWSuggestTemplate' ).replace( "{namespaces}", os_getNamespaces( r ) ) - .replace( "{dbname}", mw.config.get( 'wgDBname' ) ) - .replace( "{searchTerms}", os_encodeQuery( query ) ); - - // try to get from cache, if not fetch using ajax - var cached = os_cache[path]; - if( cached != null && cached != undefined ) { - os_updateIfRelevant( r, query, cached, path ); - } else { - var xmlhttp = sajax_init_object(); - if( xmlhttp ) { - try { - xmlhttp.open( 'GET', path, true ); - xmlhttp.onreadystatechange = function() { - if ( xmlhttp.readyState == 4 && typeof os_updateIfRelevant == 'function' ) { - os_updateIfRelevant( r, query, xmlhttp.responseText, path ); - } - }; - xmlhttp.send( null ); - } catch ( e ) { - if ( window.location.hostname == 'localhost' ) { - alert( "Your browser blocks XMLHttpRequest to 'localhost', try using a real hostname for development/testing." ); - } - throw e; - } - } - } -}; - -/** Init timed update via os_delayedUpdate() */ -window.os_fetchResults = function( r, query, timeout ) { - if( query == '' ) { - r.query = ''; - os_hideResults( r ); - return; - } else if( query == r.query ) { - return; // no change - } - - os_is_stopped = false; // make sure we're running - - // cancel any pending fetches - if( os_timer != null && os_timer.id != null ) { - clearTimeout( os_timer.id ); - } - // schedule delayed fetching of results - if( timeout != 0 ) { - os_timer = new os_Timer( setTimeout( "os_delayedFetch()", timeout ), r, query ); - } else { - os_timer = new os_Timer( null, r, query ); - os_delayedFetch(); // do it now! - } -}; - -/** Find event target */ -window.os_getTarget = function( e ) { - if ( !e ) { - e = window.event; - } - if ( e.target ) { - return e.target; - } else if ( e.srcElement ) { - return e.srcElement; - } else { - return null; - } -}; - -/** Check if x is a valid integer */ -window.os_isNumber = function( x ) { - if( x == '' || isNaN( x ) ) { - return false; - } - for( var i = 0; i < x.length; i++ ) { - var c = x.charAt( i ); - if( !( c >= '0' && c <= '9' ) ) { - return false; - } - } - return true; -}; - -/** Call this to enable suggestions on input (id=inputId), on a form (name=formName) */ -window.os_enableSuggestionsOn = function( inputId, formName ) { - os_initHandlers( inputId, formName, document.getElementById( inputId ) ); -}; - -/** Call this to disable suggestios on input box (id=inputId) */ -window.os_disableSuggestionsOn = function( inputId ) { - r = os_map[inputId]; - if( r != null ) { - // cancel/hide results - os_timer = null; - os_hideResults( r ); - // turn autocomplete on ! - document.getElementById( inputId ).setAttribute( 'autocomplete', 'on' ); - // remove descriptor - os_map[inputId] = null; - } - - // Remove the element from the os_autoload_* arrays - var index = os_autoload_inputs.indexOf( inputId ); - if ( index >= 0 ) { - os_autoload_inputs[index] = os_autoload_forms[index] = ''; - } -}; - -/************************************************ - * Div-only functions (irrelevant for datalist) - ************************************************/ - -/** Event: loss of focus of input box */ -window.os_eventBlur = function( e ) { - var targ = os_getTarget( e ); - var r = os_map[targ.id]; - if( r == null ) { - return; // not our event - } - if( !os_mouse_pressed ) { - os_hideResults( r ); - // force canvas to stay hidden - r.stayHidden = true; - // cancel any pending fetches - if( os_timer != null && os_timer.id != null ) { - clearTimeout( os_timer.id ); - } - os_timer = null; - } -}; - -/** Event: focus (catch only when stopped) */ -window.os_eventFocus = function( e ) { - var targ = os_getTarget( e ); - var r = os_map[targ.id]; - if( r == null ) { - return; // not our event - } - r.stayHidden = false; -}; - -/** - * Create and populate a
, for non--supporting browsers. - * - * @param r os_Result object - * @param results Array of the new results to replace existing ones - */ -window.os_setupDiv = function( r, results ) { - var c = document.getElementById( r.container ); - if ( c == null ) { - c = os_createContainer( r ); - } - c.innerHTML = os_createResultTable( r, results ); - // init container table sizes - var t = document.getElementById( r.resultTable ); - r.containerTotal = t.offsetHeight; - r.containerRow = t.offsetHeight / r.resultCount; - os_fitContainer( r ); - os_trimResultText( r ); - os_showResults( r ); -}; - -/** Create the result table to be placed in the container div */ -window.os_createResultTable = function( r, results ) { - var c = document.getElementById( r.container ); - var width = c.offsetWidth - os_operaWidthFix( c.offsetWidth ); - var html = ''; - r.results = []; - r.resultCount = results.length; - for( i = 0; i < results.length; i++ ) { - var title = os_decodeValue( results[i] ); - r.results[i] = title; - html += ''; - } - html += '
' + title + '
'; - return html; -}; - -/** Show results div */ -window.os_showResults = function( r ) { - if( os_is_stopped ) { - return; - } - if( r.stayHidden ) { - return; - } - os_fitContainer( r ); - var c = document.getElementById( r.container ); - r.selected = -1; - if( c != null ) { - c.scrollTop = 0; - c.style.visibility = 'visible'; - r.visible = true; - } -}; - -window.os_operaWidthFix = function( x ) { - // For browsers that don't understand overflow-x, estimate scrollbar width - if( typeof document.body.style.overflowX != 'string' ) { - return 30; - } - return 0; -}; - -/** Brower-dependent functions to find window inner size, and scroll status */ -window.f_clientWidth = function() { - return f_filterResults( - window.innerWidth ? window.innerWidth : 0, - document.documentElement ? document.documentElement.clientWidth : 0, - document.body ? document.body.clientWidth : 0 - ); -}; - -window.f_clientHeight = function() { - return f_filterResults( - window.innerHeight ? window.innerHeight : 0, - document.documentElement ? document.documentElement.clientHeight : 0, - document.body ? document.body.clientHeight : 0 - ); -}; - -window.f_scrollLeft = function() { - return f_filterResults( - window.pageXOffset ? window.pageXOffset : 0, - document.documentElement ? document.documentElement.scrollLeft : 0, - document.body ? document.body.scrollLeft : 0 - ); -}; - -window.f_scrollTop = function() { - return f_filterResults( - window.pageYOffset ? window.pageYOffset : 0, - document.documentElement ? document.documentElement.scrollTop : 0, - document.body ? document.body.scrollTop : 0 - ); -}; - -window.f_filterResults = function( n_win, n_docel, n_body ) { - var n_result = n_win ? n_win : 0; - if ( n_docel && ( !n_result || ( n_result > n_docel ) ) ) { - n_result = n_docel; - } - return n_body && ( !n_result || ( n_result > n_body ) ) ? n_body : n_result; -}; - -/** Get the height available for the results container */ -window.os_availableHeight = function( r ) { - var absTop = document.getElementById( r.container ).style.top; - var px = absTop.lastIndexOf( 'px' ); - if( px > 0 ) { - absTop = absTop.substring( 0, px ); - } - return f_clientHeight() - ( absTop - f_scrollTop() ); -}; - -/** Get element absolute position {left,top} */ -window.os_getElementPosition = function( elemID ) { - var offsetTrail = document.getElementById( elemID ); - var offsetLeft = 0; - var offsetTop = 0; - while ( offsetTrail ) { - offsetLeft += offsetTrail.offsetLeft; - offsetTop += offsetTrail.offsetTop; - offsetTrail = offsetTrail.offsetParent; - } - if ( navigator.userAgent.indexOf('Mac') != -1 && typeof document.body.leftMargin != 'undefined' ) { - offsetLeft += document.body.leftMargin; - offsetTop += document.body.topMargin; - } - return { left:offsetLeft, top:offsetTop }; -}; - -/** Create the container div that will hold the suggested titles */ -window.os_createContainer = function( r ) { - var c = document.createElement( 'div' ); - var s = document.getElementById( r.searchbox ); - var pos = os_getElementPosition( r.searchbox ); - var left = pos.left; - var top = pos.top + s.offsetHeight; - c.className = 'os-suggest'; - c.setAttribute( 'id', r.container ); - document.body.appendChild( c ); - - // dynamically generated style params - // IE workaround, cannot explicitely set "style" attribute - c = document.getElementById( r.container ); - c.style.top = top + 'px'; - c.style.left = left + 'px'; - c.style.width = s.offsetWidth + 'px'; - - // mouse event handlers - c.onmouseover = function( event ) { os_eventMouseover( r.searchbox, event ); }; - c.onmousemove = function( event ) { os_eventMousemove( r.searchbox, event ); }; - c.onmousedown = function( event ) { return os_eventMousedown( r.searchbox, event ); }; - c.onmouseup = function( event ) { os_eventMouseup( r.searchbox, event ); }; - return c; -}; - -/** change container height to fit to screen */ -window.os_fitContainer = function( r ) { - var c = document.getElementById( r.container ); - var h = os_availableHeight( r ) - 20; - var inc = r.containerRow; - h = parseInt( h / inc ) * inc; - if( h < ( 2 * inc ) && r.resultCount > 1 ) { // min: two results - h = 2 * inc; - } - if( ( h / inc ) > os_max_lines_per_suggest ) { - h = inc * os_max_lines_per_suggest; - } - if( h < r.containerTotal ) { - c.style.height = h + 'px'; - r.containerCount = parseInt( Math.round( h / inc ) ); - } else { - c.style.height = r.containerTotal + 'px'; - r.containerCount = r.resultCount; - } -}; - -/** If some entries are longer than the box, replace text with "..." */ -window.os_trimResultText = function( r ) { - // find max width, first see if we could expand the container to fit it - var maxW = 0; - for( var i = 0; i < r.resultCount; i++ ) { - var e = document.getElementById( r.resultText + i ); - if( e.offsetWidth > maxW ) { - maxW = e.offsetWidth; - } - } - var w = document.getElementById( r.container ).offsetWidth; - var fix = 0; - if( r.containerCount < r.resultCount ) { - fix = 20; // give 20px for scrollbar - } else { - fix = os_operaWidthFix( w ); - } - if( fix < 4 ) { - fix = 4; // basic padding - } - maxW += fix; - - // resize container to fit more data if permitted - var normW = document.getElementById( r.searchbox ).offsetWidth; - var prop = maxW / normW; - if( prop > os_container_max_width ) { - prop = os_container_max_width; - } else if( prop < 1 ) { - prop = 1; - } - var newW = Math.round( normW * prop ); - if( w != newW ) { - w = newW; - if( os_animation_timer != null ) { - clearInterval( os_animation_timer.id ); - } - os_animation_timer = new os_AnimationTimer( r, w ); - os_animation_timer.id = setInterval( "os_animateChangeWidth()", os_animation_delay ); - w -= fix; // this much is reserved - } - - // trim results - if( w < 10 ) { - return; - } - for( var i = 0; i < r.resultCount; i++ ) { - var e = document.getElementById( r.resultText + i ); - var replace = 1; - var lastW = e.offsetWidth + 1; - var iteration = 0; - var changedText = false; - while( e.offsetWidth > w && ( e.offsetWidth < lastW || iteration < 2 ) ) { - changedText = true; - lastW = e.offsetWidth; - var l = e.innerHTML; - e.innerHTML = l.substring( 0, l.length - replace ) + '...'; - iteration++; - replace = 4; // how many chars to replace - } - if( changedText ) { - // show hint for trimmed titles - document.getElementById( r.resultTable + i ).setAttribute( 'title', r.results[i] ); - } - } -}; - -/** Invoked on timer to animate change in container width */ -window.os_animateChangeWidth = function() { - var r = os_animation_timer.r; - var c = document.getElementById( r.container ); - var w = c.offsetWidth; - var normW = document.getElementById( r.searchbox ).offsetWidth; - var normL = os_getElementPosition( r.searchbox ).left; - var inc = os_animation_timer.inc; - var target = os_animation_timer.target; - var nw = w + inc; - if( ( inc > 0 && nw >= target ) || ( inc <= 0 && nw <= target ) ) { - // finished ! - c.style.width = target + 'px'; - clearInterval( os_animation_timer.id ); - os_animation_timer = null; - } else { - // in-progress - c.style.width = nw + 'px'; - if( document.documentElement.dir == 'rtl' ) { - c.style.left = ( normL + normW + ( target - nw ) - os_animation_timer.target - 1 ) + 'px'; - } - } -}; - -/** Change the highlighted row (i.e. suggestion), from position cur to next */ -window.os_changeHighlight = function( r, cur, next, updateSearchBox ) { - if ( next >= r.resultCount ) { - next = r.resultCount - 1; - } - if ( next < -1 ) { - next = -1; - } - r.selected = next; - if ( cur == next ) { - return; // nothing to do. - } - - if( cur >= 0 ) { - var curRow = document.getElementById( r.resultTable + cur ); - if( curRow != null ) { - curRow.className = 'os-suggest-result'; - } - } - var newText; - if( next >= 0 ) { - var nextRow = document.getElementById( r.resultTable + next ); - if( nextRow != null ) { - nextRow.className = os_HighlightClass(); - } - newText = r.results[next]; - } else { - newText = r.original; - } - - // adjust the scrollbar if any - if( r.containerCount < r.resultCount ) { - var c = document.getElementById( r.container ); - var vStart = c.scrollTop / r.containerRow; - var vEnd = vStart + r.containerCount; - if( next < vStart ) { - c.scrollTop = next * r.containerRow; - } else if( next >= vEnd ) { - c.scrollTop = ( next - r.containerCount + 1 ) * r.containerRow; - } - } - - // update the contents of the search box - if( updateSearchBox ) { - os_updateSearchQuery( r, newText ); - } -}; - -window.os_HighlightClass = function() { - var match = navigator.userAgent.match(/AppleWebKit\/(\d+)/); - if ( match ) { - var webKitVersion = parseInt( match[1] ); - if ( webKitVersion < 523 ) { - // CSS system highlight colors broken on old Safari - // https://bugs.webkit.org/show_bug.cgi?id=6129 - // Safari 3.0.4, 3.1 known ok - return 'os-suggest-result-hl-webkit'; - } - } - return 'os-suggest-result-hl'; -}; - -window.os_updateSearchQuery = function( r, newText ) { - document.getElementById( r.searchbox ).value = newText; - r.query = newText; -}; - - -/******************** - * Mouse events - ********************/ - -/** Mouse over the container */ -window.os_eventMouseover = function( srcId, e ) { - var targ = os_getTarget( e ); - var r = os_map[srcId]; - if( r == null || !os_mouse_moved ) { - return; // not our event - } - var num = os_getNumberSuffix( targ.id ); - if( num >= 0 ) { - os_changeHighlight( r, r.selected, num, false ); - } -}; - -/* Get row where the event occured (from its id) */ -window.os_getNumberSuffix = function( id ) { - var num = id.substring( id.length - 2 ); - if( !( num.charAt( 0 ) >= '0' && num.charAt( 0 ) <= '9' ) ) { - num = num.substring( 1 ); - } - if( os_isNumber( num ) ) { - return parseInt( num ); - } else { - return -1; - } -}; - -/** Save mouse move as last action */ -window.os_eventMousemove = function( srcId, e ) { - os_mouse_moved = true; -}; - -/** Mouse button held down, register possible click */ -window.os_eventMousedown = function( srcId, e ) { - var targ = os_getTarget( e ); - var r = os_map[srcId]; - if( r == null ) { - return; // not our event - } - var num = os_getNumberSuffix( targ.id ); - - os_mouse_pressed = true; - if( num >= 0 ) { - os_mouse_num = num; - // os_updateSearchQuery( r, r.results[num] ); - } - // keep the focus on the search field - document.getElementById( r.searchbox ).focus(); - - return false; // prevents selection -}; - -/** Mouse button released, check for click on some row */ -window.os_eventMouseup = function( srcId, e ) { - var targ = os_getTarget( e ); - var r = os_map[srcId]; - if( r == null ) { - return; // not our event - } - var num = os_getNumberSuffix( targ.id ); - - if( num >= 0 && os_mouse_num == num ) { - os_updateSearchQuery( r, r.results[num] ); - os_hideResults( r ); - document.getElementById( r.searchform ).submit(); - } - os_mouse_pressed = false; - // keep the focus on the search field - document.getElementById( r.searchbox ).focus(); -}; - -/** Toggle stuff seems to be dead code? */ - -/** Return the span element that contains the toggle link */ -window.os_createToggle = function( r, className ) { - var t = document.createElement( 'span' ); - t.className = className; - t.setAttribute( 'id', r.toggle ); - var link = document.createElement( 'a' ); - link.setAttribute( 'href', 'javascript:void(0);' ); - link.onclick = function() { os_toggle( r.searchbox, r.searchform ); }; - var msg = document.createTextNode( wgMWSuggestMessages[0] ); - link.appendChild( msg ); - t.appendChild( link ); - return t; -}; - -/** Call when user clicks on some of the toggle links */ -window.os_toggle = function( inputId, formName ) { - r = os_map[inputId]; - var msg = ''; - if( r == null ) { - os_enableSuggestionsOn( inputId, formName ); - r = os_map[inputId]; - msg = wgMWSuggestMessages[0]; - } else{ - os_disableSuggestionsOn( inputId, formName ); - msg = wgMWSuggestMessages[1]; - } - // change message - var link = document.getElementById( r.toggle ).firstChild; - link.replaceChild( document.createTextNode( msg ), link.firstChild ); -}; - -hookEvent( 'load', os_MWSuggestInit );