Extension:QuickLink

From MediaWiki.org
Jump to: navigation, search
MediaWiki extensions manual
Crystal Clear action run.png
QuickLink

Release status: beta

Extension QuickLink screenshot.png
Implementation Search, Ajax
Description Adds a fast link search-and-insert feature to the edit page.
Author(s) Fran Rodríguez (Kanortalk)
Latest version 0.7
MediaWiki 1.15
License GPL
Download Get the latest version in http://subtrama.net/quicklink/
Example Subtrama.net
Hooks used
AlternateEdit

Translate the QuickLink extension if it is available at translatewiki.net

Check usage and version matrix; code metrics

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 CTRL+Enter. The search field will be then focused, allowing you to navigate results using the arrow keys and select one using Enter.

You can see a live demo here.

Usage[edit | edit source]

To perform a search follow these instructions:

  1. Go to the search box by:
    • clicking in the search box
    • pressing CTRL+Enter
  2. 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.
  3. When the results are shown:
    • click one of them
    • use the arrows and press Enter
  4. Cancel search and go back to the textbox anytime by pressing ESC

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:
    • Navigate results using the up and down arrows and insert selected result by pressing Enter or the right arrow.
    • Insert any result by clicking on it.
  5. Cancel search anytime by pressing ESC, the left arrow key or entering the internal link closing sequence ']]'

Installation[edit | edit source]

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

Known issues[edit | edit source]

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

Changelog[edit | edit source]

  • 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

coding[edit | edit source]

QuickLink.php[edit | edit source]

<?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.'
);

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

    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 .= '<li><a name="QuickLinkResult'.$i.'" class="unselected" onmouseover="QuickLink.selectResult('.$i.')"  onclick="javascript:QuickLink.insertLink(\''.$ns.addslashes(str_replace('_', ' ', $row->page_title)).'\')">'.$ns.str_replace('_', ' ', $row->page_title). "</a></li>\n";
 		$i++;
	}
	
	if ($r == "")
		$html = "<p>No results found with the term '$term'</p><p>Edit to search again | Esc: cancel</p>";
	else
		$html = "<ul>$r</ul><p>Click to insert link | Esc: cancel</p>";
  
	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( "<script type=\"{$wgJsMimeType}\" src=\"$wgScriptPath/extensions/QuickLink/vegui.sk.formtools.js\">"."</script>\n");
	$outputPage->addScript( "<script type=\"{$wgJsMimeType}\" src=\"$wgScriptPath/extensions/QuickLink/QuickLink.js\">"."</script>\n");
	$outputPage->addScript( "<script type=\"{$wgJsMimeType}\">hookEvent(\"load\", QuickLink.load);</script>\n" );
}

QuickLink.css[edit | edit source]

#QuickLinkLabel {
	margin: 0 0.2em 0 0.2em;
	display: none;
}

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

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

	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;
}

#QuickLinkResults p {
	font-style: italic;
}

#QuickLinkResults p,
#QuickLinkResults ul,
#QuickLinkResults li {
	margin: 0;
	padding: 0;
	border: none;
	list-style: none inside none;
}

#QuickLinkResults p,
#QuickLinkResults a {
	padding: 0.1em 0.5em 0.1em 0.5em;
}

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

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

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

QuickLink.js[edit | edit source]

 /*
 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 = "<p>Retrieving results for '"+query+"' ...</p>";
	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;
}


/*
* Set caret at position i
*/
QuickLink.setCaretAt = function(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);	
	}	
}

QuickLink.png[edit | edit source]

Credits[edit | edit source]

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.

See also[edit | edit source]

  • Link Suggest - the original extension from which this one has derived
  • LinkSuggest, A rewritten Wikia's version of this extension used on all wikias, requires jQuery to work. Superior in that a new edit box is not required, wikilinks and templates are automatically changed in the edit box as the user types.
  • Extension:WikiEditor - Has similar functionality built in. Clicking on the link icon from the tool-bar shows a search box, typing into this will search the available pages. This extension is now included in MediaWiki by default.