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);