Extension:QuickLink

QuickLink provides two efficient ways to search and insert internal links when editing a page:
 * Typing '[[' in the textbox will trigger a search, showing the matching pages in your wiki as you type. Selecting a result inserts a link to the selected page.
 * Also, search can be also triggered by pressing . The search field will be then focused, allowing you to navigate results using the arrow keys and select one using.

You can see a live demo here.

Usage
To perform a search follow these instructions:


 * 1) Go to the search box by:
 * 2) * clicking in the search box
 * 3) * pressing
 * 4) Type a string contained in the title of the page you want to link. It doesn't need to be the first word in the title.
 * 5) When the results are shown:
 * 6) * click one of them
 * 7) * use the arrows and press
 * 8) Cancel search and go back to the textbox anytime by pressing

Also, inline search can be performed while writing in the textbox:


 * 1) Enter the internal link sequence '[[' to trigger the search
 * 2) Type the search string
 * 3) Results will be shown in the search area as you type
 * 4) When search is complete:
 * 5) * Navigate results using the  and   arrows  and insert selected result by pressing   or the   arrow.
 * 6) * Insert any result by clicking on it.
 * 7) Cancel search anytime by pressing , the   arrow key or entering the internal link closing sequence ']]'

Installation

 * 1) Download the latest version from http://subtrama.net/quicklink/.
 * 2) Extract the files and put them in  .  Note: $IP stands for the root directory of your MediaWiki installation, the same directory that holds LocalSettings.php.
 * 3) Add the line   to your LocalSettings.php

Known issues
Caret behaves strangely in IE in version 0.7 when inserting a link.

Changelog

 * Version 0.7
 * (Incomplete) IE support
 * Improved navigation: ']' and arrow keys cancel in-text search
 * Search extended to all namespaces
 * Searchbox appearance in WebKit
 * Version 0.6
 * Search results can be selected with the arrows
 * Pressing enter now selects the highlighted result
 * New theme
 * Version 0.5
 * Search box in the toolbar
 * Search triggered with CTRL+Enter and writing the '[[' sequence in the textarea

QuickLink.php
<?php /** * QuickLink extension * @author Fran Rodríguez * @copyright © 2008 Adam Meyer, © 2009 Francisco J. R. Prados */ //See below under "function wfAjaxQuickLink" for configuration if( !defined( 'MEDIAWIKI' ) ) { echo( "This file is part of an extension to the MediaWiki software and cannot be used standalone.\n" ); die( 1 ); } $wgExtensionFunctions[] = 'wfAjaxLink'; $wgAjaxExportList[] = 'wfAjaxQuickLink'; $wgHooks['AlternateEdit'][] = 'wfAjaxQuickLinkHeaders'; $wgExtensionCredits['other'][] = array(   'name'=> 'QuickLink',    'authors'=> 'Fran Rodríguez',    'version'=> '0.6',    'url' => 'http://www.mediawiki.org/wiki/Extension:QuickLink',    'description'=> 'Edit page built-in search box which provides a very quick and efficient way of inserting internal links.' );

