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