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 * @author Michal Rezler <m.rezler@centrum.cz> 15 */ 16function createToolButton(icon,label,key,id,classname){ 17 var $ = jQuery; 18 var btn = $('<button>'); 19 var ico = $('<img />'); 20 21 // preapare the basic button stuff 22 btn.attr('class', 'toolbutton'); 23 if(classname){ 24 btn.attr('class', 'toolbutton '+classname); 25 } 26 27 btn.attr('title', label); 28 if(key){ 29 btn.attr('title', label + ' ['+key.toUpperCase()+']') 30 .attr('accessKey', key); 31 } 32 33 // set IDs if given 34 if(id){ 35 btn.attr('id', id); 36 ico.attr('id', id+'_ico'); 37 } 38 39 // create the icon and add it to the button 40 if(icon.substr(0,1) == '/'){ 41 ico.attr('src', icon); 42 }else{ 43 ico.attr('src', DOKU_BASE+'lib/images/toolbar/'+icon); 44 } 45 btn.append(ico); 46 47 // we have to return a javascript object (for compatibility reasons) 48 return btn[0]; 49} 50 51/** 52 * Creates a picker window for inserting text 53 * 54 * The given list can be an associative array with text,icon pairs 55 * or a simple list of text. Style the picker window through the picker 56 * class or the picker buttons with the pickerbutton class. Picker 57 * windows are appended to the body and created invisible. 58 * 59 * @param string id the ID to assign to the picker 60 * @param array props the properties for the picker 61 * @param string edid the ID of the textarea 62 * @rteurn DOMobject the created picker 63 * @author Andreas Gohr <andi@splitbrain.org> 64 */ 65function createPicker(id,props,edid){ 66 var icobase = props['icobase']; 67 var list = props['list']; 68 var $ = jQuery; 69 70 // create the wrapping div 71 var picker = $('<div></div>'); 72 73 var className = 'picker'; 74 if(props['class']){ 75 className += ' '+props['class']; 76 } 77 78 picker.attr('class', className) 79 .attr('id', id) 80 .css('position', 'absolute') 81 .css('marginLeft', '-10000px') // no display:none, to keep access keys working 82 .css('marginTop', '-10000px'); 83 84 for(var key in list){ 85 if (!list.hasOwnProperty(key)) continue; 86 87 if(isNaN(key)){ 88 // associative array -> treat as image/value pairs 89 var btn = $('<button>'); 90 btn.attr('class', 'pickerbutton') 91 .attr('title', key); 92 93 var ico = $('<img>'); 94 if (list[key].substr(0,1) == '/') { 95 var src = list[key]; 96 } else { 97 var src = DOKU_BASE+'lib/images/'+icobase+'/'+list[key]; 98 } 99 100 ico.attr('src', src); 101 btn.append(ico); 102 103 btn.bind('click', bind(pickerInsert, key, edid)); 104 picker.append(btn); 105 }else if (typeof (list[key]) == 'string'){ 106 // a list of text -> treat as text picker 107 var btn = $('<button>'); 108 btn.attr('class', 'pickerbutton') 109 .attr('title', list[key]); 110 111 var txt = $(document.createTextNode(list[key])); 112 btn.append(txt); 113 114 btn.bind('click', bind(pickerInsert, list[key], edid)); 115 116 picker.append(btn); 117 }else{ 118 // a list of lists -> treat it as subtoolbar 119 initToolbar(picker,edid,list); 120 break; // all buttons handled already 121 } 122 123 } 124 var body = $('body'); 125 body.append(picker); 126 127 // we have to return a javascript object (for compatibility reasons) 128 return picker[0]; 129} 130 131/** 132 * Called by picker buttons to insert Text and close the picker again 133 * 134 * @author Andreas Gohr <andi@splitbrain.org> 135 */ 136function pickerInsert(text,edid){ 137 insertAtCarret(edid,text); 138 pickerClose(); 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 if(typeof(SIG) != 'undefined' && SIG != ''){ 152 btn.bind('click', bind(insertAtCarret,edid,SIG)); 153 return true; 154 } 155 return false; 156} 157 158/** 159 * Make intended formattings easier to handle 160 * 161 * Listens to all key inputs and handle indentions 162 * of lists and code blocks 163 * 164 * Currently handles space, backspce and enter presses 165 * 166 * @author Andreas Gohr <andi@splitbrain.org> 167 * @fixme handle tabs 168 */ 169function keyHandler(e){ 170 if(e.keyCode != 13 && 171 e.keyCode != 8 && 172 e.keyCode != 32) return; 173 var field = e.target; 174 var selection = getSelection(field); 175 if(selection.getLength()) return; //there was text selected, keep standard behavior 176 var search = "\n"+field.value.substr(0,selection.start); 177 var linestart = Math.max(search.lastIndexOf("\n"), 178 search.lastIndexOf("\r")); //IE workaround 179 search = search.substr(linestart); 180 181 182 if(e.keyCode == 13){ // Enter 183 // keep current indention for lists and code 184 var match = search.match(/(\n +([\*-] ?)?)/); 185 if(match){ 186 var scroll = field.scrollHeight; 187 var match2 = search.match(/^\n +[\*-]\s*$/); 188 // Cancel list if the last item is empty (i. e. two times enter) 189 if (match2 && field.value.substr(selection.start).match(/^($|\r?\n)/)) { 190 field.value = field.value.substr(0, linestart) + "\n" + 191 field.value.substr(selection.start); 192 selection.start = linestart + 1; 193 selection.end = linestart + 1; 194 setSelection(selection); 195 } else { 196 insertAtCarret(field.id,match[1]); 197 } 198 field.scrollTop += (field.scrollHeight - scroll); 199 e.preventDefault(); // prevent enter key 200 return false; 201 } 202 }else if(e.keyCode == 8){ // Backspace 203 // unindent lists 204 var match = search.match(/(\n +)([*-] ?)$/); 205 if(match){ 206 var spaces = match[1].length-1; 207 208 if(spaces > 3){ // unindent one level 209 field.value = field.value.substr(0,linestart)+ 210 field.value.substr(linestart+2); 211 selection.start = selection.start - 2; 212 selection.end = selection.start; 213 }else{ // delete list point 214 field.value = field.value.substr(0,linestart)+ 215 field.value.substr(selection.start); 216 selection.start = linestart; 217 selection.end = linestart; 218 } 219 setSelection(selection); 220 e.preventDefault(); // prevent backspace 221 return false; 222 } 223 }else if(e.keyCode == 32){ // Space 224 // intend list item 225 var match = search.match(/(\n +)([*-] )$/); 226 if(match){ 227 field.value = field.value.substr(0,linestart)+' '+ 228 field.value.substr(linestart); 229 selection.start = selection.start + 2; 230 selection.end = selection.start; 231 setSelection(selection); 232 e.preventDefault(); // prevent space 233 return false; 234 } 235 } 236} 237 238/** 239 * Determine the current section level while editing 240 * 241 * @author Andreas Gohr <gohr@cosmocode.de> 242 */ 243function currentHeadlineLevel(textboxId){ 244 var field = $(textboxId); 245 var selection = getSelection(field); 246 var search = "\n"+field.value.substr(0,selection.start); 247 var lasthl = search.lastIndexOf("\n=="); 248 if(lasthl == -1 && field.form.prefix){ 249 // we need to look in prefix context 250 search = field.form.prefix.value; 251 lasthl = search.lastIndexOf("\n=="); 252 } 253 search = search.substr(lasthl+1,6); 254 255 if(search == '======') return 1; 256 if(search.substr(0,5) == '=====') return 2; 257 if(search.substr(0,4) == '====') return 3; 258 if(search.substr(0,3) == '===') return 4; 259 if(search.substr(0,2) == '==') return 5; 260 261 return 0; 262} 263 264 265/** 266 * global var used for not saved yet warning 267 */ 268window.textChanged = false; 269 270/** 271 * Delete the draft before leaving the page 272 */ 273function deleteDraft() { 274 if (is_opera) return; 275 if (window.keepDraft) return; 276 277 // remove a possibly saved draft using ajax 278 var dwform = jQuery('#dw__editform'); 279 if(dwform.length != 0) { 280 281 jQuery.post( 282 DOKU_BASE + 'lib/exe/ajax.php', 283 { 284 call: 'draftdel', 285 id: jQuery('#dw__editform input[name=id]').val() 286 } 287 ); 288 } 289} 290 291/** 292 * Activate "not saved" dialog, add draft deletion to page unload, 293 * add handlers to monitor changes 294 * 295 * Sets focus to the editbox as well 296 */ 297addInitEvent(function () { 298 var $ = jQuery; 299 var editform = $('#dw__editform'); 300 if (editform.length == 0) return; 301 302 var edit_text = $('#wiki__text'); 303 if (edit_text.length > 0) { 304 if(edit_text.attr('readOnly')) return; 305 306 // in Firefox, keypress doesn't send the correct keycodes, 307 // in Opera, the default of keydown can't be prevented 308 if (is_opera) { 309 edit_text.keypress(keyHandler); 310 } else { 311 edit_text.keydown(keyHandler); 312 } 313 314 // set focus and place cursor at the start 315 var sel = getSelection(edit_text.get(0)); 316 sel.start = 0; 317 sel.end = 0; 318 setSelection(sel); 319 edit_text.focus(); 320 } 321 322 var checkfunc = function() { 323 textChanged = true; //global var 324 summaryCheck(); 325 }; 326 327 editform.change(checkfunc); 328 editform.keydown(checkfunc); 329 330 window.onbeforeunload = function(){ 331 if(window.textChanged) { 332 return LANG.notsavedyet; 333 } 334 }; 335 window.onunload = deleteDraft; 336 337 // reset change memory var on submit 338 $('#edbtn__save').click( 339 function() { 340 window.onbeforeunload = ''; 341 textChanged = false; 342 } 343 ); 344 $('#edbtn__preview').click( 345 function() { 346 window.onbeforeunload = ''; 347 textChanged = false; 348 window.keepDraft = true; // needed to keep draft on page unload 349 } 350 ); 351 352 var summary = $('#edit__summary'); 353 summary.change(summaryCheck); 354 summary.keyup(summaryCheck); 355 356 if (textChanged) summaryCheck(); 357}); 358 359/** 360 * Checks if a summary was entered - if not the style is changed 361 * 362 * @author Andreas Gohr <andi@splitbrain.org> 363 */ 364function summaryCheck(){ 365 var sum = jQuery('#edit__summary'); 366 367 if (sum.val() === '') { 368 sum.attr('class', 'missing'); 369 } else{ 370 sum.attr('class', 'edit'); 371 } 372} 373