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