Ajax based suggest feature for the search box
authorJens Frank <jeluf@users.mediawiki.org>
Sun, 26 Mar 2006 19:03:14 +0000 (19:03 +0000)
committerJens Frank <jeluf@users.mediawiki.org>
Sun, 26 Mar 2006 19:03:14 +0000 (19:03 +0000)
README
RELEASE-NOTES
ajax.php [new file with mode: 0644]
includes/AjaxFunctions.php [new file with mode: 0644]
includes/Article.php
includes/DefaultSettings.php
includes/OutputPage.php
languages/Messages.php
languages/MessagesDe.php
skins/MonoBook.php
skins/common/ajax.js [new file with mode: 0644]

diff --git a/README b/README
index d487a2f..051b947 100644 (file)
--- a/README
+++ b/README
@@ -49,6 +49,16 @@ not funded any of the development work.
 also released into the public domain, which does not impair the obligations of
 users under the GPL for use of the whole code or other sections thereof.
 
+[2] MediaWiki makes use of the Sajax Toolkit by modernmethod,
+       http://www.modernmethod.com/sajax/
+    which has the following license:
+
+       'This work is licensed under the Creative Commons Attribution
+        License. To view a copy of this license, visit
+        http://creativecommons.org/licenses/by/2.0/ or send a letter
+        to Creative Commons, 559 Nathan Abbott Way,
+        Stanford, California 94305, USA.'
+
 Many thanks to the Wikipedia regulars for testing and suggestions.
 
 The official website for mediawiki is located at:
index ed1c919..7097d2c 100644 (file)
@@ -717,6 +717,8 @@ fully support the editing toolbar, but was found to be too confusing.
 * (bug 4729) Add user preference that marks a user's edits as patrolled if user is able to
 * (bug 4497,4704,5010) Added some new language codes.
 * (bug 4630) Add user preference to prompt users when entering blank edit summaries
+* Added optional suggest feature for the search box. Set wgUseAjax to true to
+  enable it.
 
 === Caveats ===
 
