Extension:QuickLink

From MediaWiki.org
Jump to: navigation, search
MediaWiki extensions manual - list
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 possible

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.