fixed scrolling issue in new list editing FS#1790
[dokuwiki.git] / lib / scripts / edit.js
blob11bf991fd8dc6fa2700cd13e43f01a4c02e2dc26
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  */
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  */
15 function createToolButton(icon,label,key,id){
16     var btn = document.createElement('button');
17     var ico = document.createElement('img');
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     }
27     // set IDs if given
28     if(id){
29         btn.id = id;
30         ico.id = id+'_ico';
31     }
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);
41     return btn;
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  */
58 function createPicker(id,props,edid){
59     var icobase = props['icobase'];
60     var list    = props['list'];
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.marginLeft = '-10000px'; // no display:none, to keep access keys working
72     for(var key in list){
73         if (!list.hasOwnProperty(key)) continue;
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             addEvent(btn,'click',bind(pickerInsert,key,edid));
88             picker.appendChild(btn);
89         }else if(isString(list[key])){
90             // a list of text -> treat as text picker
91             var btn = document.createElement('button');
92             btn.className = 'pickerbutton';
93             var txt = document.createTextNode(list[key]);
94             btn.title     = list[key];
95             btn.appendChild(txt);
96             addEvent(btn,'click',bind(pickerInsert,list[key],edid));
97             picker.appendChild(btn);
98         }else{
99             // a list of lists -> treat it as subtoolbar
100             initToolbar(picker,edid,list);
101             break; // all buttons handled already
102         }
104     }
105     var body = document.getElementsByTagName('body')[0];
106     body.appendChild(picker);
107     return picker;
111  * Called by picker buttons to insert Text and close the picker again
113  * @author Andreas Gohr <andi@splitbrain.org>
114  */
115 function pickerInsert(text,edid){
116     insertAtCarret(edid,text);
117     pickerClose();
121  * Add button action for signature button
123  * @param  DOMElement btn   Button element to add the action to
124  * @param  array      props Associative array of button properties
125  * @param  string     edid  ID of the editor textarea
126  * @return boolean    If button should be appended
127  * @author Gabriel Birke <birke@d-scribe.de>
128  */
129 function addBtnActionSignature(btn, props, edid) {
130     if(typeof(SIG) != 'undefined' && SIG != ''){
131         addEvent(btn,'click',bind(insertAtCarret,edid,SIG));
132         return true;
133     }
134     return false;
138  * Make intended formattings easier to handle
140  * Listens to all key inputs and handle indentions
141  * of lists and code blocks
143  * Currently handles space, backspce and enter presses
145  * @author Andreas Gohr <andi@splitbrain.org>
146  * @fixme handle tabs
147  */
148 function keyHandler(e){
149     if(e.keyCode != 13 &&
150        e.keyCode != 8  &&
151        e.keyCode != 32) return;
152     var field     = e.target;
153     var selection = getSelection(field);
154     var search    = "\n"+field.value.substr(0,selection.start);
155     var linestart = Math.max(search.lastIndexOf("\n"),
156                              search.lastIndexOf("\r")); //IE workaround
157     search = search.substr(linestart);
159     if(e.keyCode == 13){ // Enter
160         // keep current indention for lists and code
161         var match = search.match(/(\n  +([\*-] ?)?)/);
162         if(match){
163             var scroll = field.scrollHeight;
164             insertAtCarret(field.id,match[1]);
165             field.scrollTop += (field.scrollHeight - scroll);
166             e.preventDefault(); // prevent enter key
167         }
168     }else if(e.keyCode == 8){ // Backspace
169         // unindent lists
170         var match = search.match(/(\n  +)([*-] ?)$/);
171         if(match){
172             var spaces = match[1].length-1;
174             if(spaces > 3){ // unindent one level
175                 field.value = field.value.substr(0,linestart)+
176                               field.value.substr(linestart+2);
177                 selection.start = selection.start - 2;
178                 selection.end   = selection.start;
179             }else{ // delete list point
180                 field.value = field.value.substr(0,linestart)+
181                               field.value.substr(selection.start);
182                 selection.start = linestart;
183                 selection.end   = linestart;
184             }
185             setSelection(selection);
186             e.preventDefault(); // prevent backspace
187         }
188     }else if(e.keyCode == 32){ // Space
189         // intend list item
190         var match = search.match(/(\n  +)([*-] )$/);
191         if(match){
192             field.value = field.value.substr(0,linestart)+'  '+
193                           field.value.substr(linestart);
194             selection.start = selection.start + 2;
195             selection.end   = selection.start;
196             setSelection(selection);
197             e.preventDefault(); // prevent space
198         }
199     }
202 //FIXME consolidate somewhere else
203 addInitEvent(function(){
204     var field = $('wiki__text');
205     if(!field) return;
206     addEvent(field,'keypress',keyHandler);
210  * Determine the current section level while editing
212  * @author Andreas Gohr <gohr@cosmocode.de>
213  */
214 function currentHeadlineLevel(textboxId){
215     var field     = $(textboxId);
216     var selection = getSelection(field);
217     var search    = "\n"+field.value.substr(0,selection.start);
218     var lasthl    = search.lastIndexOf("\n==");
219     if(lasthl == -1 && field.form.prefix){
220         // we need to look in prefix context
221         search = field.form.prefix.value;
222         lasthl    = search.lastIndexOf("\n==");
223     }
224     search    = search.substr(lasthl+1,6);
226     if(search == '======') return 1;
227     if(search.substr(0,5) == '=====') return 2;
228     if(search.substr(0,4) == '====') return 3;
229     if(search.substr(0,3) == '===') return 4;
230     if(search.substr(0,2) == '==') return 5;
232     return 0;
237  * global var used for not saved yet warning
238  */
239 var textChanged = false;
242  * Check for changes before leaving the page
243  */
244 function changeCheck(msg){
245   if(textChanged){
246     var ok = confirm(msg);
247     if(ok){
248         // remove a possibly saved draft using ajax
249         var dwform = $('dw__editform');
250         if(dwform){
251             var params = 'call=draftdel';
252             params += '&id='+encodeURIComponent(dwform.elements.id.value);
254             var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
255             sackobj.AjaxFailedAlert = '';
256             sackobj.encodeURIString = false;
257             sackobj.runAJAX(params);
258             // we send this request blind without waiting for
259             // and handling the returned data
260         }
261     }
262     return ok;
263   }else{
264     return true;
265   }
269  * Add changeCheck to all Links and Forms (except those with a
270  * JSnocheck class), add handlers to monitor changes
272  * Sets focus to the editbox as well
274  * @fixme this is old and crappy code. needs to be redone
275  */
276 function initChangeCheck(msg){
277     var edit_text   = document.getElementById('wiki__text');
278     if(!edit_text) return;
280     // add change check for links
281     var links = document.getElementsByTagName('a');
282     for(var i=0; i < links.length; i++){
283         if(links[i].className.indexOf('JSnocheck') == -1){
284             links[i].onclick = function(){
285                                     var rc = changeCheck(msg);
286                                     if(window.event) window.event.returnValue = rc;
287                                     return rc;
288                                };
289         }
290     }
291     // add change check for forms
292     var forms = document.forms;
293     for(i=0; i < forms.length; i++){
294         if(forms[i].className.indexOf('JSnocheck') == -1){
295             forms[i].onsubmit = function(){
296                                     var rc = changeCheck(msg);
297                                     if(window.event) window.event.returnValue = rc;
298                                     return rc;
299                                };
300         }
301     }
303     // reset change memory var on submit
304     var btn_save        = document.getElementById('edbtn__save');
305     btn_save.onclick    = function(){ textChanged = false; };
306     var btn_prev        = document.getElementById('edbtn__preview');
307     btn_prev.onclick    = function(){ textChanged = false; };
309     // add change memory setter
310     edit_text.onchange = function(){
311         textChanged = true; //global var
312         summaryCheck();
313     };
314     var summary = document.getElementById('edit__summary');
315     addEvent(summary, 'change', summaryCheck);
316     addEvent(summary, 'keyup', summaryCheck);
317     if (textChanged) summaryCheck();
319     // set focus
320     edit_text.focus();
324  * Checks if a summary was entered - if not the style is changed
326  * @author Andreas Gohr <andi@splitbrain.org>
327  */
328 function summaryCheck(){
329     var sum = document.getElementById('edit__summary');
330     if(sum.value === ''){
331         sum.className='missing';
332     }else{
333         sum.className='edit';
334     }
339  * Class managing the timer to display a warning on a expiring lock
340  */
341 function locktimer_class(){
342         this.sack     = null;
343         this.timeout  = 0;
344         this.timerID  = null;
345         this.lasttime = null;
346         this.msg      = '';
347         this.pageid   = '';
349 var locktimer = new locktimer_class();
350     locktimer.init = function(timeout,msg,draft){
351         // init values
352         locktimer.timeout  = timeout*1000;
353         locktimer.msg      = msg;
354         locktimer.draft    = draft;
355         locktimer.lasttime = new Date();
357         if(!$('dw__editform')) return;
358         locktimer.pageid = $('dw__editform').elements.id.value;
359         if(!locktimer.pageid) return;
361         // init ajax component
362         locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php');
363         locktimer.sack.AjaxFailedAlert = '';
364         locktimer.sack.encodeURIString = false;
365         locktimer.sack.onCompletion = locktimer.refreshed;
367         // register refresh event
368         addEvent($('dw__editform').elements.wikitext,'keypress',function(){locktimer.refresh();});
370         // start timer
371         locktimer.reset();
372     };
374     /**
375      * (Re)start the warning timer
376      */
377     locktimer.reset = function(){
378         locktimer.clear();
379         locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout);
380     };
382     /**
383      * Display the warning about the expiring lock
384      */
385     locktimer.warning = function(){
386         locktimer.clear();
387         alert(locktimer.msg);
388     };
390     /**
391      * Remove the current warning timer
392      */
393     locktimer.clear = function(){
394         if(locktimer.timerID !== null){
395             window.clearTimeout(locktimer.timerID);
396             locktimer.timerID = null;
397         }
398     };
400     /**
401      * Refresh the lock via AJAX
402      *
403      * Called on keypresses in the edit area
404      */
405     locktimer.refresh = function(){
406         var now = new Date();
407         // refresh every minute only
408         if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
409             var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid);
410             if(locktimer.draft){
411                 var dwform = $('dw__editform');
412                 params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value);
413                 params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value);
414                 params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value);
415                 params += '&date='+encodeURIComponent(dwform.elements.date.value);
416             }
417             locktimer.sack.runAJAX(params);
418             locktimer.lasttime = now;
419         }
420     };
423     /**
424      * Callback. Resets the warning timer
425      */
426     locktimer.refreshed = function(){
427         var data  = this.response;
428         var error = data.charAt(0);
429             data  = data.substring(1);
431         $('draft__status').innerHTML=data;
432         if(error != '1') return; // locking failed
433         locktimer.reset();
434     };
435 // end of locktimer class functions