diff --git a/ajax.php b/ajax.php
new file mode 100644 (file)
index 0000000..933134f
--- /dev/null
+++ b/ajax.php
@@ -0,0 +1,71 @@
+<?php
+
+$wgRequestTime = microtime();
+
+unset( $IP );
+@ini_set( 'allow_url_fopen', 0 ); # For security...
+
+# Valid web server entry point, enable includes.
+# Please don't move this line to includes/Defines.php. This line essentially defines
+# a valid entry point. If you put it in includes/Defines.php, then any script that includes
+# it becomes an entry point, thereby defeating its purpose.
+define( 'MEDIAWIKI', true );
+require_once( './includes/Defines.php' );
+require_once( './LocalSettings.php' );
+require_once( 'includes/Setup.php' );
+require_once( 'includes/AjaxFunctions.php' );
+
+if ( ! $wgUseAjax ) {
+       die ( -1 );
+}
+
+wfProfileIn( 'main-misc-setup' );
+
+header( 'Content-Type: text/html; charset=utf-8', true );
+
+// List of exported PHP functions
+$sajax_export_list = array( 'wfSajaxSearch' );
+
+$mode = "";
+
+$wgAjaxCachePolicy = new AjaxCachePolicy();
+
+if (! empty($_GET["rs"])) {
+       $mode = "get";
+}
+
+if (!empty($_POST["rs"])) {
+       $mode = "post";
+}
+
+if (empty($mode)) {
+       return;
+}
+
+if ($mode == "get") {
+       $func_name = $_GET["rs"];
+       if (! empty($_GET["rsargs"])) {
+               $args = $_GET["rsargs"];
+       } else {
+               $args = array();
+       }
+} else {
+       $func_name = $_POST["rs"];
+       if (! empty($_POST["rsargs"])) {
+               $args = $_POST["rsargs"];
+       } else {
+               $args = array();
+       }
+}
+
+if (! in_array($func_name, $sajax_export_list)) {
+       echo "-:$func_name not callable";
+} else {
+       echo "+:";
+       $result = call_user_func_array($func_name, $args);
+       $wgAjaxCachePolicy->writeHeader();
+       echo $result;
+}
+exit;
+
+?>
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php
new file mode 100644 (file)
index 0000000..5d95820
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+
+if( !defined( 'MEDIAWIKI' ) )
+        die( -1 );
+
+require_once('WebRequest.php');
+
+/**
+ * Function converts an Javascript escaped string back into a string with specified charset (default is UTF-8).
+ * Modified function from http://pure-essence.net/stuff/code/utf8RawUrlDecode.phps
+ *
+ * @param string $source escaped with Javascript's escape() function
+ * @param string $iconv_to destination character set will be used as second paramether in the iconv function. Default is UTF-8.
+ * @return string
+ */
+function js_unescape($source, $iconv_to = 'UTF-8') {
+   $decodedStr = '';
+   $pos = 0;
+   $len = strlen ($source);
+   while ($pos < $len) {
+       $charAt = substr ($source, $pos, 1);
+       if ($charAt == '%') {
+           $pos++;
+           $charAt = substr ($source, $pos, 1);
+           if ($charAt == 'u') {
+               // we got a unicode character
+               $pos++;
+               $unicodeHexVal = substr ($source, $pos, 4);
+               $unicode = hexdec ($unicodeHexVal);
+               $decodedStr .= code2utf($unicode);
+               $pos += 4;
+           }
+           else {
+               // we have an escaped ascii character
+               $hexVal = substr ($source, $pos, 2);
+               $decodedStr .= chr (hexdec ($hexVal));
+               $pos += 2;
+           }
+       }
+       else {
+           $decodedStr .= $charAt;
+           $pos++;
+       }
+   }
+
+   if ($iconv_to != "UTF-8") {
+       $decodedStr = iconv("UTF-8", $iconv_to, $decodedStr);
+   }
+  
+   return $decodedStr;
+}
+
+/**
+ * Function coverts number of utf char into that character.
+ * Function taken from: http://sk2.php.net/manual/en/function.utf8-encode.php#49336
+ *
+ * @param int $num
+ * @return utf8char
+ */
+function code2utf($num){
+   if ( $num<128 )
+       return chr($num);
+   if ( $num<2048 )
+       return chr(($num>>6)+192).chr(($num&63)+128);
+   if ( $num<65536 )
+       return chr(($num>>12)+224).chr((($num>>6)&63)+128).chr(($num&63)+128);
+   if ( $num<2097152 )
+       return chr(($num>>18)+240).chr((($num>>12)&63)+128).chr((($num>>6)&63)+128) .chr(($num&63)+128);
+   return '';
+}
+
+class AjaxCachePolicy {
+       var $policy;
+
+       function AjaxCachePolicy( $policy = null ) {
+               $this->policy = $policy;
+       }
+
+       function setPolicy( $policy ) {
+               $this->policy = $policy;
+       }
+
+       function writeHeader() {
+               header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
+               if ( is_null( $this->policy ) ) {
+                       // Bust cache in the head
+                       header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    // Date in the past
+                       // always modified
+                       header ("Cache-Control: no-cache, must-revalidate");  // HTTP/1.1
+                       header ("Pragma: no-cache");                          // HTTP/1.0
+               } else {
+                       header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->policy ) . " GMT");
+                       header ("Cache-Control: public,max-age={$this->policy}");
+               }
+       }
+}
+                       
+
+function wfSajaxSearch( $term ) {
+       global $wgContLang, $wgUser, $wgRequest, $wgAjaxCachePolicy;
+       $limit = 16;
+       
+       $l = new Linker;
+
+       $term = str_replace( ' ', '_', $wgContLang->ucfirst( 
+                       $wgContLang->checkTitleEncoding( $wgContLang->recodeInput( js_unescape( $term ) ) )
+               ) );
+
+       if ( strlen( str_replace( '_', '', $term ) )<3 )
+               return;
+
+       $wgAjaxCachePolicy->setPolicy( 30*60 );
+
+       $db =& wfGetDB( DB_SLAVE );
+       $res = $db->select( 'page', 'page_title',
+                       array(  'page_namespace' => 0,
+                               "page_title LIKE '". $db->strencode( $term) ."%'" ),
+                               "wfSajaxSearch",
+                               array( 'LIMIT' => $limit+1 )
+                       );
+
+       $r = "";
+
+       $i=0;
+       while ( ( $row = $db->fetchObject( $res ) ) && ( ++$i <= $limit ) ) {
+               $nt = Title::newFromDBkey( $row->page_title );
+               $r .= '<li>' . $l->makeKnownLinkObj( $nt ) . "</li>\n";
+       }
+       if ( $i > $limit ) {
+               $more = '<i>' .  $l->makeKnownLink( $wgContLang->specialPage( "Allpages" ),
+                                               wfMsg('moredotdotdot'),
+                                               "namespace=0&from=" . wfUrlEncode ( $term ) ) .
+                       '</i>';
+       } else {
+               $more = '';
+       }
+
+       $term = htmlspecialchars( $term );
+       return '<div style="float:right; border:solid 1px black;background:gainsboro;padding:2px;"><a onclick="Searching_Hide_Results();">' 
+               . wfMsg( 'hideresults' ) . '</a></div>'
+               . '<h1 class="firstHeading">'.wfMsg('search')
+               . '</h1><div id="contentSub">'.wfMsg('searchquery', $term) . '</div><ul><li>'
+               . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ),
+                                       wfMsg( 'searchcontaining', $term ),
+                                       "search=$term&fulltext=Search" )
+               . '</li><li>' . $l->makeKnownLink( $wgContLang->specialPage( 'Search' ),
+                                       wfMsg( 'searchnamed', $term ) ,
+                                       "search=$term&go=Go" )
+               . "</li></ul><h2>" . wfMsg( 'articletitles', $term ) . "</h2>"
+               . '<ul>' .$r .'</ul>'.$more;
+}
+
+?>
index 2d00bf1..210f775 100644 (file)
@@ -490,7 +490,7 @@ class Article {
                        }
                        $revision = Revision::newFromId( $this->mLatest );
                        if( is_null( $revision ) ) {
-                               wfDebug( "$fname failed to retrieve current page, rev_id $data->page_latest\n" );
+                               wfDebug( "$fname failed to retrieve current page, rev_id {$data->page_latest}\n" );
                                return false;
                        }
                }
