xref: /dokuwiki/lib/scripts/edit.js (revision be615a48b77032ed713b970521ed46503be853e7)
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,classname){
16    var btn = document.createElement('button');
17    var ico = document.createElement('img');
18
19    // preapare the basic button stuff
20    btn.className = 'toolbutton';
21    if(classname){
22        btn.className += ' '+classname;
23    }
24    btn.title = label;
25    if(key){
26        btn.title += ' ['+key.toUpperCase()+']';
27        btn.accessKey = key;
28    }
29
30    // set IDs if given
31    if(id){
32        btn.id = id;
33        ico.id = id+'_ico';
34    }
35
36    // create the icon and add it to the button
37    if(icon.substr(0,1) == '/'){
38        ico.src = icon;
39    }else{
40        ico.src = DOKU_BASE+'lib/images/toolbar/'+icon;
41    }
42    btn.appendChild(ico);
43
44    return btn;
45}
46
47/**
48 * Creates a picker window for inserting text
49 *
50 * The given list can be an associative array with text,icon pairs
51 * or a simple list of text. Style the picker window through the picker
52 * class or the picker buttons with the pickerbutton class. Picker
53 * windows are appended to the body and created invisible.
54 *
55 * @param  string id    the ID to assign to the picker
56 * @param  array  props the properties for the picker
57 * @param  string edid  the ID of the textarea
58 * @rteurn DOMobject    the created picker
59 * @author Andreas Gohr <andi@splitbrain.org>
60 */
61function createPicker(id,props,edid){
62    var icobase = props['icobase'];
63    var list    = props['list'];
64
65    // create the wrapping div
66    var picker            = document.createElement('div');
67    picker.className      = 'picker';
68    if(props['class']){
69        picker.className += ' '+props['class'];
70    }
71    picker.id               = id;
72    picker.style.position   = 'absolute';
73    picker.style.marginLeft = '-10000px'; // no display:none, to keep access keys working
74    picker.style.marginTop  = '-10000px';
75
76    for(var key in list){
77        if (!list.hasOwnProperty(key)) continue;
78
79        if(isNaN(key)){
80            // associative array -> treat as image/value pairs
81            var btn = document.createElement('button');
82            btn.className = 'pickerbutton';
83            var ico = document.createElement('img');
84            if(list[key].substr(0,1) == '/'){
85                ico.src = list[key];
86            }else{
87                ico.src = DOKU_BASE+'lib/images/'+icobase+'/'+list[key];
88            }
89            btn.title     = key;
90            btn.appendChild(ico);
91            addEvent(btn,'click',bind(pickerInsert,key,edid));
92            picker.appendChild(btn);
93        }else if(isString(list[key])){
94            // a list of text -> treat as text picker
95            var btn = document.createElement('button');
96            btn.className = 'pickerbutton';
97            var txt = document.createTextNode(list[key]);
98            btn.title     = list[key];
99            btn.appendChild(txt);
100            addEvent(btn,'click',bind(pickerInsert,list[key],edid));
101            picker.appendChild(btn);
102        }else{
103            // a list of lists -> treat it as subtoolbar
104            initToolbar(picker,edid,list);
105            break; // all buttons handled already
106        }
107
108    }
109    var body = document.getElementsByTagName('body')[0];
110    body.appendChild(picker);
111    return picker;
112}
113
114/**
115 * Called by picker buttons to insert Text and close the picker again
116 *
117 * @author Andreas Gohr <andi@splitbrain.org>
118 */
119function pickerInsert(text,edid){
120    insertAtCarret(edid,text);
121    pickerClose();
122}
123
124/**
125 * Add button action for signature button
126 *
127 * @param  DOMElement btn   Button element to add the action to
128 * @param  array      props Associative array of button properties
129 * @param  string     edid  ID of the editor textarea
130 * @return boolean    If button should be appended
131 * @author Gabriel Birke <birke@d-scribe.de>
132 */
133function addBtnActionSignature(btn, props, edid) {
134    if(typeof(SIG) != 'undefined' && SIG != ''){
135        addEvent(btn,'click',bind(insertAtCarret,edid,SIG));
136        return true;
137    }
138    return false;
139}
140
141/**
142 * Make intended formattings easier to handle
143 *
144 * Listens to all key inputs and handle indentions
145 * of lists and code blocks
146 *
147 * Currently handles space, backspce and enter presses
148 *
149 * @author Andreas Gohr <andi@splitbrain.org>
150 * @fixme handle tabs
151 */
152function keyHandler(e){
153    if(e.keyCode != 13 &&
154       e.keyCode != 8  &&
155       e.keyCode != 32) return;
156    var field     = e.target;
157    var selection = getSelection(field);
158    var search    = "\n"+field.value.substr(0,selection.start);
159    var linestart = Math.max(search.lastIndexOf("\n"),
160                             search.lastIndexOf("\r")); //IE workaround
161    search = search.substr(linestart);
162
163
164    if(e.keyCode == 13){ // Enter
165        // keep current indention for lists and code
166        var match = search.match(/(\n  +([\*-] ?)?)/);
167        if(match){
168            var scroll = field.scrollHeight;
169            insertAtCarret(field.id,match[1]);
170            field.scrollTop += (field.scrollHeight - scroll);
171            e.preventDefault(); // prevent enter key
172            return false;
173        }
174    }else if(e.keyCode == 8){ // Backspace
175        // unindent lists
176        var match = search.match(/(\n  +)([*-] ?)$/);
177        if(match){
178            var spaces = match[1].length-1;
179
180            if(spaces > 3){ // unindent one level
181                field.value = field.value.substr(0,linestart)+
182                              field.value.substr(linestart+2);
183                selection.start = selection.start - 2;
184                selection.end   = selection.start;
185            }else{ // delete list point
186                field.value = field.value.substr(0,linestart)+
187                              field.value.substr(selection.start);
188                selection.start = linestart;
189                selection.end   = linestart;
190            }
191            setSelection(selection);
192            e.preventDefault(); // prevent backspace
193            return false;
194        }
195    }else if(e.keyCode == 32){ // Space
196        // intend list item
197        var match = search.match(/(\n  +)([*-] )$/);
198        if(match){
199            field.value = field.value.substr(0,linestart)+'  '+
200                          field.value.substr(linestart);
201            selection.start = selection.start + 2;
202            selection.end   = selection.start;
203            setSelection(selection);
204            e.preventDefault(); // prevent space
205            return false;
206        }
207    }
208}
209
210//FIXME consolidate somewhere else
211addInitEvent(function(){
212    var field = $('wiki__text');
213    if(!field) return;
214    addEvent(field,'keydown',keyHandler);
215});
216
217/**
218 * Determine the current section level while editing
219 *
220 * @author Andreas Gohr <gohr@cosmocode.de>
221 */
222function currentHeadlineLevel(textboxId){
223    var field     = $(textboxId);
224    var selection = getSelection(field);
225    var search    = "\n"+field.value.substr(0,selection.start);
226    var lasthl    = search.lastIndexOf("\n==");
227    if(lasthl == -1 && field.form.prefix){
228        // we need to look in prefix context
229        search = field.form.prefix.value;
230        lasthl    = search.lastIndexOf("\n==");
231    }
232    search    = search.substr(lasthl+1,6);
233
234    if(search == '======') return 1;
235    if(search.substr(0,5) == '=====') return 2;
236    if(search.substr(0,4) == '====') return 3;
237    if(search.substr(0,3) == '===') return 4;
238    if(search.substr(0,2) == '==') return 5;
239
240    return 0;
241}
242
243
244/**
245 * global var used for not saved yet warning
246 */
247var textChanged = false;
248
249/**
250 * Check for changes before leaving the page
251 */
252function changeCheck(msg){
253  if(textChanged){
254    var ok = confirm(msg);
255    if(ok){
256        // remove a possibly saved draft using ajax
257        var dwform = $('dw__editform');
258        if(dwform){
259            var params = 'call=draftdel';
260            params += '&id='+encodeURIComponent(dwform.elements.id.value);
261
262            var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
263            sackobj.AjaxFailedAlert = '';
264            sackobj.encodeURIString = false;
265            sackobj.runAJAX(params);
266            // we send this request blind without waiting for
267            // and handling the returned data
268        }
269    }
270    return ok;
271  }else{
272    return true;
273  }
274}
275
276/**
277 * Add changeCheck to all Links and Forms (except those with a
278 * JSnocheck class), add handlers to monitor changes
279 *
280 * Sets focus to the editbox as well
281 *
282 * @fixme this is old and crappy code. needs to be redone
283 */
284function initChangeCheck(msg){
285    var edit_text   = document.getElementById('wiki__text');
286    if(!edit_text) return;
287    if(edit_text.readOnly) return;
288    if(!$('dw__editform')) return;
289
290    // add change check for links
291    var links = document.getElementsByTagName('a');
292    for(var i=0; i < links.length; i++){
293        if(links[i].className.indexOf('JSnocheck') == -1){
294            links[i].onclick = function(){
295                                    var rc = changeCheck(msg);
296                                    if(window.event) window.event.returnValue = rc;
297                                    return rc;
298                               };
299        }
300    }
301    // add change check for forms
302    var forms = document.forms;
303    for(i=0; i < forms.length; i++){
304        if(forms[i].className.indexOf('JSnocheck') == -1){
305            forms[i].onsubmit = function(){
306                                    var rc = changeCheck(msg);
307                                    if(window.event) window.event.returnValue = rc;
308                                    return rc;
309                               };
310        }
311    }
312
313    // reset change memory var on submit
314    var btn_save        = document.getElementById('edbtn__save');
315    btn_save.onclick    = function(){ textChanged = false; };
316    var btn_prev        = document.getElementById('edbtn__preview');
317    btn_prev.onclick    = function(){ textChanged = false; };
318
319    // add change memory setter
320    edit_text.onchange = function(){
321        textChanged = true; //global var
322        summaryCheck();
323    };
324    var summary = document.getElementById('edit__summary');
325    addEvent(summary, 'change', summaryCheck);
326    addEvent(summary, 'keyup', summaryCheck);
327    if (textChanged) summaryCheck();
328
329    // set focus
330    edit_text.focus();
331}
332
333/**
334 * Checks if a summary was entered - if not the style is changed
335 *
336 * @author Andreas Gohr <andi@splitbrain.org>
337 */
338function summaryCheck(){
339    var sum = document.getElementById('edit__summary');
340    if(sum.value === ''){
341        sum.className='missing';
342    }else{
343        sum.className='edit';
344    }
345}
346
347
348/**
349 * Class managing the timer to display a warning on a expiring lock
350 */
351function locktimer_class(){
352        this.sack     = null;
353        this.timeout  = 0;
354        this.timerID  = null;
355        this.lasttime = null;
356        this.msg      = '';
357        this.pageid   = '';
358};
359var locktimer = new locktimer_class();
360    locktimer.init = function(timeout,msg,draft){
361        // init values
362        locktimer.timeout  = timeout*1000;
363        locktimer.msg      = msg;
364        locktimer.draft    = draft;
365        locktimer.lasttime = new Date();
366
367        if(!$('dw__editform')) return;
368        locktimer.pageid = $('dw__editform').elements.id.value;
369        if(!locktimer.pageid) return;
370
371        // init ajax component
372        locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php');
373        locktimer.sack.AjaxFailedAlert = '';
374        locktimer.sack.encodeURIString = false;
375        locktimer.sack.onCompletion = locktimer.refreshed;
376
377        // register refresh event
378        addEvent($('dw__editform').elements.wikitext,'keypress',function(){locktimer.refresh();});
379
380        // start timer
381        locktimer.reset();
382    };
383
384    /**
385     * (Re)start the warning timer
386     */
387    locktimer.reset = function(){
388        locktimer.clear();
389        locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout);
390    };
391
392    /**
393     * Display the warning about the expiring lock
394     */
395    locktimer.warning = function(){
396        locktimer.clear();
397        alert(locktimer.msg);
398    };
399
400    /**
401     * Remove the current warning timer
402     */
403    locktimer.clear = function(){
404        if(locktimer.timerID !== null){
405            window.clearTimeout(locktimer.timerID);
406            locktimer.timerID = null;
407        }
408    };
409
410    /**
411     * Refresh the lock via AJAX
412     *
413     * Called on keypresses in the edit area
414     */
415    locktimer.refresh = function(){
416        var now = new Date();
417        // refresh every minute only
418        if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
419            var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid);
420            if(locktimer.draft){
421                var dwform = $('dw__editform');
422                params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value);
423                params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value);
424                params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value);
425                params += '&date='+encodeURIComponent(dwform.elements.date.value);
426            }
427            locktimer.sack.runAJAX(params);
428            locktimer.lasttime = now;
429        }
430    };
431
432
433    /**
434     * Callback. Resets the warning timer
435     */
436    locktimer.refreshed = function(){
437        var data  = this.response;
438        var error = data.charAt(0);
439            data  = data.substring(1);
440
441        $('draft__status').innerHTML=data;
442        if(error != '1') return; // locking failed
443        locktimer.reset();
444    };
445// end of locktimer class functions
446
447