Podświetlanie składni w edytorze XML dzięki editArea. Closes #17. Niestety dla tekstó...
[redakcja.git] / project / static / js / autocompletion.js
1 /**\r
2  * Autocompletion class\r
3  * \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
5  * \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
9  * \r
10  * - init param: autocompletion_start\r
11  * - Button name: "autocompletion"\r
12  */  \r
13 \r
14 var EditArea_autocompletion= {\r
15         \r
16         /**\r
17          * Get called once this file is loaded (editArea still not initialized)\r
18          *\r
19          * @return nothing       \r
20          */                     \r
21         init: function(){       \r
22                 //      alert("test init: "+ this._someInternalFunction(2, 3));\r
23                 \r
24                 if(editArea.settings["autocompletion"])\r
25                         this.enabled= true;\r
26                 else\r
27                         this.enabled= false;\r
28                 this.current_word               = false;\r
29                 this.shown                              = 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
37                 \r
38                 this.file_syntax_datas  = {};\r
39         }\r
40         /**\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
45          * \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
49          */     \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
55                                 break;\r
56                 }\r
57                 return false;\r
58         }*/\r
59         /**\r
60          * Get called once EditArea is fully loaded and initialised\r
61          *       \r
62          * @return nothing\r
63          */                     \r
64         ,onload: function(){ \r
65                 if(this.enabled)\r
66                 {\r
67                         var icon= document.getElementById("autocompletion");\r
68                         if(icon)\r
69                                 editArea.switchClassSticky(icon, 'editAreaButtonSelected', true);\r
70                 }\r
71                 \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
75                 \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
79                 \r
80         }\r
81         \r
82         /**\r
83          * Is called each time the user touch a keyboard key.\r
84          *       \r
85          * @param (event) e: the keydown event\r
86          * @return true - pass to next handler in chain, false - stop chain execution\r
87          * @type boolean         \r
88          */\r
89         ,onkeydown: function(e){\r
90                 if(!this.enabled)\r
91                         return true;\r
92                         \r
93                 if (EA_keys[e.keyCode])\r
94                         letter=EA_keys[e.keyCode];\r
95                 else\r
96                         letter=String.fromCharCode(e.keyCode);  \r
97                 // shown\r
98                 if( this._isShown() )\r
99                 {       \r
100                         // if escape, hide the box\r
101                         if(letter=="Esc")\r
102                         {\r
103                                 this._hide();\r
104                                 return false;\r
105                         }\r
106                         // Enter\r
107                         else if( letter=="Entrer")\r
108                         {\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
112                                 {\r
113                                         as[ this.selectIndex ].onmousedown();\r
114                                         return false\r
115                                 }\r
116                                 // simply add an enter in the code\r
117                                 else\r
118                                 {\r
119                                         this._hide();\r
120                                         return true;\r
121                                 }\r
122                         }\r
123                         else if( letter=="Tab" || letter=="Down")\r
124                         {\r
125                                 this._selectNext();\r
126                                 return false;\r
127                         }\r
128                         else if( letter=="Up")\r
129                         {\r
130                                 this._selectBefore();\r
131                                 return false;\r
132                         }\r
133                 }\r
134                 // hidden\r
135                 else\r
136                 {\r
137                         \r
138                 }\r
139                 \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
142                 {\r
143                         //parent.console.log('SHOW SUGGEST');\r
144                         this.forceDisplay                       = true;\r
145                         this.autoSelectIfOneResult      = true;\r
146                         this._checkLetter();\r
147                         return false;\r
148                 }\r
149                 \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
153                 return true;\r
154         }       \r
155         /**\r
156          * Executes a specific command, this function handles plugin commands.\r
157          *\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
161          * @type boolean        \r
162          */\r
163         ,execCommand: function(cmd, param){\r
164                 switch( cmd ){\r
165                         case 'toggle_autocompletion':\r
166                                 var icon= document.getElementById("autocompletion");\r
167                                 if(!this.enabled)\r
168                                 {\r
169                                         if(icon != null){\r
170                                                 editArea.restoreClass(icon);\r
171                                                 editArea.switchClassSticky(icon, 'editAreaButtonSelected', true);\r
172                                         }\r
173                                         this.enabled= true;\r
174                                 }\r
175                                 else\r
176                                 {\r
177                                         this.enabled= false;\r
178                                         if(icon != null)\r
179                                                 editArea.switchClassSticky(icon, 'editAreaButtonNormal', false);\r
180                                 }\r
181                                 return true;\r
182                 }\r
183                 return true;\r
184         }\r
185         ,_checkDelayAndCursorBeforeDisplay: function()\r
186         {\r
187                 this.checkDelayTimer = setTimeout("if(editArea.textarea.selectionStart == "+ editArea.textarea.selectionStart +") EditArea_autocompletion._checkLetter();",  this.delayBeforeDisplay - editArea.check_line_selection_timer - 5 );\r
188         }\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
196         }\r
197         // display the suggested box\r
198         ,_show: function(){\r
199                 if( !this._isShown() )\r
200                 {\r
201                         this.container.style.display="block";\r
202                         this.selectIndex        = -1;\r
203                         this.shown      = true;\r
204                 }\r
205         }\r
206         // is the suggested box displayed?\r
207         ,_isShown: function(){\r
208                 return this.shown;\r
209         }\r
210         // setter and getter\r
211         ,_isInMiddleWord: function( new_value ){\r
212                 if( typeof( new_value ) == "undefined" )\r
213                         return this.isInMiddleWord;\r
214                 else\r
215                         this.isInMiddleWord     = new_value;\r
216         }\r
217         // select the next element in the suggested box\r
218         ,_selectNext: function()\r
219         {\r
220                 var as  = this.container.getElementsByTagName('A');\r
221                 \r
222                 // clean existing elements\r
223                 for( var i=0; i<as.length; i++ )\r
224                 {\r
225                         if( as[i].className )\r
226                                 as[i].className = as[i].className.replace(/ focus/g, '');\r
227                 }\r
228                 \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
232         }\r
233         // select the previous element in the suggested box\r
234         ,_selectBefore: function()\r
235         {\r
236                 var as  = this.container.getElementsByTagName('A');\r
237                 \r
238                 // clean existing elements\r
239                 for( var i=0; i<as.length; i++ )\r
240                 {\r
241                         if( as[i].className )\r
242                                 as[i].className = as[ i ].className.replace(/ focus/g, '');\r
243                 }\r
244                 \r
245                 this.selectIndex--;\r
246                 \r
247                 this.selectIndex        = ( this.selectIndex >= as.length || this.selectIndex < 0 ) ? as.length-1 : this.selectIndex;\r
248                 as[ this.selectIndex ].className        += " focus";\r
249         }\r
250         ,_select: function( content )\r
251         {\r
252                 cursor_forced_position  = content.indexOf( '{@}' );\r
253                 content = content.replace(/{@}/g, '' );\r
254                 editArea.getIESelection();\r
255                 \r
256                 // retrive the number of matching characters\r
257                 var start_index = Math.max( 0, editArea.textarea.selectionEnd - content.length );\r
258                 \r
259                 line_string     =       editArea.textarea.value.substring( start_index, editArea.textarea.selectionEnd + 1);\r
260                 limit   = line_string.length -1;\r
261                 nbMatch = 0;\r
262                 for( i =0; i<limit ; i++ )\r
263                 {\r
264                         if( line_string.substring( limit - i - 1, limit ) == content.substring( 0, i + 1 ) )\r
265                                 nbMatch = i + 1;\r
266                 }\r
267                 // if characters match, we should include them in the selection that will be replaced\r
268                 if( nbMatch > 0 )\r
269                         parent.editAreaLoader.setSelectionRange(editArea.id, editArea.textarea.selectionStart - nbMatch , editArea.textarea.selectionEnd);\r
270                 \r
271                 parent.editAreaLoader.setSelectedText(editArea.id, content );\r
272                 range= parent.editAreaLoader.getSelectionRange(editArea.id);\r
273                 \r
274                 if( cursor_forced_position != -1 )\r
275                         new_pos = range["end"] - ( content.length-cursor_forced_position );\r
276                 else\r
277                         new_pos = range["end"]; \r
278                 parent.editAreaLoader.setSelectionRange(editArea.id, new_pos, new_pos);\r
279                 this._hide();\r
280         }\r
281         \r
282         \r
283         /**\r
284          * Parse the AUTO_COMPLETION part of syntax definition files\r
285          */\r
286         ,_parseSyntaxAutoCompletionDatas: function(){\r
287                 //foreach syntax loaded\r
288                 for(var lang in parent.editAreaLoader.load_syntax)\r
289                 {\r
290                         if(!parent.editAreaLoader.syntax[lang]['autocompletion'])       // init the regexp if not already initialized\r
291                         {\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
295                                 {\r
296                                         // parse them\r
297                                         for(var i in parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION'])\r
298                                         {\r
299                                                 datas   = parent.editAreaLoader.load_syntax[lang]['AUTO_COMPLETION'][i];\r
300                                                 tmp     = {};\r
301                                                 if(datas["CASE_SENSITIVE"]!="undefined" && datas["CASE_SENSITIVE"]==false)\r
302                                                         tmp["modifiers"]="i";\r
303                                                 else\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
312                                                 {\r
313                                                         tmp["keywords"][prefix]= {\r
314                                                                 prefix: prefix,\r
315                                                                 prefix_name: prefix,\r
316                                                                 prefix_reg: new RegExp("(?:"+ parent.editAreaLoader.get_escaped_regexp( prefix ) +")(?:"+ tmp["prefix_separator"] +")$", tmp["modifiers"] ),\r
317                                                                 datas: []\r
318                                                         };\r
319                                                         for( var j=0; j<datas["KEYWORDS"][prefix].length; j++ )\r
320                                                         {\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
326                                                                 };\r
327                                                                 \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
331                                                                 \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
335                                                         }\r
336                                                                 \r
337                                                 }\r
338                                                 tmp["max_text_length"]= datas["MAX_TEXT_LENGTH"];\r
339                                                 parent.editAreaLoader.syntax[lang]['autocompletion'][i] = tmp;\r
340                                         }\r
341                                 }\r
342                         }\r
343                 }\r
344         }\r
345         \r
346         ,_checkLetter: function(){\r
347                 // check that syntax hasn't changed\r
348                 if( this.curr_syntax_str != editArea.settings['syntax'] )\r
349                 {\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
355                 }\r
356                 \r
357                 if( editArea.is_editable )\r
358                 {\r
359                         time=new Date;\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
365                         var results= [];\r
366                         \r
367                         \r
368                         for(var i in this.curr_syntax)\r
369                         {\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
374                                 {\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
377                         \r
378                                         // check if it match a possible word\r
379                                         var match_word= last_chars.match(this.curr_syntax[i]["match_word"]);\r
380                                         \r
381                                         //console.log( match_word );\r
382                                         if( match_word )\r
383                                         {\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
388                                                 {\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
391                                                         {\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
395                                                                 {\r
396                                                         //              parent.console.log('match');\r
397                                                                         hasMatch = false;\r
398                                                                         var before = last_chars.substr( 0, last_chars.length - begin_word.length );\r
399                                                                         \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
402                                                                         {\r
403                                                                                 if( ! before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) )\r
404                                                                                         hasMatch = true;\r
405                                                                         }\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
408                                                                         {\r
409                                                                                 if( before.match( this.curr_syntax[i]["keywords"][prefix]['prefix_reg'] ) )\r
410                                                                                         hasMatch = true;\r
411                                                                         }\r
412                                                                         \r
413                                                                         if( hasMatch )\r
414                                                                                 results[results.length]= [ this.curr_syntax[i]["keywords"][prefix], this.curr_syntax[i]["keywords"][prefix]['datas'][j] ];\r
415                                                                 }       \r
416                                                         }\r
417                                                 }\r
418                                         }\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
422                                         {\r
423                                                 for(var prefix in this.curr_syntax[i]["keywords"])\r
424                                                 {\r
425                                                         for(var j=0; j<this.curr_syntax[i]["keywords"][prefix]['datas'].length; j++)\r
426                                                         {\r
427                                                                 hasMatch = false;\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
430                                                                 {\r
431                                                                         hasMatch        = true;\r
432                                                                 }\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
435                                                                 {\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
438                                                                                 hasMatch = true;\r
439                                                                 }       \r
440                                                                         \r
441                                                                 if( hasMatch )\r
442                                                                         results[results.length]= [ this.curr_syntax[i]["keywords"][prefix], this.curr_syntax[i]["keywords"][prefix]['datas'][j] ];      \r
443                                                         }\r
444                                                 }\r
445                                         }\r
446                                 }\r
447                         }\r
448                         \r
449                         // there is only one result, and we can select it automatically\r
450                         if( results.length == 1 && this.autoSelectIfOneResult )\r
451                         {\r
452                         //      console.log( results );\r
453                                 this._select( results[0][1]['replace_with'] );\r
454                         }\r
455                         else if( results.length == 0 )\r
456                         {\r
457                                 this._hide();\r
458                         }\r
459                         else\r
460                         {\r
461                                 // build the suggestion box content\r
462                                 var lines=[];\r
463                                 for(var i=0; i<results.length; i++)\r
464                                 {\r
465                                         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'];\r
466                                         if(results[i][0]['prefix_name'].length>0)\r
467                                                 line+='<span class="prefix">'+ results[i][0]['prefix_name'] +'</span>';\r
468                                         line+='</a></li>';\r
469                                         lines[lines.length]=line;\r
470                                 }\r
471                                 // sort results\r
472                                 this.container.innerHTML                = '<ul>'+ lines.sort().join('') +'</ul>';\r
473                                 \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
477                                 this._show();\r
478                         }\r
479                                 \r
480                         this.autoSelectIfOneResult = false;\r
481                         time=new Date;\r
482                         t2= time.getTime();\r
483                 \r
484                         //parent.console.log( begin_word +"\n"+ (t2-t1) +"\n"+ html );\r
485                 }\r
486         }\r
487 };\r
488 \r
489 // Load as a plugin\r
490 editArea.settings['plugins'][ editArea.settings['plugins'].length ] = 'autocompletion';\r
491 editArea.add_plugin('autocompletion', EditArea_autocompletion);