diff --git a/app/base/BaseApiModel.php b/app/base/BaseApiModel.php index 38934cfa..6458fdb0 100644 --- a/app/base/BaseApiModel.php +++ b/app/base/BaseApiModel.php @@ -1,4 +1,7 @@ get_route(); $data['route_path'] = ($route) ? $router->get_route()->path : ""; - $path = _dir(APP_DIR, 'views', "{$template}.php"); + $defaultHandler->addDataTable('Template Data', $data); - if ( ! is_file($path)) + $template_path = _dir(APP_DIR, 'views', "{$template}.php"); + + if ( ! is_file($template_path)) { throw new Exception("Invalid template : {$path}"); + die(); } ob_start(); extract($data); include _dir(APP_DIR, 'views', 'header.php'); - include $path; + include $template_path; + include _dir(APP_DIR, 'views', 'footer.php'); $buffer = ob_get_contents(); ob_end_clean(); @@ -55,7 +62,7 @@ class BaseController { /** * Output json with the proper content type * - * @param mixed data + * @param mixed $data * @return void */ public function outputJSON($data) @@ -68,5 +75,27 @@ class BaseController { header("Content-type: application/json"); echo $data; } + + /** + * Redirect to the selected page + * + * @param string $url + * @param int $code + * @return void + */ + public function redirect($url, $code, $type="anime") + { + $url = full_url($url, $type); + + $codes = [ + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other' + ]; + + header("HTTP/1.1 {$code} {$codes[$code]}"); + header("Location: {$url}"); + die(); + } } // End of BaseController.php \ No newline at end of file diff --git a/app/base/BaseDBModel.php b/app/base/BaseDBModel.php index a4480090..174eae35 100644 --- a/app/base/BaseDBModel.php +++ b/app/base/BaseDBModel.php @@ -1,4 +1,7 @@ 3) return $api_path; $cached_image = "{$series_slug}.{$ext}"; $cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}"; diff --git a/app/base/Router.php b/app/base/Router.php index a1c41940..b7704c6b 100644 --- a/app/base/Router.php +++ b/app/base/Router.php @@ -1,4 +1,7 @@ params['controller']; $action_method = $route->params['action']; $params = (isset($route->params['params'])) ? $route->params['params'] : []; + + if ( ! empty($route->tokens)) + { + foreach($route->tokens as $key => $v) + { + if (array_key_exists($key, $route->params)) + { + $params[$key] = $route->params[$key]; + } + } + } } $controller = new $controller_name(); // Run the appropriate controller method + $defaultHandler->addDataTable('controller_args', $params); call_user_func_array([$controller, $action_method], $params); } @@ -123,7 +138,20 @@ class Router { { $path = $route['path']; unset($route['path']); - $this->router->add($name, $path)->addValues($route); + + if ( ! array_key_exists('tokens', $route)) + { + $this->router->add($name, $path)->addValues($route); + } + else + { + $tokens = $route['tokens']; + unset($route['tokens']); + + $this->router->add($name, $path) + ->addValues($route) + ->addTokens($tokens); + } } } } diff --git a/app/base/functions.php b/app/base/functions.php index eded0de8..bc326bd6 100644 --- a/app/base/functions.php +++ b/app/base/functions.php @@ -17,25 +17,50 @@ function is_selected($a, $b) } /** - * Generate full url path from the route path based on config + * Inverse of selected helper function * - * @param string $path - The route path - * @param [string] $host - The controller (anime or manga), defaults to anime + * @param string $a - First item to compare + * @param string $b - Second item to compare * @return string */ -function full_url($path, $type="anime") +function is_not_selected($a, $b) +{ + return ($a !== $b) ? 'selected' : ''; +} + +/** + * Generate full url path from the route path based on config + * + * @param string $path - (optional) The route path + * @param string $type - (optional) The controller (anime or manga), defaults to anime + * @return string + */ +function full_url($path="", $type="anime") { global $config; $config_path = $config->{"{$type}_path"}; $config_host = $config->{"{$type}_host"}; + $config_default_route = $config->{"default_{$type}_path"}; // Remove beginning/trailing slashes $config_path = trim($config_path, '/'); $path = trim($path, '/'); + // Remove any optional parameters from the route + $path = preg_replace('`{/.*?}`i', '', $path); + + // Set the default view + if ($path === '') + { + $path .= trim($config_default_route, '/'); + if ($config->default_to_list_view) $path .= '/list'; + } + + // Set the appropriate HTTP host $host = ($config_host !== '') ? $config_host : $_SERVER['HTTP_HOST']; + // Set an leading folder if ($config_path !== '') { $path = "{$config_path}/{$path}"; @@ -44,4 +69,16 @@ function full_url($path, $type="anime") return "//{$host}/{$path}"; } +/** + * Get the last segment of the current url + * + * @return string + */ +function last_segment() +{ + $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + $segments = explode('/', $path); + return end($segments); +} + // End of functions.php \ No newline at end of file diff --git a/app/config/config.php b/app/config/config.php index 748300d6..2d4f409c 100644 --- a/app/config/config.php +++ b/app/config/config.php @@ -3,6 +3,10 @@ return (object)[ // Username for feeds 'hummingbird_username' => 'timw4mail', + // Included config files + 'routes' => require _dir(CONF_DIR, 'routes.php'), + 'database' => require _dir(CONF_DIR, 'database.php'), + // ---------------------------------------------------------------------------- // Routing // @@ -15,11 +19,14 @@ return (object)[ 'anime_path' => '', 'manga_path' => '', + // Default pages for anime/manga + 'default_anime_path' => '/watching', + 'default_manga_path' => '/all', + + // Default to list view? + 'default_to_list_view' => FALSE, + // Cache paths 'data_cache_path' => _dir(APP_DIR, 'cache'), 'img_cache_path' => _dir(ROOT_DIR, 'public/images'), - - // Included config files - 'routes' => require _dir(CONF_DIR, 'routes.php'), - 'database' => require _dir(CONF_DIR, 'database.php'), ]; \ No newline at end of file diff --git a/app/config/routes.php b/app/config/routes.php index f2d9916b..e164aa80 100644 --- a/app/config/routes.php +++ b/app/config/routes.php @@ -2,120 +2,178 @@ return [ 'anime' => [ + 'index' => [ + 'path' => '/', + 'controller' => 'AnimeController', + 'action' => 'redirect', + 'params' => [ + 'url' => '', // Determined by config + 'code' => '301' + ] + ], 'all' => [ - 'path' => '/all', + 'path' => '/all{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'all', - 'title' => WHOSE . " Anime List · All" + 'title' => WHOSE . " Anime List · All" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], - 'index' => [ - 'path' => '/', + 'watching' => [ + 'path' => '/watching{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'currently-watching', - 'title' => WHOSE . " Anime List · Watching" + 'title' => WHOSE . " Anime List · Watching" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'plan_to_watch' => [ - 'path' => '/plan_to_watch', + 'path' => '/plan_to_watch{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'plan-to-watch', - 'title' => WHOSE . " Anime List · Plan to Watch" + 'title' => WHOSE . " Anime List · Plan to Watch" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'on_hold' => [ - 'path' => '/on_hold', + 'path' => '/on_hold{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'on-hold', - 'title' => WHOSE . " Anime List · On Hold" + 'title' => WHOSE . " Anime List · On Hold" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'dropped' => [ - 'path' => '/dropped', + 'path' => '/dropped{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'dropped', - 'title' => WHOSE . " Anime List · Dropped" + 'title' => WHOSE . " Anime List · Dropped" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'completed' => [ - 'path' => '/completed', + 'path' => '/completed{/view}', 'controller' => 'AnimeController', 'action' => 'anime_list', 'params' => [ 'type' => 'completed', - 'title' => WHOSE . " Anime List · Completed" + 'title' => WHOSE . " Anime List · Completed" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'collection' => [ - 'path' => '/collection', + 'path' => '/collection{/view}', 'controller' => 'AnimeController', 'action' => 'collection', - 'params' => [] + 'params' => [], + 'tokens' => [ + 'view' => '[a-z_]+' + ] ] ], 'manga' => [ + 'index' => [ + 'path' => '/', + 'controller' => 'MangaController', + 'action' => 'redirect', + 'params' => [ + 'url' => '', // Determined by config + 'code' => '301', + 'type' => 'manga' + ] + ], 'all' => [ - 'path' => '/all', + 'path' => '/all{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'all', 'title' => WHOSE . " Manga List · All" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], - 'index' => [ - 'path' => '/', + 'reading' => [ + 'path' => '/reading{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Reading', 'title' => WHOSE . " Manga List · Reading" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'plan_to_read' => [ - 'path' => '/plan_to_read', + 'path' => '/plan_to_read{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Plan to Read', 'title' => WHOSE . " Manga List · Plan to Read" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'on_hold' => [ - 'path' => '/on_hold', + 'path' => '/on_hold{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'On Hold', 'title' => WHOSE . " Manga List · On Hold" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'dropped' => [ - 'path' => '/dropped', + 'path' => '/dropped{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Dropped', 'title' => WHOSE . " Manga List · Dropped" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], 'completed' => [ - 'path' => '/completed', + 'path' => '/completed{/view}', 'controller' => 'MangaController', 'action' => 'manga_list', 'params' => [ 'type' => 'Completed', 'title' => WHOSE . " Manga List · Completed" + ], + 'tokens' => [ + 'view' => '[a-z_]+' ] ], ] diff --git a/app/controllers/AnimeController.php b/app/controllers/AnimeController.php index 334d9238..7573e279 100644 --- a/app/controllers/AnimeController.php +++ b/app/controllers/AnimeController.php @@ -1,4 +1,7 @@ '/', - 'Plan to Watch' => '/plan_to_watch', - 'On Hold' => '/on_hold', - 'Dropped' => '/dropped', - 'Completed' => '/completed', - 'Collection' => '/collection', - 'All' => '/all' + 'Watching' => '/watching{/view}', + 'Plan to Watch' => '/plan_to_watch{/view}', + 'On Hold' => '/on_hold{/view}', + 'Dropped' => '/dropped{/view}', + 'Completed' => '/completed{/view}', + 'Collection' => '/collection{/view}', + 'All' => '/all{/view}' ]; /** @@ -39,6 +48,12 @@ class AnimeController extends BaseController { parent::__construct(); $this->model = new AnimeModel(); $this->collection_model = new AnimeCollectionModel(); + + $this->base_data = [ + 'url_type' => 'anime', + 'other_type' => 'manga', + 'nav_routes' => $this->nav_routes, + ]; } /** @@ -48,17 +63,21 @@ class AnimeController extends BaseController { * @param string $title - The title of the page * @return void */ - public function anime_list($type, $title) + public function anime_list($type, $title, $view) { + $view_map = [ + '' => 'cover', + 'list' => 'list' + ]; + $data = ($type != 'all') ? $this->model->get_list($type) : $this->model->get_all_lists(); - $this->outputHTML('anime/list', [ + $this->outputHTML('anime/' . $view_map[$view], array_merge($this->base_data, [ 'title' => $title, - 'nav_routes' => $this->nav_routes, 'sections' => $data - ]); + ])); } /** @@ -66,15 +85,19 @@ class AnimeController extends BaseController { * * @return void */ - public function collection() + public function collection($view) { + $view_map = [ + '' => 'collection', + 'list' => 'collection_list' + ]; + $data = $this->collection_model->get_collection(); - $this->outputHTML('anime/collection', [ + $this->outputHTML('anime/' . $view_map[$view], array_merge($this->base_data, [ 'title' => WHOSE . " Anime Collection", - 'nav_routes' => $this->nav_routes, 'sections' => $data - ]); + ])); } } // End of AnimeController.php \ No newline at end of file diff --git a/app/controllers/MangaController.php b/app/controllers/MangaController.php index 991936f2..8039fd07 100644 --- a/app/controllers/MangaController.php +++ b/app/controllers/MangaController.php @@ -1,56 +1,67 @@ - '/', - 'Plan to Read' => '/plan_to_read', - 'On Hold' => '/on_hold', - 'Dropped' => '/dropped', - 'Completed' => '/completed', - 'All' => '/all' - ]; - - /** - * Constructor - */ - public function __construct() - { - parent::__construct(); - $this->model = new MangaModel(); - } - - /** - * Get a section of the manga list - * - * @param string $status - * @param string $title - * @return void - */ - public function manga_list($status, $title) - { - $data = ($status !== 'all') - ? [$status => $this->model->get_list($status)] - : $this->model->get_all_lists(); - - $this->outputHTML('manga/list', [ - 'title' => $title, - 'nav_routes' => $this->nav_routes, - 'sections' => $data - ]); - } -} + '/reading{/view}', + 'Plan to Read' => '/plan_to_read{/view}', + 'On Hold' => '/on_hold{/view}', + 'Dropped' => '/dropped{/view}', + 'Completed' => '/completed{/view}', + 'All' => '/all{/view}' + ]; + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(); + $this->model = new MangaModel(); + } + + /** + * Get a section of the manga list + * + * @param string $status + * @param string $title + * @param string $view + * @return void + */ + public function manga_list($status, $title, $view) + { + $view_map = [ + '' => 'cover', + 'list' => 'list' + ]; + + $data = ($status !== 'all') + ? [$status => $this->model->get_list($status)] + : $this->model->get_all_lists(); + + $this->outputHTML('manga/' . $view_map[$view], [ + 'url_type' => 'manga', + 'other_type' => 'anime', + 'title' => $title, + 'nav_routes' => $this->nav_routes, + 'sections' => $data + ]); + } +} // End of MangaController.php \ No newline at end of file diff --git a/app/models/AnimeCollectionModel.php b/app/models/AnimeCollectionModel.php index 2627044b..061ac8d9 100644 --- a/app/models/AnimeCollectionModel.php +++ b/app/models/AnimeCollectionModel.php @@ -1,4 +1,7 @@ _get_list(); - - foreach ($data as $key => &$val) - { - $this->sort_by_name($val); - } - - return $data; - } - - /** - * Get a category out of the full list - * - * @param string $status - * @return array - */ - public function get_list($status) - { - $data = $this->_get_list($status); - - $this->sort_by_name($data); - - return $data; - } - - /** - * Massage the list of manga entries into something more usable - * - * @param string $status - * @return array - */ - private function _get_list($status="all") - { - global $defaultHandler; - - $cache_file = _dir($this->config->data_cache_path, 'manga.json'); - - $config = [ - 'query' => [ - 'user_id' => $this->config->hummingbird_username - ], - 'allow_redirects' => false - ]; - - $response = $this->client->get($this->_url('/manga_library_entries'), $config); - - $defaultHandler->addDataTable('response', (array)$response); - - if ($response->getStatusCode() != 200) - { - if ( ! file_exists($cache_file)) - { - throw new Exception($response->getEffectiveUrl()); - } - else - { - $raw_data = json_decode(file_get_contents($cache_file), TRUE); - } - } - else - { - // Reorganize data to be more usable - $raw_data = $response->json(); - - // Cache data in case of downtime - file_put_contents($cache_file, json_encode($raw_data)); - } - - $data = [ - 'Reading' => [], - 'Plan to Read' => [], - 'On Hold' => [], - 'Dropped' => [], - 'Completed' => [], - ]; - $manga_data = []; - - // Massage the two lists into one - foreach($raw_data['manga'] as $manga) - { - $manga_data[$manga['id']] = $manga; - } - - // Filter data by status - foreach($raw_data['manga_library_entries'] as &$entry) - { - $entry['manga'] = $manga_data[$entry['manga_id']]; - - // Cache poster images - $entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga_id'], 'manga'); - - switch($entry['status']) - { - case "Plan to Read": - $data['Plan to Read'][] = $entry; - break; - - case "Dropped": - $data['Dropped'][] = $entry; - break; - - case "On Hold": - $data['On Hold'][] = $entry; - break; - - case "Currently Reading": - $data['Reading'][] = $entry; - break; - - case "Completed": - default: - $data['Completed'][] = $entry; - break; - } - } - - return (array_key_exists($status, $data)) ? $data[$status] : $data; - } - - /** - * Sort the manga entries by their title - * - * @param array $array - * @return void - */ - private function sort_by_name(&$array) - { - $sort = array(); - - foreach($array as $key => $item) - { - $sort[$key] = $item['manga']['romaji_title']; - } - - array_multisort($sort, SORT_ASC, $array); - } -} +_get_list(); + + foreach ($data as $key => &$val) + { + $this->sort_by_name($val); + } + + return $data; + } + + /** + * Get a category out of the full list + * + * @param string $status + * @return array + */ + public function get_list($status) + { + $data = $this->_get_list($status); + + $this->sort_by_name($data); + + return $data; + } + + /** + * Massage the list of manga entries into something more usable + * + * @param string $status + * @return array + */ + private function _get_list($status="all") + { + global $defaultHandler; + + $cache_file = _dir($this->config->data_cache_path, 'manga.json'); + + $config = [ + 'query' => [ + 'user_id' => $this->config->hummingbird_username + ], + 'allow_redirects' => false + ]; + + $response = $this->client->get($this->_url('/manga_library_entries'), $config); + + $defaultHandler->addDataTable('response', (array)$response); + + if ($response->getStatusCode() != 200) + { + if ( ! file_exists($cache_file)) + { + throw new Exception($response->getEffectiveUrl()); + } + else + { + $raw_data = json_decode(file_get_contents($cache_file), TRUE); + } + } + else + { + // Reorganize data to be more usable + $raw_data = $response->json(); + + // Cache data in case of downtime + file_put_contents($cache_file, json_encode($raw_data)); + } + + $data = [ + 'Reading' => [], + 'Plan to Read' => [], + 'On Hold' => [], + 'Dropped' => [], + 'Completed' => [], + ]; + $manga_data = []; + + // Massage the two lists into one + foreach($raw_data['manga'] as $manga) + { + $manga_data[$manga['id']] = $manga; + } + + // Filter data by status + foreach($raw_data['manga_library_entries'] as &$entry) + { + $entry['manga'] = $manga_data[$entry['manga_id']]; + + // Cache poster images + $entry['manga']['poster_image'] = $this->get_cached_image($entry['manga']['poster_image'], $entry['manga_id'], 'manga'); + + switch($entry['status']) + { + case "Plan to Read": + $data['Plan to Read'][] = $entry; + break; + + case "Dropped": + $data['Dropped'][] = $entry; + break; + + case "On Hold": + $data['On Hold'][] = $entry; + break; + + case "Currently Reading": + $data['Reading'][] = $entry; + break; + + case "Completed": + default: + $data['Completed'][] = $entry; + break; + } + } + + return (array_key_exists($status, $data)) ? $data[$status] : $data; + } + + /** + * Sort the manga entries by their title + * + * @param array $array + * @return void + */ + private function sort_by_name(&$array) + { + $sort = array(); + + foreach($array as $key => $item) + { + $sort[$key] = $item['manga']['romaji_title']; + } + + array_multisort($sort, SORT_ASC, $array); + } +} // End of MangaModel.php \ No newline at end of file diff --git a/app/views/anime/collection.php b/app/views/anime/collection.php index 9e9cf143..8faec9e6 100644 --- a/app/views/anime/collection.php +++ b/app/views/anime/collection.php @@ -1,32 +1,27 @@ - -

Anime Collection [Manga List]

- -
- $items): ?> -
-

-
- - -
- -
- - ({$item['alternate_title']})" : ""; ?> -
- - -
-
- -
+
+ $items): ?> +
+

+
+ + +
+ +
+ + ({$item['alternate_title']})" : ""; ?> +
+ + +
+
+
- -
- - \ No newline at end of file +
+ +
\ No newline at end of file diff --git a/app/views/anime/collection_list.php b/app/views/anime/collection_list.php new file mode 100644 index 00000000..92471f92 --- /dev/null +++ b/app/views/anime/collection_list.php @@ -0,0 +1,43 @@ +
+ $items): ?> +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TitleAlternate TitleEpisode CountEpisode LengthShow TypeAge RatingNotes
+ + + +
+
+ +
+ + + \ No newline at end of file diff --git a/app/views/anime/cover.php b/app/views/anime/cover.php new file mode 100644 index 00000000..480e6090 --- /dev/null +++ b/app/views/anime/cover.php @@ -0,0 +1,29 @@ +
+ $items): ?> +
+

+
+ + +
+ +
+ + ({$item['anime']['alternate_title']})" : ""; ?> +
+ + +
+
+ +
+
+ +
diff --git a/app/views/anime/list.php b/app/views/anime/list.php index 4dbdf483..1594c85f 100644 --- a/app/views/anime/list.php +++ b/app/views/anime/list.php @@ -1,34 +1,42 @@ - -

Anime List [Manga List]

- -
- $items): ?> -
-

-
- +
+ $items): ?> +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TitleAlternate TitleAiring StatusScoreTypeProgressRated
-
- -
- - ({$item['anime']['alternate_title']})" : ""; ?> -
- - -
+
- - - - - - - \ No newline at end of file +
/ 10 Episodes: /
+ +
+ + + \ No newline at end of file diff --git a/app/views/anime/nav.php b/app/views/anime/nav.php deleted file mode 100644 index 983bbd9e..00000000 --- a/app/views/anime/nav.php +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/app/views/footer.php b/app/views/footer.php new file mode 100644 index 00000000..691287b6 --- /dev/null +++ b/app/views/footer.php @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/views/header.php b/app/views/header.php index c98b9b19..ce5e8e27 100644 --- a/app/views/header.php +++ b/app/views/header.php @@ -1,7 +1,24 @@ - - - - <?= $title ?> - - - \ No newline at end of file + + + + <?= $title ?> + + + + +

["> List]

+ +
+ +
\ No newline at end of file diff --git a/app/views/manga/cover.php b/app/views/manga/cover.php new file mode 100644 index 00000000..2c1465cc --- /dev/null +++ b/app/views/manga/cover.php @@ -0,0 +1,30 @@ +
+ $items): ?> +
+

+
+ + +
+ +
+ + ({$item['manga']['english_title']})" : ""; ?> +
+ + +
+ */ ?> +
+
+ +
+
+ +
\ No newline at end of file diff --git a/app/views/manga/list.php b/app/views/manga/list.php index 47d3c9e3..15c9ebf4 100644 --- a/app/views/manga/list.php +++ b/app/views/manga/list.php @@ -1,35 +1,40 @@ - -

Manga List [">Anime List]

- -
- $items): ?> -
-

-
- - -
- -
- - ({$item['manga']['english_title']})" : ""; ?> -
- - -
- */ ?> -
-
- -
-
- -
- - \ No newline at end of file +
+ $items): ?> +

+ + + + + + + + + + + + + + + + + + + + + + + +
TitleAlternate TitleRatingChaptersType
+ + + + 0) ? (int)($item['rating'] * 2) : '-' ?> / 10 / 0) ? $item['manga']['chapter_count'] : "-" ?>
+ +
+ + + \ No newline at end of file diff --git a/app/views/manga/nav.php b/app/views/manga/nav.php deleted file mode 100644 index fb6d3fcf..00000000 --- a/app/views/manga/nav.php +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/index.php b/index.php index ad693c2e..8e64bd60 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,7 @@ tr:nth-child(odd) { + background: #ddd; +} + +.align_left { + text-align:left; +} + +.align_right { + text-align:right; +} + .round_all { border-radius:0.5em; } diff --git a/public/js/table_sorter/jquery.metadata.js b/public/js/table_sorter/jquery.metadata.js new file mode 100644 index 00000000..6a984dbc --- /dev/null +++ b/public/js/table_sorter/jquery.metadata.js @@ -0,0 +1,122 @@ +/* + * Metadata - jQuery plugin for parsing metadata from elements + * + * Copyright (c) 2006 John Resig, Yehuda Katz, J�örn Zaefferer, Paul McLanahan + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * Revision: $Id$ + * + */ + +/** + * Sets the type of metadata to use. Metadata is encoded in JSON, and each property + * in the JSON will become a property of the element itself. + * + * There are three supported types of metadata storage: + * + * attr: Inside an attribute. The name parameter indicates *which* attribute. + * + * class: Inside the class attribute, wrapped in curly braces: { } + * + * elem: Inside a child element (e.g. a script tag). The + * name parameter indicates *which* element. + * + * The metadata for an element is loaded the first time the element is accessed via jQuery. + * + * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements + * matched by expr, then redefine the metadata type and run another $(expr) for other elements. + * + * @name $.metadata.setType + * + * @example

This is a p

+ * @before $.metadata.setType("class") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from the class attribute + * + * @example

This is a p

+ * @before $.metadata.setType("attr", "data") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a "data" attribute + * + * @example

This is a p

+ * @before $.metadata.setType("elem", "script") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a nested script element + * + * @param String type The encoding type + * @param String name The name of the attribute to be used to get metadata (optional) + * @cat Plugins/Metadata + * @descr Sets the type of encoding to be used when loading metadata for the first time + * @type undefined + * @see metadata() + */ + +(function($) { + +$.extend({ + metadata : { + defaults : { + type: 'class', + name: 'metadata', + cre: /({.*})/, + single: 'metadata' + }, + setType: function( type, name ){ + this.defaults.type = type; + this.defaults.name = name; + }, + get: function( elem, opts ){ + var settings = $.extend({},this.defaults,opts); + // check for empty string in single property + if ( !settings.single.length ) settings.single = 'metadata'; + + var data = $.data(elem, settings.single); + // returned cached data if it already exists + if ( data ) return data; + + data = "{}"; + + if ( settings.type == "class" ) { + var m = settings.cre.exec( elem.className ); + if ( m ) + data = m[1]; + } else if ( settings.type == "elem" ) { + if( !elem.getElementsByTagName ) + return undefined; + var e = elem.getElementsByTagName(settings.name); + if ( e.length ) + data = $.trim(e[0].innerHTML); + } else if ( elem.getAttribute != undefined ) { + var attr = elem.getAttribute( settings.name ); + if ( attr ) + data = attr; + } + + if ( data.indexOf( '{' ) <0 ) + data = "{" + data + "}"; + + data = eval("(" + data + ")"); + + $.data( elem, settings.single, data ); + return data; + } + } +}); + +/** + * Returns the metadata object for the first member of the jQuery object. + * + * @name metadata + * @descr Returns element's metadata object + * @param Object opts An object contianing settings to override the defaults + * @type jQuery + * @cat Plugins/Metadata + */ +$.fn.metadata = function( opts ){ + return $.metadata.get( this[0], opts ); +}; + +})(jQuery); \ No newline at end of file diff --git a/public/js/table_sorter/jquery.tablesorter.js b/public/js/table_sorter/jquery.tablesorter.js new file mode 100644 index 00000000..9b587312 --- /dev/null +++ b/public/js/table_sorter/jquery.tablesorter.js @@ -0,0 +1,1031 @@ +/* + * + * TableSorter 2.0 - Client-side table sorting with ease! + * Version 2.0.5b + * @requires jQuery v1.2.3 + * + * Copyright (c) 2007 Christian Bach + * Examples and docs at: http://tablesorter.com + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ +/** + * + * @description Create a sortable table with multi-column sorting capabilitys + * + * @example $('table').tablesorter(); + * @desc Create a simple tablesorter interface. + * + * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); + * @desc Create a tablesorter interface and sort on the first and secound column column headers. + * + * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); + * + * @desc Create a tablesorter interface and disableing the first and second column headers. + * + * + * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); + * + * @desc Create a tablesorter interface and set a column parser for the first + * and second column. + * + * + * @param Object + * settings An object literal containing key/value pairs to provide + * optional settings. + * + * + * @option String cssHeader (optional) A string of the class name to be appended + * to sortable tr elements in the thead of the table. Default value: + * "header" + * + * @option String cssAsc (optional) A string of the class name to be appended to + * sortable tr elements in the thead on a ascending sort. Default value: + * "headerSortUp" + * + * @option String cssDesc (optional) A string of the class name to be appended + * to sortable tr elements in the thead on a descending sort. Default + * value: "headerSortDown" + * + * @option String sortInitialOrder (optional) A string of the inital sorting + * order can be asc or desc. Default value: "asc" + * + * @option String sortMultisortKey (optional) A string of the multi-column sort + * key. Default value: "shiftKey" + * + * @option String textExtraction (optional) A string of the text-extraction + * method to use. For complex html structures inside td cell set this + * option to "complex", on large tables the complex option can be slow. + * Default value: "simple" + * + * @option Object headers (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortList (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortForce (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * prepended to user-selected rules. Default value: null + * + * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever + * to use String.localeCampare method or not. Default set to true. + * + * + * @option Array sortAppend (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * appended to user-selected rules. Default value: null + * + * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter + * should apply fixed widths to the table columns. This is usefull when + * using the pager companion plugin. This options requires the dimension + * jquery plugin. Default value: false + * + * @option Boolean cancelSelection (optional) Boolean flag indicating if + * tablesorter should cancel selection of the table headers text. + * Default value: true + * + * @option Boolean debug (optional) Boolean flag indicating if tablesorter + * should display debuging information usefull for development. + * + * @type jQuery + * + * @name tablesorter + * + * @cat Plugins/Tablesorter + * + * @author Christian Bach/christian.bach@polyester.se + */ + +(function ($) { + $.extend({ + tablesorter: new + function () { + + var parsers = [], + widgets = []; + + this.defaults = { + cssHeader: "header", + cssAsc: "headerSortUp", + cssDesc: "headerSortDown", + cssChildRow: "expand-child", + sortInitialOrder: "asc", + sortMultiSortKey: "shiftKey", + sortForce: null, + sortAppend: null, + sortLocaleCompare: true, + textExtraction: "simple", + parsers: {}, widgets: [], + widgetZebra: { + css: ["even", "odd"] + }, headers: {}, widthFixed: false, + cancelSelection: true, + sortList: [], + headerList: [], + dateFormat: "us", + decimal: '/\.|\,/g', + onRenderHeader: null, + selectorHeaders: 'thead th', + debug: false + }; + + /* debuging utils */ + + function benchmark(s, d) { + log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); + } + + this.benchmark = benchmark; + + function log(s) { + if (typeof console != "undefined" && typeof console.debug != "undefined") { + console.log(s); + } else { + alert(s); + } + } + + /* parsers utils */ + + function buildParserCache(table, $headers) { + + if (table.config.debug) { + var parsersDebug = ""; + } + + if (table.tBodies.length == 0) return; // In the case of empty tables + var rows = table.tBodies[0].rows; + + if (rows[0]) { + + var list = [], + cells = rows[0].cells, + l = cells.length; + + for (var i = 0; i < l; i++) { + + var p = false; + + if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { + + p = getParserById($($headers[i]).metadata().sorter); + + } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { + + p = getParserById(table.config.headers[i].sorter); + } + if (!p) { + + p = detectParserForColumn(table, rows, -1, i); + } + + if (table.config.debug) { + parsersDebug += "column:" + i + " parser:" + p.id + "\n"; + } + + list.push(p); + } + } + + if (table.config.debug) { + log(parsersDebug); + } + + return list; + }; + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var l = parsers.length, + node = false, + nodeValue = false, + keepLooking = true; + while (nodeValue == '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); + nodeValue = trimAndGetNodeText(table.config, node); + if (table.config.debug) { + log('Checking if value was empty on row:' + rowIndex); + } + } else { + keepLooking = false; + } + } + for (var i = 1; i < l; i++) { + if (parsers[i].is(nodeValue, table, node)) { + return parsers[i]; + } + } + // 0 is always the generic parser (text) + return parsers[0]; + } + + function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { + return rows[rowIndex].cells[cellIndex]; + } + + function trimAndGetNodeText(config, node) { + return $.trim(getElementText(config, node)); + } + + function getParserById(name) { + var l = parsers.length; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == name.toLowerCase()) { + return parsers[i]; + } + } + return false; + } + + /* utils */ + + function buildCache(table) { + + if (table.config.debug) { + var cacheTime = new Date(); + } + + var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, + totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, + parsers = table.config.parsers, + cache = { + row: [], + normalized: [] + }; + + for (var i = 0; i < totalRows; ++i) { + + /** Add the table data to main data array */ + var c = $(table.tBodies[0].rows[i]), + cols = []; + + // if this is a child row, add it to the last row's children and + // continue to the next row + if (c.hasClass(table.config.cssChildRow)) { + cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); + // go to the next for loop + continue; + } + + cache.row.push(c); + + for (var j = 0; j < totalCells; ++j) { + cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); + } + + cols.push(cache.normalized.length); // add position for rowCache + cache.normalized.push(cols); + cols = null; + }; + + if (table.config.debug) { + benchmark("Building cache for " + totalRows + " rows:", cacheTime); + } + + return cache; + }; + + function getElementText(config, node) { + + var text = ""; + + if (!node) return ""; + + if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; + + if (config.textExtraction == "simple") { + if (config.supportsTextContent) { + text = node.textContent; + } else { + if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { + text = node.childNodes[0].innerHTML; + } else { + text = node.innerHTML; + } + } + } else { + if (typeof(config.textExtraction) == "function") { + text = config.textExtraction(node); + } else { + text = $(node).text(); + } + } + return text; + } + + function appendToTable(table, cache) { + + if (table.config.debug) { + var appendTime = new Date() + } + + var c = cache, + r = c.row, + n = c.normalized, + totalRows = n.length, + checkCell = (n[0].length - 1), + tableBody = $(table.tBodies[0]), + rows = []; + + + for (var i = 0; i < totalRows; i++) { + var pos = n[i][checkCell]; + + rows.push(r[pos]); + + if (!table.config.appender) { + + //var o = ; + var l = r[pos].length; + for (var j = 0; j < l; j++) { + tableBody[0].appendChild(r[pos][j]); + } + + // + } + } + + + + if (table.config.appender) { + + table.config.appender(table, rows); + } + + rows = null; + + if (table.config.debug) { + benchmark("Rebuilt table:", appendTime); + } + + // apply table widgets + applyWidget(table); + + // trigger sortend + setTimeout(function () { + $(table).trigger("sortEnd"); + }, 0); + + }; + + function buildHeaders(table) { + + if (table.config.debug) { + var time = new Date(); + } + + var meta = ($.metadata) ? true : false; + + var header_index = computeTableHeaderCellIndexes(table); + + $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { + + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + // this.column = index; + this.order = formatSortingOrder(table.config.sortInitialOrder); + + + this.count = this.order; + + if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; + if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); + + if (!this.sortDisabled) { + var $th = $(this).addClass(table.config.cssHeader); + if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); + } + + // add cell to headerList + table.config.headerList[index] = this; + }); + + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + + return $tableHeaders; + + }; + + // from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + + + function computeTableHeaderCellIndexes(t) { + var matrix = []; + var lookup = {}; + var thead = t.getElementsByTagName('THEAD')[0]; + var trs = thead.getElementsByTagName('TR'); + + for (var i = 0; i < trs.length; i++) { + var cells = trs[i].cells; + for (var j = 0; j < cells.length; j++) { + var c = cells[j]; + + var rowIndex = c.parentNode.rowIndex; + var cellId = rowIndex + "-" + c.cellIndex; + var rowSpan = c.rowSpan || 1; + var colSpan = c.colSpan || 1 + var firstAvailCol; + if (typeof(matrix[rowIndex]) == "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (var k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) == "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + for (var k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) == "undefined") { + matrix[k] = []; + } + var matrixrow = matrix[k]; + for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function checkCellColSpan(table, rows, row) { + var arr = [], + r = table.tHead.rows, + c = r[row].cells; + + for (var i = 0; i < c.length; i++) { + var cell = c[i]; + + if (cell.colSpan > 1) { + arr = arr.concat(checkCellColSpan(table, headerArr, row++)); + } else { + if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { + arr.push(cell); + } + // headerArr[row] = (i+row); + } + } + return arr; + }; + + function checkHeaderMetadata(cell) { + if (($.metadata) && ($(cell).metadata().sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptions(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptionsSortingLocked(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; + return false; + } + + function applyWidget(table) { + var c = table.config.widgets; + var l = c.length; + for (var i = 0; i < l; i++) { + + getWidgetById(c[i]).format(table); + } + + } + + function getWidgetById(name) { + var l = widgets.length; + for (var i = 0; i < l; i++) { + if (widgets[i].id.toLowerCase() == name.toLowerCase()) { + return widgets[i]; + } + } + }; + + function formatSortingOrder(v) { + if (typeof(v) != "Number") { + return (v.toLowerCase() == "desc") ? 1 : 0; + } else { + return (v == 1) ? 1 : 0; + } + } + + function isValueInArray(v, a) { + var l = a.length; + for (var i = 0; i < l; i++) { + if (a[i][0] == v) { + return true; + } + } + return false; + } + + function setHeadersCss(table, $headers, list, css) { + // remove all header information + $headers.removeClass(css[0]).removeClass(css[1]); + + var h = []; + $headers.each(function (offset) { + if (!this.sortDisabled) { + h[this.column] = $(this); + } + }); + + var l = list.length; + for (var i = 0; i < l; i++) { + h[list[i][0]].addClass(css[list[i][1]]); + } + } + + function fixColumnWidth(table, $headers) { + var c = table.config; + if (c.widthFixed) { + var colgroup = $(''); + $("tr:first td", table.tBodies[0]).each(function () { + colgroup.append($('').css('width', $(this).width())); + }); + $(table).prepend(colgroup); + }; + } + + function updateHeaderSortCount(table, sortList) { + var c = table.config, + l = sortList.length; + for (var i = 0; i < l; i++) { + var s = sortList[i], + o = c.headerList[s[0]]; + o.count = s[1]; + o.count++; + } + } + + /* sorting methods */ + + function multisort(table, sortList, cache) { + + if (table.config.debug) { + var sortTime = new Date(); + } + + var dynamicExp = "var sortWrapper = function(a,b) {", + l = sortList.length; + + // TODO: inline functions. + for (var i = 0; i < l; i++) { + + var c = sortList[i][0]; + var order = sortList[i][1]; + // var s = (getCachedSortType(table.config.parsers,c) == "text") ? + // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? + // "sortNumeric" : "sortNumericDesc"); + // var s = (table.config.parsers[c].type == "text") ? ((order == 0) + // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? + // makeSortNumeric(c) : makeSortNumericDesc(c)); + var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); + var e = "e" + i; + + dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c + // + "]); "; + dynamicExp += "if(" + e + ") { return " + e + "; } "; + dynamicExp += "else { "; + + } + + // if value is the same keep orignal order + var orgOrderCol = cache.normalized[0].length - 1; + dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; + + for (var i = 0; i < l; i++) { + dynamicExp += "}; "; + } + + dynamicExp += "return 0; "; + dynamicExp += "}; "; + + if (table.config.debug) { + benchmark("Evaling expression:" + dynamicExp, new Date()); + } + + eval(dynamicExp); + + cache.normalized.sort(sortWrapper); + + if (table.config.debug) { + benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); + } + + return cache; + }; + + function makeSortFunction(type, direction, index) { + var a = "a[" + index + "]", + b = "b[" + index + "]"; + if (type == 'text' && direction == 'asc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; + } else if (type == 'text' && direction == 'desc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; + } else if (type == 'numeric' && direction == 'asc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; + } else if (type == 'numeric' && direction == 'desc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; + } + }; + + function makeSortText(i) { + return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; + }; + + function makeSortTextDesc(i) { + return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; + }; + + function makeSortNumeric(i) { + return "a[" + i + "]-b[" + i + "];"; + }; + + function makeSortNumericDesc(i) { + return "b[" + i + "]-a[" + i + "];"; + }; + + function sortText(a, b) { + if (table.config.sortLocaleCompare) return a.localeCompare(b); + return ((a < b) ? -1 : ((a > b) ? 1 : 0)); + }; + + function sortTextDesc(a, b) { + if (table.config.sortLocaleCompare) return b.localeCompare(a); + return ((b < a) ? -1 : ((b > a) ? 1 : 0)); + }; + + function sortNumeric(a, b) { + return a - b; + }; + + function sortNumericDesc(a, b) { + return b - a; + }; + + function getCachedSortType(parsers, i) { + return parsers[i].type; + }; /* public methods */ + this.construct = function (settings) { + return this.each(function () { + // if no thead or tbody quit. + if (!this.tHead || !this.tBodies) return; + // declare + var $this, $document, $headers, cache, config, shiftDown = 0, + sortOrder; + // new blank config object + this.config = {}; + // merge and extend. + config = $.extend(this.config, $.tablesorter.defaults, settings); + // store common expression for speed + $this = $(this); + // save the settings where they read + $.data(this, "tablesorter", config); + // build headers + $headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + this.config.parsers = buildParserCache(this, $headers); + // build the cache for the tbody cells + cache = buildCache(this); + // get the css class names, could be done else where. + var sortCSS = [config.cssDesc, config.cssAsc]; + // fixate columns if the users supplies the fixedWidth option + fixColumnWidth(this); + // apply event handling to headers + // this is to big, perhaps break it out? + $headers.click( + + function (e) { + var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; + if (!this.sortDisabled && totalRows > 0) { + // Only call sortStart if sorting is + // enabled. + $this.trigger("sortStart"); + // store exp, for speed + var $cell = $(this); + // get current column index + var i = this.column; + // get current column sort order + this.order = this.count++ % 2; + // always sort on the locked order. + if(this.lockedOrder) this.order = this.lockedOrder; + + // user only whants to sort on one + // column + if (!e[config.sortMultiSortKey]) { + // flush the sort list + config.sortList = []; + if (config.sortForce != null) { + var a = config.sortForce; + for (var j = 0; j < a.length; j++) { + if (a[j][0] != i) { + config.sortList.push(a[j]); + } + } + } + // add column to sort list + config.sortList.push([i, this.order]); + // multi column sorting + } else { + // the user has clicked on an all + // ready sortet column. + if (isValueInArray(i, config.sortList)) { + // revers the sorting direction + // for all tables. + for (var j = 0; j < config.sortList.length; j++) { + var s = config.sortList[j], + o = config.headerList[s[0]]; + if (s[0] == i) { + o.count = s[1]; + o.count++; + s[1] = o.count % 2; + } + } + } else { + // add column to sort list array + config.sortList.push([i, this.order]); + } + }; + setTimeout(function () { + // set css for headers + setHeadersCss($this[0], $headers, config.sortList, sortCSS); + appendToTable( + $this[0], multisort( + $this[0], config.sortList, cache) + ); + }, 1); + // stop normal event by returning false + return false; + } + // cancel selection + }).mousedown(function () { + if (config.cancelSelection) { + this.onselectstart = function () { + return false + }; + return false; + } + }); + // apply easy methods that trigger binded events + $this.bind("update", function () { + var me = this; + setTimeout(function () { + // rebuild parsers. + me.config.parsers = buildParserCache( + me, $headers); + // rebuild the cache map + cache = buildCache(me); + }, 1); + }).bind("updateCell", function (e, cell) { + var config = this.config; + // get position from the dom. + var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; + // update cache + cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( + getElementText(config, cell), cell); + }).bind("sorton", function (e, list) { + $(this).trigger("sortStart"); + config.sortList = list; + // update and store the sortlist + var sortList = config.sortList; + // update header count index + updateHeaderSortCount(this, sortList); + // set css for headers + setHeadersCss(this, $headers, sortList, sortCSS); + // sort the table and append it to the dom + appendToTable(this, multisort(this, sortList, cache)); + }).bind("appendCache", function () { + appendToTable(this, cache); + }).bind("applyWidgetId", function (e, id) { + getWidgetById(id).format(this); + }).bind("applyWidgets", function () { + // apply widgets + applyWidget(this); + }); + if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { + config.sortList = $(this).metadata().sortlist; + } + // if user has supplied a sort list to constructor. + if (config.sortList.length > 0) { + $this.trigger("sorton", [config.sortList]); + } + // apply widgets + applyWidget(this); + }); + }; + this.addParser = function (parser) { + var l = parsers.length, + a = true; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + parsers.push(parser); + }; + }; + this.addWidget = function (widget) { + widgets.push(widget); + }; + this.formatFloat = function (s) { + var i = parseFloat(s); + return (isNaN(i)) ? 0 : i; + }; + this.formatInt = function (s) { + var i = parseInt(s); + return (isNaN(i)) ? 0 : i; + }; + this.isDigit = function (s, config) { + // replace all an wanted chars and match. + return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); + }; + this.clearTableBody = function (table) { + if ($.browser.msie) { + function empty() { + while (this.firstChild) + this.removeChild(this.firstChild); + } + empty.apply(table.tBodies[0]); + } else { + table.tBodies[0].innerHTML = ""; + } + }; + } + }); + + // extend plugin scope + $.fn.extend({ + tablesorter: $.tablesorter.construct + }); + + // make shortcut + var ts = $.tablesorter; + + // add default parsers + ts.addParser({ + id: "text", + is: function (s) { + return true; + }, format: function (s) { + return $.trim(s.toLocaleLowerCase()); + }, type: "text" + }); + + ts.addParser({ + id: "digit", + is: function (s, table) { + var c = table.config; + return $.tablesorter.isDigit(s, c); + }, format: function (s) { + return $.tablesorter.formatFloat(s); + }, type: "numeric" + }); + + ts.addParser({ + id: "currency", + is: function (s) { + return /^[£$€?.]/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function (s) { + return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); + }, format: function (s) { + var a = s.split("."), + r = "", + l = a.length; + for (var i = 0; i < l; i++) { + var item = a[i]; + if (item.length == 2) { + r += "0" + item; + } else { + r += item; + } + } + return $.tablesorter.formatFloat(r); + }, type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function (s) { + return /^(https?|ftp|file):\/\/$/.test(s); + }, format: function (s) { + return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); + }, type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function (s) { + return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( + new RegExp(/-/g), "/")).getTime() : "0"); + }, type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function (s) { + return /\%$/.test($.trim(s)); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function (s) { + return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + + ts.addParser({ + id: "shortDate", + is: function (s) { + return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); + }, format: function (s, table) { + var c = table.config; + s = s.replace(/\-/g, "/"); + if (c.dateFormat == "us") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); + } else if (c.dateFormat == "uk") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); + } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { + s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); + } + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "time", + is: function (s) { + return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "metadata", + is: function (s) { + return false; + }, format: function (s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, type: "numeric" + }); + // add default widgets + ts.addWidget({ + id: "zebra", + format: function (table) { + if (table.config.debug) { + var time = new Date(); + } + var $tr, row = -1, + odd; + // loop through the visible rows + $("tr:visible", table.tBodies[0]).each(function (i) { + $tr = $(this); + // style children rows the same way the parent + // row was styled + if (!$tr.hasClass(table.config.cssChildRow)) row++; + odd = (row % 2 == 0); + $tr.removeClass( + table.config.widgetZebra.css[odd ? 0 : 1]).addClass( + table.config.widgetZebra.css[odd ? 1 : 0]) + }); + if (table.config.debug) { + $.tablesorter.benchmark("Applying Zebra widget", time); + } + } + }); +})(jQuery); \ No newline at end of file diff --git a/public/js/table_sorter/jquery.tablesorter.min.js b/public/js/table_sorter/jquery.tablesorter.min.js new file mode 100644 index 00000000..b8605df1 --- /dev/null +++ b/public/js/table_sorter/jquery.tablesorter.min.js @@ -0,0 +1,4 @@ + +(function($){$.extend({tablesorter:new +function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((ab)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i