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 var btn = createToolButton(tb[i]['icon'], 139 tb[i]['title'], 140 tb[i]['key']); 141 142 var actionFunc = 'addBtnAction'+tb[i]['type'].charAt(0).toUpperCase()+tb[i]['type'].substring(1); 143 var exists = eval("typeof("+actionFunc+") == 'function'"); 144 if(exists) 145 { 146 if(eval(actionFunc+"(btn, tb[i], edid, i)")) 147 toolbar.appendChild(btn); 148 } 149 } // end for 150} 151 152/** 153 * Add button action for format buttons 154 * 155 * @param DOMElement btn Button element to add the action to 156 * @param array props Associative array of button properties 157 * @param string edid ID of the editor textarea 158 * @return boolean If button should be appended 159 * @author Gabriel Birke <birke@d-scribe.de> 160 */ 161function addBtnActionFormat(btn, props, edid) 162{ 163 var sample = props['title']; 164 if(props['sample']){ sample = props['sample']; } 165 eval("btn.onclick = function(){insertTags('"+ 166 jsEscape(edid)+"','"+ 167 jsEscape(props['open'])+"','"+ 168 jsEscape(props['close'])+"','"+ 169 jsEscape(sample)+ 170 "');return false;}"); 171 172 return true; 173} 174 175/** 176 * Add button action for insert buttons 177 * 178 * @param DOMElement btn Button element to add the action to 179 * @param array props Associative array of button properties 180 * @param string edid ID of the editor textarea 181 * @return boolean If button should be appended 182 * @author Gabriel Birke <birke@d-scribe.de> 183 */ 184function addBtnActionInsert(btn, props, edid) 185{ 186 eval("btn.onclick = function(){insertAtCarret('"+ 187 jsEscape(edid)+"','"+ 188 jsEscape(props['insert'])+ 189 "');return false;}"); 190 return true; 191} 192 193/** 194 * Add button action for signature button 195 * 196 * @param DOMElement btn Button element to add the action to 197 * @param array props Associative array of button properties 198 * @param string edid ID of the editor textarea 199 * @return boolean If button should be appended 200 * @author Gabriel Birke <birke@d-scribe.de> 201 */ 202function addBtnActionSignature(btn, props, edid) 203{ 204 if(typeof(SIG) != 'undefined' && SIG != ''){ 205 eval("btn.onclick = function(){insertAtCarret('"+ 206 jsEscape(edid)+"','"+ 207 jsEscape(SIG)+ 208 "');return false;}"); 209 return true; 210 } 211 return false; 212} 213 214/** 215 * Add button action for picker buttons and create picker element 216 * 217 * @param DOMElement btn Button element to add the action to 218 * @param array props Associative array of button properties 219 * @param string edid ID of the editor textarea 220 * @param int id Unique number of the picker 221 * @return boolean If button should be appended 222 * @author Gabriel Birke <birke@d-scribe.de> 223 */ 224function addBtnActionPicker(btn, props, edid, id) 225{ 226 createPicker('picker'+id, 227 props['list'], 228 props['icobase'], 229 edid); 230 eval("btn.onclick = function(){showPicker('picker"+id+ 231 "',this);return false;}"); 232 return true; 233} 234 235/** 236 * Add button action for the mediapopup button 237 * 238 * @param DOMElement btn Button element to add the action to 239 * @param array props Associative array of button properties 240 * @return boolean If button should be appended 241 * @author Gabriel Birke <birke@d-scribe.de> 242 */ 243function addBtnActionMediapopup(btn, props) 244{ 245 eval("btn.onclick = function(){window.open('"+ 246 jsEscape(props['url']+NS)+"','"+ 247 jsEscape(props['name'])+"','"+ 248 jsEscape(props['options'])+ 249 "');return false;}"); 250 return true; 251} 252 253/** 254 * Format selection 255 * 256 * Apply tagOpen/tagClose to selection in textarea, use sampleText instead 257 * of selection if there is none. Copied and adapted from phpBB 258 * 259 * @author phpBB development team 260 * @author MediaWiki development team 261 * @author Andreas Gohr <andi@splitbrain.org> 262 * @author Jim Raynor <jim_raynor@web.de> 263 */ 264function insertTags(edid,tagOpen, tagClose, sampleText) { 265 var txtarea = document.getElementById(edid); 266 // IE 267 if(document.selection && !is_gecko) { 268 var theSelection = document.selection.createRange().text; 269 var replaced = true; 270 if(!theSelection){ 271 replaced = false; 272 theSelection=sampleText; 273 } 274 txtarea.focus(); 275 276 // This has change 277 var text = theSelection; 278 if(theSelection.charAt(theSelection.length - 1) == " "){// exclude ending space char, if any 279 theSelection = theSelection.substring(0, theSelection.length - 1); 280 r = document.selection.createRange(); 281 r.text = tagOpen + theSelection + tagClose + " "; 282 } else { 283 r = document.selection.createRange(); 284 r.text = tagOpen + theSelection + tagClose; 285 } 286 if(!replaced){ 287 r.moveStart('character',-text.length-tagClose.length); 288 r.moveEnd('character',-tagClose.length); 289 } 290 r.select(); 291 // Mozilla 292 } else if(txtarea.selectionStart || txtarea.selectionStart == '0') { 293 replaced = false; 294 var startPos = txtarea.selectionStart; 295 var endPos = txtarea.selectionEnd; 296 if(endPos - startPos){ replaced = true; } 297 var scrollTop=txtarea.scrollTop; 298 var myText = (txtarea.value).substring(startPos, endPos); 299 if(!myText) { myText=sampleText;} 300 if(myText.charAt(myText.length - 1) == " "){ // exclude ending space char, if any 301 subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " "; 302 } else { 303 subst = tagOpen + myText + tagClose; 304 } 305 txtarea.value = txtarea.value.substring(0, startPos) + subst + 306 txtarea.value.substring(endPos, txtarea.value.length); 307 txtarea.focus(); 308 309 //set new selection 310 if(replaced){ 311 var cPos=startPos+(tagOpen.length+myText.length+tagClose.length); 312 txtarea.selectionStart=cPos; 313 txtarea.selectionEnd=cPos; 314 }else{ 315 txtarea.selectionStart=startPos+tagOpen.length; 316 txtarea.selectionEnd=startPos+tagOpen.length+myText.length; 317 } 318 txtarea.scrollTop=scrollTop; 319 // All others 320 } else { 321 var copy_alertText=alertText; 322 var re1=new RegExp("\\$1","g"); 323 var re2=new RegExp("\\$2","g"); 324 copy_alertText=copy_alertText.replace(re1,sampleText); 325 copy_alertText=copy_alertText.replace(re2,tagOpen+sampleText+tagClose); 326 327 if (sampleText) { 328 text=prompt(copy_alertText); 329 } else { 330 text=""; 331 } 332 if(!text) { text=sampleText;} 333 text=tagOpen+text+tagClose; 334 //append to the end 335 txtarea.value += "\n"+text; 336 337 // in Safari this causes scrolling 338 if(!is_safari) { 339 txtarea.focus(); 340 } 341 342 } 343 // reposition cursor if possible 344 if (txtarea.createTextRange){ 345 txtarea.caretPos = document.selection.createRange().duplicate(); 346 } 347} 348 349/* 350 * Insert the given value at the current cursor position 351 * 352 * @see http://www.alexking.org/index.php?content=software/javascript/content.php 353 */ 354function insertAtCarret(edid,value){ 355 var field = document.getElementById(edid); 356 357 //IE support 358 if (document.selection) { 359 field.focus(); 360 sel = document.selection.createRange(); 361 sel.text = value; 362 //MOZILLA/NETSCAPE support 363 }else if (field.selectionStart || field.selectionStart == '0') { 364 var startPos = field.selectionStart; 365 var endPos = field.selectionEnd; 366 var scrollTop = field.scrollTop; 367 field.value = field.value.substring(0, startPos) + 368 value + 369 field.value.substring(endPos, field.value.length); 370 371 field.focus(); 372 var cPos=startPos+(value.length); 373 field.selectionStart=cPos; 374 field.selectionEnd=cPos; 375 field.scrollTop=scrollTop; 376 } else { 377 field.value += "\n"+value; 378 } 379 // reposition cursor if possible 380 if (field.createTextRange){ 381 field.caretPos = document.selection.createRange().duplicate(); 382 } 383} 384 385 386/** 387 * global var used for not saved yet warning 388 */ 389var textChanged = false; 390 391/** 392 * Check for changes before leaving the page 393 */ 394function changeCheck(msg){ 395 if(textChanged){ 396 var ok = confirm(msg); 397 if(ok){ 398 // remove a possibly saved draft using ajax 399 var dwform = $('dw__editform'); 400 if(dwform){ 401 var params = 'call=draftdel'; 402 params += '&id='+encodeURIComponent(dwform.elements.id.value); 403 404 var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 405 sackobj.AjaxFailedAlert = ''; 406 sackobj.encodeURIString = false; 407 sackobj.runAJAX(params); 408 // we send this request blind without waiting for 409 // and handling the returned data 410 } 411 } 412 return ok; 413 }else{ 414 return true; 415 } 416} 417 418/** 419 * Add changeCheck to all Links and Forms (except those with a 420 * JSnocheck class), add handlers to monitor changes 421 * 422 * Sets focus to the editbox as well 423 */ 424function initChangeCheck(msg){ 425 if(!document.getElementById){ return false; } 426 // add change check for links 427 var links = document.getElementsByTagName('a'); 428 for(var i=0; i < links.length; i++){ 429 if(links[i].className.indexOf('JSnocheck') == -1){ 430 links[i].onclick = function(){ 431 var rc = changeCheck(msg); 432 if(window.event) window.event.returnValue = rc; 433 return rc; 434 }; 435 links[i].onkeypress = function(){ 436 var rc = changeCheck(msg); 437 if(window.event) window.event.returnValue = rc; 438 return rc; 439 }; 440 } 441 } 442 // add change check for forms 443 var forms = document.forms; 444 for(i=0; i < forms.length; i++){ 445 if(forms[i].className.indexOf('JSnocheck') == -1){ 446 forms[i].onsubmit = function(){ 447 var rc = changeCheck(msg); 448 if(window.event) window.event.returnValue = rc; 449 return rc; 450 }; 451 } 452 } 453 454 // reset change memory var on submit 455 var btn_save = document.getElementById('edbtn__save'); 456 btn_save.onclick = function(){ textChanged = false; }; 457 btn_save.onkeypress = function(){ textChanged = false; }; 458 var btn_prev = document.getElementById('edbtn__preview'); 459 btn_prev.onclick = function(){ textChanged = false; }; 460 btn_prev.onkeypress = function(){ textChanged = false; }; 461 462 // add change memory setter 463 var edit_text = document.getElementById('wiki__text'); 464 edit_text.onchange = function(){ 465 textChanged = true; //global var 466 summaryCheck(); 467 }; 468 edit_text.onkeyup = summaryCheck; 469 var summary = document.getElementById('edit__summary'); 470 addEvent(summary, 'change', summaryCheck); 471 addEvent(summary, 'keyup', summaryCheck); 472 if (textChanged) summaryCheck(); 473 474 // set focus 475 edit_text.focus(); 476} 477 478/** 479 * Checks if a summary was entered - if not the style is changed 480 * 481 * @author Andreas Gohr <andi@splitbrain.org> 482 */ 483function summaryCheck(){ 484 var sum = document.getElementById('edit__summary'); 485 if(sum.value === ''){ 486 sum.className='missing'; 487 }else{ 488 sum.className='edit'; 489 } 490} 491 492 493/** 494 * Class managing the timer to display a warning on a expiring lock 495 */ 496function locktimer_class(){ 497 this.sack = null; 498 this.timeout = 0; 499 this.timerID = null; 500 this.lasttime = null; 501 this.msg = ''; 502 this.pageid = ''; 503}; 504var locktimer = new locktimer_class(); 505 locktimer.init = function(timeout,msg,draft){ 506 // init values 507 locktimer.timeout = timeout*1000; 508 locktimer.msg = msg; 509 locktimer.draft = draft; 510 locktimer.lasttime = new Date(); 511 512 if(!$('dw__editform')) return; 513 locktimer.pageid = $('dw__editform').elements.id.value; 514 if(!locktimer.pageid) return; 515 516 // init ajax component 517 locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 518 locktimer.sack.AjaxFailedAlert = ''; 519 locktimer.sack.encodeURIString = false; 520 locktimer.sack.onCompletion = locktimer.refreshed; 521 522 // register refresh event 523 addEvent($('dw__editform').elements.wikitext,'keyup',function(){locktimer.refresh();}); 524 525 // start timer 526 locktimer.reset(); 527 }; 528 529 /** 530 * (Re)start the warning timer 531 */ 532 locktimer.reset = function(){ 533 locktimer.clear(); 534 locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout); 535 }; 536 537 /** 538 * Display the warning about the expiring lock 539 */ 540 locktimer.warning = function(){ 541 locktimer.clear(); 542 alert(locktimer.msg); 543 }; 544 545 /** 546 * Remove the current warning timer 547 */ 548 locktimer.clear = function(){ 549 if(locktimer.timerID !== null){ 550 window.clearTimeout(locktimer.timerID); 551 locktimer.timerID = null; 552 } 553 }; 554 555 /** 556 * Refresh the lock via AJAX 557 * 558 * Called on keypresses in the edit area 559 */ 560 locktimer.refresh = function(){ 561 var now = new Date(); 562 // refresh every minute only 563 if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time 564 var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid); 565 if(locktimer.draft){ 566 var dwform = $('dw__editform'); 567 params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value); 568 params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value); 569 params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value); 570 params += '&date='+encodeURIComponent(dwform.elements.date.value); 571 } 572 locktimer.sack.runAJAX(params); 573 locktimer.lasttime = now; 574 } 575 }; 576 577 578 /** 579 * Callback. Resets the warning timer 580 */ 581 locktimer.refreshed = function(){ 582 var data = this.response; 583 var error = data.charAt(0); 584 data = data.substring(1); 585 586 $('draft__status').innerHTML=data; 587 if(error != '1') return; // locking failed 588 locktimer.reset(); 589 }; 590// end of locktimer class functions 591 592