Участник:Js/linkcomplete.js

Материал из Википедии — свободной энциклопедии
Страница персонального оформления. У этого JS-кода есть документация: Участник:Js/linkcomplete.
После сохранения очистите кэш браузера.
function linkComplete(){//global wrapper


var suggestState, useOpenSearch = false
var typedTitle, typedTitleOrig, suggestNS, suggestNamespace, suggestSuffix
var suggestLimit = 20, suggestURL, queryCont, typingTimeoutId = 0, suggestRedirects, displayAllNS
var results, resultsIdx, sel
var txtBC //text before cursor

var aj, ajPath = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?action=', ajTimeoutId, ajCache = {}, nsNames
var cycleNS = [14, 2, 6, 4]

//Initialization
if (window.opera && opera.version() < '9.5') return
txtBox = document.getElementById('wpTextbox1')
addHandler(txtBox, window.opera?'keypress':'keydown', textOnKeyPress)
addToolbarButton('[\[→]]', suggestStart, '', 'Suggest link [ctrl-↓]', window.es_accesskey || '')

//intercept InsertTags
/*
window.insertTags_Old = insertTags
insertTags = function (op, cl, sample){
  insertTags_Old(op, cl, sample)
  if (/\[\[/.test(op)) { sel.put(''); suggestStart() }
}
*/
return





function delayedInit(){
appendCSS('\
table#lc_results {border:1px solid gray; padding:1px; background:#F5F5F5; cursor:pointer}\
table#lc_results th {background:#EAEAFF; white-space:nowrap}\
table#lc_results th div {float:right; margin-left:1em; border:1px outset gray; font-weight:normal}\
tr.ac_selected {background:gray}\
textarea.suggesting {' + (window.opera?'border:2px inset #EEEEEE;':'') + 'border-left-color:orange}')
 addHandler(txtBox, 'click', function(){suggestOff()})
//define local NS
switch (mw.config.get('wgDBname')){
case 'ruwiki':
nsNames = {
'-2':'Медиа', '-1':'Служебная', 1:'Обсуждение', 
2:'Участник|Участница', 3:'Обсуждение участника|обсуждение участницы',
4:'Википедия|ВП',5:'Обсуждение Википедии', 
6:'Изображение', 7:'Обсуждение изображения',
9:'Обсуждение MediaWiki',
10:'Шаблон', 11:'Обсуждение шаблона',
14:'Категория', 15:'Обсуждение категории',
100:'Портал', 101:'Обсуждение портала'
}
break
case 'enwiki':
nsNames = { 4:'Wikipedia|WP', 5:'Wikipedia talk|WT', 100:'Portal', 101:'Portal talk' }
break
default: //API request?
}
//add canonical NS
var can='Media|Special||Talk|User|*|Project|*|Image|*|MediaWiki|*|Template|*|Help|*|Category|*'.split('|')
for (var i=0; i<can.length; i++)
  nsNames[i-2] = (nsNames[i-2] ? nsNames[i-2]+'|' : '') + (can[i]=='*' ? can[i-1]+' talk' : can[i])
//initialize selection
sel = selectionTools
sel.ini(txtBox)
}




// *** EVENTS ***

function textOnKeyPress(e){
 e = e || window.event
 var key = e.keyCode
 ajCancel()
  if (e.ctrlKey && (key==38||key==40)){ //up or dn
    eventStop(e)
    suggestStart(key==38) //opensearch or not
    return
 }
 if (!suggestState) return

 //while suggesting 

 //escape always stops the script
 if (key==27){ eventStop(e); suggestCancel(); return } 

 //if user already started to type something: simply restart the timeout, waiting for him to stop
 if (typingTimeoutId){ typingTimeout(true); return } 
 
 //in the suggestion mode
 switch (key){
  case 37:case 8:case 46: eventStop(e); suggestCancel(); return //left, bs, delete
  case 38: eventStop(e); suggestNext(-1); return //up
  case 40: eventStop(e); suggestNext(+1); return //down
  case 32: if (e.ctrlKey) { eventStop(e); suggestStart(!useOpenSearch); return }; break
  case 39: //right
    eventStop(e)
    if (typedTitle) suggestAccept()
    else suggestNamespaces(true) //show all namespaces
    return
  case 13: eventStop(e); suggestAccept(true); return //Enter
	//maybe also check e.shiftKey || e.ctrlKey ?
  case 17: case 16: return //Ctrl or Shift: do nothing
  //case 32: eventStop(e); suggestStart();  return //space
 } 
 //for all other keys
 typingTimeout(true)
 
}

function typingTimeout(isStart){
 if (isStart){
   popupHide()
   typingTimeout(false)
   typingTimeoutId = setTimeout(suggestContinue, 500)
 }else{
   if (typingTimeoutId) clearTimeout(typingTimeoutId)
   typingTimeoutId = 0
 }
}


function suggestOn(){
 suggestNS = 0; suggestSuffix = ''; suggestNamespace = ''; suggestRedirects = false
 typingTimeoutId = 0 
 if (!suggestState) txtBox.className += ' suggesting'
 suggestState = true
}
function suggestOff(msg, period){
 if (!suggestState) return
 suggestState = false
 ajCancel()
 txtBox.className = txtBox.className.replace(/ ?suggesting/, '')
 popupHide()
 sel.put('')
 if (msg) msgShow(msg, period || 2000)
}

function suggestCancel(){
 typingTimeout(false) //just in case
 replaceTitle(typedTitleOrig, true) //replace with old if we changed lettercase
 suggestOff()
}

function rememberCursorPosition(){
 txtBC = sel.getCursorText(true) //get text before cursor 
}

function suggestStart(isOpenSearch){
 //initialize
 if (!nsNames) delayedInit()
 if (typeof isOpenSearch == 'boolean') useOpenSearch = isOpenSearch
 if (!suggestState){//just started
   if (sel.get()) return //do nothing if text selected?
   rememberCursorPosition() //into txtBC
 }
 suggestOn()
 //determine what's before cursor
 var ma, res, line = txtBC.substring(txtBC.lastIndexOf('\n'))
 if (ma=line.match(/\[\[:?([^\]\[\|]*)$/)){ //[[words
   res = parseNS(ma[1])
   suggestNS = res[0]
   typedNamespace = res[1] ? res[1]+':' : ''
   typedTitle = res[2]
   //check for already existing ending brackets
   line = sel.getCursorText(false) 
   if (/\n/.test(line)) line = line.substring(0, line.indexOf('\n')) //cur line after cursor 
   ma = line.match(/\|?\]\]/) //the rest of the line
   if (ma) suggestSuffix = ma[0]
 //else if (ma=line.match(/\{\{([^\}\]\|]+)$/)) //template
  // typedTitle = ma[1]
  // suggestNS = 10
 }else if (ma=line.match(/[^\s\[\]\|\{\}"]+$/)){ //word before cursor
   typedTitle = ma[0]
   sel.move(-typedTitle.length)
   sel.put('[[', false)
   sel.move(typedTitle.length)
   rememberCursorPosition()
 }else{ //start a new link
   sel.put('[[', false)
   typedTitle = typedNamespace = ''
   rememberCursorPosition()
 }
 //normalize and go
 typedTitleOrig = typedTitle
 replaceTitle(normalize(typedTitle))
 sel.save()
 if (typedTitle) titlesRequest()
 else suggestNamespaces()
}


function suggestContinue(){ //called on timeout, checks if selection was changed to cancel continuos suggestion
 typingTimeoutId = 0
 var txtBCOld = txtBC
 rememberCursorPosition() 
 //check that cursor moved to the right but stayed on the same line
 if ((txtBC.length <= txtBCOld.length)
  || (txtBC.substring (0, txtBCOld.length) != txtBCOld)
  || (/\n/.test(txtBC.substring(txtBCOld.length))))
    suggestOff()
 else //restart suggestions
    suggestStart()
}


function suggestNamespaces(isAll){ //start suggesting namespaces
 function getNS(nn){ var ns = nsNames[nn].split('|')[0];  return ns ? ns+':' : '' }
 results = []
 //create array
 if (isAll){
   for (var i=-2; i<110; i++)
     if (typeof nsNames[i] == 'string') results.push(getNS(i))
 }else{
   results.push('')
   for (var i=0; i<cycleNS.length; i++) results.push(getNS(cycleNS[i]))
 }
 displayAllNS = isAll
 //try to find already type namespace
 resultsIdx = -1
 for (var i=0; i<results.length; i++) if (results[i] == typedNamespace) {resultsIdx = i; break }
 popupShow()
 popupUpdate()
}



// *** API ***
function titlesRequest(){
 if (typedTitle=='z') {//local testing
   results = ['Z', 'Zebra', 'Zombie', 'Zaraza']
   titlesShow()
   return 
 }
 var ns = suggestNS, pr = typedTitle, url
 if (ns<0){ //allpages won't work with negative namespaces
   useOpenSearch = true;  ns = '';  pr = typedNamespace + pr
 }
 msgShow(pr + ' ...' + (suggestRedirects ? ' (redirects)':''))
 if (useOpenSearch) url = 'opensearch&search=<pr>&limit=<li>&namespace=<ns>'
 else url ='query&format=json&list=allpages&apprefix=<pr>&aplimit=<li>&apnamespace=<ns>&apfilterredir=<re>'
 url = url.replace('<li>', suggestLimit)
 url = url.replace('<ns>', ns)
 url = url.replace('<re>', suggestRedirects ? 'redirects' : 'nonredirects')
 url = url.replace('<pr>', encodeURIComponent(pr))
 ajCall(url, titlesReceive, 5000)
}



function titlesReceive(query){
 zq = query
 //analyze result
 var q = query, err = ''
 if (!q) err='API: unrecognized error'
 else if (q==-1) err='API: bad response'
 else if (q==-2) err='API: timeout'
 else if (q.error) err='API response: ' + q.error.code + ':' + q.error.info
 if (err) {suggestOff(err); return }
 //convert to array
 if (q.length){//opensearch
   results = new Array(q[1].length)
   for (var i=0; i<q[1].length; i++) results[i] = q[1][i]
   queryCont = false 
 }else{//allpages
   if (!(q=q.query)) err='API: empty response'
   else if (!(q=getAnyChild(q))) err='API: empty results'
   if (err) {suggestOff(err); return }
   //convert titles into array
   results = new Array(q.length)
   for (var ttl, i=0; i<q.length; i++) results[i] = q[i].title || q[i].name || q[i]['*']
   queryCont = getAnyChild(getAnyChild(query['query-continue']))
 }
 //separate namespace from titles
 if (suggestNS && results.length>0){
   suggestNamespace = results[0].substring(0, results[0].indexOf(':')) //get correct namespace name
   for (var i=0; i<results.length; i++) results[i] = results[i].substring(suggestNamespace.length+1)
 }
 //check for ending space, because opensearch also returns a title w/o space
 if (/ $/.test(typedTitle)){
   for (var i=0; i<results.length; i++)//remove all imcompatible titles
     if (typedTitle.toLowerCase() != results[i].substring(0, typedTitle.length).toLowerCase())
        results.splice(i, 1)
   
 }
 titlesShow()
}



// *** INTERACTION ***

function titlesShow(){ //display result
 msgHide()
 resultsIdx = 0
 if (results.length == 0) return suggestOff('x  x  x')
 else if (results.length == 1) msgShow(results[0], 2000)
 else if (results[0]==uppercase(typedTitle)) resultsIdx = 1 //several results: skip "the same as typed text"
 popupShow()
 suggestNext()
}


function suggestNext(step){
 //if (!sel.compare()) { suggestContinue(); return }
 //increment
 if (step) resultsIdx += step
 if (resultsIdx < 0) resultsIdx = results.length-1
 else if (resultsIdx > results.length-1) resultsIdx = 0
 // !!! temp? 
 sel.put('')
 //suggest next ...
 if (typedTitle) replaceTitle(results[resultsIdx])
 else replaceNamespace(results[resultsIdx])
 sel.save()
 popupUpdate()
}


function replaceTitle(newTitle, isForce){ //2nd arg: always replace 1st letter
 sel.put('') //remove selected part of suggestion
 //replace typed part if needed: opensearch is case-insensitive
 var newTyped = newTitle.substring(0, typedTitle.length) //new "typed text"
 if (((isForce || suggestNS) ? typedTitle : uppercase(typedTitle)) != newTyped) { 
   sel.move(-typedTitle.length, 0)
   typedTitle = newTyped
   sel.put(typedTitle, false) 
 }
 //suggest the remaining part
 var newSuggested = newTitle.substring(typedTitle.length)
 if (newSuggested) sel.put(newSuggested) //insert new
}

function replaceNamespace(newName){
 sel.move(-typedNamespace.length, 0) //select old suggestion
 typedNamespace = newName
 sel.put(typedNamespace, false)
 rememberCursorPosition() 
}


function suggestAccept(needPipe){
 //if (cursorPosChanged()) return
 if (!typedTitle) return //accepted namespace - nothing else to do
 if (suggestNS==0 && needPipe) replaceTitle(results[resultsIdx], true) //make sure target is upercase
 sel.collapse() //accept selected part
 suggestOff()
 if (suggestSuffix) return
 if (suggestNS==14) sel.put('\n', true) //line break after category
 if (needPipe){ //insert pipe so user can type link name
   sel.put('|', false)
   sel.put(']]', true)
   if (suggestNS==14 && mw.config.get('wgNamespaceNumber')!=0) sel.put('{\{PAGENAME}}')
 }else{
   sel.put(']]')
   sel.collapse(false)
 }
}


function cursorPosChanged(){
 //if (sel.compare()) return false
 var msg = 'text selection changed'
 if (window.opera && opera.version < '9.50') msg += ', Opera < 9.50 is incompatible, please upgrade'
 suggestOff(msg, 2000)
 return true
}




// *** POPUP ***
var popupDiv, popupTable
function popupShow(){ //create and show results table
 //if (results.length < 2) { popupTable = null; return } //no popup if there is only one result
 if (!popupDiv){//create
   popupDiv = document.createElement('div')
   document.body.appendChild(popupDiv)
   addHandler(popupDiv, 'mouseup', popupOnClick) //mouseup is to detect middle clicks as well
 }

 var hdrText = '', hdrStyle = ''

 if (!typedTitle){
    if (!displayAllNS) hdrText += ' <div>[→]</div>'
    hdrText += '___ :'
 }else{
   if (useOpenSearch){
      hdrStyle = 'font-style:italic'
      hdrText += ' <div>opensearch</div>'
   }else{
     hdrText += '<div>pages</div>'
   }
   hdrText += suggestNamespace + ':'
   if (queryCont) hdrText += '<span style="font-weight:notmal"> ...</span>'
 }
 var html = '<table id="lc_results"><tr><th style="text-align:left;' 
  + hdrStyle + '">' + hdrText + '</th></tr>'
 for (i=0; i<results.length; i++)
   html += '<tr><td style="cursor:pointer" id="ac_result_'+i+'">'+results[i]+'&nbsp;</td></tr>'
 //if (queryCont) html += '<tr><td id="ac_next" style="text-align:center; line-height:0.2em">...</td></tr>'
 popupDiv.innerHTML = html + '</table>'
 popupTable = popupDiv.firstChild
 popupTable.style.cssText = 'position:absolute; right:3px; z-index:100; width:auto; display:none' //opacity:0.8;
 if (suggestRedirects) popupTable.style.fontStyle = 'italic'
}
function popupHide(){ if (popupTable) popupTable.style.display = 'none' }
function popupOnClick(e){
 //setTimeout(sel.focus, 100)
 txtBox.focus()
 e = e || window.event
 var targ = e.target || e.srcElement
 if (targ.nodeName=='DIV'){
   if (!typedTitle) suggestNamespaces(true)
   else suggestStart(!useOpenSearch)
   return
 }else if (targ.nodeName=='TH'){
   //txtBox.focus()
 }
 //find row number
 var tr = targ.parentNode, trs = tr.parentNode.getElementsByTagName('tr'), i
 for (i=1; i<trs.length; i++) if (trs[i]==tr) break //start with 1 to skip header row
 if (i >= trs.length) return popupHide() //not found for some reason
 resultsIdx = i - 1
 //shift or middle click - open window
 var isIE = navigator.userAgent.indexOf('MSIE') != -1
 if (typedTitle && (e.shiftKey || (isIE && e.button == 4) || (!isIE && e.button == 1))){
   window.open (mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('\$1', suggestNamespace+':'+results[resultsIdx]))
   return
 }
 //otherwise - accept
 suggestNext()
 suggestAccept(true)
}
function popupUpdate(){
 if (!popupTable) return
 popupTable.style.top = windowScrolled() + 'px'
 popupTable.style.display = ''
 var trs = popupTable.getElementsByTagName('tr')
 if (popupTable.lastMarked) trs[popupTable.lastMarked].className = '' //unmark previous
 if (resultsIdx == -1) return
 trs[resultsIdx+1].className = 'ac_selected'
 popupTable.lastMarked = resultsIdx + 1
}


// *** MESSAGE ***
var msgDiv, msgTimeoutId
function msgShow(msg, period){
 if (!msgDiv){ //create
   msgDiv = document.createElement('div')
   msgDiv.style.cssText = 'position:absolute; z-index:100; background:#FFE7A4; color:black; border:1px solid gray; padding:2px'
   document.body.appendChild(msgDiv)
 }
 if (msgTimeoutId) { clearTimeout(msgTimeoutId); msgTimeoutId = 0 }
 //position
 var pos = getElemPos(txtBox)
 msgDiv.style.left = (pos[0] + 2)+ 'px'
 msgDiv.style.top = (Math.max(pos[1], windowScrolled()) + 2) + 'px'
 //show
 msgDiv.style.display = ''
 msgDiv.innerHTML = msg
 msgTimeoutId = setTimeout(msgHide, period || 5000)
}
function msgHide(){  if (msgDiv) msgDiv.style.display = 'none' }


// *** AAJAX ***
function ajCall(params, func, timeout){
 if (ajCache[params]) { func(ajCache[params]); return }
 if (aj) ajCancel()
 //if (!aj){
 aj = sajax_init_object()
 aj.onreadystatechange = ajOnReady
 //}else ajCancel() 
 ajParams = params
 aj.open('GET', ajPath + params, true)
 ajTimeoutId = setTimeout(ajOnTimeout, timeout || 10000)
 aj.send(null)

 function ajOnReady(){
   if (aj.readyState != 4) return
   clearTimeout(ajTimeoutId)
   if (aj.status != 200) return func(-1)
   var q
   try {
     eval('q='+aj.responseText)
     ajCache[ajParams] = q
   }catch(e){}
   func(q)
 }

 function ajOnTimeout(){
   ajCancel()
   func(-2)
 }

}

function ajCancel(){
 if (aj && (aj.readyState==2 || aj.readyState==3)){
   clearTimeout(ajTimeoutId)
   aj.abort()
 }
}

function getAnyChildKey(obj) { for(var key in obj) return key; return null }
function getAnyChild(obj) { var key = getAnyChildKey(obj); return key ? obj[key] : null }



// *** MISC ***

function normalize(tt){ return tt.replace(/[_ ]+/g,' ').replace(/^ /, '') } //except uppercase and ending spaces
function uppercase(tt){ return tt.substring(0,1).toUpperCase() + tt.substring(1) }

function parseNS(pgname){ // 'project:dd'  -> [4, 'project', 'd']
 var pos = pgname.indexOf(':'), nsn, nsname
 if (pos != -1){
   nsname = pgname.substring(0, pos)
   for (var i in nsNames)
     if (('|'+nsNames[i].toLowerCase()+'|').indexOf('|'+nsname.toLowerCase()+'|') != -1)
        {nsn = i; break}
 }
 if (nsn) pgname = pgname.substring(pos+1)
 else { nsn = 0; nsname = '' }
 return [nsn, nsname, pgname]
}


function addToolbarButton(name, onclick, id, tooltip, accesskey){
 var toolbar = document.getElementById('toolbar')
 if (!toolbar) return
 var newBtn = document.createElement('input')
 newBtn.type = 'button'; newBtn.style.cssText = 'background:#adbede; height:22px; vertical-align:middle; padding:0'
 if (name) newBtn.value = name
 if (onclick) newBtn.onclick = onclick
 if (id) newBtn.id = id
 if (tooltip) newBtn.title = tooltip
 if (accesskey) newBtn.accessKey = accesskey
 toolbar.appendChild(newBtn)
 return newBtn
}


// *** DOM function ***
function getElemPos(elem){
 var x = 0, y = 0
 while (elem){  x += elem.offsetLeft;  y += elem.offsetTop;  elem = elem.offsetParent }
	if (navigator.userAgent.indexOf('Mac') != -1 && typeof document.body.leftMargin != 'undefined'){
		x += document.body.leftMargin
		y += document.body.topMargin
	}
	return [x, y]
}
function windowScrolled(){
if (self.pageYOffset) // all except Explorer
 return self.pageYOffset
else if (document.documentElement && document.documentElement.scrollTop)	// Explorer 6 Strict
 return document.documentElement.scrollTop
else if (document.body) // all other Explorers
 return document.body.scrollTop
}

function eventStop(ev){
 if (ev.preventDefault) ev.preventDefault(); else ev.returnValue = false
}















}//linkComplete



if (mw.config.get('wgAction')=='edit' || mw.config.get('wgAction')=='submit')
$(linkComplete)






// *** TEXT SELECTION ***

var selectionTools = new function(){

var tBox
this.ini = function(el){ tBox = el } // !!!  maybe check for really old browsers and quit?

this.get = function (){
 this.focus()
 if (document.selection)//IE/Opera
   return document.selection.createRange().text
 else if (tBox.selectionStart || tBox.selectionStart == '0')// Mozilla
   return tBox.value.substring(tBox.selectionStart, tBox.selectionEnd)
 else 
   return null
}

this.put = function(txt, isCollapse){ //isCollapse: true: set cursor at start, false: at the end of selection
 this.focus()
 if (tBox.selectionStart || tBox.selectionStart == '0'){// Mozilla/Opera
    var p1 = tBox.selectionStart, p2 = tBox.value.length - tBox.selectionEnd
    var s = tBox.scrollTop
    tBox.value = tBox.value.substring(0, p1) + txt 
     + tBox.value.substring(tBox.selectionEnd, tBox.value.length)
    tBox.scrollTop = s
    tBox.selectionStart =  p1
    tBox.selectionEnd = tBox.value.length - p2
 }else if (document.selection){//IE
   r = document.selection.createRange()
   r.text = txt
   r.select() //IE need this to make sure cursor is after selection
   this.move(-txt.length, 0)
 }
 if (typeof isCollapse == 'boolean') this.collapse(isCollapse)
}

this.collapse = function(isStart){ //true: collapse to selection start
 if (tBox.selectionStart || tBox.selectionStart == '0'){// Mozilla
   if (isStart) tBox.selectionEnd = tBox.selectionStart
   else tBox.selectionStart = tBox.selectionEnd
 }else if (document.selection){//IE
   var r = document.selection.createRange()
   r.collapse(isStart ? true : false)
   r.select()
 }
}


this.move = function(mv1, mv2){
 if (typeof mv2 == 'undefined'){//move cursor, i.e.empty selection
   if (mv1>0) this.move(mv1, 0); else this.move(0, mv1)
 }else if (tBox.selectionStart || tBox.selectionStart == '0'){
   var end = tBox.selectionEnd + mv2
   tBox.selectionStart += mv1
   tBox.selectionEnd = end
 }else if (document.selection){
   var r = document.selection.createRange()
   r.moveStart('character', mv1) 
   r.moveEnd('character', mv2)
   r.select()
 } 
}


this.pos = function(){ //returns the position of selection end
 this.focus()
 if (tBox.selectionStart || tBox.selectionStart=='0')
   return tBox.selectionEnd
 else if (document.selection){
   var r = document.selection.createRange()
   r.moveToElementText(tBox)
   r.setEndPoint('StartToEnd', document.selection.createRange())
   return tBox.value.length - r.text.replace(/n/g, '\n\r').length
 }
}
// flaw in IE?   "text |" and   "text\n|" will return the same cursor position, be

//returns the whole text before or after the cursor;  we assume that the selection is empty
this.getCursorText = function(isBefore){
 if (tBox.selectionStart || tBox.selectionStart=='0'){
   if (isBefore) return tBox.value.substring(0, tBox.selectionStart)
   else return tBox.value.substring(tBox.selectionEnd, tBox.value.length)
 }else if (document.selection){
   var s = document.selection.createRange(), r = s.duplicate()
   r.moveToElementText(tBox)
   if (isBefore) r.setEndPoint('EndToStart', s)
   else r.setEndPoint('StartToEnd', s)
   var txt = r.text, old = s.text.length, cont = true
   do { s.moveStart('character', -1)} while ((s.text.length==old) && (txt+='\n')) //add missing \n
   return txt
 }
}

this.focus = function(){ tBox.focus() }

this.save = function(){
 if (tBox.selectionStart || tBox.selectionStart == '0'){
   tBox.selStart = tBox.selectionStart;  tBox.selEnd = tBox.selectionEnd
 }else if (document.selection)
   tBox.selRange = document.selection.createRange()
}

this.restore = function(){
 if (tBox.selRange) tBox.selRange.select()
 else this.move(tBox.selStart-tBox.selectionStart, tBox.selEnd-tBox.selectionEnd)
}

this.compare = function(){
 if (tBox.selRange) return document.selection.createRange().isEqual(tBox.selRange)
 else return (tBox.selStart==tBox.selectionStart && tBox.selEnd==tBox.selectionEnd)
}


}