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