xref: /dokuwiki/lib/scripts/edit.js (revision ff482cae0f5a620704d845037d60ae13ab851410)
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 * @author Michal Rezler <m.rezler@centrum.cz>
15 */
16function createToolButton(icon,label,key,id,classname){
17    var $ = jQuery;
18    var btn = $('<button>');
19    var ico = $('<img />');
20
21    // preapare the basic button stuff
22    btn.attr('class', 'toolbutton');
23    if(classname){
24        btn.attr('class', 'toolbutton '+classname);
25    }
26
27    btn.attr('title', label);
28    if(key){
29        btn.attr('title', label + ' ['+key.toUpperCase()+']')
30            .attr('accessKey', key);
31    }
32
33    // set IDs if given
34    if(id){
35        btn.attr('id', id);
36        ico.attr('id', id+'_ico');
37    }
38
39    // create the icon and add it to the button
40    if(icon.substr(0,1) == '/'){
41        ico.attr('src', icon);
42    }else{
43        ico.attr('src', DOKU_BASE+'lib/images/toolbar/'+icon);
44    }
45    btn.append(ico);
46
47    // we have to return a javascript object (for compatibility reasons)
48    return btn[0];
49}
50
51/**
52 * Creates a picker window for inserting text
53 *
54 * The given list can be an associative array with text,icon pairs
55 * or a simple list of text. Style the picker window through the picker
56 * class or the picker buttons with the pickerbutton class. Picker
57 * windows are appended to the body and created invisible.
58 *
59 * @param  string id    the ID to assign to the picker
60 * @param  array  props the properties for the picker
61 * @param  string edid  the ID of the textarea
62 * @rteurn DOMobject    the created picker
63 * @author Andreas Gohr <andi@splitbrain.org>
64 */
65function createPicker(id,props,edid){
66    var icobase = props['icobase'];
67    var list = props['list'];
68    var $ = jQuery;
69
70    // create the wrapping div
71    var picker = $('<div></div>');
72
73    var className = 'picker';
74    if(props['class']){
75        className += ' '+props['class'];
76    }
77
78    picker.attr('class', className)
79        .attr('id', id)
80        .css('position', 'absolute')
81        .css('marginLeft', '-10000px') // no display:none, to keep access keys working
82        .css('marginTop', '-10000px');
83
84    for(var key in list){
85        if (!list.hasOwnProperty(key)) continue;
86
87        if(isNaN(key)){
88            // associative array -> treat as image/value pairs
89            var btn = $('<button>');
90            btn.attr('class', 'pickerbutton')
91                .attr('title', key);
92
93            var ico = $('<img>');
94            if (list[key].substr(0,1) == '/') {
95                var src = list[key];
96            } else {
97                var src = DOKU_BASE+'lib/images/'+icobase+'/'+list[key];
98            }
99
100            ico.attr('src', src);
101            btn.append(ico);
102
103            btn.bind('click', bind(pickerInsert, key, edid));
104            picker.append(btn);
105        }else if (typeof (list[key]) == 'string'){
106            // a list of text -> treat as text picker
107            var btn = $('<button>');
108            btn.attr('class', 'pickerbutton')
109                .attr('title', list[key]);
110
111            var txt = $(document.createTextNode(list[key]));
112            btn.append(txt);
113
114            btn.bind('click', bind(pickerInsert, list[key], edid));
115
116            picker.append(btn);
117        }else{
118            // a list of lists -> treat it as subtoolbar
119            initToolbar(picker,edid,list);
120            break; // all buttons handled already
121        }
122
123    }
124    var body = $('body');
125    body.append(picker);
126
127    // we have to return a javascript object (for compatibility reasons)
128    return picker[0];
129}
130
131/**
132 * Called by picker buttons to insert Text and close the picker again
133 *
134 * @author Andreas Gohr <andi@splitbrain.org>
135 */
136function pickerInsert(text,edid){
137    insertAtCarret(edid,text);
138    pickerClose();
139}
140
141/**
142 * Add button action for signature button
143 *
144 * @param  DOMElement btn   Button element to add the action to
145 * @param  array      props Associative array of button properties
146 * @param  string     edid  ID of the editor textarea
147 * @return boolean    If button should be appended
148 * @author Gabriel Birke <birke@d-scribe.de>
149 */
150function addBtnActionSignature(btn, props, edid) {
151    if(typeof(SIG) != 'undefined' && SIG != ''){
152        btn.bind('click', bind(insertAtCarret,edid,SIG));
153        return true;
154    }
155    return false;
156}
157
158/**
159 * Make intended formattings easier to handle
160 *
161 * Listens to all key inputs and handle indentions
162 * of lists and code blocks
163 *
164 * Currently handles space, backspce and enter presses
165 *
166 * @author Andreas Gohr <andi@splitbrain.org>
167 * @fixme handle tabs
168 */
169function keyHandler(e){
170    if(e.keyCode != 13 &&
171       e.keyCode != 8  &&
172       e.keyCode != 32) return;
173    var field     = e.target;
174    var selection = getSelection(field);
175    if(selection.getLength()) return; //there was text selected, keep standard behavior
176    var search    = "\n"+field.value.substr(0,selection.start);
177    var linestart = Math.max(search.lastIndexOf("\n"),
178                             search.lastIndexOf("\r")); //IE workaround
179    search = search.substr(linestart);
180
181
182    if(e.keyCode == 13){ // Enter
183        // keep current indention for lists and code
184        var match = search.match(/(\n  +([\*-] ?)?)/);
185        if(match){
186            var scroll = field.scrollHeight;
187            var match2 = search.match(/^\n  +[\*-]\s*$/);
188            // Cancel list if the last item is empty (i. e. two times enter)
189            if (match2 && field.value.substr(selection.start).match(/^($|\r?\n)/)) {
190                field.value = field.value.substr(0, linestart) + "\n" +
191                              field.value.substr(selection.start);
192                selection.start = linestart + 1;
193                selection.end = linestart + 1;
194                setSelection(selection);
195            } else {
196                insertAtCarret(field.id,match[1]);
197            }
198            field.scrollTop += (field.scrollHeight - scroll);
199            e.preventDefault(); // prevent enter key
200            return false;
201        }
202    }else if(e.keyCode == 8){ // Backspace
203        // unindent lists
204        var match = search.match(/(\n  +)([*-] ?)$/);
205        if(match){
206            var spaces = match[1].length-1;
207
208            if(spaces > 3){ // unindent one level
209                field.value = field.value.substr(0,linestart)+
210                              field.value.substr(linestart+2);
211                selection.start = selection.start - 2;
212                selection.end   = selection.start;
213            }else{ // delete list point
214                field.value = field.value.substr(0,linestart)+
215                              field.value.substr(selection.start);
216                selection.start = linestart;
217                selection.end   = linestart;
218            }
219            setSelection(selection);
220            e.preventDefault(); // prevent backspace
221            return false;
222        }
223    }else if(e.keyCode == 32){ // Space
224        // intend list item
225        var match = search.match(/(\n  +)([*-] )$/);
226        if(match){
227            field.value = field.value.substr(0,linestart)+'  '+
228                          field.value.substr(linestart);
229            selection.start = selection.start + 2;
230            selection.end   = selection.start;
231            setSelection(selection);
232            e.preventDefault(); // prevent space
233            return false;
234        }
235    }
236}
237
238//FIXME consolidate somewhere else
239addInitEvent(function(){
240    var field = jQuery('#wiki__text');
241    if(field.length == 0) return;
242    // in Firefox, keypress doesn't send the correct keycodes,
243    // in Opera, the default of keydown can't be prevented
244    if (is_opera) {
245        field.bind('keypress', keyHandler);
246    } else {
247        field.bind('keydown', keyHandler);
248    }
249});
250
251/**
252 * Determine the current section level while editing
253 *
254 * @author Andreas Gohr <gohr@cosmocode.de>
255 */
256function currentHeadlineLevel(textboxId){
257    var field     = $(textboxId);
258    var selection = getSelection(field);
259    var search    = "\n"+field.value.substr(0,selection.start);
260    var lasthl    = search.lastIndexOf("\n==");
261    if(lasthl == -1 && field.form.prefix){
262        // we need to look in prefix context
263        search = field.form.prefix.value;
264        lasthl    = search.lastIndexOf("\n==");
265    }
266    search    = search.substr(lasthl+1,6);
267
268    if(search == '======') return 1;
269    if(search.substr(0,5) == '=====') return 2;
270    if(search.substr(0,4) == '====') return 3;
271    if(search.substr(0,3) == '===') return 4;
272    if(search.substr(0,2) == '==') return 5;
273
274    return 0;
275}
276
277
278/**
279 * global var used for not saved yet warning
280 */
281var textChanged = false;
282
283/**
284 * Delete the draft before leaving the page
285 */
286function deleteDraft() {
287    if (is_opera) return;
288    if (window.keepDraft) return;
289
290    // remove a possibly saved draft using ajax
291    var dwform = $('dw__editform');
292    if(dwform){
293        var params = 'call=draftdel';
294        params += '&id='+encodeURIComponent(dwform.elements.id.value);
295
296        var sackobj = new sack(DOKU_BASE + 'lib/exe/ajax.php');
297        // this needs to be synchronous and GET to not be aborted upon page unload
298        sackobj.asynchronous = false;
299        sackobj.method = 'GET';
300        sackobj.AjaxFailedAlert = '';
301        sackobj.encodeURIString = false;
302        sackobj.runAJAX(params);
303    }
304}
305
306/**
307 * Activate "not saved" dialog, add draft deletion to page unload,
308 * add handlers to monitor changes
309 *
310 * Sets focus to the editbox as well
311 */
312addInitEvent(function (){
313    var editform = $('dw__editform');
314    if (!editform) return;
315
316    var edit_text   = $('wiki__text');
317    if(edit_text) {
318        if(edit_text.readOnly) return;
319
320        // set focus
321        edit_text.focus();
322    }
323
324    var checkfunc = function(){
325        textChanged = true; //global var
326        summaryCheck();
327    };
328    addEvent(editform, 'change', checkfunc);
329    addEvent(editform, 'keydown', checkfunc);
330
331    window.onbeforeunload = function(){
332        if(textChanged) {
333            return LANG.notsavedyet;
334        }
335    };
336    window.onunload = deleteDraft;
337
338    // reset change memory var on submit
339    addEvent($('edbtn__save'), 'click', function(){
340        textChanged = false;
341    });
342    addEvent($('edbtn__preview'), 'click', function(){
343        textChanged = false;
344        window.keepDraft = true; // needed to keep draft on page unload
345    });
346
347    var summary = $('edit__summary');
348    addEvent(summary, 'change', summaryCheck);
349    addEvent(summary, 'keyup', summaryCheck);
350    if (textChanged) summaryCheck();
351});
352
353/**
354 * Checks if a summary was entered - if not the style is changed
355 *
356 * @author Andreas Gohr <andi@splitbrain.org>
357 */
358function summaryCheck(){
359    var sum = document.getElementById('edit__summary');
360    if(sum.value === ''){
361        sum.className='missing';
362    }else{
363        sum.className='edit';
364    }
365}
366
367