User:Nx/Scripts/LinkSuggest.js
From RationalWiki
Note: After saving, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Clear the cache in Tools → Preferences
/* Wikilink autocompletion, based on Wikia's LinkSuggest extension */ function getX(element) { var curX = 0; if (obj.offsetParent) { do { curX += obj.offsetLeft; } while (obj = obj.offsetParent); } return curX; } function getY(element) { var curY = 0; if (obj.offsetParent) { do { curY += obj.offsetTop; } while (obj = obj.offsetParent); } return curY; } function getStyle(el,styleProp) { if (window.getComputedStyle) var y = document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp); else if (el.currentStyle) var y = el.currentStyle[styleProp]; return y; } LinkSuggest = function(_textbox) { this.textbox = _textbox; this.dropdown = document.createElement('div'); this.dropdown.style.position = "absolute"; this.dropdown.style.display = "none"; this.dropdown.style.backgroundColor = "lightgrey"; this.dropdown.className = "LinkSuggest_dropdown"; var _this = this; addHandler(this.dropdown,"mouseover",function(event) { _this.mouseOver(event); }); addHandler(this.dropdown,"mouseout",function(event) { _this.mouseOut(event); }); addHandler(this.dropdown,"click",function(event) { _this.click(event); }); var dropdownUl = document.createElement('ul'); this.dropdown.appendChild(dropdownUl); //this.dropdown = this.textbox.parentNode.appendChild(this.dropdown); this.imagePreview = document.createElement('div'); this.imagePreview.style.position = "absolute"; this.imagePreview.className = "LinkSuggest_ImagePreview"; this.imagePreview.style.display = "none"; this.imagePreviewImg = document.createElement('img'); this.imagePreviewImg.src = ""; this.imagePreviewImg.style.display = "none"; this.imagePreviewImg = this.imagePreview.appendChild(this.imagePreviewImg); //this.imagePreview = this.textbox.parentNode.appendChild(this.imagePreview); this.test = document.createElement("pre"); //this.test = this.textbox.parentNode.insertBefore(this.test, this.textbox); this.test.style.visibility = "hidden"; this.test.style.whiteSpace = "pre-wrap"; this.test.style.position = "absolute"; //this.test.style.left = "0"; //this.test.style.top = "0"; this.originalQuery = ""; this.containerOpen = false; this.selectedIndex = -1; this.numItems = 0; this.results = []; this.previewTimer = null; this.isIgnoreKey = function(nKeyCode) { if ((nKeyCode == 9) || (nKeyCode == 13) || // tab, enter (nKeyCode == 16) || (nKeyCode == 17) || // shift, ctl (nKeyCode >= 18 && nKeyCode <= 20) || // alt,pause/break,caps lock (nKeyCode == 27) || // esc //(nKeyCode >= 33 && nKeyCode <= 35) || // page up,page down,end //(nKeyCode >= 36 && nKeyCode <= 40) || // home,left,up,down,right //(nKeyCode == 40) || // down (nKeyCode >= 44 && nKeyCode <= 45)) { // print screen,insert return true; } return false; }; this.keydown = function(event) { switch (event.keyCode) { case 13: // enter if (this.containerOpen) { if (this.selectedIndex >= 0 && this.selectedIndex < this.numItems) { event.stopPropagation(); event.preventDefault(); this.autoComplete(); } else { this.toggleContainer(false); } } break; case 27: // esc this.toggleContainer(false); return; case 38: // up if (this.containerOpen) { event.stopPropagation(); event.preventDefault(); this.moveSelection(event.keyCode); } break; case 40: // down if (this.containerOpen) { event.stopPropagation(); event.preventDefault(); this.moveSelection(event.keyCode); } break; } }; this.keyup = function(event) { if (this.isIgnoreKey(event.keyCode)) { return; } var text = this.textbox.value.replace(/\r/g, ""); var caret = this.getCaret(); var queryStartAt; // also look forward, to see if we closed this one for(var i = caret; i < text.length; i++) { var c = text.charAt (i) ; if((c == "[") && (text.charAt(i - 1) == "[")) { break ; } if((c == "]") && (text.charAt(i - 1) == "]")) { return ; } if((c == "{") && (text.charAt(i - 1) == "{")) { break ; } if((c == "}") && (text.charAt(i - 1) == "}")) { return ; } } for(var i = caret; i >= 0; i--) { var c = text.charAt(i); //if(c == "]" || c == "|") { if ( (c == "|") || ( (c == "]") && (text.charAt(i-1) == "]") ) || ( (c == "}") && (text.charAt(i-1) == "}") ) ) { this.toggleContainer(false) ; return; } //return; //} if((c == "[") && (text.charAt(i - 1) == "[")) { this.originalQuery = text.substr(i + 1, (caret - i - 1)); queryReal = this.originalQuery; if (this.originalQuery.indexOf(':')==0){ this.isColon = true; queryReal = queryReal.replace(':',''); } else { this.isColon = false; } this.isTemplate = false; queryStartAt = i; break; } if((c == "{") && (text.charAt(i - 1) == "{") && (text.charAt(i - 2) != "{") ) { this.originalQuery = text.substr(i + 1, (caret - i - 1)); this.isColon = false; if (this.originalQuery.length >= 6 && this.originalQuery.toLowerCase().indexOf('subst:') == 0){ queryReal = "Template:"+this.originalQuery.replace(/subst:/i,''); this.isSubstTemplate = true; } else if (this.originalQuery.indexOf(':')==0){ queryReal = this.originalQuery.replace(':',''); this.isColon = true; } else { queryReal = "Template:"+this.originalQuery; this.isSubstTemplate = false; } this.isTemplate = true; queryStartAt = i; break; } } if(queryStartAt >= 0 && queryReal.length > 2 && this.originalQuery != this.prevQuery ) { this.sendQuery(queryReal); this.prevQuery = this.originalQuery; } }; this.sendQuery = function(query) { var api = sajax_init_object(); if (!api) { return false; } api.open('GET', wgServer + wgScriptPath + '/api.php?format=json&action=opensearch&search=' + encodeURI(query) + '&namespace=0&suggest', true); var _this = this; api.onreadystatechange = function() { if(api.readyState==4) { if(api.status==200) { var data = eval('(' + api.responseText + ')'); _this.showSuggestions(data); } } }; api.send(null); }; this.showSuggestions = function(data) { this.updatePosition(); if (this.selectedIndex >= 0 && this.selectedIndex < this.numItems) { this.unhighlight(this.selectedIndex); } var dropdownUl = this.dropdown.firstChild; while ( dropdownUl.childNodes.length > 0 ) { dropdownUl.removeChild(dropdownUl.firstChild); } if (data.length == 2 && data[1].length > 0) { this.results = data[1]; this.toggleContainer(true); this.numItems = this.results.length; this.selectedIndex = -1; for (var i = 0; i < this.results.length; i++) { var listitem = document.createElement('li'); if (this.isTemplate) { this.results[i] = this.results[i].substring(9); } listitem.innerHTML = this.results[i]; listitem.index = i; dropdownUl.appendChild(listitem); } } else { this.toggleContainer(false); } }; this.moveSelection = function(nKeyCode) { if(this.containerOpen) { var newSelection = (nKeyCode == 40) ? (this.selectedIndex + 1) : (this.selectedIndex- 1); if (newSelection < 0 || newSelection >= this.numItems) { return; } var oldSelection = this.selectedIndex; this.selectedIndex = newSelection; var dropdownUl = this.dropdown.firstChild; if ( oldSelection >= 0 && oldSelection < dropdownUl.childNodes.length ) { dropdownUl.childNodes[oldSelection].className = ""; dropdownUl.childNodes[oldSelection].style.backgroundColor = "transparent"; dropdownUl.childNodes[oldSelection].style.color = "inherit"; this.unhighlight(oldSelection); } //paranoia if ( newSelection >= dropdownUl.childNodes.length ) { return; } dropdownUl.childNodes[newSelection].className = "LinkSuggest_selected"; dropdownUl.childNodes[newSelection].style.backgroundColor = "blue"; dropdownUl.childNodes[newSelection].style.color = "white"; this.highlight(newSelection); } }; this.autoComplete = function() { this.toggleContainer(false); var result = this.results[this.selectedIndex]; this.textbox.focus(); var scrollTop = this.textbox.scrollTop; var text = this.textbox.value.replace(/\r/g, ""); var caret = this.getCaret(); for(var i = caret; i >= 0; i--) { // break for templates and normal links if( ( ( text.charAt(i - 1) == "[" ) && !this.isTemplate ) || ( ( text.charAt(i - 1) == "{" ) && this.isTemplate ) ) { break; } } var textBefore = text.substr(0, i); var newVal = textBefore + ((this.isTemplate && this.isSubstTemplate) ? 'subst:' : '' ) + (this.isColon ? ':' : '') + result + (this.isTemplate ? "}}" : "]]") + text.substr(i + this.originalQuery.length); this.textbox.value = newVal; this.setCaret(i +(this.isColon ? 1 : 0) + ((this.isTemplate && this.isSubstTemplate) ? 6 : 0 ) + result.length + 2); this.textbox.scrollTop = scrollTop; }; this.updatePosition = function() { pos = this.getCaretPosition(); this.dropdown.style.left=pos[1] + "px"; this.dropdown.style.top=pos[0] + "px"; this.imagePreview.style.left = parseFloat(getStyle(this.dropdown,'left')) + parseFloat(getStyle(this.dropdown,'width')) + "px"; this.imagePreview.style.top = getStyle(this.dropdown,'top'); }; this.toggleContainer = function(show) { if (show) { this.dropdown = this.textbox.parentNode.appendChild(this.dropdown); this.imagePreview = this.textbox.parentNode.appendChild(this.imagePreview); this.dropdown.style.display="block"; this.containerOpen = true; } else { this.dropdown.style.display="none"; this.containerOpen = false; if (this.selectedIndex >= 0 && this.selectedIndex < this.numItems) { this.unhighlight(this.selectedIndex); } } }; this.getCaret = function() { if (typeof(this.textbox.selectionStart) != undefined) { return this.textbox.selectionStart; } else { // hack for IE this.textbox.focus(); var sel = document.selection.createRange(); var sel2 = sel.duplicate(); sel2.moveToElementText(this.textbox); var caretPos = -1; while(sel2.inRange(sel)) { sel2.moveStart('character'); caretPos++; } return caretPos; } }; this.setCaret = function(pos) { if(this.textbox.setSelectionRange) { this.textbox.focus(); this.textbox.setSelectionRange(pos, pos); } else if (this.textbox.createTextRange) { var range = this.textbox.createTextRange(); range.collapse(true); range.moveEnd('character', pos); range.moveStart('character', pos); range.select(); } }; this.getCaretPosition = function() { var text = this.textbox.value.replace(/\r/g, ""); var caret = this.getCaret(); var lineLength = this.getLineLength(); var row = 0; var charInLine = 0; var lastSpaceInLine = 0; for(i = 0; i < caret; i++) { charInLine++; if(text.charAt(i) == " ") { lastSpaceInLine = charInLine; } else if(text.charAt(i) == "\n") { lastSpaceInLine = 0; charInLine = 0; row++; } if(charInLine > lineLength) { if(lastSpaceInLine > 0) { charInLine = charInLine - lastSpaceInLine; lastSpaceInLine = 0; row++; } } } var nextSpace = 0; for(j = caret; j < caret + lineLength; j++) { if(text.charAt(j) == " " || text.charAt(j) == "\n" || caret == text.length) { nextSpace = j; break; } } if(nextSpace > lineLength && caret <= lineLength) { charInLine = caret - lastSpaceInLine; row++; } this.row = row; //hack, since getting the line-height is unreliable this.test.style.fontSize = getStyle(this.textbox,'font-size'); this.test.style.fontFamily = getStyle(this.textbox,'font-family'); this.test.style.lineHeight = getStyle(this.textbox,'line-height'); this.test.style.marginLeft = getStyle(this.textbox,'margin-left'); this.test.style.marginRight = getStyle(this.textbox,'margin-right'); this.test.style.marginTop = getStyle(this.textbox,'margin-top'); this.test.style.marginBottom = getStyle(this.textbox,'margin-bottom'); this.test.style.paddingLeft = getStyle(this.textbox,'padding-left'); this.test.style.paddingRight = getStyle(this.textbox,'padding-right'); this.test.style.paddingTop = getStyle(this.textbox,'padding-top'); this.test.style.paddingBottom = getStyle(this.textbox,'padding-bottom'); //incorporate scrollbar this.test.style.width = this.textbox.clientWidth + "px"; this.test = this.textbox.parentNode.insertBefore(this.test, this.textbox); this.test.innerHTML = this.textbox.value.substr(0,caret).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); var top = parseFloat(getStyle(this.test,'height')); //now get the left position this.test.style.width = "auto"; this.test.innerHTML = this.textbox.value.substr(caret - charInLine, charInLine - this.originalQuery.length).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); var tempsave = this.test.style.display; this.test.style.display="inline"; var left = this.test.offsetWidth; this.test.style.display=tempsave; this.test.innerHTML = ""; //var top = 19+(2+parseFloat(getStyle(this.textbox,'line-height'))*row)-this.textbox.scrollTop; //var left = 3+(8*(charInLine-this.originalQuery.length))-this.textbox.scrollLeft; left += this.textbox.offsetLeft; top -= this.textbox.scrollTop; top += this.textbox.offsetTop; return [top,left]; }; this.mouseOver = function(event) { var target = this.getTarget(event); if (target.nodeName.toLowerCase() == "li") { target.className = "LinkSuggest_selected"; target.style.backgroundColor = "blue"; target.style.color = "white"; if (target.nodeName.toLowerCase() == "li" && target.index != null) { if (this.selectedIndex >= 0 && this.selectedIndex < this.numItems) { this.unhighlight(this.selectedIndex); } this.highlight(target.index); } } }; this.mouseOut = function(event) { var target = this.getTarget(event); if (target.nodeName.toLowerCase() == "li") { target.className = ""; target.style.backgroundColor = "transparent"; target.style.color = "inherit"; if (target.nodeName.toLowerCase() == "li" && target.index != null) { this.unhighlight(target.index); if (this.selectedIndex >= 0 && this.selectedIndex < this.numItems) { this.highlight(this.selectedIndex); } } if (this.selectedIndex >= 0 && this.selectedIndex < this.numItems) { var dropdownUl = this.dropdown.firstChild; dropdownUl.childNodes[this.selectedIndex].className = "LinkSuggest_selected"; dropdownUl.childNodes[this.selectedIndex].style.backgroundColor = "blue"; dropdownUl.childNodes[this.selectedIndex].style.color = "white"; } } }; this.click = function(event) { var target = this.getTarget(event); if (target.nodeName.toLowerCase() == "li" && target.index != null) { this.selectedIndex = target.index; this.autoComplete(); } }; this.getLineLength = function() { return Math.floor(this.textbox.scrollWidth/8); }; this.getTarget = function(ev) { var n = ev.target || ev.srcElement; try { if (n && 3 == n.nodeType) { return n.parentNode; } } catch(e) { } return n; }; this.highlight = function(index) { if (this.originalQuery.toLowerCase().indexOf('file:') == 0 || this.originalQuery.toLowerCase().indexOf('image:') == 0) { //result always starts with File:, even if you type Image: //var filename = this.results[index].substring(5); var _this = this; this.previewTimer = setTimeout(function() {_this.preview(_this.results[index])}, 750); this.imagePreviewImg.style.display = "none"; this.imagePreview.style.left = parseFloat(getStyle(this.dropdown,'left')) + parseFloat(getStyle(this.dropdown,'width')) + "px"; this.imagePreview.style.top = getStyle(this.dropdown,'top'); this.imagePreview.style.display = "block"; if (typeof (injectSpinner) == 'function') injectSpinner (this.imagePreviewImg, 'wpLSPreviewSpinner'); } }; this.unhighlight = function(index) { this.imagePreview.style.display = "none"; this.imagePreviewImg.src = ""; if (typeof (removeSpinner) == 'function') removeSpinner ('wpLSPreviewSpinner'); clearTimeout(this.previewTimer); }; this.preview = function(filename) { if (typeof (removeSpinner) == 'function') removeSpinner ('wpLSPreviewSpinner'); var api = sajax_init_object(); if (!api) { return false; } api.open('GET', wgServer + wgScriptPath + '/api.php?format=json&action=query&prop=imageinfo&iiprop=url&titles=' + encodeURI(filename) + '&iiurlwidth=300', true); var _this = this; api.onreadystatechange = function() { if(api.readyState==4) { if(api.status==200) { var data = eval('(' + api.responseText + ')'); var url = ""; for (var index in data['query']['pages']) { page = data['query']['pages'][index]; if(typeof(page) !== 'function') { break; } } if ( typeof(page['imageinfo'][0]) != undefined ) { url = page['imageinfo'][0]['thumburl']; } _this.imagePreviewImg.src = url; _this.imagePreviewImg.style.display = 'block'; } } }; api.send(null); } } addOnloadHook(function () { var textbox = document.getElementById('wpTextbox1'); if (!textbox) { return; } var MyLinkSuggest = new LinkSuggest(textbox); addHandler(textbox,"keydown",function(event) { MyLinkSuggest.keydown(event); }); addHandler(textbox,"keyup",function(event) { MyLinkSuggest.keyup(event); }); addHandler(textbox,"scroll",function(event) { MyLinkSuggest.updatePosition(); }); });