57264a69fd6d4e0065d24b3132253a440adfd416
[Packages/TYPO3.CMS.git] / typo3 / sysext / t3editor / res / jslib / ts_codecompletion / tscodecompletion.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2008-2010 Stephan Petzl <spetzl@gmx.at> and Christian Kartnig <office@hahnepeter.de>
5 * All rights reserved
6 *
7 * This script is part of the TYPO3 project. The TYPO3 project is
8 * free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * The GNU General Public License can be found at
14 * http://www.gnu.org/copyleft/gpl.html.
15 * A copy is found in the textfile GPL.txt and important notices to the license
16 * from the author is found in LICENSE.txt distributed with these scripts.
17 *
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This copyright notice MUST APPEAR in all copies of the script!
25 ***************************************************************/
26
27 /**
28 * @fileoverview contains the TsCodeCompletion class
29 */
30
31 /**
32 * Construct a new TsCodeCompletion object.
33 * @class This is the main class of the codeCompletion.
34 * it is directly invoked by the editor. It instantiates all other classes
35 * manages the control flow and takes care of the completionbox
36 *
37 * @constructor
38 * @param codeMirror codeMirror instance, for retrieving the cursor position
39 * @param outerdiv div that contains the editor, for DOM manipulation
40 * @return A new TsCodeCompletion instance
41 */
42 var TsCodeCompletion = function(codeMirror, outerdiv) {
43 // private Vars
44 var tsRef = new TsRef();
45 var mirror = codeMirror;
46 var options = {ccWords : 10};
47 // t3editor index (=0 if there is just one editor on the page, should be set from outside)
48 var index = 0;
49
50 var currWord = 0;
51 var cc_up;
52 var cc_down;
53 var mousePos = {x:0,y:0};
54 var proposals;
55 var compResult;
56 var cc = 0;
57 var linefeedsPrepared = false;
58 var currentCursorPosition = null;
59
60 Event.observe(document, 'mousemove', saveMousePos, false);
61
62 // load the external templates ts-setup into extTsObjTree
63 var extTsObjTree = new Object();
64 var parser = new TsParser(tsRef, extTsObjTree);
65 loadExtTemplatesAsync();
66
67
68 // TODO port plugin to t3editor.js
69
70 // plugin-array will be retrieved through AJAX from the conf array
71 // plugins can be attached by regular TYPO3-extensions
72 var plugins = [];
73
74 // we add the description plugin here because its packed with the codecompletion currently
75 // maybe we will swap it to an external plugin in future
76 var plugin = new Object();
77 plugin.extpath = T3editor.PATH_t3e;
78 plugin.classpath = 'res/jslib/ts_codecompletion/descriptionPlugin.js';
79 plugin.classname = 'DescriptionPlugin';
80
81 plugins.push(plugin);
82
83
84 var codeCompleteBox = new Element("DIV", {
85 "class": "t3e_codeCompleteBox"
86 });
87 codeCompleteBox.hide();
88 outerdiv.appendChild(codeCompleteBox);
89
90 // load the external xml-reference
91 tsRef.loadTsrefAsync();
92
93 // plugins will be provided with the pluginContext
94 var pluginContext = new Object();
95 pluginContext.outerdiv = outerdiv;
96 pluginContext.codeCompleteBox = codeCompleteBox;
97 pluginContext.tsRef = tsRef;
98 pluginContext.parser = parser;
99 pluginContext.plugins = plugins;
100 pluginContext.codeMirror = codeMirror;
101
102 // should we use a pluginmanager so no for loops are required on each hook?
103 // e.g. pluginmanager.call('afterKeyUp',....);
104 loadPluginArray();
105
106
107 /**
108 * loads the array of registered codecompletion plugins
109 * to register a plugin you have to add an array to the localconf
110 * $TYPO3_CONF_VARS['EXTCONF']['t3editor']['plugins'][] = array(
111 * 'extpath' => t3lib_div::getIndpEnv('TYPO3_SITE_URL').t3lib_extMgm::siteRelPath($_EXTKEY),
112 * 'classpath' => 'js/my_plugin.js',
113 * 'classname'=> 'MyPlugin'
114 * );
115 */
116 function loadPluginArray() {
117 var urlParameters = '&ajaxID=tx_t3editor::getPlugins';
118 new Ajax.Request(
119 T3editor.URL_typo3 + 'ajax.php',
120 {
121 parameters: urlParameters,
122 method: 'get',
123 onSuccess: function(transport) {
124 var loadedPlugins = eval('('+ transport.responseText +')');
125 plugins = plugins.concat(loadedPlugins);
126 // register an internal plugin
127 loadPlugins();
128 }
129 }
130 );
131 }
132
133 /**
134 * instantiates all plugins and adds the instances to the plugin array
135 */
136 function loadPlugins() {
137 for (var i = 0; i < plugins.length; i++) {
138 var script = document.createElement('script');
139 script.setAttribute('type', 'text/javascript');
140 script.setAttribute('src', plugins[i].extpath+plugins[i].classpath);
141 document.getElementsByTagName('head')[0].appendChild(script);
142 window.setTimeout(makeInstance.bind(this,plugins[i],i),1000);
143 }
144 }
145
146 /**
147 * makes a single plugin instance
148 */
149 function makeInstance(plugin, i) {
150 try {
151 var localname = "plugins[" + i + "].obj";
152 eval(localname+' = new ' + plugin.classname + '();');
153 var obj = eval(localname);
154 } catch(e) {
155 throw("error occured while trying to make new instance of \"" + plugin.classname + "\"! maybe syntax error or wrong filepath?");
156 return;
157 }
158 obj.init(pluginContext,plugin);
159 }
160
161 /**
162 * all external templates along the rootline have to be loaded,
163 * this function retrieves the JSON code by comitting a AJAX request
164 */
165 function loadExtTemplatesAsync() {
166 var urlParameters = '&ajaxID=tx_t3editor_codecompletion::loadTemplates&pageId=' + getGetVar('id');
167 new Ajax.Request(
168 T3editor.URL_typo3 + 'ajax.php',
169 {
170 method: 'get',
171 parameters: urlParameters,
172 onSuccess: function(transport) {
173 extTsObjTree.c = eval('('+ transport.responseText +')');
174 resolveExtReferencesRec(extTsObjTree.c);
175 }
176 }
177 );
178 }
179
180 /**
181 * since the references are not resolved server side we have to do it client-side
182 * benefit: less loading time due to less data which has to be transmitted
183 */
184 function resolveExtReferencesRec(childNodes) {
185 for(var key in childNodes) {
186 var childNode;
187 // if the childnode has a value and there is a parto of a reference operator ('<')
188 // and it does not look like a html tag ('>')
189 if (childNodes[key].v && childNodes[key].v[0] == '<'
190 && childNodes[key].v.indexOf('>') == -1 ) {
191 var path = childNodes[key].v.replace(/</,"").strip();
192 // if there are still whitespaces its no path
193 if (path.indexOf(' ') == -1) {
194 childNode = getExtChildNode(path);
195 // if the node was found - reference it
196 if (childNode != null) {
197 childNodes[key] = childNode;
198 }
199 }
200 }
201 // if there was no reference-resolving then we go deeper into the tree
202 if (!childNode && childNodes[key].c) {
203 resolveExtReferencesRec(childNodes[key].c);
204 }
205 }
206 }
207
208 function getExtChildNode(path) {
209 var extTree = extTsObjTree;
210 var path = path.split('.');
211 var pathSeg;
212 var i;
213 for ( i=0; i < path.length; i++) {
214 pathSeg = path[i];
215 if(extTree.c == null || extTree.c[pathSeg] == null) {
216 return null;
217 }
218 extTree = extTree.c[pathSeg];
219 }
220 return extTree;
221 }
222
223 /**
224 * replaces editor functions insertNewlineAtCursor and indentAtCursor
225 * with modified ones that only execute when codecompletion box is not shown
226 */
227 // TODO check if this wokrs correctly after updating the codemirror base
228 function prepareLinefeeds() {
229 mirror.win.select.insertNewlineAtCursor_original = mirror.win.select.insertNewlineAtCursor;
230 mirror.win.select.insertNewlineAtCursor = function(window) {
231 if (cc==0) {
232 mirror.win.select.insertNewlineAtCursor_original(window);
233 }
234 };
235 mirror.editor.indentAtCursor_original = mirror.editor.indentAtCursor;
236 mirror.editor.indentAtCursor = function() {
237 if (cc==0) {
238 mirror.editor.indentAtCursor_original();
239 }
240 };
241 linefeedsPrepared = true;
242 }
243
244 /**
245 * Eventhandler function for mouseclicks
246 * ends the codecompletion
247 * @param event fired prototype event object
248 * @type void
249 */
250 this.click = function(event) {
251 endAutoCompletion();
252 }
253
254 function getFilter(cursorNode) {
255 if(cursorNode.currentText) {
256 var filter = cursorNode.currentText.replace('.','');
257 return filter.replace(/\s/g,"");
258 } else {
259 return "";
260 }
261 }
262
263 function getCursorNode() {
264 var cursorNode = mirror.win.select.selectionTopNode(mirror.win.document.body, false);
265 // cursorNode is null if the cursor is positioned at the beginning of the first line
266 if (cursorNode == null) {
267 cursorNode = mirror.editor.container.firstChild;
268 } else if (cursorNode.tagName=='BR') {
269 // if cursor is at the end of the line -> jump to beginning of the next line
270 cursorNode = cursorNode.nextSibling;
271 }
272 return cursorNode;
273 }
274
275
276 function getCurrentLine(cursor) {
277 var line = "";
278 var currentNode = cursor.start.node.parentNode;
279 while (currentNode.tagName !='BR') {
280 if (currentNode.hasChildNodes()
281 && currentNode.firstChild.nodeType == 3
282 && currentNode.currentText.length > 0) {
283 line = currentNode.currentText + line;
284 }
285 if (currentNode.previousSibling == null) {
286 break;
287 } else {
288 currentNode = currentNode.previousSibling;
289 }
290 }
291 return line;
292 }
293
294 /**
295 * Eventhandler function executed after keystroke release
296 * triggers CC on pressed dot and typing on
297 * @param event fired prototype event object
298 * @type void
299 */
300 this.keyUp = function(event) {
301 var keycode = event.keyCode;
302 if (keycode == 190) {
303 refreshCodeCompletion();
304 } else if (cc == 1) {
305 if (keycode != Event.KEY_DOWN && keycode != Event.KEY_UP) {
306 refreshCodeCompletion();
307 }
308 }
309 }
310
311 /**
312 * Eventhandler function executed after keystroke release
313 * triggers CC on pressed dot and typing on
314 * @param event fired prototype event object
315 * @type void
316 */
317 this.keyDown = function(event) {
318
319 // prepareLinefeeds() gets called the first time keyDown is executed.
320 // we have to put this here, cause in the constructor mirror.editor is not yet loaded
321 if (!linefeedsPrepared) {
322 prepareLinefeeds();
323 }
324 var keycode = event.keyCode;
325 if (cc == 1) {
326 if (keycode == Event.KEY_UP) {
327 // arrow up: move up cursor in codecomplete box
328 event.stop();
329 codeCompleteBoxMoveUpCursor();
330 for (var i=0; i<plugins.length; i++) {
331 if (plugins[i].obj && plugins[i].obj.afterKeyUp) {
332 plugins[i].obj.afterKeyUp(proposals[currWord],compResult);
333 }
334 }
335
336 } else if (keycode == Event.KEY_DOWN) {
337 // Arrow down: move down cursor in codecomplete box
338 event.stop();
339 codeCompleteBoxMoveDownCursor();
340 for (var i=0; i<plugins.length; i++){
341 if (plugins[i].obj && plugins[i].obj.afterKeyDown) {
342 plugins[i].obj.afterKeyDown(proposals[currWord],compResult);
343 }
344 }
345
346 } else if (keycode == Event.KEY_ESC || keycode == Event.KEY_LEFT || keycode== Event.KEY_RIGHT) {
347 // Esc, Arrow Left, Arrow Right: if codecomplete box is showing, hide it
348 endAutoCompletion();
349
350 } else if (keycode == Event.KEY_RETURN) {
351 event.stop();
352 if (currWord != -1) {
353 insertCurrWordAtCursor();
354 }
355 endAutoCompletion();
356
357 } else if (keycode == 32 && !event.ctrlKey) {
358 endAutoCompletion();
359
360 } else if (keycode == 32 && event.ctrlKey) {
361 refreshCodeCompletion();
362
363 } else if (keycode == Event.KEY_BACKSPACE) {
364 var cursorNode = mirror.win.select.selectionTopNode(mirror.win.document.body, false);
365 if (cursorNode.innerHTML == '.') {
366 // force full refresh at keyUp
367 compResult = null;
368 }
369 }
370
371 } else { // if autocompletion is deactivated and ctrl+space is pressed
372 if (keycode == 32 && event.ctrlKey) {
373 event.stop();
374 refreshCodeCompletion();
375 }
376 }
377 }
378
379 function refreshCodeCompletion() {
380 // init vars for up/down moving in word list
381 cc_up = 0;
382 cc_down = options.ccWords-1;
383
384 // clear the last completion wordposition
385 currWord = -1;
386 mirror.editor.highlightAtCursor();
387
388 // retrieves the node right to the cursor
389 currentCursorPosition = mirror.win.select.markSelection(mirror.win);
390 cursorNode = getCursorNode();
391
392 // the cursornode has to be stored cause inserted breaks have to be deleted after pressing enter if the codecompletion is active
393 var filter = getFilter(cursorNode);
394
395 if (compResult == null || cursorNode.innerHTML == '.') {
396 // TODO: implement cases: operatorCompletion reference/copy path completion (formerly found in getCompletionResults())
397 var currentTsTreeNode = parser.buildTsObjTree(mirror.editor.container.firstChild, cursorNode);
398 compResult = new CompletionResult(tsRef,currentTsTreeNode);
399 }
400
401 proposals = compResult.getFilteredProposals(filter);
402
403 // if proposals are found - show box
404 if (proposals.length > 0) {
405
406 // make UL list of completation proposals
407 var html = '<ul>';
408 for (i = 0; i < proposals.length; i++) {
409 html += '<li style="height:16px;vertical-align:middle;" ' +
410 'id="cc_word_' + i + '" ' +
411 'onclick="T3editor.instances[' + index + '].tsCodeCompletion.insertCurrWordAtCursor(' + i + ');T3editor.instances[' + index + '].tsCodeCompletion.endAutoCompletion();" ' +
412 'onmouseover="T3editor.instances[' + index + '].tsCodeCompletion.onMouseOver(' + i + ',event);">' +
413 '<span class="word_' + proposals[i].cssClass + '">' +
414 proposals[i].word +
415 '</span></li>';
416 }
417 html += '</ul>';
418
419 // put HTML and show box
420 codeCompleteBox.innerHTML = html;
421 codeCompleteBox.show();
422 codeCompleteBox.scrollTop = 0;
423
424 // init styles
425 codeCompleteBox.style.overflowY = 'scroll';
426 codeCompleteBox.style.height = (options.ccWords * ($("cc_word_0").offsetHeight)) + 'px';
427
428
429 var leftpos = (Position.cumulativeOffset($$('.t3e_iframe_wrap')[index])[0] + Position.cumulativeOffset(cursorNode)[0] + cursorNode.offsetWidth) + 'px';
430 var toppos = (Position.cumulativeOffset(cursorNode)[1] + cursorNode.offsetHeight - Element.cumulativeScrollOffset(cursorNode)[1]) + 'px';
431 codeCompleteBox.setStyle({left: leftpos, top: toppos});
432
433 // set flag to 1 - needed for continue typing word.
434 cc = 1;
435
436 // highlight first word in list
437 highlightCurrWord(0);
438 for (var i=0;i<plugins.length;i++) {
439 if (plugins[i].obj && plugins[i].obj.afterCCRefresh) {
440 plugins[i].obj.afterCCRefresh(proposals[currWord],compResult);
441 }
442 }
443 } else {
444 endAutoCompletion();
445 }
446 }
447
448
449
450
451 /**
452 * hides codecomplete box and resets completionResult
453 * afterwards the interceptor method endCodeCompletion gets called
454 * @type void
455 */
456 this.endAutoCompletion = function() {
457 endAutoCompletion();
458 }
459
460 function endAutoCompletion(){
461 cc = 0;
462 codeCompleteBox.hide();
463 // force full refresh
464 compResult = null;
465 for (var i=0;i<plugins.length;i++) {
466 if (plugins[i].obj && plugins[i].obj.endCodeCompletion) {
467 plugins[i].obj.endCodeCompletion();
468 }
469 }
470 }
471
472
473 /**
474 * move cursor in autcomplete box up
475 */
476 function codeCompleteBoxMoveUpCursor() {
477 // if previous position was first or position not initialized - then move cursor to last word, else decrease position
478 if (currWord == 0 || currWord == -1) {
479 var id = proposals.length - 1;
480 } else {
481 var id = currWord - 1;
482 }
483 // hightlight new cursor position
484 highlightCurrWord(id);
485 // update id of first and last showing proposals and scroll box
486 if (currWord < cc_up || currWord == (proposals.length - 1)) {
487 cc_up = currWord;
488 cc_down = currWord + (options.ccWords - 1);
489 if (cc_up === (proposals.length - 1)) {
490 cc_down = proposals.length - 1;
491 cc_up = cc_down - (options.ccWords - 1);
492 }
493 codeCompleteBox.scrollTop = cc_up * 16;
494 }
495 }
496
497 /**
498 * move cursor in codecomplete box down
499 */
500 function codeCompleteBoxMoveDownCursor() {
501 // if previous position was last word in list - then move cursor to first word if not than position ++
502 if (currWord == proposals.length - 1) {
503 var id = 0;
504 } else {
505 var id = currWord + 1;
506 }
507 // highlight new cursor position
508 highlightCurrWord(id);
509
510 // update id of first and last showing proposals and scroll box
511 if (currWord > cc_down || currWord == 0) {
512 cc_down = currWord;
513 cc_up = currWord - (options.ccWords - 1);
514 if (cc_down == 0) {
515 cc_up = 0;
516 cc_down = options.ccWords - 1;
517 }
518 codeCompleteBox.scrollTop = cc_up * 16;
519 }
520 }
521
522 function saveMousePos(event){
523 mousePos.x = event.clientX;
524 mousePos.y = event.clientY;
525 }
526
527 /**
528 * highlights entry in codecomplete box by id
529 * @param {int} id
530 * @type void
531 */
532 this.onMouseOver = function(id,event){
533 highlightCurrWord(id,event);
534 for(var i=0;i<plugins.length;i++){
535 if(plugins[i].obj && plugins[i].obj.afterMouseOver) {
536 plugins[i].obj.afterMouseOver(proposals[currWord],compResult);
537 }
538 }
539 }
540
541 function highlightCurrWord(id,event) {
542 // if it is a mouseover event
543 if(event){
544 // if mousecoordinates haven't changed -> mouseover was triggered by scrolling of the result list -> don't highlight another word (return)
545 if(mousePos.x == event.clientX && mousePos.y == event.clientY) {
546 return;
547 }
548 mousePos.x = event.clientX;
549 mousePos.y = event.clientY;
550 }
551 if (currWord != -1) {
552 $('cc_word_' + currWord).className = '';
553 }
554 $('cc_word_' + id).className = 'active';
555 currWord = id;
556 }
557
558 /**
559 * Insert the currently selected item in the proposal list
560 * of the codecompletion box into the editor div at cursor position
561 * @type void
562 * @see #highlightCurrWord
563 */
564 this.insertCurrWordAtCursor = function(){
565 insertCurrWordAtCursor();
566 }
567
568 /**
569 * insert selected word into text from codecompletebox
570 */
571 function insertCurrWordAtCursor() {
572 var word = proposals[currWord].word;
573 // tokenize current line
574 mirror.editor.highlightAtCursor();
575 var select = mirror.win.select;
576 var cursorNode = getCursorNode();
577
578 if (cursorNode.currentText
579 && cursorNode.currentText != '.'
580 && cursorNode.currentText.strip() != '' ) {
581 // if there is some typed text already, left to the "." -> simply replace node content with the word
582 cursorNode.innerHTML = word;
583 cursorNode.currentText = word;
584 select.setCursorPos(mirror.editor.container, {node: cursorNode, offset: 0});
585 } else { // if there is no text there, insert the word at the cursor position
586 mirror.replaceSelection(word);
587 }
588 }
589
590
591 /**
592 * retrieves the get-variable with the specified name
593 */
594 function getGetVar(name){
595 var get_string = document.location.search;
596 var return_value = '';
597 var value;
598 do { //This loop is made to catch all instances of any get variable.
599 var name_index = get_string.indexOf(name + '=');
600 if (name_index != -1) {
601 get_string = get_string.substr(name_index + name.length + 1, get_string.length - name_index);
602 end_of_value = get_string.indexOf('&');
603 if (end_of_value != -1) {
604 value = get_string.substr(0, end_of_value);
605 } else {
606 value = get_string;
607 }
608
609 if (return_value == '' || value == '') {
610 return_value += value;
611 } else {
612 return_value += ', ' + value;
613 }
614 }
615 } while(name_index != -1);
616
617 // Restores all the blank spaces.
618 var space = return_value.indexOf('+');
619 while(space != -1) {
620 return_value = return_value.substr(0, space) + ' ' +
621 return_value.substr(space + 1, return_value.length);
622 space = return_value.indexOf('+');
623 }
624
625 return(return_value);
626 }
627 }
628
629 document.observe('t3editor:init', function(event) {
630 that = event.memo.t3editor;
631 that.tsCodeCompletion = new TsCodeCompletion(that.mirror, that.outerdiv);
632 });
633
634
635 document.observe('t3editor:keyup', function(event) {
636 that = event.memo.t3editor;
637 if (that.tsCodeCompletion) that.tsCodeCompletion.keyUp(event.memo.actualEvent);
638 });
639
640 document.observe('t3editor:keydown', function(event) {
641 that = event.memo.t3editor;
642 if (that.tsCodeCompletion) that.tsCodeCompletion.keyDown(event.memo.actualEvent);
643 });
644
645 document.observe('t3editor:click', function(event) {
646 that = event.memo.t3editor;
647 if (that.tsCodeCompletion) that.tsCodeCompletion.click(event.memo.actualEvent);
648 });
649