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