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