xref: /dokuwiki/lib/scripts/edit.js (revision 2b03e74de514af15d4004e7635625fa64816231a)
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    if(!document.getElementById){ return; }
129    var toolbar = document.getElementById(tbid);
130    var cnt = tb.length;
131    for(var i=0; i<cnt; i++){
132        // create new button
133        btn = createToolButton(tb[i]['icon'],
134                               tb[i]['title'],
135                               tb[i]['key']);
136
137        // add button action dependend on type
138        switch(tb[i]['type']){
139            case 'format':
140                var sample = tb[i]['title'];
141                if(tb[i]['sample']){ sample = tb[i]['sample']; }
142
143                eval("btn.onclick = function(){insertTags('"+
144                                        jsEscape(edid)+"','"+
145                                        jsEscape(tb[i]['open'])+"','"+
146                                        jsEscape(tb[i]['close'])+"','"+
147                                        jsEscape(sample)+
148                                    "');return false;}");
149                toolbar.appendChild(btn);
150                break;
151            case 'insert':
152                eval("btn.onclick = function(){insertAtCarret('"+
153                                        jsEscape(edid)+"','"+
154                                        jsEscape(tb[i]['insert'])+
155                                    "');return false;}");
156                toolbar.appendChild(btn);
157                break;
158            case 'signature':
159                if(typeof(SIG) != 'undefined' && SIG != ''){
160                    eval("btn.onclick = function(){insertAtCarret('"+
161                                            jsEscape(edid)+"','"+
162                                            jsEscape(SIG)+
163                                        "');return false;}");
164                    toolbar.appendChild(btn);
165                }
166                break;
167            case 'picker':
168                createPicker('picker'+i,
169                             tb[i]['list'],
170                             tb[i]['icobase'],
171                             edid);
172                eval("btn.onclick = function(){showPicker('picker"+i+
173                                    "',this);return false;}");
174                toolbar.appendChild(btn);
175                break;
176            case 'mediapopup':
177                eval("btn.onclick = function(){window.open('"+
178                                        jsEscape(tb[i]['url']+NS)+"','"+
179                                        jsEscape(tb[i]['name'])+"','"+
180                                        jsEscape(tb[i]['options'])+
181                                    "');return false;}");
182                toolbar.appendChild(btn);
183                break;
184        } // end switch
185    } // end for
186}
187
188/**
189 * Format selection
190 *
191 * Apply tagOpen/tagClose to selection in textarea, use sampleText instead
192 * of selection if there is none. Copied and adapted from phpBB
193 *
194 * @author phpBB development team
195 * @author MediaWiki development team
196 * @author Andreas Gohr <andi@splitbrain.org>
197 * @author Jim Raynor <jim_raynor@web.de>
198 */
199function insertTags(edid,tagOpen, tagClose, sampleText) {
200  var txtarea = document.getElementById(edid);
201  // IE
202  if(document.selection  && !is_gecko) {
203    var theSelection = document.selection.createRange().text;
204    var replaced = true;
205    if(!theSelection){
206      replaced = false;
207      theSelection=sampleText;
208    }
209    txtarea.focus();
210
211    // This has change
212    var text = theSelection;
213    if(theSelection.charAt(theSelection.length - 1) == " "){// exclude ending space char, if any
214      theSelection = theSelection.substring(0, theSelection.length - 1);
215      r = document.selection.createRange();
216      r.text = tagOpen + theSelection + tagClose + " ";
217    } else {
218      r = document.selection.createRange();
219      r.text = tagOpen + theSelection + tagClose;
220    }
221    if(!replaced){
222      r.moveStart('character',-text.length-tagClose.length);
223      r.moveEnd('character',-tagClose.length);
224    }
225    r.select();
226  // Mozilla
227  } else if(txtarea.selectionStart || txtarea.selectionStart == '0') {
228    replaced = false;
229    var startPos = txtarea.selectionStart;
230    var endPos   = txtarea.selectionEnd;
231    if(endPos - startPos){ replaced = true; }
232    var scrollTop=txtarea.scrollTop;
233    var myText = (txtarea.value).substring(startPos, endPos);
234    if(!myText) { myText=sampleText;}
235    if(myText.charAt(myText.length - 1) == " "){ // exclude ending space char, if any
236      subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " ";
237    } else {
238      subst = tagOpen + myText + tagClose;
239    }
240    txtarea.value = txtarea.value.substring(0, startPos) + subst +
241                    txtarea.value.substring(endPos, txtarea.value.length);
242    txtarea.focus();
243
244    //set new selection
245    if(replaced){
246      var cPos=startPos+(tagOpen.length+myText.length+tagClose.length);
247      txtarea.selectionStart=cPos;
248      txtarea.selectionEnd=cPos;
249    }else{
250      txtarea.selectionStart=startPos+tagOpen.length;
251      txtarea.selectionEnd=startPos+tagOpen.length+myText.length;
252    }
253    txtarea.scrollTop=scrollTop;
254  // All others
255  } else {
256    var copy_alertText=alertText;
257    var re1=new RegExp("\\$1","g");
258    var re2=new RegExp("\\$2","g");
259    copy_alertText=copy_alertText.replace(re1,sampleText);
260    copy_alertText=copy_alertText.replace(re2,tagOpen+sampleText+tagClose);
261
262    if (sampleText) {
263      text=prompt(copy_alertText);
264    } else {
265      text="";
266    }
267    if(!text) { text=sampleText;}
268    text=tagOpen+text+tagClose;
269    //append to the end
270    txtarea.value += "\n"+text;
271
272    // in Safari this causes scrolling
273    if(!is_safari) {
274      txtarea.focus();
275    }
276
277  }
278  // reposition cursor if possible
279  if (txtarea.createTextRange){
280    txtarea.caretPos = document.selection.createRange().duplicate();
281  }
282}
283
284/*
285 * Insert the given value at the current cursor position
286 *
287 * @see http://www.alexking.org/index.php?content=software/javascript/content.php
288 */
289function insertAtCarret(edid,value){
290  var field = document.getElementById(edid);
291
292  //IE support
293  if (document.selection) {
294    field.focus();
295    sel = document.selection.createRange();
296    sel.text = value;
297  //MOZILLA/NETSCAPE support
298  }else if (field.selectionStart || field.selectionStart == '0') {
299    var startPos  = field.selectionStart;
300    var endPos    = field.selectionEnd;
301    var scrollTop = field.scrollTop;
302    field.value = field.value.substring(0, startPos) +
303                  value +
304                  field.value.substring(endPos, field.value.length);
305
306    field.focus();
307    var cPos=startPos+(value.length);
308    field.selectionStart=cPos;
309    field.selectionEnd=cPos;
310    field.scrollTop=scrollTop;
311  } else {
312    field.value += "\n"+value;
313  }
314  // reposition cursor if possible
315  if (field.createTextRange){
316    field.caretPos = document.selection.createRange().duplicate();
317  }
318}
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='+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            links[i].onkeypress = function(){
371                                    var rc = changeCheck(msg);
372                                    if(window.event) window.event.returnValue = rc;
373                                    return rc;
374                               };
375        }
376    }
377    // add change check for forms
378    var forms = document.forms;
379    for(i=0; i < forms.length; i++){
380        if(forms[i].className.indexOf('JSnocheck') == -1){
381            forms[i].onsubmit = function(){
382                                    var rc = changeCheck(msg);
383                                    if(window.event) window.event.returnValue = rc;
384                                    return rc;
385                               };
386        }
387    }
388
389    // reset change memory var on submit
390    var btn_save        = document.getElementById('edbtn__save');
391    btn_save.onclick    = function(){ textChanged = false; };
392    btn_save.onkeypress = function(){ textChanged = false; };
393    var btn_prev        = document.getElementById('edbtn__preview');
394    btn_prev.onclick    = function(){ textChanged = false; };
395    btn_prev.onkeypress = function(){ textChanged = false; };
396
397    // add change memory setter
398    var edit_text   = document.getElementById('wiki__text');
399    edit_text.onchange = function(){
400        textChanged = true; //global var
401        summaryCheck();
402    };
403    edit_text.onkeyup  = summaryCheck;
404    var summary = document.getElementById('edit__summary');
405    addEvent(summary, 'change', summaryCheck);
406    addEvent(summary, 'keyup', summaryCheck);
407
408    // set focus
409    edit_text.focus();
410}
411
412/**
413 * Checks if a summary was entered - if not the style is changed
414 *
415 * @author Andreas Gohr <andi@splitbrain.org>
416 */
417function summaryCheck(){
418    var sum = document.getElementById('edit__summary');
419    if(sum.value === ''){
420        sum.className='missing';
421    }else{
422        sum.className='edit';
423    }
424}
425
426
427/**
428 * Class managing the timer to display a warning on a expiring lock
429 */
430function locktimer_class(){
431        this.sack     = null;
432        this.timeout  = 0;
433        this.timerID  = null;
434        this.lasttime = null;
435        this.msg      = '';
436        this.pageid   = '';
437};
438var locktimer = new locktimer_class();
439    locktimer.init = function(timeout,msg,draft){
440        // init values
441        locktimer.timeout  = timeout*1000;
442        locktimer.msg      = msg;
443        locktimer.draft    = draft;
444        locktimer.lasttime = new Date();
445
446        if(!$('dw__editform')) return;
447        locktimer.pageid = $('dw__editform').elements.id.value;
448        if(!locktimer.pageid) return;
449
450        // init ajax component
451        locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php');
452        locktimer.sack.AjaxFailedAlert = '';
453        locktimer.sack.encodeURIString = false;
454        locktimer.sack.onCompletion = locktimer.refreshed;
455
456        // register refresh event
457        addEvent($('dw__editform').elements.wikitext,'keyup',function(){locktimer.refresh();});
458
459        // start timer
460        locktimer.reset();
461    };
462
463    /**
464     * (Re)start the warning timer
465     */
466    locktimer.reset = function(){
467        locktimer.clear();
468        locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout);
469    };
470
471    /**
472     * Display the warning about the expiring lock
473     */
474    locktimer.warning = function(){
475        locktimer.clear();
476        alert(locktimer.msg);
477    };
478
479    /**
480     * Remove the current warning timer
481     */
482    locktimer.clear = function(){
483        if(locktimer.timerID !== null){
484            window.clearTimeout(locktimer.timerID);
485            locktimer.timerID = null;
486        }
487    };
488
489    /**
490     * Refresh the lock via AJAX
491     *
492     * Called on keypresses in the edit area
493     */
494    locktimer.refresh = function(){
495        var now = new Date();
496        // refresh every minute only
497        if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
498            var params = 'call=lock&id='+encodeURI(locktimer.pageid);
499            if(locktimer.draft){
500                var dwform = $('dw__editform');
501                params += '&prefix='+encodeURI(dwform.elements.prefix.value);
502                params += '&wikitext='+encodeURI(dwform.elements.wikitext.value);
503                params += '&suffix='+encodeURI(dwform.elements.suffix.value);
504                params += '&date='+encodeURI(dwform.elements.date.value);
505            }
506            locktimer.sack.runAJAX(params);
507            locktimer.lasttime = now;
508        }
509    };
510
511
512    /**
513     * Callback. Resets the warning timer
514     */
515    locktimer.refreshed = function(){
516        var data  = this.response;
517        var error = data.charAt(0);
518            data  = data.substring(1);
519
520        $('draft__status').innerHTML=data;
521        if(error != '1') return; // locking failed
522        locktimer.reset();
523    };
524// end of locktimer class functions
525
526