diff --git a/application/controllers/FilterController.php b/application/controllers/FilterController.php
index 213e8eea2..00e598ea9 100644
--- a/application/controllers/FilterController.php
+++ b/application/controllers/FilterController.php
@@ -33,9 +33,6 @@ use Icinga\Filter\Filter;
use Icinga\Filter\FilterAttribute;
use Icinga\Filter\Type\TextFilter;
use Icinga\Application\Logger;
-use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
-use Icinga\Module\Monitoring\Filter\UrlViewFilter;
-use Icinga\Module\Monitoring\DataView\HostStatus;
use Icinga\Web\Url;
/**
@@ -50,6 +47,8 @@ class FilterController extends ActionController
*/
private $registry;
+ private $moduleRegistry;
+
/**
* Entry point for filtering, uses the filter_domain and filter_module request parameter
* to determine which filter registry should be used
@@ -57,19 +56,27 @@ class FilterController extends ActionController
public function indexAction()
{
$this->registry = new Filter();
+ $query = $this->getRequest()->getParam('query', '');
+ $target = $this->getRequest()->getParam('filter_domain', '');
if ($this->getRequest()->getHeader('accept') == 'application/json') {
$this->getResponse()->setHeader('Content-Type', 'application/json');
-
$this->setupQueries(
- $this->getParam('filter_domain', ''),
+ $target,
$this->getParam('filter_module', '')
);
-
- $this->_helper->json($this->parse($this->getRequest()->getParam('query', '')));
+ $this->_helper->json($this->parse($query, $target));
} else {
- $this->redirect('index/welcome');
+ $this->setupQueries(
+ $target,
+ $this->getParam('filter_module')
+ );
+ $urlTarget = $this->parse($query, $target);
+ die(print_r($urlTarget,true));
+ $this->redirect($urlTarget['urlParam']);
}
+
+
}
/**
@@ -82,6 +89,7 @@ class FilterController extends ActionController
{
$class = '\\Icinga\\Module\\' . ucfirst($module) . '\\Filter\\Registry';
$factory = strtolower($domain) . 'Filter';
+ $this->moduleRegistry = $class;
$this->registry->addDomain($class::$factory());
}
@@ -91,17 +99,16 @@ class FilterController extends ActionController
* @param String $text The query to parse
* @return array The result structure to be returned in json format
*/
- private function parse($text)
+ private function parse($text, $target)
{
try {
- $view = HostStatus::fromRequest($this->getRequest());
- $urlParser = new UrlViewFilter($view);
- $queryTree = $this->registry->createQueryTreeForFilter($text);
+ $queryTree = $this->registry->createQueryTreeForFilter($text);
+ $registry = $this->moduleRegistry;
return array(
'state' => 'success',
'proposals' => $this->registry->getProposalsForQuery($text),
- 'urlParam' => $urlParser->fromTree($queryTree)
+ 'urlParam' => $registry::getUrlForTarget($target, $queryTree)
);
} catch (\Exception $exc) {
Logger::error($exc);
diff --git a/application/views/helpers/MainDetail.php b/application/views/helpers/MainDetail.php
index bba30ad7a..e229c41a4 100644
--- a/application/views/helpers/MainDetail.php
+++ b/application/views/helpers/MainDetail.php
@@ -31,6 +31,8 @@
/**
* Helper to render main and detail contents into a container
*/
+use Icinga\Application\Icinga;
+
class Zend_View_Helper_MainDetail extends Zend_View_Helper_Abstract
{
/**
diff --git a/application/views/scripts/config/module/overview.phtml b/application/views/scripts/config/module/overview.phtml
index 652967b8d..9eaa5357a 100644
--- a/application/views/scripts/config/module/overview.phtml
+++ b/application/views/scripts/config/module/overview.phtml
@@ -31,10 +31,10 @@ $modules = $this->modules->paginate();
if ($module->enabled): ?>
{{OK_ICON}}
- = $this->escape($module->name); ?>
+ = $this->escape($module->name); ?>
else: ?>
{{REMOVE_ICON}}
- = $this->escape($module->name); ?>
+ = $this->escape($module->name); ?>
endif ?>
(=
$module->enabled ? ($module->loaded ? 'enabled' : 'failed') : 'disabled' ?>)
diff --git a/library/Icinga/Data/DataArray/Datasource.php b/library/Icinga/Data/DataArray/Datasource.php
index 2f59cdff6..e511c623b 100644
--- a/library/Icinga/Data/DataArray/Datasource.php
+++ b/library/Icinga/Data/DataArray/Datasource.php
@@ -72,16 +72,10 @@ class Datasource implements DatasourceInterface
return $this;
}
$result = array();
- $filters = $query->listFilters();
+
$columns = $query->getColumns();
foreach ($this->data as & $row) {
- // Skip rows that do not match applied filters
- foreach ($filters as $f) {
- if ($row->{$f[0]} !== $f[1]) {
- continue 2;
- }
- }
// Get only desired columns if asked so
if (empty($columns)) {
diff --git a/library/Icinga/Filter/Filterable.php b/library/Icinga/Filter/Filterable.php
index d95155af1..d54c77cef 100644
--- a/library/Icinga/Filter/Filterable.php
+++ b/library/Icinga/Filter/Filterable.php
@@ -29,11 +29,41 @@
namespace Icinga\Filter;
+/**
+ * Interface for filterable data sources
+ */
interface Filterable
{
+ /**
+ * Return true when this field is filterable, otherwise false
+ *
+ * @param string $field The field to test for being filterable
+ * @return boolean True when this field is filterable, otherwise false
+ */
public function isValidFilterTarget($field);
+
+ /**
+ * Return the internal, resolved name of the given field
+ *
+ * @param string $field The field to resolve
+ * @return string The resolved name or null if the field is not resolvable
+ */
public function getMappedField($field);
+
+ /**
+ * Apply all filters of this filterable on the datasource
+ */
public function applyFilter();
+
+ /**
+ * Remove all filters from this datasource
+ */
public function clearFilter();
+
+ /**
+ * Add a filter to this datasource
+ *
+ * @param mixed $filter The filter to use
+ */
public function addFilter($filter);
}
diff --git a/library/Icinga/Filter/Registry.php b/library/Icinga/Filter/Registry.php
new file mode 100644
index 000000000..83b8a726c
--- /dev/null
+++ b/library/Icinga/Filter/Registry.php
@@ -0,0 +1,43 @@
+
+ * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
+ * @author Icinga Development Team
+ */
+// {{{ICINGA_LICENSE_HEADER}}}
+
+
+namespace Icinga\Filter;
+
+
+use Icinga\Filter\Query\Tree;
+
+/**
+ * Interface for filter registries
+ * Class Registry
+ * @package Icinga\Filter
+ */
+interface Registry
+{
+ public static function getUrlForTarget($domain, Tree $filter);
+}
\ No newline at end of file
diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php
index 5974dd958..e5ee5a5b3 100755
--- a/library/Icinga/Web/Controller/ActionController.php
+++ b/library/Icinga/Web/Controller/ActionController.php
@@ -106,23 +106,6 @@ class ActionController extends Zend_Controller_Action
}
}
- private function dispatchDetailView($url)
- {
- // strip the base URL from the detail $url
- $url = substr($url, strlen($this->getRequest()->getBaseUrl()));
- // the host is mandatory, but ignored in Zend
- $req = new Request('http://ignoredhost/' . $url);
- $req->setUser($this->getRequest()->getUser());
- $req->setBaseUrl($this->getRequest()->getBaseUrl());
- $router = Zend_Controller_Front::getInstance()->getRouter();
- $router->route($req);
- Zend_Controller_Front::getInstance()->setRequest($req);
- $detailHtml = $this->view->action($req->getActionName(), $req->getControllerName(), $req->getModuleName());
- Zend_Controller_Front::getInstance()->setRequest($this->getRequest());
- $this->_helper->layout->assign('detailContent', $detailHtml);
- $this->_helper->layout->assign('detailClass', 'col-sm-12 col-xs-12 col-md-12 col-lg-6');
- $this->_helper->layout->assign('mainClass', 'col-sm-12 col-xs-12 col-md-12 col-lg-6');
- }
/**
* Check whether the controller requires a login. That is when the controller requires authentication and the
@@ -223,30 +206,9 @@ class ActionController extends Zend_Controller_Action
Benchmark::measure('Action::postDispatch()');
if ($this->_request->isXmlHttpRequest()) {
- $this->_helper->layout()->setLayout('body');
+ $target = ($this->getParam('render') === 'detail') ? 'inline' : 'body';
+ $this->_helper->layout()->setLayout($target);
}
-
- if ($this->getParam('detail', false)) {
- $detail = $this->getParam('detail');
-
- // Zend uses the GET variables when calling getParam, therefore we have to persist the params,
- // clear the $_GET array, call the detail view with the url set in $detail and afterwards recreate
- // the $_GET array. If this is not done the following issues occur:
- //
- // - A stackoverflow issue due to infinite nested calls of buildDetailView (as the detailview has the same
- // postDispatch method) when 'detail' is not set to null
- //
- // - Params (like filters in the URL) from the detail view would be applied on all links of the master view
- // as those would be in the $_GET array after building the detail view. E.g. if you have a grid in the
- // master and a detail view filtering showing one host in detail, the pagination links of the grid would
- // contain the host filter of the detail view
- //
- $params = $_GET;
- $_GET['detail'] = null;
- $this->dispatchDetailView($detail);
- $_GET = $params;
- }
-
}
/**
diff --git a/library/Icinga/Web/Request.php b/library/Icinga/Web/Request.php
index ca620916d..1ee620514 100644
--- a/library/Icinga/Web/Request.php
+++ b/library/Icinga/Web/Request.php
@@ -62,4 +62,6 @@ class Request extends Zend_Controller_Request_Http
{
return $this->user;
}
+
+
}
diff --git a/library/Icinga/Web/Widget/FilterBadgeRenderer.php b/library/Icinga/Web/Widget/FilterBadgeRenderer.php
index d5bde0006..aa274f89f 100644
--- a/library/Icinga/Web/Widget/FilterBadgeRenderer.php
+++ b/library/Icinga/Web/Widget/FilterBadgeRenderer.php
@@ -75,7 +75,7 @@ class FilterBadgeRenderer implements Widget
$url = $this->urlFilter->fromTree($newTree);
$url = $basePath . (empty($allParams) ? '?' : '&') . $url;
- return ' '
+ return ' '
. $this->conjunctionCellar . ' '
. ucfirst($node->left) . ' '
. $node->operator . ' '
diff --git a/library/Icinga/Web/Widget/FilterBox.php b/library/Icinga/Web/Widget/FilterBox.php
index 784b987f0..1d5b503c0 100644
--- a/library/Icinga/Web/Widget/FilterBox.php
+++ b/library/Icinga/Web/Widget/FilterBox.php
@@ -99,10 +99,10 @@ EOT;
$form->setTokenDisabled();
$form->addElement(
'text',
- 'filter',
+ 'query',
array(
'label' => 'Filter Results',
- 'name' => 'filter',
+ 'name' => 'query',
'data-icinga-component' => 'app/semanticsearch',
'data-icinga-filter-domain' => $this->domain,
'data-icinga-filter-module' => $this->module
@@ -111,7 +111,6 @@ EOT;
$form->removeAttrib('data-icinga-component');
$form->setIgnoreChangeDiscarding(true);
-
$badges = new FilterBadgeRenderer($this->initialFilter);
$html = str_replace('{{FORM}}', $form->render($view), self::$TPL);
return str_replace('{{BADGES}}', $badges->render($view), $html);
diff --git a/library/Icinga/Web/Widget/SortBox.php b/library/Icinga/Web/Widget/SortBox.php
index 2b2c9051a..63fd1244b 100644
--- a/library/Icinga/Web/Widget/SortBox.php
+++ b/library/Icinga/Web/Widget/SortBox.php
@@ -34,6 +34,7 @@ use Zend_View_Abstract;
use Icinga\Web\Form\Decorator\ConditionalHidden;
use Zend_Form_Element_Submit;
+
/**
* Sortbox widget
*
@@ -77,7 +78,7 @@ class SortBox implements Widget
/**
* A request object used for initial form population
*
- * @var Icinga\Web\Request
+ * @var \Icinga\Web\Request
*/
private $request;
@@ -166,6 +167,7 @@ class SortBox implements Widget
$form->addElement($this->createFallbackSubmitButton());
if ($this->request) {
+ $form->setAction($this->request->getRequestUri());
$form->populate($this->request->getParams());
}
return $form->render($view);
diff --git a/library/Icinga/Web/Widget/Tab.php b/library/Icinga/Web/Widget/Tab.php
index ae8b13355..424076824 100644
--- a/library/Icinga/Web/Widget/Tab.php
+++ b/library/Icinga/Web/Widget/Tab.php
@@ -221,7 +221,7 @@ class Tab implements Widget
*/
public function render(Zend_View_Abstract $view)
{
- $class = $this->active ? ' class="active"' : '';
+ $class = $this->active ? ' class="active" ' : '';
$caption = $this->title;
if ($this->icon !== null) {
@@ -238,7 +238,7 @@ class Tab implements Widget
$tagParams .= ' ' . $key . '="' . $value . '"';
}
}
- $tab = '' . $caption . '';
+ $tab = '' . $caption . '';
} else {
$tab = $caption;
}
diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php
index e8c6d54fc..497296f54 100644
--- a/modules/monitoring/application/controllers/ListController.php
+++ b/modules/monitoring/application/controllers/ListController.php
@@ -52,6 +52,7 @@ use Icinga\Module\Monitoring\DataView\Comment as CommentView;
use Icinga\Module\Monitoring\DataView\Groupsummary as GroupsummaryView;
use Icinga\Module\Monitoring\DataView\EventHistory as EventHistoryView;
use Icinga\Module\Monitoring\Filter\UrlViewFilter;
+use Icinga\Module\Monitoring\DataView\ServiceStatus;
use Icinga\Filter\Filterable;
class Monitoring_ListController extends MonitoringController
@@ -131,7 +132,8 @@ class Monitoring_ListController extends MonitoringController
)
);
$query = $dataview->getQuery();
- $this->setupFilterControl($dataview);
+ $this->setupFilterControl($dataview, 'host');
+
$this->setupSortControl(array(
'host_last_check' => 'Last Host Check',
'host_severity' => 'Host Severity',
@@ -152,6 +154,8 @@ class Monitoring_ListController extends MonitoringController
{
$this->compactView = 'services-compact';
$this->view->services = $this->fetchServices();
+
+ $this->setupFilterControl(ServiceStatus::fromRequest($this->getRequest()), 'service');
$this->setupSortControl(array(
'service_last_check' => 'Last Service Check',
'service_severity' => 'Severity',
@@ -437,12 +441,12 @@ class Monitoring_ListController extends MonitoringController
$this->view->sortControl->applyRequest($this->getRequest());
}
- private function setupFilterControl(Filterable $dataview)
+ private function setupFilterControl(Filterable $dataview, $domain)
{
$parser = new UrlViewFilter($dataview);
$this->view->filterBox = new FilterBox(
- $parser->parseUrl(),
- 'host',
+ $parser->fromRequest($this->getRequest()),
+ $domain,
'monitoring'
);
diff --git a/modules/monitoring/application/controllers/ShowController.php b/modules/monitoring/application/controllers/ShowController.php
index 1742b5648..977aaa1b2 100644
--- a/modules/monitoring/application/controllers/ShowController.php
+++ b/modules/monitoring/application/controllers/ShowController.php
@@ -55,7 +55,16 @@ class Monitoring_ShowController extends MonitoringController
*/
public function init()
{
- $this->view->object = AbstractObject::fromRequest($this->getRequest());
+
+ if ($this->getRequest()->getActionName() === 'host') {
+ $this->view->object = new Host($this->getRequest());
+ } elseif ($this->getRequest()->getActionName() === 'service') {
+ $this->view->object = new Service($this->getRequest());
+
+ } else {
+ $this->view->object = AbstractObject::fromRequest($this->getRequest());
+ }
+
$this->createTabs();
}
diff --git a/modules/monitoring/application/views/scripts/list/services.phtml b/modules/monitoring/application/views/scripts/list/services.phtml
index 7c7e828aa..18b171658 100644
--- a/modules/monitoring/application/views/scripts/list/services.phtml
+++ b/modules/monitoring/application/views/scripts/list/services.phtml
@@ -5,8 +5,19 @@ $viewHelper = $this->getHelper('MonitoringState');
= $this->tabs->render($this); ?>
Services Status
- = $this->sortControl->render($this); ?>
- = $this->paginationControl($services, null, null, array('preserve' => $this->preserve)); ?>
+
+
+
+ = $this->filterBox->render($this); ?>
+
+
+ = $this->sortControl->render($this); ?>
+
+
+
+ = $this->paginationControl($services, null, null, array('preserve' => $this->preserve)); ?>
+
+
diff --git a/modules/monitoring/library/Monitoring/Controller.php b/modules/monitoring/library/Monitoring/Controller.php
index 79ef9fac2..c4d6d152c 100644
--- a/modules/monitoring/library/Monitoring/Controller.php
+++ b/modules/monitoring/library/Monitoring/Controller.php
@@ -13,6 +13,7 @@ use Icinga\Web\Controller\ActionController;
*/
class Controller extends ActionController
{
+
/**
* Retrieve services from either given parameters or request
*
diff --git a/modules/monitoring/library/Monitoring/DataView/DataView.php b/modules/monitoring/library/Monitoring/DataView/DataView.php
index 1a5d805c6..aad28053b 100644
--- a/modules/monitoring/library/Monitoring/DataView/DataView.php
+++ b/modules/monitoring/library/Monitoring/DataView/DataView.php
@@ -74,7 +74,7 @@ abstract class DataView implements Filterable
$view = new static(Backend::createBackend($request->getParam('backend')), $columns);
$parser = new UrlViewFilter($view);
- $view->getQuery()->setFilter($parser->parseUrl());
+ $view->getQuery()->setFilter($parser->fromRequest($request));
$order = $request->getParam('dir');
if ($order !== null) {
@@ -102,8 +102,11 @@ abstract class DataView implements Filterable
public static function fromParams(array $params, array $columns = null)
{
$view = new static(Backend::createBackend($params['backend']), $columns);
+
foreach ($params as $key => $value) {
- $view->getQuery()->where($key, $value);
+ if ($view->isValidFilterTarget($key)) {
+ $view->getQuery()->where($key, $value);
+ }
}
$order = isset($params['order']) ? $params['order'] : null;
if ($order !== null) {
diff --git a/modules/monitoring/library/Monitoring/Filter/Registry.php b/modules/monitoring/library/Monitoring/Filter/Registry.php
index 08f54b783..3f8f7a232 100644
--- a/modules/monitoring/library/Monitoring/Filter/Registry.php
+++ b/modules/monitoring/library/Monitoring/Filter/Registry.php
@@ -28,20 +28,35 @@
namespace Icinga\Module\Monitoring\Filter;
+use Icinga\Application\Icinga;
+use Icinga\Application\Logger;
use Icinga\Filter\Domain;
use Icinga\Filter\FilterAttribute;
use Icinga\Filter\Query\Node;
+use Icinga\Filter\Query\Tree;
use Icinga\Filter\Type\BooleanFilter;
use Icinga\Filter\Type\TextFilter;
use Icinga\Filter\Type\TimeRangeSpecifier;
+use Icinga\Module\Monitoring\DataView\HostStatus;
+use Icinga\Module\Monitoring\DataView\ServiceStatus;
use Icinga\Module\Monitoring\Filter\Type\StatusFilter;
+use Icinga\Filter\Registry as FilterRegistry;
+use Icinga\Module\Monitoring\Object\Host;
+use Icinga\Web\Request;
+use Zend_Controller_Request_Exception;
+use Icinga\Web\Url;
/**
* Factory class to create filter for different monitoring objects
*
*/
-class Registry
+class Registry implements FilterRegistry
{
+ /**
+ * Return a TimeRangeSpecifier for the 'Next Check' query
+ *
+ * @return TimeRangeSpecifier
+ */
public static function getNextCheckFilterType()
{
$type = new TimeRangeSpecifier();
@@ -54,6 +69,11 @@ class Registry
return $type;
}
+ /**
+ * Return a TimeRangeSpecifier for the 'Last Check' query
+ *
+ * @return TimeRangeSpecifier
+ */
public static function getLastCheckFilterType()
{
$type = new TimeRangeSpecifier();
@@ -68,6 +88,11 @@ class Registry
return $type;
}
+ /**
+ * Registry function for the host domain
+ *
+ * @return Domain the domain to use in the filter registry
+ */
public static function hostFilter()
{
$domain = new Domain('Host');
@@ -78,7 +103,6 @@ class Registry
->setField('host_name')
)->registerAttribute(
FilterAttribute::create(StatusFilter::createForHost())
- ->setHandledAttributes('State', 'Status', 'Current Status')
->setField('host_state')
)->registerAttribute(
FilterAttribute::create(new BooleanFilter(
@@ -103,4 +127,104 @@ class Registry
);
return $domain;
}
+
+ /**
+ * Registry function for the service domain
+ *
+ * @return Domain the domain to use in the filter registry
+ */
+ public static function serviceFilter()
+ {
+ $domain = new Domain('Service');
+
+ $domain->registerAttribute(
+ FilterAttribute::create(new TextFilter())
+ ->setHandledAttributes('Name', 'Servicename')
+ ->setField('service_name')
+ )->registerAttribute(
+ FilterAttribute::create(StatusFilter::createForService())
+ ->setField('service_state')
+ )->registerAttribute(
+ FilterAttribute::create(StatusFilter::createForHost())
+ ->setHandledAttributes('Host')
+ ->setField('host_state')
+ )->registerAttribute(
+ FilterAttribute::create(new BooleanFilter(
+ array(
+ 'service_is_flapping' => 'Flapping',
+ 'service_problem' => 'In Problem State',
+ 'service_notifications_enabled' => 'Sending Notifications',
+ 'service_active_checks_enabled' => 'Active',
+ 'service_passive_checks_enabled' => 'Accepting Passive Checks',
+ 'service_handled' => 'Handled',
+ 'service_in_downtime' => 'In Downtime',
+ 'host_in_downtime' => 'In Host Downtime'
+ )
+ ))
+ )->registerAttribute(
+ FilterAttribute::create(self::getLastCheckFilterType())
+ ->setHandledAttributes('Last Check', 'Check')
+ ->setField('service_last_check')
+ )->registerAttribute(
+ FilterAttribute::create(self::getNextCheckFilterType())
+ ->setHandledAttributes('Next Check')
+ ->setField('service_next_check')
+ );
+ return $domain;
+ }
+
+ /**
+ * Resolve the given filter to an url, using the referer as the base url and base filter
+ *
+ * @param $domain The domain to filter for
+ * @param Tree $filter The tree representing the fiter
+ *
+ * @return string An url
+ * @throws Zend_Controller_Request_Exception Called if no referer is available
+ */
+ public static function getUrlForTarget($domain, Tree $filter)
+ {
+ if (!isset($_SERVER['HTTP_REFERER'])) {
+ throw new Zend_Controller_Request_Exception('You can\'t use this method without an referer');
+ }
+ $request = Icinga::app()->getFrontController()->getRequest();
+ switch ($domain) {
+ case 'host':
+ $view = HostStatus::fromRequest($request);
+ break;
+ case 'service':
+ $view = ServiceStatus::fromRequest($request);
+ break;
+ default:
+ Logger::error('Invalid filter domain requested : %s', $domain);
+ throw new Exception('Unknown Domain ' . $domain);
+ }
+ $urlParser = new UrlViewFilter($view);
+ $lastQuery = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY);
+ $lastPath = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_PATH);
+ $lastFilter = $urlParser->parseUrl($lastQuery);
+ $lastParameters = array();
+
+ parse_str($lastQuery, $lastParameters);
+ if ($lastFilter->root) {
+ $filter->insert($lastFilter->root);
+ }
+ $params = array();
+ foreach ($lastParameters as $key => $param) {
+ if (!$filter->hasNodeWithAttribute($key)) {
+ $params[$key] = $param;
+ }
+ }
+
+ $baseUrl = Url::fromPath($lastPath, $params);
+ $urlString = $baseUrl->getRelativeUrl();
+ if (stripos($urlString, '?') === false) {
+ $urlString .= '?';
+ } else {
+ $urlString .= '&';
+ }
+ $urlString .= $urlParser->fromTree($filter);
+ return '/' . $urlString;
+ }
+
}
diff --git a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php
index fb93a2bea..fd23004f5 100644
--- a/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php
+++ b/modules/monitoring/library/Monitoring/Filter/Type/StatusFilter.php
@@ -127,7 +127,6 @@ class StatusFilter extends FilterType
public static function createForService()
{
$status = new StatusFilter();
- $status->setType(self::TYPE_SERVICE);
$status->setBaseStates(
array(
'Ok' => 0,
diff --git a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php
index 5434218d1..cc5900d5b 100644
--- a/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php
+++ b/modules/monitoring/library/Monitoring/Filter/UrlViewFilter.php
@@ -33,6 +33,7 @@ namespace Icinga\Module\Monitoring\Filter;
use Icinga\Filter\Filterable;
use Icinga\Filter\Query\Tree;
use Icinga\Filter\Query\Node;
+use Icinga\Web\Request;
use Icinga\Web\Url;
use Icinga\Application\Logger;
@@ -114,6 +115,15 @@ class UrlViewFilter
return $tree->getCopyForFilterable($this->target);
}
+ public function fromRequest($request)
+ {
+ if($request->getParam('query')) {
+ return $this->parseUrl(urldecode($request->getParam('query')));
+ } else {
+ return $this->parseUrl(parse_url($request->getBaseUrl(), PHP_URL_QUERY));
+ }
+ }
+
/**
* Convert a tree node and it's subnodes to a request string
*
diff --git a/modules/monitoring/library/Monitoring/Object/AbstractObject.php b/modules/monitoring/library/Monitoring/Object/AbstractObject.php
index e2ea7259f..d66d12f4f 100644
--- a/modules/monitoring/library/Monitoring/Object/AbstractObject.php
+++ b/modules/monitoring/library/Monitoring/Object/AbstractObject.php
@@ -52,6 +52,7 @@ abstract class AbstractObject
$this->comments = Comment::fromRequest(
$this->request,
array(
+ 'comment_internal_id',
'comment_timestamp',
'comment_author',
'comment_data',
@@ -59,6 +60,7 @@ abstract class AbstractObject
)
)->getQuery()
->where('comment_objecttype_id', 1)
+
->fetchAll();
return $this;
@@ -175,7 +177,6 @@ abstract class AbstractObject
abstract public function populate();
-
public static function fromRequest(Request $request)
{
if ($request->has('service') && $request->has('host')) {
diff --git a/public/js/icinga/components/ajaxPostSubmitForm.js b/public/js/icinga/components/ajaxPostSubmitForm.js
index a6ece8fe8..f5beb30bf 100644
--- a/public/js/icinga/components/ajaxPostSubmitForm.js
+++ b/public/js/icinga/components/ajaxPostSubmitForm.js
@@ -74,7 +74,7 @@ define(['components/app/container', 'jquery'], function(Container, $) {
}
}).done(function() {
var container = getOwnerContainer(form);
- container.replaceDomFromUrl(container.getContainerHref());
+ container.setUrl(container.getUrl());
}).error(function() {
submit.removeAttr('disabled');
});
diff --git a/public/js/icinga/components/container.js b/public/js/icinga/components/container.js
index b568abb33..758079d04 100644
--- a/public/js/icinga/components/container.js
+++ b/public/js/icinga/components/container.js
@@ -26,11 +26,11 @@
*/
// {{{ICINGA_LICENSE_HEADER}}}
-define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITemplate'],
- function($, logger, componentLoader, URI) {
+define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITemplate', 'icinga/util/url'],
+ function($, logger, componentLoader, URI, Tpl, urlMgr) {
"use strict";
- var Icinga;
+ var Icinga;
/**
* Enumeration of possible container types
@@ -57,6 +57,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
*/
var detailContainer = null;
+ var pendingDetailRequest = null;
/**
* A handler for accessing icinga containers, i.e. the #icingamain, #icingadetail containers and specific 'app/container'
* components.
@@ -74,12 +75,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
*/
var Container = function(target) {
- /**
- * Set to true when no history changes should be made
- *
- * @type {boolean} true to disable History.js calls, false to reenable them
- */
- this.freezeHistory = false;
/**
* Return the container that is at the nearest location to this element, or the element itself if it is a container
@@ -116,7 +111,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
} else {
this.containerType = CONTAINER_TYPES.GENERIC;
}
- this.containerDom.attr('data-icinga-href', this.getContainerHref());
if (this.containerDom.data('loadIndicator') !== true) {
this.installDefaultLoadIndicator();
@@ -124,6 +118,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
}
};
+
/**
* Returns the window without the hostname
*
@@ -133,99 +128,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
return window.location.pathname + window.location.search + window.location.hash;
};
- /**
- * Extract and return the main container's location from the current Url
- *
- * This takes the window's Url and removes the detail part
- *
- * @returns {string} The Url of the main container
- */
- var getMainContainerHrefFromUrl = function(baseUrl) {
- // main has the url without the icingadetail part
- var href = URI(getWindowLocationWithoutHost(baseUrl));
- href.removeQuery('detail');
- return href.href();
- };
-
- /**
- * Return the detail container's location from the current Url
- *
- * This takes the detail parameter of the url and returns it or
- * undefined if no location is given
- *
- * @returns {string|undefined} The Url of the detail container or undefined if no detail container is active
- */
- var getDetailContainerHrefFromUrl = function(baseUrl) {
- var location = new URI(baseUrl);
- var href = URI.parseQuery(location.query()).detail;
- if (!href) {
- return;
- }
- // detail is a query param, so it is possible that (due to a bug or whatever) multiple
- // detail fields are declared and returned as arrays
- if (typeof href !== 'string') {
- href = href[0];
- }
- // transform the detail parmameter to an Url
- return URI(href).href();
- };
-
- /**
- * Return the Url of this container
- *
- * This is mostly determined by the Url of the window, but for generic containers we have to rely on the
- * "data-icinga-href" attribute of the container (which is also available for main and detail, but less
- * reliable during history changes)
- *
- * @returns {String|undefined} The Url of the container or undefined if the container has no Url set
- */
- this.getContainerHref = function(baseUrl) {
- baseUrl = baseUrl || getWindowLocationWithoutHost();
- switch (this.containerType) {
- case CONTAINER_TYPES.MAIN:
- return getMainContainerHrefFromUrl(baseUrl);
- case CONTAINER_TYPES.DETAIL:
- return getDetailContainerHrefFromUrl(baseUrl);
- case CONTAINER_TYPES.GENERIC:
- if (this.containerDom.attr('data-icinga-href')) {
- return URI(this.containerDom.attr('data-icinga-href'));
- } else {
- return URI(baseUrl).href();
- }
- }
- };
-
- /**
- * Return a href with representing the current view, but url as the main container
- *
- * @param {URI} url The main Url to use as an URI.js object
- *
- * @returns {URI} The modified URI.js containing the new main and the current detail link
- */
- var setMainContainerHref = function(url, baseUrl) {
- var detail = getDetailContainerHrefFromUrl(baseUrl);
- if (detail) {
- url.addQuery('detail', detail);
- }
- return url;
- };
-
- /**
- * Return a complete Href string representing the current detail href and the provided main Url
- *
- * @param {URI} url The detail Url to use as an URI.js object
- *
- * @returns {URI} The modified URI.js containing the new detail and the current main link
- */
- var setDetailContainerHref = function(url, baseUrl) {
- var location = new URI(baseUrl);
- location.removeQuery('detail');
- if (typeof url !== 'undefined') { // no detail Url given
- location.addQuery('detail', url);
- }
- return location;
- };
-
/**
* Create default load mask
*
@@ -234,7 +136,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
var createDefaultLoadIndicator = function() {
this.showDetail();
-
if (this.containerDom.find('div.load-indicator').length === 0) {
var content = '' +
' ' +
@@ -253,46 +154,6 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
this.containerDom.find('div.load-indicator').remove();
};
- /**
- * Update the Url of this container and let the Url reflect the new changes, if required
- *
- * This updates the window Url and the data-icinga-href attribute of the container. The latter one is required
- * to see which url is the last one the container displayed (e.g. after History changes, the url has changed
- * but the containers data-icinga-href still points to the containers element).
- *
- * @param {String|URI} url An Url string or a URI.js object representing the new Url for this container
- *
- * @return {String} url The new Url of the application (main and detail)
- */
- this.updateContainerHref = function(url, baseUrl) {
- baseUrl = baseUrl || getWindowLocationWithoutHost();
- if (typeof url === "string") {
- url = URI(url);
- }
- var containerUrl, windowUrl;
- switch (this.containerType) {
- case CONTAINER_TYPES.MAIN:
- windowUrl = setMainContainerHref(url, baseUrl);
- containerUrl = windowUrl.clone().removeQuery('detail');
- break;
- case CONTAINER_TYPES.DETAIL:
- windowUrl = setDetailContainerHref(url, baseUrl);
- containerUrl = url;
- break;
- case CONTAINER_TYPES.GENERIC:
- containerUrl = url;
- windowUrl = baseUrl;
- break;
- }
-
- if (containerUrl) {
- this.containerDom.attr('data-icinga-href', containerUrl);
- } else {
- this.containerDom.removeAttr('data-icinga-href');
- }
-
- return windowUrl.href();
- };
/**
* Load the provided url, stop all pending requests for this container and call replaceDom for the returned html
@@ -301,9 +162,59 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
*
* @param {String, URI} url The Url to load or and URI.js object encapsulating it
*/
- this.replaceDomFromUrl = function(url) {
+ this.updateFromUrl = function(url) {
+
+ if (this.containerType === CONTAINER_TYPES.DETAIL) {
+ urlMgr.setDetailUrl(url);
+ } else {
+ urlMgr.setMainUrl(url);
+
+ }
+ };
+
+ this.replaceDomAsync = function(url) {
+ if (url === '') {
+ this.containerDom.empty();
+ this.hideDetail();
+ return;
+ }
+ if (pendingDetailRequest) {
+ pendingDetailRequest.abort();
+ }
this.containerDom.trigger('showLoadIndicator');
- Icinga.replaceBodyFromUrl(this.updateContainerHref(url));
+ pendingDetailRequest = $.ajax({
+ 'url' : url,
+ 'data' : {
+ 'render' : 'detail'
+ }
+ }).done(
+ (function(response) {
+ this.replaceDom($(response));
+ }).bind(this)
+ ).fail(
+ (function(response, reason) {
+ var errorReason;
+ if (response.statusCode.toString()[0] === '4') {
+ errorReason = 'The Requested View Couldn\'t Be Found ';
+ } else {
+ errorReason = 'An Internal Error Occured';
+ }
+ this.replaceDom(
+ $(' ').text(errorReason)
+ );
+ }).bind(this)
+ ).always((function() {
+ this.containerDom.trigger('hideLoadIndicator');
+ }).bind(this));
+ };
+
+ this.getUrl = function() {
+ if (this.containerType === CONTAINER_TYPES.DETAIL) {
+ return urlMgr.detailUrl;
+ } else {
+ return urlMgr.mainUrl;
+ }
+
};
/**
@@ -377,6 +288,51 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
this.containerDom.off('hideLoadIndicator');
};
+ this.onLinkClick = function(ev, target) {
+ if ($.trim($(target).attr('href')) === '#') {
+ return true;
+ }
+ var url = URI($(target).attr('href'));
+ var explicitTarget = $(target).attr('data-icinga-target');
+
+ var isHash = ('#' + url.fragment() === url.href());
+ if (isHash) {
+
+ explicitTarget = this.containerType === CONTAINER_TYPES.MAIN ? 'main' : 'detail';
+ }
+ if (explicitTarget) {
+
+ urlMgr[{
+ 'main' : 'setMainUrl',
+ 'detail' : 'setDetailUrl',
+ 'self' : 'setUrl'
+ }[explicitTarget]](url.href());
+
+ } else if (this.containerType === CONTAINER_TYPES.MAIN) {
+ urlMgr.setDetailUrl(url.href());
+ } else {
+ urlMgr.setMainUrl(url.href());
+ }
+
+
+ ev.preventDefault();
+ ev.stopPropagation();
+ return false;
+
+ };
+
+ this.setUrl = function(url) {
+ if (typeof url === 'string') {
+ url = URI(url);
+ }
+ console.log(url);
+ if (this.containerType === CONTAINER_TYPES.MAIN) {
+ urlMgr.setMainUrl(url.href());
+ } else {
+ urlMgr.setDetailUrl(url.href());
+ }
+ }
+
this.construct(target);
};
@@ -389,12 +345,11 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
* when the link should be catched and processed internally
*/
Container.isExternalLink = function(link) {
- if (link[0] === '#') {
- return true;
- }
return (/^\/\//).test(URI(link).relativeTo(window.location.href).href());
};
+
+
/**
* Return the page's detail container (which is always there)
*
@@ -402,7 +357,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
*/
Container.getDetailContainer = function() {
detailContainer = detailContainer || new Container('#icingadetail');
- if(!jQuery.contains(document.body, detailContainer)) {
+ if(!jQuery.contains(document.body, mainContainer)) {
detailContainer = new Container('#icingadetail');
}
return detailContainer;
@@ -450,6 +405,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
* Available as a static method on the Container object or as an instance method
*/
Container.prototype.hideDetail = Container.hideDetail = function() {
+ urlMgr.setDetailUrl('');
var mainDom = Container.getMainContainer().containerDom,
detailDom = Container.getDetailContainer().containerDom;
@@ -464,47 +420,7 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
mainDom.addClass('col-sm-12');
detailDom.addClass('hidden-sm');
detailDom.removeAttr('data-icinga-href');
- if (typeof this.freezeHistory === 'undefined' || !this.freezeHistory) {
- History.replaceState(
- {},
- document.title,
- URI(window.location.href).removeQuery('detail').href()
- );
- }
};
- if (Modernizr.history) {
- /**
- * Register the click behaviour of the main container, which means that every link, if not catched in a
- * more specific handler, causes an update of the main container if it's not external or a browser behaviour link
- * (those starting with '#').
- */
- $('body').on('click', '#icingamain, #icingadetail', function(ev) {
-
- var targetEl = ev.target || ev.toElement || ev.relatedTarget;
- if (targetEl.tagName.toLowerCase() !== 'a') {
- return true;
- }
-
- if (Container.isExternalLink($(targetEl).attr('href'))) {
- return true;
- } else {
- if ($(targetEl).attr('data-icinga-target') === 'detail') {
- Icinga.replaceBodyFromUrl(
- detailContainer.updateContainerHref(URI($(targetEl).attr('href')).href())
- );
- } else {
- Icinga.replaceBodyFromUrl(
- mainContainer.updateContainerHref(URI($(targetEl).attr('href')).href())
- );
- }
-
- ev.preventDefault();
- ev.stopPropagation();
- return false;
- }
- });
- }
-
/**
* Injects the icinga object into the Container class
*
@@ -516,5 +432,28 @@ define(['jquery', 'logging', 'icinga/componentLoader', 'URIjs/URI', 'URIjs/URITe
Icinga = icingaObj;
};
+ $('body').on('click', '*[data-icinga-component="app/container"], #icingamain, #icingadetail', function(ev) {
+ var targetEl = ev.target || ev.toElement || ev.relatedTarget;
+
+ if (targetEl.tagName.toLowerCase() !== 'a') {
+ targetEl = $(targetEl).parents('a')[0];
+ if (!targetEl) {
+ return true;
+ }
+ }
+ return (new Container(targetEl)).onLinkClick(ev, targetEl);
+
+ });
+
+ $(window).on('hashchange', (function() {
+ urlMgr.syncWithUrl();
+ Container.getDetailContainer().replaceDomAsync(urlMgr.detailUrl);
+ }));
+
+
+ if (urlMgr.detailUrl) {
+ Container.getDetailContainer().replaceDomAsync(urlMgr.detailUrl);
+ }
+
return Container;
});
diff --git a/public/js/icinga/components/mainDetailGrid.js b/public/js/icinga/components/mainDetailGrid.js
index da8232413..5b817a088 100644
--- a/public/js/icinga/components/mainDetailGrid.js
+++ b/public/js/icinga/components/mainDetailGrid.js
@@ -25,8 +25,8 @@
* @author Icinga Development Team
*/
// {{{ICINGA_LICENSE_HEADER}}}
-define(['components/app/container', 'jquery', 'logging', 'URIjs/URI', 'URIjs/URITemplate'],
-function(Container, $, logger, URI) {
+define(['components/app/container', 'jquery', 'logging', 'URIjs/URI', 'URIjs/URITemplate', 'icinga/util/url'],
+function(Container, $, logger, URI, tpl, urlMgr) {
"use strict";
/**
@@ -141,7 +141,7 @@ function(Container, $, logger, URI) {
}
}
- Container.getDetailContainer().replaceDomFromUrl($('a', this).attr('href'));
+ urlMgr.setDetailUrl($('a', this).attr('href'));
if (!ev.ctrlKey && !ev.metaKey) {
$('tr', $(this).parent()).removeClass('active');
}
@@ -160,24 +160,31 @@ function(Container, $, logger, URI) {
controlForms.on('submit', function(evt) {
var container = (new Container(this));
var form = $(this);
- var url = URI(container.getContainerHref());
- url.search(URI.parseQuery(form.serialize()));
- container.replaceDomFromUrl(url.href());
+ var url = container.getUrl();
+
+ if (url.indexOf('?') >= 0) {
+ url += '&';
+ } else {
+ url += '?';
+ }
+ url += form.serialize();
+ container.setUrl(url);
evt.preventDefault();
evt.stopPropagation();
return false;
});
- $('.pagination li a', contentNode.parent()).on('click', function(ev) {
+ $('.pagination li a, a.filter-badge', contentNode.parent()).on('click', function(ev) {
var container = (new Container(this));
- logger.debug("Pagination clicked in " + container.containerType);
+
// Detail will be removed when main pagination changes
if (container.containerType === 'icingamain') {
- Icinga.replaceBodyFromUrl(URI($(this).attr('href')).removeQuery('detail'));
+ urlMgr.setMainUrl(URI($(this).attr('href')));
+ urlMgr.setDetailUrl('');
} else {
- container.replaceDomFromUrl($(this).attr('href'));
+ urlMgr.setDetailUrl(URI($(this).attr('href')));
}
ev.preventDefault();
@@ -187,7 +194,7 @@ function(Container, $, logger, URI) {
};
var getSelectedRows = function() {
- return $('a[href="' + Container.getDetailContainer().getContainerHref() + '"]', determineContentTable()).
+ return $('a[href="' + urlMgr.getDetailUrl() + '"]', determineContentTable()).
parentsUntil('table', 'tr');
};
@@ -214,9 +221,11 @@ function(Container, $, logger, URI) {
this.container.removeDefaultLoadIndicator();
controlForms = determineControlForms();
contentNode = determineContentTable();
+ this.syncSelectionWithDetail();
this.registerControls();
this.registerTableLinks();
this.registerHistoryChanges();
+
};
this.construct(gridDomNode);
diff --git a/public/js/icinga/components/semanticsearch.js b/public/js/icinga/components/semanticsearch.js
index 648bb80de..1ba0dc449 100644
--- a/public/js/icinga/components/semanticsearch.js
+++ b/public/js/icinga/components/semanticsearch.js
@@ -53,8 +53,7 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
* Request new proposals for the given input box
*/
this.getProposal = function() {
- var text = this.inputDom.val().trim();
-
+ var text = $.trim(this.inputDom.val());
if (this.pendingRequest) {
this.pendingRequest.abort();
@@ -114,9 +113,9 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
return {
data: {
'cache' : (new Date()).getTime(),
- 'query' : query,
- 'filter_domain' : this.domain,
- 'filter_module' : this.module
+ 'query' : query,
+ 'filter_domain' : this.domain,
+ 'filter_module' : this.module
},
headers: {
'Accept': 'application/json'
@@ -131,7 +130,8 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
* @param {Object} response The jquery response object inheritn XHttpResponse Attributes
*/
this.showProposals = function(response) {
- if (response.proposals.length === 0) {
+
+ if (!response || !response.proposals || response.proposals.length === 0) {
this.inputDom.popover('destroy');
return;
}
@@ -167,10 +167,12 @@ define(['jquery', 'logging', 'URIjs/URI', 'components/app/container'], function(
var query = $.trim(this.inputDom.val());
this.pendingRequest = $.ajax(this.getRequestParams(query))
.done((function(response) {
- var container = new Container($(this.inputDom));
- var url = container.getContainerHref();
- url += ( url.indexOf('?') === -1 ? '?' : '&' ) + response.urlParam;
- container.replaceDomFromUrl(url);
+ var domContainer = new Container(this.inputDom);
+ var url = response.urlParam;
+
+ if (url) {
+ domContainer.setUrl(url);
+ }
}).bind(this));
};
diff --git a/public/js/icinga/icinga.js b/public/js/icinga/icinga.js
index ec3b6a474..48840f4e3 100755
--- a/public/js/icinga/icinga.js
+++ b/public/js/icinga/icinga.js
@@ -31,90 +31,94 @@ define([
'logging',
'icinga/componentLoader',
'components/app/container',
- 'URIjs/URI'
-], function ($, log, components, Container, URI) {
+ 'URIjs/URI',
+ 'icinga/util/url'
+], function ($, log, components, Container, URI, urlMgr) {
'use strict';
/**
* Icinga prototype
*/
var Icinga = function() {
+ var pendingRequest = null;
- var ignoreHistoryChanges = false;
-
+ /**
+ * Initia
+ */
var initialize = function () {
components.load();
- ignoreHistoryChanges = true;
- registerGenericHistoryHandler();
- ignoreHistoryChanges = false;
log.debug("Initialization finished");
-
};
/**
- * Register handler for handling the history state generically
+ * Globally open the given url and reload the main/detail box to represent it
*
+ * @param url The url to load
*/
- var registerGenericHistoryHandler = function() {
- var lastUrl = URI(window.location.href);
- History.Adapter.bind(window, 'popstate', function() {
- if (ignoreHistoryChanges) {
+ this.openUrl = function(url) {
+ if (pendingRequest) {
+ pendingRequest.abort();
+ }
+ pendingRequest = $.ajax({
+ "url": url
+ }).done(function(response) {
+ var dom = $(response);
+ var detailDom = null;
+ if (urlMgr.detailUrl) {
+ detailDom = $('#icingadetail');
+ }
+ $(document.body).empty().append(dom);
+ if (detailDom && detailDom.length) {
+ $('#icingadetail').replaceWith(detailDom);
+ Container.showDetail();
+ }
+ components.load();
+ Container.getMainContainer();
+ }).fail(function(response, reason) {
+ if (reason === 'abort') {
return;
}
-
- gotoUrl(History.getState().url);
- lastUrl = URI(window.location.href);
+ log.error("Request failed: ", response.message);
});
};
- var gotoUrl = function(href) {
- if (typeof document.body.pending !== 'undefined') {
- document.body.pending.abort();
- }
- if (typeof href === 'string') {
- href = URI(href);
- }
- document.body.pending = $.ajax({
- url: href.href()
- }).done(function(domNodes) {
- $('body').empty().append(jQuery.parseHTML(domNodes));
- ignoreHistoryChanges = true;
- History.pushState({}, document.title, href.href());
- ignoreHistoryChanges = false;
- components.load();
- }).error(function(xhr, textStatus, errorThrown) {
- if (xhr.responseText) {
- $('body').empty().append(jQuery.parseHTML(xhr.responseText));
- } else if (textStatus !== 'abort') {
- logging.emergency('Could not load URL', xhr.href, textStatus, errorThrown);
- }
- });
-
- return false;
- };
-
if (Modernizr.history) {
- $(document.body).on('click', '#icinganavigation', function(ev) {
- var targetEl = ev.target || ev.toElement || ev.relatedTarget;
- if (targetEl.tagName.toLowerCase() !== 'a') {
- return true;
+ /**
+ * Event handler that will be called when the url change
+ */
+ urlMgr.syncWithUrl();
+ var lastMain = urlMgr.mainUrl;
+ $(window).on('pushstate', (function() {
+ urlMgr.syncWithUrl();
+ if (urlMgr.mainUrl !== lastMain) {
+ this.openUrl(urlMgr.getUrl());
+ lastMain = urlMgr.mainUrl;
+ }
+ // If an anchor is set, scroll to it's position
+ if ($('#' + urlMgr.anchor).length) {
+ $(document.body).scrollTo($('#' + urlMgr.anchor));
+ }
+ }).bind(this));
+
+ /**
+ * Event handler for browser back/forward events
+ */
+ $(window).on('popstate', (function() {
+ var lastMain = urlMgr.mainUrl;
+ urlMgr.syncWithUrl();
+ if (urlMgr.mainUrl !== lastMain) {
+ this.openUrl(urlMgr.getUrl());
}
- var href = $(targetEl).attr('href');
- if (Container.isExternalLink(href)) {
- return true;
- }
- ev.preventDefault();
- ev.stopPropagation();
- gotoUrl(href);
- return false;
- });
+ }).bind(this));
}
+
$(document).ready(initialize.bind(this));
Container.setIcinga(this);
+
this.components = components;
- this.replaceBodyFromUrl = gotoUrl;
+
};
diff --git a/public/js/icinga/util/url.js b/public/js/icinga/util/url.js
new file mode 100644
index 000000000..7fb337a33
--- /dev/null
+++ b/public/js/icinga/util/url.js
@@ -0,0 +1,194 @@
+// {{{ICINGA_LICENSE_HEADER}}}
+/**
+ * This file is part of Icinga 2 Web.
+ *
+ * Icinga 2 Web - Head for multiple monitoring backends.
+ * Copyright (C) 2013 Icinga Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * @copyright 2013 Icinga Development Team
+ * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
+ * @author Icinga Development Team
+ */
+// {{{ICINGA_LICENSE_HEADER}}}
+
+define(['jquery', 'URIjs/URI'], function($, URI, Container) {
+ "use strict";
+
+ var currentUrl = URI(window.location.href);
+
+ /**
+ * Utility class for Url handling
+ *
+ */
+ var URLMgr = function() {
+ /**
+ * The current url of the main part
+ * @type {string}
+ */
+ this.mainUrl = '';
+
+ /**
+ * The current main anchor
+ * @type {string}
+ */
+ this.anchor = '';
+
+ /**
+ * The current detail url
+ *
+ * @type {string}
+ */
+ this.detailUrl = '';
+
+ /**
+ * The current anchor of the detail url
+ *
+ * @type {string}
+ */
+ this.detailAnchor = '';
+
+ /**
+ * Extract the anchor of the main url part from the given url
+ *
+ * @param {String|URI} url An URL object to extract the information from
+ * @returns {*}
+ */
+ this.getMainAnchor = function(url) {
+ url = url || URI(window.location.href);
+ if (typeof url === 'string') {
+ url = URI(url);
+ }
+ var fragment = url.fragment();
+ if (fragment.length === 0) {
+ return '';
+ }
+ var parts = fragment.split('!');
+ if (parts.length > 0) {
+ return parts[0];
+ } else {
+ return '';
+ }
+ };
+
+ /**
+ * Extract the detail url a the given url. Returns a [URL, ANCHOR] Tupel
+ *
+ * @param String url An optional url to parse (otherwise window.location.href is used)
+ * @returns {Array} A [{String} Url, {String} anchor] tupel
+ */
+ this.getDetailUrl = function(url) {
+ url = url || URI(window.location.href);
+ if (typeof url === 'string') {
+ url = URI(url);
+ }
+
+ var fragment = url.fragment();
+ if (fragment.length === 0) {
+ return '';
+ }
+ var parts = fragment.split('!', 2);
+
+ if (parts.length === 2) {
+ var result = /detail=(.*)$/.exec(parts[1]);
+ if (!result || result.length < 2) {
+ return '';
+ }
+ return result[1].replace('%23', '#').split('#');
+ } else {
+ return '';
+ }
+ };
+
+ /**
+ * Overwrite the detail Url and update the hash
+ *
+ * @param String url The url to use for the detail part
+ */
+ this.setDetailUrl = function(url) {
+ if (typeof url === 'string') {
+ url = URI(url);
+ }
+ if( !url.fragment() || url.href() !== '#' + url.fragment()) {
+ this.detailUrl = url.clone().fragment('').href();
+ }
+ this.detailAnchor = this.getMainAnchor(url);
+
+ window.location.hash = this.getUrlHash();
+ };
+
+ /**
+ * Get the hash of the current detail url and anchor i
+ *
+ * @returns {string}
+ */
+ this.getUrlHash = function() {
+ var anchor = '#' + this.anchor +
+ '!' + ($.trim(this.detailUrl) ? 'detail=' : '') + this.detailUrl +
+ (this.detailAnchor ? '%23' : '') + this.detailAnchor;
+ anchor = $.trim(anchor);
+ if (anchor === '#!' || anchor === '#') {
+ anchor = '';
+ }
+ return anchor;
+ };
+
+ /**
+ * Set the main url to be used
+ *
+ * This triggers the pushstate event or causes a page reload if the history api is
+ * not available
+ *
+ * @param url
+ */
+ this.setMainUrl = function(url) {
+ this.anchor = this.getMainAnchor(url);
+ this.mainUrl = URI(url).clone().fragment('').href();
+ if (!Modernizr.history) {
+ window.location.href = this.mainUrl + this.getUrlHash();
+ } else {
+ window.history.pushState({}, document.title, this.mainUrl + this.getUrlHash());
+ $(window).trigger('pushstate');
+ }
+ };
+
+ /**
+ * Return the href (main path + hash)
+ *
+ * @returns {string}
+ */
+ this.getUrl = function() {
+ return this.mainUrl + this.getUrlHash();
+ };
+
+ /**
+ * Take the current url and sync the internal state of this url manager with it
+ */
+ this.syncWithUrl = function() {
+ this.mainUrl = URI(window.location.href).clone().fragment('').href();
+ this.anchor = this.getMainAnchor();
+ var urlAnchorTupel = this.getDetailUrl();
+ this.detailUrl = urlAnchorTupel[0] || '';
+ this.detailAnchor = urlAnchorTupel[1] || '';
+ };
+
+
+ this.syncWithUrl();
+ };
+ var urlMgr = new URLMgr();
+
+ return urlMgr;
+});
diff --git a/public/js/main.js b/public/js/main.js
index 6bdf90485..79cf52d29 100755
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -3,9 +3,8 @@ requirejs.config({
'urlArgs': "bust=" + (new Date()).getTime(),
'paths': {
'jquery': 'vendor/jquery-1.8.3',
- 'jqueryPlugins': 'vendor/jqueryPlugins/',
+ 'jquery_scrollto': 'vendor/jquery.scrollto',
'bootstrap': 'vendor/bootstrap/bootstrap.min',
- 'history': 'vendor/history',
'logging': 'icinga/util/logging',
'URIjs': 'vendor/uri',
'datetimepicker': 'vendor/bootstrap/datetimepicker.min'
@@ -14,21 +13,23 @@ requirejs.config({
'datetimepicker': {
'exports': 'datetimepicker'
},
+ 'jquery_scrollto': {
+ exports: 'jquery_scrollto'
+ },
'jquery' : {
exports: 'jquery'
}
}
});
-define(['jquery', 'history'], function ($) {
+define(['jquery'], function ($, history) {
+ window.$ = $;
+ window.jQuery = $;
- requirejs(['bootstrap'], function() {
+ requirejs(['bootstrap','jquery_scrollto'], function() {
requirejs(['datetimepicker']);
});
-
requirejs(['icinga/icinga'], function (Icinga) {
- window.$ = $;
- window.jQuery = $;
window.Icinga = Icinga;
});
diff --git a/public/js/vendor/history.js b/public/js/vendor/history.js
index fa8bdd192..df9a84f94 100644
--- a/public/js/vendor/history.js
+++ b/public/js/vendor/history.js
@@ -1 +1 @@
-(function(e,t){"use strict";var n=e.History=e.History||{};if(typeof n.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");n.Adapter={handlers:{},_uid:1,uid:function(e){return e._uid||(e._uid=n.Adapter._uid++)},bind:function(e,t,r){var i=n.Adapter.uid(e);n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[],n.Adapter.handlers[i][t].push(r),e["on"+t]=function(e,t){return function(r){n.Adapter.trigger(e,t,r)}}(e,t)},trigger:function(e,t,r){r=r||{};var i=n.Adapter.uid(e),s,o;n.Adapter.handlers[i]=n.Adapter.handlers[i]||{},n.Adapter.handlers[i][t]=n.Adapter.handlers[i][t]||[];for(s=0,o=n.Adapter.handlers[i][t].length;s")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window)
+typeof JSON!="object"&&(JSON={}),function(){"use strict";function f(e){return e<10?"0"+e:e}function quote(e){return escapable.lastIndex=0,escapable.test(e)?'"'+e.replace(escapable,function(e){var t=meta[e];return typeof t=="string"?t:"\\u"+("0000"+e.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var n,r,i,s,o=gap,u,a=t[e];a&&typeof a=="object"&&typeof a.toJSON=="function"&&(a=a.toJSON(e)),typeof rep=="function"&&(a=rep.call(t,e,a));switch(typeof a){case"string":return quote(a);case"number":return isFinite(a)?String(a):"null";case"boolean":case"null":return String(a);case"object":if(!a)return"null";gap+=indent,u=[];if(Object.prototype.toString.apply(a)==="[object Array]"){s=a.length;for(n=0;n")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window)
diff --git a/public/js/vendor/jquery.scrollto.js b/public/js/vendor/jquery.scrollto.js
new file mode 100644
index 000000000..3d1f1f2bc
--- /dev/null
+++ b/public/js/vendor/jquery.scrollto.js
@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2007-2013 Ariel Flesler - afleslergmailcom | http://flesler.blogspot.com
+ * Dual licensed under MIT and GPL.
+ * @author Ariel Flesler
+ * @version 1.4.6
+ */
+;(function($){var h=$.scrollTo=function(a,b,c){$(window).scrollTo(a,b,c)};h.defaults={axis:'xy',duration:parseFloat($.fn.jquery)>=1.3?0:1,limit:true};h.window=function(a){return $(window)._scrollable()};$.fn._scrollable=function(){return this.map(function(){var a=this,isWin=!a.nodeName||$.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!isWin)return a;var b=(a.contentWindow||a).document||a.ownerDocument||a;return/webkit/i.test(navigator.userAgent)||b.compatMode=='BackCompat'?b.body:b.documentElement})};$.fn.scrollTo=function(e,f,g){if(typeof f=='object'){g=f;f=0}if(typeof g=='function')g={onAfter:g};if(e=='max')e=9e9;g=$.extend({},h.defaults,g);f=f||g.duration;g.queue=g.queue&&g.axis.length>1;if(g.queue)f/=2;g.offset=both(g.offset);g.over=both(g.over);return this._scrollable().each(function(){if(e==null)return;var d=this,$elem=$(d),targ=e,toff,attr={},win=$elem.is('html,body');switch(typeof targ){case'number':case'string':if(/^([+-]=?)?\d+(\.\d+)?(px|%)?$/.test(targ)){targ=both(targ);break}targ=$(targ,this);if(!targ.length)return;case'object':if(targ.is||targ.style)toff=(targ=$(targ)).offset()}$.each(g.axis.split(''),function(i,a){var b=a=='x'?'Left':'Top',pos=b.toLowerCase(),key='scroll'+b,old=d[key],max=h.max(d,a);if(toff){attr[key]=toff[pos]+(win?0:old-$elem.offset()[pos]);if(g.margin){attr[key]-=parseInt(targ.css('margin'+b))||0;attr[key]-=parseInt(targ.css('border'+b+'Width'))||0}attr[key]+=g.offset[pos]||0;if(g.over[pos])attr[key]+=targ[a=='x'?'width':'height']()*g.over[pos]}else{var c=targ[pos];attr[key]=c.slice&&c.slice(-1)=='%'?parseFloat(c)/100*max:c}if(g.limit&&/^\d+$/.test(attr[key]))attr[key]=attr[key]<=0?0:Math.min(attr[key],max);if(!i&&g.queue){if(old!=attr[key])animate(g.onAfterFirst);delete attr[key]}});animate(g.onAfter);function animate(a){$elem.animate(attr,f,g.easing,a&&function(){a.call(this,targ,g)})}}).end()};h.max=function(a,b){var c=b=='x'?'Width':'Height',scroll='scroll'+c;if(!$(a).is('html,body'))return a[scroll]-$(a)[c.toLowerCase()]();var d='client'+c,html=a.ownerDocument.documentElement,body=a.ownerDocument.body;return Math.max(html[scroll],body[scroll])-Math.min(html[d],body[d])};function both(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery);
+;(function($){var h=location.href.replace(/#.*/,'');var i=$.localScroll=function(a){$('body').localScroll(a)};i.defaults={duration:1000,axis:'y',event:'click',stop:true,target:window};i.hash=function(a){if(location.hash){a=$.extend({},i.defaults,a);a.hash=false;if(a.reset){var d=a.duration;delete a.duration;$(a.target).scrollTo(0,a);a.duration=d}scroll(0,location,a)}};$.fn.localScroll=function(b){b=$.extend({},i.defaults,b);return b.lazy?this.bind(b.event,function(e){var a=$([e.target,e.target.parentNode]).filter(filter)[0];if(a)scroll(e,a,b)}):this.find('a,area').filter(filter).bind(b.event,function(e){scroll(e,this,b)}).end().end();function filter(){return!!this.href&&!!this.hash&&this.href.replace(this.hash,'')==h&&(!b.filter||$(this).is(b.filter))}};function scroll(e,a,b){var c=a.hash.slice(1),elem=document.getElementById(c)||document.getElementsByName(c)[0];if(!elem)return;if(e)e.preventDefault();var d=$(b.target);if(b.lock&&d.is(':animated')||b.onBefore&&b.onBefore(e,elem,d)===false)return;if(b.stop)d._scrollable().stop(true);if(b.hash){var f=b.offset;f=f&&f.top||f||0;var g=elem.id==c?'id':'name',$a=$(' ').attr(g,c).css({position:'absolute',top:$(window).scrollTop()+f,left:$(window).scrollLeft()});elem[g]='';$('body').prepend($a);location=a.hash;$a.remove();elem[g]=c}d.scrollTo(elem,b).trigger('notify.serialScroll',[elem])}})(jQuery);
\ No newline at end of file
diff --git a/test/js/test/icinga/components/containerTest.js b/test/js/test/icinga/components/containerTest.js
index 04bb0fad3..2bc65d5e0 100644
--- a/test/js/test/icinga/components/containerTest.js
+++ b/test/js/test/icinga/components/containerTest.js
@@ -96,8 +96,10 @@ describe('The container component', function() {
*/
it('should provide access to the main and detail component', function() {
createDOM();
+
rjsmock.registerDependencies({
- 'URIjs/URI' : URI
+ 'URIjs/URI' : URI,
+ 'icinga/util/url' : 'icinga/util/url.js'
});
requireNew('icinga/components/container.js');
var Container = rjsmock.getDefine();
@@ -109,41 +111,4 @@ describe('The container component', function() {
$('#icingadetail')[0], 'Assert the DOM of the detail container being #icingadetail');
});
- /**
- * Test dynamic Url update
- */
- it('should automatically update its part of the URL if assigning a new URL', function() {
- rjsmock.registerDependencies({
- 'URIjs/URI' : URI
- });
- requireNew('icinga/components/container.js');
- createDOM();
- var Container = rjsmock.getDefine();
- var url = Container.getMainContainer().updateContainerHref('/some/other/url?test');
- window.setWindowUrl(url);
- Container.getMainContainer().containerDom.attr('data-icinga-href').should.equal('/some/other/url?test');
-
- url.should.equal(
- '/some/other/url?test',
- 'Assert the main container updating the url correctly');
-
- url = Container.getDetailContainer().updateContainerHref('/some/detail/url?test');
- window.setWindowUrl(url);
-
- Container.getDetailContainer().containerDom.attr('data-icinga-href').should.equal('/some/detail/url?test');
- url.should.equal(
- '/some/other/url?test&detail=' + encodeURIComponent('/some/detail/url?test'),
- 'Assert the detail container only updating the "detail" portion of the URL'
- );
-
- url = Container.getMainContainer().updateContainerHref('/some/other2/url?test=test');
-
- window.setWindowUrl(Container.getMainContainer().getContainerHref(window.location.href));
- Container.getMainContainer().containerDom.attr('data-icinga-href').should.equal('/some/other2/url?test=test');
- url.should.equal(
- '/some/other2/url?test=test&detail=' + encodeURIComponent('/some/detail/url?test'),
- 'Assert the main container keeping the detail portion untouched if being assigned a new URL'
- );
- });
-
});
diff --git a/test/php/library/Icinga/Web/Widget/TabTest.php b/test/php/library/Icinga/Web/Widget/TabTest.php
index c9bf03180..02b7dd9cb 100644
--- a/test/php/library/Icinga/Web/Widget/TabTest.php
+++ b/test/php/library/Icinga/Web/Widget/TabTest.php
@@ -107,7 +107,7 @@ class TabTest extends PHPUnit_Framework_TestCase
$this->assertEquals(
1,
preg_match(
- '/Title text<\/a><\/li>/i',
+ '/Title text<\/a><\/li>/i',
$html
),
'Asserting an url being rendered inside an HTML anchor. got ' . $html
|