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