xref: /dokuwiki/lib/scripts/edit.js (revision 47583ff0428eaa0d97e26f61f81ac6d199e9b446)
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 * @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.display  = 'none';
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            eval("btn.onclick = function(){pickerInsert('"+
88                                  jsEscape(key)+"','"+
89                                  jsEscape(edid)+"');return false;}");
90            picker.appendChild(btn);
91        }else if(isString(list[key])){
92            // a list of text -> treat as text picker
93            var btn = document.createElement('button');
94            btn.className = 'pickerbutton';
95            var txt = document.createTextNode(list[key]);
96            btn.title     = list[key];
97            btn.appendChild(txt);
98            eval("btn.onclick = function(){pickerInsert('"+
99                                  jsEscape(list[key])+"','"+
100                                  jsEscape(edid)+"');return false;}");
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    // insert
121    insertAtCarret(edid,text);
122    // close picker
123    pickerClose();
124}
125
126/**
127 * Show a previosly created picker window
128 *
129 * @author Andreas Gohr <andi@splitbrain.org>
130 */
131function showPicker(pickerid,btn){
132    var picker = document.getElementById(pickerid);
133    var x = findPosX(btn);
134    var y = findPosY(btn);
135    if(picker.style.display == 'none'){
136        picker.style.display = 'block';
137        picker.style.left = (x+3)+'px';
138        picker.style.top = (y+btn.offsetHeight+3)+'px';
139    }else{
140        picker.style.display = 'none';
141    }
142}
143
144/**
145 * Add button action for signature button
146 *
147 * @param  DOMElement btn   Button element to add the action to
148 * @param  array      props Associative array of button properties
149 * @param  string     edid  ID of the editor textarea
150 * @return boolean    If button should be appended
151 * @author Gabriel Birke <birke@d-scribe.de>
152 */
153function addBtnActionSignature(btn, props, edid)
154{
155    if(typeof(SIG) != 'undefined' && SIG != ''){
156        eval("btn.onclick = function(){insertAtCarret('"+
157            jsEscape(edid)+"','"+
158            jsEscape(SIG)+
159        "');return false;}");
160        return true;
161    }
162    return false;
163}
164
165
166/**
167 * Add button action for the mediapopup button
168 *
169 * @param  DOMElement btn   Button element to add the action to
170 * @param  array      props Associative array of button properties
171 * @return boolean    If button should be appended
172 * @author Gabriel Birke <birke@d-scribe.de>
173 */
174function addBtnActionMediapopup(btn, props)
175{
176    eval("btn.onclick = function(){window.open('"+DOKU_BASE+
177        jsEscape(props['url']+encodeURIComponent(NS))+"','"+
178        jsEscape(props['name'])+"','"+
179        jsEscape(props['options'])+
180    "');return false;}");
181    return true;
182}
183
184function addBtnActionAutohead(btn, props, edid, id)
185{
186    eval("btn.onclick = function(){"+
187    "insertHeadline('"+edid+"',"+props['mod']+",'"+jsEscape(props['text'])+"'); "+
188    "return false};");
189    return true;
190}
191
192/**
193 * Make intended formattings easier to handle
194 *
195 * Listens to all key inputs and handle indentions
196 * of lists and code blocks
197 *
198 * Currently handles space, backspce and enter presses
199 *
200 * @author Andreas Gohr <andi@splitbrain.org>
201 * @fixme handle tabs
202 */
203function keyHandler(e){
204    if(e.keyCode != 13 &&
205       e.keyCode != 8  &&
206       e.keyCode != 32) return;
207    var field     = e.target;
208    var selection = getSelection(field);
209    var search    = "\n"+field.value.substr(0,selection.start);
210    var linestart = Math.max(search.lastIndexOf("\n"),
211                             search.lastIndexOf("\r")); //IE workaround
212    search = search.substr(linestart);
213
214    if(e.keyCode == 13){ // Enter
215        // keep current indention for lists and code
216        var match = search.match(/(\n  +([\*-] ?)?)/);
217        if(match){
218            insertAtCarret(field.id,match[1]);
219            e.preventDefault(); // prevent enter key
220        }
221    }else if(e.keyCode == 8){ // Backspace
222        // unindent lists
223        var match = search.match(/(\n  +)([*-] ?)$/);
224        if(match){
225            var spaces = match[1].length-1;
226
227            if(spaces > 3){ // unindent one level
228                field.value = field.value.substr(0,linestart)+
229                              field.value.substr(linestart+2);
230                selection.start = selection.start - 2;
231                selection.end   = selection.start;
232            }else{ // delete list point
233                field.value = field.value.substr(0,linestart)+
234                              field.value.substr(selection.start);
235                selection.start = linestart;
236                selection.end   = linestart;
237            }
238            setSelection(selection);
239            e.preventDefault(); // prevent backspace
240        }
241    }else if(e.keyCode == 32){ // Space
242        // intend list item
243        var match = search.match(/(\n  +)([*-] )$/);
244        if(match){
245            field.value = field.value.substr(0,linestart)+'  '+
246                          field.value.substr(linestart);
247            selection.start = selection.start + 2;
248            selection.end   = selection.start;
249            setSelection(selection);
250            e.preventDefault(); // prevent space
251        }
252    }
253}
254
255//FIXME consolidate somewhere else
256addInitEvent(function(){
257    var field = $('wiki__text');
258    if(!field) return;
259    addEvent(field,'keydown',keyHandler);
260});
261
262/**
263 * Determine the current section level while editing
264 *
265 * @author Andreas Gohr <gohr@cosmocode.de>
266 */
267function currentHeadlineLevel(textboxId){
268    var field     = $(textboxId);
269    var selection = getSelection(field);
270    var search    = "\n"+field.value.substr(0,selection.start);
271    var lasthl    = search.lastIndexOf("\n==");
272    if(lasthl == -1 && field.form.prefix){
273        // we need to look in prefix context
274        search = field.form.prefix.value;
275        lasthl    = search.lastIndexOf("\n==");
276    }
277    search    = search.substr(lasthl+1,6);
278
279    if(search == '======') return 1;
280    if(search.substr(0,5) == '=====') return 2;
281    if(search.substr(0,4) == '====') return 3;
282    if(search.substr(0,3) == '===') return 4;
283    if(search.substr(0,2) == '==') return 5;
284
285    return 0;
286}
287
288/**
289 * Insert a new headline based on the current section level
290 *
291 * @param string textboxId - the edit field ID
292 * @param int    mod       - the headline modificator ( -1, 0, 1)
293 * @param string text      - the sample text passed to insertTags
294 */
295function insertHeadline(textboxId,mod,text){
296    var lvl = currentHeadlineLevel(textboxId);
297
298
299    // determine new level
300    lvl += mod;
301    if(lvl < 1) lvl = 1;
302    if(lvl > 5) lvl = 5;
303
304    var tags = '=';
305    for(var i=0; i<=5-lvl; i++) tags += '=';
306    insertTags(textboxId, tags+' ', ' '+tags+"\n", text);
307    pickerClose();
308}
309
310/**
311 * global var used for not saved yet warning
312 */
313var textChanged = false;
314
315/**
316 * Check for changes before leaving the page
317 */
318function changeCheck(msg){
319  if(textChanged){
320    var ok = confirm(msg);
321    if(ok){
322        // remove a possibly saved draft using ajax
323        var dwform = $('dw__editform');
324        if(dwform){
325            var params = 'call=draftdel';
326            params += '&id='+encodeURIComponent(dwform.elements.id.value);
327
328            var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
329            sackobj.AjaxFailedAlert = '';
330            sackobj.encodeURIString = false;
331            sackobj.runAJAX(params);
332            // we send this request blind without waiting for
333            // and handling the returned data
334        }
335    }
336    return ok;
337  }else{
338    return true;
339  }
340}
341
342/**
343 * Add changeCheck to all Links and Forms (except those with a
344 * JSnocheck class), add handlers to monitor changes
345 *
346 * Sets focus to the editbox as well
347 *
348 * @fixme this is old and crappy code. needs to be redone
349 */
350function initChangeCheck(msg){
351    var edit_text   = document.getElementById('wiki__text');
352    if(!edit_text) return;
353
354    // add change check for links
355    var links = document.getElementsByTagName('a');
356    for(var i=0; i < links.length; i++){
357        if(links[i].className.indexOf('JSnocheck') == -1){
358            links[i].onclick = function(){
359                                    var rc = changeCheck(msg);
360                                    if(window.event) window.event.returnValue = rc;
361                                    return rc;
362                               };
363        }
364    }
365    // add change check for forms
366    var forms = document.forms;
367    for(i=0; i < forms.length; i++){
368        if(forms[i].className.indexOf('JSnocheck') == -1){
369            forms[i].onsubmit = function(){
370                                    var rc = changeCheck(msg);
371                                    if(window.event) window.event.returnValue = rc;
372                                    return rc;
373                               };
374        }
375    }
376
377    // reset change memory var on submit
378    var btn_save        = document.getElementById('edbtn__save');
379    btn_save.onclick    = function(){ textChanged = false; };
380    var btn_prev        = document.getElementById('edbtn__preview');
381    btn_prev.onclick    = function(){ textChanged = false; };
382
383    // add change memory setter
384    edit_text.onchange = function(){
385        textChanged = true; //global var
386        summaryCheck();
387    };
388    var summary = document.getElementById('edit__summary');
389    addEvent(summary, 'change', summaryCheck);
390    addEvent(summary, 'keyup', summaryCheck);
391    if (textChanged) summaryCheck();
392
393    // set focus
394    edit_text.focus();
395}
396
397/**
398 * Checks if a summary was entered - if not the style is changed
399 *
400 * @author Andreas Gohr <andi@splitbrain.org>
401 */
402function summaryCheck(){
403    var sum = document.getElementById('edit__summary');
404    if(sum.value === ''){
405        sum.className='missing';
406    }else{
407        sum.className='edit';
408    }
409}
410
411
412/**
413 * Class managing the timer to display a warning on a expiring lock
414 */
415function locktimer_class(){
416        this.sack     = null;
417        this.timeout  = 0;
418        this.timerID  = null;
419        this.lasttime = null;
420        this.msg      = '';
421        this.pageid   = '';
422};
423var locktimer = new locktimer_class();
424    locktimer.init = function(timeout,msg,draft){
425        // init values
426        locktimer.timeout  = timeout*1000;
427        locktimer.msg      = msg;
428        locktimer.draft    = draft;
429        locktimer.lasttime = new Date();
430
431        if(!$('dw__editform')) return;
432        locktimer.pageid = $('dw__editform').elements.id.value;
433        if(!locktimer.pageid) return;
434
435        // init ajax component
436        locktimer.sack = new sack(DOKU_BASE + 'lib/exe/ajax.php');
437        locktimer.sack.AjaxFailedAlert = '';
438        locktimer.sack.encodeURIString = false;
439        locktimer.sack.onCompletion = locktimer.refreshed;
440
441        // register refresh event
442        addEvent($('dw__editform').elements.wikitext,'keypress',function(){locktimer.refresh();});
443
444        // start timer
445        locktimer.reset();
446    };
447
448    /**
449     * (Re)start the warning timer
450     */
451    locktimer.reset = function(){
452        locktimer.clear();
453        locktimer.timerID = window.setTimeout("locktimer.warning()", locktimer.timeout);
454    };
455
456    /**
457     * Display the warning about the expiring lock
458     */
459    locktimer.warning = function(){
460        locktimer.clear();
461        alert(locktimer.msg);
462    };
463
464    /**
465     * Remove the current warning timer
466     */
467    locktimer.clear = function(){
468        if(locktimer.timerID !== null){
469            window.clearTimeout(locktimer.timerID);
470            locktimer.timerID = null;
471        }
472    };
473
474    /**
475     * Refresh the lock via AJAX
476     *
477     * Called on keypresses in the edit area
478     */
479    locktimer.refresh = function(){
480        var now = new Date();
481        // refresh every minute only
482        if(now.getTime() - locktimer.lasttime.getTime() > 30*1000){ //FIXME decide on time
483            var params = 'call=lock&id='+encodeURIComponent(locktimer.pageid);
484            if(locktimer.draft){
485                var dwform = $('dw__editform');
486                params += '&prefix='+encodeURIComponent(dwform.elements.prefix.value);
487                params += '&wikitext='+encodeURIComponent(dwform.elements.wikitext.value);
488                params += '&suffix='+encodeURIComponent(dwform.elements.suffix.value);
489                params += '&date='+encodeURIComponent(dwform.elements.date.value);
490            }
491            locktimer.sack.runAJAX(params);
492            locktimer.lasttime = now;
493        }
494    };
495
496
497    /**
498     * Callback. Resets the warning timer
499     */
500    locktimer.refreshed = function(){
501        var data  = this.response;
502        var error = data.charAt(0);
503            data  = data.substring(1);
504
505        $('draft__status').innerHTML=data;
506        if(error != '1') return; // locking failed
507        locktimer.reset();
508    };
509// end of locktimer class functions
510
511