MediaWiki:Gadget-Autocomplete.js

/* Autocomplete for links and templates Written by משתמש mw.loader.using(['jquery.ui', 'jquery.textSelection'], function {   //extends jquery with autoCompleteWikiText functionality for autocomplete of links and templates    $.fn.autoCompleteWikiText = function(options) {        var mode = "none",            templateDataCache = {},            ctrl = $(this),            settings = $.extend(true, { positionMy: $('body').is('.rtl') ? "left top" : "right top", // be default, open below the control positionAt: $('body').is('.rtl') ? "left bottom" : "right bottom", positionOf: ctrl, positionOffset: "0", filterResponse: function(a) { return a;           }, // function that expects array of string and returns array of strings menuCSS: { width: 'auto', maxHeight: '30em', 'overflow-y': 'auto' },           itemCSS: { right: 'inherit' },           onselected: function(item) { var pos = ctrl.textSelection('getCaretPosition') - 1, txt = ctrl.val, open, close, caretBackwards;

switch (mode) { case "none": return; case "templateValue": open = "|"; close = ""; caretBackwards = 0; break; case "templateParams": open = "|"; close = "="; caretBackwards = 0; break; case "template": item = item.substr(mw.config.get('wgFormattedNamespaces')[10].length + 1); caretBackwards = 2; open = ""; break; case "link": open = "";                       close = ""; caretBackwards = 0; if (item[item.length - 1] == ')') item += '|';                       break;                }                var lastbegin = txt.lastIndexOf(open, pos);                if (txt[lastbegin + 2] == ':')                    item = ':' + item;

var newTxt = txt.substr(0, lastbegin) + open + item + close + txt.substr(pos + 1); var orgScroll = ctrl.scrollTop; ctrl.val(newTxt); ctrl.textSelection('setSelection', {                   start: lastbegin + (open + item + close).length - caretBackwards                }); ctrl.scrollTop(orgScroll); }       }, options);

function findLinks(res) { var pos = ctrl.textSelection('getCaretPosition') - 1; var txt = ctrl.val;

var lastbegin = txt.lastIndexOf("", pos);           var lastend = txt.lastIndexOf("", pos); var isLink = lastbegin > lastend; if (isLink) { mode = 'link'; fillLinksList(res, txt.substr(lastbegin + 2, pos - lastbegin)); } else { lastbegin = txt.lastIndexOf("", pos); var isTemplate = lastbegin > lastend; if (isTemplate) { var prefixName = mw.config.get('wgFormattedNamespaces')[10] + ':' + txt.substr(lastbegin + 2, pos - lastbegin - 1); mode = (prefixName.indexOf('|') > -1) ? 'templateParams' : 'template'; fillLinksList(res, prefixName); } else { mode = "none"; res([]); }           }        }

function resolveTempalte(templateName) { var dfd = new jQuery.Deferred; if (!templateName) return dfd.reject.promise; if (templateDataCache[templateName]) return dfd.resolve.promise; var api = new mw.Api; api.get({               action: 'templatedata',                titles: templateName,                redirects: 1            }).done(function(data) {                if (!data.pages) return dfd.reject;                for (var pageid in data.pages) {                    templateDataCache[templateName] = data.pages[pageid];                    dfd.resolve;                }                if (!templateDataCache[templateName]) dfd.reject;            }); return dfd.promise; }

function resolveApi(queryType, queryValue) { var dfd = new jQuery.Deferred, api = new mw.Api;

switch (queryType) { case 'users': api.get({                       action: 'query',                        list: 'allusers',                        auactiveusers: 1,                        auprefix: queryValue                    }).done(function(data) {                        if (data && data.query && data.query.allusers) dfd.resolve($.map(data.query.allusers, function(e) {                            return e.name;                        }));                        else dfd.reject;                    }); break; case 'pages': api.get({                       action: 'opensearch',                        search: queryValue                    }).done(function(data) {                        if (data[1]) dfd.resolve(settings.filterResponse(data[1]));                        else dfd.reject;                    }); break; default: throw 'unexpected queryType'; }

return dfd.promise; }

function fillLinksList(res, txt) { txt = $.trim(txt); if (txt.length <= 1 || (mode != 'templateParams' && txt.indexOf('|') > -1) || (txt.indexOf('#') > -1 && mw.config.get('wgNamespaceNumber') === 0)) res([]); else if (mode === 'templateParams') { var templateMatch = /(.+?)\|(?:.*\|)?([^=]+$)/.exec(txt); $.when(resolveTempalte(templateMatch && templateMatch[1])).done(function {                   var curTemplateData = templateDataCache[templateMatch[1]],                    	suggestions = [],                    	curParamIndex = txt.split('|').length - 1;                    for (var paramName in curTemplateData.params) {                        if (paramName == curParamIndex) {                            var paramValue = templateMatch[2];                            var dfd;                            switch (curTemplateData.params[paramName].type) {                                case 'wiki-page-name':                                    dfd = $.when(resolveApi('pages', paramValue));                                    mode = 'templateValue';                                    break;                                case 'wiki-file-name':                                    dfd = $.when(resolveApi('pages', 'File:' + paramValue)); mode = 'templateValue'; break; case 'wiki-template-name': dfd = $.when(resolveApi('pages', 'Template:' + paramValue)); mode = 'templateValue'; break; case 'wiki-user-name': dfd = $.when(resolveApi('users', paramValue)); mode = 'templateValue'; break; default: return res([]); // dont suggest for this indexed param }                           return dfd.done(res).fail(function {                                res([]);                            }); }                       if (paramName === '1' || txt.indexOf(paramName) > -1) continue; //dont suggest used params suggestions.push(paramName); }                   res(suggestions); }).fail(res);           } else if (txt.indexOf('#') > -1) {                var pageTitle = txt.substr(0, txt.indexOf('#'));                var sectionPrefix = txt.substr(txt.indexOf('#') + 1);                var api = new mw.Api;                api.get({ action: 'parse', page: pageTitle, prop: 'sections' }).done(function(data) { if (data && data.parse && data.parse.sections) res($(data.parse.sections).map(function { return this.line.indexOf(sectionPrefix) == 0 ? (pageTitle + '#' + this.line.replace(/[|\[\]\{\}]/g, escape)) : null; }));               });            } else {                $.when(resolveApi('pages', txt)).done(res).fail(function { res([]) });           }        }

ctrl.autocomplete({           source: function(request, response) {                if (fixArrowsBug(this))                    response([]);                else                    findLinks(response);            },            focus: function {                return false;            },            select: function(e, ui) {                settings.onselected(ui.item.value);                return false;            },            open: function {                $(".ui-autocomplete")                    .css(settings.menuCSS)                    .position({ my: settings.positionMy, at: settings.positionAt, of: settings.positionOf, offset: settings.positionOffset, collision: 'none fit' })                   .find('li').css(settings.itemCSS);            }        }); var fixed, stfu, escapes = 0; //this is hack to prevent known serious bug in autocomplete.js that prevent default of the up and down key which may drive you crazy.... function fixArrowsBug(self) { if (fixed) return false; fixed = true;

// on click selection may change. close the menu ctrl.on("click.autocomplete", function(e){		clearTimeout(self.searching);		self.close;	  }); ctrl.off("keydown.autocomplete"); ctrl.off("keydown.autocomplete0"); ctrl.on("keydown.autocomplete",               function(event) {                    var keyCode = $.ui.keyCode;                    // hack to allow cancelling the gadget: mostly useful when editing templates.                    escapes = event.keyCode == keyCode.ESCAPE ? escapes + 1 : 0;                    if ( stfu || ( stfu = escapes >= 3 ) ) {                        self.close(event);                    	return;                    }                    switch (event.keyCode) {                        case keyCode.PAGE_UP:                            self._move("previousPage", event);                            break;                        case keyCode.PAGE_DOWN:                            self._move("nextPage", event);                            break;                        case keyCode.UP:                            if (!self.menu.element.is(":visible")) return;                            self._move("previous", event); // prevent moving cursor to beginning of text field in some browsers event.preventDefault; break; case keyCode.DOWN: if (!self.menu.element.is(":visible")) return; self._move("next", event); // prevent moving cursor to end of text field in some browsers event.preventDefault; break; case keyCode.ENTER: case keyCode.NUMPAD_ENTER: // when menu is open or has focus if (self.menu.active) { event.preventDefault; }                           //passthrough - ENTER and TAB both select the current element case keyCode.TAB: if (!self.menu.active) { return; }                           self.menu.select(event); break; case keyCode.ESCAPE: self.element.val(self.term); self.close(event); break;

case keyCode.SHIFT: case keyCode.CONTROL: case keyCode.ALT: case keyCode.COMMAND: case keyCode.COMMAND_RIGHT: case keyCode.INSERT: case keyCode.CAPS_LOCK: case keyCode.END: case keyCode.HOME: case keyCode.LEFT: case keyCode.RIGHT: // ignore metakeys (shift, ctrl, alt) break; default: // keypress is triggered before the input value is changed clearTimeout(self.searching); self.searching = setTimeout(function {                               self.search(null, event);                            }, self.options.delay); break; }               });            return true;        }    } });

if ($.inArray(mw.config.get('wgAction'), ['edit', 'submit']) + 1) mw.loader.using(['jquery.ui', 'jquery.textSelection'], function {       //enable autocomplete for editbox, relative to editform in an offset of -80 vertical        $("#wpTextbox1").autoCompleteWikiText({ positionAt: $('#wpTextbox1').prop('dir') == 'rtl' ? "left top" : "right top", positionOf: '#editform', positionOffset: "0 0", menuCSS: { background: '#E0EEF7', opacity: 0.8 },           itemCSS: { padding: 0, margin: 0 }       });    });