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