/* function wfQuickLinkNSSearch($str, $arr) { $len = strlen($str);
 * Finds string $str in array $array
 * Used to find namespace name

foreach ($arr as $k => $v) if (!strcasecmp($str, substr($v, 0, $len))) return $k;

return FALSE; } function wfAjaxLink { global $wgUseAjax, $wgOut, $wgTitle; if (!$wgUseAjax) { wfDebug('wfAjaxLink: $wgUseAjax is not enabled, aborting extension setup.'); return; } } function wfAjaxQuickLink( $term ) { //configure the following to change settings $limit = 8; //number of results to spit back $location = 1; //set to 1 to search anywhere in the article name, set to 0 to search only at the begining global $wgCanonicalNamespaceNames, $wgNamespaceAliases; global $wgContLang, $wgOut; $response = new AjaxResponse; $db = wfGetDB( DB_SLAVE ); $l = new Linker; $term = str_replace(' ', '_', $term); // if ':' is found in the search term, limit search to that namespace $use_ns = substr($term, 0, strpos($term, ':')); $search_ns = "TRUE"; if ($use_ns) { $term = preg_replace('/^[^:]+:/', '', $term); $ns_number = wfQuickLinkNSSearch($use_ns, $wgCanonicalNamespaceNames); if ($ns_number === FALSE) $ns_number = wfQuickLinkNSSearch($use_ns, $wgNamespaceAliases); if ($ns_number !== FALSE) { $ons_number = $ns_number; if ($ns_number == NS_MEDIA) $ns_number = NS_FILE; $search_ns = "page_namespace=".$ns_number; }   }

if($location == 1) {		$res = $db->select( 'page', array('page_namespace', 'page_title'),			array( $search_ns, 'page_namespace >=0', "UPPER(CONVERT(page_title USING utf8)) LIKE '%" .$db->strencode( strtoupper ($term)). "%'"				),				"wfSajaxSearch",				array( 'LIMIT' => $limit )			); }	else {				$res = $db->select( 'page', array('page_namespace', 'page_title'),			array( $search_ns, 'page_namespace >=0', "UPPER(CONVERT(page_title USING utf8)) LIKE '". $db->strencode( strtoupper ($term)) ."%'" ),				"wfSajaxSearch",				array( 'LIMIT' => $limit )			); } 	// iterate through the results and create match list $r = ""; $i = 0; while ($row = $db->fetchObject( $res ) ){

$ns = ""; if($row->page_namespace > 0) // For all namespaces except main, add 'namespace:' to link. $ns = $wgCanonicalNamespaceNames[$row->page_namespace].":"; $r .= 'page_title)).'\')">'.$ns.str_replace('_', ' ', $row->page_title). "\n"; $i++; }	if ($r == "") $html = " No results found with the term '$term' Edit to search again | Esc: cancel "; else $html = "$r Click to insert link | Esc: cancel "; return $html; } function wfAjaxQuickLinkHeaders{ global $wgOut; wfAjaxSetQuickLinkHeaders($wgOut); return true; } function wfAjaxSetQuickLinkHeaders($outputPage) { global $wgJsMimeType, $wgStylePath, $wgScriptPath; $outputPage->addLink( 		array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => $wgScriptPath.'/extensions/QuickLink/QuickLink.css' ) 	);	//$outputPage->addScript( ""." \n"); $outputPage->addScript( ""." \n"); $outputPage->addScript( "hookEvent(\"load\", QuickLink.load); \n" ); }

QuickLink.css
margin: 0 0.2em 0 0.2em; display: none; }
 * 1) QuickLinkLabel {

background-image: url('QuickLink.png'); background-repeat: no-repeat; padding-left: 16px; }	text-align: right; display: inline block; float: right; }
 * 1) QuickLinkInput{
 * 1) QuickLinkBox {

/* Spotlight style */ position: absolute; width: 20em; right: 1em; margin-top: 2em;
 * 1) QuickLinkResults {

padding: 0.5em 0 0.5em 0;

/* almost-white transparent background */ background: #fafafa; border: 1px solid #ccc; filter:alpha(opacity=90); -moz-opacity:0.9; -khtml-opacity: 0.9; opacity: 0.9; }

font-style: italic; }
 * 1) QuickLinkResults p {

margin: 0; padding: 0; border: none; list-style: none inside none; }
 * 1) QuickLinkResults p,
 * 2) QuickLinkResults ul,
 * 3) QuickLinkResults li {

padding: 0.1em 0.5em 0.1em 0.5em; }
 * 1) QuickLinkResults p,
 * 2) QuickLinkResults a {

display: block; color: #0066CC; }
 * 1) QuickLinkResults a {

text-decoration: none; color: #0066CC; }
 * 1) QuickLinkResults a:hover {

background-color: #0066CC; color: white; text-decoration: none; }
 * 1) QuickLinkResults a.selected {

QuickLink.js
/* QuickLink.js Copyright Francisco J. R. Prados 2009, Adam Meyer 2007 Thanks to: Simon Litt, Robinson Weijman and Pasi Kallinen for the comments and code */ var QuickLink = new Object;

QuickLink.searchTimeout = 500; QuickLink.timer = 0; QuickLink.counting = false;

QuickLink.searchBuffer = null;

QuickLink.searchBox = null; QuickLink.textarea = null; QuickLink.inputField = null;

QuickLink.caret = 0; QuickLink.linkStart = -1; QuickLink.lastSelectedItem = -1;

QuickLink.ctrlPressed = false;

/* QuickLink.insertLink function This function will be triggered when clicking one of the results returned to the QuickLinkResults div by the wfAjaxQuickLink call QuickLink.insertLink = function(name) {

// if QuickLink.linkStart > 0, the search was triggered by the [[ sequence	// otherwise the search was triggered by the input search box	// QuickLink.caret must be up-to-date:	// -it is updated in onclick textarea event	// -it is updated in onkeydown textarea event

if(QuickLink.linkStart>0) { QuickLink.textarea.value = QuickLink.textarea.value.substr(0, QuickLink.linkStart-2) +  + name +  + QuickLink.textarea.value.substr(QuickLink.caret); QuickLink.setCaretAt(QuickLink.linkStart + name.length + 2); }	else { QuickLink.textarea.value = QuickLink.textarea.value.substr( 0, QuickLink.caret ) +  + name +  + QuickLink.textarea.value.substr(QuickLink.caret); QuickLink.setCaretAt(QuickLink.caret + name.length + 4); QuickLink.inputField.value = ''; }

QuickLink.resetSearchBox; QuickLink.textarea.focus; }

/* OnClick function for textarea OnFocus function for textarea Updates caret, since it needs to be updated (IE forgets carets when textarea goes out of focus) QuickLink.textareaOnClick = function(e) { QuickLink.resetSearchBox; QuickLink.caret = QuickLink.searchCaret; }

/* Init function Creates all the elements neeeded for the search, attachtes the events and calls QuickLink.resetSearchBox QuickLink.load = function {

QuickLink.textarea = document.getElementById( 'wpTextbox1');

// first check if the textbox is readonly // (i.e. in protected pages)

if(!QuickLink.textarea.readOnly) {

var toolbar = document.getElementById("toolbar"); QuickLink.textarea.onkeyup = QuickLink.textSearch; QuickLink.textarea.onkeydown = QuickLink.captureKeys; QuickLink.textarea.onclick = QuickLink.textareaOnClick; QuickLink.textarea.onfocus = QuickLink.textareaOnClick; // create the input search box in toolbar var box = document.createElement("div"); box.id = "QuickLinkBox"; toolbar.appendChild(box); var label = document.createElement("span"); label.innerHTML = "QuickLink"; label.id = "QuickLinkLabel"; box.appendChild(label); QuickLink.inputField = document.createElement("input"); QuickLink.inputField.id = "QuickLinkInput"; QuickLink.inputField.setAttribute("type", "search"); QuickLink.inputField.setAttribute("placeholder", "Type to insert link"); QuickLink.inputField.onkeyup = QuickLink.delayedSearch; box.appendChild(QuickLink.inputField); var help = document.createElement("a"); help.innerHTML = "?"; help.title = "Press Ctrl+Space while writing the article to trigger the search box. Type a string and the matching results will be shown. Press enter to select the first result, or Esc to return to the writing box."; //box.appendChild(help); // create the box for results QuickLink.searchBox = document.createElement("div"); QuickLink.searchBox.id = "QuickLinkResults"; toolbar.appendChild(QuickLink.searchBox);

// reset the search box to the initial state QuickLink.resetSearchBox; } }

QuickLink.searchCall = function(query) {

if(	QuickLink.lastSelectedItem >= 0) { }

//QuickLink.searchBox.innerHTML = " Retrieving results for '"+query+"' ... "; sajax_do_call("wfAjaxQuickLink", [query], QuickLink.AJAXStateChange);

}

/* Callback called when request.readyState = 4 QuickLink.AJAXStateChange = function(request) { QuickLink.searchBox.innerHTML = request.responseText; QuickLink.selectResult(0); }

/* Delayed search method for the QuickLink.inputField.onkeyup event QuickLink.delayedSearch = function(e) {

var event = (e) ? e : window.event; var keyCode = event.keyCode ? event.keyCode : event.charCode ? event.charCode : event.which ? event.which : void 0;

if(keyCode == 27) { QuickLink.resetSearchBox; QuickLink.textarea.focus; }	else if(keyCode == 13) { QuickLink.insertSelectedResult; }	else if(keyCode == 38) { QuickLink.selectPreviousResult; }	else if(keyCode == 40) { QuickLink.selectNextResult; }	else{

if(QuickLink.counting) clearTimeout(QuickLink.timer);

QuickLink.timer = setTimeout("QuickLink.inputFieldSearch",QuickLink.searchTimeout); QuickLink.counting = true; } }

/* Search method for the QuickLink.inputField.onkeyup event QuickLink.inputFieldSearch = function { // reset delayed search QuickLink.counting = false; var query = document.getElementById('QuickLinkInput').value;

if (query != QuickLink.searchBuffer) {

QuickLink.searchBuffer = query;

if(query.length > 0) { QuickLink.initSearchBox;

if (query.length < 30 && query.length > 0 && query.value != "") { QuickLink.searchCall(query); }		}		else QuickLink.resetSearchBox; } }

/* Search method for the QuickLink.textarea.onkeyup event Search will be triggered when typing the sequence [[, or placing the caret by an existing sequence @pre: QuickLink.caret is up to date - it is updated in textarea.onkeydown and textarea.mouseclick events QuickLink.textSearch = function(e) {

var evt = (e) ? e : window.event; var code = evt.keyCode ? evt.keyCode : evt.charCode ? evt.charCode : evt.which ? evt.which : void 0;

if(code == 17) QuickLink.ctrlPressed = false; else if(code == 38 || code == 40) return;

QuickLink.caret = QuickLink.searchCaret;

if(QuickLink.linkStart<0 || QuickLink.caret < QuickLink.linkStart) { if(this.value.charAt(QuickLink.caret-1)=='[' && this.value.charAt(QuickLink.caret-2)=='[') QuickLink.linkStart = QuickLink.caret; else QuickLink.resetSearchBox; }	else if(this.value.charAt(QuickLink.caret-1)==']') { QuickLink.resetSearchBox; }	else { QuickLink.initSearchBox; var searchString = this.value.substr(QuickLink.linkStart,QuickLink.caret-QuickLink.linkStart); //caretxy = vsk_frm_cursor_offset(QuickLink.textarea); //console.log(caretxy); //QuickLink.searchBox.style.position = 'absolute'; //QuickLink.searchBox.style.top = caretxy.y + this.offsetTop + "px"; //QuickLink.searchBox.style.left = caretxy.x + this.offsetLeft + "px"; QuickLink.inputField.value = searchString; QuickLink.searchCall(searchString); } }

/* Key capture method for the QuickLink.textarea.onkeydown event Updates QuickLink.caret, since IE forgets caret when textarea goes out of focus QuickLink.captureKeys = function(e) { var evt = (e) ? e : window.event; var code = evt.keyCode ? evt.keyCode : evt.charCode ? evt.charCode : evt.which ? evt.which : void 0;

switch(code) { // space case 32: if(QuickLink.ctrlPressed) { // trigger search and breack event bubble // update caret and break ctrlPressed flag QuickLink.ctrlPressed = false; QuickLink.caret = QuickLink.searchCaret; QuickLink.inputField.focus; return false; }			break; // enter case 13: if(QuickLink.linkStart>0) { QuickLink.insertSelectedResult; return false; }			break; // up		case 38: if(QuickLink.linkStart>0) { QuickLink.selectPreviousResult; return false; }			break; // right case 39: if(QuickLink.linkStart>0) { QuickLink.insertSelectedResult; return false; }			break; // down case 40: if(QuickLink.linkStart>0) { QuickLink.selectNextResult; return false; }			break; // left case 37: // ']'		case 187: // escape case 27: if(QuickLink.linkStart>0) QuickLink.resetSearchBox; break; case 17: QuickLink.ctrlPressed = true; break; //e.cancelBubble=true; //e.stopPropagation } }

/* Search caret method - IE safe QuickLink.searchCaret = function { var caret = QuickLink.textarea.selectionEnd; if(!caret) { if( document.selection ){ // current selection var range = document.selection.createRange; var stored_range = range.duplicate; // Select all text stored_range.moveToElementText( QuickLink.textarea ); // move 'dummy' end point to end point of original range stored_range.setEndPoint( 'EndToEnd', range ); // calculate caret position caret = stored_range.text.length - range.text.length ; } else { caret = 0; }	} //alert(caret); return caret; }

/* QuickLink.setCaretAt = function(i) {
 * Set caret at position i

if(QuickLink.textarea.setSelectionRange) QuickLink.textarea.setSelectionRange(i,i);

else if(QuickLink.textarea.createTextRange) { var range = document.selection.createRange; var stored_range = range.duplicate; stored_range.moveToElementText( QuickLink.textarea ); stored_range.collapse(true); stored_range.moveStart( 'character', i ); stored_range.moveEnd( 'character', 0 ); stored_range.select; }

}

QuickLink.initSearchBox = function { QuickLink.searchBox.style.visibility = 'visible'; QuickLink.lastSelectedItem = -1; }

QuickLink.resetSearchBox = function { QuickLink.inputField.value = ''; QuickLink.searchBox.style.visibility = 'hidden'; QuickLink.linkStart = -1; QuickLink.lastSelectedItem = -1; }

QuickLink.selectResult = function(item) {

// if QuickLink.lastSelectedItem < 0 then notihng was selected

if(QuickLink.lastSelectedItem >= 0) if(QuickLink.searchBox.getElementsByTagName('a').item(QuickLink.lastSelectedItem)) QuickLink.searchBox.getElementsByTagName('a')[QuickLink.lastSelectedItem].className = 'unselected';

// select an item only if item > -1 // if item is out of range, QuickLink.lastSelectedItem = -1

if(item >= 0) if(QuickLink.searchBox.getElementsByTagName('a').item(item)) { QuickLink.searchBox.getElementsByTagName('a')[item].className = 'selected'; QuickLink.lastSelectedItem = item; }		else { QuickLink.lastSelectedItem = -1; } }

QuickLink.insertSelectedResult = function { var selectedResult = QuickLink.searchBox.getElementsByTagName('a').item(QuickLink.lastSelectedItem); if(selectedResult) selectedResult.onclick; }

QuickLink.selectNextResult = function { var max = QuickLink.searchBox.getElementsByTagName('a').length; if(QuickLink.lastSelectedItem < max-1) { QuickLink.selectResult(QuickLink.lastSelectedItem+1); }	}

QuickLink.selectPreviousResult = function { if(QuickLink.lastSelectedItem > 0) { QuickLink.selectResult(QuickLink.lastSelectedItem-1); }	}

Credits
The code is based in the extension written by Adam Meyer,  Link Suggest. Also thanks to Simon Litt, Robinson Weijman and Pasi Kallinen for the comments and code.