xref: /dokuwiki/lib/scripts/edit.js (revision 7441e340554a7a982047997cf61f72adaefacc99)
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    if(selection.getLength()) return; //there was text selected, keep standard behavior
159    var search    = "\n"+field.value.substr(0,selection.start);
160    var linestart = Math.max(search.lastIndexOf("\n"),
161                             search.lastIndexOf("\r")); //IE workaround
162    search = search.substr(linestart);
163
164
165    if(e.keyCode == 13){ // Enter
166        // keep current indention for lists and code
167        var match = search.match(/(\n  +([\*-] ?)?)/);
168        if(match){
169            var scroll = field.scrollHeight;
170            var match2 = search.match(/^\n  +[\*-]\s*$/);
171            // Cancel list if the last item is empty (i. e. two times enter)
172            if (match2 && field.value.substr(selection.start).match(/^($|\r?\n)/)) {
173                field.value = field.value.substr(0, linestart) + "\n" +
174                              field.value.substr(selection.start);
175                selection.start = linestart + 1;
176                selection.end = linestart + 1;
177                setSelection(selection);
178            } else {
179                insertAtCarret(field.id,match[1]);
180            }
181            field.scrollTop += (field.scrollHeight - scroll);
182            e.preventDefault(); // prevent enter key
183            return false;
184        }
185    }else if(e.keyCode == 8){ // Backspace
186        // unindent lists
187        var match = search.match(/(\n  +)([*-] ?)$/);
188        if(match){
189            var spaces = match[1].length-1;
190
191            if(spaces > 3){ // unindent one level
192                field.value = field.value.substr(0,linestart)+
193                              field.value.substr(linestart+2);
194                selection.start = selection.start - 2;
195                selection.end   = selection.start;
196            }else{ // delete list point
197                field.value = field.value.substr(0,linestart)+
198                              field.value.substr(selection.start);
199                selection.start = linestart;
200                selection.end   = linestart;
201            }
202            setSelection(selection);
203            e.preventDefault(); // prevent backspace
204            return false;
205        }
206    }else if(e.keyCode == 32){ // Space
207        // intend list item
208        var match = search.match(/(\n  +)([*-] )$/);
209        if(match){
210            field.value = field.value.substr(0,linestart)+'  '+
211                          field.value.substr(linestart);
212            selection.start = selection.start + 2;
213            selection.end   = selection.start;
214            setSelection(selection);
215            e.preventDefault(); // prevent space
216            return false;
217        }
218    }
219}
220
221//FIXME consolidate somewhere else
222addInitEvent(function(){
223    var field = $('wiki__text');
224    if(!field) return;
225    // in Firefox, keypress doesn't send the correct keycodes,
226    // in Opera, the default of keydown can't be prevented
227    if (is_opera) {
228        addEvent(field,'keypress',keyHandler);
229    } else {
230        addEvent(field,'keydown',keyHandler);
231    }
232});
233
234/**
235 * Determine the current section level while editing
236 *
237 * @author Andreas Gohr <gohr@cosmocode.de>
238 */
239function currentHeadlineLevel(textboxId){
240    var field     = $(textboxId);
241    var selection = getSelection(field);
242    var search    = "\n"+field.value.substr(0,selection.start);
243    var lasthl    = search.lastIndexOf("\n==");
244    if(lasthl == -1 && field.form.prefix){
245        // we need to look in prefix context
246        search = field.form.prefix.value;
247        lasthl    = search.lastIndexOf("\n==");
248    }
249    search    = search.substr(lasthl+1,6);
250
251    if(search == '======') return 1;
252    if(search.substr(0,5) == '=====') return 2;
253    if(search.substr(0,4) == '====') return 3;
254    if(search.substr(0,3) == '===') return 4;
255    if(search.substr(0,2) == '==') return 5;
256
257    return 0;
258}
259
260
261/**
262 * global var used for not saved yet warning
263 */
264var textChanged = false;
265
266/**
267 * Delete the draft before leaving the page
268 */
269function deleteDraft() {
270    if (is_opera) return;
271    if (window.keepDraft) return;
272
273    // remove a possibly saved draft using ajax
274    var dwform = $('dw__editform');
275    if(dwform){
276        var params = 'call=draftdel';
277        params += '&id='+encodeURIComponent(dwform.elements.id.value);
278
279        var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
280        // this needs to be synchronous and GET to not be aborted upon page unload
281        sackobj.asynchronous = false;
282        sackobj.method = 'GET';
283        sackobj.AjaxFailedAlert = '';
284        sackobj.encodeURIString = false;
285        sackobj.runAJAX(params);
286    }
287}
288
289/**
290 * Activate "not saved" dialog, add draft deletion to page unload,
291 * add handlers to monitor changes
292 *
293 * Sets focus to the editbox as well
294 */
295addInitEvent(function (){
296    var editform = $('dw__editform');
297    if (!editform) return;
298
299    var edit_text   = $('wiki__text');
300    if(edit_text) {
301        if(edit_text.readOnly) return;
302
303        // set focus
304        edit_text.focus();
305    }
306
307    var checkfunc = function(){
308        textChanged = true; //global var
309        summaryCheck();
310    };
311    addEvent(editform, 'change', checkfunc);
312    addEvent(editform, 'keydown', checkfunc);
313
314    window.onbeforeunload = function(){
315        if(textChanged) {
316            return LANG.notsavedyet;
317        }
318    };
319    window.onunload = deleteDraft;
320
321    // reset change memory var on submit
322    addEvent($('edbtn__save'), 'click', function(){
323        textChanged = false;
324    });
325    addEvent($('edbtn__preview'), 'click', function(){
326        textChanged = false;
327        window.keepDraft = true; // needed to keep draft on page unload
328    });
329
330    var summary = $('edit__summary');
331    addEvent(summary, 'change', summaryCheck);
332    addEvent(summary, 'keyup', summaryCheck);
333    if (textChanged) summaryCheck();
334});
335
336/**
337 * Checks if a summary was entered - if not the style is changed
338 *
339 * @author Andreas Gohr <andi@splitbrain.org>
340 */
341function summaryCheck(){
342    var sum = document.getElementById('edit__summary');
343    if(sum.value === ''){
344        sum.className='missing';
345    }else{
346        sum.className='edit';
347    }
348}
349
350