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