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