git-svn-id: https://svn.typo3.org/TYPO3v4/Core/trunk@1771 709f56b5-9817-0410-a4d7...
authorKasper Skårhøj <kasper@typo3.org>
Wed, 25 Oct 2006 10:30:52 +0000 (10:30 +0000)
committerKasper Skårhøj <kasper@typo3.org>
Wed, 25 Oct 2006 10:30:52 +0000 (10:30 +0000)
85 files changed:
t3lib/class.t3lib_cli.php [new file with mode: 0644]
t3lib/class.t3lib_topmenubase.php [new file with mode: 0644]
typo3/alt_main_new.php [new file with mode: 0644]
typo3/cleaner_check.sh [new file with mode: 0755]
typo3/cleaner_fix.sh [new file with mode: 0755]
typo3/cli_dispatch.phpsh [new file with mode: 0755]
typo3/gfx/x_dividerbg.png [new file with mode: 0644]
typo3/gfx/x_menubackground.gif [new file with mode: 0644]
typo3/gfx/x_menulayerbg.png [new file with mode: 0644]
typo3/gfx/x_state_checked.png [new file with mode: 0644]
typo3/gfx/x_t3logo.png [new file with mode: 0644]
typo3/gfx/x_thereismore.png [new file with mode: 0644]
typo3/logomenu.php [new file with mode: 0644]
typo3/mod.php [new file with mode: 0644]
typo3/prototype.js [new file with mode: 0644]
typo3/scriptaculous/builder.js [new file with mode: 0644]
typo3/scriptaculous/controls.js [new file with mode: 0644]
typo3/scriptaculous/dragdrop.js [new file with mode: 0644]
typo3/scriptaculous/effects.js [new file with mode: 0644]
typo3/scriptaculous/scriptaculous.js [new file with mode: 0644]
typo3/scriptaculous/slider.js [new file with mode: 0644]
typo3/scriptaculous/unittest.js [new file with mode: 0644]
typo3/sysext/beuser/class.tx_beuser.php [new file with mode: 0644]
typo3/sysext/lowlevel/HOWTO_clean_up_TYPO3_installations.txt [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.cleanflexform.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.deleted.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.double_files.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.lost_files.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.missing_files.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.missing_relations.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.orphan_records.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.rte_images.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/class.versions.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.cleanflexform.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.deleted.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.double_files.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.lost_files.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.missing_files.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.missing_relations.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.orphan_records.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.rte_images.php [new file with mode: 0644]
typo3/sysext/lowlevel/clmods/clmods/class.versions.php [new file with mode: 0644]
typo3/sysext/lowlevel/dbint/cli/cleaner_cli.php [new file with mode: 0755]
typo3/sysext/lowlevel/dbint/cli/refindex_cli.php [new file with mode: 0755]
typo3/sysext/lowlevel/ext_localconf.php [new file with mode: 0644]
typo3/sysext/topapps/cache/conf.php [new file with mode: 0644]
typo3/sysext/topapps/cache/index.php [new file with mode: 0644]
typo3/sysext/topapps/cache/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/clock/conf.php [new file with mode: 0644]
typo3/sysext/topapps/clock/index.php [new file with mode: 0644]
typo3/sysext/topapps/clock/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/dashboard/conf.php [new file with mode: 0644]
typo3/sysext/topapps/dashboard/index.php [new file with mode: 0644]
typo3/sysext/topapps/dashboard/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/dashboard/x_dashboard.png [new file with mode: 0644]
typo3/sysext/topapps/dashboard/x_search.png [new file with mode: 0644]
typo3/sysext/topapps/ext_emconf.php [new file with mode: 0644]
typo3/sysext/topapps/ext_tables.php [new file with mode: 0644]
typo3/sysext/topapps/menu/conf.php [new file with mode: 0644]
typo3/sysext/topapps/menu/index.php [new file with mode: 0644]
typo3/sysext/topapps/search/conf.php [new file with mode: 0644]
typo3/sysext/topapps/search/index.php [new file with mode: 0644]
typo3/sysext/topapps/search/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/search/x_search.png [new file with mode: 0644]
typo3/sysext/topapps/shortcut/addedit.png [new file with mode: 0644]
typo3/sysext/topapps/shortcut/conf.php [new file with mode: 0644]
typo3/sysext/topapps/shortcut/config.png [new file with mode: 0644]
typo3/sysext/topapps/shortcut/index.php [new file with mode: 0644]
typo3/sysext/topapps/shortcut/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/shortcut/mediamanager.png [new file with mode: 0644]
typo3/sysext/topapps/shortcut/module.png [new file with mode: 0644]
typo3/sysext/topapps/shortcut/user.png [new file with mode: 0644]
typo3/sysext/topapps/submodules/conf.php [new file with mode: 0644]
typo3/sysext/topapps/submodules/index.php [new file with mode: 0644]
typo3/sysext/topapps/user/be_users.gif [new file with mode: 0644]
typo3/sysext/topapps/user/conf.php [new file with mode: 0644]
typo3/sysext/topapps/user/index.php [new file with mode: 0644]
typo3/sysext/topapps/user/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/workspaces/conf.php [new file with mode: 0644]
typo3/sysext/topapps/workspaces/index.php [new file with mode: 0644]
typo3/sysext/topapps/workspaces/locallang.xml [new file with mode: 0644]
typo3/sysext/topapps/workspaces/sys_workspace.png [new file with mode: 0644]
typo3/sysext/topapps/xyzcorp/conf.php [new file with mode: 0644]
typo3/sysext/topapps/xyzcorp/index.php [new file with mode: 0644]
typo3/sysext/topapps/xyzcorp/logo.png [new file with mode: 0644]

diff --git a/t3lib/class.t3lib_cli.php b/t3lib/class.t3lib_cli.php
new file mode 100644 (file)
index 0000000..75b098b
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2006 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Contains base class for TYPO3 cli scripts
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ *
+ *
+ *   60: class t3lib_cli
+ *   83:     function t3lib_cli()
+ *   96:     function cli_getArg($option,$argv)
+ *  112:     function cli_getArgIndex()
+ *  131:     function cli_keyboardInput()
+ *  142:     function cli_keyboardInput_yes()
+ *  154:     function cli_echo($string='',$force=FALSE)
+ *  169:     function cli_help()
+ *  207:     function cli_indent($str,$indent)
+ *
+ * TOTAL FUNCTIONS: 8
+ * (This index is automatically created/updated by the extension "extdeveval")
+ *
+ */
+
+
+/**
+ * TYPO3 cli script basis
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage t3lib
+ */
+class t3lib_cli {
+
+       var $cli_args = array();                        // Command line arguments, exploded into key => value-array pairs
+       var $cli_options = array(
+               array('-s','Silent operation, will only output errors and important messages.'),
+               array('--silent','Same as -s'),
+               array('-ss','Super silent, will not even output errors or important messages.'),
+       );
+       var $cli_help = array(
+                       'name' => 'CLI base class (overwrite this...)',
+                       'synopsis' => '###OPTIONS###',
+                       'description' => 'Class with basic functionality for CLI scripts (overwrite this...)',
+                       'examples' => 'Give examples...',
+                       'options' => '',
+                       'license' => 'GNU GPL - free software!',
+                       'author' => '[Author name]',
+               );
+       var $stdin = NULL;      
+
+
+       /**
+        * Constructor
+        * Make sure child classes also call this!
+        *
+        * @return      void
+        */
+       function t3lib_cli()    {
+                       // Loads the cli_args array with command line arguments
+               $this->cli_args = $this->cli_getArgIndex();
+       }
+
+       /**
+        * Finds the arg token (like "-s") in argv and returns the rest of argv from that point on.
+        * This should only be used in special cases since this->cli_args should already be prepared with an index of values!
+        *
+        * @param       string          Option string, eg. "-s"
+        * @param       array           Input argv array
+        * @return      array           Output argv array with all options AFTER the found option.
+        */
+       function cli_getArgArray($option,$argv) {
+               while (count($argv) && strcmp($argv[0],$option))        {
+                       array_shift($argv);
+               }
+
+               if (!strcmp($argv[0],$option))  {
+                       array_shift($argv);
+                       return count($argv) ? $argv : array('');
+               }
+       }
+
+       /**
+        * Return true if option is found
+        *
+        * @param       string          Option string, eg. "-s"
+        * @return      boolean         TRUE if option found
+        */
+       function cli_isArg($option)     {
+               return isset($this->cli_args[$option]);
+       }
+
+       /**
+        * Return argument value
+        *
+        * @param       string          Option string, eg. "-s"
+        * @param       integer         Value index, default is 0 (zero) = the first one...
+        * @return      boolean         TRUE if option found
+        */
+       function cli_argValue($option,$idx=0)   {
+               return is_array($this->cli_args[$option]) ? $this->cli_args[$option][$idx] : '';
+       }
+
+       /**
+        * Will parse "_SERVER[argv]" into an index of options and values
+        * Argument names (eg. "-s") will be keys and values after (eg. "-s value1 value2 ..." or "-s=value1") will be in the array.
+        * Array is empty if no values
+        *
+        * @return      array
+        */
+       function cli_getArgIndex()      {
+               $cli_options = array();
+               $index = '_DEFAULT';
+               foreach($_SERVER['argv'] as $token)     {
+                       if ($token{0}==='-')    {
+                               list($index,$opt) = explode('=',$token,2);
+                               if (isset($cli_options[$index]))        {
+                                       echo 'ERROR: Option '.$index.' was used twice!'.chr(10);
+                                       exit;
+                               }
+                               $cli_options[$index] = array();
+                               if (isset($opt))        {
+                                       $cli_options[$index][] = $opt;
+                               }
+                       } else {
+                               $cli_options[$index][] = $token;
+                       }
+               }
+               return $cli_options;
+       }
+       
+       /**
+        * Validates if the input arguments in this->cli_args are all listed in this->cli_options and if not, will exit with an error.
+        */
+       function cli_validateArgs() {
+               $cli_args_copy = $this->cli_args;
+               unset($cli_args_copy['_DEFAULT']);
+               $allOptions = array();
+
+               foreach($this->cli_options as $cfg)     {
+                       $allOptions[] = $cfg[0];
+                       $argSplit = t3lib_div::trimExplode(' ',$cfg[0],1);
+                       if (isset($cli_args_copy[$argSplit[0]]))        {
+
+                               foreach($argSplit as $i => $v)  {
+                                       $ii=$i;
+                                       if ($i>0)       {
+                                               if (!isset($cli_args_copy[$argSplit[0]][$i-1])) {
+                                                       echo 'ERROR: Option "'.$argSplit[0].'" requires a value ("'.$v.'") on position '.$i.chr(10);
+                                                       exit;
+                                               }
+                                       }
+                               }
+
+                               $ii++;
+                               if (isset($cli_args_copy[$argSplit[0]][$ii-1])) {
+                                       echo 'ERROR: Option "'.$argSplit[0].'" does not support a value on position '.$ii.chr(10);
+                                       exit;
+                               }
+                               
+                               unset($cli_args_copy[$argSplit[0]]);
+                       }
+               }
+               
+               if (count($cli_args_copy))      {
+                       echo wordwrap('ERROR: Option '.implode(',',array_keys($cli_args_copy)).' was unknown to this script!'.chr(10).'(Options are: '.implode(', ',$allOptions).')'.chr(10));
+                       exit;
+               }
+       }
+
+       /**
+        * Asks stdin for keyboard input and returns the line (after enter is pressed)
+        *
+        * @return      string
+        */
+       function cli_keyboardInput()    {
+               
+                       // Have to open the stdin stream only ONCE! otherwise I cannot read multiple lines from it... :
+               if (!$this->stdin)      {
+                       $this->stdin = fopen('php://stdin', 'r');
+               }
+               
+               while (FALSE == ($line = fgets($this->stdin,1000)))     {}
+
+               return trim($line);
+       }
+
+       /**
+        * Asks for Yes/No from shell and returns true if "y" or "yes" is found as input.
+        *
+        * @param       string          String to ask before...
+        * @return      boolean         TRUE if "y" or "yes" is the input (case insensitive)
+        */
+       function cli_keyboardInput_yes($msg='') {
+               echo $msg.' (Yes/No + return): ';       // ONLY makes sense to echo it out since we are awaiting keyboard input - that cannot be silenced...
+               return t3lib_div::inList('y,yes',strtolower($this->cli_keyboardInput()));
+       }
+
+       /**
+        * Echos strings to shell, but respective silent-modes
+        *
+        * @param       string          The string
+        * @param       boolean         If string should be written even if -s is set (-ss will subdue it!)
+        * @return      boolean         Returns TRUE if string was outputted.
+        */
+       function cli_echo($string='',$force=FALSE)      {
+               if (isset($this->cli_args['-ss'])) {
+                       // Nothing to do...
+               } elseif (isset($this->cli_args['-s']) || isset($this->cli_args['--silent'])) {
+                       if ($force)     { echo $string; return TRUE; }
+               } else { echo $string; return TRUE; }
+
+               return FALSE;
+       }
+
+       /**
+        * Prints help-output from ->cli_help array
+        *
+        * @return      void
+        */
+       function cli_help()     {
+               foreach($this->cli_help as $key => $value)      {
+                       $this->cli_echo(strtoupper($key).":\n");
+                       switch($key) {
+                               case 'synopsis':
+                                       $optStr = '';
+                                       foreach ($this->cli_options as $v)      {
+                                               $optStr.=' ['.$v[0].']';
+                                       }
+                                       $this->cli_echo($this->cli_indent(str_replace('###OPTIONS###',trim($optStr),$value),4)."\n\n");
+                               break;
+                               case 'options':
+                                       $this->cli_echo($this->cli_indent($value,4)."\n");
+
+                                       $maxLen = 0;
+                                       foreach ($this->cli_options as $v)      {
+                                               if (strlen($v[0])>$maxLen)      $maxLen=strlen($v[0]);
+                                       }
+
+                                       foreach ($this->cli_options as $v)      {
+                                               $this->cli_echo($v[0].substr($this->cli_indent(rtrim($v[1].chr(10).$v[2]),$maxLen+4),strlen($v[0]))."\n");
+                                       }
+                                       $this->cli_echo("\n");
+                               break;
+                               default:
+                                       $this->cli_echo($this->cli_indent($value,4)."\n\n");
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * Indentation function for 75 char wide lines.
+        *
+        * @param       string          String to break and indent.
+        * @param       integer         Number of space chars to indent.
+        * @return      string          Result
+        */
+       function cli_indent($str,$indent)       {
+               $lines = explode(chr(10),wordwrap($str,75-$indent));
+               $indentStr = str_pad('',$indent,' ');
+               foreach($lines as $k => $v)     {
+                       $lines[$k] = $indentStr.$lines[$k];
+               }
+               return implode(chr(10),$lines);
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/t3lib/class.t3lib_topmenubase.php b/t3lib/class.t3lib_topmenubase.php
new file mode 100644 (file)
index 0000000..562dd57
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2006 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Base class for scripts delivering content to the top menu bar/icon panel.
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ */
+
+
+
+
+/**
+ * Base class for scripts delivering content to the top menu bar/icon panel.
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage core
+ */
+class t3lib_topmenubase {
+
+       function menuItems($menuItems)  {
+               $output = '';
+               
+                       // Traverse items:
+               foreach($menuItems as $item)    {
+                       
+                               // Divider has no other options:
+                       if ($item['title']=='--div--')  {
+                               $output.= '<div class="menuLayerItem_divider" onmouseover="menuOpenSub(this);"></div>';
+                       } else {
+                       
+                               $itemCode = '';
+                               $onClick = '';
+                       
+                                       // Render subitems if any:
+                               if (is_array($item['subitems']))        {
+                                       $itemCode.= $this->menuLayer($item['subitems'],$item['id']);
+                               }
+                       
+                                       // Render state icon if any:
+                               switch ($item['state']) {
+                                       case 'checked';
+                                               $itemCode.= '<img src="gfx/x_state_checked.png" width="16" class="menulayerItemIcon">';
+                                       break;
+                                       default:
+                                       $itemCode.= '<img src="gfx/clear.gif" width="16" class="menulayerItemIcon">';
+                                       break;
+                               }
+                       
+                                       // Render icon if any:
+                               if ($item['icon'])      {
+                                       if (is_array($item['icon']))    {
+                                               $itemCode.= '<img '.t3lib_iconWorks::skinImg('',$item['icon'][0],$item['icon'][1]).' class="menulayerItemIcon" alt="" />';
+                                       } else {
+                                               $itemCode.= $item['icon'];
+                                       }
+                               }
+                       
+                                       // Title:
+                               $itemCode.= htmlspecialchars($item['title']).'&nbsp;&nbsp;';
+                       
+                                       // if subitems, show arrow pointing right:
+                               $itemCode.= is_array($item['subitems']) ? '<img src="gfx/x_thereismore.png" class="menulayerItemIcon" style="padding-left:40px;">' : ''; 
+                       
+                                       // Set onclick handlers:
+                               $onClick.= $item['xurl'] ? "if (Event.element(event)==this){openUrlInWindow('".$item['xurl']."','aWindow');}" : '';
+                               $onClick.= $item['url'] ? "if (Event.element(event)==this){content.document.location='".$item['url']."';}" : '';
+                               $onClick.= $item['onclick'] ? $item['onclick'] : $item['onclick'];
+               
+                                       // Wrap it all up:
+                               $output.= '<div '.($item['id'] ? 'id="'.htmlspecialchars($item['id']).'"' : '').'class="menuLayerItem" onmouseover="menuOpenSub(this);"'.($onClick ? ' onclick="'.htmlspecialchars($onClick).'"' : '').'>'.$itemCode.'</div>';
+                               $output.= $item['html'];
+                       }
+               }
+
+               return $output;
+       }
+       /**
+        *
+        */
+       function menuLayer($menuItems,$baseid='')       {
+               $output = $this->menuItems($menuItems);
+                       
+                       // Encapsulate in menu layer:
+               return $this->simpleLayer($output,$baseid?$baseid.'-layer':'');
+       }
+       
+       function simpleLayer($output,$id='',$class='menulayer') {
+               return '<div class="'.$class.'" style="display: none;"'.($id?' id="'.htmlspecialchars($id).'"':'').'>'.$output.'</div>';
+       }
+       
+       function menuItemLayer($id,$content,$onclick='')        {
+               return '<div id="'.$id.'" class="menuItems menu-normal" style="float: left;" onclick="menuToggleState(\''.$id.'\');'.$onclick.'" onmouseover="menuMouseOver(\''.$id.'\');" onmouseout="menuMouseOut(\''.$id.'\');">'.$content.'</div>';
+       }
+       function menuItemObject($id,$functionContent)   {
+               return '
+               <script>
+                       menuItemObjects[\''.$id.'\'] = {
+                               '.$functionContent.'
+                       }               
+               </script>               
+               ';
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/typo3/alt_main_new.php b/typo3/alt_main_new.php
new file mode 100644 (file)
index 0000000..9299b3c
--- /dev/null
@@ -0,0 +1,919 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 1999-2005 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Main frameset of the TYPO3 backend
+ * Sending the GET var "alt_main.php?edit=[page id]" will load the page id in the editing module configured.
+ *
+ * $Id: alt_main.php 1421 2006-04-10 09:27:15Z mundaun $
+ * Revised for TYPO3 3.6 2/2003 by Kasper Skaarhoj
+ * XHTML Compliant (almost)
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ *
+ *
+ *   72: class SC_alt_main
+ *   91:     function init()
+ *  113:     function generateJScode()
+ *  386:     function editPageHandling()
+ *  437:     function startModule()
+ *  459:     function main()
+ *  533:     function printContent()
+ *
+ * TOTAL FUNCTIONS: 6
+ * (This index is automatically created/updated by the extension "extdeveval")
+ *
+ */
+
+require ('init.php');
+require ('template.php');
+require_once (PATH_t3lib.'class.t3lib_loadmodules.php');
+require_once (PATH_t3lib.'class.t3lib_basicfilefunc.php');
+require_once ('class.alt_menu_functions.inc');
+$LANG->includeLLFile('EXT:lang/locallang_misc.xml');
+
+
+
+
+/**
+ * Script Class for rendering of the main frameset for the TYPO3 backend.
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage core
+ */
+class SC_alt_main {
+
+               // Internal, dynamic:
+       var $content;
+       var $mainJScode;
+       var $loadModules;               // Load modules-object
+       var $alt_menuObj;               // Menu functions object.
+
+               // Internal, static:
+       var $leftMenuFrameW = 130;
+       var $selMenuFrame = 130;
+       var $topFrameH = 32;
+       var $shortcutFrameH = 30;
+       
+       
+       var $topMenu = 21;
+       var $topIcons = 40;
+
+       /**
+        * Initialization of the script class
+        *
+        * @return      void
+        */
+       function init() {
+               global $TBE_MODULES,$TBE_STYLES;
+
+                       // Initializes the backend modules structure for use later.
+               $this->loadModules = t3lib_div::makeInstance('t3lib_loadModules');
+               $this->loadModules->load($TBE_MODULES);
+
+                       // Instantiates thee menu object which will generate some JavaScript for the goToModule() JS function in this frameset.
+               $this->alt_menuObj = t3lib_div::makeInstance('alt_menu_functions');
+
+                       // Check for distances defined in the styles array:
+               if ($TBE_STYLES['dims']['leftMenuFrameW'])              $this->leftMenuFrameW = $TBE_STYLES['dims']['leftMenuFrameW'];
+               if ($TBE_STYLES['dims']['topFrameH'])           $this->topFrameH = $TBE_STYLES['dims']['topFrameH'];
+               if ($TBE_STYLES['dims']['shortcutFrameH'])              $this->shortcutFrameH = $TBE_STYLES['dims']['shortcutFrameH'];
+               if ($TBE_STYLES['dims']['selMenuFrame'])                $this->selMenuFrame = $TBE_STYLES['dims']['selMenuFrame'];
+       }
+
+       /**
+        * Generates the JavaScript code for the frameset.
+        *
+        * @return      void
+        */
+       function generateJScode()       {
+               global $BE_USER,$LANG;
+
+               $pt3 = t3lib_div::dirname(t3lib_div::getIndpEnv('SCRIPT_NAME')).'/';
+               $goToModule_switch = $this->alt_menuObj->topMenu($this->loadModules->modules,0,"",4);
+               $fsMod = implode(chr(10),$this->alt_menuObj->fsMod);
+
+                       // If another page module was specified, replace the default Page module with the new one
+               $newPageModule = trim($GLOBALS['BE_USER']->getTSConfigVal('options.overridePageModule'));
+               $pageModule = t3lib_BEfunc::isModuleSetInTBE_MODULES($newPageModule) ? $newPageModule : 'web_layout';
+
+               $this->mainJScode='
+       
+       /**
+        * "Content" Iframe resize function
+        */
+       function resize_Iframe() {
+               var container = document.getElementById("content");
+               var menuHeight = '.(is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['icons']) ? $this->topIcons+$this->topMenu : $this->topMenu).'; //taille du menu (a définir dynamiquement)
+               container.style.height = (document.body.clientHeight-menuHeight)+"px";
+       }
+       window.onload = resize_Iframe;
+       window.onresize = resize_Iframe;        
+               
+       /**
+        * Function similar to PHPs  rawurlencode();
+        */
+       function rawurlencode(str)      {       //
+               var output = escape(str);
+               output = str_replace("*","%2A", output);
+               output = str_replace("+","%2B", output);
+               output = str_replace("/","%2F", output);
+               output = str_replace("@","%40", output);
+               return output;
+       }
+
+       /**
+        * String-replace function
+        */
+       function str_replace(match,replace,string)      {       //
+               var input = ""+string;
+               var matchStr = ""+match;
+               if (!matchStr)  {return string;}
+               var output = "";
+               var pointer=0;
+               var pos = input.indexOf(matchStr);
+               while (pos!=-1) {
+                       output+=""+input.substr(pointer, pos-pointer)+replace;
+                       pointer=pos+matchStr.length;
+                       pos = input.indexOf(match,pos+1);
+               }
+               output+=""+input.substr(pointer);
+               return output;
+       }
+
+       /**
+        * TypoSetup object.
+        */
+       function typoSetup()    {       //
+               this.PATH_typo3 = "'.$pt3.'";
+               this.PATH_typo3_enc = "'.rawurlencode($pt3).'";
+               this.username = "'.$BE_USER->user['username'].'";
+               this.uniqueID = "'.t3lib_div::shortMD5(uniqid('')).'";
+               this.navFrameWidth = 0;
+       }
+       var TS = new typoSetup();
+
+       /**
+        * Functions for session-expiry detection:
+        */
+       function busy() {       //
+               this.loginRefreshed = busy_loginRefreshed;
+               this.checkLoginTimeout = busy_checkLoginTimeout;
+               this.openRefreshWindow = busy_OpenRefreshWindow;
+               this.busyloadTime=0;
+               this.openRefreshW=0;
+               this.reloginCancelled=0;
+       }
+       function busy_loginRefreshed()  {       //
+               var date = new Date();
+               this.busyloadTime = Math.floor(date.getTime()/1000);
+               this.openRefreshW=0;
+       }
+       function busy_checkLoginTimeout()       {       //
+               var date = new Date();
+               var theTime = Math.floor(date.getTime()/1000);
+               if (theTime > this.busyloadTime+'.intval($BE_USER->auth_timeout_field).'-30)    {
+                       return true;
+               }
+       }
+       function busy_OpenRefreshWindow()       {       //
+               vHWin=window.open("login_frameset.php","relogin_"+TS.uniqueID,"height=350,width=700,status=0,menubar=0,location=1");
+               vHWin.focus();
+               this.openRefreshW=1;
+       }
+       function busy_checkLoginTimeout_timer() {       //
+               if (busy.checkLoginTimeout() && !busy.reloginCancelled && !busy.openRefreshW)   {
+                       if (confirm('.$GLOBALS['LANG']->JScharCode($LANG->sL('LLL:EXT:lang/locallang_core.php:mess.refresh_login')).')) {
+                               busy.openRefreshWindow();
+                       } else  {
+                               busy.reloginCancelled = 1;
+                       }
+               }
+               window.setTimeout("busy_checkLoginTimeout_timer();",2*1000);    // Each 2nd second is enough for checking. The popup will be triggered 10 seconds before the login expires (see above, busy_checkLoginTimeout())
+
+                       // Detecting the frameset module navigation frame widths (do this AFTER setting new timeout so that any errors in the code below does not prevent another time to be set!)
+               if (top && top.content && top.content.nav_frame && top.content.nav_frame.document && top.content.nav_frame.document.body)       {
+                       TS.navFrameWidth = (top.content.nav_frame.document.documentElement && top.content.nav_frame.document.documentElement.clientWidth) ? top.content.nav_frame.document.documentElement.clientWidth : top.content.nav_frame.document.body.clientWidth;
+               }
+       }
+
+       /**
+        * Launcing information window for records/files (fileref as "table" argument)
+        */
+       function launchView(table,uid,bP)       {       //
+               var backPath= bP ? bP : "";
+               var thePreviewWindow="";
+               thePreviewWindow = window.open(TS.PATH_typo3+"show_item.php?table="+escape(table)+"&uid="+escape(uid),"ShowItem"+TS.uniqueID,"height=300,width=550,status=0,menubar=0,resizable=0,location=0,directories=0,scrollbars=1,toolbar=0");
+               if (thePreviewWindow && thePreviewWindow.focus) {
+                       thePreviewWindow.focus();
+               }
+       }
+
+       /**
+        * Opens plain window with url
+        */
+       function openUrlInWindow(url,windowName)        {       //
+               regularWindow = window.open(url,windowName,"status=1,menubar=1,resizable=1,location=1,directories=0,scrollbars=1,toolbar=1");
+               regularWindow.focus();
+               return false;
+       }
+
+       /**
+        * Loads a URL in the topmenuFrame
+        */
+       function loadTopMenu(url)       {       //
+               top.topmenuFrame.location = url;
+       }
+
+       /**
+        * Loads a page id for editing in the page edit module:
+        */
+       function loadEditId(id,addGetVars)      {       //
+               top.fsMod.recentIds["web"]=id;
+               top.fsMod.navFrameHighlightedID["web"]="pages"+id+"_0";         // For highlighting
+
+               if (top.content && top.content.nav_frame && top.content.nav_frame.refresh_nav)  {
+                       top.content.nav_frame.refresh_nav();
+               }
+
+               top.goToModule("'.$pageModule.'", 0, addGetVars?addGetVars:"");
+       }
+
+       /**
+        * Returns incoming URL (to a module) unless nextLoadModuleUrl is set. If that is the case nextLoadModuleUrl is returned (and cleared)
+        * Used by the shortcut frame to set a "intermediate URL"
+        */
+       var nextLoadModuleUrl="";
+       function getModuleUrl(inUrl)    {       //
+               var nMU;
+               if (top.nextLoadModuleUrl)      {
+                       nMU=top.nextLoadModuleUrl;
+                       top.nextLoadModuleUrl="";
+                       return nMU;
+               } else {
+                       return inUrl;
+               }
+       }
+
+       /**
+        * Print properties of an object
+        */
+       function debugObj(obj,name,printEach)   {       //
+               var acc;
+               for (i in obj) {
+                       if (obj[i])     {
+                               if (printEach)  {
+                                       alert(i+":  "+obj[i]);
+                               } else {
+                                       acc+=i+":  "+obj[i]+"\n";
+                               }
+                       }
+               }
+               if (!printEach) alert("Object: "+name+"\n\n"+acc);
+       }
+
+       /**
+        * Initialize login expiration warning object
+        */
+       var busy = new busy();
+       busy.loginRefreshed();
+       busy_checkLoginTimeout_timer();
+
+
+       /**
+        * Highlight module:
+        */
+       var currentlyHighLightedId = "";
+       var currentlyHighLighted_restoreValue = "";
+       var currentlyHighLightedMain = "";
+       function highlightModuleMenuItem(trId, mainModule)      {       //
+               currentlyHighLightedMain = mainModule;
+                       // Get document object:
+               if (top.menu && top.menu.document)      {
+                       var docObj = top.menu.document;
+                       var HLclass = mainModule ? "c-mainitem-HL" : "c-subitem-row-HL";
+               } else if (top.topmenuFrame && top.topmenuFrame.document)       {
+                       var docObj = top.topmenuFrame.document;
+                       var HLclass = mainModule ? "c-mainitem-HL" : "c-subitem-HL";
+               }
+
+               if (docObj)     {
+                               // Reset old:
+                       if (currentlyHighLightedId && docObj.getElementById(currentlyHighLightedId))    {
+                               docObj.getElementById(currentlyHighLightedId).attributes.getNamedItem("class").nodeValue = currentlyHighLighted_restoreValue;
+                       }
+                               // Set new:
+                       currentlyHighLightedId = trId;
+                       if (currentlyHighLightedId && docObj.getElementById(currentlyHighLightedId))    {
+                               var classAttribObject = docObj.getElementById(currentlyHighLightedId).attributes.getNamedItem("class");
+                               currentlyHighLighted_restoreValue = classAttribObject.nodeValue;
+                               classAttribObject.nodeValue = HLclass;
+                       }
+               }
+       }
+
+       /**
+        * Function restoring previous selection in left menu after clearing cache
+        */
+       function restoreHighlightedModuleMenuItem() {   //
+               if (currentlyHighLightedId) {
+                       highlightModuleMenuItem(currentlyHighLightedId,currentlyHighLightedMain);
+               }
+       }
+
+       /**
+        * Function used to switch switch module.
+        */
+       var currentModuleLoaded = "";
+       function goToModule(modName,cMR_flag,addGetVars)        {       //
+               var additionalGetVariables = "";
+               if (addGetVars) additionalGetVariables = addGetVars;
+
+               var cMR = 0;
+               if (cMR_flag)   cMR = 1;
+
+               currentModuleLoaded = modName;
+
+               switch(modName) {'.$goToModule_switch.'
+               }
+       }
+
+       /**
+        * Frameset Module object
+        *
+        * Used in main modules with a frameset for submodules to keep the ID between modules
+        * Typically that is set by something like this in a Web>* sub module:
+        *              if (top.fsMod) top.fsMod.recentIds["web"] = "\'.intval($this->id).\'";
+        *              if (top.fsMod) top.fsMod.recentIds["file"] = "...(file reference/string)...";
+        */
+       function fsModules()    {       //
+               this.recentIds=new Array();                                     // used by frameset modules to track the most recent used id for list frame.
+               this.navFrameHighlightedID=new Array();         // used by navigation frames to track which row id was highlighted last time
+               this.currentMainLoaded="";
+               this.currentBank="0";
+       }
+       var fsMod = new fsModules();
+       '.$fsMod.'
+
+               // Used by Frameset Modules
+       var condensedMode = '.($BE_USER->uc['condensedMode']?1:0).';
+       var currentSubScript = "";
+       var currentSubNavScript = "";
+
+               // Used for tab-panels:
+       var DTM_currentTabs = new Array();
+               ';
+
+                       // Check editing of page:
+               $this->editPageHandling();
+               $this->startModule();
+       }
+
+       /**
+        * Checking if the "&edit" variable was sent so we can open for editing the page.
+        * Code based on code from "alt_shortcut.php"
+        *
+        * @return      void
+        */
+       function editPageHandling()     {
+               global $BE_USER;
+
+               if (!t3lib_extMgm::isLoaded('cms'))     return;
+
+                       // EDIT page:
+               $editId = preg_replace('/[^[:alnum:]_]/','',t3lib_div::_GET('edit'));
+               $theEditRec = '';
+
+               if ($editId)    {
+
+                               // Looking up the page to edit, checking permissions:
+                       $where = ' AND ('.$BE_USER->getPagePermsClause(2).' OR '.$BE_USER->getPagePermsClause(16).')';
+                       if (t3lib_div::testInt($editId))        {
+                               $theEditRec = t3lib_BEfunc::getRecordWSOL('pages',$editId,'*',$where);
+                       } else {
+                               $records = t3lib_BEfunc::getRecordsByField('pages','alias',$editId,$where);
+                               if (is_array($records)) {
+                                       reset($records);
+                                       $theEditRec = current($records);
+                                       t3lib_BEfunc::workspaceOL('pages', $theEditRec);
+                               }
+                       }
+
+                               // If the page was accessible, then let the user edit it.
+                       if (is_array($theEditRec) && $BE_USER->isInWebMount($theEditRec['uid']))        {
+                                       // Setting JS code to open editing:
+                               $this->mainJScode.='
+               // Load page to edit:
+       window.setTimeout("top.loadEditId('.intval($theEditRec['uid']).');",500);
+                       ';
+                                       // Checking page edit parameter:
+                               if(!$BE_USER->getTSConfigVal('options.shortcut_onEditId_dontSetPageTree')) {
+
+                                               // Expanding page tree:
+                                       t3lib_BEfunc::openPageTree(intval($theEditRec['pid']),!$BE_USER->getTSConfigVal('options.shortcut_onEditId_keepExistingExpanded'));
+                               }
+                       } else {
+                               $this->mainJScode.='
+               // Warning about page editing:
+       alert('.$GLOBALS['LANG']->JScharCode(sprintf($GLOBALS['LANG']->getLL('noEditPage'),$editId)).');
+                       ';
+                       }
+               }
+       }
+
+       /**
+        * Sets the startup module from either GETvars module and mpdParams or user configuration.
+        *
+        * @return      void
+        */
+       function startModule() {
+               global $BE_USER;
+               $module = preg_replace('/[^[:alnum:]_]/','',t3lib_div::_GET('module'));
+               if (!$module && $BE_USER->uc['startInTaskCenter']) {
+                       $module = 'user_task';
+               }
+
+               $params = t3lib_div::_GET('modParams');
+               if ($module) {
+                       $this->mainJScode.='
+               // open in module:
+       window.setTimeout("top.goToModule(\''.$module.'\',false,\''.$params.'\');",500);
+                       ';
+               }
+       }
+
+
+       /**
+        * Creates the header and frameset of the backend interface
+        *
+        * @return      void
+        */
+       function main() {
+               global $BE_USER,$TYPO3_CONF_VARS;
+
+                       // Set doktype:
+               $GLOBALS['TBE_TEMPLATE']->docType='xhtml_frames';
+
+                       // Make JS:
+               $this->generateJScode();
+               $GLOBALS['TBE_TEMPLATE']->JScode= '
+                       <script type="text/javascript" src="md5.js"></script>
+                       <script type="text/javascript" src="../t3lib/jsfunc.evalfield.js"></script>
+                       ';
+               $GLOBALS['TBE_TEMPLATE']->JScode.=$GLOBALS['TBE_TEMPLATE']->wrapScriptTags($this->mainJScode);
+
+                       // Title:
+               $title = $TYPO3_CONF_VARS['SYS']['sitename'] ? $TYPO3_CONF_VARS['SYS']['sitename'].' [TYPO3 '.TYPO3_version.']' : 'TYPO3 '.TYPO3_version;
+
+               //Add styles
+               $this->content.='
+               
+               <script src="prototype.js" language="JavaScript" type="text/javascript"></script>
+               <script src="scriptaculous/scriptaculous.js" language="JavaScript" type="text/javascript"></script>
+               <script type="text/javascript" language="JavaScript">
+                       function getElementContent(placeholderId, frequency, url) {
+                               if (!url)       {
+                                       if (placeholderId=="_logoMenu") {
+                                               var url = "logomenu.php?cmd=menuitem";
+                                       } else {
+                                               var url = "mod.php?M="+placeholderId+"&cmd=menuitem";
+                                       }
+                               }
+                               var pars = "";
+                               
+                               if (frequency)  {
+                                       var myAjax = new Ajax.PeriodicalUpdater(
+                                               placeholderId, 
+                                               url, 
+                                               {
+                                                       method: "get", 
+                                                       parameters: pars,
+                                                       evalScripts: true,
+                                                       frequency: frequency
+                                               });
+                               } else {
+                                       var myAjax = new Ajax.Updater(
+                                               placeholderId, 
+                                               url, 
+                                               {
+                                                       method: "get", 
+                                                       parameters: pars,
+                                                       evalScripts: true
+                                               });
+                               }
+                       }                       
+                       
+                       
+                       var menuActive = "";
+                       var menuItemObjects = new Array();
+                       
+                       function menuToggleState(ID) {
+                               if (menuActive) {
+                                       menuReset();
+                               } else {
+                                       menuSet(ID);
+                               }
+                       }
+                       function menuMouseOver(ID) {
+                               if (menuActive) {
+                                       menuSet(ID);
+                               }
+                       }
+                       function menuMouseOut(ID)       {
+                               return;
+                               if (menuActive) {
+                                       hideOpenLayerStack();
+                                       Element.removeClassName(menuActive, "menu-hilight");
+                                       Element.addClassName(menuActive, "menu-normal");
+                               }
+                       }
+                       
+                       function menuSet(ID) {
+                               if (menuActive != ID)   {
+                                       hideOpenLayerStack();
+                                       if (menuActive) {
+                                               Element.removeClassName(menuActive, "menu-hilight");
+                                               Element.addClassName(menuActive, "menu-normal");
+                                       }
+                                       menuActive = ID;
+
+                                       Element.addClassName(menuActive, "menu-hilight");
+                                       Element.removeClassName(menuActive, "menu-normal");
+
+                                               // Show layer below
+                                       var nodes = $A($(ID).childNodes);
+                                       nodes.each(function(node){
+                                               if (node.nodeType==1)   {       // This type seems to represent tags, not CDATA (which for some reason stops JS execution!)
+                                                       if (Element.hasClassName(node,"menulayer"))     {
+                                                               menuShowLayer(node, ID);
+                                                       }
+                                               }
+                                       });
+
+                                       if (menuItemObjects[ID])        menuItemObjects[ID].onActivate();
+                               }
+                       }
+                       function menuReset() {
+                               hideOpenLayerStack();
+                               Element.removeClassName(menuActive, "menu-hilight");
+                               Element.addClassName(menuActive, "menu-normal");
+                               menuActive = "";
+                       }
+                       
+                       function hideOpenLayerStack() {
+                               var layersToHide = document.getElementsByClassName("menulayer", "menu");
+                               var nodes = $A(layersToHide);
+                               nodes.each(function(node){
+                                       Element.hide(node);
+//                                     Effect.Fade(node,{duration: 0.4});
+                               });                             
+                       }
+                       function menuShowLayer(menyLayerObj, alignWithID)       {
+//                             Element.show(menyLayerObj);
+
+                               var rightEdgeOfLayer = $(alignWithID).offsetLeft + Element.getDimensions(menyLayerObj).width;
+                               if (rightEdgeOfLayer>document.body.clientWidth) {
+                                       Element.setStyle(menyLayerObj,{left: ($(alignWithID).offsetLeft + Element.getDimensions(alignWithID).width - Element.getDimensions(menyLayerObj).width)+\'px\'}); 
+                               } else {
+                                       Element.setStyle(menyLayerObj,{left: ($(alignWithID).offsetLeft)+\'px\'}); 
+                               }
+                               Element.setStyle(menyLayerObj,{top: \''.$this->topMenu.'px\'}); 
+                               
+                               Effect.Appear(menyLayerObj,{duration: 0.4});
+                       }       
+                       
+                       function menuOpenSub(el)        {
+
+                                       // First, show node:
+                               var nodes = $A(el.childNodes);
+                               nodes.each(function(node){
+                                       if (node.nodeType==1)   {       // This type seems to represent tags, not CDATA (which for some reason stops JS execution!)
+                                               //debugObj(node);
+                                               if (Element.hasClassName(node,"menulayer") && !Element.visible(node))   {
+                                                       Effect.Appear(node,{duration: 0.4});
+                                                       Element.setStyle(node,{left: ($(el).offsetLeft + Element.getDimensions(el).width - 5) +\'px\'}); 
+                                                       
+                                                       if (el.id && menuItemObjects[""+el.id]) {
+                                                               menuItemObjects[""+el.id].onActivate();
+                                                       }
+                                               }
+                                       }
+                               });
+
+                                       // Hide everyone except this:
+                               var siblings = $A(el.parentNode.childNodes);
+                               siblings.each(function(sibling){
+                                       if (sibling.nodeType==1)        {
+                                               var equal =  sibling==el;
+                                               if (!equal)     {
+                                                       var layersToHide = document.getElementsByClassName("menulayer", sibling);
+                                                       var nodes = $A(layersToHide);
+                                                       nodes.each(function(node){
+                                                               Element.hide(node);
+                                                       });
+                                               }
+                                       }
+                               });
+                       }               
+                       
+                       
+                               
+               </script>               
+               
+               
+               <style type="text/css" id="internalStyle">
+                               /*<![CDATA[*/
+                                       
+<!--
+
+body, html {
+       width: 100%;
+       height: 100%;
+       margin: 0;
+       padding: 0;
+}
+
+#menu {
+       position: absolute;
+       top: 0px;
+       left: 0px;
+       height: '.$this->topMenu.'px;
+       z-index: 99;
+       border-bottom: 1px solid #666666;
+       border-top: 1px solid black;
+       width: 100%;
+       background-image: url("gfx/x_menubackground.gif");
+}
+
+#icons {
+       position: absolute;
+       top: '.$this->topMenu.'px;
+       left: 0px;
+       height: '.$this->topIcons.'px;
+       z-index: 98;
+       background-color: #eeeeee;
+       border-bottom: 1px solid #666666;
+       width: 100%;
+}
+
+#content {
+       position: absolute;
+       top: '.(is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['icons']) ? $this->topIcons+$this->topMenu : $this->topMenu).'px;
+       left: 0px;
+       width: 100%;
+       height: 100%;
+       border: 0px; 
+       z-index: 1;
+}
+
+.menu-hilight {
+       background-color: #d0e7b1;
+       padding-left: 0px; 
+       padding-right: 0px;
+       border-left: 1px solid #999999;
+       border-right: 1px solid #999999;
+}
+.menu-normal {
+       padding-left: 1px; 
+       padding-right: 1px;
+}
+.menuItems {
+       height: '.($this->topMenu-1).'px;
+       cursor: hand;
+       padding-top:2px;
+       font-size: 12px;
+}
+.menulayer {
+       position: absolute;
+       border: 1px solid #aaaaaa;
+       background-image : url("gfx/x_menulayerbg.png");
+}
+
+.menulayer DIV.menuLayerItem {
+       cursor: hand;
+       width: 100%; 
+       height: 16px;
+       white-space: nowrap;
+       padding-top: 3px;
+       padding-bottom: 3px;
+       font-size: 11px;
+}
+.menulayerItemIcon     {
+       vertical-align: middle;
+       padding-right: 3px;
+}
+
+.menulayer DIV.menuLayerItem_divider {
+       background-image : url("gfx/x_dividerbg.png");
+       width: 100%;
+       height: 7px;
+}
+.menulayer DIV.menuLayerItem:hover {
+       background-color: #d0e7b1;
+}
+
+html {
+       /*for IE6*/
+       _overflow: hidden; 
+}
+
+body { 
+ overflow: hidden; 
+}
+
+
+
+/* Specific for applications: (TODO: Move this into general API:) */
+
+.dashboard-col {
+       float: left;
+       border: 1px solid red;
+}
+.dashboard-dock {
+       border: 1px solid yellow;
+       float: none;
+}
+.dashboard-item {
+       background-color: white;
+       border: 1px solid blue;
+       margin: 10 10 10 10;
+}
+.dashboard-dock .dashboard-item {
+       background-color: yellow;
+}
+.dashboard-dock .dashboard-item .dashboard-icon {
+       display: visible;
+}
+.dashboard-dock .dashboard-item .dashboard-content {
+       display: none
+}
+.dashboard-col .dashboard-item .dashboard-icon {
+       display: none;
+}
+
+.dashboard-item-hover {
+       border: 3px solid green;
+}
+
+-->
+                               /*]]>*/
+
+</style>
+               ';
+               
+               
+               
+                       // Start page header:
+               $this->content.=$GLOBALS['TBE_TEMPLATE']->startPage($title);
+/*
+                       // Creates frameset
+               $fr_content = '<frame name="content" src="alt_intro.php" marginwidth="0" marginheight="0" frameborder="0" scrolling="auto" noresize="noresize" />';
+               $fr_toplogo = '<frame name="toplogo" src="alt_toplogo.php" marginwidth="0" marginheight="0" frameborder="0" scrolling="no" noresize="noresize" />';
+               $fr_topmenu = '<frame name="topmenuFrame" src="alt_topmenu_dummy.php" marginwidth="0" marginheight="0" frameborder="0" scrolling="no" noresize="noresize" />';
+
+               $shortcutFrame=array();
+               if ($BE_USER->getTSConfigVal('options.shortcutFrame'))  {
+                       $shortcutFrame['rowH']=','.$this->shortcutFrameH;
+                       $shortcutFrame['frameDef']='<frame name="shortcutFrame" src="alt_shortcut.php" marginwidth="0" marginheight="0" frameborder="0" scrolling="no" noresize="noresize" />';
+               }
+
+                       // XHTML notice: ' framespacing="0" frameborder="0" border="0"' in FRAMESET elements breaks compatibility with XHTML-frames, but HOW ELSE can I control the visual appearance?
+               if ($GLOBALS['BE_USER']->uc['noMenuMode'])      {
+                       $this->content.= '
+                       <frameset rows="'.$this->topFrameH.',*'.$shortcutFrame['rowH'].'" framespacing="0" frameborder="0" border="0">
+                               '.(!strcmp($BE_USER->uc['noMenuMode'],'icons') ? '
+                               <frameset cols="'.$this->leftMenuFrameW.',*" framespacing="0" frameborder="0" border="0">
+                                       '.$fr_toplogo.'
+                                       '.$fr_topmenu.'
+                               </frameset>' : '
+                               <frameset cols="'.$this->leftMenuFrameW.','.$this->selMenuFrame.',*" framespacing="0" frameborder="0" border="0">
+                                       '.$fr_toplogo.'
+                                       <frame name="menu" src="alt_menu_sel.php" scrolling="no" noresize="noresize" />
+                                       '.$fr_topmenu.'
+                               </frameset>').'
+                               '.$fr_content.'
+                               '.$shortcutFrame['frameDef'].'
+                       </frameset>
+                       ';
+               } else {
+                       $this->content.='
+                       <frameset rows="'.$this->topFrameH.',*'.$shortcutFrame['rowH'].'" framespacing="0" frameborder="0" border="0">
+                               <frameset cols="'.$this->leftMenuFrameW.',*" framespacing="0" frameborder="0" border="0">
+                                       '.$fr_toplogo.'
+                                       '.$fr_topmenu.'
+                               </frameset>
+                               <frameset cols="'.$this->leftMenuFrameW.',*" framespacing="0" frameborder="0" border="0">
+                                       <frame name="menu" src="alt_menu.php" marginwidth="0" marginheight="0" scrolling="auto" noresize="noresize" />
+                                       '.$fr_content.'
+                               </frameset>
+                               '.$shortcutFrame['frameDef'].'
+                       </frameset>
+                       ';
+               }
+               $this->content.='
+
+</html>';
+*/
+
+
+
+$c_menu = '';
+$c_icons = '';
+$populate = '';
+if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['menu'])) {
+       $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['menu'] = array();
+}
+$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['menu'] = array_merge(array('_logoMenu' => array('leftAlign'=>TRUE)), $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['menu']);
+
+foreach($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['menu'] as $modName => $options)        {
+       $align = $options['leftAlign'];
+       $c_menu.='<div id="'.$modName.'"'.(!$options['simpleContainer'] ? ' class="menuItems menu-normal"' : '').' style="float: '.($align?'left':'right').';"'.(!$options['simpleContainer'] ? ' onclick="menuToggleState(\''.$modName.'\');" onmouseover="menuMouseOver(\''.$modName.'\');" onmouseout="menuMouseOut(\''.$modName.'\');"':'').'></div>';
+       $populate.= '
+                               getElementContent("'.$modName.'");';
+}
+
+if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['icons'])) {
+       foreach($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['topApps']['icons'] as $modName => $options)        {
+               $align = $options['leftAlign'];
+               $c_icons.='<div id="'.$modName.'" style="float: '.($align?'left':'right').';"></div>';
+               $populate.= '
+                                       getElementContent("'.$modName.'");';
+       }
+}
+
+$this->content.= ('<body>
+                       <div id="menu">'.$c_menu.'</div>
+                       '.($c_icons ? '<div id="icons">'.$c_icons.'</div>' : '').'
+                       <iframe src="./alt_intro.php" id="content" name="content"></iframe>
+                       
+
+                       <script type="text/javascript" language="JavaScript">
+                               '.$populate.'
+                       </script>               
+                       
+                       
+</body>');
+
+$this->content.= ('</html>');
+       }
+
+       /**
+        * Outputting the accumulated content to screen
+        *
+        * @return      void
+        */
+       function printContent() {
+               echo $this->content;
+       }
+}
+
+// Include extension?
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['typo3/alt_main.php'])     {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['typo3/alt_main.php']);
+}
+
+
+
+
+
+
+
+// ******************************
+// Starting document output
+// ******************************
+
+// Make instance:
+$SOBE = t3lib_div::makeInstance('SC_alt_main');
+$SOBE->init();
+$SOBE->main();
+$SOBE->printContent();
+
+?>
\ No newline at end of file
diff --git a/typo3/cleaner_check.sh b/typo3/cleaner_check.sh
new file mode 100755 (executable)
index 0000000..bb26c13
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+./cli_dispatch.phpsh lowlevel_cleaner missing_files -r -v 2 -s --refindex check
+./cli_dispatch.phpsh lowlevel_cleaner double_files -r -v 2 -s --refindex ignore
+./cli_dispatch.phpsh lowlevel_cleaner lost_files -r -v 2 -s --refindex ignore
+./cli_dispatch.phpsh lowlevel_cleaner orphan_records -r -v 2 -s
+./cli_dispatch.phpsh lowlevel_cleaner versions -r -v 2 -s
+./cli_dispatch.phpsh lowlevel_cleaner deleted -r -v 1 -s
+./cli_dispatch.phpsh lowlevel_cleaner missing_relations -r -v 2 -s --refindex ignore
+./cli_dispatch.phpsh lowlevel_cleaner cleanflexform -r -v 2 -s
+./cli_dispatch.phpsh lowlevel_cleaner rte_images -r -v 2 -s --refindex ignore
+
diff --git a/typo3/cleaner_fix.sh b/typo3/cleaner_fix.sh
new file mode 100755 (executable)
index 0000000..2c74f48
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+./cli_dispatch.phpsh lowlevel_cleaner missing_files -r -v 2 -s --AUTOFIX --YES --refindex update
+./cli_dispatch.phpsh lowlevel_cleaner double_files -r -v 2 -s --AUTOFIX --YES --refindex update
+./cli_dispatch.phpsh lowlevel_cleaner lost_files -r -v 2 -s --AUTOFIX --YES --refindex update
+./cli_dispatch.phpsh lowlevel_cleaner orphan_records -r -v 2 -s --AUTOFIX --YES
+./cli_dispatch.phpsh lowlevel_cleaner versions -r -v 2 -s --AUTOFIX --YES
+./cli_dispatch.phpsh lowlevel_cleaner deleted -r -v 1 -s --AUTOFIX --YES
+./cli_dispatch.phpsh lowlevel_cleaner missing_relations -r -v 2 -s --AUTOFIX --YES --refindex update
+./cli_dispatch.phpsh lowlevel_cleaner cleanflexform -r -v 2 -s --AUTOFIX --YES
+./cli_dispatch.phpsh lowlevel_cleaner rte_images -r -v 2 -s --refindex ignore
+
diff --git a/typo3/cli_dispatch.phpsh b/typo3/cli_dispatch.phpsh
new file mode 100755 (executable)
index 0000000..d7a5875
--- /dev/null
@@ -0,0 +1,41 @@
+#! /usr/bin/php -q
+<?php
+
+// *****************************************
+// CLI module dispatcher.
+// This script can take a "cliKey" as first argument and uses that to look up the path of the script to include in the end.
+// See configuration of this feature in $TYPO3_CONF_VARS['SC_OPTIONS']['GLOBAL']['cliKeys']
+// The point is to have only ONE script dealing with the environment initialization while the actual processing is all a developer should care for.
+// *****************************************
+
+       // Defining circumstances for CLI mode:
+define('TYPO3_cliMode', TRUE);
+
+       // Defining PATH_thisScript here: Must be the ABSOLUTE path of this script in the right context:
+       // This will work as long as the script is called by it's absolute path!
+$temp_PATH_thisScript = isset($_ENV['_']) ? $_ENV['_'] : $_SERVER['_'];
+$BACK_PATH = '';
+
+       // Alternatively, in some environments, we might be able to figure out the absolute path (with no "../" and "./" in) from environment variables...
+if ($temp_PATH_thisScript{0}!='/')     {
+       $temp_CURRENT_DIR = $_SERVER['PWD'].'/';
+       $temp_PATH_thisScript = $temp_CURRENT_DIR.ereg_replace('\.\/','',$temp_PATH_thisScript);
+       if (!@is_file($temp_PATH_thisScript))   {
+               die(wordwrap('ERROR: '.$temp_PATH_thisScript.' was not a file. Maybe your environment does not support running this script with a relative path? Try to run the script with its absolute path and you should be fine.'.chr(10).chr(10)));
+       }
+}
+define('PATH_thisScript',$temp_PATH_thisScript);
+
+       // First argument is a key that 
+define('TYPO3_cliKey', $_SERVER["argv"][0]);
+
+       // Include init file:
+require(dirname(PATH_thisScript).'/'.$BACK_PATH.'init.php');
+
+if (defined('TYPO3_cliInclude'))       {
+       include(TYPO3_cliInclude);
+} else {
+       echo 'ERROR: Nothing to include.'.chr(10);
+       exit;
+}
+?>
diff --git a/typo3/gfx/x_dividerbg.png b/typo3/gfx/x_dividerbg.png
new file mode 100644 (file)
index 0000000..287d45f
Binary files /dev/null and b/typo3/gfx/x_dividerbg.png differ
diff --git a/typo3/gfx/x_menubackground.gif b/typo3/gfx/x_menubackground.gif
new file mode 100644 (file)
index 0000000..f2b0c1a
Binary files /dev/null and b/typo3/gfx/x_menubackground.gif differ
diff --git a/typo3/gfx/x_menulayerbg.png b/typo3/gfx/x_menulayerbg.png
new file mode 100644 (file)
index 0000000..59ca8c0
Binary files /dev/null and b/typo3/gfx/x_menulayerbg.png differ
diff --git a/typo3/gfx/x_state_checked.png b/typo3/gfx/x_state_checked.png
new file mode 100644 (file)
index 0000000..c056717
Binary files /dev/null and b/typo3/gfx/x_state_checked.png differ
diff --git a/typo3/gfx/x_t3logo.png b/typo3/gfx/x_t3logo.png
new file mode 100644 (file)
index 0000000..31018df
Binary files /dev/null and b/typo3/gfx/x_t3logo.png differ
diff --git a/typo3/gfx/x_thereismore.png b/typo3/gfx/x_thereismore.png
new file mode 100644 (file)
index 0000000..58cd520
Binary files /dev/null and b/typo3/gfx/x_thereismore.png differ
diff --git a/typo3/logomenu.php b/typo3/logomenu.php
new file mode 100644 (file)
index 0000000..203ab86
--- /dev/null
@@ -0,0 +1,187 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2006 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Logo menu
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ */
+
+
+require ('init.php');
+require ('template.php');
+
+require_once(PATH_t3lib.'class.t3lib_topmenubase.php');
+
+
+
+/**
+ * Script Class for rendering logo menu
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage core
+ */
+class SC_logomenu extends t3lib_topmenubase {
+       
+       var $id = '_logomenu';
+       
+       /**
+        * Main function
+        *
+        * @return      void
+        */
+       function main() {
+               switch((string)t3lib_div::_GET('cmd'))  {
+                       case 'menuitem':
+                               echo '
+                               <img src="gfx/x_t3logo.png" width="61" height="16" hspace="3" alt="" />';
+                               
+                               $menuItems = array(
+                                       array(
+                                               'title' => 'About TYPO3',
+                                               'xurl' => 'http://typo3.com/',
+                                               'subitems' => array(
+                                                       array(
+                                                               'title' => 'License',
+                                                               'xurl' => 'http://typo3.com/License.1625.0.html',
+                                                       ),
+                                                       array(
+                                                               'title' => 'Support',
+                                                               'subitems' => array(
+                                                                       array(
+                                                                               'title' => 'Mailing lists',
+                                                                               'xurl' => 'http://lists.netfielders.de/cgi-bin/mailman/listinfo',
+                                                                       ),
+                                                                       array(
+                                                                               'title' => 'Documentation',
+                                                                               'xurl' => 'http://typo3.org/documentation/',
+                                                                       ),
+                                                                       array(
+                                                                               'title' => 'Find consultancy',
+                                                                               'xurl' => 'http://typo3.com/Consultancies.1248.0.html',
+                                                                       ),
+                                                               )
+                                                       ),
+                                                       array(
+                                                               'title' => 'Contribute',
+                                                               'xurl' => 'http://typo3.org/community/participate/'
+                                                       ),
+                                                       array(
+                                                               'title' => 'Donate',
+                                                               'xurl' => 'http://typo3.com/Donations.1261.0.html',
+                                                               'icon' => '1'
+                                                       )
+                                               )
+                                       ),
+                                       array(
+                                               'title' => 'Extensions',
+                                               'url' => 'mod/tools/em/index.php'
+                                       ),
+                                       array(
+                                               'title' => 'Menu preferences and such things',
+                                               'onclick' => 'alert("A dialog is now shown which will allow user configuration of items in the menu");event.stopPropagation();',
+                                               'state' => 'checked'
+                                       ),
+                                       array(
+                                               'title' => '--div--'
+                                       ),
+                                       array(
+                                               'title' => 'Recent Items',
+                                               'id' => $this->id.'_recent',
+                                               'subitems' => array(),
+                                               'html' => $this->menuItemObject($this->id.'_recent','
+                                                       fetched: false,
+                                                       onActivate: function() {
+//                                                             if (!this.fetched)      {
+                                                                       //Element.update("'.$this->id.'_recent-layer","asdfasdf");
+                                                                       getElementContent("'.$this->id.'_recent-layer", 0, "logomenu.php?cmd=recent")
+                                                                       this.fetched = true;
+//                                                             }
+                                                       }
+                                               ')
+                                       ),
+                                       array(
+                                               'title' => '--div--'
+                                       ),
+                                       array(
+                                               'title' => 'View frontend',
+                                               'xurl' => t3lib_div::getIndpEnv('TYPO3_SITE_URL')
+                                       ),
+                                       array(
+                                               'title' => 'Log out',
+                                               'onclick' => "top.document.location='logout.php';"
+                                       ),
+                               );
+                               
+                               echo $this->menuLayer($menuItems);
+                       break;
+                       case 'recent':
+
+                               $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
+                                                       'sys_log.*, MAX(sys_log.tstamp) AS tstamp_MAX', 
+                                                       'sys_log,pages', 
+                                                       'pages.uid=sys_log.event_pid AND sys_log.userid='.intval($GLOBALS['BE_USER']->user['uid']).
+                                                               ' AND sys_log.event_pid>0 AND sys_log.type=1 AND sys_log.action=2 AND sys_log.error=0',
+                                                       'tablename,recuid',
+                                                       'tstamp_MAX DESC',
+                                                       20
+                                               );
+                                       
+                               $items = array();
+                               
+                               while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res))       {
+                                       $elRow = t3lib_BEfunc::getRecord($row['tablename'],$row['recuid']);
+                                       if (is_array($elRow))   {
+                                               $items[] = array(
+                                                       'title' => t3lib_div::fixed_lgd_cs(t3lib_BEfunc::getRecordTitle($row['tablename'],$elRow),$GLOBALS['BE_USER']->uc['titleLen']).' - '.t3lib_BEfunc::calcAge(time()-$row['tstamp_MAX']),
+                                                       'icon' => array(t3lib_iconworks::getIcon($row['tablename'],$elRow),'width="18" height="16"'),
+                                                       'onclick' => 'content.'.t3lib_BEfunc::editOnClick('&edit['.$row['tablename'].']['.$row['recuid'].']=edit','','dummy.php')
+                                               );
+                                       }
+                               }
+
+                               echo $this->menuItems($items);
+                       break;
+               }
+       }
+}
+
+// Include extension?
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['typo3/logomenu.php'])     {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['typo3/logomenu.php']);
+}
+
+
+
+// Make instance:
+$SOBE = t3lib_div::makeInstance('SC_logomenu');
+$SOBE->main();
+?>
\ No newline at end of file
diff --git a/typo3/mod.php b/typo3/mod.php
new file mode 100644 (file)
index 0000000..b65cd6e
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2006 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Module Dispatch script
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ */
+
+unset($MCONF);
+require('init.php');
+require('template.php');
+
+       // Find module path:
+$temp_M = (string)t3lib_div::_GET('M');
+if ($temp_path = $TBE_MODULES['_PATHS'][$temp_M])      {
+       $MCONF['_'] = 'mod.php?M='.rawurlencode($temp_M);
+       require($temp_path.'conf.php');
+       $BACK_PATH='';
+       require($temp_path.'index.php');
+} else {
+       #debug($TBE_MODULES);
+       die('Value "'.htmlspecialchars($temp_M).'" for "M" was not found as a module');
+}
+
+?>
diff --git a/typo3/prototype.js b/typo3/prototype.js
new file mode 100644 (file)
index 0000000..14edec8
--- /dev/null
@@ -0,0 +1,2241 @@
+/*  Prototype JavaScript framework, version 1.5.0_rc1
+ *  (c) 2005 Sam Stephenson <sam@conio.net>
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.5.0_rc1',
+  ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
+
+  emptyFunction: function() {},
+  K: function(x) {return x}
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (var property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object == undefined) return 'undefined';
+      if (object == null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({}, object);
+  }
+});
+
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function() {
+    return __method.apply(object, args.concat($A(arguments)));
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function(event) {
+    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    var digits = this.toString(16);
+    if (this < 16) return '0' + digits;
+    return digits;
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  }
+});
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0; i < arguments.length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback(this);
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += (replacement(match) || '').toString();
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? div.childNodes[0].nodeValue : '';
+  },
+
+  toQueryParams: function() {
+    var pairs = this.match(/^\??(.*)$/)[1].split('&');
+    return pairs.inject({}, function(params, pairString) {
+      var pair  = pairString.split('=');
+      var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
+      params[decodeURIComponent(pair[0])] = value;
+      return params;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  camelize: function() {
+    var oStringList = this.split('-');
+    if (oStringList.length == 1) return oStringList[0];
+
+    var camelizedString = this.indexOf('-') == 0
+      ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
+      : oStringList[0];
+
+    for (var i = 1, len = oStringList.length; i < len; i++) {
+      var s = oStringList[i];
+      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
+    }
+
+    return camelizedString;
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.replace(/\\/g, '\\\\');
+    if (useDoubleQuotes)
+      return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    else
+      return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + (object[match[3]] || '').toString();
+    });
+  }
+}
+
+var $break    = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != $continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(iterator(value, index));
+    });
+    return results;
+  },
+
+  detect: function (iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.collect(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.collect(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.collect(Prototype.K);
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0; i < iterable.length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0; i < this.length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != undefined || value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value && value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0; i < this.length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function() {
+    return this.inject([], function(array, value) {
+      return array.include(value) ? array : array.concat([value]);
+    });
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  }
+});
+var Hash = {
+  _each: function(iterator) {
+    for (var key in this) {
+      var value = this[key];
+      if (typeof value == 'function') continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject($H(this), function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  toQueryString: function() {
+    return this.map(function(pair) {
+      return pair.map(encodeURIComponent).join('=');
+    }).join('&');
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  }
+}
+
+function $H(object) {
+  var hash = Object.extend({}, object || {});
+  Object.extend(hash, Enumerable);
+  Object.extend(hash, Hash);
+  return hash;
+}
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responderToAdd) {
+    if (!this.include(responderToAdd))
+      this.responders.push(responderToAdd);
+  },
+
+  unregister: function(responderToRemove) {
+    this.responders = this.responders.without(responderToRemove);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (responder[callback] && typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+  },
+
+  responseIsSuccess: function() {
+    return this.transport.status == undefined
+        || this.transport.status == 0
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  responseIsFailure: function() {
+    return !this.responseIsSuccess();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    var parameters = this.options.parameters || '';
+    if (parameters.length > 0) parameters += '&_=';
+
+    /* Simulate other verbs over post */
+    if (this.options.method != 'get' && this.options.method != 'post') {
+      parameters += (parameters.length > 0 ? '&' : '') + '_method=' + this.options.method;
+      this.options.method = 'post';
+    }
+
+    try {
+      this.url = url;
+      if (this.options.method == 'get' && parameters.length > 0)
+        this.url += (this.url.match(/\?/) ? '&' : '?') + parameters;
+
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.options.method, this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous)
+        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      var body = this.options.postBody ? this.options.postBody : parameters;
+      this.transport.send(this.options.method == 'post' ? body : null);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  setRequestHeaders: function() {
+    var requestHeaders =
+      ['X-Requested-With', 'XMLHttpRequest',
+       'X-Prototype-Version', Prototype.Version,
+       'Accept', 'text/javascript, text/html, application/xml, text/xml, */*'];
+
+    if (this.options.method == 'post') {
+      requestHeaders.push('Content-type', this.options.contentType);
+
+      /* Force "Connection: close" for Mozilla browsers to work around
+       * a bug where XMLHttpReqeuest sends an incorrect Content-length
+       * header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType)
+        requestHeaders.push('Connection', 'close');
+    }
+
+    if (this.options.requestHeaders)
+      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+    for (var i = 0; i < requestHeaders.length; i += 2)
+      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState != 1)
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  header: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) {}
+  },
+
+  evalJSON: function() {
+    try {
+      return eval('(' + this.header('X-JSON') + ')');
+    } catch (e) {}
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  respondToReadyState: function(readyState) {
+    var event = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (event == 'Complete') {
+      try {
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      if ((this.header('Content-type') || '').match(/^text\/javascript/i))
+        this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + event] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + event, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+    if (event == 'Complete')
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.containers = {
+      success: container.success ? $(container.success) : $(container),
+      failure: container.failure ? $(container.failure) :
+        (container.success ? null : $(container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, object) {
+      this.updateContent();
+      onComplete(transport, object);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.responseIsSuccess() ?
+      this.containers.success : this.containers.failure;
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts)
+      response = response.stripScripts();
+
+    if (receiver) {
+      if (this.options.insertion) {
+        new this.options.insertion(receiver, response);
+      } else {
+        Element.update(receiver, response);
+      }
+    }
+
+    if (this.responseIsSuccess()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $() {
+  var results = [], element;
+  for (var i = 0; i < arguments.length; i++) {
+    element = arguments[i];
+    if (typeof element == 'string')
+      element = document.getElementById(element);
+    results.push(Element.extend(element));
+  }
+  return results.reduce();
+}
+
+document.getElementsByClassName = function(className, parentElement) {
+  var children = ($(parentElement) || document.body).getElementsByTagName('*');
+  return $A(children).inject([], function(elements, child) {
+    if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      elements.push(Element.extend(child));
+    return elements;
+  });
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element)
+  var Element = new Object();
+
+Element.extend = function(element) {
+  if (!element) return;
+  if (_nativeExtensions || element.nodeType == 3) return element;
+
+  if (!element._extended && element.tagName && element != window) {
+    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
+
+    if (element.tagName == 'FORM')
+      Object.extend(methods, Form.Methods);
+    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
+      Object.extend(methods, Form.Element.Methods);
+
+    for (var property in methods) {
+      var value = methods[property];
+      if (typeof value == 'function')
+        element[property] = cache.findOrStore(value);
+    }
+  }
+
+  element._extended = true;
+  return element;
+}
+
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+}
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, html) {
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    element = $(element);
+    return $A(element.getElementsByTagName('*'));
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    element = $(element);
+    if (typeof selector == 'string')
+      selector = new Selector(selector);
+    return selector.match(element);
+  },
+
+  up: function(element, expression, index) {
+    return Selector.findElement($(element).ancestors(), expression, index);
+  },
+
+  down: function(element, expression, index) {
+    return Selector.findElement($(element).descendants(), expression, index);
+  },
+
+  previous: function(element, expression, index) {
+    return Selector.findElement($(element).previousSiblings(), expression, index);
+  },
+
+  next: function(element, expression, index) {
+    return Selector.findElement($(element).nextSiblings(), expression, index);
+  },
+
+  getElementsBySelector: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  getElementsByClassName: function(element, className) {
+    element = $(element);
+    return document.getElementsByClassName(className, element);
+  },
+
+  getHeight: function(element) {
+    element = $(element);
+    return element.offsetHeight;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    return Element.classNames(element).include(className);
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).add(className);
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).remove(className);
+    return element;
+  },
+
+  observe: function() {
+    Event.observe.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  stopObserving: function() {
+    Event.stopObserving.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.match(/^\s*$/);
+  },
+
+  childOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var x = element.x ? element.x : element.offsetLeft,
+        y = element.y ? element.y : element.offsetTop;
+    window.scrollTo(x, y);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    var value = element.style[style.camelize()];
+    if (!value) {
+      if (document.defaultView && document.defaultView.getComputedStyle) {
+        var css = document.defaultView.getComputedStyle(element, null);
+        value = css ? css.getPropertyValue(style) : null;
+      } else if (element.currentStyle) {
+        value = element.currentStyle[style.camelize()];
+      }
+    }
+
+    if (window.opera && ['left', 'top', 'right', 'bottom'].include(style))
+      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+
+    return value == 'auto' ? null : value;
+  },
+
+  setStyle: function(element, style) {
+    element = $(element);
+    for (var name in style)
+      element.style[name.camelize()] = style[name];
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    if (Element.getStyle(element, 'display') != 'none')
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = '';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = 'none';
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return;
+    element._overflow = element.style.overflow || 'auto';
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  }
+}
+
+// IE is missing .innerHTML support for TABLE-related elements
+if(document.all){
+  Element.Methods.update = function(element, html) {
+    element = $(element);
+    var tagName = element.tagName.toUpperCase();
+    if (['THEAD','TBODY','TR','TD'].indexOf(tagName) > -1) {
+      var div = document.createElement('div');
+      switch (tagName) {
+        case 'THEAD':
+        case 'TBODY':
+          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
+          depth = 2;
+          break;
+        case 'TR':
+          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
+          depth = 3;
+          break;
+        case 'TD':
+          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
+          depth = 4;
+      }
+      $A(element.childNodes).each(function(node){
+        element.removeChild(node)
+      });
+      depth.times(function(){ div = div.firstChild });
+
+      $A(div.childNodes).each(
+        function(node){ element.appendChild(node) });
+    } else {
+      element.innerHTML = html.stripScripts();
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  }
+}
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if (!window.HTMLElement && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  /* Emulate HTMLElement, HTMLFormElement, HTMLInputElement, HTMLTextAreaElement,
+     and HTMLSelectElement in Safari */
+  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
+    var klass = window['HTML' + tag + 'Element'] = {};
+    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
+  });
+}
+
+Element.addMethods = function(methods) {
+  Object.extend(Element.Methods, methods || {});
+
+  function copy(methods, destination) {
+    var cache = Element.extend.cache;
+    for (var property in methods) {
+      var value = methods[property];
+      destination[property] = cache.findOrStore(value);
+    }
+  }
+
+  if (typeof HTMLElement != 'undefined') {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Form.Methods, HTMLFormElement.prototype);
+    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
+      copy(Form.Element.Methods, klass.prototype);
+    });
+    _nativeExtensions = true;
+  }
+}
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content.stripScripts();
+
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        var tagName = this.element.tagName.toLowerCase();
+        if (tagName == 'tbody' || tagName == 'tr') {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse(false).each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set(this.toArray().concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set(this.select(function(className) {
+      return className != classNameToRemove;
+    }).join(' '));
+  },
+
+  toString: function() {
+    return this.toArray().join(' ');
+  }
+}
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+  initialize: function(expression) {
+    this.params = {classNames: []};
+    this.expression = expression.toString().strip();
+    this.parseExpression();
+    this.compileMatcher();
+  },
+
+  parseExpression: function() {
+    function abort(message) { throw 'Parse error in selector: ' + message; }
+
+    if (this.expression == '')  abort('empty expression');
+
+    var params = this.params, expr = this.expression, match, modifier, clause, rest;
+    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) {
+      params.attributes = params.attributes || [];
+      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
+      expr = match[1];
+    }
+
+    if (expr == '*') return this.params.wildcard = true;
+
+    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
+      modifier = match[1], clause = match[2], rest = match[3];
+      switch (modifier) {
+        case '#':       params.id = clause; break;
+        case '.':       params.classNames.push(clause); break;
+        case '':
+        case undefined: params.tagName = clause.toUpperCase(); break;
+        default:        abort(expr.inspect());
+      }
+      expr = rest;
+    }
+
+    if (expr.length > 0) abort(expr.inspect());
+  },
+
+  buildMatchExpression: function() {
+    var params = this.params, conditions = [], clause;
+
+    if (params.wildcard)
+      conditions.push('true');
+    if (clause = params.id)
+      conditions.push('element.id == ' + clause.inspect());
+    if (clause = params.tagName)
+      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+    if ((clause = params.classNames).length > 0)
+      for (var i = 0; i < clause.length; i++)
+        conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
+    if (clause = params.attributes) {
+      clause.each(function(attribute) {
+        var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
+        var splitValueBy = function(delimiter) {
+          return value + ' && ' + value + '.split(' + delimiter.inspect() + ')';
+        }
+
+        switch (attribute.operator) {
+          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
+          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
+          case '|=':      conditions.push(
+                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
+                          ); break;
+          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
+          case '':
+          case undefined: conditions.push(value + ' != null'); break;
+          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
+        }
+      });
+    }
+
+    return conditions.join(' && ');
+  },
+
+  compileMatcher: function() {
+    this.match = new Function('element', 'if (!element.tagName) return false; \
+      return ' + this.buildMatchExpression());
+  },
+
+  findElements: function(scope) {
+    var element;
+
+    if (element = $(this.params.id))
+      if (this.match(element))
+        if (!scope || Element.childOf(element, scope))
+          return [element];
+
+    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+    var results = [];
+    for (var i = 0; i < scope.length; i++)
+      if (this.match(element = scope[i]))
+        results.push(Element.extend(element));
+
+    return results;
+  },
+
+  toString: function() {
+    return this.expression;
+  }
+}
+
+Object.extend(Selector, {
+  matchElements: function(elements, expression) {
+    var selector = new Selector(expression);
+    return elements.select(selector.match.bind(selector));
+  },
+
+  findElement: function(elements, expression, index) {
+    if (typeof expression == 'number') index = expression, expression = false;
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    return expressions.map(function(expression) {
+      return expression.strip().split(/\s+/).inject([null], function(results, expr) {
+        var selector = new Selector(expr);
+        return results.inject([], function(elements, result) {
+          return elements.concat(selector.findElements(result || element));
+        });
+      });
+    }).flatten();
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  }
+};
+
+Form.Methods = {
+  serialize: function(form) {
+    var elements = Form.getElements($(form));
+    var queryComponents = new Array();
+
+    for (var i = 0; i < elements.length; i++) {
+      var queryComponent = Form.Element.serialize(elements[i]);
+      if (queryComponent)
+        queryComponents.push(queryComponent);
+    }
+
+    return queryComponents.join('&');
+  },
+
+  getElements: function(form) {
+    form = $(form);
+    var elements = new Array();
+
+    for (var tagName in Form.Element.Serializers) {
+      var tagElements = form.getElementsByTagName(tagName);
+      for (var j = 0; j < tagElements.length; j++)
+        elements.push(tagElements[j]);
+    }
+    return elements;
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name)
+      return inputs;
+
+    var matchingInputs = new Array();
+    for (var i = 0; i < inputs.length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) ||
+          (name && input.name != name))
+        continue;
+      matchingInputs.push(input);
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.blur();
+      element.disabled = 'true';
+    }
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.disabled = '';
+    }
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    return Form.getElements(form).find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    Field.activate(Form.findFirstElement(form));
+    return form;
+  }
+}
+
+Object.extend(Form, Form.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+}
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter) {
+      var key = encodeURIComponent(parameter[0]);
+      if (key.length == 0) return;
+
+      if (parameter[1].constructor != Array)
+        parameter[1] = [parameter[1]];
+
+      return parameter[1].map(function(value) {
+        return key + '=' + encodeURIComponent(value);
+      }).join('&');
+    }
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter)
+      return parameter[1];
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    element.focus();
+    if (element.select)
+      element.select();
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.disabled = '';
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = 'true';
+    return element;
+  }
+}
+
+Object.extend(Form.Element, Form.Element.Methods);
+var Field = Form.Element;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+      default:
+        return Form.Element.Serializers.textarea(element);
+    }
+    return false;
+  },
+
+  inputSelector: function(element) {
+    if (element.checked)
+      return [element.name, element.value];
+  },
+
+  textarea: function(element) {
+    return [element.name, element.value];
+  },
+
+  select: function(element) {
+    return Form.Element.Serializers[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var value = '', opt, index = element.selectedIndex;
+    if (index >= 0) {
+      opt = element.options[index];
+      value = opt.value || opt.text;
+    }
+    return [element.name, value];
+  },
+
+  selectMany: function(element) {
+    var value = [];
+    for (var i = 0; i < element.length; i++) {
+      var opt = element.options[i];
+      if (opt.selected)
+        value.push(opt.value || opt.text);
+    }
+    return [element.name, value];
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+  initialize: function(element, frequency, callback) {
+    this.frequency = frequency;
+    this.element   = $(element);
+    this.callback  = callback;
+
+    this.lastValue = this.getValue();
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    var elements = Form.getElements(this.element);
+    for (var i = 0; i < elements.length; i++)
+      this.registerCallback(elements[i]);
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+
+  element: function(event) {
+    return event.target || event.srcElement;
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0; i < Event.observers.length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.attachEvent))
+      name = 'keydown';
+
+    Event._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.detachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      try {
+        element.detachEvent('on' + name, observer);
+      } catch (e) {}
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+if (navigator.appVersion.match(/\bMSIE\b/))
+  Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent==document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!window.opera || element.tagName=='BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';;
+    element.style.left   = left + 'px';;
+    element.style.width  = width + 'px';;
+    element.style.height = height + 'px';;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
+
+Element.addMethods();
\ No newline at end of file
diff --git a/typo3/scriptaculous/builder.js b/typo3/scriptaculous/builder.js
new file mode 100644 (file)
index 0000000..9737621
--- /dev/null
@@ -0,0 +1,119 @@
+// script.aculo.us builder.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+var Builder = {
+  NODEMAP: {
+    AREA: 'map',
+    CAPTION: 'table',
+    COL: 'table',
+    COLGROUP: 'table',
+    LEGEND: 'fieldset',
+    OPTGROUP: 'select',
+    OPTION: 'select',
+    PARAM: 'object',
+    TBODY: 'table',
+    TD: 'table',
+    TFOOT: 'table',
+    TH: 'table',
+    THEAD: 'table',
+    TR: 'table'
+  },
+  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+  //       due to a Firefox bug
+  node: function(elementName) {
+    elementName = elementName.toUpperCase();
+    
+    // try innerHTML approach
+    var parentTag = this.NODEMAP[elementName] || 'div';
+    var parentElement = document.createElement(parentTag);
+    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+    } catch(e) {}
+    var element = parentElement.firstChild || null;
+      
+    // see if browser added wrapping tags
+    if(element && (element.tagName != elementName))
+      element = element.getElementsByTagName(elementName)[0];
+    
+    // fallback to createElement approach
+    if(!element) element = document.createElement(elementName);
+    
+    // abort if nothing could be created
+    if(!element) return;
+
+    // attributes (or text)
+    if(arguments[1])
+      if(this._isStringOrNumber(arguments[1]) ||
+        (arguments[1] instanceof Array)) {
+          this._children(element, arguments[1]);
+        } else {
+          var attrs = this._attributes(arguments[1]);
+          if(attrs.length) {
+            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+              parentElement.innerHTML = "<" +elementName + " " +
+                attrs + "></" + elementName + ">";
+            } catch(e) {}
+            element = parentElement.firstChild || null;
+            // workaround firefox 1.0.X bug
+            if(!element) {
+              element = document.createElement(elementName);
+              for(attr in arguments[1]) 
+                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+            }
+            if(element.tagName != elementName)
+              element = parentElement.getElementsByTagName(elementName)[0];
+            }
+        } 
+
+    // text, or array of children
+    if(arguments[2])
+      this._children(element, arguments[2]);
+
+     return element;
+  },
+  _text: function(text) {
+     return document.createTextNode(text);
+  },
+  _attributes: function(attributes) {
+    var attrs = [];
+    for(attribute in attributes)
+      attrs.push((attribute=='className' ? 'class' : attribute) +
+          '="' + attributes[attribute].toString().escapeHTML() + '"');
+    return attrs.join(" ");
+  },
+  _children: function(element, children) {
+    if(typeof children=='object') { // array can hold nodes and text
+      children.flatten().each( function(e) {
+        if(typeof e=='object')
+          element.appendChild(e)
+        else
+          if(Builder._isStringOrNumber(e))
+            element.appendChild(Builder._text(e));
+      });
+    } else
+      if(Builder._isStringOrNumber(children)) 
+         element.appendChild(Builder._text(children));
+  },
+  _isStringOrNumber: function(param) {
+    return(typeof param=='string' || typeof param=='number');
+  },
+  dump: function(scope) { 
+    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 
+  
+    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
+      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
+      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
+      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
+      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
+      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);
+  
+    tags.each( function(tag){ 
+      scope[tag] = function() { 
+        return Builder.node.apply(Builder, [tag].concat($A(arguments)));  
+      } 
+    });
+  }
+}
\ No newline at end of file
diff --git a/typo3/scriptaculous/controls.js b/typo3/scriptaculous/controls.js
new file mode 100644 (file)
index 0000000..bb4186d
--- /dev/null
@@ -0,0 +1,833 @@
+// script.aculo.us controls.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality 
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least, 
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method 
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
+// allows smart autocompletion after linebreaks.
+
+if(typeof Effect == 'undefined')
+  throw("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
+
+    if(this.setOptions)
+      this.setOptions(options);
+    else
+      this.options = options || {};
+
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
+    this.options.frequency    = this.options.frequency || 0.4;
+    this.options.minChars     = this.options.minChars || 1;
+    this.options.onShow       = this.options.onShow || 
+      function(element, update){ 
+        if(!update.style.position || update.style.position=='absolute') {
+          update.style.position = 'absolute';
+          Position.clone(element, update, {
+            setHeight: false, 
+            offsetTop: element.offsetHeight
+          });
+        }
+        Effect.Appear(update,{duration:0.15});
+      };
+    this.options.onHide = this.options.onHide || 
+      function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+    if(typeof(this.options.tokens) == 'string') 
+      this.options.tokens = new Array(this.options.tokens);
+
+    this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
+
+    Element.hide(this.update);
+
+    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && 
+      (navigator.appVersion.indexOf('MSIE')>0) &&
+      (navigator.userAgent.indexOf('Opera')<0) &&
+      (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '<iframe id="' + this.update.id + '_iefix" '+
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+    this.iefix.style.zIndex = 1;
+    this.update.style.zIndex = 2;
+    Element.show(this.iefix);
+  },
+
+  hide: function() {
+    this.stopIndicator();
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+    if(this.iefix) Element.hide(this.iefix);
+  },
+
+  startIndicator: function() {
+    if(this.options.indicator) Element.show(this.options.indicator);
+  },
+
+  stopIndicator: function() {
+    if(this.options.indicator) Element.hide(this.options.indicator);
+  },
+
+  onKeyPress: function(event) {
+    if(this.active)
+      switch(event.keyCode) {
+       case Event.KEY_TAB:
+       case Event.KEY_RETURN:
+         this.selectEntry();
+         Event.stop(event);
+       case Event.KEY_ESC:
+         this.hide();
+         this.active = false;
+         Event.stop(event);
+         return;
+       case Event.KEY_LEFT:
+       case Event.KEY_RIGHT:
+         return;
+       case Event.KEY_UP:
+         this.markPrevious();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+      }
+     else 
+       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
+         (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  activate: function() {
+    this.changed = false;
+    this.hasFocus = true;
+    this.getUpdatedChoices();
+  },
+
+  onHover: function(event) {
+    var element = Event.findElement(event, 'LI');
+    if(this.index != element.autocompleteIndex) 
+    {
+        this.index = element.autocompleteIndex;
+        this.render();
+    }
+    Event.stop(event);
+  },
+  
+  onClick: function(event) {
+    var element = Event.findElement(event, 'LI');
+    this.index = element.autocompleteIndex;
+    this.selectEntry();
+    this.hide();
+  },
+  
+  onBlur: function(event) {
+    // needed to make click events working
+    setTimeout(this.hide.bind(this), 250);
+    this.hasFocus = false;
+    this.active = false;     
+  }, 
+  
+  render: function() {
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+  
+  markPrevious: function() {
+    if(this.index > 0) this.index--
+      else this.index = this.entryCount-1;
+    this.getEntry(this.index).scrollIntoView(true);
+  },
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
+      else this.index = 0;
+    this.getEntry(this.index).scrollIntoView(false);
+  },
+  
+  getEntry: function(index) {
+    return this.update.firstChild.childNodes[index];
+  },
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
+  },
+  
+  selectEntry: function() {
+    this.active = false;
+    this.updateElement(this.getCurrentEntry());
+  },
+
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+    var value = '';
+    if (this.options.select) {
+      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+    } else
+      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+      if (whitespace)
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
+    } else {
+      this.element.value = value;
+    }
+    this.element.focus();
+    
+    if (this.options.afterUpdateElement)
+      this.options.afterUpdateElement(this.element, selectedElement);
+  },
+
+  updateChoices: function(choices) {
+    if(!this.changed && this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.firstChild);
+
+      if(this.update.firstChild && this.update.firstChild.childNodes) {
+        this.entryCount = 
+          this.update.firstChild.childNodes.length;
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+      this.index = 0;
+      
+      if(this.entryCount==1 && this.options.autoSelect) {
+        this.selectEntry();
+        this.hide();
+      } else {
+        this.render();
+      }
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
+      this.startIndicator();
+      this.getUpdatedChoices();
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+    else
+      var ret = this.element.value;
+
+    return /\n/.test(ret) ? '' : ret;
+  },
+
+  findLastToken: function() {
+    var lastTokenPos = -1;
+
+    for (var i=0; i<this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
+    }
+    return lastTokenPos;
+  }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+  initialize: function(element, update, url, options) {
+    this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
+  },
+
+  getUpdatedChoices: function() {
+    entry = encodeURIComponent(this.options.paramName) + '=' + 
+      encodeURIComponent(this.getToken());
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
+
+    if(this.options.defaultParams) 
+      this.options.parameters += '&' + this.options.defaultParams;
+
+    new Ajax.Request(this.url, this.options);
+  },
+
+  onComplete: function(request) {
+    this.updateChoices(request.responseText);
+  }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+//                    text only at the beginning of strings in the 
+//                    autocomplete array. Defaults to true, which will
+//                    match text at the beginning of any *word* in the
+//                    strings in the autocomplete array. If you want to
+//                    search anywhere in the string, additionally set
+//                    the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+//                   a partial match (unlike minChars, which defines
+//                   how many characters are required to do any match
+//                   at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+//                 Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector' 
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+  initialize: function(element, update, array, options) {
+    this.baseInitialize(element, update, options);
+    this.options.array = array;
+  },
+
+  getUpdatedChoices: function() {
+    this.updateChoices(this.options.selector(this));
+  },
+
+  setOptions: function(options) {
+    this.options = Object.extend({
+      choices: 10,
+      partialSearch: true,
+      partialChars: 2,
+      ignoreCase: true,
+      fullSearch: false,
+      selector: function(instance) {
+        var ret       = []; // Beginning matches
+        var partial   = []; // Inside matches
+        var entry     = instance.getToken();
+        var count     = 0;
+
+        for (var i = 0; i < instance.options.array.length &&  
+          ret.length < instance.options.choices ; i++) { 
+
+          var elem = instance.options.array[i];
+          var foundPos = instance.options.ignoreCase ? 
+            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
+            elem.indexOf(entry);
+
+          while (foundPos != -1) {
+            if (foundPos == 0 && elem.length != entry.length) { 
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+                elem.substr(entry.length) + "</li>");
+              break;
+            } else if (entry.length >= instance.options.partialChars && 
+              instance.options.partialSearch && foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+                  foundPos + entry.length) + "</li>");
+                break;
+              }
+            }
+
+            foundPos = instance.options.ignoreCase ? 
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+              elem.indexOf(entry, foundPos + 1);
+
+          }
+        }
+        if (partial.length)
+          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+        return "<ul>" + ret.join('') + "</ul>";
+      }
+    }, options || {});
+  }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+  setTimeout(function() {
+    Field.activate(field);
+  }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      okButton: true,
+      okText: "ok",
+      cancelLink: true,
+      cancelText: "cancel",
+      savingText: "Saving...",
+      clickToEditText: "Click to edit",
+      okText: "ok",
+      rows: 1,
+      onComplete: function(transport, element) {
+        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert("Error communicating with the server: " + transport.responseText.stripTags());
+      },
+      callback: function(form) {
+        return Form.serialize(form);
+      },
+      handleLineBreaks: true,
+      loadingText: 'Loading...',
+      savingClassName: 'inplaceeditor-saving',
+      loadingClassName: 'inplaceeditor-loading',
+      formClassName: 'inplaceeditor-form',
+      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+      highlightendcolor: "#FFFFFF",
+      externalControl: null,
+      submitOnBlur: false,
+      ajaxOptions: {},
+      evalScripts: false
+    }, options || {});
+
+    if(!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + "-inplaceeditor";
+      if ($(this.options.formId)) {
+        // there's already a form with that name, don't specify an id
+        this.options.formId = null;
+      }
+    }
+    
+    if (this.options.externalControl) {
+      this.options.externalControl = $(this.options.externalControl);
+    }
+    
+    this.originalBackground = Element.getStyle(this.element, 'background-color');
+    if (!this.originalBackground) {
+      this.originalBackground = "transparent";
+    }
+    
+    this.element.title = this.options.clickToEditText;
+    
+    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+    Event.observe(this.element, 'click', this.onclickListener);
+    Event.observe(this.element, 'mouseover', this.mouseoverListener);
+    Event.observe(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.observe(this.options.externalControl, 'click', this.onclickListener);
+      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  },
+  enterEditMode: function(evt) {
+    if (this.saving) return;
+    if (this.editing) return;
+    this.editing = true;
+    this.onEnterEditMode();
+    if (this.options.externalControl) {
+      Element.hide(this.options.externalControl);
+    }
+    Element.hide(this.element);
+    this.createForm();
+    this.element.parentNode.insertBefore(this.form, this.element);
+    if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
+    // stop the event to avoid a page refresh in Safari
+    if (evt) {
+      Event.stop(evt);
+    }
+    return false;
+  },
+  createForm: function() {
+    this.form = document.createElement("form");
+    this.form.id = this.options.formId;
+    Element.addClassName(this.form, this.options.formClassName)
+    this.form.onsubmit = this.onSubmit.bind(this);
+
+    this.createEditField();
+
+    if (this.options.textarea) {
+      var br = document.createElement("br");
+      this.form.appendChild(br);
+    }
+
+    if (this.options.okButton) {
+      okButton = document.createElement("input");
+      okButton.type = "submit";
+      okButton.value = this.options.okText;
+      okButton.className = 'editor_ok_button';
+      this.form.appendChild(okButton);
+    }
+
+    if (this.options.cancelLink) {
+      cancelLink = document.createElement("a");
+      cancelLink.href = "#";
+      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+      cancelLink.onclick = this.onclickCancel.bind(this);
+      cancelLink.className = 'editor_cancel';      
+      this.form.appendChild(cancelLink);
+    }
+  },
+  hasHTMLLineBreaks: function(string) {
+    if (!this.options.handleLineBreaks) return false;
+    return string.match(/<br/i) || string.match(/<p>/i);
+  },
+  convertHTMLLineBreaks: function(string) {
+    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+  },
+  createEditField: function() {
+    var text;
+    if(this.options.loadTextURL) {
+      text = this.options.loadingText;
+    } else {
+      text = this.getText();
+    }
+
+    var obj = this;
+    
+    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+      this.options.textarea = false;
+      var textField = document.createElement("input");
+      textField.obj = this;
+      textField.type = "text";
+      textField.name = "value";
+      textField.value = text;
+      textField.style.backgroundColor = this.options.highlightcolor;
+      textField.className = 'editor_field';
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0) textField.size = size;
+      if (this.options.submitOnBlur)
+        textField.onblur = this.onSubmit.bind(this);
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement("textarea");
+      textArea.obj = this;
+      textArea.name = "value";
+      textArea.value = this.convertHTMLLineBreaks(text);
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      textArea.className = 'editor_field';      
+      if (this.options.submitOnBlur)
+        textArea.onblur = this.onSubmit.bind(this);
+      this.editField = textArea;
+    }
+    
+    if(this.options.loadTextURL) {
+      this.loadExternalText();
+    }
+    this.form.appendChild(this.editField);
+  },
+  getText: function() {
+    return this.element.innerHTML;
+  },
+  loadExternalText: function() {
+    Element.addClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = true;
+    new Ajax.Request(
+      this.options.loadTextURL,
+      Object.extend({
+        asynchronous: true,
+        onComplete: this.onLoadedExternalText.bind(this)
+      }, this.options.ajaxOptions)
+    );
+  },
+  onLoadedExternalText: function(transport) {
+    Element.removeClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = false;
+    this.editField.value = transport.responseText.stripTags();
+    Field.scrollFreeActivate(this.editField);
+  },
+  onclickCancel: function() {
+    this.onComplete();
+    this.leaveEditMode();
+    return false;
+  },
+  onFailure: function(transport) {
+    this.options.onFailure(transport);
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+      this.oldInnerHTML = null;
+    }
+    return false;
+  },
+  onSubmit: function() {
+    // onLoading resets these so we need to save them away for the Ajax call
+    var form = this.form;
+    var value = this.editField.value;
+    
+    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+    // to be displayed indefinitely
+    this.onLoading();
+    
+    if (this.options.evalScripts) {
+      new Ajax.Request(
+        this.url, Object.extend({
+          parameters: this.options.callback(form, value),
+          onComplete: this.onComplete.bind(this),
+          onFailure: this.onFailure.bind(this),
+          asynchronous:true, 
+          evalScripts:true
+        }, this.options.ajaxOptions));
+    } else  {
+      new Ajax.Updater(
+        { success: this.element,
+          // don't update on failure (this could be an option)
+          failure: null }, 
+        this.url, Object.extend({
+          parameters: this.options.callback(form, value),
+          onComplete: this.onComplete.bind(this),
+          onFailure: this.onFailure.bind(this)
+        }, this.options.ajaxOptions));
+    }
+    // stop the event to avoid a page refresh in Safari
+    if (arguments.length > 1) {
+      Event.stop(arguments[0]);
+    }
+    return false;
+  },
+  onLoading: function() {
+    this.saving = true;
+    this.removeForm();
+    this.leaveHover();
+    this.showSaving();
+  },
+  showSaving: function() {
+    this.oldInnerHTML = this.element.innerHTML;
+    this.element.innerHTML = this.options.savingText;
+    Element.addClassName(this.element, this.options.savingClassName);
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+  },
+  removeForm: function() {
+    if(this.form) {
+      if (this.form.parentNode) Element.remove(this.form);
+      this.form = null;
+    }
+  },
+  enterHover: function() {
+    if (this.saving) return;
+    this.element.style.backgroundColor = this.options.highlightcolor;
+    if (this.effect) {
+      this.effect.cancel();
+    }
+    Element.addClassName(this.element, this.options.hoverClassName)
+  },
+  leaveHover: function() {
+    if (this.options.backgroundColor) {
+      this.element.style.backgroundColor = this.oldBackground;
+    }
+    Element.removeClassName(this.element, this.options.hoverClassName)
+    if (this.saving) return;
+    this.effect = new Effect.Highlight(this.element, {
+      startcolor: this.options.highlightcolor,
+      endcolor: this.options.highlightendcolor,
+      restorecolor: this.originalBackground
+    });
+  },
+  leaveEditMode: function() {
+    Element.removeClassName(this.element, this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+    if (this.options.externalControl) {
+      Element.show(this.options.externalControl);
+    }
+    this.editing = false;
+    this.saving = false;
+    this.oldInnerHTML = null;
+    this.onLeaveEditMode();
+  },
+  onComplete: function(transport) {
+    this.leaveEditMode();
+    this.options.onComplete.bind(this)(transport, this.element);
+  },
+  onEnterEditMode: function() {},
+  onLeaveEditMode: function() {},
+  dispose: function() {
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+    }
+    this.leaveEditMode();
+    Event.stopObserving(this.element, 'click', this.onclickListener);
+    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  }
+};
+
+Ajax.InPlaceCollectionEditor = Class.create();
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
+  createEditField: function() {
+    if (!this.cached_selectTag) {
+      var selectTag = document.createElement("select");
+      var collection = this.options.collection || [];
+      var optionTag;
+      collection.each(function(e,i) {
+        optionTag = document.createElement("option");
+        optionTag.value = (e instanceof Array) ? e[0] : e;
+        if((typeof this.options.value == 'undefined') && 
+          ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
+        if(this.options.value==optionTag.value) optionTag.selected = true;
+        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
+        selectTag.appendChild(optionTag);
+      }.bind(this));
+      this.cached_selectTag = selectTag;
+    }
+
+    this.editField = this.cached_selectTag;
+    if(this.options.loadTextURL) this.loadExternalText();
+    this.form.appendChild(this.editField);
+    this.options.callback = function(form, value) {
+      return "value=" + encodeURIComponent(value);
+    }
+  }
+});
+
+// Delayed observer, like Form.Element.Observer, 
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+  initialize: function(element, delay, callback) {
+    this.delay     = delay || 0.5;
+    this.element   = $(element);
+    this.callback  = callback;
+    this.timer     = null;
+    this.lastValue = $F(this.element); 
+    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+  },
+  delayedListener: function(event) {
+    if(this.lastValue == $F(this.element)) return;
+    if(this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+    this.lastValue = $F(this.element);
+  },
+  onTimerEvent: function() {
+    this.timer = null;
+    this.callback(this.element, $F(this.element));
+  }
+};
diff --git a/typo3/scriptaculous/dragdrop.js b/typo3/scriptaculous/dragdrop.js
new file mode 100644 (file)
index 0000000..979268d
--- /dev/null
@@ -0,0 +1,981 @@
+// script.aculo.us dragdrop.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+// 
+// See scriptaculous.js for full license.
+
+/*--------------------------------------------------------------------------*/
+
+if(typeof Effect == 'undefined')
+  throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+var Droppables = {
+  drops: [],
+
+  remove: function(element) {
+    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
+  },
+
+  add: function(element) {
+    element = $(element);
+    var options = Object.extend({
+      greedy:     true,
+      hoverclass: null,
+      tree:       false
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = [];
+      var containment = options.containment;
+      if((typeof containment == 'object') && 
+        (containment.constructor == Array)) {
+        containment.each( function(c) { options._containers.push($(c)) });
+      } else {
+        options._containers.push($(containment));
+      }
+    }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
+
+    Element.makePositioned(element); // fix IE
+    options.element = element;
+
+    this.drops.push(options);
+  },
+  
+  findDeepestChild: function(drops) {
+    deepest = drops[0];
+      
+    for (i = 1; i < drops.length; ++i)
+      if (Element.isParent(drops[i].element, deepest.element))
+        deepest = drops[i];
+    
+    return deepest;
+  },
+
+  isContained: function(element, drop) {
+    var containmentNode;
+    if(drop.tree) {
+      containmentNode = element.treeNode; 
+    } else {
+      containmentNode = element.parentNode;
+    }
+    return drop._containers.detect(function(c) { return containmentNode == c });
+  },
+  
+  isAffected: function(point, element, drop) {
+    return (
+      (drop.element!=element) &&
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &&
+      ((!drop.accept) ||
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &&
+      Position.within(drop.element, point[0], point[1]) );
+  },
+
+  deactivate: function(drop) {
+    if(drop.hoverclass)
+      Element.removeClassName(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(drop.hoverclass)
+      Element.addClassName(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(point, element) {
+    if(!this.drops.length) return;
+    var affected = [];
+    
+    if(this.last_active) this.deactivate(this.last_active);
+    this.drops.each( function(drop) {
+      if(Droppables.isAffected(point, element, drop))
+        affected.push(drop);
+    });
+        
+    if(affected.length>0) {
+      drop = Droppables.findDeepestChild(affected);
+      Position.within(drop.element, point[0], point[1]);
+      if(drop.onHover)
+        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+      
+      Droppables.activate(drop);
+    }
+  },
+
+  fire: function(event, element) {
+    if(!this.last_active) return;
+    Position.prepare();
+
+    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element, event);
+  },
+
+  reset: function() {
+    if(this.last_active)
+      this.deactivate(this.last_active);
+  }
+}
+
+var Draggables = {
+  drags: [],
+  observers: [],
+  
+  register: function(draggable) {
+    if(this.drags.length == 0) {
+      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+      
+      Event.observe(document, "mouseup", this.eventMouseUp);
+      Event.observe(document, "mousemove", this.eventMouseMove);
+      Event.observe(document, "keypress", this.eventKeypress);
+    }
+    this.drags.push(draggable);
+  },
+  
+  unregister: function(draggable) {
+    this.drags = this.drags.reject(function(d) { return d==draggable });
+    if(this.drags.length == 0) {
+      Event.stopObserving(document, "mouseup", this.eventMouseUp);
+      Event.stopObserving(document, "mousemove", this.eventMouseMove);
+      Event.stopObserving(document, "keypress", this.eventKeypress);
+    }
+  },
+  
+  activate: function(draggable) {
+    if(draggable.options.delay) { 
+      this._timeout = setTimeout(function() { 
+        Draggables._timeout = null; 
+        window.focus(); 
+        Draggables.activeDraggable = draggable; 
+      }.bind(this), draggable.options.delay); 
+    } else {
+      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+      this.activeDraggable = draggable;
+    }
+  },
+  
+  deactivate: function() {
+    this.activeDraggable = null;
+  },
+  
+  updateDrag: function(event) {
+    if(!this.activeDraggable) return;
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    // Mozilla-based browsers fire successive mousemove events with
+    // the same coordinates, prevent needless redrawing (moz bug?)
+    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
+    this._lastPointer = pointer;
+    
+    this.activeDraggable.updateDrag(event, pointer);
+  },
+  
+  endDrag: function(event) {
+    if(this._timeout) { 
+      clearTimeout(this._timeout); 
+      this._timeout = null; 
+    }
+    if(!this.activeDraggable) return;
+    this._lastPointer = null;
+    this.activeDraggable.endDrag(event);
+    this.activeDraggable = null;
+  },
+  
+  keyPress: function(event) {
+    if(this.activeDraggable)
+      this.activeDraggable.keyPress(event);
+  },
+  
+  addObserver: function(observer) {
+    this.observers.push(observer);
+    this._cacheObserverCallbacks();
+  },
+  
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] > 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
+    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+  },
+  
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable._dragging    = {};
+
+Draggable.prototype = {
+  initialize: function(element) {
+    var defaults = {
+      handle: false,
+      reverteffect: function(element, top_offset, left_offset) {
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+          queue: {scope:'_draggable', position:'end'}
+        });
+      },
+      endeffect: function(element) {
+        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
+          queue: {scope:'_draggable', position:'end'},
+          afterFinish: function(){ 
+            Draggable._dragging[element] = false 
+          }
+        }); 
+      },
+      zindex: 1000,
+      revert: false,
+      scroll: false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
+      delay: 0
+    };
+    
+    if(arguments[1] && typeof arguments[1].endeffect == 'undefined')
+      Object.extend(defaults, {
+        starteffect: function(element) {
+          element._opacity = Element.getOpacity(element);
+          Draggable._dragging[element] = true;
+          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
+        }
+      });
+    
+    var options = Object.extend(defaults, arguments[1] || {});
+
+    this.element = $(element);
+    
+    if(options.handle && (typeof options.handle == 'string')) {
+      var h = Element.childrenWithClassName(this.element, options.handle, true);
+      if(h.length>0) this.handle = h[0];
+    }
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+    
+    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+      options.scroll = $(options.scroll);
+      this._isScrollChild = Element.childOf(this.element, options.scroll);
+    }
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.delta    = this.currentDelta();
+    this.options  = options;
+    this.dragging = false;   
+
+    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+    
+    Draggables.register(this);
+  },
+  
+  destroy: function() {
+    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+    Draggables.unregister(this);
+  },
+  
+  currentDelta: function() {
+    return([
+      parseInt(Element.getStyle(this.element,'left') || '0'),
+      parseInt(Element.getStyle(this.element,'top') || '0')]);
+  },
+  
+  initDrag: function(event) {
+    if(typeof Draggable._dragging[this.element] != 'undefined' &&
+      Draggable._dragging[this.element]) return;
+    if(Event.isLeftClick(event)) {    
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName && (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='OPTION' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+        
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var pos     = Position.cumulativeOffset(this.element);
+      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+      
+      Draggables.activate(this);
+      Event.stop(event);
+    }
+  },
+  
+  startDrag: function(event) {
+    this.dragging = true;
+    
+    if(this.options.zindex) {
+      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+      this.element.style.zIndex = this.options.zindex;
+    }
+    
+    if(this.options.ghosting) {
+      this._clone = this.element.cloneNode(true);
+      Position.absolutize(this.element);
+      this.element.parentNode.insertBefore(this._clone, this.element);
+    }
+    
+    if(this.options.scroll) {
+      if (this.options.scroll == window) {
+        var where = this._getWindowScroll(this.options.scroll);
+        this.originalScrollLeft = where.left;
+        this.originalScrollTop = where.top;
+      } else {
+        this.originalScrollLeft = this.options.scroll.scrollLeft;
+        this.originalScrollTop = this.options.scroll.scrollTop;
+      }
+    }
+    
+    Draggables.notify('onStart', this, event);
+        
+    if(this.options.starteffect) this.options.starteffect(this.element);
+  },
+  
+  updateDrag: function(event, pointer) {
+    if(!this.dragging) this.startDrag(event);
+    Position.prepare();
+    Droppables.show(pointer, this.element);
+    Draggables.notify('onDrag', this, event);
+    
+    this.draw(pointer);
+    if(this.options.change) this.options.change(this);
+    
+    if(this.options.scroll) {
+      this.stopScrolling();
+      
+      var p;
+      if (this.options.scroll == window) {
+        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+      } else {
+        p = Position.page(this.options.scroll);
+        p[0] += this.options.scroll.scrollLeft;
+        p[1] += this.options.scroll.scrollTop;
+        
+        p[0] += (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0);
+        p[1] += (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0);
+        
+        p.push(p[0]+this.options.scroll.offsetWidth);
+        p.push(p[1]+this.options.scroll.offsetHeight);
+      }
+      var speed = [0,0];
+      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+      this.startScrolling(speed);
+    }
+    
+    // fix AppleWebKit rendering
+    if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+    
+    Event.stop(event);
+  },
+  
+  finishDrag: function(event, success) {
+    this.dragging = false;
+
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
+    if(success) Droppables.fire(event, this.element);
+    Draggables.notify('onEnd', this, event);
+
+    var revert = this.options.revert;
+    if(revert && typeof revert == 'function') revert = revert(this.element);
+    
+    var d = this.currentDelta();
+    if(revert && this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+        d[1]-this.delta[1], d[0]-this.delta[0]);
+    } else {
+      this.delta = d;
+    }
+
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+      
+    Draggables.deactivate(this);
+    Droppables.reset();
+  },
+  
+  keyPress: function(event) {
+    if(event.keyCode!=Event.KEY_ESC) return;
+    this.finishDrag(event, false);
+    Event.stop(event);
+  },
+  
+  endDrag: function(event) {
+    if(!this.dragging) return;
+    this.stopScrolling();
+    this.finishDrag(event, true);
+    Event.stop(event);
+  },
+  
+  draw: function(point) {
+    var pos = Position.cumulativeOffset(this.element);
+    if(this.options.ghosting) {
+      var r   = Position.realOffset(this.element);
+      window.status = r.inspect();
+      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+    }
+    
+    var d = this.currentDelta();
+    pos[0] -= d[0]; pos[1] -= d[1];
+    
+    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
+      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+    }
+    
+    var p = [0,1].map(function(i){ 
+      return (point[i]-pos[i]-this.offset[i]) 
+    }.bind(this));
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        p = this.options.snap(p[0],p[1],this);
+      } else {
+      if(this.options.snap instanceof Array) {
+        p = p.map( function(v, i) {
+          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+      } else {
+        p = p.map( function(v) {
+          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+      }
+    }}
+    
+    var style = this.element.style;
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = p[0] + "px";
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = p[1] + "px";
+    
+    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+  },
+  
+  stopScrolling: function() {
+    if(this.scrollInterval) {
+      clearInterval(this.scrollInterval);
+      this.scrollInterval = null;
+      Draggables._lastScrollPointer = null;
+    }
+  },
+  
+  startScrolling: function(speed) {
+    if(!(speed[0] || speed[1])) return;
+    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+    this.lastScrolled = new Date();
+    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+  },
+  
+  scroll: function() {
+    var current = new Date();
+    var delta = current - this.lastScrolled;
+    this.lastScrolled = current;
+    if(this.options.scroll == window) {
+      with (this._getWindowScroll(this.options.scroll)) {
+        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+          var d = delta / 1000;
+          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+        }
+      }
+    } else {
+      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
+    }
+    
+    Position.prepare();
+    Droppables.show(Draggables._lastPointer, this.element);
+    Draggables.notify('onDrag', this);
+    if (this._isScrollChild) {
+      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+      if (Draggables._lastScrollPointer[0] < 0)
+        Draggables._lastScrollPointer[0] = 0;
+      if (Draggables._lastScrollPointer[1] < 0)
+        Draggables._lastScrollPointer[1] = 0;
+      this.draw(Draggables._lastScrollPointer);
+    }
+    
+    if(this.options.change) this.options.change(this);
+  },
+  
+  _getWindowScroll: function(w) {
+    var T, L, W, H;
+    with (w.document) {
+      if (w.document.documentElement && documentElement.scrollTop) {
+        T = documentElement.scrollTop;
+        L = documentElement.scrollLeft;
+      } else if (w.document.body) {
+        T = body.scrollTop;
+        L = body.scrollLeft;
+      }
+      if (w.innerWidth) {
+        W = w.innerWidth;
+        H = w.innerHeight;
+      } else if (w.document.documentElement && documentElement.clientWidth) {
+        W = documentElement.clientWidth;
+        H = documentElement.clientHeight;
+      } else {
+        W = body.offsetWidth;
+        H = body.offsetHeight
+      }
+    }
+    return { top: T, left: L, width: W, height: H };
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+  initialize: function(element, observer) {
+    this.element   = $(element);
+    this.observer  = observer;
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  
+  onStart: function() {
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  
+  onEnd: function() {
+    Sortable.unmark();
+    if(this.lastValue != Sortable.serialize(this.element))
+      this.observer(this.element)
+  }
+}
+
+var Sortable = {
+  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+  
+  sortables: {},
+  
+  _findRootElement: function(element) {
+    while (element.tagName != "BODY") {  
+      if(element.id && Sortable.sortables[element.id]) return element;
+      element = element.parentNode;
+    }
+  },
+
+  options: function(element) {
+    element = Sortable._findRootElement($(element));
+    if(!element) return;
+    return Sortable.sortables[element.id];
+  },
+  
+  destroy: function(element){
+    var s = Sortable.options(element);
+    
+    if(s) {
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+      
+      delete Sortable.sortables[s.element.id];
+    }
+  },
+
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,
+      treeTag:     'ul',
+      overlap:     'vertical', // one of 'vertical', 'horizontal'
+      constraint:  'vertical', // one of 'vertical', 'horizontal', false
+      containment: element,    // also takes array of elements (or id's); or false
+      handle:      false,      // or a CSS class
+      only:        false,
+      delay:       0,
+      hoverclass:  null,
+      ghosting:    false,
+      scroll:      false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      format:      this.SERIALIZE_RULE,
+      onChange:    Prototype.emptyFunction,
+      onUpdate:    Prototype.emptyFunction
+    }, arguments[1] || {});
+
+    // clear any old sortable with same element
+    this.destroy(element);
+
+    // build options for the draggables
+    var options_for_draggable = {
+      revert:      true,
+      scroll:      options.scroll,
+      scrollSpeed: options.scrollSpeed,
+      scrollSensitivity: options.scrollSensitivity,
+      delay:       options.delay,
+      ghosting:    options.ghosting,
+      constraint:  options.constraint,
+      handle:      options.handle,
+               onEnd: function (element, event) { 
+                       var h = Element.childrenWithClassName(element.element, 'dashboard-content')[0]
+                       if (h)  {
+                               if (Element.hasClassName(element.element.parentNode,'dashboard-dock'))  {
+                                       Element.update(h,'4566');
+                               } else {
+                                       Element.update(h,'123');
+                               }
+                       }
+               }
+};
+
+    if(options.starteffect)
+      options_for_draggable.starteffect = options.starteffect;
+
+    if(options.reverteffect)
+      options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
+    if(options.endeffect)
+      options_for_draggable.endeffect = options.endeffect;
+
+    if(options.zindex)
+      options_for_draggable.zindex = options.zindex;
+
+    // build options for the droppables  
+    var options_for_droppable = {
+      overlap:     options.overlap,
+      containment: options.containment,
+      tree:        options.tree,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover
+      //greedy:      !options.dropOnEmpty
+    }
+    
+    var options_for_tree = {
+      onHover:      Sortable.onEmptyHover,
+      overlap:      options.overlap,
+      containment:  options.containment,
+      hoverclass:   options.hoverclass
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // drop on empty handling
+    if(options.dropOnEmpty || options.tree) {
+      Droppables.add(element, options_for_tree);
+      options.droppables.push(element);
+    }
+
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        Element.childrenWithClassName(e, options.handle)[0] : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      if(options.tree) e.treeNode = element;
+      options.droppables.push(e);      
+    });
+    
+    if(options.tree) {
+      (Sortable.findTreeElements(element, options) || []).each( function(e) {
+        Droppables.add(e, options_for_tree);
+        e.treeNode = element;
+        options.droppables.push(e);
+      });
+    }
+
+    // keep reference
+    this.sortables[element.id] = options;
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    return Element.findChildren(
+      element, options.only, options.tree ? true : false, options.tag);
+  },
+  
+  findTreeElements: function(element, options) {
+    return Element.findChildren(
+      element, options.only, options.tree ? true : false, options.treeTag);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(Element.isParent(dropon, element)) return;
+
+    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+      return;
+    } else if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      var nextElement = dropon.nextSibling || null;
+      if(nextElement != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, nextElement);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+  
+  onEmptyHover: function(element, dropon, overlap) {
+    var oldParentNode = element.parentNode;
+    var droponOptions = Sortable.options(dropon);
+        
+    if(!Element.isParent(dropon, element)) {
+      var index;
+      
+      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+      var child = null;
+            
+      if(children) {
+        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+        
+        for (index = 0; index < children.length; index += 1) {
+          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+            offset -= Element.offsetSize (children[index], droponOptions.overlap);
+          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+            child = index + 1 < children.length ? children[index + 1] : null;
+            break;
+          } else {
+            child = children[index];
+            break;
+          }
+        }
+      }
+      
+      dropon.insertBefore(element, child);
+      
+      Sortable.options(oldParentNode).onChange(element);
+      droponOptions.onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Element.hide(Sortable._marker);
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = $('dropmarker') || document.createElement('DIV');
+      Element.hide(Sortable._marker);
+      Element.addClassName(Sortable._marker, 'dropmarker');
+      Sortable._marker.style.position = 'absolute';
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.style.left = offsets[0] + 'px';
+    Sortable._marker.style.top = offsets[1] + 'px';
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
+      else
+        Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    
+    Element.show(Sortable._marker);
+  },
+  
+  _tree: function(element, options, parent) {
+    var children = Sortable.findElements(element, options) || [];
+  
+    for (var i = 0; i < children.length; ++i) {
+      var match = children[i].id.match(options.format);
+
+      if (!match) continue;
+      
+      var child = {
+        id: encodeURIComponent(match ? match[1] : null),
+        element: element,
+        parent: parent,
+        children: new Array,
+        position: parent.children.length,
+        container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
+      }
+      
+      /* Get the element containing the children and recurse over it */
+      if (child.container)
+        this._tree(child.container, options, child)
+      
+      parent.children.push (child);
+    }
+
+    return parent; 
+  },
+
+  /* Finds the first element of the given tag type within a parent element.
+    Used for finding the first LI[ST] within a L[IST]I[TEM].*/
+  _findChildrenElement: function (element, containerTag) {
+    if (element && element.hasChildNodes)
+      for (var i = 0; i < element.childNodes.length; ++i)
+        if (element.childNodes[i].tagName == containerTag)
+          return element.childNodes[i];
+  
+    return null;
+  },
+
+  tree: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag: sortableOptions.tag,
+      treeTag: sortableOptions.treeTag,
+      only: sortableOptions.only,
+      name: element.id,
+      format: sortableOptions.format
+    }, arguments[1] || {});
+    
+    var root = {
+      id: null,
+      parent: null,
+      children: new Array,
+      container: element,
+      position: 0
+    }
+    
+    return Sortable._tree (element, options, root);
+  },
+
+  /* Construct a [i] index for a particular node */
+  _constructIndex: function(node) {
+    var index = '';
+    do {
+      if (node.id) index = '[' + node.position + ']' + index;
+    } while ((node = node.parent) != null);
+    return index;
+  },
+
+  sequence: function(element) {
+    element = $(element);
+    var options = Object.extend(this.options(element), arguments[1] || {});
+    
+    return $(this.findElements(element, options) || []).map( function(item) {
+      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+    });
+  },
+
+  setSequence: function(element, new_sequence) {
+    element = $(element);
+    var options = Object.extend(this.options(element), arguments[2] || {});
+    
+    var nodeMap = {};
+    this.findElements(element, options).each( function(n) {
+        if (n.id.match(options.format))
+            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+        n.parentNode.removeChild(n);
+    });
+   
+    new_sequence.each(function(ident) {
+      var n = nodeMap[ident];
+      if (n) {
+        n[1].appendChild(n[0]);
+        delete nodeMap[ident];
+      }
+    });
+  },
+  
+  serialize: function(element) {
+    element = $(element);
+    var options = Object.extend(Sortable.options(element), arguments[1] || {});
+    var name = encodeURIComponent(
+      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
+    
+    if (options.tree) {
+      return Sortable.tree(element, arguments[1]).children.map( function (item) {
+        return [name + Sortable._constructIndex(item) + "[id]=" + 
+                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+      }).flatten().join('&');
+    } else {
+      return Sortable.sequence(element, arguments[1]).map( function(item) {
+        return name + "[]=" + encodeURIComponent(item);
+      }).join('&');
+    }
+  }
+}
+
+/* Returns true if child is contained within element */
+Element.isParent = function(child, element) {
+  if (!child.parentNode || child == element) return false;
+
+  if (child.parentNode == element) return true;
+
+  return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {    
+  if(!element.hasChildNodes()) return null;
+  tagName = tagName.toUpperCase();
+  if(only) only = [only].flatten();
+  var elements = [];
+  $A(element.childNodes).each( function(e) {
+    if(e.tagName && e.tagName.toUpperCase()==tagName &&
+      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+        elements.push(e);
+    if(recursive) {
+      var grandchildren = Element.findChildren(e, only, recursive, tagName);
+      if(grandchildren) elements.push(grandchildren);
+    }
+  });
+
+  return (elements.length>0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+  if (type == 'vertical' || type == 'height')
+    return element.offsetHeight;
+  else
+    return element.offsetWidth;
+}
\ No newline at end of file
diff --git a/typo3/scriptaculous/effects.js b/typo3/scriptaculous/effects.js
new file mode 100644 (file)
index 0000000..8aa6d13
--- /dev/null
@@ -0,0 +1,977 @@
+// script.aculo.us effects.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+//  Justin Palmer (http://encytemedia.com/)
+//  Mark Pilgrim (http://diveintomark.org/)
+//  Martin Bialasinki
+// 
+// See scriptaculous.js for full license.  
+
+// converts rgb() and #xxx to #xxxxxx format,  
+// returns self (or first argument) if not convertable  
+String.prototype.parseColor = function() {  
+  var color = '#';  
+  if(this.slice(0,4) == 'rgb(') {  
+    var cols = this.slice(4,this.length-1).split(',');  
+    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
+  } else {  
+    if(this.slice(0,1) == '#') {  
+      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
+      if(this.length==7) color = this.toLowerCase();  
+    }  
+  }  
+  return(color.length==7 ? color : (arguments[0] || this));  
+}
+
+/*--------------------------------------------------------------------------*/
+
+Element.collectTextNodes = function(element) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+  }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? 
+        Element.collectTextNodesIgnoreClass(node, className) : ''));
+  }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+  element = $(element);  
+  Element.setStyle(element, {fontSize: (percent/100) + 'em'});   
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+}
+
+Element.getOpacity = function(element){  
+  var opacity;
+  if (opacity = Element.getStyle(element, 'opacity'))  
+    return parseFloat(opacity);  
+  if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))  
+    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
+  return 1.0;  
+}
+
+Element.setOpacity = function(element, value){  
+  element= $(element);  
+  if (value == 1){
+    Element.setStyle(element, { opacity: 
+      (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
+      0.999999 : 1.0 });
+    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
+      Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
+  } else {  
+    if(value < 0.00001) value = 0;  
+    Element.setStyle(element, {opacity: value});
+    if(/MSIE/.test(navigator.userAgent) && !window.opera)  
+     Element.setStyle(element, 
+       { filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
+                 'alpha(opacity='+value*100+')' });  
+  }
+}  
+Element.getInlineOpacity = function(element){  
+  return $(element).style.opacity || '';
+}  
+
+Element.childrenWithClassName = function(element, className, findFirst) {
+  var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
+  var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) { 
+    return (c.className && c.className.match(classNameRegExp));
+  });
+  if(!results) results = [];
+  return results;
+}
+
+Element.forceRerendering = function(element) {
+  try {
+    element = $(element);
+    var n = document.createTextNode(' ');
+    element.appendChild(n);
+    element.removeChild(n);
+  } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+  var args = arguments;
+  this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+  _elementDoesNotExistError: {
+    name: 'ElementDoesNotExistError',
+    message: 'The specified DOM element does not exist, but is required for this effect to operate'
+  },
+  tagifyText: function(element) {
+    if(typeof Builder == 'undefined')
+      throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
+      
+    var tagifyStyle = 'position:relative';
+    if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
+    element = $(element);
+    $A(element.childNodes).each( function(child) {
+      if(child.nodeType==3) {
+        child.nodeValue.toArray().each( function(character) {
+          element.insertBefore(
+            Builder.node('span',{style: tagifyStyle},
+              character == ' ' ? String.fromCharCode(160) : character), 
+              child);
+        });
+        Element.remove(child);
+      }
+    });
+  },
+  multiple: function(element, effect) {
+    var elements;
+    if(((typeof element == 'object') || 
+        (typeof element == 'function')) && 
+       (element.length))
+      elements = element;
+    else
+      elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var masterDelay = options.delay;
+
+    $A(elements).each( function(element, index) {
+      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+    });
+  },
+  PAIRS: {
+    'slide':  ['SlideDown','SlideUp'],
+    'blind':  ['BlindDown','BlindUp'],
+    'appear': ['Appear','Fade']
+  },
+  toggle: function(element, effect) {
+    element = $(element);
+    effect = (effect || 'appear').toLowerCase();
+    var options = Object.extend({
+      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+    }, arguments[2] || {});
+    Effect[element.visible() ? 
+      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+  }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = Prototype.K;
+
+Effect.Transitions.sinoidal = function(pos) {
+  return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse  = function(pos) {
+  return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+}
+Effect.Transitions.wobble = function(pos) {
+  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+  return (Math.floor(pos*10) % 2 == 0 ? 
+    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+Effect.Transitions.none = function(pos) {
+  return 0;
+}
+Effect.Transitions.full = function(pos) {
+  return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+  initialize: function() {
+    this.effects  = [];
+    this.interval = null;
+  },
+  _each: function(iterator) {
+    this.effects._each(iterator);
+  },
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    var position = (typeof effect.options.queue == 'string') ? 
+      effect.options.queue : effect.options.queue.position;
+    
+    switch(position) {
+      case 'front':
+        // move unstarted effects after this effect  
+        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+            e.startOn  += effect.finishOn;
+            e.finishOn += effect.finishOn;
+          });
+        break;
+      case 'end':
+        // start effect after last queued effect has finished
+        timestamp = this.effects.pluck('finishOn').max() || timestamp;
+        break;
+    }
+    
+    effect.startOn  += timestamp;
+    effect.finishOn += timestamp;
+
+    if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+      this.effects.push(effect);
+    
+    if(!this.interval) 
+      this.interval = setInterval(this.loop.bind(this), 40);
+  },
+  remove: function(effect) {
+    this.effects = this.effects.reject(function(e) { return e==effect });
+    if(this.effects.length == 0) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  },
+  loop: function() {
+    var timePos = new Date().getTime();
+    this.effects.invoke('loop', timePos);
+  }
+});
+
+Effect.Queues = {
+  instances: $H(),
+  get: function(queueName) {
+    if(typeof queueName != 'string') return queueName;
+    
+    if(!this.instances[queueName])
+      this.instances[queueName] = new Effect.ScopedQueue();
+      
+    return this.instances[queueName];
+  }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+  transition: Effect.Transitions.sinoidal,
+  duration:   1.0,   // seconds
+  fps:        25.0,  // max. 25fps due to Effect.Queue implementation
+  sync:       false, // true for combining
+  from:       0.0,
+  to:         1.0,
+  delay:      0.0,
+  queue:      'parallel'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+  position: null,
+  start: function(options) {
+    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+    this.currentFrame = 0;
+    this.state        = 'idle';
+    this.startOn      = this.options.delay*1000;
+    this.finishOn     = this.startOn + (this.options.duration*1000);
+    this.event('beforeStart');
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).add(this);
+  },
+  loop: function(timePos) {
+    if(timePos >= this.startOn) {
+      if(timePos >= this.finishOn) {
+        this.render(1.0);
+        this.cancel();
+        this.event('beforeFinish');
+        if(this.finish) this.finish(); 
+        this.event('afterFinish');
+        return;  
+      }
+      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
+      var frame = Math.round(pos * this.options.fps * this.options.duration);
+      if(frame > this.currentFrame) {
+        this.render(pos);
+        this.currentFrame = frame;
+      }
+    }
+  },
+  render: function(pos) {
+    if(this.state == 'idle') {
+      this.state = 'running';
+      this.event('beforeSetup');
+      if(this.setup) this.setup();
+      this.event('afterSetup');
+    }
+    if(this.state == 'running') {
+      if(this.options.transition) pos = this.options.transition(pos);
+      pos *= (this.options.to-this.options.from);
+      pos += this.options.from;
+      this.position = pos;
+      this.event('beforeUpdate');
+      if(this.update) this.update(pos);
+      this.event('afterUpdate');
+    }
+  },
+  cancel: function() {
+    if(!this.options.sync)
+      Effect.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).remove(this);
+    this.state = 'finished';
+  },
+  event: function(eventName) {
+    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+    if(this.options[eventName]) this.options[eventName](this);
+  },
+  inspect: function() {
+    return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+  }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+  initialize: function(effects) {
+    this.effects = effects || [];
+    this.start(arguments[1]);
+  },
+  update: function(position) {
+    this.effects.invoke('render', position);
+  },
+  finish: function(position) {
+    this.effects.each( function(effect) {
+      effect.render(1.0);
+      effect.cancel();
+      effect.event('beforeFinish');
+      if(effect.finish) effect.finish(position);
+      effect.event('afterFinish');
+    });
+  }
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    // make this work on IE on elements without 'layout'
+    if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
+      this.element.setStyle({zoom: 1});
+    var options = Object.extend({
+      from: this.element.getOpacity() || 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    this.element.setOpacity(position);
+  }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({
+      x:    0,
+      y:    0,
+      mode: 'relative'
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Bug in Opera: Opera returns the "real" position of a static element or
+    // relative element that does not have top/left explicitly set.
+    // ==> Always set top and left for position relative elements in your stylesheets 
+    // (to 0 if you do not need them) 
+    this.element.makePositioned();
+    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
+    if(this.options.mode == 'absolute') {
+      // absolute movement, so we need to calc deltaX and deltaY
+      this.options.x = this.options.x - this.originalLeft;
+      this.options.y = this.options.y - this.originalTop;
+    }
+  },
+  update: function(position) {
+    this.element.setStyle({
+      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
+      top:  Math.round(this.options.y  * position + this.originalTop)  + 'px'
+    });
+  }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+  return new Effect.Move(element, 
+    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({
+      scaleX: true,
+      scaleY: true,
+      scaleContent: true,
+      scaleFromCenter: false,
+      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleFrom: 100.0,
+      scaleTo:   percent
+    }, arguments[2] || {});
+    this.start(options);
+  },
+  setup: function() {
+    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+    this.elementPositioning = this.element.getStyle('position');
+    
+    this.originalStyle = {};
+    ['top','left','width','height','fontSize'].each( function(k) {
+      this.originalStyle[k] = this.element.style[k];
+    }.bind(this));
+      
+    this.originalTop  = this.element.offsetTop;
+    this.originalLeft = this.element.offsetLeft;
+    
+    var fontSize = this.element.getStyle('font-size') || '100%';
+    ['em','px','%','pt'].each( function(fontSizeType) {
+      if(fontSize.indexOf(fontSizeType)>0) {
+        this.fontSize     = parseFloat(fontSize);
+        this.fontSizeType = fontSizeType;
+      }
+    }.bind(this));
+    
+    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+    
+    this.dims = null;
+    if(this.options.scaleMode=='box')
+      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+    if(/^content/.test(this.options.scaleMode))
+      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+    if(!this.dims)
+      this.dims = [this.options.scaleMode.originalHeight,
+                   this.options.scaleMode.originalWidth];
+  },
+  update: function(position) {
+    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+    if(this.options.scaleContent && this.fontSize)
+      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+  },
+  finish: function(position) {
+    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+  },
+  setDimensions: function(height, width) {
+    var d = {};
+    if(this.options.scaleX) d.width = Math.round(width) + 'px';
+    if(this.options.scaleY) d.height = Math.round(height) + 'px';
+    if(this.options.scaleFromCenter) {
+      var topd  = (height - this.dims[0])/2;
+      var leftd = (width  - this.dims[1])/2;
+      if(this.elementPositioning == 'absolute') {
+        if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
+        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+      } else {
+        if(this.options.scaleY) d.top = -topd + 'px';
+        if(this.options.scaleX) d.left = -leftd + 'px';
+      }
+    }
+    this.element.setStyle(d);
+  }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Prevent executing on elements not in the layout flow
+    if(this.element.getStyle('display')=='none') { this.cancel(); return; }
+    // Disable background image during the effect
+    this.oldStyle = {
+      backgroundImage: this.element.getStyle('background-image') };
+    this.element.setStyle({backgroundImage: 'none'});
+    if(!this.options.endcolor)
+      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+    if(!this.options.restorecolor)
+      this.options.restorecolor = this.element.getStyle('background-color');
+    // init color calculations
+    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+  },
+  update: function(position) {
+    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+  },
+  finish: function() {
+    this.element.setStyle(Object.extend(this.oldStyle, {
+      backgroundColor: this.options.restorecolor
+    }));
+  }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    this.start(arguments[1] || {});
+  },
+  setup: function() {
+    Position.prepare();
+    var offsets = Position.cumulativeOffset(this.element);
+    if(this.options.offset) offsets[1] += this.options.offset;
+    var max = window.innerHeight ? 
+      window.height - window.innerHeight :
+      document.body.scrollHeight - 
+        (document.documentElement.clientHeight ? 
+          document.documentElement.clientHeight : document.body.clientHeight);
+    this.scrollStart = Position.deltaY;
+    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+  },
+  update: function(position) {
+    Position.prepare();
+    window.scrollTo(Position.deltaX, 
+      this.scrollStart + (position*this.delta));
+  }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
+  var options = Object.extend({
+  from: element.getOpacity() || 1.0,
+  to:   0.0,
+  afterFinishInternal: function(effect) { 
+    if(effect.options.to!=0) return;
+    effect.element.hide();
+    effect.element.setStyle({opacity: oldOpacity}); 
+  }}, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+  element = $(element);
+  var options = Object.extend({
+  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+  to:   1.0,
+  // force Safari to render floated elements properly
+  afterFinishInternal: function(effect) {
+    effect.element.forceRerendering();
+  },
+  beforeSetup: function(effect) {
+    effect.element.setOpacity(effect.options.from);
+    effect.element.show(); 
+  }}, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+  element = $(element);
+  var oldStyle = { 
+    opacity: element.getInlineOpacity(), 
+    position: element.getStyle('position'),
+    top:  element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height
+  };
+  return new Effect.Parallel(
+   [ new Effect.Scale(element, 200, 
+      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
+     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
+     Object.extend({ duration: 1.0, 
+      beforeSetupInternal: function(effect) {
+        Position.absolutize(effect.effects[0].element)
+      },
+      afterFinishInternal: function(effect) {
+         effect.effects[0].element.hide();
+         effect.effects[0].element.setStyle(oldStyle); }
+     }, arguments[1] || {})
+   );
+}
+
+Effect.BlindUp = function(element) {
+  element = $(element);
+  element.makeClipping();
+  return new Effect.Scale(element, 0,
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      restoreAfterFinish: true,
+      afterFinishInternal: function(effect) {
+        effect.element.hide();
+        effect.element.undoClipping();
+      } 
+    }, arguments[1] || {})
+  );
+}
+
+Effect.BlindDown = function(element) {
+  element = $(element);
+  var elementDimensions = element.getDimensions();
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
+    scaleX: false,
+    scaleFrom: 0,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      effect.element.makeClipping();
+      effect.element.setStyle({height: '0px'});
+      effect.element.show(); 
+    },  
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping();
+    }
+  }, arguments[1] || {}));
+}
+
+Effect.SwitchOff = function(element) {
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
+  return new Effect.Appear(element, Object.extend({
+    duration: 0.4,
+    from: 0,
+    transition: Effect.Transitions.flicker,
+    afterFinishInternal: function(effect) {
+      new Effect.Scale(effect.element, 1, { 
+        duration: 0.3, scaleFromCenter: true,
+        scaleX: false, scaleContent: false, restoreAfterFinish: true,
+        beforeSetup: function(effect) { 
+          effect.element.makePositioned();
+          effect.element.makeClipping();
+        },
+        afterFinishInternal: function(effect) {
+          effect.element.hide();
+          effect.element.undoClipping();
+          effect.element.undoPositioned();
+          effect.element.setStyle({opacity: oldOpacity});
+        }
+      })
+    }
+  }, arguments[1] || {}));
+}
+
+Effect.DropOut = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left'),
+    opacity: element.getInlineOpacity() };
+  return new Effect.Parallel(
+    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
+      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+    Object.extend(
+      { duration: 0.5,
+        beforeSetup: function(effect) {
+          effect.effects[0].element.makePositioned(); 
+        },
+        afterFinishInternal: function(effect) {
+          effect.effects[0].element.hide();
+          effect.effects[0].element.undoPositioned();
+          effect.effects[0].element.setStyle(oldStyle);
+        } 
+      }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left') };
+    return new Effect.Move(element, 
+      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+        effect.element.undoPositioned();
+        effect.element.setStyle(oldStyle);
+  }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+  element = $(element);
+  element.cleanWhitespace();
+  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
+  var elementDimensions = element.getDimensions();
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
+    scaleX: false, 
+    scaleFrom: window.opera ? 0 : 1,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      effect.element.makePositioned();
+      effect.element.firstChild.makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping();
+      effect.element.setStyle({height: '0px'});
+      effect.element.show(); },
+    afterUpdateInternal: function(effect) {
+      effect.element.firstChild.setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
+    },
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping(); 
+      // IE will crash if child is undoPositioned first
+      if(/MSIE/.test(navigator.userAgent) && !window.opera){
+        effect.element.undoPositioned();
+        effect.element.firstChild.undoPositioned();
+      }else{
+        effect.element.firstChild.undoPositioned();
+        effect.element.undoPositioned();
+      }
+      effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
+    }, arguments[1] || {})
+  );
+}
+
+Effect.SlideUp = function(element) {
+  element = $(element);
+  element.cleanWhitespace();
+  var oldInnerBottom = $(element.firstChild).getStyle('bottom');
+  return new Effect.Scale(element, window.opera ? 0 : 1,
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'box',
+    scaleFrom: 100,
+    restoreAfterFinish: true,
+    beforeStartInternal: function(effect) {
+      effect.element.makePositioned();
+      effect.element.firstChild.makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping();
+      effect.element.show(); },  
+    afterUpdateInternal: function(effect) {
+      effect.element.firstChild.setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); },
+    afterFinishInternal: function(effect) {
+      effect.element.hide();
+      effect.element.undoClipping();
+      effect.element.firstChild.undoPositioned();
+      effect.element.undoPositioned();
+      effect.element.setStyle({bottom: oldInnerBottom}); }
+   }, arguments[1] || {})
+  );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish 
+Effect.Squish = function(element) {
+  return new Effect.Scale(element, window.opera ? 1 : 0, 
+    { restoreAfterFinish: true,
+      beforeSetup: function(effect) {
+        effect.element.makeClipping(effect.element); },  
+      afterFinishInternal: function(effect) {
+        effect.element.hide(effect.element); 
+        effect.element.undoClipping(effect.element); }
+  });
+}
+
+Effect.Grow = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.full
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();    
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = dims.width;
+      initialMoveY = moveY = 0;
+      moveX = -dims.width;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = dims.height;
+      moveY = -dims.height;
+      break;
+    case 'bottom-right':
+      initialMoveX = dims.width;
+      initialMoveY = dims.height;
+      moveX = -dims.width;
+      moveY = -dims.height;
+      break;
+    case 'center':
+      initialMoveX = dims.width / 2;
+      initialMoveY = dims.height / 2;
+      moveX = -dims.width / 2;
+      moveY = -dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Move(element, {
+    x: initialMoveX,
+    y: initialMoveY,
+    duration: 0.01, 
+    beforeSetup: function(effect) {
+      effect.element.hide();
+      effect.element.makeClipping();
+      effect.element.makePositioned();
+    },
+    afterFinishInternal: function(effect) {
+      new Effect.Parallel(
+        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+          new Effect.Scale(effect.element, 100, {
+            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+        ], Object.extend({
+             beforeSetup: function(effect) {
+               effect.effects[0].element.setStyle({height: '0px'});
+               effect.effects[0].element.show(); 
+             },
+             afterFinishInternal: function(effect) {
+               effect.effects[0].element.undoClipping();
+               effect.effects[0].element.undoPositioned();
+               effect.effects[0].element.setStyle(oldStyle); 
+             }
+           }, options)
+      )
+    }
+  });
+}
+
+Effect.Shrink = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.none
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = dims.width;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = dims.height;
+      break;
+    case 'bottom-right':
+      moveX = dims.width;
+      moveY = dims.height;
+      break;
+    case 'center':  
+      moveX = dims.width / 2;
+      moveY = dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Parallel(
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+    ], Object.extend({            
+         beforeStartInternal: function(effect) {
+           effect.effects[0].element.makePositioned();
+           effect.effects[0].element.makeClipping(); },
+         afterFinishInternal: function(effect) {
+           effect.effects[0].element.hide();
+           effect.effects[0].element.undoClipping();
+           effect.effects[0].element.undoPositioned();
+           effect.effects[0].element.setStyle(oldStyle); }
+       }, options)
+  );
+}
+
+Effect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var oldOpacity = element.getInlineOpacity();
+  var transition = options.transition || Effect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+  reverser.bind(transition);
+  return new Effect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 3.0, from: 0,
+      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+    }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height };
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 5, Object.extend({   
+    scaleContent: false,
+    scaleX: false,
+    afterFinishInternal: function(effect) {
+    new Effect.Scale(element, 1, { 
+      scaleContent: false, 
+      scaleY: false,
+      afterFinishInternal: function(effect) {
+        effect.element.hide();
+        effect.element.undoClipping(); 
+        effect.element.setStyle(oldStyle);
+      } });
+  }}, arguments[1] || {}));
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each( 
+  function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+  s = effect.gsub(/_/, '-').camelize();
+  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+  new Effect[effect_class](element, options);
+  return $(element);
+};
+
+Element.addMethods();
\ No newline at end of file
diff --git a/typo3/scriptaculous/scriptaculous.js b/typo3/scriptaculous/scriptaculous.js
new file mode 100644 (file)
index 0000000..42d5dce
--- /dev/null
@@ -0,0 +1,49 @@
+// script.aculo.us scriptaculous.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var Scriptaculous = {
+  Version: '1.6.4',
+  require: function(libraryName) {
+    // inserting via DOM fails in Safari 2.0, so brute force approach
+    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
+  },
+  load: function() {
+    if((typeof Prototype=='undefined') || 
+       (typeof Element == 'undefined') || 
+       (typeof Element.Methods=='undefined') ||
+       parseFloat(Prototype.Version.split(".")[0] + "." +
+                  Prototype.Version.split(".")[1]) < 1.5)
+       throw("script.aculo.us requires the Prototype JavaScript framework >= 1.5.0");
+    
+    $A(document.getElementsByTagName("script")).findAll( function(s) {
+      return (s.src && s.src.match(/scriptaculous\.js(\?.*)?$/))
+    }).each( function(s) {
+      var path = s.src.replace(/scriptaculous\.js(\?.*)?$/,'');
+      var includes = s.src.match(/\?.*load=([a-z,]*)/);
+      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider').split(',').each(
+       function(include) { Scriptaculous.require(path+include+'.js') });
+    });
+  }
+}
+
+Scriptaculous.load();
\ No newline at end of file
diff --git a/typo3/scriptaculous/slider.js b/typo3/scriptaculous/slider.js
new file mode 100644 (file)
index 0000000..859ce1d
--- /dev/null
@@ -0,0 +1,294 @@
+// script.aculo.us slider.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Marty Haught, Thomas Fuchs 
+//
+// See http://script.aculo.us for more info
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+if(!Control) var Control = {};
+Control.Slider = Class.create();
+
+// options:
+//  axis: 'vertical', or 'horizontal' (default)
+//
+// callbacks:
+//  onChange(value)
+//  onSlide(value)
+Control.Slider.prototype = {
+  initialize: function(handle, track, options) {
+    var slider = this;
+    
+    if(handle instanceof Array) {
+      this.handles = handle.collect( function(e) { return $(e) });
+    } else {
+      this.handles = [$(handle)];
+    }
+    
+    this.track   = $(track);
+    this.options = options || {};
+
+    this.axis      = this.options.axis || 'horizontal';
+    this.increment = this.options.increment || 1;
+    this.step      = parseInt(this.options.step || '1');
+    this.range     = this.options.range || $R(0,1);
+    
+    this.value     = 0; // assure backwards compat
+    this.values    = this.handles.map( function() { return 0 });
+    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
+    this.options.startSpan = $(this.options.startSpan || null);
+    this.options.endSpan   = $(this.options.endSpan || null);
+
+    this.restricted = this.options.restricted || false;
+
+    this.maximum   = this.options.maximum || this.range.end;
+    this.minimum   = this.options.minimum || this.range.start;
+
+    // Will be used to align the handle onto the track, if necessary
+    this.alignX = parseInt(this.options.alignX || '0');
+    this.alignY = parseInt(this.options.alignY || '0');
+    
+    this.trackLength = this.maximumOffset() - this.minimumOffset();
+
+    this.handleLength = this.isVertical() ? 
+      (this.handles[0].offsetHeight != 0 ? 
+        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) : 
+      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth : 
+        this.handles[0].style.width.replace(/px$/,""));
+
+    this.active   = false;
+    this.dragging = false;
+    this.disabled = false;
+
+    if(this.options.disabled) this.setDisabled();
+
+    // Allowed values array
+    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
+    if(this.allowedValues) {
+      this.minimum = this.allowedValues.min();
+      this.maximum = this.allowedValues.max();
+    }
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+
+    // Initialize handles in reverse (make sure first handle is active)
+    this.handles.each( function(h,i) {
+      i = slider.handles.length-1-i;
+      slider.setValue(parseFloat(
+        (slider.options.sliderValue instanceof Array ? 
+          slider.options.sliderValue[i] : slider.options.sliderValue) || 
+         slider.range.start), i);
+      Element.makePositioned(h); // fix IE
+      Event.observe(h, "mousedown", slider.eventMouseDown);
+    });
+    
+    Event.observe(this.track, "mousedown", this.eventMouseDown);
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+    
+    this.initialized = true;
+  },
+  dispose: function() {
+    var slider = this;    
+    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
+    Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    this.handles.each( function(h) {
+      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
+    });
+  },
+  setDisabled: function(){
+    this.disabled = true;
+  },
+  setEnabled: function(){
+    this.disabled = false;
+  },  
+  getNearestValue: function(value){
+    if(this.allowedValues){
+      if(value >= this.allowedValues.max()) return(this.allowedValues.max());
+      if(value <= this.allowedValues.min()) return(this.allowedValues.min());
+      
+      var offset = Math.abs(this.allowedValues[0] - value);
+      var newValue = this.allowedValues[0];
+      this.allowedValues.each( function(v) {
+        var currentOffset = Math.abs(v - value);
+        if(currentOffset <= offset){
+          newValue = v;
+          offset = currentOffset;
+        } 
+      });
+      return newValue;
+    }
+    if(value > this.range.end) return this.range.end;
+    if(value < this.range.start) return this.range.start;
+    return value;
+  },
+  setValue: function(sliderValue, handleIdx){
+    if(!this.active) {
+      this.activeHandleIdx = handleIdx || 0;
+      this.activeHandle    = this.handles[this.activeHandleIdx];
+      this.updateStyles();
+    }
+    handleIdx = handleIdx || this.activeHandleIdx || 0;
+    if(this.initialized && this.restricted) {
+      if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
+        sliderValue = this.values[handleIdx-1];
+      if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
+        sliderValue = this.values[handleIdx+1];
+    }
+    sliderValue = this.getNearestValue(sliderValue);
+    this.values[handleIdx] = sliderValue;
+    this.value = this.values[0]; // assure backwards compat
+    
+    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 
+      this.translateToPx(sliderValue);
+    
+    this.drawSpans();
+    if(!this.dragging || !this.event) this.updateFinished();
+  },
+  setValueBy: function(delta, handleIdx) {
+    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
+      handleIdx || this.activeHandleIdx || 0);
+  },
+  translateToPx: function(value) {
+    return Math.round(
+      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * 
+      (value - this.range.start)) + "px";
+  },
+  translateToValue: function(offset) {
+    return ((offset/(this.trackLength-this.handleLength) * 
+      (this.range.end-this.range.start)) + this.range.start);
+  },
+  getRange: function(range) {
+    var v = this.values.sortBy(Prototype.K); 
+    range = range || 0;
+    return $R(v[range],v[range+1]);
+  },
+  minimumOffset: function(){
+    return(this.isVertical() ? this.alignY : this.alignX);
+  },
+  maximumOffset: function(){
+    return(this.isVertical() ? 
+      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
+        this.track.style.height.replace(/px$/,"")) - this.alignY : 
+      (this.track.offsetWidth != 0 ? this.track.offsetWidth : 
+        this.track.style.width.replace(/px$/,"")) - this.alignY);
+  },  
+  isVertical:  function(){
+    return (this.axis == 'vertical');
+  },
+  drawSpans: function() {
+    var slider = this;
+    if(this.spans)
+      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
+    if(this.options.startSpan)
+      this.setSpan(this.options.startSpan,
+        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
+    if(this.options.endSpan)
+      this.setSpan(this.options.endSpan, 
+        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
+  },
+  setSpan: function(span, range) {
+    if(this.isVertical()) {
+      span.style.top = this.translateToPx(range.start);
+      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
+    } else {
+      span.style.left = this.translateToPx(range.start);
+      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
+    }
+  },
+  updateStyles: function() {
+    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
+    Element.addClassName(this.activeHandle, 'selected');
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      if(!this.disabled){
+        this.active = true;
+        
+        var handle = Event.element(event);
+        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
+        var track = handle;
+        if(track==this.track) {
+          var offsets  = Position.cumulativeOffset(this.track); 
+          this.event = event;
+          this.setValue(this.translateToValue( 
+           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
+          ));
+          var offsets  = Position.cumulativeOffset(this.activeHandle);
+          this.offsetX = (pointer[0] - offsets[0]);
+          this.offsetY = (pointer[1] - offsets[1]);
+        } else {
+          // find the handle (prevents issues with Safari)
+          while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
+            handle = handle.parentNode;
+        
+          this.activeHandle    = handle;
+          this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
+          this.updateStyles();
+        
+          var offsets  = Position.cumulativeOffset(this.activeHandle);
+          this.offsetX = (pointer[0] - offsets[0]);
+          this.offsetY = (pointer[1] - offsets[1]);
+        }
+      }
+      Event.stop(event);
+    }
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) this.dragging = true;
+      this.draw(event);
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+      Event.stop(event);
+   }
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.track);
+    pointer[0] -= this.offsetX + offsets[0];
+    pointer[1] -= this.offsetY + offsets[1];
+    this.event = event;
+    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
+    if(this.initialized && this.options.onSlide)
+      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },  
+  finishDrag: function(event, success) {
+    this.active = false;
+    this.dragging = false;
+    this.updateFinished();
+  },
+  updateFinished: function() {
+    if(this.initialized && this.options.onChange) 
+      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
+    this.event = null;
+  }
+}
\ No newline at end of file
diff --git a/typo3/scriptaculous/unittest.js b/typo3/scriptaculous/unittest.js
new file mode 100644 (file)
index 0000000..215563a
--- /dev/null
@@ -0,0 +1,552 @@
+// script.aculo.us unittest.js v1.6.4, Wed Sep 06 11:30:58 CEST 2006
+
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+//           (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+// experimental, Firefox-only
+Event.simulateMouse = function(element, eventName) {
+  var options = Object.extend({
+    pointerX: 0,
+    pointerY: 0,
+    buttons: 0
+  }, arguments[2] || {});
+  var oEvent = document.createEvent("MouseEvents");
+  oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
+    options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
+    false, false, false, false, 0, $(element));
+  
+  if(this.mark) Element.remove(this.mark);
+  this.mark = document.createElement('div');
+  this.mark.appendChild(document.createTextNode(" "));
+  document.body.appendChild(this.mark);
+  this.mark.style.position = 'absolute';
+  this.mark.style.top = options.pointerY + "px";
+  this.mark.style.left = options.pointerX + "px";
+  this.mark.style.width = "5px";
+  this.mark.style.height = "5px;";
+  this.mark.style.borderTop = "1px solid red;"
+  this.mark.style.borderLeft = "1px solid red;"
+  
+  if(this.step)
+    alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
+  
+  $(element).dispatchEvent(oEvent);
+};
+
+// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
+// You need to downgrade to 1.0.4 for now to get this working
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
+Event.simulateKey = function(element, eventName) {
+  var options = Object.extend({
+    ctrlKey: false,
+    altKey: false,
+    shiftKey: false,
+    metaKey: false,
+    keyCode: 0,
+    charCode: 0
+  }, arguments[2] || {});
+
+  var oEvent = document.createEvent("KeyEvents");
+  oEvent.initKeyEvent(eventName, true, true, window, 
+    options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+    options.keyCode, options.charCode );
+  $(element).dispatchEvent(oEvent);
+};
+
+Event.simulateKeys = function(element, command) {
+  for(var i=0; i<command.length; i++) {
+    Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
+  }
+};
+
+var Test = {}
+Test.Unit = {};
+
+// security exception workaround
+Test.Unit.inspect = Object.inspect;
+
+Test.Unit.Logger = Class.create();
+Test.Unit.Logger.prototype = {
+  initialize: function(log) {
+    this.log = $(log);
+    if (this.log) {
+      this._createLogTable();
+    }
+  },
+  start: function(testName) {
+    if (!this.log) return;
+    this.testName = testName;
+    this.lastLogLine = document.createElement('tr');
+    this.statusCell = document.createElement('td');
+    this.nameCell = document.createElement('td');
+    this.nameCell.appendChild(document.createTextNode(testName));
+    this.messageCell = document.createElement('td');
+    this.lastLogLine.appendChild(this.statusCell);
+    this.lastLogLine.appendChild(this.nameCell);
+    this.lastLogLine.appendChild(this.messageCell);
+    this.loglines.appendChild(this.lastLogLine);
+  },
+  finish: function(status, summary) {
+    if (!this.log) return;
+    this.lastLogLine.className = status;
+    this.statusCell.innerHTML = status;
+    this.messageCell.innerHTML = this._toHTML(summary);
+  },
+  message: function(message) {
+    if (!this.log) return;
+    this.messageCell.innerHTML = this._toHTML(message);
+  },
+  summary: function(summary) {
+    if (!this.log) return;
+    this.logsummary.innerHTML = this._toHTML(summary);
+  },
+  _createLogTable: function() {
+    this.log.innerHTML =
+    '<div id="logsummary"></div>' +
+    '<table id="logtable">' +
+    '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
+    '<tbody id="loglines"></tbody>' +
+    '</table>';
+    this.logsummary = $('logsummary')
+    this.loglines = $('loglines');
+  },
+  _toHTML: function(txt) {
+    return txt.escapeHTML().replace(/\n/g,"<br/>");
+  }
+}
+
+Test.Unit.Runner = Class.create();
+Test.Unit.Runner.prototype = {
+  initialize: function(testcases) {
+    this.options = Object.extend({
+      testLog: 'testlog'
+    }, arguments[1] || {});
+    this.options.resultsURL = this.parseResultsURLQueryParameter();
+    if (this.options.testLog) {
+      this.options.testLog = $(this.options.testLog) || null;
+    }
+    if(this.options.tests) {
+      this.tests = [];
+      for(var i = 0; i < this.options.tests.length; i++) {
+        if(/^test/.test(this.options.tests[i])) {
+          this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
+        }
+      }
+    } else {
+      if (this.options.test) {
+        this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
+      } else {
+        this.tests = [];
+        for(var testcase in testcases) {
+          if(/^test/.test(testcase)) {
+            this.tests.push(
+               new Test.Unit.Testcase(
+                 this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, 
+                 testcases[testcase], testcases["setup"], testcases["teardown"]
+               ));
+          }
+        }
+      }
+    }
+    this.currentTest = 0;
+    this.logger = new Test.Unit.Logger(this.options.testLog);
+    setTimeout(this.runTests.bind(this), 1000);
+  },
+  parseResultsURLQueryParameter: function() {
+    return window.location.search.parseQuery()["resultsURL"];
+  },
+  // Returns:
+  //  "ERROR" if there was an error,
+  //  "FAILURE" if there was a failure, or
+  //  "SUCCESS" if there was neither
+  getResult: function() {
+    var hasFailure = false;
+    for(var i=0;i<this.tests.length;i++) {
+      if (this.tests[i].errors > 0) {
+        return "ERROR";
+      }
+      if (this.tests[i].failures > 0) {
+        hasFailure = true;
+      }
+    }
+    if (hasFailure) {
+      return "FAILURE";
+    } else {
+      return "SUCCESS";
+    }
+  },
+  postResults: function() {
+    if (this.options.resultsURL) {
+      new Ajax.Request(this.options.resultsURL, 
+        { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
+    }
+  },
+  runTests: function() {
+    var test = this.tests[this.currentTest];
+    if (!test) {
+      // finished!
+      this.postResults();
+      this.logger.summary(this.summary());
+      return;
+    }
+    if(!test.isWaiting) {
+      this.logger.start(test.name);
+    }
+    test.run();
+    if(test.isWaiting) {
+      this.logger.message("Waiting for " + test.timeToWait + "ms");
+      setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
+    } else {
+      this.logger.finish(test.status(), test.summary());
+      this.currentTest++;
+      // tail recursive, hopefully the browser will skip the stackframe
+      this.runTests();
+    }
+  },
+  summary: function() {
+    var assertions = 0;
+    var failures = 0;
+    var errors = 0;
+    var messages = [];
+    for(var i=0;i<this.tests.length;i++) {
+      assertions +=   this.tests[i].assertions;
+      failures   +=   this.tests[i].failures;
+      errors     +=   this.tests[i].errors;
+    }
+    return (
+      (this.options.context ? this.options.context + ': ': '') + 
+      this.tests.length + " tests, " + 
+      assertions + " assertions, " + 
+      failures   + " failures, " +
+      errors     + " errors");
+  }
+}
+
+Test.Unit.Assertions = Class.create();
+Test.Unit.Assertions.prototype = {
+  initialize: function() {
+    this.assertions = 0;
+    this.failures   = 0;
+    this.errors     = 0;
+    this.messages   = [];
+  },
+  summary: function() {
+    return (
+      this.assertions + " assertions, " + 
+      this.failures   + " failures, " +
+      this.errors     + " errors" + "\n" +
+      this.messages.join("\n"));
+  },
+  pass: function() {
+    this.assertions++;
+  },
+  fail: function(message) {
+    this.failures++;
+    this.messages.push("Failure: " + message);
+  },
+  info: function(message) {
+    this.messages.push("Info: " + message);
+  },
+  error: function(error) {
+    this.errors++;
+    this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
+  },
+  status: function() {
+    if (this.failures > 0) return 'failed';
+    if (this.errors > 0) return 'error';
+    return 'passed';
+  },
+  assert: function(expression) {
+    var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
+    try { expression ? this.pass() : 
+      this.fail(message); }
+    catch(e) { this.error(e); }
+  },
+  assertEqual: function(expected, actual) {
+    var message = arguments[2] || "assertEqual";
+    try { (expected == actual) ? this.pass() :
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
+        '", actual "' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertEnumEqual: function(expected, actual) {
+    var message = arguments[2] || "assertEnumEqual";
+    try { $A(expected).length == $A(actual).length && 
+      expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ?
+        this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 
+          ', actual ' + Test.Unit.inspect(actual)); }
+    catch(e) { this.error(e); }
+  },
+  assertNotEqual: function(expected, actual) {
+    var message = arguments[2] || "assertNotEqual";
+    try { (expected != actual) ? this.pass() : 
+      this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertIdentical: function(expected, actual) { 
+    var message = arguments[2] || "assertIdentical"; 
+    try { (expected === actual) ? this.pass() : 
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
+        '", actual "' + Test.Unit.inspect(actual) + '"'); } 
+    catch(e) { this.error(e); } 
+  },
+  assertNotIdentical: function(expected, actual) { 
+    var message = arguments[2] || "assertNotIdentical"; 
+    try { !(expected === actual) ? this.pass() : 
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
+        '", actual "' + Test.Unit.inspect(actual) + '"'); } 
+    catch(e) { this.error(e); } 
+  },
+  assertNull: function(obj) {
+    var message = arguments[1] || 'assertNull'
+    try { (obj==null) ? this.pass() : 
+      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertMatch: function(expected, actual) {
+    var message = arguments[2] || 'assertMatch';
+    var regex = new RegExp(expected);
+    try { (regex.exec(actual)) ? this.pass() :
+      this.fail(message + ' : regex: "' +  Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertHidden: function(element) {
+    var message = arguments[1] || 'assertHidden';
+    this.assertEqual("none", element.style.display, message);
+  },
+  assertNotNull: function(object) {
+    var message = arguments[1] || 'assertNotNull';
+    this.assert(object != null, message);
+  },
+  assertType: function(expected, actual) {
+    var message = arguments[2] || 'assertType';
+    try { 
+      (actual.constructor == expected) ? this.pass() : 
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
+        '", actual "' + (actual.constructor) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertNotOfType: function(expected, actual) {
+    var message = arguments[2] || 'assertNotOfType';
+    try { 
+      (actual.constructor != expected) ? this.pass() : 
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) +  
+        '", actual "' + (actual.constructor) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertInstanceOf: function(expected, actual) {
+    var message = arguments[2] || 'assertInstanceOf';
+    try { 
+      (actual instanceof expected) ? this.pass() : 
+      this.fail(message + ": object was not an instance of the expected type"); }
+    catch(e) { this.error(e); } 
+  },
+  assertNotInstanceOf: function(expected, actual) {
+    var message = arguments[2] || 'assertNotInstanceOf';
+    try { 
+      !(actual instanceof expected) ? this.pass() : 
+      this.fail(message + ": object was an instance of the not expected type"); }
+    catch(e) { this.error(e); } 
+  },
+  assertRespondsTo: function(method, obj) {
+    var message = arguments[2] || 'assertRespondsTo';
+    try {
+      (obj[method] && typeof obj[method] == 'function') ? this.pass() : 
+      this.fail(message + ": object doesn't respond to [" + method + "]"); }
+    catch(e) { this.error(e); }
+  },
+  assertReturnsTrue: function(method, obj) {
+    var message = arguments[2] || 'assertReturnsTrue';
+    try {
+      var m = obj[method];
+      if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
+      m() ? this.pass() : 
+      this.fail(message + ": method returned false"); }
+    catch(e) { this.error(e); }
+  },
+  assertReturnsFalse: function(method, obj) {
+    var message = arguments[2] || 'assertReturnsFalse';
+    try {
+      var m = obj[method];
+      if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)];
+      !m() ? this.pass() : 
+      this.fail(message + ": method returned true"); }
+    catch(e) { this.error(e); }
+  },
+  assertRaise: function(exceptionName, method) {
+    var message = arguments[2] || 'assertRaise';
+    try { 
+      method();
+      this.fail(message + ": exception expected but none was raised"); }
+    catch(e) {
+      (e.name==exceptionName) ? this.pass() : this.error(e); 
+    }
+  },
+  assertElementsMatch: function() {
+    var expressions = $A(arguments), elements = $A(expressions.shift());
+    if (elements.length != expressions.length) {
+      this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions');
+      return false;
+    }
+    elements.zip(expressions).all(function(pair, index) {
+      var element = $(pair.first()), expression = pair.last();
+      if (element.match(expression)) return true;
+      this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect());
+    }.bind(this)) && this.pass();
+  },
+  assertElementMatches: function(element, expression) {
+    this.assertElementsMatch([element], expression);
+  },
+  benchmark: function(operation, iterations) {
+    var startAt = new Date();
+    (iterations || 1).times(operation);
+    var timeTaken = ((new Date())-startAt);
+    this.info((arguments[2] || 'Operation') + ' finished ' + 
+       iterations + ' iterations in ' + (timeTaken/1000)+'s' );
+    return timeTaken;
+  },
+  _isVisible: function(element) {
+    element = $(element);
+    if(!element.parentNode) return true;
+    this.assertNotNull(element);
+    if(element.style && Element.getStyle(element, 'display') == 'none')
+      return false;
+    
+    return this._isVisible(element.parentNode);
+  },
+  assertNotVisible: function(element) {
+    this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
+  },
+  assertVisible: function(element) {
+    this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
+  },
+  benchmark: function(operation, iterations) {
+    var startAt = new Date();
+    (iterations || 1).times(operation);
+    var timeTaken = ((new Date())-startAt);
+    this.info((arguments[2] || 'Operation') + ' finished ' + 
+       iterations + ' iterations in ' + (timeTaken/1000)+'s' );
+    return timeTaken;
+  }
+}
+
+Test.Unit.Testcase = Class.create();
+Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
+  initialize: function(name, test, setup, teardown) {
+    Test.Unit.Assertions.prototype.initialize.bind(this)();
+    this.name           = name;
+    
+    if(typeof test == 'string') {
+      test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,');
+      test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)');
+      this.test = function() {
+        eval('with(this){'+test+'}');
+      }
+    } else {
+      this.test = test || function() {};
+    }
+    
+    this.setup          = setup || function() {};
+    this.teardown       = teardown || function() {};
+    this.isWaiting      = false;
+    this.timeToWait     = 1000;
+  },
+  wait: function(time, nextPart) {
+    this.isWaiting = true;
+    this.test = nextPart;
+    this.timeToWait = time;
+  },
+  run: function() {
+    try {
+      try {
+        if (!this.isWaiting) this.setup.bind(this)();
+        this.isWaiting = false;
+        this.test.bind(this)();
+      } finally {
+        if(!this.isWaiting) {
+          this.teardown.bind(this)();
+        }
+      }
+    }
+    catch(e) { this.error(e); }
+  }
+});
+
+// *EXPERIMENTAL* BDD-style testing to please non-technical folk
+// This draws many ideas from RSpec http://rspec.rubyforge.org/
+
+Test.setupBDDExtensionMethods = function(){
+  var METHODMAP = {
+    shouldEqual:     'assertEqual',
+    shouldNotEqual:  'assertNotEqual',
+    shouldEqualEnum: 'assertEnumEqual',
+    shouldBeA:       'assertType',
+    shouldNotBeA:    'assertNotOfType',
+    shouldBeAn:      'assertType',
+    shouldNotBeAn:   'assertNotOfType',
+    shouldBeNull:    'assertNull',
+    shouldNotBeNull: 'assertNotNull',
+    
+    shouldBe:        'assertReturnsTrue',
+    shouldNotBe:     'assertReturnsFalse',
+    shouldRespondTo: 'assertRespondsTo'
+  };
+  Test.BDDMethods = {};
+  for(m in METHODMAP) {
+    Test.BDDMethods[m] = eval(
+      'function(){'+
+      'var args = $A(arguments);'+
+      'var scope = args.shift();'+
+      'scope.'+METHODMAP[m]+'.apply(scope,(args || []).concat([this])); }');
+  }
+  [Array.prototype, String.prototype, Number.prototype].each(
+    function(p){ Object.extend(p, Test.BDDMethods) }
+  );
+}
+
+Test.context = function(name, spec, log){
+  Test.setupBDDExtensionMethods();
+  
+  var compiledSpec = {};
+  var titles = {};
+  for(specName in spec) {
+    switch(specName){
+      case "setup":
+      case "teardown":
+        compiledSpec[specName] = spec[specName];
+        break;
+      default:
+        var testName = 'test'+specName.gsub(/\s+/,'-').camelize();
+        var body = spec[specName].toString().split('\n').slice(1);
+        if(/^\{/.test(body[0])) body = body.slice(1);
+        body.pop();
+        body = body.map(function(statement){ 
+          return statement.strip()
+        });
+        compiledSpec[testName] = body.join('\n');
+        titles[testName] = specName;
+    }
+  }
+  new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name });
+};
\ No newline at end of file
diff --git a/typo3/sysext/beuser/class.tx_beuser.php b/typo3/sysext/beuser/class.tx_beuser.php
new file mode 100644 (file)
index 0000000..6b22a91
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 1999-2005 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Class, adding SU link to context menu
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ */
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * Class, adding SU link to context menu
+ *
+ * @author     Kasper Skaarhoj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage tx_beuser
+ */
+class tx_beuser {
+
+       /**
+        * Adding various standard options to the context menu.
+        * This includes both first and second level.
+        *
+        * @param       object          The calling object. Value by reference.
+        * @param       array           Array with the currently collected menu items to show.
+        * @param       string          Table name of clicked item.
+        * @param       integer         UID of clicked item.
+        * @return      array           Modified $menuItems array
+        */
+       function main(&$backRef,$menuItems,$table,$uid) {
+               global $BE_USER,$TCA,$LANG;
+
+               $localItems = array();  // Accumulation of local items.
+
+                       // Detecting menu level
+               if ($BE_USER->isAdmin() && !$backRef->cmLevel && $table == 'be_users')  {       // LEVEL: Primary menu.
+                       
+                               // "SU" element added:
+                       $url = t3lib_extMgm::extRelPath('beuser').'mod/index.php?SwitchUser='.rawurlencode($uid).'&switchBackUser=1';
+                       $localItems[] = $backRef->linkItem(
+                               'Switch To User',
+                               $backRef->excludeIcon('<img '.t3lib_iconWorks::skinImg($GLOBALS['BACK_PATH'],'gfx/su_back.gif').' border="0" align="top" title="" alt="" />'),
+                               $backRef->urlRefForCM($url,'',1,'top'),
+                               1
+                       );
+                       
+                       $menuItems=array_merge($menuItems,$localItems);
+               }
+               return $menuItems;
+       }
+
+       /**
+        * Include local lang file.
+        *
+        * @return      array           Local lang array.
+        */
+       function includeLL()    {
+               global $LANG;
+
+               $LOCAL_LANG = $LANG->includeLLFile('EXT:extra_page_cm_options/locallang.php',FALSE);
+               return $LOCAL_LANG;
+       }
+}
+
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/extra_page_cm_options/class.tx_extrapagecmoptions.php'])      {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/extra_page_cm_options/class.tx_extrapagecmoptions.php']);
+}
+?>
diff --git a/typo3/sysext/lowlevel/HOWTO_clean_up_TYPO3_installations.txt b/typo3/sysext/lowlevel/HOWTO_clean_up_TYPO3_installations.txt
new file mode 100644 (file)
index 0000000..c19ffdc
--- /dev/null
@@ -0,0 +1,184 @@
+INTRODUCTION
+For various reasons your TYPO3 installation may over time accumulate data with integrity problems or data you wish to delete completely.
+For instance, why keep old versions of published content? Keep that in your backup - don't load your running website with that overhead!
+Or what about deleted records? Why not flush them - they also fill up your database and filesystem and most likely you can rely on your backups in case of an emergency recovery?
+Also, relations between records and files inside TYPO3 may be lost over time for various reasons. If your website runs as it should such "integrity problems" are mostly easy to automatically repair by simply removing the references pointing to a missing record or file. 
+However, it might also be "soft references" from eg. typolinks (<link 123>...</link>) or a file references in a TypoScript template (something.file = fileadmin/template/miss_me.jpg) which are missing. Those cannot be automatically repaired but the cleanup script incorporates warnings that will tell you about these problems if they exist and you can manually fix them.
+This script provides solutions to these problems by offering an array of tools that can analyze your TYPO3 installation for various problems and in some cases offer fixes for them. Also third party extensions can plug additional functionality into the script.
+
+
+
+PREPARATIONS:
+THERE IS ABSOLUTELY NO WARRANTY associated with this script! It is completely on your OWN RISK that you run it. It may cause accidential data loss due to software bugs or circumstances that it does not know about yet - or data loss might happen due to misuse!
+
+ALWAYS make a complete backup of your website! That means:
+* Dump the complete database to an SQL file. This can usually be done from the command line like this:
+       mysqldump [database name] -u [database user] -p --add-drop-table > ./mywebsite.sql
+* Save all files in the webroot of your site. I usually do this from the command line like this:
+       tar czf ./mywebsite.tgz [webroot directory of your site]
+
+Before running with the --AUTOFIX option ALWAYS make sure to add the parameter "--dryrun" to see what would be fixed.
+
+Also, NEVER BYPASS the REFERENCE INDEX CHECK if --AUTOFIX is used for those tools which require a clean reference index.
+
+It could be a good idea to run a myisamchk on your database just to make sure MySQL has everything pulled together right. Something like this will do:
+       myisamchk [path_to_mysql_databases]/[database_name]/*.MYI -s -r
+
+
+
+RUNNING the SCRIPT:
+The "[base command]" is:
+       [typo3_site_directory]/typo3/cli_dispatch.phpsh lowlevel_cleaner
+
+Try this first. If it all works out you should see a help-screen. Otherwise there will be instructions about what to do. For instance, you will have to create a backend user, "_cli_lowlevel", with any random password since you never need to log in with the user. Never mind permissions, they are not important since this script will force the user to run as "admin" in "Live" workspace.
+You can use the script entirely by following the help screens. However, through this document you get some idea about the best order of events since they may affect each other.
+
+For each of the tools in the test you can see a help screen by running:
+       [base command] [toolkey]
+
+Example with the tool "orphan_records":
+       [typo3_site_directory]/typo3/cli_dispatch.phpsh lowlevel_cleaner orphan_records
+
+
+
+SUGGESTED ORDER OF CLEAN UP:
+The suggested order below assumes that you are interested in running all these tests. Maybe you are not! So you should check the description of each one and if there is any of the tests you wish not to run, just leave it out. It kind of gets simpler that way since the complexity mostly is when you wish to run all tests successively in which case there is an optimal order that ensures you don't have to run the tests all over again.
+
+[base command] orphan_records -r --AUTOFIX
+       - As a beginning, get all orphaned records out of the system since you probably want to. Since orphan records may keep some missing relations from being detected it's a good idea to get them out immediately.
+
+[base command] versions -r --AUTOFIX
+       - Flush all published versions now if you like. Published versions may also keep references to records which could affect other tests, hence do it now if you want to.
+       
+[base command] tx_templavoila_unusedce -r --AUTOFIX
+       - (Assumes usage of "TemplaVoila" extension!)
+       - This should be done AFTER flushing published versions (since versions could reference elements that might be safe to remove)
+       - This should be done BEFORE flushing deleted versions (since this tool will create new deleted records), given that you want to completely flush them of course.
+       - You should run it over again until there remains no more unused elements. You need to do this because deleting elements might generate new unused elements if the now-deleted elements had references.
+
+[base command] double_files -r --AUTOFIX
+       - Fix any files referenced twice or more before you delete records (which could potentially delete a file that is referenced by another file).
+
+[base command] deleted -r --AUTOFIX
+       - Flush deleted records. As a rule of thumb, tools that create deleted records should be run before this one so the deleted records they create are also flushed (if you like to of course)
+       
+[base command] missing_relations -r --AUTOFIX
+       - Remove missing relations at this point.
+       - If you get an error like this; "t3lib_refindex::setReferenceValue(): ERROR: No reference record with hash="132ddb399c0b15593f0d95a58159439f" was found!" just run the test again until no errors occur. The reason is that another fixed reference in the same record and field changed the reference index hash. Running the test again will find the new hash string which will then work for you.
+
+[base command] cleanflexform -r --AUTOFIX
+       - After the "deleted" tool since we cannot clean-up deleted records and to make sure nothing unimportant is cleaned up
+
+[base command] rte_images -r --AUTOFIX
+       - Will be affected by flushed deleted records, versions and orphans so must be run after any of those tests.
+       
+
+
+EXECUTED ANYTIME:
+These can be executed anytime, however you should wait till all deleted records and versions are flushed so you don't waste system resources on fixing deleted records.
+
+       [base command] missing_files -r --AUTOFIX
+       [base command] lost_files -r --AUTOFIX
+
+
+
+NIGHTLY REPORTS OF PROBLEMS IN THE SYSTEM:
+If you wish to scan your TYPO3 installations for problems with a cronjob or so, a shell script that outputs a report could look like this:
+
+       #!/bin/sh
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner missing_files -r -v 2 -s --refindex check
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner double_files -r -v 2 -s --refindex ignore
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner lost_files -r -v 2 -s --refindex ignore
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner orphan_records -r -v 2 -s
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner versions -r -v 2 -s
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner deleted -r -v 1 -s
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner missing_relations -r -v 2 -s --refindex ignore
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner cleanflexform -r -v 2 -s
+       /[WEBROOT_ABS_PATH]/typo3/cli_dispatch.phpsh lowlevel_cleaner rte_images -r -v 2 -s --refindex ignore
+
+You may wish to set the verbosity level (-v) to "3" instead of "2" as in the case above, depending on how important you consider the warnings.
+You might also wish to disable tests like "deleted" which would report deleted records - something that might not warrant a warning, frankly speaking...
+
+
+
+ADDING YOUR OWN TOOLS TO THE TEST:
+You can plug additional analysis tools into the cleaner script. All you need to do is create a class with a few specific functions and configure the cleaner to use it. You should encapsulate your class in an extension (as always).
+In the steps below, substitute these strings with corresponding values:
+       - YOUREXTKEYNOUS = Your extension key, no underscores!
+       - YOUREXTKEY = Your full extension key
+       - CLEANERTOOL = Name prefix for your cleaner module
+
+STEP1: Set up your class as a tool for the cleaner:
+- In the "ext_localconf.php" file of your extension, add this:
+
+       $TYPO3_CONF_VARS['EXTCONF']['lowlevel']['cleanerModules']['tx_YOUREXTKEYNOUS_CLEANERTOOL'] = 
+               array('EXT:YOUREXTKEY/class.YOUREXTKEYNOUS_CLEANERTOOL.php:tx_YOUREXTKEYNOUS_CLEANERTOOL');
+
+- In your extension, create this PHP file:
+       YOUREXTKEY/class.YOUREXTKEYNOUS_CLEANERTOOL.php
+
+- Finally, make sure to "Clear cache in typo3conf/" after having done this!
+
+STEP2: Build your cleaner class:
+- In the new PHP file, create a class with these basic functions:
+
+       class YOUREXTKEYNOUS_CLEANERTOOL extends tx_lowlevel_cleaner_core {
+
+               /**
+                * Constructor
+                */
+               function YOUREXTKEYNOUS_CLEANERTOOL()   {
+                       parent::tx_lowlevel_cleaner_core();
+
+                               // Setting up help:
+                       $this->cli_options[] = array('--option1 value', 'Description...');
+                       $this->cli_options[] = array('--option2 value', 'Description...');
+
+                       $this->cli_help['name'] = 'YOUREXTKEYNOUS_CLEANERTOOL -- DESCRIPTION HERE!';
+                       $this->cli_help['description'] = trim('LONG DESCRIPTION HERE');
+
+                       $this->cli_help['examples'] = 'EXAMPLES HERE';
+               }
+
+               /**
+                * Analyze and return result
+                */
+               function main() {
+
+                               // Initialize result array:
+                       $resultArray = array(
+                               'message' => $this->cli_help['name'].
+                                                       chr(10).chr(10).
+                                                       $this->cli_help['description'],
+                               'headers' => array(
+                                       'SOME_ANALYSIS_1' => array('HEADER','DESCRIPTION',VERBOSITY_LEVEL 0-3),
+                                       'SOME_ANALYSIS_2' => array('HEADER','DESCRIPTION',VERBOSITY_LEVEL 0-3),
+                                       'SOME_ANALYSIS_...' => array('HEADER','DESCRIPTION',VERBOSITY_LEVEL 0-3),
+                               ),
+                               'SOME_ANALYSIS_1' => array(),
+                               'SOME_ANALYSIS_2' => array(),
+                               'SOME_ANALYSIS_...' => array(),
+                       );
+
+                               // HERE you run your analysis and put result into 
+                               // $resultArray['SOME_ANALYSIS_1']
+                               // $resultArray['SOME_ANALYSIS_2']
+                               // $resultArray['SOME_ANALYSIS_...']
+
+                       return $resultArray;
+               }
+
+               /**
+                * Mandatory autofix function
+                */
+               function main_autoFix($resultArray)     {
+                       // HERE you traverse the result array and AUTOFIX what can be fixed
+                       // Make sure to use $this->cli_noExecutionCheck() - see examples from bundled tools
+               }
+       }
+
+
+STEP3: Develop your tool to do something...
+- You should now be able to see your tool appear in the list of tools and you should see output from it when you choose it.
+- Make sure to study the bundled tools from EXT:lowlevel/clmods/. Try to deliver the same high quality of documentation and coding style from there. In particular how the constructor is used to set help-message information.
+- Also, take a look at t3lib_cli which is the very base class - you can use the functions in there in your script.
diff --git a/typo3/sysext/lowlevel/clmods/class.cleanflexform.php b/typo3/sysext/lowlevel/clmods/class.cleanflexform.php
new file mode 100644 (file)
index 0000000..fb74b9f
--- /dev/null
@@ -0,0 +1,190 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 1999-2005 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Cleaner module: cleanflexform
+ * User function called from tx_lowlevel_cleaner_core configured in ext_localconf.php
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ *
+ *
+ *   57: class tx_lowlevel_cleanflexform extends tx_lowlevel_cleaner_core
+ *   64:     function tx_lowlevel_cleanflexform()
+ *   89:     function main()
+ *  122:     function main_parseTreeCallBack($tableName,$uid,$echoLevel,$versionSwapmode,$rootIsVersion)
+ *  154:     function main_autoFix($resultArray)
+ *
+ * TOTAL FUNCTIONS: 4
+ * (This index is automatically created/updated by the extension "extdeveval")
+ *
+ */
+
+
+/**
+ * cleanflexform
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage tx_lowlevel
+ */
+class tx_lowlevel_cleanflexform extends tx_lowlevel_cleaner_core {
+
+       /**
+        * Constructor
+        *
+        * @return      void
+        */
+       function tx_lowlevel_cleanflexform()    {
+               parent::tx_lowlevel_cleaner_core();
+
+                       // Setting up help:
+               $this->cli_options[] = array('--echotree level', 'When "level" is set to 1 or higher you will see the page of the page tree outputted as it is traversed. A value of 2 for "level" will show even more information.');
+               $this->cli_options[] = array('--pid id', 'Setting start page in page tree. Default is the page tree root, 0 (zero)');
+               $this->cli_options[] = array('--depth int', 'Setting traversal depth. 0 (zero) will only analyse start page (see --pid), 1 will traverse one level of subpages etc.');
+
+               $this->cli_help['name'] = 'cleanflexform -- Find flexform fields with unclean XML';
+               $this->cli_help['description'] = trim('
+Traversing page tree and finding records with FlexForm fields with XML that could be cleaned up. This will just remove obsolete data garbage.
+
+Automatic Repair:
+Cleaning XML for FlexForm fields.
+');
+
+               $this->cli_help['examples'] = '';
+       }
+
+       /**
+        * Find orphan records
+        * VERY CPU and memory intensive since it will look up the whole page tree!
+        *
+        * @return      array
+        */
+       function main() {
+               global $TYPO3_DB;
+
+                       // Initialize result array:
+               $resultArray = array(
+                       'message' => $this->cli_help['name'].chr(10).chr(10).$this->cli_help['description'],
+                       'headers' => array(
+                               'dirty' => array('','',2),
+                       ),
+                       'dirty' => array()
+               );
+
+               $startingPoint = $this->cli_isArg('--pid') ? t3lib_div::intInRange($this->cli_argValue('--pid'),0) : 0;
+               $depth = $this->cli_isArg('--depth') ? t3lib_div::intInRange($this->cli_argValue('--depth'),0) : 1000;
+
+               $this->cleanFlexForm_dirtyFields = &$resultArray['dirty'];
+               $this->genTree_traverseDeleted = FALSE; // Do not repair flexform data in deleted records.
+
+               $this->genTree($startingPoint,$depth,(int)$this->cli_argValue('--echotree'),'main_parseTreeCallBack');
+
+               return $resultArray;
+       }
+
+       /**
+        * Call back function for page tree traversal!
+        *
+        * @param       string          Table name
+        * @param       integer         UID of record in processing
+        * @param       integer         Echo level  (see calling function
+        * @param       string          Version swap mode on that level (see calling function
+        * @param       integer         Is root version (see calling function
+        * @return      void
+        */
+       function main_parseTreeCallBack($tableName,$uid,$echoLevel,$versionSwapmode,$rootIsVersion)     {
+
+               t3lib_div::loadTCA($tableName);
+               foreach($GLOBALS['TCA'][$tableName]['columns'] as $colName => $config)  {
+                       if ($config['config']['type']=='flex')  {
+                               if ($echoLevel>2)       echo chr(10).'                  [cleanflexform:] Field "'.$colName.'" in '.$tableName.':'.$uid.' was a flexform and...';
+
+                               $recRow = t3lib_BEfunc::getRecordRaw($tableName,'uid='.intval($uid));
+                               $flexObj = t3lib_div::makeInstance('t3lib_flexformtools');
+                               if ($recRow[$colName])  {
+
+                                               // Clean XML:
+                                       $newXML = $flexObj->cleanFlexFormXML($tableName,$colName,$recRow);
+
+                                       if (md5($recRow[$colName])!=md5($newXML))       {
+                                               if ($echoLevel>2)       echo ' was DIRTY, needs cleanup!';
+                                               $this->cleanFlexForm_dirtyFields[] = $tableName.':'.$uid.':'.$colName;
+                                       } else {
+                                               if ($echoLevel>2)       echo ' was CLEAN';
+                                       }
+                               } else if ($echoLevel>2)        echo ' was EMPTY';
+                       }
+               }
+       }
+
+       /**
+        * Mandatory autofix function
+        * Will run auto-fix on the result array. Echos status during processing.
+        *
+        * @param       array           Result array from main() function
+        * @return      void
+        */
+       function main_autoFix($resultArray)     {
+               foreach($resultArray['dirty'] as $fieldID)      {
+                       list($table, $uid, $field) = explode(':',$fieldID);
+                       echo 'Cleaning XML in "'.$fieldID.'": ';
+                       if ($bypass = $this->cli_noExecutionCheck($fieldID))    {
+                               echo $bypass;
+                       } else {
+
+                                       // Clean XML:
+                               $data = array();
+                               $recRow = t3lib_BEfunc::getRecordRaw($table,'uid='.intval($uid));
+                               $flexObj = t3lib_div::makeInstance('t3lib_flexformtools');
+                               if ($recRow[$field])    {
+                                       $data[$table][$uid][$field] = $flexObj->cleanFlexFormXML($table,$field,$recRow);
+                               }
+
+                                       // Execute Data array:
+                               $tce = t3lib_div::makeInstance('t3lib_TCEmain');
+                               $tce->stripslashes_values = FALSE;
+                               $tce->dontProcessTransformations = TRUE;
+                               $tce->bypassWorkspaceRestrictions = TRUE;
+                               $tce->bypassFileHandling = TRUE;
+
+                               $tce->start($data,array());     // check has been done previously that there is a backend user which is Admin and also in live workspace
+                               $tce->process_datamap();
+
+                                       // Return errors if any:
+                               if (count($tce->errorLog))      {
+                                       echo '  ERROR from "TCEmain":'.chr(10).'TCEmain:'.implode(chr(10).'TCEmain:',$tce->errorLog);
+                               } else echo 'DONE';
+                       }
+                       echo chr(10);
+               }
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/typo3/sysext/lowlevel/clmods/class.deleted.php b/typo3/sysext/lowlevel/clmods/class.deleted.php
new file mode 100644 (file)
index 0000000..3decb18
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 1999-2005 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Cleaner module: Deleted records
+ * User function called from tx_lowlevel_cleaner_core configured in ext_localconf.php
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ *
+ *
+ *   56: class tx_lowlevel_deleted extends tx_lowlevel_cleaner_core
+ *   63:     function tx_lowlevel_deleted()
+ *   88:     function main()
+ *  116:     function main_autoFix($resultArray)
+ *
+ * TOTAL FUNCTIONS: 3
+ * (This index is automatically created/updated by the extension "extdeveval")
+ *
+ */
+
+
+/**
+ * Looking for Deleted records
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage tx_lowlevel
+ */
+class tx_lowlevel_deleted extends tx_lowlevel_cleaner_core {
+
+       /**
+        * Constructor
+        *
+        * @return      [type]          ...
+        */
+       function tx_lowlevel_deleted()  {
+               parent::tx_lowlevel_cleaner_core();
+
+                       // Setting up help:
+               $this->cli_options[] = array('--echotree level', 'When "level" is set to 1 or higher you will see the page of the page tree outputted as it is traversed. A value of 2 for "level" will show even more information.');
+               $this->cli_options[] = array('--pid id', 'Setting start page in page tree. Default is the page tree root, 0 (zero)');
+               $this->cli_options[] = array('--depth int', 'Setting traversal depth. 0 (zero) will only analyse start page (see --pid), 1 will traverse one level of subpages etc.');
+
+               $this->cli_help['name'] = 'deleted -- To find and flush deleted records in the page tree';
+               $this->cli_help['description'] = trim('
+Traversing page tree and finding deleted records
+
+Automatic Repair:
+Although deleted records are not errors to be repaired, this tool allows you to flush the deleted records completely from the system as an automatic action. Limiting this lookup by --pid and --depth can help you to narrow in the operation to a part of the page tree.
+');
+
+               $this->cli_help['examples'] = '';
+       }
+
+       /**
+        * Find orphan records
+        * VERY CPU and memory intensive since it will look up the whole page tree!
+        *
+        * @return      array
+        */
+       function main() {
+               global $TYPO3_DB;
+
+                       // Initialize result array:
+               $resultArray = array(
+                       'message' => $this->cli_help['name'].chr(10).chr(10).$this->cli_help['description'],
+                       'headers' => array(
+                               'deleted' => array('Index of deleted records','These are records from the page tree having the deleted-flag set. The --AUTOFIX option will flush them completely!',1),
+                       ),
+                       'deleted' => array(),
+               );
+
+               $startingPoint = $this->cli_isArg('--pid') ? t3lib_div::intInRange($this->cli_argValue('--pid'),0) : 0;
+               $depth = $this->cli_isArg('--depth') ? t3lib_div::intInRange($this->cli_argValue('--depth'),0) : 1000;
+               $this->genTree($startingPoint,$depth,(int)$this->cli_argValue('--echotree'));
+
+               $resultArray['deleted'] = $this->recStats['deleted'];
+
+               return $resultArray;
+       }
+
+       /**
+        * Mandatory autofix function
+        * Will run auto-fix on the result array. Echos status during processing.
+        *
+        * @param       array           Result array from main() function
+        * @return      void
+        */
+       function main_autoFix($resultArray)     {
+
+                       // Putting "tx_templavoila_datastructure" table in the bottom:
+               if (isset($resultArray['deleted']['tx_templavoila_datastructure']))     {
+                       $_tx_templavoila_datastructure = $resultArray['deleted']['tx_templavoila_datastructure'];
+                       unset($resultArray['deleted']['tx_templavoila_datastructure']);
+                       $resultArray['deleted']['tx_templavoila_datastructure'] = $_tx_templavoila_datastructure;
+               }
+
+                       // Putting "pages" table in the bottom:
+               if (isset($resultArray['deleted']['pages']))    {
+                       $_pages = $resultArray['deleted']['pages'];
+                       unset($resultArray['deleted']['pages']);
+                       $resultArray['deleted']['pages'] = array_reverse($_pages);      // To delete sub pages first assuming they are accumulated from top of page tree.
+               }
+
+                       // Traversing records:
+               foreach($resultArray['deleted'] as $table => $list)     {
+                       echo 'Flushing deleted records from table "'.$table.'":'.chr(10);
+                       foreach($list as $uid)  {
+                               echo '  Flushing record "'.$table.':'.$uid.'": ';
+                               if ($bypass = $this->cli_noExecutionCheck($table.':'.$uid))     {
+                                       echo $bypass;
+                               } else {
+
+                                               // Execute CMD array:
+                                       $tce = t3lib_div::makeInstance('t3lib_TCEmain');
+                                       $tce->stripslashes_values = FALSE;
+                                       $tce->start(array(),array());
+                                       $tce->deleteRecord($table,$uid, TRUE, TRUE);    // Notice, we are deleting pages with no regard to subpages/subrecords - we do this since they should also be included in the set of deleted pages of course (no un-deleted record can exist under a deleted page...)
+
+                                               // Return errors if any:
+                                       if (count($tce->errorLog))      {
+                                               echo '  ERROR from "TCEmain":'.chr(10).'TCEmain:'.implode(chr(10).'TCEmain:',$tce->errorLog);
+                                       } else echo "DONE";
+                               }
+                               echo chr(10);
+                       }
+               }
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/typo3/sysext/lowlevel/clmods/class.double_files.php b/typo3/sysext/lowlevel/clmods/class.double_files.php
new file mode 100644 (file)
index 0000000..40a0ee6
--- /dev/null
@@ -0,0 +1,225 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 1999-2005 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Cleaner module: Double Files
+ * User function called from tx_lowlevel_cleaner_core configured in ext_localconf.php
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ *
+ *
+ *   58: class tx_lowlevel_double_files extends tx_lowlevel_cleaner_core
+ *   67:     function tx_lowlevel_double_files()
+ *   99:     function main()
+ *  182:     function main_autoFix($resultArray)
+ *
+ * TOTAL FUNCTIONS: 3
+ * (This index is automatically created/updated by the extension "extdeveval")
+ *
+ */
+
+
+require_once (PATH_t3lib.'class.t3lib_basicfilefunc.php');
+
+/**
+ * Looking for double files
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ * @package TYPO3
+ * @subpackage tx_lowlevel
+ */
+class tx_lowlevel_double_files extends tx_lowlevel_cleaner_core {
+
+       var $checkRefIndex = TRUE;
+
+       /**
+        * Constructor
+        *
+        * @return      void
+        */
+       function tx_lowlevel_double_files()     {
+               parent::tx_lowlevel_cleaner_core();
+
+                       // Setting up help:
+               $this->cli_help['name'] = 'double_files -- Looking for files from TYPO3 managed records which are referenced more than one time (only one time allowed)';
+               $this->cli_help['description'] = trim('
+Assumptions:
+- a perfect integrity of the reference index table (always update the reference index table before using this tool!)
+- files found in deleted records are included (otherwise you would see a false list of lost files)
+
+Files attached to records in TYPO3 using a "group" type configuration in TCA or FlexForm DataStructure are managed exclusively by the system and there must always exist a 1-1 reference between the file and the reference in the record.
+This tool will expose when such files are referenced from multiple locations which is considered an integrity error.
+If a multi-reference is found it was typically created because the record was copied or modified outside of TCEmain which will otherwise maintain the relations correctly.
+Multi-references should be resolved to 1-1 references as soon as possible. The danger of keeping multi-references is that if the file is removed from one of the refering records it will actually be deleted in the file system, leaving missing files for the remaining referers!
+
+Automatic Repair of Errors:
+- The multi-referenced file is copied under a new name and references updated.
+
+Manual repair suggestions:
+- None that can not be handled by the automatic repair.
+');
+
+               $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_cleaner double_files -s -r
+This will check the system for double files relations.';
+       }
+
+       /**
+        * Find managed files which are referred to more than one time
+        * Fix methods: API in t3lib_refindex that allows to change the value of a reference (we could copy the file) or remove reference
+        *
+        * @return      array
+        */
+       function main() {
+               global $TYPO3_DB;
+
+                       // Initialize result array:
+               $resultArray = array(
+                       'message' => $this->cli_help['name'].chr(10).chr(10).$this->cli_help['description'],
+                       'headers' => array(
+                               'multipleReferencesList_count' => array('Number of multi-reference files','(See below)',0),
+                               'singleReferencesList_count' => array('Number of files correctly referenced','The amount of correct 1-1 references',0),
+                               'multipleReferencesList' => array('Entries with files having multiple references','These are serious problems that should be resolved ASAP to prevent data loss! '.$this->label_infoString,3),
+                               'dirname_registry' => array('Registry of directories in which files are found.','Registry includes which table/field pairs store files in them plus how many files their store.',0),
+                               'missingFiles' => array('Tracking missing files','(Extra feature, not related to tracking of double references. Further, the list may include more files than found in the missing_files()-test because this list includes missing files from deleted records.)',0),
+                               'warnings' => array('Warnings picked up','',2)
+                       ),
+                       'multipleReferencesList_count' => 0,
+                       'singleReferencesList_count' => 0,
+                       'multipleReferencesList' => array(),
+                       'dirname_registry' => array(),
+                       'missingFiles' => array(),
+                       'warnings' => array()
+               );
+
+                       // Select all files in the reference table not found by a soft reference parser (thus TCA configured)
+               $recs = $TYPO3_DB->exec_SELECTgetRows(
+                       '*',
+                       'sys_refindex',
+                       'ref_table='.$TYPO3_DB->fullQuoteStr('_FILE', 'sys_refindex').
+                               ' AND softref_key='.$TYPO3_DB->fullQuoteStr('', 'sys_refindex'),
+                       '',
+                       'sorting DESC'
+               );
+
+                       // Traverse the files and put into a large table:
+               $tempCount = array();
+               if (is_array($recs)) {
+                       foreach($recs as $rec)  {
+
+                                       // Compile info string for location of reference:
+                               $infoString = $this->infoStr($rec);
+
+                                       // Registering occurencies in directories:
+                               $resultArray['dirname_registry'][dirname($rec['ref_string'])][$rec['tablename'].':'.$rec['field']]++;
+
+                                       // Handle missing file:
+                               if (!@is_file(PATH_site.$rec['ref_string']))    {
+                                       $resultArray['missingFiles'][$rec['ref_string']][$rec['hash']] = $infoString;
+                               }
+
+                                       // Add entry if file has multiple references pointing to it:
+                               if (isset($tempCount[$rec['ref_string']]))      {
+                                       if (!is_array($resultArray['multipleReferencesList'][$rec['ref_string']]))      {
+                                               $resultArray['multipleReferencesList'][$rec['ref_string']] = array();
+                                               $resultArray['multipleReferencesList'][$rec['ref_string']][$tempCount[$rec['ref_string']][1]] = $tempCount[$rec['ref_string']][0];
+                                       }
+                                       $resultArray['multipleReferencesList'][$rec['ref_string']][$rec['hash']] = $infoString;
+                               } else {
+                                       $tempCount[$rec['ref_string']] = array($infoString,$rec['hash']);
+                               }
+                       }
+               }
+
+                       // Add count for multi-references:
+               $resultArray['multipleReferencesList_count'] = count($resultArray['multipleReferencesList']);
+               $resultArray['singleReferencesList_count'] = count($tempCount) - $resultArray['multipleReferencesList_count'];
+
+                       // Sort dirname registry and add warnings for directories outside uploads/
+               ksort($resultArray['dirname_registry']);
+               foreach($resultArray['dirname_registry'] as $dir => $temp)      {
+                       if (!t3lib_div::isFirstPartOfStr($dir,'uploads/'))      {
+                               $resultArray['warnings'][] = 'Directory "'.$dir.'" was outside uploads/ which is unusual practice in TYPO3 although not forbidden. Directory used by the following table:field pairs: '.implode(',',array_keys($temp));
+                       }
+               }
+
+               return $resultArray;
+       }
+
+       /**
+        * Mandatory autofix function
+        * Will run auto-fix on the result array. Echos status during processing.
+        *
+        * @param       array           Result array from main() function
+        * @return      void
+        */
+       function main_autoFix($resultArray)     {
+               foreach($resultArray['multipleReferencesList'] as $key => $value)       {
+                       $absFileName = t3lib_div::getFileAbsFileName($key);
+                       if ($absFileName && @is_file($absFileName))     {
+                               echo 'Processing file: '.$key.chr(10);
+                               $c=0;
+                               foreach($value as $hash => $recReference)       {
+                                       if ($c==0)      {
+                                               echo '  Keeping '.$key.' for record "'.$recReference.'"'.chr(10);
+                                       } else {
+                                                       // Create unique name for file:
+                                               $fileFunc = t3lib_div::makeInstance('t3lib_basicFileFunctions');
+                                               $newName = $fileFunc->getUniqueName(basename($key), dirname($absFileName));
+                                               echo '  Copying '.$key.' to '.substr($newName,strlen(PATH_site)).' for record "'.$recReference.'": ';
+
+                                               if ($bypass = $this->cli_noExecutionCheck($recReference))       {
+                                                       echo $bypass;
+                                               } else {
+                                                       t3lib_div::upload_copy_move($absFileName,$newName);
+                                                       clearstatcache();
+
+                                                       if (@is_file($newName)) {
+                                                               $sysRefObj = t3lib_div::makeInstance('t3lib_refindex');
+                                                               $error = $sysRefObj->setReferenceValue($hash,basename($newName));
+                                                               if ($error)     {
+                                                                       echo '  ERROR:  t3lib_refindex::setReferenceValue(): '.$error.chr(10);
+                                                                       exit;
+                                                               } else echo "DONE";
+                                                       } else {
+                                                               echo '  ERROR: File "'.$newName.'" was not created!';
+                                                       }
+                                               }
+                                               echo chr(10);
+                                       }
+                                       $c++;
+                               }
+                       } else {
+                               echo '  ERROR: File "'.$absFileName.'" was not found!';
+                       }
+               }
+       }
+}
+
+?>
\ No newline at end of file
diff --git a/typo3/sysext/lowlevel/clmods/class.lost_files.php b/typo3/sysext/lowlevel/clmods/class.lost_files.php
new file mode 100644 (file)
index 0000000..0b8ab74
--- /dev/null
@@ -0,0 +1,213 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 1999-2005 Kasper Skaarhoj (kasperYYYY@typo3.com)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Cleaner module: Lost files
+ * User function called from tx_lowlevel_cleaner_core configured in ext_localconf.php
+ *
+ * @author     Kasper Skårhøj <kasperYYYY@typo3.com>
+ */
+/**
+ * [CLASS/FUNCTION INDEX of SCRIPT]
+ *
+ *
+ *
+ *   56: class tx_lowlevel_lost_files extends tx_lowlevel_cleaner_core