index e63ff14..43d0c57 100644 (file)
@@ -1897,5 +1897,10 @@ $wgJobRunRate = 1;
  */
 $wgJobLogFile = false;
 
+/**
+ * Enable use of AJAX features, currently auto suggestion for the search bar
+ */
+$wgUseAjax = false;
+
 
 ?>
index b90faf8..aea3fef 100644 (file)
@@ -460,6 +460,7 @@ class OutputPage {
        function output() {
                global $wgUser, $wgOutputEncoding;
                global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType, $wgProfiler;
+               global $wgJsMimeType, $wgStylePath, $wgUseAjax, $wgScriptPath, $wgServer;
 
                if( $this->mDoNothing ){
                        return;
@@ -468,6 +469,14 @@ class OutputPage {
                wfProfileIn( $fname );
                $sk = $wgUser->getSkin();
 
+               if ( $wgUseAjax ) {
+                       $this->addScript( "<script type=\"{$wgJsMimeType}\">
+                               var wgScriptPath=\"{$wgScriptPath}\";
+                               var wgServer=\"{$wgServer}\";
+                       </script>" );
+                       $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/ajax.js\"></script>\n" );
+               }
+
                if ( '' != $this->mRedirect ) {
                        if( substr( $this->mRedirect, 0, 4 ) != 'http' ) {
                                # Standards require redirect URLs to be absolute
index 4c587c0..ad14e2c 100644 (file)
@@ -1986,6 +1986,10 @@ Please confirm that really want to recreate this article.',
 
 'youhavenewmessagesmulti' => "You have new messages on $1",
 'newtalkseperator' => ',_',
+'searchcontaining' => "Search for articles containing ''$1''.",
+'searchnamed' => "Search for articles named ''$1''.",
+'articletitles' => "Articles starting with ''$1''",
+'hideresults' => 'Hide results',
 
 );
 
index 46f35e6..76f041c 100644 (file)
@@ -1127,9 +1127,14 @@ $3
 Wenn Sie *nicht* $2 sind, folgen Sie dem Link bitte nicht.
 
 Der Bestätigungskode läuft am $4 ab.
-"
+",
+'searchcontaining' => "Suche nach Artikeln, in denen ''$1'' vorkommt.",
+'searchnamed' => "Suche nach Artikeln, deren Name ''$1'' enthält.",
+'articletitles' => "Artikel, die mit ''$1'' beginnen",
+'hideresults' => 'Verbergen',
+
 
 );
 
 
-?>
\ No newline at end of file
+?>
index 72b6006..2200f55 100644 (file)
@@ -55,7 +55,6 @@ class MonoBookTemplate extends QuickTemplate {
        <head>
                <meta http-equiv="Content-Type" content="<?php $this->text('mimetype') ?>; charset=<?php $this->text('charset') ?>" />
                <?php $this->html('headlinks') ?>
-               <?php $this->html('headscripts') ?>
                <title><?php $this->text('pagetitle') ?></title>
                <style type="text/css" media="screen,projection">/*<![CDATA[*/ @import "<?php $this->text('stylepath') ?>/<?php $this->text('stylename') ?>/main.css?5"; /*]]>*/</style>
                <link rel="stylesheet" type="text/css" <?php if(empty($this->data['printable']) ) { ?>media="print"<?php } ?> href="<?php $this->text('stylepath') ?>/common/commonPrint.css" />
@@ -82,6 +81,8 @@ class MonoBookTemplate extends QuickTemplate {
                <script type="<?php $this->text('jsmimetype') ?>"><?php $this->html('userjsprev') ?></script>
 <?php  }
                if($this->data['trackbackhtml']) print $this->data['trackbackhtml']; ?>
+               <!-- Head Scripts -->
+               <?php $this->html('headscripts') ?>
        </head>
 <body <?php if($this->data['body_ondblclick']) { ?>ondblclick="<?php $this->text('body_ondblclick') ?>"<?php } ?>
 <?php if($this->data['body_onload'    ]) { ?>onload="<?php     $this->text('body_onload')     ?>"<?php } ?>
diff --git a/skins/common/ajax.js b/skins/common/ajax.js
new file mode 100644 (file)
index 0000000..b1f3e05
--- /dev/null
@@ -0,0 +1,177 @@
+// remote scripting library
+// (c) copyright 2005 modernmethod, inc
+var sajax_debug_mode = false;
+var sajax_request_type = "GET";
+
+var started;
+var typing;
+var memory=null;
+var body=null;
+var oldbody=null;
+
+function sajax_debug(text) {
+       if (sajax_debug_mode)
+               alert("RSD: " + text)
+}
+
+
+function sajax_init_object() {
+       sajax_debug("sajax_init_object() called..")
+       var A;
+       try {
+               A=new ActiveXObject("Msxml2.XMLHTTP");
+       } catch (e) {
+               try {
+                       A=new ActiveXObject("Microsoft.XMLHTTP");
+               } catch (oc) {
+                       A=null;
+               }
+       }
+       if(!A && typeof XMLHttpRequest != "undefined")
+               A = new XMLHttpRequest();
+       if (!A)
+               sajax_debug("Could not create connection object.");
+       return A;
+}
+
+
+function sajax_do_call(func_name, args) {
+       var i, x, n;
+       var uri;
+       var post_data;
+       uri = wgServer + "/" + wgScriptPath + "/ajax.php";
+       if (sajax_request_type == "GET") {
+               if (uri.indexOf("?") == -1)
+                       uri = uri + "?rs=" + escape(func_name);
+               else
+                       uri = uri + "&rs=" + escape(func_name);
+               for (i = 0; i < args.length-1; i++)
+                       uri = uri + "&rsargs[]=" + escape(args[i]);
+               uri = uri + "&rsrnd=" + new Date().getTime();
+               post_data = null;
+       } else {
+               post_data = "rs=" + escape(func_name);
+               for (i = 0; i < args.length-1; i++)
+                       post_data = post_data + "&rsargs[]=" + escape(args[i]);
+       }
+       x = sajax_init_object();
+       x.open(sajax_request_type, uri, true);
+       if (sajax_request_type == "POST") {
+               x.setRequestHeader("Method", "POST " + uri + " HTTP/1.1");
+               x.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+       }
+       x.setRequestHeader("Pragma", "cache=yes");
+       x.setRequestHeader("Cache-Control", "no-transform");
+       x.onreadystatechange = function() {
+               if (x.readyState != 4)
+                       return;
+               sajax_debug("received " + x.responseText);
+               var status;
+               var data;
+               status = x.responseText.charAt(0);
+               data = x.responseText.substring(2);
+               if (status == "-")
+                       alert("Error: " + data);
+               else
+                       args[args.length-1](data);
+       }
+       x.send(post_data);
+       sajax_debug(func_name + " uri = " + uri + "/post = " + post_data);
+       sajax_debug(func_name + " waiting..");
+       delete x;
+}
+
+// Remove the typing barrier to allow call() to complete
+function Search_doneTyping()
+{
+       typing=false;
+}
+
+// Wait 500ms to run call()
+function Searching_Go()
+{
+        setTimeout("Searching_Call()", 500);
+}
+
+// If the user is typing wait until they are done.
+function Search_Typing() {
+       started=true;
+       typing=true;
+       window.status = "Waiting until you're done typing...";
+       setTimeout("Search_doneTyping()", 500);
+
+       // I believe these are needed by IE for when the users press return?
+       if (window.event)
+       {
+               if (event.keyCode == 13)
+               {
+                       event.cancelBubble = true;
+                       event.returnValue = false;
+               }
+       }
+}
+
+// Set the body div to the results
+function Searching_SetResult(result)
+{
+        //body.innerHTML = result;
+       t = document.getElementById("searchTarget");
+       if ( t == null ) {
+               oldbody=body.innerHTML;
+               body.innerHTML= '<div id="searchTargetContainer"><div id="searchTarget" ></div></div>' ;
+               t = document.getElementById("searchTarget");
+       }
+       t.innerHTML = result;
+       t.style.display='block';
+}
+
+function Searching_Hide_Results()
+{
+       t = document.getElementById("searchTarget");
+       t.style.display='none';
+       body.innerHTML = oldbody;
+}
+
+
+// This will call the php function that will eventually
+// return a results table
+function Searching_Call()
+{
+       var x;
+       Searching_Go();
+
+       //Don't proceed if user is typing
+       if (typing)
+               return;
+
+       x = document.getElementById("searchInput").value;
+
+       // Don't search again if the query is the same
+       if (x==memory)
+               return;
+
+       memory=x;
+       if (started) {
+               // Don't search for blank or < 3 chars.
+               if ((x=="") || (x.length < 3))
+               {
+                       return;
+               }
+               x_wfSajaxSearch(x, Searching_SetResult);
+       }
+}
+
+function x_wfSajaxSearch() {
+       sajax_do_call( "wfSajaxSearch", x_wfSajaxSearch.arguments );
+}
+
+       
+//Initialize
+function sajax_onload() {
+       x = document.getElementById( 'searchInput' );
+       x.onkeypress= function() { Search_Typing(); };
+       Searching_Go();
+       body = document.getElementById("content");
+}
+
+hookEvent("load", sajax_onload);