a35879010228be11f8d2f62b45b7ce27f1579ab8
[Packages/TYPO3.CMS.git] / typo3 / sysext / t3editor / jslib / t3editor.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2007 Tobias Liebig <mail_typo3@etobi.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 /* t3editor.js is based on codemirror.js from the Codemirror editor.
27 * See LICENSE file for further informations
28 */
29
30
31
32 /**
33 * Browser checks
34 * inspired by tinyMCE
35 */
36 var ua = navigator.userAgent;
37 var isMSIE = (navigator.appName == "Microsoft Internet Explorer");
38 var isMSIE5 = this.isMSIE && (ua.indexOf('MSIE 5') != -1);
39 var isMSIE5_0 = this.isMSIE && (ua.indexOf('MSIE 5.0') != -1);
40 var isMSIE7 = this.isMSIE && (ua.indexOf('MSIE 7') != -1);
41 var isGecko = ua.indexOf('Gecko') != -1; // Will also be true on Safari
42 var isSafari = ua.indexOf('Safari') != -1;
43 var isOpera = window['opera'] && opera.buildNumber ? true : false;
44 var isMac = ua.indexOf('Mac') != -1;
45 var isNS7 = ua.indexOf('Netscape/7') != -1;
46 var isNS71 = ua.indexOf('Netscape/7.1') != -1;
47
48
49
50 // collection of all t3editor instances on the current page
51 var t3e_instances = {};
52
53
54
55 /* CodeMirror main module
56 *
57 * Implements the CodeMirror constructor and prototype, which take care
58 * of initializing the editor and managing the highlighting and
59 * indentation, and some functions for transforming arbitrary DOM
60 * structures into plain sequences of <span> and <br> elements.
61 */
62
63 // The MirrorOptions object is used to specify a default
64 // configuration. If you specify such an object before loading this
65 // file, the values you put into it will override the defaults given
66 // below.
67 var t3eOptions = window.t3eOptions || {};
68
69 // safeKeys specifies the set of keys that will probably not modify
70 // the content of the editor, and thus do not have to be responded to.
71 // You usually won't have to change this.
72 // reindentKeys gives the keys that should cause the editor to
73 // re-indent the current line
74 // reindentAfterKeys works like reindentKeys, but in this case the
75 // key's normal effect is first allowed to take place. Use this for
76 // keys that might change the indentation level of the current line.
77 // stylesheet is the filename of the stylesheet that should be used to
78 // colour the code in the editor.
79 // parser should refer to a function that, when given a string stream
80 // (see stringstream.js), produces an object that acts as a stream of
81 // tokens plus some other functionality. See parsejavascript.js for an
82 // example and more information.
83 // linesPerPass is the maximum amount of lines that the highlighter
84 // tries to colour in one shot. Setting this too high will cause the
85 // code to 'freeze' the browser for noticeable intervals.
86 // passDelay gives the amount of milliseconds between colouring passes
87 var t3eOptions = {
88 safeKeys: { "KEY_ARROW_UP":true,
89 "KEY_ARROW_DOWN":true,
90 "KEY_ARROW_LEFT":true,
91 "KEY_ARROW_RIGHT":true,
92 "KEY_END":true,
93 "KEY_HOME":true,
94 "KEY_PAGE_UP":true,
95 "KEY_PAGE_DOWN":true,
96 "KEY_SHIFT":true,
97 "KEY_CTRL":true,
98 "KEY_ALT":true,
99 "KEY_SELECT":true
100 },
101 reindentKeys: {"KEY_TAB":true},
102 reindentAfterKeys: {"KEY_RIGHT_SQUARE_BRACKET":true},
103 stylesheet: PATH_t3e+"css/t3editor.css",
104 parser: parseTypoScript,
105 linesPerPass: 10,
106 passDelay: 200,
107 autoComplete: true,
108 acWords:5
109 };
110 // These default options can be overridden by passing a set of options
111 // to a specific CodeMirror constructor.
112
113 var t3editor = function(){
114 // The HTML elements whose content should be suffixed by a newline
115 // when converting them to flat text.
116 var newlineElements = {"P":true, "DIV":true, "LI":true};
117
118 // Helper function for traverseDOM. Flattens an arbitrary DOM node
119 // into an array of textnodes and <br> tags.
120 function simplifyDOM(root) {
121 var doc = root.ownerDocument;
122 var result = [];
123 var leaving = false;
124
125 function simplifyNode(node) {
126 leaving = false;
127
128 if (node.nodeType == 3) {
129 node.nodeValue = node.nodeValue.replace(/[\n\r]/g, "").replace(/[\t ]/g, nbsp);
130 result.push(node);
131 }
132 else if (node.nodeName == "BR" && node.childNodes.length == 0) {
133 result.push(node);
134 }
135 else {
136 // forEach(node.childNodes, simplifyNode);
137 $A(node.childNodes).each(simplifyNode);
138 if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
139 leaving = true;
140 el = new Element('SPAN');
141 result.push(new Element('BR'));
142 }
143 }
144 }
145
146 simplifyNode(root);
147 return result;
148 }
149
150 // Creates a MochiKit-style iterator that goes over a series of DOM
151 // nodes. The values it yields are strings, the textual content of
152 // the nodes. It makes sure that all nodes up to and including the
153 // one whose text is being yielded have been 'normalized' to be just
154 // <span> and <br> elements.
155 // See the story.html file for some short remarks about the use of
156 // continuation-passing style in this iterator.
157 function traverseDOM(start){
158 function yield(value, c){cc = c; return value;}
159 function push(fun, arg, c){return function(){return fun(arg, c);};}
160 function stop(){cc = stop; throw StopIteration;};
161 var cc = push(scanNode, start, stop);
162 var owner = start.ownerDocument;
163
164 // Create a function that can be used to insert nodes after the
165 // one given as argument.
166 function pointAt(node){
167 var parent = node.parentNode;
168 var next = node.nextSibling;
169 if (next)
170 return function(newnode){parent.insertBefore(newnode, next);};
171 else
172 return function(newnode){parent.appendChild(newnode);};
173 }
174 var point = null;
175
176 // Insert a normalized node at the current point. If it is a text
177 // node, wrap it in a <span>, and give that span a currentText
178 // property -- this is used to cache the nodeValue, because
179 // directly accessing nodeValue is horribly slow on some browsers.
180 // The dirty property is used by the highlighter to determine
181 // which parts of the document have to be re-highlighted.
182 function insertPart(part){
183 var text = "\n";
184 if (part.nodeType == 3) {
185 text = part.nodeValue;
186
187 newpart = new Element('SPAN',{"class": "part"});
188 newpart.appendChild(part);
189 part = newpart;
190 part.currentText = text;
191 }
192 part.dirty = true;
193 point(part);
194 return text;
195 }
196
197 // Extract the text and newlines from a DOM node, insert them into
198 // the document, and yield the textual content. Used to replace
199 // non-normalized nodes.
200 function writeNode(node, c){
201 var toYield = [];
202 var simdom = simplifyDOM(node);
203 simdom.each(
204 function(part) {
205 toYield.push(insertPart(part));
206 }
207 );
208
209 return yield(toYield.join(""), c);
210 }
211
212 // Check whether a node is a normalized <span> element.
213 function partNode(node){
214 if (node.nodeName == "SPAN" && node.childNodes.length == 1 && node.firstChild.nodeType == 3){
215 node.currentText = node.firstChild.nodeValue;
216 return true;
217 }
218 return false;
219 }
220
221 // Handle a node. Add its successor to the continuation if there
222 // is one, find out whether the node is normalized. If it is,
223 // yield its content, otherwise, normalize it (writeNode will take
224 // care of yielding).
225 function scanNode(node, c){
226 if (node.nextSibling)
227 c = push(scanNode, node.nextSibling, c);
228
229 if (partNode(node)){
230 return yield(node.currentText, c);
231 }
232 else if (node.nodeName == "BR") {
233 return yield("\n", c);
234 }
235 else {
236 point = pointAt(node);
237 Element.remove(node);
238 return writeNode(node, c);
239 }
240 }
241
242 // MochiKit iterators are objects with a next function that
243 // returns the next value or throws StopIteration when there are
244 // no more values.
245 return {next: function(){return cc();}};
246 } // traverseDOM
247
248 var nbspRegexp = new RegExp(nbsp, "g");
249
250
251
252 function t3editor(theTextarea, index, options) {
253
254 // Use passed options, if any, to override defaults.
255 this.options = options || t3eOptions; // {}
256 for (var lo in options) {
257 this.options[lo] = options[lo];
258 }
259
260 //History Array
261 this.history = [];
262 //Max History Size
263 this.historySize = 100;
264 //Init history position
265 this.currHistoryPosition = -1;
266
267 // memorize the textarea
268 this.textarea = $(theTextarea);
269
270 this.documentname = this.textarea.readAttribute('alt');
271
272 // count index (helpful if more than one editor is on the page)
273 this.index = index;
274
275 // create the wrapping div
276 this.outerdiv = new Element("div", {
277 "class": "t3e_outerdiv",
278 "id": "t3e_"+this.textarea.getAttribute('id')
279 }
280 );
281
282 // place the div before the textarea
283 this.textarea.parentNode.insertBefore(this.outerdiv,$(this.textarea));
284
285 // an overlay that covers the whole editor
286 this.modalOverlay = new Element("DIV", {
287 "class": "t3e_modalOverlay",
288 "id": "t3e_modalOverlay_wait"
289 }
290 );
291 this.modalOverlay.hide();
292 this.modalOverlay.setStyle(this.outerdiv.getDimensions());
293 this.modalOverlay.setStyle({opacity: 0.5});
294 this.outerdiv.appendChild(this.modalOverlay);
295
296 this.helpOverlay = new Element("DIV", {
297 "class": "t3e_modalOverlay",
298 "id": "t3e_modalOverlay_help"
299 }
300 );
301 // TODO: fill with senseful content, make it dynamic
302 this.helpOverlay.innerHTML = "<h2>t3editor</h2>"+
303 "<p>put some helpful text here</p><br/><br/>"+
304 "<p>Hotkeys:</p>"+
305 "<p>"+
306 "<strong>CTRL-S</strong> send code to server<br/>"+
307 "<strong>CTRL-F11</strong> toggle fullscreen mode<br/>"+
308 "<strong>CTRL-SPACE</strong> auto-complete (based on letters at current cursor-position)<br/>"+
309 "<strong>CTRL-Z</strong> undo<br/>"+
310 "<strong>CTRL-Y</strong> redo<br/>"+
311 "</p><br/>"+
312 "<p><a href='javascript:void(0)' onclick='t3e_instances["+this.index+"].toggleHelp();'>click here to close this help window</a></p>"+
313 "";
314 this.helpOverlay.hide();
315 this.outerdiv.appendChild(this.helpOverlay);
316
317 // wrapping the linenumbers
318 this.linenum_wrap = new Element("DIV", {
319 "class": "t3e_linenum_wrap"
320 }
321 );
322 // the "linenumber" list itself
323 this.linenum = new Element("DL", {
324 "class": "t3e_linenum"
325 }
326 );
327 this.linenum_wrap.appendChild(this.linenum);
328 this.outerdiv.appendChild(this.linenum_wrap);
329
330 //autocomplete box
331 this.autoCompleteBox = new Element("DIV",{
332 "class": "t3e_autoCompleteBox"
333 }
334 );
335 this.autoCompleteBox.hide();
336 this.outerdiv.appendChild(this.autoCompleteBox);
337
338 // wrapping the iframe
339 this.iframe_wrap = new Element("DIV", {
340 "class": "t3e_iframe_wrap"
341 }
342 );
343
344 // the iframe (the actual "editor")
345 // display: block occasionally suppresses some Firefox bugs, so we
346 // always add it, redundant as it sounds.
347 this.iframe = new Element("IFRAME", {
348 "style": "border: 0; display: block;",
349 "class": "t3e_iframe"
350 }
351 );
352
353 this.iframe_wrap.appendChild(this.iframe);
354 this.outerdiv.appendChild(this.iframe_wrap);
355
356 // wrapping the footer/statusline
357 this.footer_wrap = new Element("DIV", {
358 "class": "t3e_footer_wrap"
359 }
360 );
361 this.outerdiv.appendChild(this.footer_wrap);
362
363 // footer item: show help Window
364 // TODO make this more flexible! And get rid of inline css and unsed options!
365 this.fitem_help = this.createFooterItem('Help', true, "this.toggleHelp()");
366 this.footer_wrap.appendChild(this.fitem_help);
367
368 // footer item: options menu
369 this.fitem_options_overlay = new Element("DIV", {
370 "class": "t3e_footer_overlay",
371 "id": "t3e_footer_overlay_options"
372 }
373 );
374
375 // TODO make this more flexible! And get rid of inline css and unsed options!
376 this.fitem_options_overlay.innerHTML = '<ul>'+
377 // '<li style="color:grey"><input type="checkbox" disabled="disabled" /> Syntax highlighting</li>'+
378 '<li><input type="checkbox" onclick="t3e_instances['+this.index+'].fitem_options_overlay.hide();t3e_instances['+this.index+'].toggleAutoComplete();" id="t3e_autocomplete" checked="checked" /><label for="t3e_autocomplete">AutoCompletion</label></li>'+
379 '<li><span onclick="t3e_instances['+this.index+'].fitem_options_overlay.hide();t3e_instances['+this.index+'].footeritem_demo_click();">Test snippets</span></li>'+
380 '<li><input type="checkbox" onclick="t3e_instances['+this.index+'].fitem_options_overlay.hide();t3e_instances['+this.index+'].toggleFullscreen();" id="t3e_fullscreen" /> <label for="t3e_fullscreen">Fullscreen</label></li>'+
381 // '<li style="color:grey"><input type="checkbox" disabled="disabled" /> other fancy stuff</li>'+
382 '</ul>';
383 this.fitem_options_overlay.hide();
384 this.fitem_options = this.createFooterItem('Options', true, this.fitem_options_overlay);
385 this.footer_wrap.appendChild(this.fitem_options);
386 this.footer_wrap.appendChild(this.fitem_options_overlay);
387
388 // footer item: status field (total line numbers)
389 this.fitem_status = this.createFooterItem('', false);
390 this.footer_wrap.appendChild(this.fitem_status);
391
392 // footer item: "name" of the document (taken from textarea alt-attribut), and save indicator
393 this.fitem_name = this.createFooterItem(this.documentname, false);
394 this.footer_wrap.appendChild(this.fitem_name);
395
396 // window and document objects from the iframe
397 this.win = this.iframe.contentWindow;
398 this.doc = this.win.document;
399
400 // make the iframe "editable"
401 this.doc.designMode = "on";
402
403 this.doc.open();
404 this.doc.write(
405 "<html><head>"+
406 "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
407 t3eOptions.stylesheet +
408 "\"/></head>" +
409 "<body class=\"editbox\" spellcheck=\"false\"></body></html>");
410 this.doc.close();
411
412 // An array of known 'dirty' nodes, nodes that have been modified
413 // since they were last parsed.
414 this.dirty = [];
415
416 // dimensions
417 this.width = $(this.textarea).getDimensions().width;
418 this.height = $(this.textarea).getDimensions().height;
419
420 var content = this.textarea.value;
421
422 // hide the textarea
423 this.textarea.hide();
424
425 // Some browsers immediately produce a <body> in a new <iframe>,
426 // others only do so later and fire an onload event when they do.
427 if (this.doc.body) {
428 this.init(content);
429 } else {
430 // connect(this.iframe, "onload", bind(function(){disconnectAll(this.iframe, "onload"); this.init(content);}, this));
431 Event.observe(this.iframe, "load", function(){ this.init(content);}.bindAsEventListener(this));
432 }
433 }
434
435
436
437 t3editor.prototype = {
438
439 textModified: false, // editor-content has been modified
440 saveAjaxEvent: null, // Event for save code with ajax
441
442 // Called after we are sure that our frame has a body
443 init: function (code) {
444 this.container = this.doc.body;
445
446 // fetch key press events
447 Event.observe(this.doc, "keydown", this.keyDown.bindAsEventListener(this));
448 Event.observe(this.doc, "keyup", this.keyUp.bindAsEventListener(this));
449
450 // fetch scroll events for updateing line numbers
451 Event.observe(this.doc, "scroll", this.scroll.bindAsEventListener(this));
452 Event.observe(this.win, "scroll", this.scroll.bindAsEventListener(this));
453
454 // fetch mouse click event
455 Event.observe(this.doc, "click", this.click.bindAsEventListener(this));
456
457 // get the form object (needed for Ajax saving)
458 var form = $(this.textarea.form)
459 this.saveButtons = form.getInputs('submit', 'submit');
460
461 // initialize ajax saving events
462 this.saveAjaxEvent = this.saveAjax.bind(this);
463 this.saveButtons.each(function(button) {
464 Event.observe(button,'click',this.saveAjaxEvent);
465 }.bind(this));
466
467 // resize the editor
468 this.resize(this.width, this.height);
469
470 //Import code to editor. If code is empty the method importCode put a BR or SPAN into the codewindow - dependence on browser
471 this.importCode(code);
472
473 // set focus
474 this.win.focus();
475 var cursor = new select.Cursor(this.container);
476 cursor.focus();
477
478 },
479
480 // for demonstation only!
481 footeritem_demo_click: function() {
482 // insertNewlineAtCursor(this.win);
483
484 // focus editor and cursor
485 this.win.focus();
486 var cursor = new select.Cursor(this.container);
487 cursor.start = this.cursorObj;
488 cursor.focus();
489
490 select.insertTextAtCursor(this.win, "page = PAGE");select.insertNewlineAtCursor(this.win);
491 select.insertTextAtCursor(this.win, "page {"); select.insertNewlineAtCursor(this.win);
492 select.insertTextAtCursor(this.win, " 10 = TEXT");select.insertNewlineAtCursor(this.win);
493 select.insertTextAtCursor(this.win, " 10.value = Hello World!"); select.insertNewlineAtCursor(this.win);
494 select.insertTextAtCursor(this.win, "}"); select.insertNewlineAtCursor(this.win);
495
496 this.markCursorDirty();
497 this.scheduleHighlight();
498 },
499
500 // toggle between the textarea and t3editor
501 toggleView: function(checkboxEnabled) {
502 if (checkboxEnabled) {
503 this.textarea.value = this.getCode();
504 this.outerdiv.hide();
505 this.textarea.show();
506 this.saveButtons.each(function(button) {
507 Event.stopObserving(button,'click',this.saveAjaxEvent);
508 }.bind(this));
509 } else {
510 this.importCode(this.textarea.value);
511 this.textarea.hide();
512 this.outerdiv.show();
513 this.saveButtons.each(function(button) {
514 Event.observe(button,'click',this.saveAjaxEvent);
515 }.bind(this));
516 }
517 },
518
519 // create an item for the footer line and connect an event
520 createFooterItem: function(title, mouseover, clickAction) {
521 var item = new Element("DIV", {
522 "class": "t3e_footer_item"
523 }
524 );
525 item.innerHTML = title;
526
527 if (mouseover) {
528 item.addClassName('t3e_clickable');
529 Event.observe(item, "mouseover", function(e){Event.element(e).addClassName('t3e_footeritem_active');} );
530 Event.observe(item, "mouseout", function(e){Event.element(e).removeClassName('t3e_footeritem_active');} );
531 }
532 if (typeof clickAction == 'object') { // display an overlay
533 Event.observe(item, "click", function(e){ clickAction.toggle(); } );
534
535 } else if (typeof clickAction == 'string' && clickAction != '') { // execute a method
536 Event.observe(item, "click", function(e){ eval(clickAction); }.bindAsEventListener(this) );
537 }
538
539 return item;
540 },
541
542 // resize the editor
543 resize: function(width, height) {
544 if (this.outerdiv) {
545
546 // TODO: make it more flexible, get rid of "hardcoded" numbers!
547
548 newheight = (height - 1);
549 newwidth = (width + 11);
550 if (isMSIE) newwidth = newwidth + 8;
551
552 this.outerdiv.setStyle({
553 height: newheight,
554 width: newwidth
555 });
556
557 this.linenum_wrap.setStyle({
558 height: (height - 22) // less footer height (TODO)
559 });
560
561 numwwidth = this.linenum_wrap.getWidth();
562 if (isMSIE) numwwidth = numwwidth - 17;
563 if (!isMSIE) numwwidth = numwwidth - 11;
564
565 this.iframe.setStyle({
566 height: (height - 22), // less footer height (TODO)
567 width: (width - numwwidth)
568 });
569
570 this.modalOverlay.setStyle(this.outerdiv.getDimensions());
571 }
572 },
573
574 // toggle between normal view and fullscreen mode
575 toggleFullscreen : function() {
576 if (this.outerdiv.hasClassName('t3e_fullscreen')) {
577 // turn fullscreen off
578 this.outerdiv.removeClassName('t3e_fullscreen');
579 h = this.textarea.getDimensions().height;
580 w = this.textarea.getDimensions().width;
581
582 // hide the scrollbar of the body
583 $$('body')[0].setStyle({overflow : ''});
584
585 } else {
586 // turn fullscreen on
587 this.outerdiv.addClassName('t3e_fullscreen');
588 h = window.innerHeight ? window.innerHeight : $$('body')[0].getHeight();
589 w = window.innerWidth ? window.innerWidth : $$('body')[0].getWidth();
590
591 // TODO: proof if this is needed anymore
592 w = w - 13;
593
594 // hide the scrollbar of the body
595 $$('body')[0].setStyle({overflow : 'hidden'});
596 }
597
598 this.resize(w,h);
599 },
600
601 toggleHelp: function() {
602 this.modalOverlay.toggle();
603 this.helpOverlay.toggle();
604 },
605
606
607 //toggle AutoCompletation beetwen on and off
608 toggleAutoComplete : function() {
609 this.options.autoComplete = (this.options.autoComplete)?false:true;
610 },
611
612 //autocomplete box
613 autoComplete : function() {
614 this.clicked = false;
615 //get lastword into this.lastWord
616 this.getLastWord();
617 // init vars for up/down moving in word list
618 this.ac_up = 0;
619 this.ac_down = this.options.acWords-1;
620
621 //refresh cursorObj
622 var cursor = new select.Cursor(this.container);
623 this.cursorObj = cursor.start;
624 //init currWord, used in word list. Contain selected word
625 this.currWord = -1;
626
627 // If lastword is not empty and not space - continue
628 if (this.lastWord!='&nbsp;' && this.lastWord){
629 // get list of words
630 this.words = this.getCompleteWordsByTrigger(this.lastWord.toLowerCase());
631 // if words are found - show box
632 if (this.words.length > 0){
633 // make UL list of completation words
634 var html = '<ul>';
635 for (i=0;i<this.words.length;i++){
636 html+= '<li style="height:16px;vertical-align:middle;" id="ac_word_'+i+'" onclick="t3e_instances['+this.index+'].clicked=true;t3e_instances['+this.index+'].insertCurrWordAtCursor();" onmouseover="t3e_instances['+this.index+'].highlightCurrWord('+i+');"><span class="word_'+this.words[i].type+'">'+this.words[i].word+'</span></li>';
637 }
638 html+='</ul>';
639 //put HTML and show box
640 this.autoCompleteBox.innerHTML = html;
641 this.autoCompleteBox.show();
642 this.autoCompleteBox.scrollTop = 0;
643 // init styles
644 if (this.words.length > this.options.acWords){
645 this.autoCompleteBox.style.overflowY = 'scroll';
646 if (isGecko){
647 this.autoCompleteBox.style.height = (this.options.acWords*($("ac_word_0").offsetHeight))+'px';
648 }else{
649 this.autoCompleteBox.style.height = (this.options.acWords*($("ac_word_0").offsetHeight))+4+'px';
650 this.autoCompleteBox.style.width = this.autoCompleteBox.offsetWidth+20+'px';
651 }
652
653 }else{
654 this.autoCompleteBox.style.overflowY = 'auto';
655 this.autoCompleteBox.style.height = 'auto';
656 this.autoCompleteBox.style.width = 'auto'; // '0px';
657 }
658
659 // positioned box to word
660 this.autoCompleteBox.style.left = Position.cumulativeOffset(this.iframe)[0]-Position.cumulativeOffset(this.outerdiv)[0]+Position.cumulativeOffset(cursor.start)[0]+cursor.start.offsetWidth;
661 this.autoCompleteBox.style.top = Position.cumulativeOffset(this.iframe)[1]-Position.cumulativeOffset(this.outerdiv)[1]+Position.cumulativeOffset(cursor.start)[1]+cursor.start.offsetHeight-this.container.scrollTop;
662 // set flag to 1 - needed for continue typing word.
663 this.ac = 1;
664 //highlight first word in list
665 this.highlightCurrWord(0);
666 }
667 }
668 },
669 // Get word where cursor focused
670 getLastWord : function (){
671 var cursor = new select.Cursor(this.container);
672 if (cursor.start){
673 this.lastTrigger = this.lastWord;
674 this.lastWord = (cursor.start.innerHTML)?cursor.start.innerHTML:'';
675 }
676 },
677
678 // highlighitng word in autocomplete box by id
679 highlightCurrWord : function (id) {
680 if (this.currWord!=-1){
681 $('ac_word_'+this.currWord).className = '';
682 }
683 $('ac_word_'+id).className = 'active';
684 this.currWord = id;
685 },
686
687 //insert selected word into text from autocompletebox
688 insertCurrWordAtCursor: function (){
689 var trigger = this.lastWord;
690 var insertText = this.words[this.currWord].word;
691 //if MSIE and select word my mouse click
692 var cursor = new select.Cursor(this.container);
693 if (isMSIE && this.clicked){
694 if (trigger.length > 0){
695 this.cursorObj.innerHTML = insertText;
696 this.win.focus();
697 cursor.start = this.cursorObj;
698 cursor.focus();
699 this.highlightAtCursor(cursor);
700 }
701 }
702 // if Safari browser
703 else if (isSafari){
704 if (trigger.length > 0){
705 this.cursorObj.innerHTML = insertText;
706 if (this.clicked){
707 this.win.focus();
708 }
709 cursor.start = this.cursorObj;
710 cursor.focus();
711 this.highlightAtCursor(cursor);
712 }
713 }
714 //for all others times
715 else{
716 if (trigger.length > 0){
717 cursor.start.innerHTML = '';
718 }
719 select.insertTextAtCursor (this.win,insertText);
720 if (this.clicked){
721 this.win.focus();
722 }
723 cursor.focus();
724 this.highlightAtCursor(cursor);
725 }
726 // set ac flag to 0 - autocomplete is finish
727 this.ac = 0;
728 //hide box
729 this.autoCompleteBox.hide();
730 },
731 //return words for autocomplete by trigger (part of word)
732 getCompleteWordsByTrigger : function (trigger){
733 result = [];
734
735 for(word in typoscriptWords){
736 lword = word.toLowerCase();
737 if (lword.indexOf(trigger) === 0){
738 var wordObj = new Object();
739 wordObj.word = word;
740 wordObj.type = typoscriptWords[word];
741 result.push(wordObj);
742 }
743 }
744 return result;
745 },
746
747 //move cursor in autcomplete box up
748 autoCompleteBoxMoveUpCursor : function () {
749 // if previous position was first - then move cursor to last word if not than position --
750 if (this.currWord == 0){
751 var id = this.words.length-1;
752 }else{
753 var id = this.currWord-1;
754 }
755 // hightlight new cursor position
756 this.highlightCurrWord (id);
757 //update id of first and last showing words and scroll box
758 if (this.currWord < this.ac_up || this.currWord == (this.words.length-1)){
759 this.ac_up = this.currWord;
760 this.ac_down = this.currWord+(this.options.acWords-1);
761 if (this.ac_up === this.words.length-1){
762 this.ac_down = this.words.length-1;
763 this.ac_up = this.ac_down-(this.options.acWords-1);
764 }
765 this.autoCompleteBox.scrollTop = this.ac_up*16;
766 }
767 },
768 //move cursor in autocomplete box down
769 autoCompleteBoxMoveDownCursor : function () {
770 // if previous position was last word in list - then move cursor to first word if not than position ++
771 if (this.currWord == this.words.length-1){
772 var id = 0;
773 }else{
774 var id = this.currWord+1;
775 }
776 // hightlight new cursor position
777 this.highlightCurrWord (id);
778 //update id of first and last showing words and scroll box
779 if (this.currWord > this.ac_down || this.currWord==0){
780 this.ac_down = this.currWord;
781 this.ac_up = this.currWord-(this.options.acWords-1);
782 if (this.ac_down == 0){
783 this.ac_up = 0;
784 this.ac_down = this.options.acWords-1;
785 }
786 this.autoCompleteBox.scrollTop = this.ac_up*16;
787 }
788 },
789 // put code to history
790 pushToHistory:function () {
791 var obj = {};
792 //create SPAN mark of cursor
793 var cursorEl = this.win.document.createElement("SPAN");
794 cursorEl.id = "cursor";
795 this.refreshCursorObj();
796 // added mark to code
797 if (this.initable){
798 if (!this.cursorObj){
799 if (this.container.firstChild){
800 this.win.document.body.insertBefore(cursorEl,this.container.firstChild);
801 }
802 }else{
803 this.win.document.body.insertBefore(cursorEl,this.cursorObj);
804 }
805 }else{
806 this.win.document.body.appendChild(cursorEl);
807 }
808 //save code and text to history object
809 obj.code = this.container.innerHTML;
810
811 // TODO
812 /// obj.text = this.getCode();
813
814 // check if was undo/redo than refresh history array
815 if (this.currHistoryPosition+1 < this.history.length){
816 this.history = this.history.slice (0,this.currHistoryPosition+1);
817 this.currHistoryPosition = this.history.length-1;
818 }
819 //push history oject to history array
820 this.history.push(obj);
821 this.currHistoryPosition++;
822 //check limit of history size
823 if (this.currHistoryPosition > this.historySize){
824 this.history = this.history.slice ((this.history.length-this.historySize-1));
825 this.currHistoryPosition = this.history.length-1;
826 }
827 },
828
829 //undo function
830 undo: function () {
831 //check if position in history not first
832 if (this.currHistoryPosition > 0){
833 this.currHistoryPosition--;
834 var obj = this.history[this.currHistoryPosition];
835 if (!obj){return ;}
836 //insert code from history
837 this.container.innerHTML = obj.code;
838 //focus cursor to next el of marked span
839 var cursor = new select.Cursor(this.container);
840 var cursorEl = this.win.document.getElementById('cursor');
841 if (cursorEl){
842 cursor.start = cursorEl.nextSibling;
843 cursor.focus();
844 }
845 }
846
847 },
848
849 //redo function
850 redo: function () {
851 //check if position in history not last
852 if (this.currHistoryPosition < this.history.length){
853 this.currHistoryPosition++;
854 var obj = this.history[this.currHistoryPosition];
855 if (!obj){return ;}
856 //insert code from history
857 this.container.innerHTML = obj.code;
858 //focus cursor to next el of marked span
859 var cursor = new select.Cursor(this.container);
860 var cursorEl = this.win.document.getElementById('cursor');
861 if (cursorEl){
862 cursor.start = cursorEl.nextSibling;
863 cursor.focus();
864 }
865 }
866
867 },
868 // check changes in history
869 checkHistoryChanges:function () {
870
871 var code = this.container.innerHTML;
872 if (this.undoable == 1){
873 this.undoable = 0;
874 return ;
875 }
876 if (this.redoable == 1){
877 this.redoable = 0;
878 return ;
879 }
880 if (!this.history[this.currHistoryPosition]){
881 this.pushToHistory();
882 return ;
883 }
884 if (code != this.history[this.currHistoryPosition].code){
885 this.pushToHistory();
886 }
887
888 },
889
890 // update the line numbers
891 updateLinenum: function() {
892 var theMatch = this.container.innerHTML.match(/<br/gi);
893 if (!theMatch) {
894 theMatch = '1';
895 } else if (isMSIE) {
896 theMatch.push('1');
897 }
898
899 var bodyContentLineCount = theMatch.length;
900 disLineCount = this.linenum.childNodes.length;
901 while (disLineCount != bodyContentLineCount) {
902 if (disLineCount > bodyContentLineCount) {
903 this.linenum.removeChild(this.linenum.lastChild);
904 disLineCount--;
905 } else if (disLineCount < bodyContentLineCount) {
906 ln = $(document.createElement('dt'));
907 ln.update(disLineCount+1+'.');
908 ln.addClassName(disLineCount%2==1?'even':'odd');
909 ln.setAttribute('id','ln'+(disLineCount+1));
910 this.linenum.appendChild(ln);
911 disLineCount++;
912 }
913 }
914
915 this.fitem_status.update(bodyContentLineCount + ' lines');
916 this.fitem_name.update(this.documentname + (this.textModified?' <span alt="document has been modified">*</span>':''));
917 },
918
919 // scroll the line numbers
920 scroll: function() {
921 var scrOfX = 0, scrOfY = 0;
922 if( typeof( this.win.pageYOffset ) == 'number' ) {
923 // Netscape compliant
924 scrOfY = this.win.pageYOffset;
925 scrOfX = this.win.pageXOffset;
926 } else if( this.doc.body && ( this.doc.body.scrollLeft || this.doc.body.scrollTop ) ) {
927 // DOM compliant
928 scrOfY = this.doc.body.scrollTop;
929 scrOfX = this.doc.body.scrollLeft;
930 } else if( this.doc.documentElement && ( this.doc.documentElement.scrollLeft || this.doc.documentElement.scrollTop ) ) {
931 // IE6 standards compliant mode
932 scrOfY = this.doc.documentElement.scrollTop;
933 scrOfX = this.doc.documentElement.scrollLeft;
934 }
935 this.linenum_wrap.scrollTop = scrOfY;
936 },
937
938 // click event. Refresh cursor object. if autocomplete is not finish - finish it and hide box
939 click: function() {
940 if (this.ac === 1){this.ac = 0;this.autoCompleteBox.hide();}
941 this.refreshCursorObj();
942 },
943
944 // Split a chunk of code into lines, put them in the frame, and
945 // schedule them to be coloured.
946 importCode: function(code) {
947 while ((child = this.container.firstChild)) {
948 this.container.removeChild(child);
949 }
950
951 if (code == "\n" || code == "\r\n" || code == "\r"){code = '';}
952 var lines = code.replace(/[ \t]/g, nbsp).replace(/\r\n?/g, "\n").split("\n");
953
954 for (var i = 0; i != lines.length; i++) {
955 if (i > 0)
956 this.container.appendChild(this.win.document.createElement('BR'));
957 var line = lines[i];
958 if (line.length > 0)
959 this.container.appendChild(this.doc.createTextNode(line));
960 }
961 if (code == "") {
962 var empty = this.win.document.createElement('BR');//(isGecko && !isSafari)?this.win.document.createElement('BR'):this.win.document.createElement('SPAN');
963 this.container.appendChild(empty);
964 }
965
966 if (this.container.firstChild){
967 this.addDirtyNode(this.container.firstChild);
968 this.scheduleHighlight(); // this.highlightDirty();
969 }
970 this.updateLinenum();
971 },
972
973 // Extract the code from the editor.
974 getCode: function() {
975 if (!this.container.firstChild)
976 return "";
977
978 var accum = [];
979
980 tdom = traverseDOM(this.container.firstChild);
981 try {
982 while (tmp = tdom.next()) {
983 accum.push(tmp);
984 }
985 } catch(e) {
986 if (e != StopIteration) throw e;
987 }
988 return accum.join("").replace(nbspRegexp, " ");
989 },
990
991 // Intercept enter and any keys that are specified to re-indent
992 // the current line.
993 keyDown: function(event) {
994 var keycode = event.keyCode;
995 if (keycode == Event.KEY_RETURN) {
996 event.stop();
997 if (this.ac === 1) {
998 this.insertCurrWordAtCursor();
999 } else if (!isMac) {
1000 select.insertNewlineAtCursor(this.win);
1001 this.indentAtCursor();
1002 }
1003 this.updateLinenum();
1004
1005 } else if (keycode == 83 && event.ctrlKey) { // CTRL-S save via ajax request
1006 this.saveAjax();
1007 event.stop();
1008 return;
1009
1010 } else if (keycode == 122 && event.ctrlKey) { // CTRL-F11 toogle fullscreen mode
1011 this.toggleFullscreen();
1012 event.stop();
1013 return;
1014
1015 // TODO
1016 } else if (keycode == 32 && event.ctrlKey && this.options.autoComplete){ // CTRL-Space call autocomplete if autocomplete turn on
1017 this.autoComplete();
1018 event.stop();
1019 } else if (keycode == 38 && this.ac == 1){ // arrow up: move up cursor in autocomplete box
1020 event.stop();
1021 window.setTimeout('t3e_instances['+this.index+'].autoCompleteBoxMoveUpCursor()',100);
1022 } else if (keycode == 40 && this.ac == 1){ // Arrow down: move down cursor in autocomplete box
1023 event.stop();
1024 window.setTimeout('t3e_instances['+this.index+'].autoCompleteBoxMoveDownCursor();',100);
1025 } else if (keycode == 27 && this.ac === 1){ // Esc: if autocomplete box is showing. by ESC press it's hide and autocomplete is finish
1026 this.ac = 0;
1027 this.autoCompleteBox.hide();
1028 } else if (keycode == 90 && event.ctrlKey){
1029 this.undoable = 1;
1030 this.undo();
1031 event.stop();
1032 } else if (keycode == 89 && event.ctrlKey){
1033 this.redoable = 1;
1034 this.redo();
1035 event.stop();
1036 }
1037 },
1038
1039 // Re-indent when a key in options.reindentAfterKeys is released,
1040 // mark the node at the cursor dirty when a non-safe key is
1041 // released.
1042 keyUp: function(event) {
1043 // var name = event.key().string;
1044 var keycode = event.keyCode;
1045
1046 // TODO
1047 /*
1048 if (this.options.reindentAfterKeys.hasOwnProperty(name))
1049 this.indentAtCursor();
1050 else */
1051 // TODO
1052 if (
1053 keycode != 37
1054 && keycode != 38
1055 && keycode != 39
1056 && keycode != 40
1057 && keycode != 35
1058 && keycode != 36
1059 && keycode != 33
1060 && keycode != 34
1061 && keycode != 16
1062 && keycode != 17
1063 && keycode != 18
1064 && !event.ctrlKey) {
1065 this.markCursorDirty();
1066 this.checkTextModified();
1067 window.setTimeout('t3e_instances['+this.index+'].checkHistoryChanges();',100);
1068 }
1069
1070 if (this.ac===1){ // if autocomplete now is not finish, but started and continue typing - refresh autocomplete box
1071 this.getLastWord();
1072 if (this.lastTrigger!=this.lastWord){
1073 this.autoCompleteBox.hide();
1074 this.ac = 0;
1075 this.autoComplete();
1076 }
1077 }
1078
1079 if (keycode == Event.KEY_RETURN
1080 || keycode == Event.KEY_BACKSPACE
1081 || keycode == Event.KEY_DELETE ) {
1082 this.updateLinenum();
1083 }
1084
1085 this.refreshCursorObj();
1086 },
1087
1088 refreshCursorObj: function () {
1089 var cursor = new select.Cursor(this.container);
1090 this.cursorObj = cursor.start;
1091 },
1092
1093
1094 // check if code in editor has been modified since last saving
1095 checkTextModified: function() {
1096 if (!this.textModified) {
1097 this.textModified = true;
1098 this.updateLinenum();
1099 }
1100 },
1101
1102
1103 // send ajax request to save the code
1104 saveAjax: function(event) {
1105 if (event) {
1106 // event = new Event(event);
1107 Event.stop(event);
1108 }
1109
1110 this.modalOverlay.show();
1111 this.textarea.value = this.getCode();
1112
1113 /* erst ab prototype 1.5.1
1114 Form.request($(this.textarea.form),{
1115 onComplete: function(){ alert('Form data saved!'); }
1116 });
1117 */
1118
1119 formdata = "submitAjax=1&" + Form.serialize($(this.textarea.form));
1120
1121 var myAjax = new Ajax.Request(
1122 $(this.textarea.form).action,
1123 { method: "post",
1124 parameters: formdata,
1125 onComplete: this.saveAjaxOnSuccess.bind(this)
1126 });
1127 },
1128
1129 // callback if ajax saving was successful
1130 saveAjaxOnSuccess: function(ajaxrequest) {
1131 if (ajaxrequest.status == 200
1132 && ajaxrequest.responseText == "OK") {
1133 this.textModified = false;
1134 this.updateLinenum();
1135 } else {
1136 // TODO: handle if session is timed out
1137 alert("An error occured while saving the data.");
1138 };
1139 this.modalOverlay.hide();
1140
1141 },
1142
1143
1144 // Ensure that the start of the line the cursor is on is parsed
1145 // and coloured properly, so that the correct indentation can be
1146 // computed.
1147 highlightAtCursor: function(cursor) {
1148 if (cursor.valid) {
1149 var node = cursor.start || this.container.firstChild;
1150 if (node) {
1151 // If the node is a text node, it will be recognized as
1152 // dirty anyway, and some browsers do not allow us to add
1153 // properties to text nodes.
1154 if (node.nodeType != 3)
1155 node.dirty = true;
1156 // Store selection, highlight, restore selection.
1157 var sel = select.markSelection(this.win);
1158 this.highlight(node);
1159 select.selectMarked(sel);
1160 // Cursor information is probably no longer valid after
1161 // highlighting.
1162 cursor = new select.Cursor(this.container);
1163 }
1164 }
1165 return cursor;
1166 },
1167
1168 // Adjust the amount of whitespace at the start of the line that
1169 // the cursor is on so that it is indented properly.
1170 indentAtCursor: function() {
1171 var cursor = new select.Cursor(this.container);
1172 // The line has to have up-to-date lexical information, so we
1173 // highlight it first.
1174 cursor = this.highlightAtCursor(cursor);
1175 // If we couldn't determine the place of the cursor, there's
1176 // nothing to indent.
1177 if (!cursor.valid)
1178 return;
1179
1180 // start is the <br> before the current line, or null if this is
1181 // the first line.
1182 var start = cursor.startOfLine();
1183 // whiteSpace is the whitespace span at the start of the line,
1184 // or null if there is no such node.
1185 var whiteSpace = start ? start.nextSibling : this.container.lastChild;
1186 if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
1187 whiteSpace = null;
1188
1189 // Sometimes the first character on a line can influence the
1190 // correct indentation, so we retrieve it.
1191 var firstText = whiteSpace ? whiteSpace.nextSibling : start ? start.nextSibling : this.container.firstChild;
1192 var firstChar = (start && firstText && firstText.currentText) ? firstText.currentText.charAt(0) : "";
1193
1194 // Ask the lexical context for the correct indentation, and
1195 // compute how much this differs from the current indentation.
1196
1197 var indent = start ? start.lexicalContext.indentation(firstChar) : 0;
1198 var indentDiff = indent - (whiteSpace ? whiteSpace.currentText.length : 0);
1199
1200 // If there is too much, this is just a matter of shrinking a span.
1201 if (indentDiff < 0) {
1202 whiteSpace.currentText = repeatString(nbsp, indent);
1203 whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
1204 }
1205 // Not enough...
1206 else if (indentDiff > 0) {
1207 // If there is whitespace, we grow it.
1208 if (whiteSpace) {
1209 whiteSpace.currentText += repeatString(nbsp, indentDiff);
1210 whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
1211 }
1212 // Otherwise, we have to add a new whitespace node.
1213 else {
1214 whiteSpace = new Element('SPAN',{"class": "whitespace part"});
1215 whiteSpace.innerHTML = repeatString(nbsp, indentDiff);
1216 if (start)
1217 insertAfter(whiteSpace, start);
1218 else
1219 insertAtStart(whiteSpace, this.containter);
1220 }
1221 // If the cursor is at the start of the line, move it to after
1222 // the whitespace.
1223 if (cursor.start == start)
1224 cursor.start = whiteSpace;
1225 }
1226
1227 if (cursor.start == whiteSpace)
1228 cursor.focus();
1229 },
1230
1231 // highlight is a huge function defined below.
1232 highlight: highlight,
1233
1234 // Find the node that the cursor is in, mark it as dirty, and make
1235 // sure a highlight pass is scheduled.
1236 markCursorDirty: function() {
1237 var cursor = new select.Cursor(this.container);
1238 if (cursor.valid) {
1239 var node = cursor.start || this.container.firstChild;
1240 if (node) {
1241 this.addDirtyNode(node);
1242 this.scheduleHighlight();
1243 }
1244 }
1245 },
1246
1247 // Add a node to the set of dirty nodes, if it isn't already in
1248 // there.
1249 addDirtyNode: function(node) {
1250 if (this.dirty.indexOf(node) == -1) {
1251 if (node.nodeType != 3)
1252 node.dirty = true;
1253 this.dirty.push(node);
1254 }
1255 },
1256
1257 // Cause a highlight pass to happen in options.passDelay
1258 // milliseconds. Clear the existing timeout, if one exists. This
1259 // way, the passes do not happen while the user is typing, and
1260 // should as unobtrusive as possible.
1261 scheduleHighlight: function() {
1262 if (this.highlightTimeout) clearTimeout(this.highlightTimeout);
1263 this.highlightTimeout = setTimeout(this.highlightDirty.bind(this), this.options.passDelay);
1264
1265 },
1266
1267 // Fetch one dirty node, and remove it from the dirty set.
1268 getDirtyNode: function() {
1269 while (this.dirty.length > 0) {
1270 var found = this.dirty.pop();
1271 // If the node has been coloured in the meantime, or is no
1272 // longer in the document, it should not be returned.
1273 if ((found.dirty || found.nodeType == 3) && found.parentNode)
1274 return found;
1275 }
1276 return null;
1277 },
1278
1279 // Pick dirty nodes, and highlight them, until
1280 // options.linesPerPass lines have been highlighted. The highlight
1281 // method will continue to next lines as long as it finds dirty
1282 // nodes. It returns an object indicating the amount of lines
1283 // left, and information about the place where it stopped. If
1284 // there are dirty nodes left after this function has spent all
1285 // its lines, it shedules another highlight to finish the job.
1286 highlightDirty: function() {
1287 var lines = this.options.linesPerPass;
1288 var sel = select.markSelection(this.win);
1289 var start;
1290 while (lines > 0 && (start = this.getDirtyNode())){
1291 var result = this.highlight(start, lines);
1292 if (result) {
1293 lines = result.left;
1294 if (result.node && result.dirty)
1295 this.addDirtyNode(result.node);
1296 }
1297 }
1298 select.selectMarked(sel);
1299 if (start)
1300 this.scheduleHighlight();
1301 }
1302 }
1303
1304 // The function that does the actual highlighting/colouring (with
1305 // help from the parser and the DOM normalizer). Its interface is
1306 // rather overcomplicated, because it is used in different
1307 // situations: ensuring that a certain line is highlighted, or
1308 // highlighting up to X lines starting from a certain point. The
1309 // 'from' argument gives the node at which it should start. If this
1310 // is null, it will start at the beginning of the frame. When a
1311 // number of lines is given with the 'lines' argument, it will colour
1312 // no more than that amount. If at any time it comes across a
1313 // 'clean' line (no dirty nodes), it will stop.
1314 function highlight(from, lines){
1315 var container = this.container;
1316 var document = this.doc;
1317 // this.updateLinenum();
1318
1319 if (!container.firstChild)
1320 return;
1321 // Backtrack to the first node before from that has a partial
1322 // parse stored.
1323 while (from && !from.parserFromHere)
1324 from = from.previousSibling;
1325 // If we are at the end of the document, do nothing.
1326 if (from && !from.nextSibling)
1327 return;
1328
1329 // Check whether a part (<span> node) and the corresponding token
1330 // match.
1331 function correctPart(token, part){
1332 return !part.reduced && part.currentText == token.value && hasClass(part, token.style);
1333 }
1334 // Shorten the text associated with a part by chopping off
1335 // characters from the front. Note that only the currentText
1336 // property gets changed. For efficiency reasons, we leave the
1337 // nodeValue alone -- we set the reduced flag to indicate that
1338 // this part must be replaced.
1339 function shortenPart(part, minus){
1340 part.currentText = part.currentText.substring(minus);
1341 part.reduced = true;
1342 }
1343 // Create a part corresponding to a given token.
1344 function tokenPart(token){
1345 var part = new Element('SPAN',{"class": "part " + token.style});
1346 part.update(token.value);
1347 part.currentText = token.value;
1348 part.innerHTML = token.value;
1349 return part;
1350 }
1351
1352 // Get the token stream. If from is null, we start with a new
1353 // parser from the start of the frame, otherwise a partial parse
1354 // is resumed.
1355 var parsed = from ? from.parserFromHere(multiStringStream(traverseDOM(from.nextSibling)))
1356 : this.options.parser(multiStringStream(traverseDOM(container.firstChild)));
1357
1358 // parts is a wrapper that makes it possible to 'delay' going to
1359 // the next DOM node until we are completely done with the one
1360 // before it. This is necessary because we are constantly poking
1361 // around in the DOM tree, and if the next node is fetched to
1362 // early it might get replaced before it is used.
1363 var parts = {
1364 current: null,
1365 forward: false,
1366 // Get the current part.
1367 get: function(){
1368 if (!this.current)
1369 this.current = from ? from.nextSibling : container.firstChild;
1370 else if (this.forward)
1371 this.current = this.current.nextSibling;
1372 this.forward = false;
1373 return this.current;
1374 },
1375 // Advance to the next part (do not fetch it yet).
1376 next: function(){
1377 if (this.forward)
1378 this.get();
1379 this.forward = true;
1380 },
1381 // Remove the current part from the DOM tree, and move to the
1382 // next.
1383 remove: function(){
1384 this.current = this.get().previousSibling;
1385 container.removeChild(this.current ? this.current.nextSibling : container.firstChild);
1386 this.forward = true;
1387 },
1388 // Advance to the next part that is not empty, discarding empty
1389 // parts.
1390 nextNonEmpty: function(){
1391 var part = this.get();
1392 while (part.nodeName == "SPAN" && part.currentText == ""){
1393 var old = part;
1394 this.remove();
1395 part = this.get();
1396 // Adjust selection information, if any. See select.js for
1397 // details.
1398 select.replaceSelection(old.firstChild, part.firstChild || part, 0, 0);
1399 }
1400 return part;
1401 }
1402 };
1403
1404 var lineDirty = false;
1405
1406 // This forEach loops over the tokens from the parsed stream, and
1407 // at the same time uses the parts object to proceed through the
1408 // corresponding DOM nodes.
1409 try {
1410 while (true) { // stopped by StopIteration
1411 token = parsed.next();
1412 var part = parts.nextNonEmpty();
1413 if (token.value == "\n"){
1414 // The idea of the two streams actually staying synchronized
1415 // is such a long shot that we explicitly check.
1416 if (part.nodeName != "BR")
1417 throw "Parser out of sync. Expected BR.";
1418 if (part.dirty || !part.lexicalContext)
1419 lineDirty = true;
1420 // Every <br> gets a copy of the parser state and a lexical
1421 // context assigned to it. The first is used to be able to
1422 // later resume parsing from this point, the second is used
1423 // for indentation.
1424 part.parserFromHere = parsed.copy();
1425 part.lexicalContext = token.lexicalContext;
1426 part.dirty = false;
1427 // A clean line means we are done. Throwing a StopIteration is
1428 // the way to break out of a MochiKit forEach loop.
1429 if ((lines !== undefined && --lines <= 0) || !lineDirty)
1430 throw StopIteration;
1431 lineDirty = false;
1432 parts.next();
1433 }
1434 else {
1435 if (part.nodeName != "SPAN")
1436 throw "Parser out of sync. Expected SPAN.";
1437 if (part.dirty)
1438 lineDirty = true;
1439
1440 // If the part matches the token, we can leave it alone.
1441 if (correctPart(token, part)){
1442 part.dirty = false;
1443 parts.next();
1444 }
1445 // Otherwise, we have to fix it.
1446 else {
1447 lineDirty = true;
1448 // Insert the correct part.
1449 var newPart = tokenPart(token);
1450 container.insertBefore(newPart, part);
1451 var tokensize = token.value.length;
1452 var offset = 0;
1453 // Eat up parts until the text for this token has been
1454 // removed, adjusting the stored selection info (see
1455 // select.js) in the process.
1456 while (tokensize > 0) {
1457 part = parts.get();
1458 var partsize = part.currentText.length;
1459 select.replaceSelection(part.firstChild, newPart.firstChild, tokensize, offset);
1460 if (partsize > tokensize){
1461 shortenPart(part, tokensize);
1462 tokensize = 0;
1463 }
1464 else {
1465 tokensize -= partsize;
1466 offset += partsize;
1467 parts.remove();
1468 }
1469 }
1470 }
1471 }
1472 }// while
1473 } catch (e) {
1474 if (e != StopIteration) {
1475 throw e;
1476 }
1477 }
1478 this.refreshCursorObj();
1479 this.initable = 1;
1480
1481 // window.setTimeout('t3e_instances['+this.index+'].checkHistoryChanges();',100);
1482
1483 // The function returns some status information that is used by
1484 // hightlightDirty to determine whether and where it has to
1485 // continue.
1486 return {left: lines,
1487 node: parts.get(),
1488 dirty: lineDirty};
1489 }
1490
1491 return t3editor;
1492 }();
1493
1494
1495 // ------------------------------------------------------------------------
1496
1497
1498
1499 function t3editor_toggleEditor(checkbox,index) {
1500 if (index == undefined) {
1501 $$('textarea.t3editor').each(
1502 function(textarea,i) {
1503 t3editor_toggleEditor(checkbox,i);
1504 }
1505 );
1506 } else {
1507 if (t3e_instances[index] != undefined) {
1508 var t3e = t3e_instances[index];
1509 t3e.toggleView(checkbox.checked);
1510 } else if (!checkbox.checked) {
1511 var t3e = new t3editor($$('textarea.t3editor')[index], index);
1512 t3e_instances[index] = t3e;
1513 }
1514 }
1515 }
1516
1517 // ------------------------------------------------------------------------
1518
1519
1520 /**
1521 * everything ready: turn textareas into fancy editors
1522 */
1523 Event.observe(window,'load',function() {
1524 $$('textarea.t3editor').each(
1525 function(textarea,i) {
1526 if ($('t3editor_disableEditor_'+(i+1)+'_checkbox') && !$('t3editor_disableEditor_'+(i+1)+'_checkbox').checked) {
1527 var t3e = new t3editor(textarea,i);
1528 t3e_instances[i] = t3e;
1529 }
1530 }
1531 );
1532 });