diff options
author | rvelices <rv-github@modusoptimus.com> | 2007-02-23 13:18:34 +0000 |
---|---|---|
committer | rvelices <rv-github@modusoptimus.com> | 2007-02-23 13:18:34 +0000 |
commit | cb2408a82c9bc93bef177dc33a8981bc36800839 (patch) | |
tree | 85728267a379dd1b39ac089ab2021f000e6cb668 | |
parent | 6f03e29735ea395f31d09bbfd15a4e15eaf961e3 (diff) |
Plugins:
- display author and and author url (if present) on plugin admin page
- uniformized versions/authors... for all plugins in svn
- security fix (html escape name, version, uri, author... to avoid javascript injection which could automatically simulate click on Install)
- added confirmation for install/uninstall plugins
Web services:
- web service explorer now caches method details in order to avoid unnecessary web calls
- web service explorer can now send parameters as arrays
- web service explorer uses now prototype.js version 1.5
- small improvements
- added and use function bad_request (sends http status code 400)
git-svn-id: http://piwigo.org/svn/trunk@1852 68402e56-0260-453c-a942-63ccdbb3a9ee
-rw-r--r-- | admin/include/functions_plugins.inc.php | 28 | ||||
-rw-r--r-- | admin/plugins.php | 31 | ||||
-rw-r--r-- | include/functions_html.inc.php | 17 | ||||
-rw-r--r-- | include/functions_tag.inc.php | 61 | ||||
-rw-r--r-- | include/section_init.inc.php | 39 | ||||
-rw-r--r-- | include/ws_core.inc.php | 5 | ||||
-rw-r--r-- | include/ws_functions.inc.php | 51 | ||||
-rw-r--r-- | plugins/add_index/main.inc.php | 7 | ||||
-rw-r--r-- | plugins/admin_advices/main.inc.php | 7 | ||||
-rw-r--r-- | plugins/admin_multi_view/main.inc.php | 2 | ||||
-rw-r--r-- | plugins/event_tracer/main.inc.php | 5 | ||||
-rw-r--r-- | plugins/hello_world/main.inc.php | 7 | ||||
-rw-r--r-- | template/yoga/admin/plugins.tpl | 6 | ||||
-rw-r--r-- | tools/prototype.js | 1496 | ||||
-rw-r--r-- | tools/ws.htm | 141 |
15 files changed, 1375 insertions, 528 deletions
diff --git a/admin/include/functions_plugins.inc.php b/admin/include/functions_plugins.inc.php index 80027b6e2..dfbfbb8a3 100644 --- a/admin/include/functions_plugins.inc.php +++ b/admin/include/functions_plugins.inc.php @@ -41,25 +41,41 @@ function get_fs_plugins() and file_exists($path.'/main.inc.php') ) { - $plugin = array('name'=>$file, 'version'=>'0', 'uri'=>'', 'description'=>''); + $plugin = array( + 'name'=>$file, + 'version'=>'0', + 'uri'=>'', + 'description'=>'', + 'author'=>'', + ); $plg_data = implode( '', file($path.'/main.inc.php') ); - if ( preg_match("|Plugin Name: (.*)|i", $plg_data, $val) ) + if ( preg_match("|Plugin Name: (.*)|", $plg_data, $val) ) { $plugin['name'] = trim( $val[1] ); } - if (preg_match("|Version: (.*)|i", $plg_data, $val)) + if (preg_match("|Version: (.*)|", $plg_data, $val)) { $plugin['version'] = trim($val[1]); } - if ( preg_match("|Plugin URI: (.*)|i", $plg_data, $val) ) + if ( preg_match("|Plugin URI: (.*)|", $plg_data, $val) ) { - $plugin['uri'] = $val[1]; + $plugin['uri'] = trim($val[1]); } - if ( preg_match("|Description: (.*)|i", $plg_data, $val) ) + if ( preg_match("|Description: (.*)|", $plg_data, $val) ) { $plugin['description'] = trim($val[1]); } + if ( preg_match("|Author: (.*)|", $plg_data, $val) ) + { + $plugin['author'] = trim($val[1]); + } + if ( preg_match("|Author URI: (.*)|", $plg_data, $val) ) + { + $plugin['author uri'] = trim($val[1]); + } + // IMPORTANT SECURITY ! + $plugin = array_map('htmlspecialchars', $plugin); $plugins[$file] = $plugin; } } diff --git a/admin/plugins.php b/admin/plugins.php index 72695c3fa..da16841de 100644 --- a/admin/plugins.php +++ b/admin/plugins.php @@ -3,7 +3,6 @@ // | PhpWebGallery - a PHP based picture gallery | // | Copyright (C) 2003-2007 PhpWebGallery Team - http://phpwebgallery.net | // +-----------------------------------------------------------------------+ -// | branch : BSF (Best So Far) // | file : $Id$ // | last update : $Date$ // | last modifier : $Author$ @@ -38,9 +37,9 @@ $my_base_url = PHPWG_ROOT_PATH.'admin.php?page=plugins'; // +-----------------------------------------------------------------------+ // | perform requested actions | // +-----------------------------------------------------------------------+ -if ( isset($_REQUEST['action']) and isset($_REQUEST['plugin']) ) +if ( isset($_GET['action']) and isset($_GET['plugin']) ) { - $plugin_id = $_REQUEST['plugin']; + $plugin_id = $_GET['plugin']; $crt_db_plugin = get_db_plugins('', $plugin_id); if (!empty($crt_db_plugin)) { @@ -54,7 +53,7 @@ if ( isset($_REQUEST['action']) and isset($_REQUEST['plugin']) ) $errors = array(); $file_to_include = PHPWG_PLUGINS_PATH.$plugin_id.'/maintain.inc.php'; - switch ( $_REQUEST['action'] ) + switch ( $_GET['action'] ) { case 'install': if ( !empty($crt_db_plugin)) @@ -89,7 +88,7 @@ INSERT INTO '.PLUGINS_TABLE.' (id,version) VALUES ("' case 'activate': if ( !isset($crt_db_plugin) ) { - array_push($errors, 'CANNOT '. $_REQUEST['action'] .' - NOT INSTALLED'); + array_push($errors, 'CANNOT '. $_GET['action'] .' - NOT INSTALLED'); } if ($crt_db_plugin['state']!='inactive') { @@ -114,7 +113,7 @@ UPDATE '.PLUGINS_TABLE.' SET state="active" WHERE id="'.$plugin_id.'"'; case 'deactivate': if ( !isset($crt_db_plugin) ) { - die ('CANNOT '. $_REQUEST['action'] .' - NOT INSTALLED'); + die ('CANNOT '. $_GET['action'] .' - NOT INSTALLED'); } if ($crt_db_plugin['state']!='active') { @@ -134,7 +133,7 @@ UPDATE '.PLUGINS_TABLE.' SET state="inactive" WHERE id="'.$plugin_id.'"'; case 'uninstall': if ( !isset($crt_db_plugin) ) { - die ('CANNOT '. $_REQUEST['action'] .' - NOT INSTALLED'); + die ('CANNOT '. $_GET['action'] .' - NOT INSTALLED'); } $query = ' DELETE FROM '.PLUGINS_TABLE.' WHERE id="'.$plugin_id.'"'; @@ -181,11 +180,25 @@ foreach( $fs_plugins as $plugin_id => $fs_plugin ) { $display_name='<a href="'.$fs_plugin['uri'].'">'.$display_name.'</a>'; } + $desc = $fs_plugin['description']; + if (!empty($fs_plugin['author'])) + { + $desc.= ' (<em>'; + if (!empty($fs_plugin['author uri'])) + { + $desc.= '<a href="'.$fs_plugin['author uri'].'">'.$fs_plugin['author'].'</a>'; + } + else + { + $desc.= $fs_plugin['author']; + } + $desc.= '</em>)'; + } $template->assign_block_vars( 'plugins.plugin', array( 'NAME' => $display_name, 'VERSION' => $fs_plugin['version'], - 'DESCRIPTION' => $fs_plugin['description'], + 'DESCRIPTION' => $desc, 'CLASS' => ($num++ % 2 == 1) ? 'row2' : 'row1', ) ); @@ -218,6 +231,7 @@ foreach( $fs_plugins as $plugin_id => $fs_plugin ) 'L_ACTION' => l10n('Uninstall'), ) ); + $template->assign_block_vars( 'plugins.plugin.action.confirm', array()); break; } } @@ -229,6 +243,7 @@ foreach( $fs_plugins as $plugin_id => $fs_plugin ) 'L_ACTION' => l10n('Install'), ) ); + $template->assign_block_vars( 'plugins.plugin.action.confirm', array()); } } diff --git a/include/functions_html.inc.php b/include/functions_html.inc.php index c0edf6ed0..74934cbf0 100644 --- a/include/functions_html.inc.php +++ b/include/functions_html.inc.php @@ -627,6 +627,23 @@ function page_forbidden($msg, $alternate_url=null) } /** + * exits the current script with 400 code + * @param string msg a message to display + * @param string alternate_url redirect to this url + */ +function bad_request($msg, $alternate_url=null) +{ + set_status_header(400); + if ($alternate_url==null) + $alternate_url = make_index_url(); + redirect_html( $alternate_url, + '<div style="text-align:left; margin-left:5em;margin-bottom:5em;"> +<h1 style="text-align:left; font-size:36px;">Bad request</h1><br/>' +.$msg.'</div>', + 5 ); +} + +/** * exits the current script with 404 code when a page cannot be found * @param string msg a message to display * @param string alternate_url redirect to this url diff --git a/include/functions_tag.inc.php b/include/functions_tag.inc.php index 4f8c95563..c6dc01db6 100644 --- a/include/functions_tag.inc.php +++ b/include/functions_tag.inc.php @@ -271,4 +271,65 @@ SELECT id, name, url_name, count(*) counter usort($tags, 'name_compare'); return $tags; } + +/** + * return a list of tags corresponding to any of ids, url_names, names + * + * @param array ids + * @param array url_names + * @param array names + * @return array + */ +function find_tags($ids, $url_names=array(), $names=array() ) +{ + $where_clauses = array(); + if ( !empty($ids) ) + { + $where_clauses[] = 'id IN ('.implode(',', $ids).')'; + } + if ( !empty($url_names) ) + { + $where_clauses[] = + 'url_name IN ('. + implode( + ',', + array_map( + create_function('$s', 'return "\'".$s."\'";'), + $url_names + ) + ) + .')'; + } + if ( !empty($names) ) + { + $where_clauses[] = + 'name IN ('. + implode( + ',', + array_map( + create_function('$s', 'return "\'".$s."\'";'), + $names + ) + ) + .')'; + } + if (empty($where_clauses)) + { + return array(); + } + + $query = ' +SELECT id, url_name, name + FROM '.TAGS_TABLE.' + WHERE '. implode( ' + OR ', $where_clauses); + + $result = pwg_query($query); + $tags = array(); + while ($row = mysql_fetch_assoc($result)) + { + array_push($tags, $row); + } + return $tags; +} ?>
\ No newline at end of file diff --git a/include/section_init.inc.php b/include/section_init.inc.php index 21396955d..4239ebd93 100644 --- a/include/section_init.inc.php +++ b/include/section_init.inc.php @@ -4,7 +4,6 @@ // | Copyright (C) 2002-2003 Pierrick LE GALL - pierrick@phpwebgallery.net | // | Copyright (C) 2003-2007 PhpWebGallery Team - http://phpwebgallery.net | // +-----------------------------------------------------------------------+ -// | branch : BSF (Best So Far) // | file : $Id$ // | last update : $Date$ // | last modifier : $Author$ @@ -119,7 +118,7 @@ if (script_basename() == 'picture') // basename without file extention } else { - die('Fatal: picture identifier is missing'); + bad_request('picture identifier is missing'); } } } @@ -159,7 +158,7 @@ else if (0 === strpos(@$tokens[$next_token], 'tag')) } else { - array_push($requested_tag_url_names, "'".$tokens[$i]."'"); + array_push($requested_tag_url_names, $tokens[$i]); } $i++; } @@ -167,32 +166,10 @@ else if (0 === strpos(@$tokens[$next_token], 'tag')) if ( empty($requested_tag_ids) && empty($requested_tag_url_names) ) { - die('Fatal: at least one tag required'); - } - // tag infos - $query = ' -SELECT name, url_name, id - FROM '.TAGS_TABLE.' - WHERE '; - if ( !empty($requested_tag_ids) ) - { - $query.= 'id IN ('.implode(',', $requested_tag_ids ).')'; - } - if ( !empty($requested_tag_url_names) ) - { - if ( !empty($requested_tag_ids) ) - { - $query.= ' OR '; - } - $query.= 'url_name IN ('.implode(',', $requested_tag_url_names ).')'; - } - $result = pwg_query($query); - $tag_infos = array(); - while ($row = mysql_fetch_assoc($result)) - { - $tag_infos[ $row['id'] ] = $row; - array_push($page['tags'], $row );//we loose given tag order; is it important? + bad_request('at least one tag required'); } + + $page['tags'] = find_tags($requested_tag_ids, $requested_tag_url_names); if ( empty($page['tags']) ) { page_not_found('Requested tag does not exist', get_root_url().'tags.php' ); @@ -228,10 +205,10 @@ else if ('search' == @$tokens[$next_token]) $page['section'] = 'search'; $next_token++; - preg_match('/(\d+)/', $tokens[$next_token], $matches); + preg_match('/(\d+)/', @$tokens[$next_token], $matches); if (!isset($matches[1])) { - die('Fatal: search identifier is missing'); + bad_request('search identifier is missing'); } $page['search'] = $matches[1]; $next_token++; @@ -254,7 +231,7 @@ else if ('list' == @$tokens[$next_token]) { if (!preg_match('/^\d+(,\d+)*$/', $tokens[$next_token])) { - die('wrong format on list GET parameter'); + bad_request('wrong format on list GET parameter'); } foreach (explode(',', $tokens[$next_token]) as $image_id) { diff --git a/include/ws_core.inc.php b/include/ws_core.inc.php index a3e8c7770..915e7e147 100644 --- a/include/ws_core.inc.php +++ b/include/ws_core.inc.php @@ -464,6 +464,10 @@ Response format: ".@$this->_responseFormat." encoder:".$this->_responseEncoder." { $flags |= WS_PARAM_OPTIONAL; } + if ( $flags & WS_PARAM_FORCE_ARRAY ) + { + $flags |= WS_PARAM_ACCEPT_ARRAY; + } $options['flags'] = $flags; $params[$param] = $options; } @@ -604,6 +608,7 @@ Response format: ".@$this->_responseFormat." encoder:".$this->_responseEncoder." $param_data = array( 'name' => $name, 'optional' => ($options['flags']&WS_PARAM_OPTIONAL)?true:false, + 'acceptArray' => ($options['flags']&WS_PARAM_ACCEPT_ARRAY)?true:false, ); if (isset($options['default'])) { diff --git a/include/ws_functions.inc.php b/include/ws_functions.inc.php index 8af08204c..c68d5d195 100644 --- a/include/ws_functions.inc.php +++ b/include/ws_functions.inc.php @@ -269,8 +269,11 @@ function ws_std_get_image_xml_attributes() */ function ws_getVersion($params, &$service) { -// TODO = Version availability is under control of $conf['show_version'] - return PHPWG_VERSION; + global $conf; + if ($conf['show_version']) + return PHPWG_VERSION; + else + return new PwgError(403, 'Forbidden'); } @@ -336,14 +339,15 @@ SELECT id, name, image_order $where_clauses[] = ws_addControls( 'categories.getImages', $params, 'i.' ); $order_by = ws_std_image_sql_order($params, 'i.'); - if (empty($order_by)) - {// TODO check for category order by (image_order) - $order_by = $conf['order_by']; - } - else + if ( empty($order_by) + and count($params['cat_id'])==1 + and isset($cats[ $params['cat_id'][0] ]['image_order']) + ) { - $order_by = 'ORDER BY '.$order_by; + $order_by = $cats[ $params['cat_id'][0] ]['image_order']; } + $order_by = empty($order_by) ? $conf['order_by'] : 'ORDER BY '.$order_by; + $query = ' SELECT i.*, GROUP_CONCAT(category_id) cat_ids FROM '.IMAGES_TABLE.' i @@ -499,6 +503,10 @@ ORDER BY global_rank'; */ function ws_images_addComment($params, &$service) { + if (!$service->isPost()) + { + return new PwgError(405, "This method requires HTTP POST"); + } $params['image_id'] = (int)$params['image_id']; $query = ' SELECT DISTINCT image_id @@ -579,7 +587,7 @@ LIMIT 1;'; $image_row = mysql_fetch_assoc(pwg_query($query)); if ($image_row==null) { - return new PwgError(999, "image_id not found"); + return new PwgError(404, "image_id not found"); } $image_row = array_merge( $image_row, ws_std_get_urls($image_row) ); @@ -859,7 +867,7 @@ function ws_session_login($params, &$service) if (!$service->isPost()) { - return new PwgError(400, "This method requires POST"); + return new PwgError(405, "This method requires HTTP POST"); } if (try_log_user($params['username'], $params['password'],false)) { @@ -942,32 +950,19 @@ function ws_tags_getImages($params, &$service) { @include_once(PHPWG_ROOT_PATH.'include/functions_picture.inc.php'); global $conf; - + // first build all the tag_ids we are interested in - $tag_ids = array(); - $tags = get_available_tags(); + $params['tag_id'] = array_map( 'intval',$params['tag_id'] ); + $tags = find_tags($params['tag_id'], $params['tag_url_name'], $params['tag_name']); $tags_by_id = array(); - for( $i=0; $i<count($tags); $i++ ) - { - $tags[$i]['id']=(int)$tags[$i]['id']; - } foreach( $tags as $tag ) { + $tags['id'] = (int)$tag['id']; $tags_by_id[ $tag['id'] ] = $tag; - if ( - in_array($tag['name'], $params['tag_name']) - or - in_array($tag['url_name'], $params['tag_url_name']) - or - in_array($tag['id'], $params['tag_id']) - ) - { - $tag_ids[] = $tag['id']; - } } unset($tags); + $tag_ids = array_keys($tags_by_id); - $tag_ids = array_unique( $tag_ids ); $image_ids = array(); $image_tag_map = array(); diff --git a/plugins/add_index/main.inc.php b/plugins/add_index/main.inc.php index 07ade8e10..8d9989dbc 100644 --- a/plugins/add_index/main.inc.php +++ b/plugins/add_index/main.inc.php @@ -1,9 +1,10 @@ -<?php
-/*
+<?php /*
Plugin Name: Add Index
-Version: 1.1.0.0
+Version: 1.0
Description: Add file index.php file on all sub-directories of local galleries pictures. / Ajoute le fichier index.php sur les sous-répertoires de galeries d'images locales.
Plugin URI: http://www.phpwebgallery.net
+Author: PhpWebGallery team
+Author URI: http://www.phpwebgallery.net
*/
// +-----------------------------------------------------------------------+
// | PhpWebGallery - a PHP based picture gallery |
diff --git a/plugins/admin_advices/main.inc.php b/plugins/admin_advices/main.inc.php index 821ed9563..6db9b358a 100644 --- a/plugins/admin_advices/main.inc.php +++ b/plugins/admin_advices/main.inc.php @@ -1,9 +1,10 @@ <?php /*
-Plugin Name: Admin Advices !
-Version: 1.0.0
-Author: PhpWebGallery team
+Plugin Name: Admin Advices
+Version: 1.0
Description: Give you an advice on the administration page.
Plugin URI: http://www.phpwebgallery.net
+Author: PhpWebGallery team
+Author URI: http://www.phpwebgallery.net
*/
add_event_handler('loc_end_page_header', 'set_admin_advice_add_css' );
diff --git a/plugins/admin_multi_view/main.inc.php b/plugins/admin_multi_view/main.inc.php index f0ed7a74d..16f3e7410 100644 --- a/plugins/admin_multi_view/main.inc.php +++ b/plugins/admin_multi_view/main.inc.php @@ -3,6 +3,8 @@ Plugin Name: Multi view Version: 1.0 Description: Allows administrators to view gallery as guests and/or change the language and/or theme on the fly. Practical to debug changes ... Plugin URI: http://www.phpwebgallery.net +Author: PhpWebGallery team +Author URI: http://www.phpwebgallery.net */ add_event_handler('user_init', 'multiview_user_init' ); diff --git a/plugins/event_tracer/main.inc.php b/plugins/event_tracer/main.inc.php index 0976cae1c..d31708e9f 100644 --- a/plugins/event_tracer/main.inc.php +++ b/plugins/event_tracer/main.inc.php @@ -1,9 +1,10 @@ -<?php -/* +<?php /* Plugin Name: Event tracer Version: 1.0 Description: For developers. Shows all calls to trigger_event. Plugin URI: http://www.phpwebgallery.net +Author: PhpWebGallery team +Author URI: http://www.phpwebgallery.net */ if (!defined('PHPWG_ROOT_PATH')) die('Hacking attempt!'); diff --git a/plugins/hello_world/main.inc.php b/plugins/hello_world/main.inc.php index eb5f98b82..5f6dd9134 100644 --- a/plugins/hello_world/main.inc.php +++ b/plugins/hello_world/main.inc.php @@ -1,7 +1,10 @@ <?php /* -Plugin Name: Hello World ! -Author: PhpWebGallery team +Plugin Name: Hello World +Version: 1.0 Description: This example plugin changes the page banner for the administration page. +Plugin URI: http://www.phpwebgallery.net +Author: PhpWebGallery team +Author URI: http://www.phpwebgallery.net */ add_event_handler('loc_begin_page_header', 'hello_world_begin_header' ); diff --git a/template/yoga/admin/plugins.tpl b/template/yoga/admin/plugins.tpl index 00d344cac..370cd4141 100644 --- a/template/yoga/admin/plugins.tpl +++ b/template/yoga/admin/plugins.tpl @@ -19,7 +19,11 @@ <td>{plugins.plugin.DESCRIPTION}</td> <td> <!-- BEGIN action --> - <a href="{plugins.plugin.action.U_ACTION}" {TAG_INPUT_ENABLED}>{plugins.plugin.action.L_ACTION}</a> + <a href="{plugins.plugin.action.U_ACTION}" +<!-- BEGIN confirm --> + onclick="return confirm('Are you sure?');" +<!-- END confirm --> + {TAG_INPUT_ENABLED}>{plugins.plugin.action.L_ACTION}</a> <!-- END action --> </td> </tr> diff --git a/tools/prototype.js b/tools/prototype.js index 0e85338ba..505822177 100644 --- a/tools/prototype.js +++ b/tools/prototype.js @@ -1,5 +1,5 @@ -/* Prototype JavaScript framework, version 1.4.0 - * (c) 2005 Sam Stephenson <sam@conio.net> +/* Prototype JavaScript framework, version 1.5.0 + * (c) 2005-2007 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://prototype.conio.net/ @@ -7,11 +7,14 @@ /*--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.4.0', - ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', + Version: '1.5.0', + BrowserFeatures: { + XPath: !!document.evaluate + }, + ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)', emptyFunction: function() {}, - K: function(x) {return x} + K: function(x) { return x } } var Class = { @@ -25,22 +28,42 @@ var Class = { var Abstract = new Object(); Object.extend = function(destination, source) { - for (property in source) { + for (var property in source) { destination[property] = source[property]; } return destination; } -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; +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(); @@ -50,9 +73,9 @@ Function.prototype.bind = function() { } Function.prototype.bindAsEventListener = function(object) { - var __method = this; + var __method = this, args = $A(arguments), object = args.shift(); return function(event) { - return __method.call(object, event || window.event); + return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments))); } } @@ -77,7 +100,7 @@ var Try = { these: function() { var returnValue; - for (var i = 0; i < arguments.length; i++) { + for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { returnValue = lambda(); @@ -102,40 +125,73 @@ PeriodicalExecuter.prototype = { }, registerCallback: function() { - setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + 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.callback(this); } finally { this.currentlyExecuting = false; } } } } +String.interpret = function(value){ + return value == null ? '' : String(value); +} -/*--------------------------------------------------------------------------*/ +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 += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, -function $() { - var elements = new Array(); + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; - for (var i = 0; i < arguments.length; i++) { - var element = arguments[i]; - if (typeof element == 'string') - element = document.getElementById(element); + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, - if (arguments.length == 1) - return element; + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, - elements.push(element); - } + 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+$/, ''); + }, - return elements; -} -Object.extend(String.prototype, { stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); }, @@ -153,7 +209,7 @@ Object.extend(String.prototype, { }, evalScripts: function() { - return this.extractScripts().map(eval); + return this.extractScripts().map(function(script) { return eval(script) }); }, escapeHTML: function() { @@ -166,15 +222,28 @@ Object.extend(String.prototype, { unescapeHTML: function() { var div = document.createElement('div'); div.innerHTML = this.stripTags(); - return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; }, - toQueryParams: function() { - var pairs = this.match(/^\??(.*)$/)[1].split('&'); - return pairs.inject({}, function(params, pairString) { - var pair = pairString.split('='); - params[pair[0]] = pair[1]; - return params; + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return {}; + + return match[1].split(separator || '&').inject({}, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var name = decodeURIComponent(pair[0]); + var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; + + if (hash[name] !== undefined) { + if (hash[name].constructor != Array) + hash[name] = [hash[name]]; + if (value) hash[name].push(value); + } + else hash[name] = value; + } + return hash; }); }, @@ -182,29 +251,71 @@ Object.extend(String.prototype, { return this.split(''); }, + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + camelize: function() { - var oStringList = this.split('-'); - if (oStringList.length == 1) return oStringList[0]; + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; - var camelizedString = this.indexOf('-') == 0 - ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) - : oStringList[0]; + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; - for (var i = 1, len = oStringList.length; i < len; i++) { - var s = oStringList[i]; - camelizedString += s.charAt(0).toUpperCase() + s.substring(1); - } + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); - return camelizedString; + return camelized; }, - inspect: function() { - return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + capitalize: function(){ + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + 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 + String.interpret(object[match[3]]); + }); + } +} + var $break = new Object(); var $continue = new Object(); @@ -222,6 +333,14 @@ var Enumerable = { } catch (e) { if (e != $break) throw e; } + return this; + }, + + eachSlice: function(number, iterator) { + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.map(iterator); }, all: function(iterator) { @@ -234,7 +353,7 @@ var Enumerable = { }, any: function(iterator) { - var result = true; + var result = false; this.each(function(value, index) { if (result = !!(iterator || Prototype.K)(value, index)) throw $break; @@ -245,12 +364,12 @@ var Enumerable = { collect: function(iterator) { var results = []; this.each(function(value, index) { - results.push(iterator(value, index)); + results.push((iterator || Prototype.K)(value, index)); }); return results; }, - detect: function (iterator) { + detect: function(iterator) { var result; this.each(function(value, index) { if (iterator(value, index)) { @@ -291,6 +410,14 @@ var Enumerable = { return found; }, + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + inject: function(memo, iterator) { this.each(function(value, index) { memo = iterator(memo, value, index); @@ -300,7 +427,7 @@ var Enumerable = { invoke: function(method) { var args = $A(arguments).slice(1); - return this.collect(function(value) { + return this.map(function(value) { return value[method].apply(value, args); }); }, @@ -309,7 +436,7 @@ var Enumerable = { var result; this.each(function(value, index) { value = (iterator || Prototype.K)(value, index); - if (value >= (result || value)) + if (result == undefined || value >= result) result = value; }); return result; @@ -319,7 +446,7 @@ var Enumerable = { var result; this.each(function(value, index) { value = (iterator || Prototype.K)(value, index); - if (value <= (result || value)) + if (result == undefined || value < result) result = value; }); return result; @@ -352,7 +479,7 @@ var Enumerable = { }, sortBy: function(iterator) { - return this.collect(function(value, index) { + return this.map(function(value, index) { return {value: value, criteria: iterator(value, index)}; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; @@ -361,7 +488,7 @@ var Enumerable = { }, toArray: function() { - return this.collect(Prototype.K); + return this.map(); }, zip: function() { @@ -371,11 +498,14 @@ var Enumerable = { var collections = [this].concat(args).map($A); return this.map(function(value, index) { - iterator(value = collections.pluck(index)); - return value; + return iterator(collections.pluck(index)); }); }, + size: function() { + return this.toArray().length; + }, + inspect: function() { return '#<Enumerable:' + this.toArray().inspect() + '>'; } @@ -394,7 +524,7 @@ var $A = Array.from = function(iterable) { return iterable.toArray(); } else { var results = []; - for (var i = 0; i < iterable.length; i++) + for (var i = 0, length = iterable.length; i < length; i++) results.push(iterable[i]); return results; } @@ -402,11 +532,12 @@ var $A = Array.from = function(iterable) { Object.extend(Array.prototype, Enumerable); -Array.prototype._reverse = Array.prototype.reverse; +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++) + for (var i = 0, length = this.length; i < length; i++) iterator(this[i]); }, @@ -425,13 +556,13 @@ Object.extend(Array.prototype, { compact: function() { return this.select(function(value) { - return value != undefined || value != null; + return value != null; }); }, flatten: function() { return this.inject([], function(array, value) { - return array.concat(value.constructor == Array ? + return array.concat(value && value.constructor == Array ? value.flatten() : [value]); }); }, @@ -444,7 +575,7 @@ Object.extend(Array.prototype, { }, indexOf: function(object) { - for (var i = 0; i < this.length; i++) + for (var i = 0, length = this.length; i < length; i++) if (this[i] == object) return i; return -1; }, @@ -453,23 +584,88 @@ Object.extend(Array.prototype, { return (inline !== false ? this : this.toArray())._reverse(); }, - shift: function() { - var result = this[0]; - for (var i = 0; i < this.length - 1; i++) - this[i] = this[i + 1]; - this.length--; - return result; + 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]); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; }, inspect: function() { return '[' + this.map(Object.inspect).join(', ') + ']'; } }); -var Hash = { + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string){ + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if(window.opera){ + Array.prototype.concat = function(){ + var array = []; + for(var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for(var i = 0, length = arguments.length; i < length; i++) { + if(arguments[i].constructor == Array) { + for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + } +} +var Hash = function(obj) { + Object.extend(this, obj || {}); +}; + +Object.extend(Hash, { + toQueryString: function(obj) { + var parts = []; + + this.prototype._each.call(obj, function(pair) { + if (!pair.key) return; + + if (pair.value && pair.value.constructor == Array) { + var values = pair.value.compact(); + if (values.length < 2) pair.value = values.reduce(); + else { + key = encodeURIComponent(pair.key); + values.each(function(value) { + value = value != undefined ? encodeURIComponent(value) : ''; + parts.push(key + '=' + encodeURIComponent(value)); + }); + return; + } + } + if (pair.value == undefined) pair[1] = ''; + parts.push(pair.map(encodeURIComponent).join('=')); + }); + + return parts.join('&'); + } +}); + +Object.extend(Hash.prototype, Enumerable); +Object.extend(Hash.prototype, { _each: function(iterator) { - for (key in this) { + for (var key in this) { var value = this[key]; - if (typeof value == 'function') continue; + if (value && value == Hash.prototype[key]) continue; var pair = [key, value]; pair.key = key; @@ -487,16 +683,30 @@ var Hash = { }, merge: function(hash) { - return $H(hash).inject($H(this), function(mergedHash, pair) { + return $H(hash).inject(this, function(mergedHash, pair) { mergedHash[pair.key] = pair.value; return mergedHash; }); }, + remove: function() { + var result; + for(var i = 0, length = arguments.length; i < length; i++) { + var value = this[arguments[i]]; + if (value !== undefined){ + if (result === undefined) result = value; + else { + if (result.constructor != Array) result = [result]; + result.push(value) + } + } + delete this[arguments[i]]; + } + return result; + }, + toQueryString: function() { - return this.map(function(pair) { - return pair.map(encodeURIComponent).join('='); - }).join('&'); + return Hash.toQueryString(this); }, inspect: function() { @@ -504,14 +714,12 @@ var Hash = { 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; -} + if (object && object.constructor == Hash) return object; + return new Hash(object); +}; ObjectRange = Class.create(); Object.extend(ObjectRange.prototype, Enumerable); Object.extend(ObjectRange.prototype, { @@ -523,10 +731,10 @@ Object.extend(ObjectRange.prototype, { _each: function(iterator) { var value = this.start; - do { + while (this.include(value)) { iterator(value); value = value.succ(); - } while (this.include(value)); + } }, include: function(value) { @@ -545,9 +753,9 @@ var $R = function(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')}, - function() {return new XMLHttpRequest()} + function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; }, @@ -561,18 +769,18 @@ Ajax.Responders = { this.responders._each(iterator); }, - register: function(responderToAdd) { - if (!this.include(responderToAdd)) - this.responders.push(responderToAdd); + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); }, - unregister: function(responderToRemove) { - this.responders = this.responders.without(responderToRemove); + unregister: function(responder) { + this.responders = this.responders.without(responder); }, dispatch: function(callback, request, transport, json) { this.each(function(responder) { - if (responder[callback] && typeof responder[callback] == 'function') { + if (typeof responder[callback] == 'function') { try { responder[callback].apply(responder, [request, transport, json]); } catch (e) {} @@ -587,7 +795,6 @@ Ajax.Responders.register({ onCreate: function() { Ajax.activeRequestCount++; }, - onComplete: function() { Ajax.activeRequestCount--; } @@ -599,19 +806,15 @@ Ajax.Base.prototype = { this.options = { method: 'post', asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', 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(); + this.options.method = this.options.method.toLowerCase(); + if (typeof this.options.parameters == 'string') + this.options.parameters = this.options.parameters.toQueryParams(); } } @@ -620,6 +823,8 @@ Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + _complete: false, + initialize: function(url, options) { this.transport = Ajax.getTransport(); this.setOptions(options); @@ -627,111 +832,146 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), { }, request: function(url) { - var parameters = this.options.parameters || ''; - if (parameters.length > 0) parameters += '&_='; + this.url = url; + this.method = this.options.method; + var params = this.options.parameters; - try { - this.url = url; - if (this.options.method == 'get' && parameters.length > 0) - this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + params = Hash.toQueryString(params); + if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_=' + // when GET, append parameters to URL + if (this.method == 'get' && params) + this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params; + + try { Ajax.Responders.dispatch('onCreate', this, this.transport); - this.transport.open(this.options.method, this.url, + this.transport.open(this.method.toUpperCase(), this.url, this.options.asynchronous); - if (this.options.asynchronous) { - this.transport.onreadystatechange = this.onStateChange.bind(this); - setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); - } + 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); + var body = this.method == 'post' ? (this.options.postBody || params) : null; - } catch (e) { - this.dispatchException(e); - } - }, - - setRequestHeaders: function() { - var requestHeaders = - ['X-Requested-With', 'XMLHttpRequest', - 'X-Prototype-Version', Prototype.Version]; + this.transport.send(body); - if (this.options.method == 'post') { - requestHeaders.push('Content-type', - 'application/x-www-form-urlencoded'); + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); - /* 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]); + catch (e) { + this.dispatchException(e); + } }, onStateChange: function() { var readyState = this.transport.readyState; - if (readyState != 1) + if (readyState > 1 && !((readyState == 4) && this._complete)) this.respondToReadyState(this.transport.readyState); }, - header: function(name) { - try { - return this.transport.getResponseHeader(name); - } catch (e) {} - }, + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } - evalJSON: function() { - try { - return eval(this.header('X-JSON')); - } catch (e) {} - }, + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; - evalResponse: function() { - try { - return eval(this.transport.responseText); - } catch (e) { - this.dispatchException(e); + if (typeof extras.push == 'function') + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + return !this.transport.status + || (this.transport.status >= 200 && this.transport.status < 300); }, respondToReadyState: function(readyState) { - var event = Ajax.Request.Events[readyState]; + var state = Ajax.Request.Events[readyState]; var transport = this.transport, json = this.evalJSON(); - if (event == 'Complete') { + if (state == 'Complete') { try { + this._complete = true; (this.options['on' + this.transport.status] - || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] || Prototype.emptyFunction)(transport, json); } catch (e) { this.dispatchException(e); } - if ((this.header('Content-type') || '').match(/^text\/javascript/i)) - this.evalResponse(); + if ((this.getHeader('Content-type') || 'text/javascript').strip(). + match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) + this.evalResponse(); } try { - (this.options['on' + event] || Prototype.emptyFunction)(transport, json); - Ajax.Responders.dispatch('on' + event, this, transport, json); + (this.options['on' + state] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + state, this, transport, json); } catch (e) { this.dispatchException(e); } - /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ - if (event == 'Complete') + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalJSON: function() { + try { + var json = this.getHeader('X-JSON'); + return json ? eval('(' + json + ')') : null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } }, dispatchException: function(exception) { @@ -744,41 +984,37 @@ 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.container = { + success: (container.success || 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.options.onComplete = (function(transport, param) { this.updateContent(); - onComplete(transport, object); + onComplete(transport, param); }).bind(this); this.request(url); }, updateContent: function() { - var receiver = this.responseIsSuccess() ? - this.containers.success : this.containers.failure; + var receiver = this.container[this.success() ? 'success' : 'failure']; var response = this.transport.responseText; - if (!this.options.evalScripts) - response = response.stripScripts(); + if (!this.options.evalScripts) response = response.stripScripts(); - if (receiver) { - if (this.options.insertion) { + if (receiver = $(receiver)) { + if (this.options.insertion) new this.options.insertion(receiver, response); - } else { - Element.update(receiver, response); - } + else + receiver.update(response); } - if (this.responseIsSuccess()) { + if (this.success()) { if (this.onComplete) setTimeout(this.onComplete.bind(this), 10); } @@ -807,7 +1043,7 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { }, stop: function() { - this.updater.onComplete = undefined; + this.updater.options.onComplete = undefined; clearTimeout(this.timer); (this.onComplete || Prototype.emptyFunction).apply(this, arguments); }, @@ -827,60 +1063,227 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); -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(child); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); return elements; - }); + } + if (typeof element == 'string') + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(query.snapshotItem(i)); + return results; + }; } +document.getElementsByClassName = function(className, parentElement) { + if (Prototype.BrowserFeatures.XPath) { + var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; + return document._getElementsByXPath(q, parentElement); + } else { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + var elements = [], child; + for (var i = 0, length = children.length; i < length; i++) { + child = children[i]; + if (Element.hasClassName(child, className)) + elements.push(Element.extend(child)); + } + return elements; + } +}; + /*--------------------------------------------------------------------------*/ -if (!window.Element) { +if (!window.Element) var Element = new Object(); -} -Object.extend(Element, { +Element.extend = function(element) { + if (!element || _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); + + Object.extend(methods, Element.Methods.Simulated); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + 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() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - Element[Element.visible(element) ? 'hide' : 'show'](element); - } + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; }, - hide: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - element.style.display = 'none'; - } + hide: function(element) { + $(element).style.display = 'none'; + return element; }, - show: function() { - for (var i = 0; i < arguments.length; i++) { - var element = $(arguments[i]); - element.style.display = ''; - } + show: function(element) { + $(element).style.display = ''; + return element; }, remove: function(element) { element = $(element); element.parentNode.removeChild(element); + return element; }, update: function(element, html) { + html = typeof html == 'undefined' ? '' : html.toString(); $(element).innerHTML = html.stripScripts(); setTimeout(function() {html.evalScripts()}, 10); + return element; }, - getHeight: function(element) { + replace: function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + 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) { + return $A($(element).getElementsByTagName('*')); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { element = $(element); - return element.offsetHeight; + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + 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) { + return document.getElementsByClassName(className, element); + }, + + readAttribute: function(element, name) { + element = $(element); + if (document.all && !window.opera) { + var t = Element._attributeTranslations; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + var attribute = element.attributes[name]; + if(attribute) return attribute.nodeValue; + } + return element.getAttribute(name); + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; }, classNames: function(element) { @@ -889,67 +1292,131 @@ Object.extend(Element, { hasClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).include(className); + var elementClassName = element.className; + if (elementClassName.length == 0) return false; + if (elementClassName == className || + elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + return true; + return false; }, addClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).add(className); + Element.classNames(element).add(className); + return element; }, removeClassName: function(element, className) { if (!(element = $(element))) return; - return Element.classNames(element).remove(className); + Element.classNames(element).remove(className); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](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); - for (var i = 0; i < element.childNodes.length; i++) { - var node = element.childNodes[i]; + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) - Element.remove(node); + element.removeChild(node); + node = nextNode; } + return element; }, empty: function(element) { return $(element).innerHTML.match(/^\s*$/); }, + descendantOf: 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); + var pos = Position.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; }, getStyle: function(element, style) { element = $(element); - var value = element.style[style.camelize()]; + if (['float','cssFloat'].include(style)) + style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat'); + style = style.camelize(); + var value = element.style[style]; if (!value) { if (document.defaultView && document.defaultView.getComputedStyle) { var css = document.defaultView.getComputedStyle(element, null); - value = css ? css.getPropertyValue(style) : null; + value = css ? css[style] : null; } else if (element.currentStyle) { - value = element.currentStyle[style.camelize()]; + value = element.currentStyle[style]; } } + if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none')) + value = element['offset'+style.capitalize()] + 'px'; + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) if (Element.getStyle(element, 'position') == 'static') value = 'auto'; - + if(style == 'opacity') { + if(value) return parseFloat(value); + if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } return value == 'auto' ? null : value; }, setStyle: function(element, style) { element = $(element); - for (name in style) - element.style[name.camelize()] = style[name]; + for (var name in style) { + var value = style[name]; + if(name == 'opacity') { + if (value == 1) { + value = (/Gecko/.test(navigator.userAgent) && + !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else if(value == '') { + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')'; + } + } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat'; + element.style[name.camelize()] = value; + } + return element; }, getDimensions: function(element) { element = $(element); - if (Element.getStyle(element, 'display') != 'none') + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug return {width: element.offsetWidth, height: element.offsetHeight}; // All *Width and *Height properties give 0 on elements with display none, @@ -957,12 +1424,13 @@ Object.extend(Element, { var els = element.style; var originalVisibility = els.visibility; var originalPosition = els.position; + var originalDisplay = els.display; els.visibility = 'hidden'; els.position = 'absolute'; - els.display = ''; + els.display = 'block'; var originalWidth = element.clientWidth; var originalHeight = element.clientHeight; - els.display = 'none'; + els.display = originalDisplay; els.position = originalPosition; els.visibility = originalVisibility; return {width: originalWidth, height: originalHeight}; @@ -981,6 +1449,7 @@ Object.extend(Element, { element.style.left = 0; } } + return element; }, undoPositioned: function(element) { @@ -993,24 +1462,153 @@ Object.extend(Element, { element.style.bottom = element.style.right = ''; } + return element; }, makeClipping: function(element) { element = $(element); - if (element._overflow) return; - element._overflow = element.style.overflow; + if (element._overflow) return element; + 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; - element._overflow = undefined; + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + } +}; + +Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf}); + +Element._attributeTranslations = {}; + +Element._attributeTranslations.names = { + colspan: "colSpan", + rowspan: "rowSpan", + valign: "vAlign", + datetime: "dateTime", + accesskey: "accessKey", + tabindex: "tabIndex", + enctype: "encType", + maxlength: "maxLength", + readonly: "readOnly", + longdesc: "longDesc" +}; + +Element._attributeTranslations.values = { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + + title: function(element) { + var node = element.getAttributeNode('title'); + return node.specified ? node.nodeValue : null; } +}; + +Object.extend(Element._attributeTranslations.values, { + href: Element._attributeTranslations.values._getAttr, + src: Element._attributeTranslations.values._getAttr, + disabled: Element._attributeTranslations.values._flag, + checked: Element._attributeTranslations.values._flag, + readonly: Element._attributeTranslations.values._flag, + multiple: Element._attributeTranslations.values._flag }); +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + var t = Element._attributeTranslations; + attribute = t.names[attribute] || attribute; + return $(element).getAttributeNode(attribute).specified; + } +}; + +// IE is missing .innerHTML support for TABLE-related elements +if (document.all && !window.opera){ + Element.Methods.update = function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + var tagName = element.tagName.toUpperCase(); + if (['THEAD','TBODY','TR','TD'].include(tagName)) { + 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(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { + var className = 'HTML' + tag + 'Element'; + if(window[className]) return; + var klass = window[className] = {}; + klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; + }); + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + var cache = Element.extend.cache; + for (var property in methods) { + var value = methods[property]; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = cache.findOrStore(value); + } + } + + if (typeof HTMLElement != 'undefined') { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + 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; @@ -1029,7 +1627,8 @@ Abstract.Insertion.prototype = { try { this.element.insertAdjacentHTML(this.adjacency, this.content); } catch (e) { - if (this.element.tagName.toLowerCase() == 'tbody') { + var tagName = this.element.tagName.toUpperCase(); + if (['TBODY', 'TR'].include(tagName)) { this.insertContent(this.contentFromAnonymousTable()); } else { throw e; @@ -1128,220 +1727,358 @@ Element.ClassNames.prototype = { add: function(classNameToAdd) { if (this.include(classNameToAdd)) return; - this.set(this.toArray().concat(classNameToAdd).join(' ')); + this.set($A(this).concat(classNameToAdd).join(' ')); }, remove: function(classNameToRemove) { if (!this.include(classNameToRemove)) return; - this.set(this.select(function(className) { - return className != classNameToRemove; - }).join(' ')); + this.set($A(this).without(classNameToRemove).join(' ')); }, toString: function() { - return this.toArray().join(' '); + return $A(this).join(' '); } -} +}; Object.extend(Element.ClassNames.prototype, Enumerable); -var Field = { - clear: function() { - for (var i = 0; i < arguments.length; i++) - $(arguments[i]).value = ''; +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); }, - focus: function(element) { - $(element).focus(); + 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.readAttribute("id") == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0, length = clause.length; i < length; i++) + conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.readAttribute(' + 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('element.hasAttribute(' + attribute.name.inspect() + ')'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); }, - present: function() { - for (var i = 0; i < arguments.length; i++) - if ($(arguments[i]).value == '') return false; - return true; + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + element = $(element); \ + return ' + this.buildMatchExpression()); }, - select: function(element) { - $(element).select(); + 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, length = scope.length; i < length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; }, - activate: function(element) { - element = $(element); - element.focus(); - if (element.select) - element.select(); + toString: function() { + return this.expression; } } -/*--------------------------------------------------------------------------*/ +Object.extend(Selector, { + matchElements: function(elements, expression) { + var selector = new Selector(expression); + return elements.select(selector.match.bind(selector)).map(Element.extend); + }, + + 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.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).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 = { - 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); - } + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, getHash) { + var data = elements.inject({}, function(result, element) { + if (!element.disabled && element.name) { + var key = element.name, value = $(element).getValue(); + if (value != undefined) { + if (result[key]) { + if (result[key].constructor != Array) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return getHash ? data : Hash.toQueryString(data); + } +}; - return queryComponents.join('&'); +Form.Methods = { + serialize: function(form, getHash) { + return Form.serializeElements(Form.getElements(form), getHash); }, getElements: function(form) { - form = $(form); - var elements = new Array(); - - for (tagName in Form.Element.Serializers) { - var tagElements = form.getElementsByTagName(tagName); - for (var j = 0; j < tagElements.length; j++) - elements.push(tagElements[j]); - } - return elements; + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); }, getInputs: function(form, typeName, name) { form = $(form); var inputs = form.getElementsByTagName('input'); - if (!typeName && !name) - return inputs; + if (!typeName && !name) return $A(inputs).map(Element.extend); - var matchingInputs = new Array(); - for (var i = 0; i < inputs.length; i++) { + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { var input = inputs[i]; - if ((typeName && input.type != typeName) || - (name && input.name != name)) + if ((typeName && input.type != typeName) || (name && input.name != name)) continue; - matchingInputs.push(input); + matchingInputs.push(Element.extend(input)); } return matchingInputs; }, disable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; + form = $(form); + form.getElements().each(function(element) { element.blur(); element.disabled = 'true'; - } + }); + return form; }, enable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; + form = $(form); + form.getElements().each(function(element) { element.disabled = ''; - } + }); + return form; }, findFirstElement: function(form) { - return Form.getElements(form).find(function(element) { + return $(form).getElements().find(function(element) { return element.type != 'hidden' && !element.disabled && ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); }); }, focusFirstElement: function(form) { - Field.activate(Form.findFirstElement(form)); + form = $(form); + form.findFirstElement().activate(); + return form; + } +} + +Object.extend(Form, Form.Methods); + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; }, - reset: function(form) { - $(form).reset(); + select: function(element) { + $(element).select(); + return element; } } -Form.Element = { +Form.Element.Methods = { serialize: function(element) { element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = {}; + pair[element.name] = value; + return Hash.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); + return Form.Element.Serializers[method](element); + }, - if (parameter) { - var key = encodeURIComponent(parameter[0]); - if (key.length == 0) return; + clear: function(element) { + $(element).value = ''; + return element; + }, - if (parameter[1].constructor != Array) - parameter[1] = [parameter[1]]; + present: function(element) { + return $(element).value != ''; + }, - return parameter[1].map(function(value) { - return key + '=' + encodeURIComponent(value); - }).join('&'); - } + activate: function(element) { + element = $(element); + element.focus(); + if (element.select && ( element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type) ) ) + element.select(); + return element; }, - getValue: function(element) { + disable: function(element) { element = $(element); - var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); + element.disabled = true; + return element; + }, - if (parameter) - return parameter[1]; + enable: function(element) { + element = $(element); + element.blur(); + element.disabled = false; + return element; } } +Object.extend(Form.Element, Form.Element.Methods); +var Field = Form.Element; +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + Form.Element.Serializers = { input: function(element) { switch (element.type.toLowerCase()) { - case 'submit': - case 'hidden': - case 'password': - case 'text': - return Form.Element.Serializers.textarea(element); 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]; + return element.checked ? element.value : null; }, textarea: function(element) { - return [element.name, element.value]; + return element.value; }, select: function(element) { - return Form.Element.Serializers[element.type == 'select-one' ? + return this[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; - if (!value && !('value' in opt)) - value = opt.text; - } - return [element.name, value]; + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; }, selectMany: function(element) { - var value = new Array(); - for (var i = 0; i < element.length; i++) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; - if (opt.selected) { - var optValue = opt.value; - if (!optValue && !('value' in opt)) - optValue = opt.text; - value.push(optValue); - } + if (opt.selected) values.push(this.optionValue(opt)); } - return [element.name, value]; + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; } } /*--------------------------------------------------------------------------*/ -var $F = Form.Element.getValue; - -/*--------------------------------------------------------------------------*/ - Abstract.TimedObserver = function() {} Abstract.TimedObserver.prototype = { initialize: function(element, frequency, callback) { @@ -1359,7 +2096,9 @@ Abstract.TimedObserver.prototype = { onTimerEvent: function() { var value = this.getValue(); - if (this.lastValue != value) { + var changed = ('string' == typeof this.lastValue && 'string' == typeof value + ? this.lastValue != value : String(this.lastValue) != String(value)); + if (changed) { this.callback(this.element, value); this.lastValue = value; } @@ -1404,9 +2143,7 @@ Abstract.EventObserver.prototype = { }, registerFormCallbacks: function() { - var elements = Form.getElements(this.element); - for (var i = 0; i < elements.length; i++) - this.registerCallback(elements[i]); + Form.getElements(this.element).each(this.registerCallback.bind(this)); }, registerCallback: function(element) { @@ -1416,11 +2153,7 @@ Abstract.EventObserver.prototype = { case 'radio': Event.observe(element, 'click', this.onElementEvent.bind(this)); break; - case 'password': - case 'text': - case 'textarea': - case 'select-one': - case 'select-multiple': + default: Event.observe(element, 'change', this.onElementEvent.bind(this)); break; } @@ -1455,6 +2188,10 @@ Object.extend(Event, { 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; @@ -1510,7 +2247,7 @@ Object.extend(Event, { unloadCache: function() { if (!Event.observers) return; - for (var i = 0; i < Event.observers.length; i++) { + for (var i = 0, length = Event.observers.length; i < length; i++) { Event.stopObserving.apply(this, Event.observers[i]); Event.observers[i][0] = null; } @@ -1518,7 +2255,7 @@ Object.extend(Event, { }, observe: function(element, name, observer, useCapture) { - var element = $(element); + element = $(element); useCapture = useCapture || false; if (name == 'keypress' && @@ -1526,11 +2263,11 @@ Object.extend(Event, { || element.attachEvent)) name = 'keydown'; - this._observeAndCache(element, name, observer, useCapture); + Event._observeAndCache(element, name, observer, useCapture); }, stopObserving: function(element, name, observer, useCapture) { - var element = $(element); + element = $(element); useCapture = useCapture || false; if (name == 'keypress' && @@ -1541,13 +2278,16 @@ Object.extend(Event, { if (element.removeEventListener) { element.removeEventListener(name, observer, useCapture); } else if (element.detachEvent) { - element.detachEvent('on' + name, observer); + try { + element.detachEvent('on' + name, observer); + } catch (e) {} } } }); /* prevent memory leaks in IE */ -Event.observe(window, 'unload', Event.unloadCache, false); +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 @@ -1594,7 +2334,8 @@ var Position = { valueL += element.offsetLeft || 0; element = element.offsetParent; if (element) { - p = Element.getStyle(element, 'position'); + if(element.tagName=='BODY') break; + var p = Element.getStyle(element, 'position'); if (p == 'relative' || p == 'absolute') break; } } while (element); @@ -1650,17 +2391,6 @@ var Position = { element.offsetWidth; }, - clone: function(source, target) { - source = $(source); - target = $(target); - target.style.position = 'absolute'; - var offsets = this.cumulativeOffset(source); - target.style.top = offsets[1] + 'px'; - target.style.left = offsets[0] + 'px'; - target.style.width = source.offsetWidth + 'px'; - target.style.height = source.offsetHeight + 'px'; - }, - page: function(forElement) { var valueT = 0, valueL = 0; @@ -1677,8 +2407,10 @@ var Position = { element = forElement; do { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; + if (!window.opera || element.tagName=='BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } } while (element = element.parentNode); return [valueL, valueT]; @@ -1739,10 +2471,10 @@ var Position = { 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';; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; }, relativize: function(element) { @@ -1778,4 +2510,6 @@ if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { return [valueL, valueT]; } -}
\ No newline at end of file +} + +Element.addMethods();
\ No newline at end of file diff --git a/tools/ws.htm b/tools/ws.htm index ac525ab1d..fe2b5840b 100644 --- a/tools/ws.htm +++ b/tools/ws.htm @@ -5,22 +5,10 @@ <script type="text/javascript" src="prototype.js" ></script> <script type="text/javascript"> -function setElementText(id, text) -{ - if (!text) text=""; - var elt = document.getElementById(id); - if (!elt) alert('setElementText '+id); - elt.innerHTML = text; -} function setVisibility(id, vis) { - document.getElementById(id).style.visibility = vis; -} - -function clearError() -{ - setElementText("error", ""); + $(id).style.visibility = vis; } function dumpError(err) @@ -37,11 +25,11 @@ function dumpError(err) s += '<br/><small><pre>'+ err.stack + '</pre></small>'; } } - setElementText("error", s); + $("error").update(s); } var gServiceUrl; -var gCurrentMethodParams; +var gCachedMethods; Ajax.Responders.register({ @@ -100,13 +88,14 @@ function pwgGetJsonResult(transport) function pwgChangeUrl() { - clearError(); + $("error").update(""); setVisibility("methodListWrapper", "hidden"); - setElementText("methodList", ""); + $("methodList").update(""); setVisibility("methodWrapper", "hidden"); + setVisibility("methodDetailWrapper", "hidden"); gServiceUrl = $F('ws_url'); - gCurrentMethodParams = null; + gCachedMethods = new Hash(); try { var ajaxReq = new Ajax.Request( @@ -130,29 +119,32 @@ function onSuccess_getMethodList(transport) { ml += '<li><a href="#" onclick="return pwgSelectMethod(this.innerHTML)">'+ result.methods[i]+'</a></li>'; } - setElementText("methodList", ml); + $("methodList").update(ml); setVisibility("methodListWrapper", "visible"); } -function pwgSelectMethod(method) +function pwgSelectMethod(methodName) { - clearError(); - setElementText("methodName", method); + $("error").update(""); + $("methodName").update(methodName); setVisibility("methodDetailWrapper", "hidden"); setVisibility("methodWrapper", "visible"); - gCurrentMethodParams = null; - try { - - var ajaxReq = new Ajax.Request( - gServiceUrl, - {method:'get', parameters:'format=json&method=reflection.getMethodDetails&methodName='+method, - onSuccess: function (r) { onSuccess_getMethodDetails(r); } - } - ) - }catch (e) + if ( gCachedMethods[methodName] ) + fillNewMethod( gCachedMethods[methodName] ); + else { - dumpError( e ); + try { + var ajaxReq = new Ajax.Request( + gServiceUrl, + {method:'get', parameters:'format=json&method=reflection.getMethodDetails&methodName='+methodName, + onSuccess: function (r) { onSuccess_getMethodDetails(r); } + } + ) + }catch (e) + { + dumpError( e ); + } } return false; } @@ -160,67 +152,90 @@ function pwgSelectMethod(method) function onSuccess_getMethodDetails(transport) { var result = pwgGetJsonResult(transport); + fillNewMethod( gCachedMethods[result.name] = $H(result) ); +} + +function fillNewMethod(method) +{ var methodParamsElt = $("methodParams"); while (methodParamsElt.tBodies[0].rows.length) methodParamsElt.tBodies[0].deleteRow(methodParamsElt.tBodies[0].rows.length-1); - if (result.params) - { - gCurrentMethodParams = result.params; - if (result.params.length>0) - { - for (var i=0; i<result.params.length; i++) + if (method.params && method.params.length>0) + { + for (var i=0; i<method.params.length; i++) { var row = methodParamsElt.tBodies[0].insertRow(-1); - var isOptional = result.params[i].optional; - var defaultValue = result.params[i].defaultValue == null ? '' : result.params[i].defaultValue; + var isOptional = method.params[i].optional; + var acceptArray = method.params[i].acceptArray; + var defaultValue = method.params[i].defaultValue == null ? '' : method.params[i].defaultValue; - row.insertCell(0).innerHTML = result.params[i].name; - row.insertCell(1).innerHTML = (isOptional ? 'optional':'required'); + row.insertCell(0).innerHTML = method.params[i].name; + row.insertCell(1).innerHTML = '<span title="parameter is '+(isOptional ? 'optional':'required') +'">'+(isOptional ? '?':'*')+'</span>' + + (method.params[i].acceptArray ? ' <span title="parameter can be an array; use | (pipe) character to split values">[ ]</span>':''); row.insertCell(2).innerHTML = '<input id="methodParameterSend_'+i+'" type="checkbox" '+(isOptional ? '':'checked="checked"')+'/>'; row.insertCell(3).innerHTML = '<input id="methodParameterValue_'+i+'"" value="'+defaultValue+'" style="width:99%" onchange="$(\'methodParameterSend_'+i+'\').checked=true;"/>'; } - } - } - setElementText("methodDescription", result.description); + } + $("methodDescription").update(method.description); setVisibility("methodDetailWrapper", "visible"); } function pwgInvokeMethod( newWindow ) { - var method = document.getElementById('methodName').innerHTML; + var methodName = $('methodName').innerHTML; + var method = gCachedMethods[methodName]; var reqUrl = gServiceUrl; reqUrl += "?format="+$F('responseFormat'); - if (document.getElementById('requestFormat').value == 'get') + if ($('requestFormat').value == 'get') { - reqUrl += "&method="+method; - for ( var i=0; i<gCurrentMethodParams.length; i++) + reqUrl += "&method="+methodName; + for ( var i=0; i<method.params.length; i++) { - if (document.getElementById('methodParameterSend_'+i).checked) - reqUrl += '&'+gCurrentMethodParams[i].name+'='+$F('methodParameterValue_'+i); + if (! $('methodParameterSend_'+i).checked) + continue; + + if ( method.params[i].acceptArray && $F('methodParameterValue_'+i).split('|').length > 1 ) + { + $F('methodParameterValue_'+i).split('|').each( + function(v) { + reqUrl += '&'+method.params[i].name+'[]='+v; + } + ); + } + else + reqUrl += '&'+method.params[i].name+'='+$F('methodParameterValue_'+i); } if ( !newWindow ) - document.getElementById("invokeFrame").src = reqUrl; + $("invokeFrame").src = reqUrl; else window.open(reqUrl); } else { - var form = document.getElementById("invokeForm"); + var form = $("invokeForm"); form.action = reqUrl; - var t = '<input type="hidden" name="'+'method'+'" value="'+method+'"/>'; - for ( var i=0; i<gCurrentMethodParams.length; i++) + var t = '<input type="hidden" name="'+'method'+'" value="'+methodName+'"/>'; + for ( var i=0; i<method.params.length; i++) { - if (document.getElementById('methodParameterSend_'+i).checked) - t += '<input type="hidden" name="'+gCurrentMethodParams[i].name+'" value="'+$F('methodParameterValue_'+i)+'"/>'; + if (! $('methodParameterSend_'+i).checked) + continue; + + if ( method.params[i].acceptArray && $F('methodParameterValue_'+i).split('|').length > 1 ) + { + $F('methodParameterValue_'+i).split('|').each( + function(v) { + t += '<input type="hidden" name="'+method.params[i].name+'[]" value="'+v+'"/>'; + } + ); + } + else + t += '<input type="hidden" name="'+method.params[i].name+'" value="'+$F('methodParameterValue_'+i)+'"/>'; } form.innerHTML = t; - if ( !newWindow ) - form.target = "invokeFrame"; - else - form.target = "_blank"; + form.target = newWindow ? "_blank" : "invokeFrame"; form.submit(); } return false; @@ -356,7 +371,7 @@ a:hover { <thead> <tr> <td style="width:150px">Parameter</td> - <td>Optional</td> + <td>Extra</td> <td>Send</td> <td style="width:160px">Value</td> </tr> |