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 += ' ['+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 * @param string id the ID to assign to the picker 53 * @param array props the properties for the picker 54 * @param string edid the ID of the textarea 55 * @rteurn DOMobject the created picker 56 * @author Andreas Gohr <andi@splitbrain.org> 57 */ 58function createPicker(id,props,edid){ 59 var icobase = props['icobase']; 60 var list = props['list']; 61 62 // create the wrapping div 63 var picker = document.createElement('div'); 64 picker.className = 'picker '+props['class']; 65 picker.id = id; 66 picker.style.position = 'absolute'; 67 picker.style.display = 'none'; 68 69 for(var key in list){ 70 if (!list.hasOwnProperty(key)) continue; 71 72 if(isNaN(key)){ 73 // associative array -> treat as image/value pairs 74 var btn = document.createElement('button'); 75 btn.className = 'pickerbutton'; 76 var ico = document.createElement('img'); 77 if(list[key].substr(0,1) == '/'){ 78 ico.src = list[key]; 79 }else{ 80 ico.src = DOKU_BASE+'lib/images/'+icobase+'/'+list[key]; 81 } 82 btn.title = key; 83 btn.appendChild(ico); 84 eval("btn.onclick = function(){pickerInsert('"+ 85 jsEscape(key)+"','"+ 86 jsEscape(edid)+"');return false;}"); 87 picker.appendChild(btn); 88 }else if(isString(list[key])){ 89 // a list of text -> treat as text picker 90 var btn = document.createElement('button'); 91 btn.className = 'pickerbutton'; 92 var txt = document.createTextNode(list[key]); 93 btn.title = list[key]; 94 btn.appendChild(txt); 95 eval("btn.onclick = function(){pickerInsert('"+ 96 jsEscape(list[key])+"','"+ 97 jsEscape(edid)+"');return false;}"); 98 picker.appendChild(btn); 99 }else{ 100 // a list of lists -> treat it as subtoolbar 101 initToolbar(picker,edid,list); 102 break; // all buttons handled already 103 } 104 105 } 106 var body = document.getElementsByTagName('body')[0]; 107 body.appendChild(picker); 108 return picker; 109} 110 111/** 112 * Called by picker buttons to insert Text and close the picker again 113 * 114 * @author Andreas Gohr <andi@splitbrain.org> 115 */ 116function pickerInsert(text,edid){ 117 // insert 118 insertAtCarret(edid,text); 119 // close picker 120 pickerClose(); 121} 122 123/** 124 * Show a previosly created picker window 125 * 126 * @author Andreas Gohr <andi@splitbrain.org> 127 */ 128function showPicker(pickerid,btn){ 129 var picker = document.getElementById(pickerid); 130 var x = findPosX(btn); 131 var y = findPosY(btn); 132 if(picker.style.display == 'none'){ 133 picker.style.display = 'block'; 134 picker.style.left = (x+3)+'px'; 135 picker.style.top = (y+btn.offsetHeight+3)+'px'; 136 }else{ 137 picker.style.display = 'none'; 138 } 139} 140 141/** 142 * Add button action for signature button 143 * 144 * @param DOMElement btn Button element to add the action to 145 * @param array props Associative array of button properties 146 * @param string edid ID of the editor textarea 147 * @return boolean If button should be appended 148 * @author Gabriel Birke <birke@d-scribe.de> 149 */ 150function addBtnActionSignature(btn, props, edid) 151{ 152 if(typeof(SIG) != 'undefined' && SIG != ''){ 153 eval("btn.onclick = function(){insertAtCarret('"+ 154 jsEscape(edid)+"','"+ 155 jsEscape(SIG)+ 156 "');return false;}"); 157 return true; 158 } 159 return false; 160} 161 162 163/** 164 * Add button action for the mediapopup button 165 * 166 * @param DOMElement btn Button element to add the action to 167 * @param array props Associative array of button properties 168 * @return boolean If button should be appended 169 * @author Gabriel Birke <birke@d-scribe.de> 170 */ 171function addBtnActionMediapopup(btn, props) 172{ 173 eval("btn.onclick = function(){window.open('"+DOKU_BASE+ 174 jsEscape(props['url']+encodeURIComponent(NS))+"','"+ 175 jsEscape(props['name'])+"','"+ 176 jsEscape(props['options'])+ 177 "');return false;}"); 178 return true; 179} 180 181function addBtnActionAutohead(btn, props, edid, id) 182{ 183 eval("btn.onclick = function(){"+ 184 "insertHeadline('"+edid+"',"+props['mod']+",'"+jsEscape(props['text'])+"'); "+ 185 "return false};"); 186 return true; 187} 188 189/** 190 * Make intended formattings easier to handle 191 * 192 * Listens to all key inputs and handle indentions 193 * of lists and code blocks 194 * 195 * Currently handles space, backspce and enter presses 196 * 197 * @author Andreas Gohr <andi@splitbrain.org> 198 * @fixme handle tabs 199 */ 200function keyHandler(e){ 201 if(e.keyCode != 13 && 202 e.keyCode != 8 && 203 e.keyCode != 32) return; 204 var field = e.target; 205 var selection = getSelection(field); 206 var search = "\n"+field.value.substr(0,selection.start); 207 var linestart = Math.max(search.lastIndexOf("\n"), 208 search.lastIndexOf("\r")); //IE workaround 209 search = search.substr(linestart); 210 211 if(e.keyCode == 13){ // Enter 212 // keep current indention for lists and code 213 var match = search.match(/(\n +([\*-] ?)?)/); 214 if(match){ 215 insertAtCarret(field.id,match[1]); 216 e.preventDefault(); // prevent enter key 217 } 218 }else if(e.keyCode == 8){ // Backspace 219 // unindent lists 220 var match = search.match(/(\n +)([*-] ?)$/); 221 if(match){ 222 var spaces = match[1].length-1; 223 224 if(spaces > 3){ // unindent one level 225 field.value = field.value.substr(0,linestart)+ 226 field.value.substr(linestart+2); 227 selection.start = selection.start - 2; 228 selection.end = selection.start; 229 }else{ // delete list point 230 field.value = field.value.substr(0,linestart)+ 231 field.value.substr(selection.start); 232 selection.start = linestart; 233 selection.end = linestart; 234 } 235 setSelection(selection); 236 e.preventDefault(); // prevent backspace 237 } 238 }else if(e.keyCode == 32){ // Space 239 // intend list item 240 var match = search.match(/(\n +)([*-] )$/); 241 if(match){ 242 field.value = field.value.substr(0,linestart)+' '+ 243 field.value.substr(linestart); 244 selection.start = selection.start + 2; 245 selection.end = selection.start; 246 setSelection(selection); 247 e.preventDefault(); // prevent space 248 } 249 } 250} 251 252//FIXME consolidate somewhere else 253addInitEvent(function(){ 254 var field = $('wiki__text'); 255 if(!field) return; 256 addEvent(field,'keydown',keyHandler); 257}); 258 259/** 260 * Determine the current section level while editing 261 * 262 * @author Andreas Gohr <gohr@cosmocode.de> 263 */ 264function currentHeadlineLevel(textboxId){ 265 var field = $(textboxId); 266 var selection = getSelection(field); 267 var search = "\n"+field.value.substr(0,selection.start); 268 var lasthl = search.lastIndexOf("\n=="); 269 if(lasthl == -1 && field.form.prefix){ 270 // we need to look in prefix context 271 search = field.form.prefix.value; 272 lasthl = search.lastIndexOf("\n=="); 273 } 274 search = search.substr(lasthl+1,6); 275 276 if(search == '======') return 1; 277 if(search.substr(0,5) == '=====') return 2; 278 if(search.substr(0,4) == '====') return 3; 279 if(search.substr(0,3) == '===') return 4; 280 if(search.substr(0,2) == '==') return 5; 281 282 return 0; 283} 284 285/** 286 * Insert a new headline based on the current section level 287 * 288 * @param string textboxId - the edit field ID 289 * @param int mod - the headline modificator ( -1, 0, 1) 290 * @param string text - the sample text passed to insertTags 291 */ 292function insertHeadline(textboxId,mod,text){ 293 var lvl = currentHeadlineLevel(textboxId); 294 295 296 // determine new level 297 lvl += mod; 298 if(lvl < 1) lvl = 1; 299 if(lvl > 5) lvl = 5; 300 301 var tags = '='; 302 for(var i=0; i<=5-lvl; i++) tags += '='; 303 insertTags(textboxId, tags+' ', ' '+tags+"\n", text); 304 pickerClose(); 305} 306 307/** 308 * global var used for not saved yet warning 309 */ 310var textChanged = false; 311 312/** 313 * Check for changes before leaving the page 314 */ 315function changeCheck(msg){ 316 if(textChanged){ 317 var ok = confirm(msg); 318 if(ok){ 319 // remove a possibly saved draft using ajax 320 var dwform = $('dw__editform'); 321 if(dwform){ 322 var params = 'call=draftdel'; 323 params += '&id='+encodeURIComponent(dwform.elements.id.value); 324 325 var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 326 sackobj.AjaxFailedAlert = ''; 327 sackobj.encodeURIString = false; 328 sackobj.runAJAX(params); 329 // we send this request blind without waiting for 330 // and handling the returned data 331 } 332 } 333 return ok; 334 }else{ 335 return true; 336 } 337} 338 339/** 340 * Add changeCheck to all Links and Forms (except those with a 341 * JSnocheck class), add handlers to monitor changes 342 * 343 * Sets focus to the editbox as well 344 */ 345function initChangeCheck(msg){ 346 if(!document.getElementById){ return false; } 347 // add change check for links 348 var links = document.getElementsByTagName('a'); 349 for(var i=0; i < links.length; i++){ 350 if(links[i].className.indexOf('JSnocheck') == -1){ 351 links[i].onclick = function(){ 352 var rc = changeCheck(msg); 353 if(window.event) window.event.returnValue = rc; 354 return rc; 355 }; 356 } 357 } 358 // add change check for forms 359 var forms = document.forms; 360 for(i=0; i < forms.length; i++){ 361 if(forms[i].className.indexOf('JSnocheck') == -1){ 362 forms[i].onsubmit = function(){ 363 var rc = changeCheck(msg); 364 if(window.event) window.event.returnValue = rc; 365 return rc; 366 }; 367 } 368 } 369 370 // reset change memory var on submit 371 var btn_save = document.getElementById('edbtn__save'); 372 btn_save.onclick = function(){ textChanged = false; }; 373 var btn_prev = document.getElementById('edbtn__preview'); 374 btn_prev.onclick = function(){ textChanged = false; }; 375 376 // add change memory setter 377 var edit_text = document.getElementById('wiki__text'); 378 edit_text.onchange = function(){ 379 textChanged = true; //global var 380 summaryCheck(); 381 }; 382 edit_text.onkeyup = summaryCheck; 383 var summary = document.getElementById('edit__summary'); 384 addEvent(summary, 'change', summaryCheck); 385 addEvent(summary, 'keyup', summaryCheck); 386 if (textChanged) summaryCheck(); 387 388 // set focus 389 edit_text.focus(); 390} 391 392/** 393 * Checks if a summary was entered - if not the style is changed 394 * 395 * @author Andreas Gohr <andi@splitbrain.org> 396 */ 397function summaryCheck(){ 398 var sum = document.getElementById('edit__summary'); 399 if(sum.value === ''){ 400 sum.className='missing'; 401 }else{ 402 sum.className='edit'; 403 } 404} 405 406 407/** 408 * Class managing the timer to display a warning on a expiring lock 409 */ 410function locktimer_class(){ 411 this.sack = null; 412 this.timeout = 0; 413 this.timerID = null; 414 this.lasttime = null; 415 this.msg = ''; 416 this.pageid = ''; 417}; 418var locktimer = new locktimer_class(); 419 locktimer.init = function(timeout,msg,draft){ 420 // init values 421 locktimer.timeout = timeout*1000; 422 locktimer.msg = msg; 423 locktimer.draft = draft; 424 locktimer.lasttime = new Date(); 425 426 if(!$('dw__editform')) return; 427 locktimer.pageid = $('dw__editform').elements.id.value; 428 if(!locktimer.pageid) return; 429 430 // init ajax component 431 locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php'); 432 locktimer.sack.AjaxFailedAlert = ''; 433 locktimer.sack.encodeURIString = false; 434 locktimer.sack.onCompletion = locktimer.refreshed; 435 436 // register refresh event 437 addEvent($('dw__editform').elements.wikitext,'keypress',function(){locktimer.refresh();}); 438 439 // start timer 440 locktimer.reset(); 441 }; 442 443 /** 444 * (Re)start the warning timer 445 */ 446 locktimer.reset = function(){ 447 locktimer.clear(); 448 locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout); 449 }; 450 451 /** 452 * Display the warning about the expiring lock 453 */ 454 locktimer.warning = function(){ 455 locktimer.clear(); 456 alert(locktimer.msg); 457 }; 458 459 /** 460 * Remove the current warning timer 461 */ 462 locktimer.clear = function(){ 463 if(locktimer.timerID !== null){ 464 window.clearTimeout(locktimer.timerID); 465 locktimer.timerID = null; 466 } 467 }; 468 469 /** 470 * Refresh the lock via AJAX 471 * 472 * Called on keypresses in the edit area 473 */ 474 locktimer.refresh = function(){ 475 var now = new Date(); 476 // refresh every minute only 477 if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time 478 var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid); 479 if(locktimer.draft){ 480 var dwform = $('dw__editform'); 481 params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value); 482 params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value); 483 params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value); 484 params += '&date='+encodeURIComponent(dwform.elements.date.value); 485 } 486 locktimer.sack.runAJAX(params); 487 locktimer.lasttime = now; 488 } 489 }; 490 491 492 /** 493 * Callback. Resets the warning timer 494 */ 495 locktimer.refreshed = function(){ 496 var data = this.response; 497 var error = data.charAt(0); 498 data = data.substring(1); 499 500 $('draft__status').innerHTML=data; 501 if(error != '1') return; // locking failed 502 locktimer.reset(); 503 }; 504// end of locktimer class functions 505 506