xref: /dokuwiki/lib/scripts/edit.js (revision 0071aa2162e87ac729531c1c625d9bfb31f2adec)
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