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 <[email protected]>
|
---|
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 <[email protected]>
|
---|
14 | */
|
---|
15 | function 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 <[email protected]>
|
---|
60 | */
|
---|
61 | function 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 <[email protected]>
|
---|
118 | */
|
---|
119 | function 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 <[email protected]>
|
---|
132 | */
|
---|
133 | function 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 <[email protected]>
|
---|
150 | * @fixme handle tabs
|
---|
151 | */
|
---|
152 | function 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
|
---|
222 | addInitEvent(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 <[email protected]>
|
---|
238 | */
|
---|
239 | function 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 | */
|
---|
264 | window.textChanged = false;
|
---|
265 |
|
---|
266 | /**
|
---|
267 | * Delete the draft before leaving the page
|
---|
268 | */
|
---|
269 | function 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 | */
|
---|
295 | addInitEvent(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 and place cursor at the start
|
---|
304 | var sel = getSelection(edit_text);
|
---|
305 | sel.start = 0;
|
---|
306 | sel.end = 0;
|
---|
307 | setSelection(sel);
|
---|
308 | edit_text.focus();
|
---|
309 | }
|
---|
310 |
|
---|
311 | var checkfunc = function(){
|
---|
312 | window.textChanged = true; //global var
|
---|
313 | summaryCheck();
|
---|
314 | };
|
---|
315 | addEvent(editform, 'change', checkfunc);
|
---|
316 | addEvent(editform, 'keydown', checkfunc);
|
---|
317 |
|
---|
318 | window.onbeforeunload = function(){
|
---|
319 | if(window.textChanged) {
|
---|
320 | return LANG.notsavedyet;
|
---|
321 | }
|
---|
322 | };
|
---|
323 | window.onunload = deleteDraft;
|
---|
324 |
|
---|
325 | // reset change memory var on submit
|
---|
326 | addEvent($('edbtn__save'), 'click', function(){
|
---|
327 | window.onbeforeunload = '';
|
---|
328 | window.textChanged = false;
|
---|
329 | });
|
---|
330 | addEvent($('edbtn__preview'), 'click', function(){
|
---|
331 | window.onbeforeunload = '';
|
---|
332 | window.textChanged = false;
|
---|
333 | window.keepDraft = true; // needed to keep draft on page unload
|
---|
334 | });
|
---|
335 |
|
---|
336 | var summary = $('edit__summary');
|
---|
337 | addEvent(summary, 'change', summaryCheck);
|
---|
338 | addEvent(summary, 'keyup', summaryCheck);
|
---|
339 | if (window.textChanged) summaryCheck();
|
---|
340 | });
|
---|
341 |
|
---|
342 | /**
|
---|
343 | * Checks if a summary was entered - if not the style is changed
|
---|
344 | *
|
---|
345 | * @author Andreas Gohr <[email protected]>
|
---|
346 | */
|
---|
347 | function summaryCheck(){
|
---|
348 | var sum = document.getElementById('edit__summary');
|
---|
349 | if(sum.value === ''){
|
---|
350 | sum.className='missing';
|
---|
351 | }else{
|
---|
352 | sum.className='edit';
|
---|
353 | }
|
---|
354 | }
|
---|
355 |
|
---|