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 363 //MOZILLA/NETSCAPE support 364 }else if (field.selectionStart || field.selectionStart == '0') { 365 var startPos = field.selectionStart; 366 var endPos = field.selectionEnd; 367 var scrollTop = field.scrollTop; 368 field.value = field.value.substring(0, startPos) + 369 value + 370 field.value.substring(endPos, field.value.length); 371 372 field.focus(); 373 var cPos=startPos+(value.length); 374 field.selectionStart=cPos; 375 field.selectionEnd=cPos; 376 field.scrollTop=scrollTop; 377 } else { 378 field.value += "\n"+value; 379 } 380 // reposition cursor if possible 381 if (field.createTextRange){ 382 field.caretPos = document.selection.createRange().duplicate(); 383 } 384} 385 386 387/** 388 * global var used for not saved yet warning 389 */ 390var textChanged = false; 391 392/** 393 * Check for changes before leaving the page 394 */ 395function changeCheck(msg){ 396 if(textChanged){ 397 var ok = confirm(msg); 398 if(ok){ 399 // remove a possibly saved draft using ajax 400 var dwform = $('dw__editform'); 401 if(dwform){ 402 var params = 'call=draftdel'; 403 params += '&id='+encodeURIComponent(dwform.elements.id.value); 404 405 var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 406 sackobj.AjaxFailedAlert = ''; 407 sackobj.encodeURIString = false; 408 sackobj.runAJAX(params); 409 // we send this request blind without waiting for 410 // and handling the returned data 411 } 412 } 413 return ok; 414 }else{ 415 return true; 416 } 417} 418 419/** 420 * Add changeCheck to all Links and Forms (except those with a 421 * JSnocheck class), add handlers to monitor changes 422 * 423 * Sets focus to the editbox as well 424 */ 425function initChangeCheck(msg){ 426 if(!document.getElementById){ return false; } 427 // add change check for links 428 var links = document.getElementsByTagName('a'); 429 for(var i=0; i < links.length; i++){ 430 if(links[i].className.indexOf('JSnocheck') == -1){ 431 links[i].onclick = function(){ 432 var rc = changeCheck(msg); 433 if(window.event) window.event.returnValue = rc; 434 return rc; 435 }; 436 links[i].onkeypress = function(){ 437 var rc = changeCheck(msg); 438 if(window.event) window.event.returnValue = rc; 439 return rc; 440 }; 441 } 442 } 443 // add change check for forms 444 var forms = document.forms; 445 for(i=0; i < forms.length; i++){ 446 if(forms[i].className.indexOf('JSnocheck') == -1){ 447 forms[i].onsubmit = function(){ 448 var rc = changeCheck(msg); 449 if(window.event) window.event.returnValue = rc; 450 return rc; 451 }; 452 } 453 } 454 455 // reset change memory var on submit 456 var btn_save = document.getElementById('edbtn__save'); 457 btn_save.onclick = function(){ textChanged = false; }; 458 btn_save.onkeypress = function(){ textChanged = false; }; 459 var btn_prev = document.getElementById('edbtn__preview'); 460 btn_prev.onclick = function(){ textChanged = false; }; 461 btn_prev.onkeypress = function(){ textChanged = false; }; 462 463 // add change memory setter 464 var edit_text = document.getElementById('wiki__text'); 465 edit_text.onchange = function(){ 466 textChanged = true; //global var 467 summaryCheck(); 468 }; 469 edit_text.onkeyup = summaryCheck; 470 var summary = document.getElementById('edit__summary'); 471 addEvent(summary, 'change', summaryCheck); 472 addEvent(summary, 'keyup', summaryCheck); 473 if (textChanged) summaryCheck(); 474 475 // set focus 476 edit_text.focus(); 477} 478 479/** 480 * Checks if a summary was entered - if not the style is changed 481 * 482 * @author Andreas Gohr <andi@splitbrain.org> 483 */ 484function summaryCheck(){ 485 var sum = document.getElementById('edit__summary'); 486 if(sum.value === ''){ 487 sum.className='missing'; 488 }else{ 489 sum.className='edit'; 490 } 491} 492 493 494/** 495 * Class managing the timer to display a warning on a expiring lock 496 */ 497function locktimer_class(){ 498 this.sack = null; 499 this.timeout = 0; 500 this.timerID = null; 501 this.lasttime = null; 502 this.msg = ''; 503 this.pageid = ''; 504}; 505var locktimer = new locktimer_class(); 506 locktimer.init = function(timeout,msg,draft){ 507 // init values 508 locktimer.timeout = timeout*1000; 509 locktimer.msg = msg; 510 locktimer.draft = draft; 511 locktimer.lasttime = new Date(); 512 513 if(!$('dw__editform')) return; 514 locktimer.pageid = $('dw__editform').elements.id.value; 515 if(!locktimer.pageid) return; 516 517 // init ajax component 518 locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 519 locktimer.sack.AjaxFailedAlert = ''; 520 locktimer.sack.encodeURIString = false; 521 locktimer.sack.onCompletion = locktimer.refreshed; 522 523 // register refresh event 524 addEvent($('dw__editform').elements.wikitext,'keyup',function(){locktimer.refresh();}); 525 526 // start timer 527 locktimer.reset(); 528 }; 529 530 /** 531 * (Re)start the warning timer 532 */ 533 locktimer.reset = function(){ 534 locktimer.clear(); 535 locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout); 536 }; 537 538 /** 539 * Display the warning about the expiring lock 540 */ 541 locktimer.warning = function(){ 542 locktimer.clear(); 543 alert(locktimer.msg); 544 }; 545 546 /** 547 * Remove the current warning timer 548 */ 549 locktimer.clear = function(){ 550 if(locktimer.timerID !== null){ 551 window.clearTimeout(locktimer.timerID); 552 locktimer.timerID = null; 553 } 554 }; 555 556 /** 557 * Refresh the lock via AJAX 558 * 559 * Called on keypresses in the edit area 560 */ 561 locktimer.refresh = function(){ 562 var now = new Date(); 563 // refresh every minute only 564 if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time 565 var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid); 566 if(locktimer.draft){ 567 var dwform = $('dw__editform'); 568 params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value); 569 params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value); 570 params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value); 571 params += '&date='+encodeURIComponent(dwform.elements.date.value); 572 } 573 locktimer.sack.runAJAX(params); 574 locktimer.lasttime = now; 575 } 576 }; 577 578 579 /** 580 * Callback. Resets the warning timer 581 */ 582 locktimer.refreshed = function(){ 583 var data = this.response; 584 var error = data.charAt(0); 585 data = data.substring(1); 586 587 $('draft__status').innerHTML=data; 588 if(error != '1') return; // locking failed 589 locktimer.reset(); 590 }; 591// end of locktimer class functions 592 593