a35879010228be11f8d2f62b45b7ce27f1579ab8
1 /***************************************************************
4 * (c) 2007 Tobias Liebig <mail_typo3@etobi.de>
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.
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.
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.
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
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;
50 // collection of all t3editor instances on the current page
51 var t3e_instances
= {};
55 /* CodeMirror main module
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.
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
67 var t3eOptions
= window
.t3eOptions
|| {};
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
88 safeKeys
: { "KEY_ARROW_UP":true,
89 "KEY_ARROW_DOWN":true,
90 "KEY_ARROW_LEFT":true,
91 "KEY_ARROW_RIGHT":true,
101 reindentKeys
: {"KEY_TAB":true},
102 reindentAfterKeys
: {"KEY_RIGHT_SQUARE_BRACKET":true},
103 stylesheet
: PATH_t3e
+"css/t3editor.css",
104 parser
: parseTypoScript
,
110 // These default options can be overridden by passing a set of options
111 // to a specific CodeMirror constructor.
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};
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
;
125 function simplifyNode(node
) {
128 if (node
.nodeType
== 3) {
129 node
.nodeValue
= node
.nodeValue
.replace(/[\n\r]/g, "").replace(/[\t ]/g, nbsp
);
132 else if (node
.nodeName
== "BR" && node
.childNodes
.length
== 0) {
136 // forEach(node.childNodes, simplifyNode);
137 $A(node
.childNodes
).each(simplifyNode
);
138 if (!leaving
&& newlineElements
.hasOwnProperty(node
.nodeName
)) {
140 el
= new Element('SPAN');
141 result
.push(new Element('BR'));
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
;
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
;
170 return function(newnode
){parent
.insertBefore(newnode
, next
);};
172 return function(newnode
){parent
.appendChild(newnode
);};
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
){
184 if (part
.nodeType
== 3) {
185 text
= part
.nodeValue
;
187 newpart
= new Element('SPAN',{"class": "part"});
188 newpart
.appendChild(part
);
190 part
.currentText
= text
;
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
){
202 var simdom
= simplifyDOM(node
);
205 toYield
.push(insertPart(part
));
209 return yield(toYield
.join(""), c
);
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
;
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
);
230 return yield(node
.currentText
, c
);
232 else if (node
.nodeName
== "BR") {
233 return yield("\n", c
);
236 point
= pointAt(node
);
237 Element
.remove(node
);
238 return writeNode(node
, c
);
242 // MochiKit iterators are objects with a next function that
243 // returns the next value or throws StopIteration when there are
245 return {next: function(){return cc();}};
248 var nbspRegexp
= new RegExp(nbsp
, "g");
252 function t3editor(theTextarea
, index
, options
) {
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
];
263 this.historySize
= 100;
264 //Init history position
265 this.currHistoryPosition
= -1;
267 // memorize the textarea
268 this.textarea
= $(theTextarea
);
270 this.documentname
= this.textarea
.readAttribute('alt');
272 // count index (helpful if more than one editor is on the page)
275 // create the wrapping div
276 this.outerdiv
= new Element("div", {
277 "class": "t3e_outerdiv",
278 "id": "t3e_"+this.textarea
.getAttribute('id')
282 // place the div before the textarea
283 this.textarea
.parentNode
.insertBefore(this.outerdiv
,$(this.textarea
));
285 // an overlay that covers the whole editor
286 this.modalOverlay
= new Element("DIV", {
287 "class": "t3e_modalOverlay",
288 "id": "t3e_modalOverlay_wait"
291 this.modalOverlay
.hide();
292 this.modalOverlay
.setStyle(this.outerdiv
.getDimensions());
293 this.modalOverlay
.setStyle({opacity
: 0.5});
294 this.outerdiv
.appendChild(this.modalOverlay
);
296 this.helpOverlay
= new Element("DIV", {
297 "class": "t3e_modalOverlay",
298 "id": "t3e_modalOverlay_help"
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/>"+
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/>"+
312 "<p><a href='javascript:void(0)' onclick='t3e_instances["+this.index
+"].toggleHelp();'>click here to close this help window</a></p>"+
314 this.helpOverlay
.hide();
315 this.outerdiv
.appendChild(this.helpOverlay
);
317 // wrapping the linenumbers
318 this.linenum_wrap
= new Element("DIV", {
319 "class": "t3e_linenum_wrap"
322 // the "linenumber" list itself
323 this.linenum
= new Element("DL", {
324 "class": "t3e_linenum"
327 this.linenum_wrap
.appendChild(this.linenum
);
328 this.outerdiv
.appendChild(this.linenum_wrap
);
331 this.autoCompleteBox
= new Element("DIV",{
332 "class": "t3e_autoCompleteBox"
335 this.autoCompleteBox
.hide();
336 this.outerdiv
.appendChild(this.autoCompleteBox
);
338 // wrapping the iframe
339 this.iframe_wrap
= new Element("DIV", {
340 "class": "t3e_iframe_wrap"
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"
353 this.iframe_wrap
.appendChild(this.iframe
);
354 this.outerdiv
.appendChild(this.iframe_wrap
);
356 // wrapping the footer/statusline
357 this.footer_wrap
= new Element("DIV", {
358 "class": "t3e_footer_wrap"
361 this.outerdiv
.appendChild(this.footer_wrap
);
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
);
368 // footer item: options menu
369 this.fitem_options_overlay
= new Element("DIV", {
370 "class": "t3e_footer_overlay",
371 "id": "t3e_footer_overlay_options"
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>'+
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
);
388 // footer item: status field (total line numbers)
389 this.fitem_status
= this.createFooterItem('', false);
390 this.footer_wrap
.appendChild(this.fitem_status
);
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
);
396 // window and document objects from the iframe
397 this.win
= this.iframe
.contentWindow
;
398 this.doc
= this.win
.document
;
400 // make the iframe "editable"
401 this.doc
.designMode
= "on";
406 "<link rel=\"stylesheet\" type=\"text/css\" href=\"" +
407 t3eOptions
.stylesheet
+
409 "<body class=\"editbox\" spellcheck=\"false\"></body></html>");
412 // An array of known 'dirty' nodes, nodes that have been modified
413 // since they were last parsed.
417 this.width
= $(this.textarea
).getDimensions().width
;
418 this.height
= $(this.textarea
).getDimensions().height
;
420 var content
= this.textarea
.value
;
423 this.textarea
.hide();
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.
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));
437 t3editor
.prototype = {
439 textModified
: false, // editor-content has been modified
440 saveAjaxEvent
: null, // Event for save code with ajax
442 // Called after we are sure that our frame has a body
443 init: function (code
) {
444 this.container
= this.doc
.body
;
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));
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));
454 // fetch mouse click event
455 Event
.observe(this.doc
, "click", this.click
.bindAsEventListener(this));
457 // get the form object (needed for Ajax saving)
458 var form
= $(this.textarea
.form
)
459 this.saveButtons
= form
.getInputs('submit', 'submit');
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
);
468 this.resize(this.width
, this.height
);
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
);
475 var cursor
= new select
.Cursor(this.container
);
480 // for demonstation only!
481 footeritem_demo_click: function() {
482 // insertNewlineAtCursor(this.win);
484 // focus editor and cursor
486 var cursor
= new select
.Cursor(this.container
);
487 cursor
.start
= this.cursorObj
;
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
);
496 this.markCursorDirty();
497 this.scheduleHighlight();
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
);
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
);
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"
525 item
.innerHTML
= title
;
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');} );
532 if (typeof clickAction
== 'object') { // display an overlay
533 Event
.observe(item
, "click", function(e
){ clickAction
.toggle(); } );
535 } else if (typeof clickAction
== 'string' && clickAction
!= '') { // execute a method
536 Event
.observe(item
, "click", function(e
){ eval(clickAction
); }.bindAsEventListener(this) );
543 resize: function(width
, height
) {
546 // TODO: make it more flexible, get rid of "hardcoded" numbers!
548 newheight
= (height
- 1);
549 newwidth
= (width
+ 11);
550 if (isMSIE
) newwidth
= newwidth
+ 8;
552 this.outerdiv
.setStyle({
557 this.linenum_wrap
.setStyle({
558 height
: (height
- 22) // less footer height (TODO)
561 numwwidth
= this.linenum_wrap
.getWidth();
562 if (isMSIE
) numwwidth
= numwwidth
- 17;
563 if (!isMSIE
) numwwidth
= numwwidth
- 11;
565 this.iframe
.setStyle({
566 height
: (height
- 22), // less footer height (TODO)
567 width
: (width
- numwwidth
)
570 this.modalOverlay
.setStyle(this.outerdiv
.getDimensions());
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
;
582 // hide the scrollbar of the body
583 $$('body')[0].setStyle({overflow
: ''});
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();
591 // TODO: proof if this is needed anymore
594 // hide the scrollbar of the body
595 $$('body')[0].setStyle({overflow
: 'hidden'});
601 toggleHelp: function() {
602 this.modalOverlay
.toggle();
603 this.helpOverlay
.toggle();
607 //toggle AutoCompletation beetwen on and off
608 toggleAutoComplete : function() {
609 this.options
.autoComplete
= (this.options
.autoComplete
)?false:true;
613 autoComplete : function() {
614 this.clicked
= false;
615 //get lastword into this.lastWord
617 // init vars for up/down moving in word list
619 this.ac_down
= this.options
.acWords
-1;
622 var cursor
= new select
.Cursor(this.container
);
623 this.cursorObj
= cursor
.start
;
624 //init currWord, used in word list. Contain selected word
627 // If lastword is not empty and not space - continue
628 if (this.lastWord
!=' ' && this.lastWord
){
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
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>';
639 //put HTML and show box
640 this.autoCompleteBox
.innerHTML
= html
;
641 this.autoCompleteBox
.show();
642 this.autoCompleteBox
.scrollTop
= 0;
644 if (this.words
.length
> this.options
.acWords
){
645 this.autoCompleteBox
.style
.overflowY
= 'scroll';
647 this.autoCompleteBox
.style
.height
= (this.options
.acWords
*($("ac_word_0").offsetHeight
))+'px';
649 this.autoCompleteBox
.style
.height
= (this.options
.acWords
*($("ac_word_0").offsetHeight
))+4+'px';
650 this.autoCompleteBox
.style
.width
= this.autoCompleteBox
.offsetWidth
+20+'px';
654 this.autoCompleteBox
.style
.overflowY
= 'auto';
655 this.autoCompleteBox
.style
.height
= 'auto';
656 this.autoCompleteBox
.style
.width
= 'auto'; // '0px';
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.
664 //highlight first word in list
665 this.highlightCurrWord(0);
669 // Get word where cursor focused
670 getLastWord : function (){
671 var cursor
= new select
.Cursor(this.container
);
673 this.lastTrigger
= this.lastWord
;
674 this.lastWord
= (cursor
.start
.innerHTML
)?cursor
.start
.innerHTML
:'';
678 // highlighitng word in autocomplete box by id
679 highlightCurrWord : function (id
) {
680 if (this.currWord
!=-1){
681 $('ac_word_'+this.currWord
).className
= '';
683 $('ac_word_'+id
).className
= 'active';
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
;
697 cursor
.start
= this.cursorObj
;
699 this.highlightAtCursor(cursor
);
704 if (trigger
.length
> 0){
705 this.cursorObj
.innerHTML
= insertText
;
709 cursor
.start
= this.cursorObj
;
711 this.highlightAtCursor(cursor
);
714 //for all others times
716 if (trigger
.length
> 0){
717 cursor
.start
.innerHTML
= '';
719 select
.insertTextAtCursor (this.win
,insertText
);
724 this.highlightAtCursor(cursor
);
726 // set ac flag to 0 - autocomplete is finish
729 this.autoCompleteBox
.hide();
731 //return words for autocomplete by trigger (part of word)
732 getCompleteWordsByTrigger : function (trigger
){
735 for(word
in typoscriptWords
){
736 lword
= word
.toLowerCase();
737 if (lword
.indexOf(trigger
) === 0){
738 var wordObj
= new Object();
740 wordObj
.type
= typoscriptWords
[word
];
741 result
.push(wordObj
);
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;
753 var id
= this.currWord
-1;
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);
765 this.autoCompleteBox
.scrollTop
= this.ac_up
*16;
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){
774 var id
= this.currWord
+1;
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){
784 this.ac_down
= this.options
.acWords
-1;
786 this.autoCompleteBox
.scrollTop
= this.ac_up
*16;
789 // put code to history
790 pushToHistory:function () {
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
798 if (!this.cursorObj
){
799 if (this.container
.firstChild
){
800 this.win
.document
.body
.insertBefore(cursorEl
,this.container
.firstChild
);
803 this.win
.document
.body
.insertBefore(cursorEl
,this.cursorObj
);
806 this.win
.document
.body
.appendChild(cursorEl
);
808 //save code and text to history object
809 obj
.code
= this.container
.innerHTML
;
812 /// obj.text = this.getCode();
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;
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;
831 //check if position in history not first
832 if (this.currHistoryPosition
> 0){
833 this.currHistoryPosition
--;
834 var obj
= this.history
[this.currHistoryPosition
];
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');
842 cursor
.start
= cursorEl
.nextSibling
;
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
];
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');
862 cursor
.start
= cursorEl
.nextSibling
;
868 // check changes in history
869 checkHistoryChanges:function () {
871 var code
= this.container
.innerHTML
;
872 if (this.undoable
== 1){
876 if (this.redoable
== 1){
880 if (!this.history
[this.currHistoryPosition
]){
881 this.pushToHistory();
884 if (code
!= this.history
[this.currHistoryPosition
].code
){
885 this.pushToHistory();
890 // update the line numbers
891 updateLinenum: function() {
892 var theMatch
= this.container
.innerHTML
.match(/<br/gi);
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
);
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
);
915 this.fitem_status
.update(bodyContentLineCount
+ ' lines');
916 this.fitem_name
.update(this.documentname
+ (this.textModified
?' <span alt="document has been modified">*</span>':''));
919 // scroll the line numbers
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
) ) {
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
;
935 this.linenum_wrap
.scrollTop
= scrOfY
;
938 // click event. Refresh cursor object. if autocomplete is not finish - finish it and hide box
940 if (this.ac
=== 1){this.ac
= 0;this.autoCompleteBox
.hide();}
941 this.refreshCursorObj();
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
);
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");
954 for (var i
= 0; i
!= lines
.length
; i
++) {
956 this.container
.appendChild(this.win
.document
.createElement('BR'));
959 this.container
.appendChild(this.doc
.createTextNode(line
));
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
);
966 if (this.container
.firstChild
){
967 this.addDirtyNode(this.container
.firstChild
);
968 this.scheduleHighlight(); // this.highlightDirty();
970 this.updateLinenum();
973 // Extract the code from the editor.
974 getCode: function() {
975 if (!this.container
.firstChild
)
980 tdom
= traverseDOM(this.container
.firstChild
);
982 while (tmp
= tdom
.next()) {
986 if (e
!= StopIteration
) throw e
;
988 return accum
.join("").replace(nbspRegexp
, " ");
991 // Intercept enter and any keys that are specified to re-indent
993 keyDown: function(event
) {
994 var keycode
= event
.keyCode
;
995 if (keycode
== Event
.KEY_RETURN
) {
998 this.insertCurrWordAtCursor();
1000 select
.insertNewlineAtCursor(this.win
);
1001 this.indentAtCursor();
1003 this.updateLinenum();
1005 } else if (keycode
== 83 && event
.ctrlKey
) { // CTRL-S save via ajax request
1010 } else if (keycode
== 122 && event
.ctrlKey
) { // CTRL-F11 toogle fullscreen mode
1011 this.toggleFullscreen();
1016 } else if (keycode
== 32 && event
.ctrlKey
&& this.options
.autoComplete
){ // CTRL-Space call autocomplete if autocomplete turn on
1017 this.autoComplete();
1019 } else if (keycode
== 38 && this.ac
== 1){ // arrow up: move up cursor in autocomplete box
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
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
1027 this.autoCompleteBox
.hide();
1028 } else if (keycode
== 90 && event
.ctrlKey
){
1032 } else if (keycode
== 89 && event
.ctrlKey
){
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
1042 keyUp: function(event
) {
1043 // var name = event.key().string;
1044 var keycode
= event
.keyCode
;
1048 if (this.options.reindentAfterKeys.hasOwnProperty(name))
1049 this.indentAtCursor();
1064 && !event
.ctrlKey
) {
1065 this.markCursorDirty();
1066 this.checkTextModified();
1067 window
.setTimeout('t3e_instances['+this.index
+'].checkHistoryChanges();',100);
1070 if (this.ac
===1){ // if autocomplete now is not finish, but started and continue typing - refresh autocomplete box
1072 if (this.lastTrigger
!=this.lastWord
){
1073 this.autoCompleteBox
.hide();
1075 this.autoComplete();
1079 if (keycode
== Event
.KEY_RETURN
1080 || keycode
== Event
.KEY_BACKSPACE
1081 || keycode
== Event
.KEY_DELETE
) {
1082 this.updateLinenum();
1085 this.refreshCursorObj();
1088 refreshCursorObj: function () {
1089 var cursor
= new select
.Cursor(this.container
);
1090 this.cursorObj
= cursor
.start
;
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();
1103 // send ajax request to save the code
1104 saveAjax: function(event
) {
1106 // event = new Event(event);
1110 this.modalOverlay
.show();
1111 this.textarea
.value
= this.getCode();
1113 /* erst ab prototype 1.5.1
1114 Form.request($(this.textarea.form),{
1115 onComplete: function(){ alert('Form data saved!'); }
1119 formdata
= "submitAjax=1&" + Form
.serialize($(this.textarea
.form
));
1121 var myAjax
= new Ajax
.Request(
1122 $(this.textarea
.form
).action
,
1124 parameters
: formdata
,
1125 onComplete
: this.saveAjaxOnSuccess
.bind(this)
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();
1136 // TODO: handle if session is timed out
1137 alert("An error occured while saving the data.");
1139 this.modalOverlay
.hide();
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
1147 highlightAtCursor: function(cursor
) {
1149 var node
= cursor
.start
|| this.container
.firstChild
;
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)
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
1162 cursor
= new select
.Cursor(this.container
);
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.
1180 // start is the <br> before the current line, or null if this is
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"))
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) : "";
1194 // Ask the lexical context for the correct indentation, and
1195 // compute how much this differs from the current indentation.
1197 var indent
= start
? start
.lexicalContext
.indentation(firstChar
) : 0;
1198 var indentDiff
= indent
- (whiteSpace
? whiteSpace
.currentText
.length
: 0);
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
;
1206 else if (indentDiff
> 0) {
1207 // If there is whitespace, we grow it.
1209 whiteSpace
.currentText
+= repeatString(nbsp
, indentDiff
);
1210 whiteSpace
.firstChild
.nodeValue
= whiteSpace
.currentText
;
1212 // Otherwise, we have to add a new whitespace node.
1214 whiteSpace
= new Element('SPAN',{"class": "whitespace part"});
1215 whiteSpace
.innerHTML
= repeatString(nbsp
, indentDiff
);
1217 insertAfter(whiteSpace
, start
);
1219 insertAtStart(whiteSpace
, this.containter
);
1221 // If the cursor is at the start of the line, move it to after
1223 if (cursor
.start
== start
)
1224 cursor
.start
= whiteSpace
;
1227 if (cursor
.start
== whiteSpace
)
1231 // highlight is a huge function defined below.
1232 highlight
: highlight
,
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
);
1239 var node
= cursor
.start
|| this.container
.firstChild
;
1241 this.addDirtyNode(node
);
1242 this.scheduleHighlight();
1247 // Add a node to the set of dirty nodes, if it isn't already in
1249 addDirtyNode: function(node
) {
1250 if (this.dirty
.indexOf(node
) == -1) {
1251 if (node
.nodeType
!= 3)
1253 this.dirty
.push(node
);
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
);
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
)
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
);
1290 while (lines
> 0 && (start
= this.getDirtyNode())){
1291 var result
= this.highlight(start
, lines
);
1293 lines
= result
.left
;
1294 if (result
.node
&& result
.dirty
)
1295 this.addDirtyNode(result
.node
);
1298 select
.selectMarked(sel
);
1300 this.scheduleHighlight();
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();
1319 if (!container
.firstChild
)
1321 // Backtrack to the first node before from that has a partial
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
)
1329 // Check whether a part (<span> node) and the corresponding token
1331 function correctPart(token
, part
){
1332 return !part
.reduced
&& part
.currentText
== token
.value
&& hasClass(part
, token
.style
);
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;
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
;
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
1355 var parsed
= from ? from.parserFromHere(multiStringStream(traverseDOM(from.nextSibling
)))
1356 : this.options
.parser(multiStringStream(traverseDOM(container
.firstChild
)));
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.
1366 // Get the current part.
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
;
1375 // Advance to the next part (do not fetch it yet).
1379 this.forward
= true;
1381 // Remove the current part from the DOM tree, and move to the
1384 this.current
= this.get().previousSibling
;
1385 container
.removeChild(this.current
? this.current
.nextSibling
: container
.firstChild
);
1386 this.forward
= true;
1388 // Advance to the next part that is not empty, discarding empty
1390 nextNonEmpty: function(){
1391 var part
= this.get();
1392 while (part
.nodeName
== "SPAN" && part
.currentText
== ""){
1396 // Adjust selection information, if any. See select.js for
1398 select
.replaceSelection(old
.firstChild
, part
.firstChild
|| part
, 0, 0);
1404 var lineDirty
= false;
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.
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
)
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
1424 part
.parserFromHere
= parsed
.copy();
1425 part
.lexicalContext
= token
.lexicalContext
;
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
;
1435 if (part
.nodeName
!= "SPAN")
1436 throw "Parser out of sync. Expected SPAN.";
1440 // If the part matches the token, we can leave it alone.
1441 if (correctPart(token
, part
)){
1445 // Otherwise, we have to fix it.
1448 // Insert the correct part.
1449 var newPart
= tokenPart(token
);
1450 container
.insertBefore(newPart
, part
);
1451 var tokensize
= token
.value
.length
;
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) {
1458 var partsize
= part
.currentText
.length
;
1459 select
.replaceSelection(part
.firstChild
, newPart
.firstChild
, tokensize
, offset
);
1460 if (partsize
> tokensize
){
1461 shortenPart(part
, tokensize
);
1465 tokensize
-= partsize
;
1474 if (e
!= StopIteration
) {
1478 this.refreshCursorObj();
1481 // window.setTimeout('t3e_instances['+this.index+'].checkHistoryChanges();',100);
1483 // The function returns some status information that is used by
1484 // hightlightDirty to determine whether and where it has to
1486 return {left
: lines
,
1495 // ------------------------------------------------------------------------
1499 function t3editor_toggleEditor(checkbox
,index
) {
1500 if (index
== undefined) {
1501 $$('textarea.t3editor').each(
1502 function(textarea
,i
) {
1503 t3editor_toggleEditor(checkbox
,i
);
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
;
1517 // ------------------------------------------------------------------------
1521 * everything ready: turn textareas into fancy editors
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
;