1/** 2 * Functions for text editing (toolbar stuff) 3 * 4 * @todo I'm no JS guru please help if you know how to improve 5 * @author Andreas Gohr <andi@splitbrain.org> 6 */ 7 8/** 9 * Creates a toolbar button through the DOM 10 * 11 * Style the buttons through the toolbutton class 12 * 13 * @author Andreas Gohr <andi@splitbrain.org> 14 */ 15function createToolButton(icon,label,key,id){ 16 var btn = document.createElement('button'); 17 var ico = document.createElement('img'); 18 19 // preapare the basic button stuff 20 btn.className = 'toolbutton'; 21 btn.title = label; 22 if(key){ 23 btn.title += ' [ALT+'+key.toUpperCase()+']'; 24 btn.accessKey = key; 25 } 26 27 // set IDs if given 28 if(id){ 29 btn.id = id; 30 ico.id = id+'_ico'; 31 } 32 33 // create the icon and add it to the button 34 ico.src = DOKU_BASE+'lib/images/toolbar/'+icon; 35 btn.appendChild(ico); 36 37 return btn; 38} 39 40/** 41 * Creates a picker window for inserting text 42 * 43 * The given list can be an associative array with text,icon pairs 44 * or a simple list of text. Style the picker window through the picker 45 * class or the picker buttons with the pickerbutton class. Picker 46 * windows are appended to the body and created invisible. 47 * 48 * @author Andreas Gohr <andi@splitbrain.org> 49 */ 50function createPicker(id,list,icobase,edid){ 51 var cnt = list.length; 52 53 var picker = document.createElement('div'); 54 picker.className = 'picker'; 55 picker.id = id; 56 picker.style.position = 'absolute'; 57 picker.style.display = 'none'; 58 59 for(var key in list){ 60 if (!list.hasOwnProperty(key)) continue; 61 var btn = document.createElement('button'); 62 63 btn.className = 'pickerbutton'; 64 65 // associative array? 66 if(isNaN(key)){ 67 var ico = document.createElement('img'); 68 ico.src = DOKU_BASE+'lib/images/'+icobase+'/'+list[key]; 69 btn.title = key; 70 btn.appendChild(ico); 71 eval("btn.onclick = function(){pickerInsert('"+id+"','"+ 72 jsEscape(key)+"','"+ 73 jsEscape(edid)+"');return false;}"); 74 }else{ 75 var txt = document.createTextNode(list[key]); 76 btn.title = list[key]; 77 btn.appendChild(txt); 78 eval("btn.onclick = function(){pickerInsert('"+id+"','"+ 79 jsEscape(list[key])+"','"+ 80 jsEscape(edid)+"');return false;}"); 81 } 82 83 picker.appendChild(btn); 84 } 85 var body = document.getElementsByTagName('body')[0]; 86 body.appendChild(picker); 87} 88 89/** 90 * Called by picker buttons to insert Text and close the picker again 91 * 92 * @author Andreas Gohr <andi@splitbrain.org> 93 */ 94function pickerInsert(pickerid,text,edid){ 95 // insert 96 insertAtCarret(edid,text); 97 // close picker 98 pobj = document.getElementById(pickerid); 99 pobj.style.display = 'none'; 100} 101 102/** 103 * Show a previosly created picker window 104 * 105 * @author Andreas Gohr <andi@splitbrain.org> 106 */ 107function showPicker(pickerid,btn){ 108 var picker = document.getElementById(pickerid); 109 var x = findPosX(btn); 110 var y = findPosY(btn); 111 if(picker.style.display == 'none'){ 112 picker.style.display = 'block'; 113 picker.style.left = (x+3)+'px'; 114 picker.style.top = (y+btn.offsetHeight+3)+'px'; 115 }else{ 116 picker.style.display = 'none'; 117 } 118} 119 120/** 121 * Create a toolbar 122 * 123 * @param string tbid ID of the element where to insert the toolbar 124 * @param string edid ID of the editor textarea 125 * @param array tb Associative array defining the buttons 126 * @author Andreas Gohr <andi@splitbrain.org> 127 */ 128function initToolbar(tbid,edid,tb){ 129 var toolbar = $(tbid); 130 if(!toolbar) return; 131 132 //empty the toolbar area: 133 toolbar.innerHTML=''; 134 135 var cnt = tb.length; 136 for(var i=0; i<cnt; i++){ 137 // create new button 138 btn = createToolButton(tb[i]['icon'], 139 tb[i]['title'], 140 tb[i]['key']); 141 142 // add button action dependend on type 143 switch(tb[i]['type']){ 144 case 'format': 145 var sample = tb[i]['title']; 146 if(tb[i]['sample']){ sample = tb[i]['sample']; } 147 148 eval("btn.onclick = function(){insertTags('"+ 149 jsEscape(edid)+"','"+ 150 jsEscape(tb[i]['open'])+"','"+ 151 jsEscape(tb[i]['close'])+"','"+ 152 jsEscape(sample)+ 153 "');return false;}"); 154 toolbar.appendChild(btn); 155 break; 156 case 'insert': 157 eval("btn.onclick = function(){insertAtCarret('"+ 158 jsEscape(edid)+"','"+ 159 jsEscape(tb[i]['insert'])+ 160 "');return false;}"); 161 toolbar.appendChild(btn); 162 break; 163 case 'signature': 164 if(typeof(SIG) != 'undefined' && SIG != ''){ 165 eval("btn.onclick = function(){insertAtCarret('"+ 166 jsEscape(edid)+"','"+ 167 jsEscape(SIG)+ 168 "');return false;}"); 169 toolbar.appendChild(btn); 170 } 171 break; 172 case 'picker': 173 createPicker('picker'+i, 174 tb[i]['list'], 175 tb[i]['icobase'], 176 edid); 177 eval("btn.onclick = function(){showPicker('picker"+i+ 178 "',this);return false;}"); 179 toolbar.appendChild(btn); 180 break; 181 case 'mediapopup': 182 eval("btn.onclick = function(){window.open('"+ 183 jsEscape(tb[i]['url']+NS)+"','"+ 184 jsEscape(tb[i]['name'])+"','"+ 185 jsEscape(tb[i]['options'])+ 186 "');return false;}"); 187 toolbar.appendChild(btn); 188 break; 189 } // end switch 190 } // end for 191} 192 193/** 194 * Format selection 195 * 196 * Apply tagOpen/tagClose to selection in textarea, use sampleText instead 197 * of selection if there is none. Copied and adapted from phpBB 198 * 199 * @author phpBB development team 200 * @author MediaWiki development team 201 * @author Andreas Gohr <andi@splitbrain.org> 202 * @author Jim Raynor <jim_raynor@web.de> 203 */ 204function insertTags(edid,tagOpen, tagClose, sampleText) { 205 var txtarea = document.getElementById(edid); 206 // IE 207 if(document.selection && !is_gecko) { 208 var theSelection = document.selection.createRange().text; 209 var replaced = true; 210 if(!theSelection){ 211 replaced = false; 212 theSelection=sampleText; 213 } 214 txtarea.focus(); 215 216 // This has change 217 var text = theSelection; 218 if(theSelection.charAt(theSelection.length - 1) == " "){// exclude ending space char, if any 219 theSelection = theSelection.substring(0, theSelection.length - 1); 220 r = document.selection.createRange(); 221 r.text = tagOpen + theSelection + tagClose + " "; 222 } else { 223 r = document.selection.createRange(); 224 r.text = tagOpen + theSelection + tagClose; 225 } 226 if(!replaced){ 227 r.moveStart('character',-text.length-tagClose.length); 228 r.moveEnd('character',-tagClose.length); 229 } 230 r.select(); 231 // Mozilla 232 } else if(txtarea.selectionStart || txtarea.selectionStart == '0') { 233 replaced = false; 234 var startPos = txtarea.selectionStart; 235 var endPos = txtarea.selectionEnd; 236 if(endPos - startPos){ replaced = true; } 237 var scrollTop=txtarea.scrollTop; 238 var myText = (txtarea.value).substring(startPos, endPos); 239 if(!myText) { myText=sampleText;} 240 if(myText.charAt(myText.length - 1) == " "){ // exclude ending space char, if any 241 subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " "; 242 } else { 243 subst = tagOpen + myText + tagClose; 244 } 245 txtarea.value = txtarea.value.substring(0, startPos) + subst + 246 txtarea.value.substring(endPos, txtarea.value.length); 247 txtarea.focus(); 248 249 //set new selection 250 if(replaced){ 251 var cPos=startPos+(tagOpen.length+myText.length+tagClose.length); 252 txtarea.selectionStart=cPos; 253 txtarea.selectionEnd=cPos; 254 }else{ 255 txtarea.selectionStart=startPos+tagOpen.length; 256 txtarea.selectionEnd=startPos+tagOpen.length+myText.length; 257 } 258 txtarea.scrollTop=scrollTop; 259 // All others 260 } else { 261 var copy_alertText=alertText; 262 var re1=new RegExp("\\$1","g"); 263 var re2=new RegExp("\\$2","g"); 264 copy_alertText=copy_alertText.replace(re1,sampleText); 265 copy_alertText=copy_alertText.replace(re2,tagOpen+sampleText+tagClose); 266 267 if (sampleText) { 268 text=prompt(copy_alertText); 269 } else { 270 text=""; 271 } 272 if(!text) { text=sampleText;} 273 text=tagOpen+text+tagClose; 274 //append to the end 275 txtarea.value += "\n"+text; 276 277 // in Safari this causes scrolling 278 if(!is_safari) { 279 txtarea.focus(); 280 } 281 282 } 283 // reposition cursor if possible 284 if (txtarea.createTextRange){ 285 txtarea.caretPos = document.selection.createRange().duplicate(); 286 } 287} 288 289/* 290 * Insert the given value at the current cursor position 291 * 292 * @see http://www.alexking.org/index.php?content=software/javascript/content.php 293 */ 294function insertAtCarret(edid,value){ 295 var field = document.getElementById(edid); 296 297 //IE support 298 if (document.selection) { 299 field.focus(); 300 sel = document.selection.createRange(); 301 sel.text = value; 302 //MOZILLA/NETSCAPE support 303 }else if (field.selectionStart || field.selectionStart == '0') { 304 var startPos = field.selectionStart; 305 var endPos = field.selectionEnd; 306 var scrollTop = field.scrollTop; 307 field.value = field.value.substring(0, startPos) + 308 value + 309 field.value.substring(endPos, field.value.length); 310 311 field.focus(); 312 var cPos=startPos+(value.length); 313 field.selectionStart=cPos; 314 field.selectionEnd=cPos; 315 field.scrollTop=scrollTop; 316 } else { 317 field.value += "\n"+value; 318 } 319 // reposition cursor if possible 320 if (field.createTextRange){ 321 field.caretPos = document.selection.createRange().duplicate(); 322 } 323} 324 325 326/** 327 * global var used for not saved yet warning 328 */ 329var textChanged = false; 330 331/** 332 * Check for changes before leaving the page 333 */ 334function changeCheck(msg){ 335 if(textChanged){ 336 var ok = confirm(msg); 337 if(ok){ 338 // remove a possibly saved draft using ajax 339 var dwform = $('dw__editform'); 340 if(dwform){ 341 var params = 'call=draftdel'; 342 params += '&id='+encodeURIComponent(dwform.elements.id.value); 343 344 var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 345 sackobj.AjaxFailedAlert = ''; 346 sackobj.encodeURIString = false; 347 sackobj.runAJAX(params); 348 // we send this request blind without waiting for 349 // and handling the returned data 350 } 351 } 352 return ok; 353 }else{ 354 return true; 355 } 356} 357 358/** 359 * Add changeCheck to all Links and Forms (except those with a 360 * JSnocheck class), add handlers to monitor changes 361 * 362 * Sets focus to the editbox as well 363 */ 364function initChangeCheck(msg){ 365 if(!document.getElementById){ return false; } 366 // add change check for links 367 var links = document.getElementsByTagName('a'); 368 for(var i=0; i < links.length; i++){ 369 if(links[i].className.indexOf('JSnocheck') == -1){ 370 links[i].onclick = function(){ 371 var rc = changeCheck(msg); 372 if(window.event) window.event.returnValue = rc; 373 return rc; 374 }; 375 links[i].onkeypress = function(){ 376 var rc = changeCheck(msg); 377 if(window.event) window.event.returnValue = rc; 378 return rc; 379 }; 380 } 381 } 382 // add change check for forms 383 var forms = document.forms; 384 for(i=0; i < forms.length; i++){ 385 if(forms[i].className.indexOf('JSnocheck') == -1){ 386 forms[i].onsubmit = function(){ 387 var rc = changeCheck(msg); 388 if(window.event) window.event.returnValue = rc; 389 return rc; 390 }; 391 } 392 } 393 394 // reset change memory var on submit 395 var btn_save = document.getElementById('edbtn__save'); 396 btn_save.onclick = function(){ textChanged = false; }; 397 btn_save.onkeypress = function(){ textChanged = false; }; 398 var btn_prev = document.getElementById('edbtn__preview'); 399 btn_prev.onclick = function(){ textChanged = false; }; 400 btn_prev.onkeypress = function(){ textChanged = false; }; 401 402 // add change memory setter 403 var edit_text = document.getElementById('wiki__text'); 404 edit_text.onchange = function(){ 405 textChanged = true; //global var 406 summaryCheck(); 407 }; 408 edit_text.onkeyup = summaryCheck; 409 var summary = document.getElementById('edit__summary'); 410 addEvent(summary, 'change', summaryCheck); 411 addEvent(summary, 'keyup', summaryCheck); 412 if (textChanged) summaryCheck(); 413 414 // set focus 415 edit_text.focus(); 416} 417 418/** 419 * Checks if a summary was entered - if not the style is changed 420 * 421 * @author Andreas Gohr <andi@splitbrain.org> 422 */ 423function summaryCheck(){ 424 var sum = document.getElementById('edit__summary'); 425 if(sum.value === ''){ 426 sum.className='missing'; 427 }else{ 428 sum.className='edit'; 429 } 430} 431 432 433/** 434 * Class managing the timer to display a warning on a expiring lock 435 */ 436function locktimer_class(){ 437 this.sack = null; 438 this.timeout = 0; 439 this.timerID = null; 440 this.lasttime = null; 441 this.msg = ''; 442 this.pageid = ''; 443}; 444var locktimer = new locktimer_class(); 445 locktimer.init = function(timeout,msg,draft){ 446 // init values 447 locktimer.timeout = timeout*1000; 448 locktimer.msg = msg; 449 locktimer.draft = draft; 450 locktimer.lasttime = new Date(); 451 452 if(!$('dw__editform')) return; 453 locktimer.pageid = $('dw__editform').elements.id.value; 454 if(!locktimer.pageid) return; 455 456 // init ajax component 457 locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 458 locktimer.sack.AjaxFailedAlert = ''; 459 locktimer.sack.encodeURIString = false; 460 locktimer.sack.onCompletion = locktimer.refreshed; 461 462 // register refresh event 463 addEvent($('dw__editform').elements.wikitext,'keyup',function(){locktimer.refresh();}); 464 465 // start timer 466 locktimer.reset(); 467 }; 468 469 /** 470 * (Re)start the warning timer 471 */ 472 locktimer.reset = function(){ 473 locktimer.clear(); 474 locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout); 475 }; 476 477 /** 478 * Display the warning about the expiring lock 479 */ 480 locktimer.warning = function(){ 481 locktimer.clear(); 482 alert(locktimer.msg); 483 }; 484 485 /** 486 * Remove the current warning timer 487 */ 488 locktimer.clear = function(){ 489 if(locktimer.timerID !== null){ 490 window.clearTimeout(locktimer.timerID); 491 locktimer.timerID = null; 492 } 493 }; 494 495 /** 496 * Refresh the lock via AJAX 497 * 498 * Called on keypresses in the edit area 499 */ 500 locktimer.refresh = function(){ 501 var now = new Date(); 502 // refresh every minute only 503 if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time 504 var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid); 505 if(locktimer.draft){ 506 var dwform = $('dw__editform'); 507 params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value); 508 params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value); 509 params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value); 510 params += '&date='+encodeURIComponent(dwform.elements.date.value); 511 } 512 locktimer.sack.runAJAX(params); 513 locktimer.lasttime = now; 514 } 515 }; 516 517 518 /** 519 * Callback. Resets the warning timer 520 */ 521 locktimer.refreshed = function(){ 522 var data = this.response; 523 var error = data.charAt(0); 524 data = data.substring(1); 525 526 $('draft__status').innerHTML=data; 527 if(error != '1') return; // locking failed 528 locktimer.reset(); 529 }; 530// end of locktimer class functions 531 532