Subversion Repositories wimsdev

Rev

Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  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"), "&quot;") +"');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);