2 * Autocompletion class
\r
4 * An auto completion box appear while you're writing. It's possible to force it to appear with Ctrl+Space short cut
\r
6 * Loaded as a plugin inside editArea (everything made here could have been made in the plugin directory)
\r
7 * But is definitly linked to syntax selection (no need to do 2 different files for color and auto complete for each syntax language)
\r
8 * and add a too important feature that many people would miss if included as a plugin
\r
10 * - init param: autocompletion_start
\r
11 * - Button name: "autocompletion"
\r
14 var EditArea_autocompletion= {
\r
17 * Get called once this file is loaded (editArea still not initialized)
\r
22 // alert("test init: "+ this._someInternalFunction(2, 3));
\r
24 if(editArea.settings["autocompletion"])
\r
27 this.enabled= false;
\r
28 this.current_word = false;
\r
30 this.selectIndex = -1;
\r
31 this.forceDisplay = false;
\r
32 this.isInMiddleWord = false;
\r
33 this.autoSelectIfOneResult = false;
\r
34 this.delayBeforeDisplay = 100;
\r
35 this.checkDelayTimer = false;
\r
36 this.curr_syntax_str = '';
\r
38 this.file_syntax_datas = {};
\r
41 * Returns the HTML code for a specific control string or false if this plugin doesn't have that control.
\r
42 * A control can be a button, select list or any other HTML item to present in the EditArea user interface.
\r
43 * Language variables such as {$lang_somekey} will also be replaced with contents from
\r
44 * the language packs.
\r
46 * @param {string} ctrl_name: the name of the control to add
\r
47 * @return HTML code for a specific control or false.
\r
48 * @type string or boolean
\r
50 /*,get_control_html: function(ctrl_name){
\r
51 switch( ctrl_name ){
\r
52 case 'autocompletion':
\r
53 // Control id, button img, command
\r
54 return parent.editAreaLoader.get_button_html('autocompletion_but', 'autocompletion.gif', 'toggle_autocompletion', false, this.baseURL);
\r
60 * Get called once EditArea is fully loaded and initialised
\r
64 ,onload: function(){
\r
67 var icon= document.getElementById("autocompletion");
\r
69 editArea.switchClassSticky(icon, 'editAreaButtonSelected', true);
\r
72 this.container = document.createElement('div');
\r
73 this.container.id = "auto_completion_area";
\r
74 editArea.container.insertBefore( this.container, editArea.container.firstChild );
\r
76 // add event detection for hiding suggestion box
\r
77 parent.editAreaLoader.add_event( document, "click", function(){ editArea.plugins['autocompletion']._hide();} );
\r
78 parent.editAreaLoader.add_event( editArea.textarea, "blur", function(){ editArea.plugins['autocompletion']._hide();} );
\r
83 * Is called each time the user touch a keyboard key.
\r
85 * @param (event) e: the keydown event
\r
86 * @return true - pass to next handler in chain, false - stop chain execution
\r
89 ,onkeydown: function(e){
\r
93 if (EA_keys[e.keyCode])
\r
94 letter=EA_keys[e.keyCode];
\r
96 letter=String.fromCharCode(e.keyCode);
\r
98 if( this._isShown() )
\r
100 // if escape, hide the box
\r
107 else if( letter=="Entrer")
\r
109 var as = this.container.getElementsByTagName('A');
\r
110 // select a suggested entry
\r
111 if( this.selectIndex >= 0 && this.selectIndex < as.length )
\r
113 as[ this.selectIndex ].onmousedown();
\r
116 // simply add an enter in the code
\r
123 else if( letter=="Tab" || letter=="Down")
\r
125 this._selectNext();
\r
128 else if( letter=="Up")
\r
130 this._selectBefore();
\r
140 // show current suggestion list and do autoSelect if possible (no matter it's shown or hidden)
\r
141 if( letter=="Space" && CtrlPressed(e) )
\r
143 //parent.console.log('SHOW SUGGEST');
\r
144 this.forceDisplay = true;
\r
145 this.autoSelectIfOneResult = true;
\r
146 this._checkLetter();
\r
150 // wait a short period for check that the cursor isn't moving
\r
151 setTimeout("editArea.plugins['autocompletion']._checkDelayAndCursorBeforeDisplay();", editArea.check_line_selection_timer +5 );
\r
152 this.checkDelayTimer = false;
\r
156 * Executes a specific command, this function handles plugin commands.
\r
158 * @param {string} cmd: the name of the command being executed
\r
159 * @param {unknown} param: the parameter of the command
\r
160 * @return true - pass to next handler in chain, false - stop chain execution
\r
163 ,execCommand: function(cmd, param){
\r
165 case 'toggle_autocompletion':
\r
166 var icon= document.getElementById("autocompletion");
\r
170 editArea.restoreClass(icon);
\r
171 editArea.switchClassSticky(icon, 'editAreaButtonSelected', true);
\r
173 this.enabled= true;
\r
177 this.enabled= false;
\r
179 editArea.switchClassSticky(icon, 'editAreaButtonNormal', false);
\r
185 ,_checkDelayAndCursorBeforeDisplay: function()
\r
187 this.checkDelayTimer = setTimeout("if(editArea.textarea.selectionStart == "+ editArea.textarea.selectionStart +") EditArea_autocompletion._checkLetter();", this.delayBeforeDisplay - editArea.check_line_selection_timer - 5 );
\r
189 // hide the suggested box
\r
190 ,_hide: function(){
\r
191 this.container.style.display="none";
\r
192 this.selectIndex = -1;
\r
193 this.shown = false;
\r
194 this.forceDisplay = false;
\r
195 this.autoSelectIfOneResult = false;
\r
197 // display the suggested box
\r
198 ,_show: function(){
\r
199 if( !this._isShown() )
\r
201 this.container.style.display="block";
\r
202 this.selectIndex = -1;
\r
206 // is the suggested box displayed?
\r
207 ,_isShown: function(){
\r
210 // setter and getter
\r
211 ,_isInMiddleWord: function( new_value ){
\r
212 if( typeof( new_value ) == "undefined" )
\r
213 return this.isInMiddleWord;
\r
215 this.isInMiddleWord = new_value;
\r
217 // select the next element in the suggested box
\r
218 ,_selectNext: function()
\r
220 var as = this.container.getElementsByTagName('A');
\r
222 // clean existing elements
\r
223 for( var i=0; i<as.length; i++ )
\r
225 if( as[i].className )
\r
226 as[i].className = as[i].className.replace(/ focus/g, '');
\r
229 this.selectIndex++;
\r
230 this.selectIndex = ( this.selectIndex >= as.length || this.selectIndex < 0 ) ? 0 : this.selectIndex;
\r
231 as[ this.selectIndex ].className += " focus";
\r
233 // select the previous element in the suggested box
\r
234 ,_selectBefore: function()
\r
236 var as = this.container.getElementsByTagName('A');
\r
238 // clean existing elements
\r
239 for( var i=0; i<as.length; i++ )
\r
241 if( as[i].className )
\r
242 as[i].className = as[ i ].className.replace(/ focus/g, '');
\r
245 this.selectIndex--;
\r
247 this.selectIndex = ( this.selectIndex >= as.length || this.selectIndex < 0 ) ? as.length-1 : this.selectIndex;
\r
248 as[ this.selectIndex ].className += " focus";
\r
250 ,_select: function( content )
\r
252 cursor_forced_position = content.indexOf( '{@}' );
\r
253 content = content.replace(/{@}/g, '' );
\r
254 editArea.getIESelection();
\r
256 // retrive the number of matching characters
\r
257 var start_index = Math.max( 0, editArea.textarea.selectionEnd - content.length );
\r
259 line_string = editArea.textarea.value.substring( start_index, editArea.textarea.selectionEnd + 1);
\r
260 limit = line_string.length -1;
\r
262 for( i =0; i<limit ; i++ )
\r
264 if( line_string.substring( limit - i - 1, limit ) == content.substring( 0, i + 1 ) )
\r
267 // if characters match, we should include them in the selection that will be replaced
\r
269 parent.editAreaLoader.setSelectionRange(editArea.id, editArea.textarea.selectionStart - nbMatch , editArea.textarea.selectionEnd);
\r
271 parent.editAreaLoader.setSelectedText(editArea.id, content );
\r
272 range= parent.editAreaLoader.getSelectionRange(editArea.id);
\r
274 if( cursor_forced_position != -1 )
\r
275 new_pos = range["end"] - ( content.length-cursor_forced_position );
\r
277 new_pos = range["end"];
\r
278 parent.editAreaLoader.setSelectionRange(editArea.id, new_pos, new_pos);
\r
284 * Parse the AUTO_COMPLETION part of syntax definition files
\r
286 ,_parseSyntaxAutoCompletionDatas: function(){
\r
287 //foreach syntax loaded
\r
288 for(var lang in parent.editAreaLoader.load_syntax)
\r
290 if(!parent.editAreaLoader.syntax[lang]['autocompletion']) // init the regexp if not already initialized
\r
292 parent.editAreaLoader.syntax[lang]['autocompletion']= {};
\r
293 // the file has auto completion datas
\r
294 if(parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION'])
\r
297 for(var i in parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION'])
\r
299 datas = parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION'][i];
\r
301 if(datas["CASE_SENSITIVE"]!="undefined" && datas["CASE_SENSITIVE"]==false)
\r
302 tmp["modifiers"]="i";
\r
304 tmp["modifiers"]="";
\r
305 tmp["prefix_separator"]= datas["REGEXP"]["prefix_separator"];
\r
306 tmp["match_prefix_separator"]= new RegExp( datas["REGEXP"]["prefix_separator"] +"$", tmp["modifiers"]);
\r
307 tmp["match_word"]= new RegExp("(?:"+ datas["REGEXP"]["before_word"] +")("+ datas["REGEXP"]["possible_words_letters"] +")$", tmp["modifiers"]);
\r
308 tmp["match_next_letter"]= new RegExp("^("+ datas["REGEXP"]["letter_after_word_must_match"] +")$", tmp["modifiers"]);
\r
309 tmp["keywords"]= {};
\r
310 //console.log( datas["KEYWORDS"] );
\r
311 for( var prefix in datas["KEYWORDS"] )
\r
313 tmp["keywords"][prefix]= {
\r
315 prefix_name: prefix,
\r
316 prefix_reg: new RegExp("(?:"+ parent.editAreaLoader.get_escaped_regexp( prefix ) +")(?:"+ tmp["prefix_separator"] +")$", tmp["modifiers"] ),
\r
319 for( var j=0; j<datas["KEYWORDS"][prefix].length; j++ )
\r
321 tmp["keywords"][prefix]['datas'][j]= {
\r
322 is_typing: datas["KEYWORDS"][prefix][j][0],
\r
323 // if replace with is empty, replace with the is_typing value
\r
324 replace_with: datas["KEYWORDS"][prefix][j][1] ? datas["KEYWORDS"][prefix][j][1].replace('§', datas["KEYWORDS"][prefix][j][0] ) : '',
\r
325 comment: datas["KEYWORDS"][prefix][j][2] ? datas["KEYWORDS"][prefix][j][2] : ''
\r
328 // the replace with shouldn't be empty
\r
329 if( tmp["keywords"][prefix]['datas'][j]['replace_with'].length == 0 )
\r
330 tmp["keywords"][prefix]['datas'][j]['replace_with'] = tmp["keywords"][prefix]['datas'][j]['is_typing'];
\r
332 // if the comment is empty, display the replace_with value
\r
333 if( tmp["keywords"][prefix]['datas'][j]['comment'].length == 0 )
\r
334 tmp["keywords"][prefix]['datas'][j]['comment'] = tmp["keywords"][prefix]['datas'][j]['replace_with'].replace(/{@}/g, '' );
\r
338 tmp["max_text_length"]= datas["MAX_TEXT_LENGTH"];
\r
339 parent.editAreaLoader.syntax[lang]['autocompletion'][i] = tmp;
\r
346 ,_checkLetter: function(){
\r
347 // check that syntax hasn't changed
\r
348 if( this.curr_syntax_str != editArea.settings['syntax'] )
\r
350 if( !parent.editAreaLoader.syntax[editArea.settings['syntax']]['autocompletion'] )
\r
351 this._parseSyntaxAutoCompletionDatas();
\r
352 this.curr_syntax= parent.editAreaLoader.syntax[editArea.settings['syntax']]['autocompletion'];
\r
353 this.curr_syntax_str = editArea.settings['syntax'];
\r
354 //console.log( this.curr_syntax );
\r
357 if( editArea.is_editable )
\r
360 t1= time.getTime();
\r
361 editArea.getIESelection();
\r
362 this.selectIndex = -1;
\r
363 start=editArea.textarea.selectionStart;
\r
364 var str = editArea.textarea.value;
\r
368 for(var i in this.curr_syntax)
\r
370 var last_chars = str.substring(Math.max(0, start-this.curr_syntax[i]["max_text_length"]), start);
\r
371 var matchNextletter = str.substring(start, start+1).match( this.curr_syntax[i]["match_next_letter"]);
\r
372 // if not writting in the middle of a word or if forcing display
\r
373 if( matchNextletter || this.forceDisplay )
\r
375 // check if the last chars match a separator
\r
376 var match_prefix_separator = last_chars.match(this.curr_syntax[i]["match_prefix_separator"]);
\r
378 // check if it match a possible word
\r
379 var match_word= last_chars.match(this.curr_syntax[i]["match_word"]);
\r
381 //console.log( match_word );
\r
384 var begin_word= match_word[1];
\r
385 var match_curr_word= new RegExp("^"+ parent.editAreaLoader.get_escaped_regexp( begin_word ), this.curr_syntax[i]["modifiers"]);
\r
386 //console.log( match_curr_word );
\r
387 for(var prefix in this.curr_syntax[i]["keywords"])
\r
389 // parent.console.log( this.curr_syntax[i]["keywords"][prefix] );
\r
390 for(var j=0; j<this.curr_syntax[i]["keywords"][prefix]['datas'].length; j++)
\r
392 // parent.console.log( this.curr_syntax[i]["keywords"][prefix]['datas'][j]['is_typing'] );
\r
393 // the key word match or force display
\r
394 if( this.curr_syntax[i]["keywords"][prefix]['datas'][j]['is_typing'].match(match_curr_word) )
\r
396 // parent.console.log('match');
\r
398 var before = last_chars.substr( 0, last_chars.length - begin_word.length );
\r
400 // no prefix to match => it's valid
\r
401 if( !match_prefix_separator && this.curr_syntax[i]["keywords"][prefix]['prefix'].length == 0 )
\r
403 if( ! before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) )
\r
406 // we still need to check the prefix if there is one
\r
407 else if( this.curr_syntax[i]["keywords"][prefix]['prefix'].length > 0 )
\r
409 if( before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) )
\r
414 results[results.length]= [ this.curr_syntax[i]["keywords"][prefix], this.curr_syntax[i]["keywords"][prefix]['datas'][j] ];
\r
419 // it doesn't match any possible word but we want to display something
\r
420 // we'll display to list of all available words
\r
421 else if( this.forceDisplay || match_prefix_separator )
\r
423 for(var prefix in this.curr_syntax[i]["keywords"])
\r
425 for(var j=0; j<this.curr_syntax[i]["keywords"][prefix]['datas'].length; j++)
\r
428 // no prefix to match => it's valid
\r
429 if( !match_prefix_separator && this.curr_syntax[i]["keywords"][prefix]['prefix'].length == 0 )
\r
433 // we still need to check the prefix if there is one
\r
434 else if( match_prefix_separator && this.curr_syntax[i]["keywords"][prefix]['prefix'].length > 0 )
\r
436 var before = last_chars; //.substr( 0, last_chars.length );
\r
437 if( before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) )
\r
442 results[results.length]= [ this.curr_syntax[i]["keywords"][prefix], this.curr_syntax[i]["keywords"][prefix]['datas'][j] ];
\r
449 // there is only one result, and we can select it automatically
\r
450 if( results.length == 1 && this.autoSelectIfOneResult )
\r
452 // console.log( results );
\r
453 this._select( results[0][1]['replace_with'] );
\r
455 else if( results.length == 0 )
\r
461 // build the suggestion box content
\r
463 for(var i=0; i<results.length; i++)
\r
465 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'];
\r
466 if(results[i][0]['prefix_name'].length>0)
\r
467 line+='<span class="prefix">'+ results[i][0]['prefix_name'] +'</span>';
\r
469 lines[lines.length]=line;
\r
472 this.container.innerHTML = '<ul>'+ lines.sort().join('') +'</ul>';
\r
474 var cursor = _$("cursor_pos");
\r
475 this.container.style.top = ( cursor.cursor_top + editArea.lineHeight ) +"px";
\r
476 this.container.style.left = ( cursor.cursor_left + 8 ) +"px";
\r
480 this.autoSelectIfOneResult = false;
\r
482 t2= time.getTime();
\r
484 //parent.console.log( begin_word +"\n"+ (t2-t1) +"\n"+ html );
\r
489 // Load as a plugin
\r
490 editArea.settings['plugins'][ editArea.settings['plugins'].length ] = 'autocompletion';
\r
491 editArea.add_plugin('autocompletion', EditArea_autocompletion);