Subversion Repositories wimsdev

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

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