diff --git a/src/main/java/com/commafeed/frontend/pages/HomePage.html b/src/main/java/com/commafeed/frontend/pages/HomePage.html
index 37dd5b45..0d251047 100644
--- a/src/main/java/com/commafeed/frontend/pages/HomePage.html
+++ b/src/main/java/com/commafeed/frontend/pages/HomePage.html
@@ -5,29 +5,8 @@
-
diff --git a/src/main/java/com/commafeed/frontend/pages/HomePage.java b/src/main/java/com/commafeed/frontend/pages/HomePage.java
index d78b0f90..05366746 100644
--- a/src/main/java/com/commafeed/frontend/pages/HomePage.java
+++ b/src/main/java/com/commafeed/frontend/pages/HomePage.java
@@ -11,6 +11,7 @@ import com.commafeed.frontend.references.angular.AngularResourceReference;
import com.commafeed.frontend.references.angular.AngularSanitizeReference;
import com.commafeed.frontend.references.angularui.AngularUIReference;
import com.commafeed.frontend.references.angularuibootstrap.AngularUIBootstrapReference;
+import com.commafeed.frontend.references.angularuistate.AngularUIStateReference;
import com.commafeed.frontend.references.csstreeview.CssTreeViewReference;
import com.commafeed.frontend.references.mousetrap.MouseTrapReference;
import com.commafeed.frontend.references.nginfinitescroll.NGInfiniteScrollReference;
@@ -30,6 +31,7 @@ public class HomePage extends BasePage {
AngularSanitizeReference.renderHead(response);
AngularUIReference.renderHead(response);
AngularUIBootstrapReference.renderHead(response);
+ AngularUIStateReference.renderHead(response);
NGUploadReference.renderHead(response);
NGInfiniteScrollReference.renderHead(response);
Select2Reference.renderHead(response);
diff --git a/src/main/java/com/commafeed/frontend/references/angularuistate/AngularUIStateReference.java b/src/main/java/com/commafeed/frontend/references/angularuistate/AngularUIStateReference.java
new file mode 100644
index 00000000..cc5b574f
--- /dev/null
+++ b/src/main/java/com/commafeed/frontend/references/angularuistate/AngularUIStateReference.java
@@ -0,0 +1,30 @@
+package com.commafeed.frontend.references.angularuistate;
+
+import java.util.Arrays;
+
+import org.apache.wicket.markup.head.HeaderItem;
+import org.apache.wicket.markup.head.IHeaderResponse;
+import org.apache.wicket.markup.head.JavaScriptHeaderItem;
+import org.apache.wicket.request.resource.JavaScriptResourceReference;
+
+import com.commafeed.frontend.references.angular.AngularReference;
+
+public class AngularUIStateReference extends JavaScriptResourceReference {
+ private static final long serialVersionUID = 1L;
+
+ public static final AngularUIStateReference INSTANCE = new AngularUIStateReference();
+
+ private AngularUIStateReference() {
+ super(AngularUIStateReference.class, "angular-ui-states.js");
+ }
+
+ @Override
+ public Iterable extends HeaderItem> getDependencies() {
+ return Arrays.asList(JavaScriptHeaderItem
+ .forReference(AngularReference.INSTANCE));
+ }
+
+ public static void renderHead(final IHeaderResponse response) {
+ response.render(JavaScriptHeaderItem.forReference(INSTANCE));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/commafeed/frontend/references/angularuistate/angular-ui-states.js b/src/main/java/com/commafeed/frontend/references/angularuistate/angular-ui-states.js
new file mode 100644
index 00000000..b814fc9a
--- /dev/null
+++ b/src/main/java/com/commafeed/frontend/references/angularuistate/angular-ui-states.js
@@ -0,0 +1,995 @@
+/*jshint globalstrict:true*/
+/*global angular:false*/
+'use strict';
+
+var isDefined = angular.isDefined,
+ isFunction = angular.isFunction,
+ isString = angular.isString,
+ isObject = angular.isObject,
+ isArray = angular.isArray,
+ forEach = angular.forEach,
+ extend = angular.extend,
+ copy = angular.copy;
+
+function inherit(parent, extra) {
+ return extend(new (extend(function() {}, { prototype: parent }))(), extra);
+}
+
+/**
+ * Extends the destination object `dst` by copying all of the properties from the `src` object(s)
+ * to `dst` if the `dst` object has no own property of the same name. You can specify multiple
+ * `src` objects.
+ *
+ * @param {Object} dst Destination object.
+ * @param {...Object} src Source object(s).
+ * @see angular.extend
+ */
+function merge(dst) {
+ forEach(arguments, function(obj) {
+ if (obj !== dst) {
+ forEach(obj, function(value, key) {
+ if (!dst.hasOwnProperty(key)) dst[key] = value;
+ });
+ }
+ });
+ return dst;
+}
+
+angular.module('ui.util', ['ng']);
+angular.module('ui.router', ['ui.util']);
+angular.module('ui.state', ['ui.router', 'ui.util']);
+angular.module('ui.compat', ['ui.state']);
+
+/**
+ * Service. Manages loading of templates.
+ * @constructor
+ * @name $templateFactory
+ * @requires $http
+ * @requires $templateCache
+ * @requires $injector
+ */
+$TemplateFactory.$inject = ['$http', '$templateCache', '$injector'];
+function $TemplateFactory( $http, $templateCache, $injector) {
+
+ /**
+ * Creates a template from a configuration object.
+ * @function
+ * @name $templateFactory#fromConfig
+ * @methodOf $templateFactory
+ * @param {Object} config Configuration object for which to load a template. The following
+ * properties are search in the specified order, and the first one that is defined is
+ * used to create the template:
+ * @param {string|Function} config.template html string template or function to load via
+ * {@link $templateFactory#fromString fromString}.
+ * @param {string|Function} config.templateUrl url to load or a function returning the url
+ * to load via {@link $templateFactory#fromUrl fromUrl}.
+ * @param {Function} config.templateProvider function to invoke via
+ * {@link $templateFactory#fromProvider fromProvider}.
+ * @param {Object} params Parameters to pass to the template function.
+ * @param {Object} [locals] Locals to pass to `invoke` if the template is loaded via a
+ * `templateProvider`. Defaults to `{ params: params }`.
+ * @return {string|Promise.
} The template html as a string, or a promise for that string,
+ * or `null` if no template is configured.
+ */
+ this.fromConfig = function (config, params, locals) {
+ return (
+ isDefined(config.template) ? this.fromString(config.template, params) :
+ isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
+ isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, locals) :
+ null
+ );
+ };
+
+ /**
+ * Creates a template from a string or a function returning a string.
+ * @function
+ * @name $templateFactory#fromString
+ * @methodOf $templateFactory
+ * @param {string|Function} template html template as a string or function that returns an html
+ * template as a string.
+ * @param {Object} params Parameters to pass to the template function.
+ * @return {string|Promise.} The template html as a string, or a promise for that string.
+ */
+ this.fromString = function (template, params) {
+ return isFunction(template) ? template(params) : template;
+ };
+
+ /**
+ * Loads a template from the a URL via `$http` and `$templateCache`.
+ * @function
+ * @name $templateFactory#fromUrl
+ * @methodOf $templateFactory
+ * @param {string|Function} url url of the template to load, or a function that returns a url.
+ * @param {Object} params Parameters to pass to the url function.
+ * @return {string|Promise.} The template html as a string, or a promise for that string.
+ */
+ this.fromUrl = function (url, params) {
+ if (isFunction(url)) url = url(params);
+ if (url == null) return null;
+ else return $http
+ .get(url, { cache: $templateCache })
+ .then(function(response) { return response.data; });
+ };
+
+ /**
+ * Creates a template by invoking an injectable provider function.
+ * @function
+ * @name $templateFactory#fromUrl
+ * @methodOf $templateFactory
+ * @param {Function} provider Function to invoke via `$injector.invoke`
+ * @param {Object} params Parameters for the template.
+ * @param {Object} [locals] Locals to pass to `invoke`. Defaults to `{ params: params }`.
+ * @return {string|Promise.} The template html as a string, or a promise for that string.
+ */
+ this.fromProvider = function (provider, params, locals) {
+ return $injector.invoke(provider, null, locals || { params: params });
+ };
+}
+
+angular.module('ui.util').service('$templateFactory', $TemplateFactory);
+
+/**
+ * Matches URLs against patterns and extracts named parameters from the path or the search
+ * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list
+ * of search parameters. Multiple search parameter names are separated by '&'. Search parameters
+ * do not influence whether or not a URL is matched, but their values are passed through into
+ * the matched parameters returned by {@link UrlMatcher#exec exec}.
+ *
+ * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace
+ * syntax, which optionally allows a regular expression for the parameter to be specified:
+ *
+ * * ':' name - colon placeholder
+ * * '*' name - catch-all placeholder
+ * * '{' name '}' - curly placeholder
+ * * '{' name ':' regexp '}' - curly placeholder with regexp. Should the regexp itself contain
+ * curly braces, they must be in matched pairs or escaped with a backslash.
+ *
+ * Parameter names may contain only word characters (latin letters, digits, and underscore) and
+ * must be unique within the pattern (across both path and search parameters). For colon
+ * placeholders or curly placeholders without an explicit regexp, a path parameter matches any
+ * number of characters other than '/'. For catch-all placeholders the path parameter matches
+ * any number of characters.
+ *
+ * ### Examples
+ *
+ * * '/hello/' - Matches only if the path is exactly '/hello/'. There is no special treatment for
+ * trailing slashes, and patterns have to match the entire path, not just a prefix.
+ * * '/user/:id' - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or
+ * '/user/bob/details'. The second path segment will be captured as the parameter 'id'.
+ * * '/user/{id}' - Same as the previous example, but using curly brace syntax.
+ * * '/user/{id:[^/]*}' - Same as the previous example.
+ * * '/user/{id:[0-9a-fA-F]{1,8}}' - Similar to the previous example, but only matches if the id
+ * parameter consists of 1 to 8 hex digits.
+ * * '/files/{path:.*}' - Matches any URL starting with '/files/' and captures the rest of the
+ * path into the parameter 'path'.
+ * * '/files/*path' - ditto.
+ *
+ * @constructor
+ * @param {string} pattern the pattern to compile into a matcher.
+ *
+ * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any
+ * URL matching this matcher (i.e. any string for which {@link UrlMatcher#exec exec()} returns
+ * non-null) will start with this prefix.
+ */
+function UrlMatcher(pattern) {
+
+ // Find all placeholders and create a compiled pattern, using either classic or curly syntax:
+ // '*' name
+ // ':' name
+ // '{' name '}'
+ // '{' name ':' regexp '}'
+ // The regular expression is somewhat complicated due to the need to allow curly braces
+ // inside the regular expression. The placeholder regexp breaks down as follows:
+ // ([:*])(\w+) classic placeholder ($1 / $2)
+ // \{(\w+)(?:\:( ... ))?\} curly brace placeholder ($3) with optional regexp ... ($4)
+ // (?: ... | ... | ... )+ the regexp consists of any number of atoms, an atom being either
+ // [^{}\\]+ - anything other than curly braces or backslash
+ // \\. - a backslash escape
+ // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
+ var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
+ names = {}, compiled = '^', last = 0, m,
+ segments = this.segments = [],
+ params = this.params = [];
+
+ function addParameter(id) {
+ if (!/^\w+$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
+ if (names[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
+ names[id] = true;
+ params.push(id);
+ }
+
+ function quoteRegExp(string) {
+ return string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
+ }
+
+ this.source = pattern;
+
+ // Split into static segments separated by path parameter placeholders.
+ // The number of segments is always 1 more than the number of parameters.
+ var id, regexp, segment;
+ while ((m = placeholder.exec(pattern))) {
+ id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
+ regexp = m[4] || (m[1] == '*' ? '.*' : '[^/]*');
+ segment = pattern.substring(last, m.index);
+ if (segment.indexOf('?') >= 0) break; // we're into the search part
+ compiled += quoteRegExp(segment) + '(' + regexp + ')';
+ addParameter(id);
+ segments.push(segment);
+ last = placeholder.lastIndex;
+ }
+ segment = pattern.substring(last);
+
+ // Find any search parameter names and remove them from the last segment
+ var i = segment.indexOf('?');
+ if (i >= 0) {
+ var search = this.sourceSearch = segment.substring(i);
+ segment = segment.substring(0, i);
+ this.sourcePath = pattern.substring(0, last+i);
+
+ // Allow parameters to be separated by '?' as well as '&' to make concat() easier
+ forEach(search.substring(1).split(/[&?]/), addParameter);
+ } else {
+ this.sourcePath = pattern;
+ this.sourceSearch = '';
+ }
+
+ compiled += quoteRegExp(segment) + '$';
+ segments.push(segment);
+ this.regexp = new RegExp(compiled);
+ this.prefix = segments[0];
+}
+
+/**
+ * Returns a new matcher for a pattern constructed by appending the path part and adding the
+ * search parameters of the specified pattern to this pattern. The current pattern is not
+ * modified. This can be understood as creating a pattern for URLs that are relative to (or
+ * suffixes of) the current pattern.
+ *
+ * ### Example
+ * The following two matchers are equivalent:
+ * ```
+ * new UrlMatcher('/user/{id}?q').concat('/details?date');
+ * new UrlMatcher('/user/{id}/details?q&date');
+ * ```
+ *
+ * @param {string} pattern The pattern to append.
+ * @return {UrlMatcher} A matcher for the concatenated pattern.
+ */
+UrlMatcher.prototype.concat = function (pattern) {
+ // Because order of search parameters is irrelevant, we can add our own search
+ // parameters to the end of the new pattern. Parse the new pattern by itself
+ // and then join the bits together, but it's much easier to do this on a string level.
+ return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch);
+};
+
+UrlMatcher.prototype.toString = function () {
+ return this.source;
+};
+
+/**
+ * Tests the specified path against this matcher, and returns an object containing the captured
+ * parameter values, or null if the path does not match. The returned object contains the values
+ * of any search parameters that are mentioned in the pattern, but their value may be null if
+ * they are not present in `searchParams`. This means that search parameters are always treated
+ * as optional.
+ *
+ * ### Example
+ * ```
+ * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { x:'1', q:'hello' });
+ * // returns { id:'bob', q:'hello', r:null }
+ * ```
+ *
+ * @param {string} path The URL path to match, e.g. `$location.path()`.
+ * @param {Object} searchParams URL search parameters, e.g. `$location.search()`.
+ * @return {Object} The captured parameter values.
+ */
+UrlMatcher.prototype.exec = function (path, searchParams) {
+ var m = this.regexp.exec(path);
+ if (!m) return null;
+
+ var params = this.params, nTotal = params.length,
+ nPath = this.segments.length-1,
+ values = {}, i;
+
+ for (i=0; i} An array of parameter names. Must be treated as read-only. If the
+ * pattern has no parameters, an empty array is returned.
+ */
+UrlMatcher.prototype.parameters = function () {
+ return this.params;
+};
+
+/**
+ * Creates a URL that matches this pattern by substituting the specified values
+ * for the path and search parameters. Null values for path parameters are
+ * treated as empty strings.
+ *
+ * ### Example
+ * ```
+ * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
+ * // returns '/user/bob?q=yes'
+ * ```
+ *
+ * @param {Object} values the values to substitute for the parameters in this pattern.
+ * @return {string} the formatted URL (path and optionally search part).
+ */
+UrlMatcher.prototype.format = function (values) {
+ var segments = this.segments, params = this.params;
+ if (!values) return segments.join('');
+
+ var nPath = segments.length-1, nTotal = params.length,
+ result = segments[0], i, search, value;
+
+ for (i=0; i= 0) throw new Error("State must have a valid name");
+ if (states[name]) throw new Error("State '" + name + "'' is already defined");
+
+ // Derive parent state from a hierarchical name only if 'parent' is not explicitly defined.
+ var parent = root;
+ if (!isDefined(state.parent)) {
+ // regex matches any valid composite state name
+ // would match "contact.list" but not "contacts"
+ var compositeName = /^(.+)\.[^.]+$/.exec(name);
+ if (compositeName != null) {
+ parent = findState(compositeName[1]);
+ }
+ } else if (state.parent != null) {
+ parent = findState(state.parent);
+ }
+ state.parent = parent;
+ // state.children = [];
+ // if (parent) parent.children.push(state);
+
+ // Build a URLMatcher if necessary, either via a relative or absolute URL
+ var url = state.url;
+ if (isString(url)) {
+ if (url.charAt(0) == '^') {
+ url = state.url = $urlMatcherFactory.compile(url.substring(1));
+ } else {
+ url = state.url = (parent.navigable || root).url.concat(url);
+ }
+ } else if (isObject(url) &&
+ isFunction(url.exec) && isFunction(url.format) && isFunction(url.concat)) {
+ /* use UrlMatcher (or compatible object) as is */
+ } else if (url != null) {
+ throw new Error("Invalid url '" + url + "' in state '" + state + "'");
+ }
+
+ // Keep track of the closest ancestor state that has a URL (i.e. is navigable)
+ state.navigable = url ? state : parent ? parent.navigable : null;
+
+ // Derive parameters for this state and ensure they're a super-set of parent's parameters
+ var params = state.params;
+ if (params) {
+ if (!isArray(params)) throw new Error("Invalid params in state '" + state + "'");
+ if (url) throw new Error("Both params and url specicified in state '" + state + "'");
+ } else {
+ params = state.params = url ? url.parameters() : state.parent.params;
+ }
+
+ var paramNames = {}; forEach(params, function (p) { paramNames[p] = true; });
+ if (parent) {
+ forEach(parent.params, function (p) {
+ if (!paramNames[p]) {
+ throw new Error("Missing required parameter '" + p + "' in state '" + name + "'");
+ }
+ paramNames[p] = false;
+ });
+
+ var ownParams = state.ownParams = [];
+ forEach(paramNames, function (own, p) {
+ if (own) ownParams.push(p);
+ });
+ } else {
+ state.ownParams = params;
+ }
+
+ // If there is no explicit multi-view configuration, make one up so we don't have
+ // to handle both cases in the view directive later. Note that having an explicit
+ // 'views' property will mean the default unnamed view properties are ignored. This
+ // is also a good time to resolve view names to absolute names, so everything is a
+ // straight lookup at link time.
+ var views = {};
+ forEach(isDefined(state.views) ? state.views : { '': state }, function (view, name) {
+ if (name.indexOf('@') < 0) name = name + '@' + state.parent.name;
+ views[name] = view;
+ });
+ state.views = views;
+
+ // Keep a full path from the root down to this state as this is needed for state activation.
+ state.path = parent ? parent.path.concat(state) : []; // exclude root from path
+
+ // Speed up $state.contains() as it's used a lot
+ var includes = state.includes = parent ? extend({}, parent.includes) : {};
+ includes[name] = true;
+
+ if (!state.resolve) state.resolve = {}; // prevent null checks later
+
+ // Register the state in the global state list and with $urlRouter if necessary.
+ if (!state.abstract && url) {
+ $urlRouterProvider.when(url, function (params) {
+ $state.transitionTo(state, params, false);
+ });
+ }
+ states[name] = state;
+ return state;
+ }
+
+ // Implicit root state that is always active
+ root = registerState({
+ name: '',
+ url: '^',
+ views: null,
+ abstract: true
+ });
+ root.locals = { globals: { $stateParams: {} } };
+ root.navigable = null;
+
+
+ // .state(state)
+ // .state(name, state)
+ this.state = state;
+ function state(name, definition) {
+ /*jshint validthis: true */
+ if (isObject(name)) definition = name;
+ else definition.name = name;
+ registerState(definition);
+ return this;
+ }
+
+ // $urlRouter is injected just to ensure it gets instantiated
+ this.$get = $get;
+ $get.$inject = ['$rootScope', '$q', '$templateFactory', '$injector', '$stateParams', '$location', '$urlRouter'];
+ function $get( $rootScope, $q, $templateFactory, $injector, $stateParams, $location, $urlRouter) {
+
+ var TransitionSuperseded = $q.reject(new Error('transition superseded'));
+ var TransitionPrevented = $q.reject(new Error('transition prevented'));
+
+ $state = {
+ params: {},
+ current: root.self,
+ $current: root,
+ transition: null
+ };
+
+ // $state.go = function go(to, params) {
+ // };
+
+ $state.transitionTo = function transitionTo(to, toParams, updateLocation) {
+ if (!isDefined(updateLocation)) updateLocation = true;
+
+ to = findState(to);
+ if (to.abstract) throw new Error("Cannot transition to abstract state '" + to + "'");
+ var toPath = to.path,
+ from = $state.$current, fromParams = $state.params, fromPath = from.path;
+
+ // Starting from the root of the path, keep all levels that haven't changed
+ var keep, state, locals = root.locals, toLocals = [];
+ for (keep = 0, state = toPath[keep];
+ state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams);
+ keep++, state = toPath[keep]) {
+ locals = toLocals[keep] = state.locals;
+ }
+
+ // If we're going to the same state and all locals are kept, we've got nothing to do.
+ // But clear 'transition', as we still want to cancel any other pending transitions.
+ // TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves,
+ // because we might accidentally abort a legitimate transition initiated from code?
+ if (to === from && locals === from.locals) {
+ $state.transition = null;
+ return $q.when($state.current);
+ }
+
+ // Normalize/filter parameters before we pass them to event handlers etc.
+ var normalizedToParams = {};
+ forEach(to.params, function (name) {
+ var value = toParams[name];
+ normalizedToParams[name] = (value != null) ? String(value) : null;
+ });
+ toParams = normalizedToParams;
+
+ // Broadcast start event and cancel the transition if requested
+ if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams)
+ .defaultPrevented) return TransitionPrevented;
+
+ // Resolve locals for the remaining states, but don't update any global state just
+ // yet -- if anything fails to resolve the current state needs to remain untouched.
+ // We also set up an inheritance chain for the locals here. This allows the view directive
+ // to quickly look up the correct definition for each view in the current state. Even
+ // though we create the locals object itself outside resolveState(), it is initially
+ // empty and gets filled asynchronously. We need to keep track of the promise for the
+ // (fully resolved) current locals, and pass this down the chain.
+ var resolved = $q.when(locals);
+ for (var l=keep; l=keep; l--) {
+ exiting = fromPath[l];
+ if (exiting.self.onExit) {
+ $injector.invoke(exiting.self.onExit, exiting.self, exiting.locals.globals);
+ }
+ exiting.locals = null;
+ }
+
+ // Enter 'to' states not kept
+ for (l=keep; l' + name + ' ' + locals.$template);
+ var link = $compile(element.contents());
+ viewScope = scope.$new();
+ if (locals.$$controller) {
+ locals.$scope = viewScope;
+ var controller = $controller(locals.$$controller, locals);
+ element.contents().data('$ngControllerController', controller);
+ }
+ link(viewScope);
+ viewScope.$emit('$viewContentLoaded');
+ viewScope.$eval(onloadExp);
+
+ // TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary?
+ // $anchorScroll might listen on event...
+ $anchorScroll();
+ } else {
+ viewLocals = null;
+ view.state = null;
+ element.html('');
+ }
+ }
+ }
+ };
+ return directive;
+}
+
+angular.module('ui.state').directive('uiView', $ViewDirective);
+
+$RouteProvider.$inject = ['$stateProvider', '$urlRouterProvider'];
+function $RouteProvider( $stateProvider, $urlRouterProvider) {
+
+ var routes = [];
+
+ onEnterRoute.$inject = ['$$state'];
+ function onEnterRoute( $$state) {
+ /*jshint validthis: true */
+ this.locals = $$state.locals.globals;
+ this.params = this.locals.$stateParams;
+ }
+
+ function onExitRoute() {
+ /*jshint validthis: true */
+ this.locals = null;
+ this.params = null;
+ }
+
+ this.when = when;
+ function when(url, route) {
+ /*jshint validthis: true */
+ if (route.redirectTo != null) {
+ // Redirect, configure directly on $urlRouterProvider
+ var redirect = route.redirectTo, handler;
+ if (isString(redirect)) {
+ handler = redirect; // leave $urlRouterProvider to handle
+ } else if (isFunction(redirect)) {
+ // Adapt to $urlRouterProvider API
+ handler = function (params, $location) {
+ return redirect(params, $location.path(), $location.search());
+ };
+ } else {
+ throw new Error("Invalid 'redirectTo' in when()");
+ }
+ $urlRouterProvider.when(url, handler);
+ } else {
+ // Regular route, configure as state
+ $stateProvider.state(inherit(route, {
+ parent: null,
+ name: 'route:' + encodeURIComponent(url),
+ url: url,
+ onEnter: onEnterRoute,
+ onExit: onExitRoute
+ }));
+ }
+ routes.push(route);
+ return this;
+ }
+
+ this.$get = $get;
+ $get.$inject = ['$state', '$rootScope', '$routeParams'];
+ function $get( $state, $rootScope, $routeParams) {
+
+ var $route = {
+ routes: routes,
+ params: $routeParams,
+ current: undefined
+ };
+
+ function stateAsRoute(state) {
+ return (state.name !== '') ? state : undefined;
+ }
+
+ $rootScope.$on('$stateChangeStart', function (ev, to, toParams, from, fromParams) {
+ $rootScope.$broadcast('$routeChangeStart', stateAsRoute(to), stateAsRoute(from));
+ });
+
+ $rootScope.$on('$stateChangeSuccess', function (ev, to, toParams, from, fromParams) {
+ $route.current = stateAsRoute(to);
+ $rootScope.$broadcast('$routeChangeSuccess', stateAsRoute(to), stateAsRoute(from));
+ copy(toParams, $route.params);
+ });
+
+ $rootScope.$on('$stateChangeError', function (ev, to, toParams, from, fromParams, error) {
+ $rootScope.$broadcast('$routeChangeError', stateAsRoute(to), stateAsRoute(from), error);
+ });
+
+ return $route;
+ }
+}
+
+angular.module('ui.compat')
+ .provider('$route', $RouteProvider)
+ .directive('ngView', $ViewDirective);
diff --git a/src/main/java/com/commafeed/frontend/references/angularuistate/angular-ui-states.min.js b/src/main/java/com/commafeed/frontend/references/angularuistate/angular-ui-states.min.js
new file mode 100644
index 00000000..b0d04919
--- /dev/null
+++ b/src/main/java/com/commafeed/frontend/references/angularuistate/angular-ui-states.min.js
@@ -0,0 +1 @@
+"use strict";function inherit(r,t){return extend(new(extend(function(){},{prototype:r})),t)}function merge(r){return forEach(arguments,function(t){t!==r&&forEach(t,function(t,e){r.hasOwnProperty(e)||(r[e]=t)})}),r}function $TemplateFactory(r,t,e){this.fromConfig=function(r,t,e){return isDefined(r.template)?this.fromString(r.template,t):isDefined(r.templateUrl)?this.fromUrl(r.templateUrl,t):isDefined(r.templateProvider)?this.fromProvider(r.templateProvider,t,e):null},this.fromString=function(r,t){return isFunction(r)?r(t):r},this.fromUrl=function(e,n){return isFunction(e)&&(e=e(n)),null==e?null:r.get(e,{cache:t}).then(function(r){return r.data})},this.fromProvider=function(r,t,n){return e.invoke(r,null,n||{params:t})}}function UrlMatcher(r){function t(t){if(!/^\w+$/.test(t))throw Error("Invalid parameter name '"+t+"' in pattern '"+r+"'");if(a[t])throw Error("Duplicate parameter name '"+t+"' in pattern '"+r+"'");a[t]=!0,l.push(t)}function e(r){return r.replace(/[\\\[\]\^$*+?.()|{}]/g,"\\$&")}var n,i=/([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,a={},o="^",s=0,u=this.segments=[],l=this.params=[];this.source=r;for(var c,f,h;(n=i.exec(r))&&(c=n[2]||n[3],f=n[4]||("*"==n[1]?".*":"[^/]*"),h=r.substring(s,n.index),!(h.indexOf("?")>=0));)o+=e(h)+"("+f+")",t(c),u.push(h),s=i.lastIndex;h=r.substring(s);var $=h.indexOf("?");if($>=0){var p=this.sourceSearch=h.substring($);h=h.substring(0,$),this.sourcePath=r.substring(0,s+$),forEach(p.substring(1).split(/[&?]/),t)}else this.sourcePath=r,this.sourceSearch="";o+=e(h)+"$",u.push(h),this.regexp=RegExp(o),this.prefix=u[0]}function $UrlMatcherFactory(){this.compile=function(r){return new UrlMatcher(r)},this.isMatcher=function(r){return r instanceof UrlMatcher},this.$get=function(){return this}}function $UrlRouterProvider(r){function t(r){var t=/^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(r.source);return null!=t?t[1].replace(/\\(.)/g,"$1"):""}function e(r,t){return r.replace(/\$(\$|\d{1,2})/,function(r,e){return t["$"===e?0:Number(e)]})}function n(r,t,e){if(!e)return!1;var n=t(e,r);return isDefined(n)?n:!0}var i=[],a=null;this.rule=function(r){if(!isFunction(r))throw Error("'rule' must be a function");return i.push(r),this},this.otherwise=function(r){if(isString(r)){var t=r;r=function(){return t}}else if(!isFunction(r))throw Error("'rule' must be a function");return a=r,this},this.when=function(i,a){var o,s;if(isString(i)&&(i=r.compile(i)),r.isMatcher(i)){if(isString(a))s=r.compile(a),a=function(r){return s.format(r)};else if(!isFunction(a))throw Error("invalid 'handler' in when()");o=function(r){return n(r,a,i.exec(r.path(),r.search()))},o.prefix=isString(i.prefix)?i.prefix:""}else{if(!(i instanceof RegExp))throw Error("invalid 'what' in when()");if(isString(a))s=a,a=function(r){return e(s,r)};else if(!isFunction(a))throw Error("invalid 'handler' in when()");if(i.global||i.sticky)throw Error("when() RegExp must not be global or sticky");o=function(r){return n(r,a,i.exec(r.path()))},o.prefix=t(i)}return this.rule(o)},this.$get=["$location","$rootScope",function(r,t){function e(){var t,e,n=i.length;for(t=0;n>t;t++)if(e=i[t](r)){isString(e)&&r.replace().url(e);break}}return a&&i.push(a),t.$on("$locationChangeSuccess",e),{}}]}function $StateProvider(r,t){function e(r){var t;if(isString(r)){if(t=u[r],!t)throw Error("No such state '"+r+"'")}else if(t=u[r.name],!t||t!==r&&t.self!==r)throw Error("Invalid or unregistered state");return t}function n(n){n=inherit(n,{self:n,toString:function(){return this.name}});var i=n.name;if(!isString(i)||i.indexOf("@")>=0)throw Error("State must have a valid name");if(u[i])throw Error("State '"+i+"'' is already defined");var a=o;if(isDefined(n.parent))null!=n.parent&&(a=e(n.parent));else{var l=/^(.+)\.[^.]+$/.exec(i);null!=l&&(a=e(l[1]))}n.parent=a;var c=n.url;if(isString(c))c=n.url="^"==c.charAt(0)?t.compile(c.substring(1)):(a.navigable||o).url.concat(c);else if(isObject(c)&&isFunction(c.exec)&&isFunction(c.format)&&isFunction(c.concat));else if(null!=c)throw Error("Invalid url '"+c+"' in state '"+n+"'");n.navigable=c?n:a?a.navigable:null;var f=n.params;if(f){if(!isArray(f))throw Error("Invalid params in state '"+n+"'");if(c)throw Error("Both params and url specicified in state '"+n+"'")}else f=n.params=c?c.parameters():n.parent.params;var h={};if(forEach(f,function(r){h[r]=!0}),a){forEach(a.params,function(r){if(!h[r])throw Error("Missing required parameter '"+r+"' in state '"+i+"'");h[r]=!1});var $=n.ownParams=[];forEach(h,function(r,t){r&&$.push(t)})}else n.ownParams=f;var p={};forEach(isDefined(n.views)?n.views:{"":n},function(r,t){0>t.indexOf("@")&&(t=t+"@"+n.parent.name),p[t]=r}),n.views=p,n.path=a?a.path.concat(n):[];var m=n.includes=a?extend({},a.includes):{};return m[i]=!0,n.resolve||(n.resolve={}),!n.abstract&&c&&r.when(c,function(r){s.transitionTo(n,r,!1)}),u[i]=n,n}function i(r,t){return isObject(r)?t=r:t.name=r,n(t),this}function a(r,t,n,i,a,u){function l(r,e,a,o,s){function u(e,n){forEach(e,function(e,a){c.push(t.when(isString(e)?i.get(e):i.invoke(e,r.self,f)).then(function(r){n[a]=r}))})}var l,c=[o];a?l=e:(l={},forEach(r.params,function(r){l[r]=e[r]}));var f={$stateParams:l},h=s.globals={$stateParams:l};return u(r.resolve,h),h.$$state=r,forEach(r.views,function(e,i){var a=s[i]={$$controller:e.controller};c.push(t.when(n.fromConfig(e,l,f)||"").then(function(r){a.$template=r})),e.resolve!==r.resolve&&u(e.resolve,a)}),t.all(c).then(function(t){return merge(s.globals,t[0].globals),forEach(r.views,function(r,t){merge(s[t],s.globals)}),s})}function c(r,t,e){for(var n=0;e.length>n;n++){var i=e[n];if(r[i]!=t[i])return!1}return!0}var f=t.reject(Error("transition superseded")),h=t.reject(Error("transition prevented"));return s={params:{},current:o.self,$current:o,transition:null},s.transitionTo=function(n,$,p){if(isDefined(p)||(p=!0),n=e(n),n.abstract)throw Error("Cannot transition to abstract state '"+n+"'");var m,v,d=n.path,g=s.$current,w=s.params,E=g.path,b=o.locals,S=[];for(m=0,v=d[m];v&&v===E[m]&&c($,w,v.ownParams);m++,v=d[m])b=S[m]=v.locals;if(n===g&&b===g.locals)return s.transition=null,t.when(s.current);var P={};if(forEach(n.params,function(r){var t=$[r];P[r]=null!=t?t+"":null}),$=P,r.$broadcast("$stateChangeStart",n.self,$,g.self,w).defaultPrevented)return h;for(var x=t.when(b),y=m;d.length>y;y++,v=d[y])b=S[y]=inherit(b),x=l(v,$,v===n,x,b);var C=s.transition=x.then(function(){var t,e,o;if(s.transition!==C)return f;for(t=E.length-1;t>=m;t--)o=E[t],o.self.onExit&&i.invoke(o.self.onExit,o.self,o.locals.globals),o.locals=null;for(t=m;d.length>t;t++)e=d[t],e.locals=S[t],e.self.onEnter&&i.invoke(e.self.onEnter,e.self,e.locals.globals);s.$current=n,s.current=n.self,s.params=$,copy(s.params,a),s.transition=null;var l=n.navigable;return p&&l&&u.url(l.url.format(l.locals.globals.$stateParams)),r.$broadcast("$stateChangeSuccess",n.self,$,g.self,w),s.current},function(e){return s.transition!==C?f:(s.transition=null,r.$broadcast("$stateChangeError",n.self,$,g.self,w,e),t.reject(e))});return C},s.is=function(r){return s.$current===e(r)},s.includes=function(r){return s.$current.includes[e(r).name]},s}var o,s,u={};o=n({name:"",url:"^",views:null,"abstract":!0}),o.locals={globals:{$stateParams:{}}},o.navigable=null,this.state=i,this.$get=a,a.$inject=["$rootScope","$q","$templateFactory","$injector","$stateParams","$location","$urlRouter"]}function $ViewDirective(r,t,e,n){var i={restrict:"ECA",terminal:!0,link:function(a,o,s){function u(){var i=r.$current&&r.$current.locals[f];if(i!==c)if(l&&(l.$destroy(),l=null),i){c=i,p.state=i.$$state,o.html(i.$template);var s=t(o.contents());if(l=a.$new(),i.$$controller){i.$scope=l;var u=e(i.$$controller,i);o.contents().data("$ngControllerController",u)}s(l),l.$emit("$viewContentLoaded"),l.$eval(h),n()}else c=null,p.state=null,o.html("")}var l,c,f=s[i.name]||s.name||"",h=s.onload||"",$=o.parent().inheritedData("$uiView");0>f.indexOf("@")&&(f=f+"@"+($?$.state.name:""));var p={name:f,state:null};o.data("$uiView",p),a.$on("$stateChangeSuccess",u),u()}};return i}function $RouteProvider(r,t){function e(r){this.locals=r.locals.globals,this.params=this.locals.$stateParams}function n(){this.locals=null,this.params=null}function i(i,a){if(null!=a.redirectTo){var s,u=a.redirectTo;if(isString(u))s=u;else{if(!isFunction(u))throw Error("Invalid 'redirectTo' in when()");s=function(r,t){return u(r,t.path(),t.search())}}t.when(i,s)}else r.state(inherit(a,{parent:null,name:"route:"+encodeURIComponent(i),url:i,onEnter:e,onExit:n}));return o.push(a),this}function a(r,t,e){function n(r){return""!==r.name?r:void 0}var i={routes:o,params:e,current:void 0};return t.$on("$stateChangeStart",function(r,e,i,a){t.$broadcast("$routeChangeStart",n(e),n(a))}),t.$on("$stateChangeSuccess",function(r,e,a,o){i.current=n(e),t.$broadcast("$routeChangeSuccess",n(e),n(o)),copy(a,i.params)}),t.$on("$stateChangeError",function(r,e,i,a,o,s){t.$broadcast("$routeChangeError",n(e),n(a),s)}),i}var o=[];e.$inject=["$$state"],this.when=i,this.$get=a,a.$inject=["$state","$rootScope","$routeParams"]}var isDefined=angular.isDefined,isFunction=angular.isFunction,isString=angular.isString,isObject=angular.isObject,isArray=angular.isArray,forEach=angular.forEach,extend=angular.extend,copy=angular.copy;angular.module("ui.util",["ng"]),angular.module("ui.router",["ui.util"]),angular.module("ui.state",["ui.router","ui.util"]),angular.module("ui.compat",["ui.state"]),$TemplateFactory.$inject=["$http","$templateCache","$injector"],angular.module("ui.util").service("$templateFactory",$TemplateFactory),UrlMatcher.prototype.concat=function(r){return new UrlMatcher(this.sourcePath+r+this.sourceSearch)},UrlMatcher.prototype.toString=function(){return this.source},UrlMatcher.prototype.exec=function(r,t){var e=this.regexp.exec(r);if(!e)return null;var n,i=this.params,a=i.length,o=this.segments.length-1,s={};for(n=0;o>n;n++)s[i[n]]=decodeURIComponent(e[n+1]);for(;a>n;n++)s[i[n]]=t[i[n]];return s},UrlMatcher.prototype.parameters=function(){return this.params},UrlMatcher.prototype.format=function(r){var t=this.segments,e=this.params;if(!r)return t.join("");var n,i,a,o=t.length-1,s=e.length,u=t[0];for(n=0;o>n;n++)a=r[e[n]],null!=a&&(u+=a),u+=t[n+1];for(;s>n;n++)a=r[e[n]],null!=a&&(u+=(i?"&":"?")+e[n]+"="+encodeURIComponent(a),i=!0);return u},angular.module("ui.util").provider("$urlMatcherFactory",$UrlMatcherFactory),$UrlRouterProvider.$inject=["$urlMatcherFactoryProvider"],angular.module("ui.router").provider("$urlRouter",$UrlRouterProvider),$StateProvider.$inject=["$urlRouterProvider","$urlMatcherFactoryProvider"],angular.module("ui.state").value("$stateParams",{}).provider("$state",$StateProvider),$ViewDirective.$inject=["$state","$compile","$controller","$anchorScroll"],angular.module("ui.state").directive("uiView",$ViewDirective),$RouteProvider.$inject=["$stateProvider","$urlRouterProvider"],angular.module("ui.compat").provider("$route",$RouteProvider).directive("ngView",$ViewDirective);
\ No newline at end of file
diff --git a/src/main/webapp/css/app.css b/src/main/webapp/css/app.css
index f572afe1..449fd5d2 100644
--- a/src/main/webapp/css/app.css
+++ b/src/main/webapp/css/app.css
@@ -64,11 +64,6 @@
text-overflow: ellipsis;
}
-.css-treeview a:hover {
- cursor: pointer;
- color: black;
-}
-
/* entry list*/
.entrylist-header {
border-bottom: 1px solid #eee;
diff --git a/src/main/webapp/directives/toolbar.html b/src/main/webapp/directives/toolbar.html
index 136e8813..00dc2459 100644
--- a/src/main/webapp/directives/toolbar.html
+++ b/src/main/webapp/directives/toolbar.html
@@ -13,6 +13,7 @@