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