xref: /dokuwiki/lib/scripts/edit.js (revision 2a3623da3390f0118660d541928ae8c635631b3d)
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        var btn = createToolButton(tb[i]['icon'],
139                               tb[i]['title'],
140                               tb[i]['key']);
141
142        var actionFunc = 'addBtnAction'+tb[i]['type'].charAt(0).toUpperCase()+tb[i]['type'].substring(1);
143        var exists = eval("typeof("+actionFunc+") == 'function'");
144        if(exists)
145        {
146            if(eval(actionFunc+"(btn, tb[i], edid, i)"))
147                toolbar.appendChild(btn);
148        }
149    } // end for
150}
151
152/**
153 * Add button action for format buttons
154 *
155 * @param  DOMElement btn   Button element to add the action to
156 * @param  array      props Associative array of button properties
157 * @param  string     edid  ID of the editor textarea
158 * @return boolean    If button should be appended
159 * @author Gabriel Birke <birke@d-scribe.de>
160 */
161function addBtnActionFormat(btn, props, edid)
162{
163    var sample = props['title'];
164    if(props['sample']){ sample = props['sample']; }
165    eval("btn.onclick = function(){insertTags('"+
166        jsEscape(edid)+"','"+
167        jsEscape(props['open'])+"','"+
168        jsEscape(props['close'])+"','"+
169        jsEscape(sample)+
170    "');return false;}");
171
172    return true;
173}
174
175/**
176 * Add button action for insert buttons
177 *
178 * @param  DOMElement btn   Button element to add the action to
179 * @param  array      props Associative array of button properties
180 * @param  string     edid  ID of the editor textarea
181 * @return boolean    If button should be appended
182 * @author Gabriel Birke <birke@d-scribe.de>
183 */
184function addBtnActionInsert(btn, props, edid)
185{
186    eval("btn.onclick = function(){insertAtCarret('"+
187        jsEscape(edid)+"','"+
188        jsEscape(props['insert'])+
189    "');return false;}");
190    return true;
191}
192
193/**
194 * Add button action for signature button
195 *
196 * @param  DOMElement btn   Button element to add the action to
197 * @param  array      props Associative array of button properties
198 * @param  string     edid  ID of the editor textarea
199 * @return boolean    If button should be appended
200 * @author Gabriel Birke <birke@d-scribe.de>
201 */
202function addBtnActionSignature(btn, props, edid)
203{
204    if(typeof(SIG) != 'undefined' && SIG != ''){
205        eval("btn.onclick = function(){insertAtCarret('"+
206            jsEscape(edid)+"','"+
207            jsEscape(SIG)+
208        "');return false;}");
209        return true;
210    }
211    return false;
212}
213
214/**
215 * Add button action for picker buttons and create picker element
216 *
217 * @param  DOMElement btn   Button element to add the action to
218 * @param  array      props Associative array of button properties
219 * @param  string     edid  ID of the editor textarea
220 * @param  int        id    Unique number of the picker
221 * @return boolean    If button should be appended
222 * @author Gabriel Birke <birke@d-scribe.de>
223 */
224function addBtnActionPicker(btn, props, edid, id)
225{
226    createPicker('picker'+id,
227         props['list'],
228         props['icobase'],
229         edid);
230    eval("btn.onclick = function(){showPicker('picker"+id+
231                                    "',this);return false;}");
232    return true;
233}
234
235/**
236 * Add button action for the mediapopup button
237 *
238 * @param  DOMElement btn   Button element to add the action to
239 * @param  array      props Associative array of button properties
240 * @return boolean    If button should be appended
241 * @author Gabriel Birke <birke@d-scribe.de>
242 */
243function addBtnActionMediapopup(btn, props)
244{
245    eval("btn.onclick = function(){window.open('"+
246        jsEscape(props['url']+NS)+"','"+
247        jsEscape(props['name'])+"','"+
248        jsEscape(props['options'])+
249    "');return false;}");
250    return true;
251}
252
253/**
254 * Format selection
255 *
256 * Apply tagOpen/tagClose to selection in textarea, use sampleText instead
257 * of selection if there is none. Copied and adapted from phpBB
258 *
259 * @author phpBB development team
260 * @author MediaWiki development team
261 * @author Andreas Gohr <andi@splitbrain.org>
262 * @author Jim Raynor <jim_raynor@web.de>
263 */
264function insertTags(edid,tagOpen, tagClose, sampleText) {
265  var txtarea = document.getElementById(edid);
266  // IE
267  if(document.selection  && !is_gecko) {
268    var theSelection = document.selection.createRange().text;
269    var replaced = true;
270    if(!theSelection){
271      replaced = false;
272      theSelection=sampleText;
273    }
274    txtarea.focus();
275
276    // This has change
277    var text = theSelection;
278    if(theSelection.charAt(theSelection.length - 1) == " "){// exclude ending space char, if any
279      theSelection = theSelection.substring(0, theSelection.length - 1);
280      r = document.selection.createRange();
281      r.text = tagOpen + theSelection + tagClose + " ";
282    } else {
283      r = document.selection.createRange();
284      r.text = tagOpen + theSelection + tagClose;
285    }
286    if(!replaced){
287      r.moveStart('character',-text.length-tagClose.length);
288      r.moveEnd('character',-tagClose.length);
289    }
290    r.select();
291  // Mozilla
292  } else if(txtarea.selectionStart || txtarea.selectionStart == '0') {
293    replaced = false;
294    var startPos = txtarea.selectionStart;
295    var endPos   = txtarea.selectionEnd;
296    if(endPos - startPos){ replaced = true; }
297    var scrollTop=txtarea.scrollTop;
298    var myText = (txtarea.value).substring(startPos, endPos);
299    if(!myText) { myText=sampleText;}
300    if(myText.charAt(myText.length - 1) == " "){ // exclude ending space char, if any
301      subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " ";
302    } else {
303      subst = tagOpen + myText + tagClose;
304    }
305    txtarea.value = txtarea.value.substring(0, startPos) + subst +
306                    txtarea.value.substring(endPos, txtarea.value.length);
307    txtarea.focus();
308
309    //set new selection
310    if(replaced){
311      var cPos=startPos+(tagOpen.length+myText.length+tagClose.length);
312      txtarea.selectionStart=cPos;
313      txtarea.selectionEnd=cPos;
314    }else{
315      txtarea.selectionStart=startPos+tagOpen.length;
316      txtarea.selectionEnd=startPos+tagOpen.length+myText.length;
317    }
318    txtarea.scrollTop=scrollTop;
319  // All others
320  } else {
321    var copy_alertText=alertText;
322    var re1=new RegExp("\\$1","g");
323    var re2=new RegExp("\\$2","g");
324    copy_alertText=copy_alertText.replace(re1,sampleText);
325    copy_alertText=copy_alertText.replace(re2,tagOpen+sampleText+tagClose);
326
327    if (sampleText) {
328      text=prompt(copy_alertText);
329    } else {
330      text="";
331    }
332    if(!text) { text=sampleText;}
333    text=tagOpen+text+tagClose;
334    //append to the end
335    txtarea.value += "\n"+text;
336
337    // in Safari this causes scrolling
338    if(!is_safari) {
339      txtarea.focus();
340    }
341
342  }
343  // reposition cursor if possible
344  if (txtarea.createTextRange){
345    txtarea.caretPos = document.selection.createRange().duplicate();
346  }
347}
348
349/*
350 * Insert the given value at the current cursor position
351 *
352 * @see http://www.alexking.org/index.php?content=software/javascript/content.php
353 */
354function insertAtCarret(edid,value){
355  var field = document.getElementById(edid);
356
357  //IE support
358  if (document.selection) {
359    field.focus();
360    sel = document.selection.createRange();
361    sel.text = value;
362
363  //MOZILLA/NETSCAPE support
364  }else if (field.selectionStart || field.selectionStart == '0') {
365    var startPos  = field.selectionStart;
366    var endPos    = field.selectionEnd;
367    var scrollTop = field.scrollTop;
368    field.value = field.value.substring(0, startPos) +
369                  value +
370                  field.value.substring(endPos, field.value.length);
371
372    field.focus();
373    var cPos=startPos+(value.length);
374    field.selectionStart=cPos;
375    field.selectionEnd=cPos;
376    field.scrollTop=scrollTop;
377  } else {
378    field.value += "\n"+value;
379  }
380  // reposition cursor if possible
381  if (field.createTextRange){
382    field.caretPos = document.selection.createRange().duplicate();
383  }
384}
385
386
387/**
388 * global var used for not saved yet warning
389 */
390var textChanged = false;
391
392/**
393 * Check for changes before leaving the page
394 */
395function changeCheck(msg){
396  if(textChanged){
397    var ok = confirm(msg);
398    if(ok){
399        // remove a possibly saved draft using ajax
400        var dwform = $('dw__editform');
401        if(dwform){
402            var params = 'call=draftdel';
403            params += '&id='+encodeURIComponent(dwform.elements.id.value);
404
405            var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
406            sackobj.AjaxFailedAlert = '';
407            sackobj.encodeURIString = false;
408            sackobj.runAJAX(params);
409            // we send this request blind without waiting for
410            // and handling the returned data
411        }
412    }
413    return ok;
414  }else{
415    return true;
416  }
417}
418
419/**
420 * Add changeCheck to all Links and Forms (except those with a
421 * JSnocheck class), add handlers to monitor changes
422 *
423 * Sets focus to the editbox as well
424 */
425function initChangeCheck(msg){
426    if(!document.getElementById){ return false; }
427    // add change check for links
428    var links = document.getElementsByTagName('a');
429    for(var i=0; i < links.length; i++){
430        if(links[i].className.indexOf('JSnocheck') == -1){
431            links[i].onclick = function(){
432                                    var rc = changeCheck(msg);
433                                    if(window.event) window.event.returnValue = rc;
434                                    return rc;
435                               };
436            links[i].onkeypress = function(){
437                                    var rc = changeCheck(msg);
438                                    if(window.event) window.event.returnValue = rc;
439                                    return rc;
440                               };
441        }
442    }
443    // add change check for forms
444    var forms = document.forms;
445    for(i=0; i < forms.length; i++){
446        if(forms[i].className.indexOf('JSnocheck') == -1){
447            forms[i].onsubmit = function(){
448                                    var rc = changeCheck(msg);
449                                    if(window.event) window.event.returnValue = rc;
450                                    return rc;
451                               };
452        }
453    }
454
455    // reset change memory var on submit
456    var btn_save        = document.getElementById('edbtn__save');
457    btn_save.onclick    = function(){ textChanged = false; };
458    btn_save.onkeypress = function(){ textChanged = false; };
459    var btn_prev        = document.getElementById('edbtn__preview');
460    btn_prev.onclick    = function(){ textChanged = false; };
461    btn_prev.onkeypress = function(){ textChanged = false; };
462
463    // add change memory setter
464    var edit_text   = document.getElementById('wiki__text');
465    edit_text.onchange = function(){
466        textChanged = true; //global var
467        summaryCheck();
468    };
469    edit_text.onkeyup  = summaryCheck;
470    var summary = document.getElementById('edit__summary');
471    addEvent(summary, 'change', summaryCheck);
472    addEvent(summary, 'keyup', summaryCheck);
473    if (textChanged) summaryCheck();
474
475    // set focus
476    edit_text.focus();
477}
478
479/**
480 * Checks if a summary was entered - if not the style is changed
481 *
482 * @author Andreas Gohr <andi@splitbrain.org>
483 */
484function summaryCheck(){
485    var sum = document.getElementById('edit__summary');
486    if(sum.value === ''){
487        sum.className='missing';
488    }else{
489        sum.className='edit';
490    }
491}
492
493
494/**
495 * Class managing the timer to display a warning on a expiring lock
496 */
497function locktimer_class(){
498        this.sack     = null;
499        this.timeout  = 0;
500        this.timerID  = null;
501        this.lasttime = null;
502        this.msg      = '';
503        this.pageid   = '';
504};
505var locktimer = new locktimer_class();
506    locktimer.init = function(timeout,msg,draft){
507        // init values
508        locktimer.timeout  = timeout*1000;
509        locktimer.msg      = msg;
510        locktimer.draft    = draft;
511        locktimer.lasttime = new Date();
512
513        if(!$('dw__editform')) return;
514        locktimer.pageid = $('dw__editform').elements.id.value;
515        if(!locktimer.pageid) return;
516
517        // init ajax component
518        locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php');
519        locktimer.sack.AjaxFailedAlert = '';
520        locktimer.sack.encodeURIString = false;
521        locktimer.sack.onCompletion = locktimer.refreshed;
522
523        // register refresh event
524        addEvent($('dw__editform').elements.wikitext,'keyup',function(){locktimer.refresh();});
525
526        // start timer
527        locktimer.reset();
528    };
529
530    /**
531     * (Re)start the warning timer
532     */
533    locktimer.reset = function(){
534        locktimer.clear();
535        locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout);
536    };
537
538    /**
539     * Display the warning about the expiring lock
540     */
541    locktimer.warning = function(){
542        locktimer.clear();
543        alert(locktimer.msg);
544    };
545
546    /**
547     * Remove the current warning timer
548     */
549    locktimer.clear = function(){
550        if(locktimer.timerID !== null){
551            window.clearTimeout(locktimer.timerID);
552            locktimer.timerID = null;
553        }
554    };
555
556    /**
557     * Refresh the lock via AJAX
558     *
559     * Called on keypresses in the edit area
560     */
561    locktimer.refresh = function(){
562        var now = new Date();
563        // refresh every minute only
564        if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
565            var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid);
566            if(locktimer.draft){
567                var dwform = $('dw__editform');
568                params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value);
569                params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value);
570                params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value);
571                params += '&date='+encodeURIComponent(dwform.elements.date.value);
572            }
573            locktimer.sack.runAJAX(params);
574            locktimer.lasttime = now;
575        }
576    };
577
578
579    /**
580     * Callback. Resets the warning timer
581     */
582    locktimer.refreshed = function(){
583        var data  = this.response;
584        var error = data.charAt(0);
585            data  = data.substring(1);
586
587        $('draft__status').innerHTML=data;
588        if(error != '1') return; // locking failed
589        locktimer.reset();
590    };
591// end of locktimer class functions
592
593