xref: /dokuwiki/lib/scripts/edit.js (revision 3c743051cd4555135bdbf55c27733a2281463b4c)
1/**
2 * Functions for text editing (toolbar stuff)
3 *
4 * @todo most of the stuff in here should be revamped and then moved to toolbar.js
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 * @param  string id    the ID to assign to the picker
53 * @param  array  props the properties for the picker
54 * @param  string edid  the ID of the textarea
55 * @rteurn DOMobject    the created picker
56 * @author Andreas Gohr <andi@splitbrain.org>
57 */
58function createPicker(id,props,edid){
59    var icobase = props['icobase'];
60    var list    = props['list'];
61
62    // create the wrapping div
63    var picker            = document.createElement('div');
64    picker.className      = 'picker';
65    if(props['class']){
66        picker.className += ' '+props['class'];
67    }
68    picker.id               = id;
69    picker.style.position   = 'absolute';
70    picker.style.marginLeft = '-10000px'; // no display:none, to keep access keys working
71    picker.style.marginTop  = '-10000px';
72
73    for(var key in list){
74        if (!list.hasOwnProperty(key)) continue;
75
76        if(isNaN(key)){
77            // associative array -> treat as image/value pairs
78            var btn = document.createElement('button');
79            btn.className = 'pickerbutton';
80            var ico = document.createElement('img');
81            if(list[key].substr(0,1) == '/'){
82                ico.src = list[key];
83            }else{
84                ico.src = DOKU_BASE+'lib/images/'+icobase+'/'+list[key];
85            }
86            btn.title     = key;
87            btn.appendChild(ico);
88            addEvent(btn,'click',bind(pickerInsert,key,edid));
89            picker.appendChild(btn);
90        }else if(isString(list[key])){
91            // a list of text -> treat as text picker
92            var btn = document.createElement('button');
93            btn.className = 'pickerbutton';
94            var txt = document.createTextNode(list[key]);
95            btn.title     = list[key];
96            btn.appendChild(txt);
97            addEvent(btn,'click',bind(pickerInsert,list[key],edid));
98            picker.appendChild(btn);
99        }else{
100            // a list of lists -> treat it as subtoolbar
101            initToolbar(picker,edid,list);
102            break; // all buttons handled already
103        }
104
105    }
106    var body = document.getElementsByTagName('body')[0];
107    body.appendChild(picker);
108    return picker;
109}
110
111/**
112 * Called by picker buttons to insert Text and close the picker again
113 *
114 * @author Andreas Gohr <andi@splitbrain.org>
115 */
116function pickerInsert(text,edid){
117    insertAtCarret(edid,text);
118    pickerClose();
119}
120
121/**
122 * Add button action for signature button
123 *
124 * @param  DOMElement btn   Button element to add the action to
125 * @param  array      props Associative array of button properties
126 * @param  string     edid  ID of the editor textarea
127 * @return boolean    If button should be appended
128 * @author Gabriel Birke <birke@d-scribe.de>
129 */
130function addBtnActionSignature(btn, props, edid) {
131    if(typeof(SIG) != 'undefined' && SIG != ''){
132        addEvent(btn,'click',bind(insertAtCarret,edid,SIG));
133        return true;
134    }
135    return false;
136}
137
138/**
139 * Make intended formattings easier to handle
140 *
141 * Listens to all key inputs and handle indentions
142 * of lists and code blocks
143 *
144 * Currently handles space, backspce and enter presses
145 *
146 * @author Andreas Gohr <andi@splitbrain.org>
147 * @fixme handle tabs
148 */
149function keyHandler(e){
150    if(e.keyCode != 13 &&
151       e.keyCode != 8  &&
152       e.keyCode != 32) return;
153    var field     = e.target;
154    var selection = getSelection(field);
155    if(selection.getLength()) return; //there was text selected, keep standard behavior
156    var search    = "\n"+field.value.substr(0,selection.start);
157    var linestart = Math.max(search.lastIndexOf("\n"),
158                             search.lastIndexOf("\r")); //IE workaround
159    search = search.substr(linestart);
160
161
162    if(e.keyCode == 13){ // Enter
163        // keep current indention for lists and code
164        var match = search.match(/(\n  +([\*-] ?)?)/);
165        if(match){
166            var scroll = field.scrollHeight;
167            insertAtCarret(field.id,match[1]);
168            field.scrollTop += (field.scrollHeight - scroll);
169            e.preventDefault(); // prevent enter key
170            return false;
171        }
172    }else if(e.keyCode == 8){ // Backspace
173        // unindent lists
174        var match = search.match(/(\n  +)([*-] ?)$/);
175        if(match){
176            var spaces = match[1].length-1;
177
178            if(spaces > 3){ // unindent one level
179                field.value = field.value.substr(0,linestart)+
180                              field.value.substr(linestart+2);
181                selection.start = selection.start - 2;
182                selection.end   = selection.start;
183            }else{ // delete list point
184                field.value = field.value.substr(0,linestart)+
185                              field.value.substr(selection.start);
186                selection.start = linestart;
187                selection.end   = linestart;
188            }
189            setSelection(selection);
190            e.preventDefault(); // prevent backspace
191            return false;
192        }
193    }else if(e.keyCode == 32){ // Space
194        // intend list item
195        var match = search.match(/(\n  +)([*-] )$/);
196        if(match){
197            field.value = field.value.substr(0,linestart)+'  '+
198                          field.value.substr(linestart);
199            selection.start = selection.start + 2;
200            selection.end   = selection.start;
201            setSelection(selection);
202            e.preventDefault(); // prevent space
203            return false;
204        }
205    }
206}
207
208//FIXME consolidate somewhere else
209addInitEvent(function(){
210    var field = $('wiki__text');
211    if(!field) return;
212    addEvent(field,'keydown',keyHandler);
213});
214
215/**
216 * Determine the current section level while editing
217 *
218 * @author Andreas Gohr <gohr@cosmocode.de>
219 */
220function currentHeadlineLevel(textboxId){
221    var field     = $(textboxId);
222    var selection = getSelection(field);
223    var search    = "\n"+field.value.substr(0,selection.start);
224    var lasthl    = search.lastIndexOf("\n==");
225    if(lasthl == -1 && field.form.prefix){
226        // we need to look in prefix context
227        search = field.form.prefix.value;
228        lasthl    = search.lastIndexOf("\n==");
229    }
230    search    = search.substr(lasthl+1,6);
231
232    if(search == '======') return 1;
233    if(search.substr(0,5) == '=====') return 2;
234    if(search.substr(0,4) == '====') return 3;
235    if(search.substr(0,3) == '===') return 4;
236    if(search.substr(0,2) == '==') return 5;
237
238    return 0;
239}
240
241
242/**
243 * global var used for not saved yet warning
244 */
245var textChanged = false;
246
247/**
248 * Check for changes before leaving the page
249 */
250function changeCheck(msg){
251  if(textChanged){
252    var ok = confirm(msg);
253    if(ok){
254        // remove a possibly saved draft using ajax
255        var dwform = $('dw__editform');
256        if(dwform){
257            var params = 'call=draftdel';
258            params += '&id='+encodeURIComponent(dwform.elements.id.value);
259
260            var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
261            sackobj.AjaxFailedAlert = '';
262            sackobj.encodeURIString = false;
263            sackobj.runAJAX(params);
264            // we send this request blind without waiting for
265            // and handling the returned data
266        }
267    }
268    return ok;
269  }else{
270    return true;
271  }
272}
273
274/**
275 * Add changeCheck to all Links and Forms (except those with a
276 * JSnocheck class), add handlers to monitor changes
277 *
278 * Sets focus to the editbox as well
279 *
280 * @fixme this is old and crappy code. needs to be redone
281 */
282function initChangeCheck(msg){
283    var edit_text   = document.getElementById('wiki__text');
284    if(!edit_text) return;
285    if(edit_text.readOnly) return;
286    if(!$('dw__editform')) return;
287
288    // add change check for links
289    var links = document.getElementsByTagName('a');
290    for(var i=0; i < links.length; i++){
291        if(links[i].className.indexOf('JSnocheck') == -1){
292            links[i].onclick = function(){
293                                    var rc = changeCheck(msg);
294                                    if(window.event) window.event.returnValue = rc;
295                                    return rc;
296                               };
297        }
298    }
299    // add change check for forms
300    var forms = document.forms;
301    for(i=0; i < forms.length; i++){
302        if(forms[i].className.indexOf('JSnocheck') == -1){
303            forms[i].onsubmit = function(){
304                                    var rc = changeCheck(msg);
305                                    if(window.event) window.event.returnValue = rc;
306                                    return rc;
307                               };
308        }
309    }
310
311    // reset change memory var on submit
312    var btn_save        = document.getElementById('edbtn__save');
313    btn_save.onclick    = function(){ textChanged = false; };
314    var btn_prev        = document.getElementById('edbtn__preview');
315    btn_prev.onclick    = function(){ textChanged = false; };
316
317    // add change memory setter
318    edit_text.onchange = function(){
319        textChanged = true; //global var
320        summaryCheck();
321    };
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