Go to most recent revision | Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
4870 | bpr | 1 | /** |
2 | * Autocompletion class |
||
3 | * |
||
4 | * An auto completion box appear while you're writing. It's possible to force it to appear with Ctrl+Space short cut |
||
5 | * |
||
6 | * Loaded as a plugin inside editArea (everything made here could have been made in the plugin directory) |
||
7 | * But is definitly linked to syntax selection (no need to do 2 different files for color and auto complete for each syntax language) |
||
8 | * and add a too important feature that many people would miss if included as a plugin |
||
9 | * |
||
10 | * - init param: autocompletion_start |
||
11 | * - Button name: "autocompletion" |
||
12 | */ |
||
13 | |||
14 | var EditArea_autocompletion= { |
||
15 | |||
16 | /** |
||
17 | * Get called once this file is loaded (editArea still not initialized) |
||
18 | * |
||
19 | * @return nothing |
||
20 | */ |
||
21 | init: function(){ |
||
22 | // alert("test init: "+ this._someInternalFunction(2, 3)); |
||
23 | |||
24 | if(editArea.settings["autocompletion"]) |
||
25 | this.enabled= true; |
||
26 | else |
||
27 | this.enabled= false; |
||
28 | this.current_word = false; |
||
29 | this.shown = false; |
||
30 | this.selectIndex = -1; |
||
31 | this.forceDisplay = false; |
||
32 | this.isInMiddleWord = false; |
||
33 | this.autoSelectIfOneResult = false; |
||
34 | this.delayBeforeDisplay = 100; |
||
35 | this.checkDelayTimer = false; |
||
36 | this.curr_syntax_str = ''; |
||
37 | |||
38 | this.file_syntax_datas = {}; |
||
39 | } |
||
40 | /** |
||
41 | * Returns the HTML code for a specific control string or false if this plugin doesn't have that control. |
||
42 | * A control can be a button, select list or any other HTML item to present in the EditArea user interface. |
||
43 | * Language variables such as {$lang_somekey} will also be replaced with contents from |
||
44 | * the language packs. |
||
45 | * |
||
46 | * @param {string} ctrl_name: the name of the control to add |
||
47 | * @return HTML code for a specific control or false. |
||
48 | * @type string or boolean |
||
49 | */ |
||
50 | /*,get_control_html: function(ctrl_name){ |
||
51 | switch( ctrl_name ){ |
||
52 | case 'autocompletion': |
||
53 | // Control id, button img, command |
||
54 | return parent.editAreaLoader.get_button_html('autocompletion_but', 'autocompletion.gif', 'toggle_autocompletion', false, this.baseURL); |
||
55 | break; |
||
56 | } |
||
57 | return false; |
||
58 | }*/ |
||
59 | /** |
||
60 | * Get called once EditArea is fully loaded and initialised |
||
61 | * |
||
62 | * @return nothing |
||
63 | */ |
||
64 | ,onload: function(){ |
||
65 | if(this.enabled) |
||
66 | { |
||
67 | var icon= document.getElementById("autocompletion"); |
||
68 | if(icon) |
||
69 | editArea.switchClassSticky(icon, 'editAreaButtonSelected', true); |
||
70 | } |
||
71 | |||
72 | this.container = document.createElement('div'); |
||
73 | this.container.id = "auto_completion_area"; |
||
74 | editArea.container.insertBefore( this.container, editArea.container.firstChild ); |
||
75 | |||
76 | // add event detection for hiding suggestion box |
||
77 | parent.editAreaLoader.add_event( document, "click", function(){ editArea.plugins['autocompletion']._hide();} ); |
||
78 | parent.editAreaLoader.add_event( editArea.textarea, "blur", function(){ editArea.plugins['autocompletion']._hide();} ); |
||
79 | |||
80 | } |
||
81 | |||
82 | /** |
||
83 | * Is called each time the user touch a keyboard key. |
||
84 | * |
||
85 | * @param (event) e: the keydown event |
||
86 | * @return true - pass to next handler in chain, false - stop chain execution |
||
87 | * @type boolean |
||
88 | */ |
||
89 | ,onkeydown: function(e){ |
||
90 | if(!this.enabled) |
||
91 | return true; |
||
92 | |||
93 | if (EA_keys[e.keyCode]) |
||
94 | letter=EA_keys[e.keyCode]; |
||
95 | else |
||
96 | letter=String.fromCharCode(e.keyCode); |
||
97 | // shown |
||
98 | if( this._isShown() ) |
||
99 | { |
||
100 | // if escape, hide the box |
||
101 | if(letter=="Esc") |
||
102 | { |
||
103 | this._hide(); |
||
104 | return false; |
||
105 | } |
||
106 | // Enter |
||
107 | else if( letter=="Entrer") |
||
108 | { |
||
109 | var as = this.container.getElementsByTagName('A'); |
||
110 | // select a suggested entry |
||
111 | if( this.selectIndex >= 0 && this.selectIndex < as.length ) |
||
112 | { |
||
113 | as[ this.selectIndex ].onmousedown(); |
||
114 | return false |
||
115 | } |
||
116 | // simply add an enter in the code |
||
117 | else |
||
118 | { |
||
119 | this._hide(); |
||
120 | return true; |
||
121 | } |
||
122 | } |
||
123 | else if( letter=="Tab" || letter=="Down") |
||
124 | { |
||
125 | this._selectNext(); |
||
126 | return false; |
||
127 | } |
||
128 | else if( letter=="Up") |
||
129 | { |
||
130 | this._selectBefore(); |
||
131 | return false; |
||
132 | } |
||
133 | } |
||
134 | // hidden |
||
135 | else |
||
136 | { |
||
137 | |||
138 | } |
||
139 | |||
140 | // show current suggestion list and do autoSelect if possible (no matter it's shown or hidden) |
||
141 | if( letter=="Space" && CtrlPressed(e) ) |
||
142 | { |
||
143 | //parent.console.log('SHOW SUGGEST'); |
||
144 | this.forceDisplay = true; |
||
145 | this.autoSelectIfOneResult = true; |
||
146 | this._checkLetter(); |
||
147 | return false; |
||
148 | } |
||
149 | |||
150 | // wait a short period for check that the cursor isn't moving |
||
151 | setTimeout("editArea.plugins['autocompletion']._checkDelayAndCursorBeforeDisplay();", editArea.check_line_selection_timer +5 ); |
||
152 | this.checkDelayTimer = false; |
||
153 | return true; |
||
154 | } |
||
155 | /** |
||
156 | * Executes a specific command, this function handles plugin commands. |
||
157 | * |
||
158 | * @param {string} cmd: the name of the command being executed |
||
159 | * @param {unknown} param: the parameter of the command |
||
160 | * @return true - pass to next handler in chain, false - stop chain execution |
||
161 | * @type boolean |
||
162 | */ |
||
163 | ,execCommand: function(cmd, param){ |
||
164 | switch( cmd ){ |
||
165 | case 'toggle_autocompletion': |
||
166 | var icon= document.getElementById("autocompletion"); |
||
167 | if(!this.enabled) |
||
168 | { |
||
169 | if(icon != null){ |
||
170 | editArea.restoreClass(icon); |
||
171 | editArea.switchClassSticky(icon, 'editAreaButtonSelected', true); |
||
172 | } |
||
173 | this.enabled= true; |
||
174 | } |
||
175 | else |
||
176 | { |
||
177 | this.enabled= false; |
||
178 | if(icon != null) |
||
179 | editArea.switchClassSticky(icon, 'editAreaButtonNormal', false); |
||
180 | } |
||
181 | return true; |
||
182 | } |
||
183 | return true; |
||
184 | } |
||
185 | ,_checkDelayAndCursorBeforeDisplay: function() |
||
186 | { |
||
187 | this.checkDelayTimer = setTimeout("if(editArea.textarea.selectionStart == "+ editArea.textarea.selectionStart +") EditArea_autocompletion._checkLetter();", this.delayBeforeDisplay - editArea.check_line_selection_timer - 5 ); |
||
188 | } |
||
189 | // hide the suggested box |
||
190 | ,_hide: function(){ |
||
191 | this.container.style.display="none"; |
||
192 | this.selectIndex = -1; |
||
193 | this.shown = false; |
||
194 | this.forceDisplay = false; |
||
195 | this.autoSelectIfOneResult = false; |
||
196 | } |
||
197 | // display the suggested box |
||
198 | ,_show: function(){ |
||
199 | if( !this._isShown() ) |
||
200 | { |
||
201 | this.container.style.display="block"; |
||
202 | this.selectIndex = -1; |
||
203 | this.shown = true; |
||
204 | } |
||
205 | } |
||
206 | // is the suggested box displayed? |
||
207 | ,_isShown: function(){ |
||
208 | return this.shown; |
||
209 | } |
||
210 | // setter and getter |
||
211 | ,_isInMiddleWord: function( new_value ){ |
||
212 | if( typeof( new_value ) == "undefined" ) |
||
213 | return this.isInMiddleWord; |
||
214 | else |
||
215 | this.isInMiddleWord = new_value; |
||
216 | } |
||
217 | // select the next element in the suggested box |
||
218 | ,_selectNext: function() |
||
219 | { |
||
220 | var as = this.container.getElementsByTagName('A'); |
||
221 | |||
222 | // clean existing elements |
||
223 | for( var i=0; i<as.length; i++ ) |
||
224 | { |
||
225 | if( as[i].className ) |
||
226 | as[i].className = as[i].className.replace(/ focus/g, ''); |
||
227 | } |
||
228 | |||
229 | this.selectIndex++; |
||
230 | this.selectIndex = ( this.selectIndex >= as.length || this.selectIndex < 0 ) ? 0 : this.selectIndex; |
||
231 | as[ this.selectIndex ].className += " focus"; |
||
232 | } |
||
233 | // select the previous element in the suggested box |
||
234 | ,_selectBefore: function() |
||
235 | { |
||
236 | var as = this.container.getElementsByTagName('A'); |
||
237 | |||
238 | // clean existing elements |
||
239 | for( var i=0; i<as.length; i++ ) |
||
240 | { |
||
241 | if( as[i].className ) |
||
242 | as[i].className = as[ i ].className.replace(/ focus/g, ''); |
||
243 | } |
||
244 | |||
245 | this.selectIndex--; |
||
246 | |||
247 | this.selectIndex = ( this.selectIndex >= as.length || this.selectIndex < 0 ) ? as.length-1 : this.selectIndex; |
||
248 | as[ this.selectIndex ].className += " focus"; |
||
249 | } |
||
250 | ,_select: function( content ) |
||
251 | { |
||
252 | cursor_forced_position = content.indexOf( '{@}' ); |
||
253 | content = content.replace(/{@}/g, '' ); |
||
254 | if(editArea.isIE) |
||
255 | editArea.getIESelection(); |
||
256 | |||
257 | // retrive the number of matching characters |
||
258 | var start_index = Math.max( 0, editArea.textarea.selectionEnd - content.length ); |
||
259 | |||
260 | line_string = editArea.textarea.value.substring( start_index, editArea.textarea.selectionEnd + 1); |
||
261 | limit = line_string.length -1; |
||
262 | nbMatch = 0; |
||
263 | for( i =0; i<limit ; i++ ) |
||
264 | { |
||
265 | if( line_string.substring( limit - i - 1, limit ) == content.substring( 0, i + 1 ) ) |
||
266 | nbMatch = i + 1; |
||
267 | } |
||
268 | // if characters match, we should include them in the selection that will be replaced |
||
269 | if( nbMatch > 0 ) |
||
270 | parent.editAreaLoader.setSelectionRange(editArea.id, editArea.textarea.selectionStart - nbMatch , editArea.textarea.selectionEnd); |
||
271 | |||
272 | parent.editAreaLoader.setSelectedText(editArea.id, content ); |
||
273 | range= parent.editAreaLoader.getSelectionRange(editArea.id); |
||
274 | |||
275 | if( cursor_forced_position != -1 ) |
||
276 | new_pos = range["end"] - ( content.length-cursor_forced_position ); |
||
277 | else |
||
278 | new_pos = range["end"]; |
||
279 | parent.editAreaLoader.setSelectionRange(editArea.id, new_pos, new_pos); |
||
280 | this._hide(); |
||
281 | } |
||
282 | |||
283 | |||
284 | /** |
||
285 | * Parse the AUTO_COMPLETION part of syntax definition files |
||
286 | */ |
||
287 | ,_parseSyntaxAutoCompletionDatas: function(){ |
||
288 | //foreach syntax loaded |
||
289 | for(var lang in parent.editAreaLoader.load_syntax) |
||
290 | { |
||
291 | if(!parent.editAreaLoader.syntax[lang]['autocompletion']) // init the regexp if not already initialized |
||
292 | { |
||
293 | parent.editAreaLoader.syntax[lang]['autocompletion']= {}; |
||
294 | // the file has auto completion datas |
||
295 | if(parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION']) |
||
296 | { |
||
297 | // parse them |
||
298 | for(var i in parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION']) |
||
299 | { |
||
300 | datas = parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION'][i]; |
||
301 | tmp = {}; |
||
302 | if(datas["CASE_SENSITIVE"]!="undefined" && datas["CASE_SENSITIVE"]==false) |
||
303 | tmp["modifiers"]="i"; |
||
304 | else |
||
305 | tmp["modifiers"]=""; |
||
306 | tmp["prefix_separator"]= datas["REGEXP"]["prefix_separator"]; |
||
307 | tmp["match_prefix_separator"]= new RegExp( datas["REGEXP"]["prefix_separator"] +"$", tmp["modifiers"]); |
||
308 | tmp["match_word"]= new RegExp("(?:"+ datas["REGEXP"]["before_word"] +")("+ datas["REGEXP"]["possible_words_letters"] +")$", tmp["modifiers"]); |
||
309 | tmp["match_next_letter"]= new RegExp("^("+ datas["REGEXP"]["letter_after_word_must_match"] +")$", tmp["modifiers"]); |
||
310 | tmp["keywords"]= {}; |
||
311 | //console.log( datas["KEYWORDS"] ); |
||
312 | for( var prefix in datas["KEYWORDS"] ) |
||
313 | { |
||
314 | tmp["keywords"][prefix]= { |
||
315 | prefix: prefix, |
||
316 | prefix_name: prefix, |
||
317 | prefix_reg: new RegExp("(?:"+ parent.editAreaLoader.get_escaped_regexp( prefix ) +")(?:"+ tmp["prefix_separator"] +")$", tmp["modifiers"] ), |
||
318 | datas: [] |
||
319 | }; |
||
320 | for( var j=0; j<datas["KEYWORDS"][prefix].length; j++ ) |
||
321 | { |
||
322 | tmp["keywords"][prefix]['datas'][j]= { |
||
323 | is_typing: datas["KEYWORDS"][prefix][j][0], |
||
324 | // if replace with is empty, replace with the is_typing value |
||
325 | replace_with: datas["KEYWORDS"][prefix][j][1] ? datas["KEYWORDS"][prefix][j][1].replace('§', datas["KEYWORDS"][prefix][j][0] ) : '', |
||
326 | comment: datas["KEYWORDS"][prefix][j][2] ? datas["KEYWORDS"][prefix][j][2] : '' |
||
327 | }; |
||
328 | |||
329 | // the replace with shouldn't be empty |
||
330 | if( tmp["keywords"][prefix]['datas'][j]['replace_with'].length == 0 ) |
||
331 | tmp["keywords"][prefix]['datas'][j]['replace_with'] = tmp["keywords"][prefix]['datas'][j]['is_typing']; |
||
332 | |||
333 | // if the comment is empty, display the replace_with value |
||
334 | if( tmp["keywords"][prefix]['datas'][j]['comment'].length == 0 ) |
||
335 | tmp["keywords"][prefix]['datas'][j]['comment'] = tmp["keywords"][prefix]['datas'][j]['replace_with'].replace(/{@}/g, '' ); |
||
336 | } |
||
337 | |||
338 | } |
||
339 | tmp["max_text_length"]= datas["MAX_TEXT_LENGTH"]; |
||
340 | parent.editAreaLoader.syntax[lang]['autocompletion'][i] = tmp; |
||
341 | } |
||
342 | } |
||
343 | } |
||
344 | } |
||
345 | } |
||
346 | |||
347 | ,_checkLetter: function(){ |
||
348 | // check that syntax hasn't changed |
||
349 | if( this.curr_syntax_str != editArea.settings['syntax'] ) |
||
350 | { |
||
351 | if( !parent.editAreaLoader.syntax[editArea.settings['syntax']]['autocompletion'] ) |
||
352 | this._parseSyntaxAutoCompletionDatas(); |
||
353 | this.curr_syntax= parent.editAreaLoader.syntax[editArea.settings['syntax']]['autocompletion']; |
||
354 | this.curr_syntax_str = editArea.settings['syntax']; |
||
355 | //console.log( this.curr_syntax ); |
||
356 | } |
||
357 | |||
358 | if( editArea.is_editable ) |
||
359 | { |
||
360 | time=new Date; |
||
361 | t1= time.getTime(); |
||
362 | if(editArea.isIE) |
||
363 | editArea.getIESelection(); |
||
364 | this.selectIndex = -1; |
||
365 | start=editArea.textarea.selectionStart; |
||
366 | var str = editArea.textarea.value; |
||
367 | var results= []; |
||
368 | |||
369 | |||
370 | for(var i in this.curr_syntax) |
||
371 | { |
||
372 | var last_chars = str.substring(Math.max(0, start-this.curr_syntax[i]["max_text_length"]), start); |
||
373 | var matchNextletter = str.substring(start, start+1).match( this.curr_syntax[i]["match_next_letter"]); |
||
374 | // if not writting in the middle of a word or if forcing display |
||
375 | if( matchNextletter || this.forceDisplay ) |
||
376 | { |
||
377 | // check if the last chars match a separator |
||
378 | var match_prefix_separator = last_chars.match(this.curr_syntax[i]["match_prefix_separator"]); |
||
379 | |||
380 | // check if it match a possible word |
||
381 | var match_word= last_chars.match(this.curr_syntax[i]["match_word"]); |
||
382 | |||
383 | //console.log( match_word ); |
||
384 | if( match_word ) |
||
385 | { |
||
386 | var begin_word= match_word[1]; |
||
387 | var match_curr_word= new RegExp("^"+ parent.editAreaLoader.get_escaped_regexp( begin_word ), this.curr_syntax[i]["modifiers"]); |
||
388 | //console.log( match_curr_word ); |
||
389 | for(var prefix in this.curr_syntax[i]["keywords"]) |
||
390 | { |
||
391 | // parent.console.log( this.curr_syntax[i]["keywords"][prefix] ); |
||
392 | for(var j=0; j<this.curr_syntax[i]["keywords"][prefix]['datas'].length; j++) |
||
393 | { |
||
394 | // parent.console.log( this.curr_syntax[i]["keywords"][prefix]['datas'][j]['is_typing'] ); |
||
395 | // the key word match or force display |
||
396 | if( this.curr_syntax[i]["keywords"][prefix]['datas'][j]['is_typing'].match(match_curr_word) ) |
||
397 | { |
||
398 | // parent.console.log('match'); |
||
399 | hasMatch = false; |
||
400 | var before = last_chars.substr( 0, last_chars.length - begin_word.length ); |
||
401 | |||
402 | // no prefix to match => it's valid |
||
403 | if( !match_prefix_separator && this.curr_syntax[i]["keywords"][prefix]['prefix'].length == 0 ) |
||
404 | { |
||
405 | if( ! before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) ) |
||
406 | hasMatch = true; |
||
407 | } |
||
408 | // we still need to check the prefix if there is one |
||
409 | else if( this.curr_syntax[i]["keywords"][prefix]['prefix'].length > 0 ) |
||
410 | { |
||
411 | if( before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) ) |
||
412 | hasMatch = true; |
||
413 | } |
||
414 | |||
415 | if( hasMatch ) |
||
416 | results[results.length]= [ this.curr_syntax[i]["keywords"][prefix], this.curr_syntax[i]["keywords"][prefix]['datas'][j] ]; |
||
417 | } |
||
418 | } |
||
419 | } |
||
420 | } |
||
421 | // it doesn't match any possible word but we want to display something |
||
422 | // we'll display to list of all available words |
||
423 | else if( this.forceDisplay || match_prefix_separator ) |
||
424 | { |
||
425 | for(var prefix in this.curr_syntax[i]["keywords"]) |
||
426 | { |
||
427 | for(var j=0; j<this.curr_syntax[i]["keywords"][prefix]['datas'].length; j++) |
||
428 | { |
||
429 | hasMatch = false; |
||
430 | // no prefix to match => it's valid |
||
431 | if( !match_prefix_separator && this.curr_syntax[i]["keywords"][prefix]['prefix'].length == 0 ) |
||
432 | { |
||
433 | hasMatch = true; |
||
434 | } |
||
435 | // we still need to check the prefix if there is one |
||
436 | else if( match_prefix_separator && this.curr_syntax[i]["keywords"][prefix]['prefix'].length > 0 ) |
||
437 | { |
||
438 | var before = last_chars; //.substr( 0, last_chars.length ); |
||
439 | if( before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) ) |
||
440 | hasMatch = true; |
||
441 | } |
||
442 | |||
443 | if( hasMatch ) |
||
444 | results[results.length]= [ this.curr_syntax[i]["keywords"][prefix], this.curr_syntax[i]["keywords"][prefix]['datas'][j] ]; |
||
445 | } |
||
446 | } |
||
447 | } |
||
448 | } |
||
449 | } |
||
450 | |||
451 | // there is only one result, and we can select it automatically |
||
452 | if( results.length == 1 && this.autoSelectIfOneResult ) |
||
453 | { |
||
454 | // console.log( results ); |
||
455 | this._select( results[0][1]['replace_with'] ); |
||
456 | } |
||
457 | else if( results.length == 0 ) |
||
458 | { |
||
459 | this._hide(); |
||
460 | } |
||
461 | else |
||
462 | { |
||
463 | // build the suggestion box content |
||
464 | var lines=[]; |
||
465 | for(var i=0; i<results.length; i++) |
||
466 | { |
||
467 | var line= "<li><a href=\"#\" class=\"entry\" onmousedown=\"EditArea_autocompletion._select('"+ results[i][1]['replace_with'].replace(new RegExp('"', "g"), """) +"');return false;\">"+ results[i][1]['comment']; |
||
468 | if(results[i][0]['prefix_name'].length>0) |
||
469 | line+='<span class="prefix">'+ results[i][0]['prefix_name'] +'</span>'; |
||
470 | line+='</a></li>'; |
||
471 | lines[lines.length]=line; |
||
472 | } |
||
473 | // sort results |
||
474 | this.container.innerHTML = '<ul>'+ lines.sort().join('') +'</ul>'; |
||
475 | |||
476 | var cursor = _$("cursor_pos"); |
||
477 | this.container.style.top = ( cursor.cursor_top + editArea.lineHeight ) +"px"; |
||
478 | this.container.style.left = ( cursor.cursor_left + 8 ) +"px"; |
||
479 | this._show(); |
||
480 | } |
||
481 | |||
482 | this.autoSelectIfOneResult = false; |
||
483 | time=new Date; |
||
484 | t2= time.getTime(); |
||
485 | |||
486 | //parent.console.log( begin_word +"\n"+ (t2-t1) +"\n"+ html ); |
||
487 | } |
||
488 | } |
||
489 | }; |
||
490 | |||
491 | // Load as a plugin |
||
492 | editArea.settings['plugins'][ editArea.settings['plugins'].length ] = 'autocompletion'; |
||
493 | editArea.add_plugin('autocompletion', EditArea_autocompletion); |