xref: /dokuwiki/lib/scripts/edit.js (revision f7284726cf0d9bf0b88b4afa25ecd92d5b98caf6)
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 and add to the toolbar
133        btn = createToolButton(tb[i]['icon'],
134                               tb[i]['title'],
135                               tb[i]['key']);
136        toolbar.appendChild(btn);
137
138        // add button action dependend on type
139        switch(tb[i]['type']){
140            case 'format':
141                var sample = tb[i]['title'];
142                if(tb[i]['sample']){ sample = tb[i]['sample']; }
143
144                eval("btn.onclick = function(){insertTags('"+
145                                        jsEscape(edid)+"','"+
146                                        jsEscape(tb[i]['open'])+"','"+
147                                        jsEscape(tb[i]['close'])+"','"+
148                                        jsEscape(sample)+
149                                    "');return false;}");
150                break;
151            case 'insert':
152                eval("btn.onclick = function(){insertAtCarret('"+
153                                        jsEscape(edid)+"','"+
154                                        jsEscape(tb[i]['insert'])+
155                                    "');return false;}");
156                break;
157            case 'signature':
158								if(SIG!=''){
159		                eval("btn.onclick = function(){insertAtCarret('"+
160    		                                    jsEscape(edid)+"','"+
161        		                                jsEscape(SIG)+
162            		                        "');return false;}");
163								}
164                break;
165            case 'picker':
166                createPicker('picker'+i,
167                             tb[i]['list'],
168                             tb[i]['icobase'],
169                             edid);
170                eval("btn.onclick = function(){showPicker('picker"+i+
171                                    "',this);return false;}");
172                break;
173            case 'mediapopup':
174                eval("btn.onclick = function(){window.open('"+
175                                        jsEscape(tb[i]['url']+NS)+"','"+
176                                        jsEscape(tb[i]['name'])+"','"+
177                                        jsEscape(tb[i]['options'])+
178                                    "');return false;}");
179                break;
180        } // end switch
181    } // end for
182}
183
184/**
185 * Format selection
186 *
187 * Apply tagOpen/tagClose to selection in textarea, use sampleText instead
188 * of selection if there is none. Copied and adapted from phpBB
189 *
190 * @author phpBB development team
191 * @author MediaWiki development team
192 * @author Andreas Gohr <andi@splitbrain.org>
193 * @author Jim Raynor <jim_raynor@web.de>
194 */
195function insertTags(edid,tagOpen, tagClose, sampleText) {
196  var txtarea = document.getElementById(edid);
197  // IE
198  if(document.selection  && !is_gecko) {
199    var theSelection = document.selection.createRange().text;
200    var replaced = true;
201    if(!theSelection){
202      replaced = false;
203      theSelection=sampleText;
204    }
205    txtarea.focus();
206
207    // This has change
208    var text = theSelection;
209    if(theSelection.charAt(theSelection.length - 1) == " "){// exclude ending space char, if any
210      theSelection = theSelection.substring(0, theSelection.length - 1);
211      r = document.selection.createRange();
212      r.text = tagOpen + theSelection + tagClose + " ";
213    } else {
214      r = document.selection.createRange();
215      r.text = tagOpen + theSelection + tagClose;
216    }
217    if(!replaced){
218      r.moveStart('character',-text.length-tagClose.length);
219      r.moveEnd('character',-tagClose.length);
220    }
221    r.select();
222  // Mozilla
223  } else if(txtarea.selectionStart || txtarea.selectionStart == '0') {
224    replaced = false;
225    var startPos = txtarea.selectionStart;
226    var endPos   = txtarea.selectionEnd;
227    if(endPos - startPos){ replaced = true; }
228    var scrollTop=txtarea.scrollTop;
229    var myText = (txtarea.value).substring(startPos, endPos);
230    if(!myText) { myText=sampleText;}
231    if(myText.charAt(myText.length - 1) == " "){ // exclude ending space char, if any
232      subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " ";
233    } else {
234      subst = tagOpen + myText + tagClose;
235    }
236    txtarea.value = txtarea.value.substring(0, startPos) + subst +
237                    txtarea.value.substring(endPos, txtarea.value.length);
238    txtarea.focus();
239
240    //set new selection
241    if(replaced){
242      var cPos=startPos+(tagOpen.length+myText.length+tagClose.length);
243      txtarea.selectionStart=cPos;
244      txtarea.selectionEnd=cPos;
245    }else{
246      txtarea.selectionStart=startPos+tagOpen.length;
247      txtarea.selectionEnd=startPos+tagOpen.length+myText.length;
248    }
249    txtarea.scrollTop=scrollTop;
250  // All others
251  } else {
252    var copy_alertText=alertText;
253    var re1=new RegExp("\\$1","g");
254    var re2=new RegExp("\\$2","g");
255    copy_alertText=copy_alertText.replace(re1,sampleText);
256    copy_alertText=copy_alertText.replace(re2,tagOpen+sampleText+tagClose);
257
258    if (sampleText) {
259      text=prompt(copy_alertText);
260    } else {
261      text="";
262    }
263    if(!text) { text=sampleText;}
264    text=tagOpen+text+tagClose;
265    //append to the end
266    txtarea.value += "\n"+text;
267
268    // in Safari this causes scrolling
269    if(!is_safari) {
270      txtarea.focus();
271    }
272
273  }
274  // reposition cursor if possible
275  if (txtarea.createTextRange){
276    txtarea.caretPos = document.selection.createRange().duplicate();
277  }
278}
279
280/*
281 * Insert the given value at the current cursor position
282 *
283 * @see http://www.alexking.org/index.php?content=software/javascript/content.php
284 */
285function insertAtCarret(edid,value){
286  var field = document.getElementById(edid);
287
288  //IE support
289  if (document.selection) {
290    field.focus();
291    if(opener == null){
292      sel = document.selection.createRange();
293    }else{
294      sel = opener.document.selection.createRange();
295    }
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    return confirm(msg);
332  }else{
333    return true;
334  }
335}
336
337/**
338 * Add changeCheck to all Links and Forms (except those with a
339 * JSnocheck class), add handlers to monitor changes
340 *
341 * Sets focus to the editbox as well
342 */
343function initChangeCheck(msg){
344		if(!document.getElementById){ return false; }
345		// add change check for links
346		var links = document.getElementsByTagName('a');
347		for(var i=0; i < links.length; i++){
348				if(links[i].className.indexOf('JSnocheck') == -1){
349						links[i].onclick = function(){return changeCheck(msg);};
350						links[i].onkeypress = function(){return changeCheck(msg);};
351				}
352		}
353		// add change check for forms
354    var forms = document.forms;
355    for(i=0; i < forms.length; i++){
356				if(forms[i].className.indexOf('JSnocheck') == -1){
357		        forms[i].onsubmit = function(){return changeCheck(msg);};
358				}
359    }
360
361		// reset change memory var on submit
362		var btn_save        = document.getElementById('edbtn_save');
363		btn_save.onclick    = function(){ textChanged = false; };
364		btn_save.onkeypress = function(){ textChanged = false; };
365		var btn_prev        = document.getElementById('edbtn_preview');
366		btn_prev.onclick    = function(){ textChanged = false; };
367		btn_prev.onkeypress = function(){ textChanged = false; };
368
369		// add change memory setter
370		var edit_text   = document.getElementById('wikitext');
371		edit_text.onchange = function(){
372				textChanged = true; //global var
373				summaryCheck();
374		};
375		edit_text.onkeyup  = summaryCheck;
376		var summary = document.getElementById('summary');
377		summary.onchange = summaryCheck;
378		summary.onkeyup  = summaryCheck;
379
380		// set focus
381		edit_text.focus();
382}
383
384/**
385 * Checks if a summary was entered - if not the style is changed
386 *
387 * @author Andreas Gohr <andi@splitbrain.org>
388 */
389function summaryCheck(){
390    var sum = document.getElementById('summary');
391    if(sum.value === ''){
392      	sum.className='missing';
393    }else{
394      	sum.className='edit';
395    }
396}
397
398
399/**
400 * global variable for the locktimer
401 */
402var locktimerID;
403
404/**
405 * This starts a timer to remind the user of an expiring lock
406 * Accepts the delay in seconds and a text to display.
407 */
408function init_locktimer(delay,txt){
409  txt = escapeQuotes(txt);
410  locktimerID = self.setTimeout("locktimer('"+txt+"')", delay*1000);
411}
412
413/**
414 * This stops the timer and displays a message about the expiring lock
415 */
416function locktimer(txt){
417  clearTimeout(locktimerID);
418  alert(txt);
419}
420
421
422
423