xref: /dokuwiki/lib/scripts/edit.js (revision 6b6da7f587c020b3439dc14c8250766f895adb93)
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 += ' ['+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 * Add button action for signature button
130 *
131 * @param  DOMElement btn   Button element to add the action to
132 * @param  array      props Associative array of button properties
133 * @param  string     edid  ID of the editor textarea
134 * @return boolean    If button should be appended
135 * @author Gabriel Birke <birke@d-scribe.de>
136 */
137function addBtnActionSignature(btn, props, edid)
138{
139    if(typeof(SIG) != 'undefined' && SIG != ''){
140        eval("btn.onclick = function(){insertAtCarret('"+
141            jsEscape(edid)+"','"+
142            jsEscape(SIG)+
143        "');return false;}");
144        return true;
145    }
146    return false;
147}
148
149/**
150 * Add button action for picker buttons and create picker element
151 *
152 * @param  DOMElement btn   Button element to add the action to
153 * @param  array      props Associative array of button properties
154 * @param  string     edid  ID of the editor textarea
155 * @param  int        id    Unique number of the picker
156 * @return boolean    If button should be appended
157 * @author Gabriel Birke <birke@d-scribe.de>
158 */
159function addBtnActionPicker(btn, props, edid, id)
160{
161    createPicker('picker'+id,
162         props['list'],
163         props['icobase'],
164         edid);
165    eval("btn.onclick = function(){showPicker('picker"+id+
166                                    "',this);return false;}");
167    return true;
168}
169
170/**
171 * Add button action for the mediapopup button
172 *
173 * @param  DOMElement btn   Button element to add the action to
174 * @param  array      props Associative array of button properties
175 * @return boolean    If button should be appended
176 * @author Gabriel Birke <birke@d-scribe.de>
177 */
178function addBtnActionMediapopup(btn, props)
179{
180    eval("btn.onclick = function(){window.open('"+DOKU_BASE+
181        jsEscape(props['url']+encodeURIComponent(NS))+"','"+
182        jsEscape(props['name'])+"','"+
183        jsEscape(props['options'])+
184    "');return false;}");
185    return true;
186}
187
188function addBtnActionAutohead(btn, props, edid, id)
189{
190    eval("btn.onclick = function(){"+
191    "insertHeadline('"+edid+"',"+props['mod']+",'"+jsEscape(props['text'])+"'); "+
192    "return false};");
193    return true;
194}
195
196
197
198
199/**
200 * Determine the current section level while editing
201 *
202 * @author Andreas Gohr <gohr@cosmocode.de>
203 */
204function currentHeadlineLevel(textboxId){
205    var field     = $(textboxId);
206    var selection = getSelection(field);
207    var search    = field.value.substr(0,selection.start);
208    var lasthl    = search.lastIndexOf("\n==");
209    if(lasthl == -1 && field.form.prefix){
210        // we need to look in prefix context
211        search = field.form.prefix.value;
212        lasthl    = search.lastIndexOf("\n==");
213    }
214    search    = search.substr(lasthl+1,6);
215
216    if(search == '======') return 1;
217    if(search.substr(0,5) == '=====') return 2;
218    if(search.substr(0,4) == '====') return 3;
219    if(search.substr(0,3) == '===') return 4;
220    if(search.substr(0,2) == '==') return 5;
221
222    return 0;
223}
224
225/**
226 * Insert a new headline based on the current section level
227 *
228 * @param string textboxId - the edit field ID
229 * @param int    mod       - the headline modificator ( -1, 0, 1)
230 * @param string text      - the sample text passed to insertTags
231 */
232function insertHeadline(textboxId,mod,text){
233    var lvl = currentHeadlineLevel(textboxId);
234
235
236    // determine new level
237    lvl += mod;
238    if(lvl < 1) lvl = 1;
239    if(lvl > 5) lvl = 5;
240
241    var tags = '=';
242    for(var i=0; i<=5-lvl; i++) tags += '=';
243    insertTags(textboxId, tags+' ', ' '+tags+"\n", text);
244}
245
246/**
247 * global var used for not saved yet warning
248 */
249var textChanged = false;
250
251/**
252 * Check for changes before leaving the page
253 */
254function changeCheck(msg){
255  if(textChanged){
256    var ok = confirm(msg);
257    if(ok){
258        // remove a possibly saved draft using ajax
259        var dwform = $('dw__editform');
260        if(dwform){
261            var params = 'call=draftdel';
262            params += '&id='+encodeURIComponent(dwform.elements.id.value);
263
264            var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
265            sackobj.AjaxFailedAlert = '';
266            sackobj.encodeURIString = false;
267            sackobj.runAJAX(params);
268            // we send this request blind without waiting for
269            // and handling the returned data
270        }
271    }
272    return ok;
273  }else{
274    return true;
275  }
276}
277
278/**
279 * Add changeCheck to all Links and Forms (except those with a
280 * JSnocheck class), add handlers to monitor changes
281 *
282 * Sets focus to the editbox as well
283 */
284function initChangeCheck(msg){
285    if(!document.getElementById){ return false; }
286    // add change check for links
287    var links = document.getElementsByTagName('a');
288    for(var i=0; i < links.length; i++){
289        if(links[i].className.indexOf('JSnocheck') == -1){
290            links[i].onclick = function(){
291                                    var rc = changeCheck(msg);
292                                    if(window.event) window.event.returnValue = rc;
293                                    return rc;
294                               };
295        }
296    }
297    // add change check for forms
298    var forms = document.forms;
299    for(i=0; i < forms.length; i++){
300        if(forms[i].className.indexOf('JSnocheck') == -1){
301            forms[i].onsubmit = function(){
302                                    var rc = changeCheck(msg);
303                                    if(window.event) window.event.returnValue = rc;
304                                    return rc;
305                               };
306        }
307    }
308
309    // reset change memory var on submit
310    var btn_save        = document.getElementById('edbtn__save');
311    btn_save.onclick    = function(){ textChanged = false; };
312    var btn_prev        = document.getElementById('edbtn__preview');
313    btn_prev.onclick    = function(){ textChanged = false; };
314
315    // add change memory setter
316    var edit_text   = document.getElementById('wiki__text');
317    edit_text.onchange = function(){
318        textChanged = true; //global var
319        summaryCheck();
320    };
321    edit_text.onkeyup  = summaryCheck;
322    var summary = document.getElementById('edit__summary');
323    addEvent(summary, 'change', summaryCheck);
324    addEvent(summary, 'keyup', summaryCheck);
325    if (textChanged) summaryCheck();
326
327    // set focus
328    edit_text.focus();
329}
330
331/**
332 * Checks if a summary was entered - if not the style is changed
333 *
334 * @author Andreas Gohr <andi@splitbrain.org>
335 */
336function summaryCheck(){
337    var sum = document.getElementById('edit__summary');
338    if(sum.value === ''){
339        sum.className='missing';
340    }else{
341        sum.className='edit';
342    }
343}
344
345
346/**
347 * Class managing the timer to display a warning on a expiring lock
348 */
349function locktimer_class(){
350        this.sack     = null;
351        this.timeout  = 0;
352        this.timerID  = null;
353        this.lasttime = null;
354        this.msg      = '';
355        this.pageid   = '';
356};
357var locktimer = new locktimer_class();
358    locktimer.init = function(timeout,msg,draft){
359        // init values
360        locktimer.timeout  = timeout*1000;
361        locktimer.msg      = msg;
362        locktimer.draft    = draft;
363        locktimer.lasttime = new Date();
364
365        if(!$('dw__editform')) return;
366        locktimer.pageid = $('dw__editform').elements.id.value;
367        if(!locktimer.pageid) return;
368
369        // init ajax component
370        locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php');
371        locktimer.sack.AjaxFailedAlert = '';
372        locktimer.sack.encodeURIString = false;
373        locktimer.sack.onCompletion = locktimer.refreshed;
374
375        // register refresh event
376        addEvent($('dw__editform').elements.wikitext,'keypress',function(){locktimer.refresh();});
377
378        // start timer
379        locktimer.reset();
380    };
381
382    /**
383     * (Re)start the warning timer
384     */
385    locktimer.reset = function(){
386        locktimer.clear();
387        locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout);
388    };
389
390    /**
391     * Display the warning about the expiring lock
392     */
393    locktimer.warning = function(){
394        locktimer.clear();
395        alert(locktimer.msg);
396    };
397
398    /**
399     * Remove the current warning timer
400     */
401    locktimer.clear = function(){
402        if(locktimer.timerID !== null){
403            window.clearTimeout(locktimer.timerID);
404            locktimer.timerID = null;
405        }
406    };
407
408    /**
409     * Refresh the lock via AJAX
410     *
411     * Called on keypresses in the edit area
412     */
413    locktimer.refresh = function(){
414        var now = new Date();
415        // refresh every minute only
416        if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
417            var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid);
418            if(locktimer.draft){
419                var dwform = $('dw__editform');
420                params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value);
421                params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value);
422                params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value);
423                params += '&date='+encodeURIComponent(dwform.elements.date.value);
424            }
425            locktimer.sack.runAJAX(params);
426            locktimer.lasttime = now;
427        }
428    };
429
430
431    /**
432     * Callback. Resets the warning timer
433     */
434    locktimer.refreshed = function(){
435        var data  = this.response;
436        var error = data.charAt(0);
437            data  = data.substring(1);
438
439        $('draft__status').innerHTML=data;
440        if(error != '1') return; // locking failed
441        locktimer.reset();
442    };
443// end of locktimer class functions
444
445