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 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 @@ diff --git a/src/main/webapp/js/controllers.js b/src/main/webapp/js/controllers.js index 9f4748c9..164f2258 100644 --- a/src/main/webapp/js/controllers.js +++ b/src/main/webapp/js/controllers.js @@ -5,14 +5,17 @@ module.run(function($rootScope) { // args.entry - the entry $rootScope.$broadcast('mark', args); }); + $rootScope.$on('emitReload', function(event, args) { + $rootScope.$broadcast('reload'); + }); }); -module.controller('CategoryTreeCtrl', function($scope, $routeParams, $location, - $route, SubscriptionService) { +module.controller('CategoryTreeCtrl', function($scope, $stateParams, $location, + $state, $route, SubscriptionService) { - $scope.$on('$routeChangeSuccess', function() { - $scope.selectedType = $routeParams._type; - $scope.selectedId = $routeParams._id; + $scope.$on('$stateChangeSuccess', function() { + $scope.selectedType = $stateParams._type; + $scope.selectedId = $stateParams._id; }); $scope.SubscriptionService = SubscriptionService; @@ -52,17 +55,23 @@ module.controller('CategoryTreeCtrl', function($scope, $routeParams, $location, $scope.feedClicked = function(id) { if ($scope.selectedType == 'feed' && id == $scope.selectedId) { - $route.reload(); + $scope.$emit('emitReload'); } else { - $location.path('/feeds/view/feed/' + id); + $state.transitionTo('feeds.view', { + _type : 'feed', + _id : id + }); } }; $scope.categoryClicked = function(id) { if ($scope.selectedType == 'category' && id == $scope.selectedId) { - $route.reload(); + $scope.$emit('emitReload'); } else { - $location.path('/feeds/view/category/' + id); + $state.transitionTo('feeds.view', { + _type : 'category', + _id : id + }); } }; @@ -88,11 +97,11 @@ module.controller('CategoryTreeCtrl', function($scope, $routeParams, $location, }); }); -module.controller('FeedListCtrl', function($scope, $routeParams, $http, $route, +module.controller('FeedListCtrl', function($scope, $stateParams, $http, $route, $window, EntryService, SettingsService) { - $scope.selectedType = $routeParams._type; - $scope.selectedId = $routeParams._id; + $scope.selectedType = $stateParams._type; + $scope.selectedId = $stateParams._id; $scope.name = null; $scope.entries = []; @@ -101,7 +110,7 @@ module.controller('FeedListCtrl', function($scope, $routeParams, $http, $route, $scope.$watch('settingsService.settings.readingMode', function(newValue, oldValue) { if (newValue && oldValue && newValue != oldValue) { - $route.reload(); + $scope.$emit('emitReload'); } }); @@ -219,7 +228,7 @@ module.controller('FeedListCtrl', function($scope, $routeParams, $http, $route, openNextEntry(e); }) }); - + Mousetrap.bind('shift+space', function(e) { $scope.$apply(function() { openPreviousEntry(e); @@ -230,4 +239,12 @@ module.controller('FeedListCtrl', function($scope, $routeParams, $http, $route, openPreviousEntry(e); }) }); + + $scope.$on('reload', function(event, args) { + $scope.name = null; + $scope.entries = []; + $scope.busy = false; + $scope.hasMore = true; + $scope.loadMoreEntries(); + }); }); \ No newline at end of file diff --git a/src/main/webapp/js/directives.js b/src/main/webapp/js/directives.js index 273a94bb..3bcefc63 100644 --- a/src/main/webapp/js/directives.js +++ b/src/main/webapp/js/directives.js @@ -122,8 +122,8 @@ module.directive('category', function($compile) { }; }); -module.directive('toolbar', function($routeParams, $route, SettingsService, - EntryService, SubscriptionService) { +module.directive('toolbar', function($stateParams, $route, $location, + SettingsService, EntryService, SubscriptionService) { return { scope : {}, restrict : 'E', @@ -142,19 +142,22 @@ module.directive('toolbar', function($routeParams, $route, SettingsService, $scope.settingsService = SettingsService; $scope.refresh = function() { - $route.reload(); + $scope.$emit('emitReload'); }; $scope.markAllAsRead = function() { EntryService.mark({ - type : $routeParams._type, - id : $routeParams._id, + type : $stateParams._type, + id : $stateParams._id, read : true, }, function() { SubscriptionService.init(function() { - $route.reload(); + $scope.$emit('emitReload'); }); }); - } + }; + $scope.toAdmin = function() { + $location.path('admin'); + }; }, link : function($scope, element) { element.find('.read-mode button').bind('click', function() { diff --git a/src/main/webapp/js/main.js b/src/main/webapp/js/main.js index 009e1a1b..88246dce 100644 --- a/src/main/webapp/js/main.js +++ b/src/main/webapp/js/main.js @@ -1,12 +1,28 @@ -var app = angular.module('commafeed', [ 'ui', 'ui.bootstrap', - 'commafeed.directives', 'commafeed.controllers', 'commafeed.services', +var app = angular.module('commafeed', [ 'ui', 'ui.bootstrap', 'ui.state', + 'commafeed.directives', 'commafeed.controllers', 'commafeed.services', 'ngSanitize', 'ngUpload', 'infinite-scroll' ]); -app.config([ '$routeProvider', function($routeProvider) { - $routeProvider.when('/feeds/view/:_type/:_id', { - templateUrl : 'templates/feeds.html' +app.config(function($urlRouterProvider, $stateProvider) { + $stateProvider.state('feeds', { + abstract: true, + url: '/feeds', + templateUrl: 'templates/feeds.html' }); - $routeProvider.otherwise({ - redirectTo : '/feeds/view/category/all' + $stateProvider.state('feeds.view', { + url: '/view/:_type/:_id', + templateUrl : 'templates/feeds.view.html' }); -} ]); \ No newline at end of file + + $stateProvider.state('admin', { + abstract: true, + url: '/admin', + templateUrl: 'templates/admin.html' + }); + $stateProvider.state('admin.users', { + url: '/users', + templateUrl : 'templates/admin.users.html' + }); + + $urlRouterProvider.when('/', '/feeds/view/category/all'); + $urlRouterProvider.when('/admin', '/admin/users'); +}); \ No newline at end of file diff --git a/src/main/webapp/templates/admin.html b/src/main/webapp/templates/admin.html new file mode 100644 index 00000000..f182cddd --- /dev/null +++ b/src/main/webapp/templates/admin.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/main/webapp/templates/admin.users.html b/src/main/webapp/templates/admin.users.html new file mode 100644 index 00000000..e63103de --- /dev/null +++ b/src/main/webapp/templates/admin.users.html @@ -0,0 +1 @@ +Manage users \ No newline at end of file diff --git a/src/main/webapp/templates/feeds.html b/src/main/webapp/templates/feeds.html index cf614dbe..73713948 100644 --- a/src/main/webapp/templates/feeds.html +++ b/src/main/webapp/templates/feeds.html @@ -1,27 +1,24 @@ -
- -
-

{{name}}  »

-
- -
-
- - {{entry.feedName}} - - - - -
-
-

- -

-
-
+
+
+ +
+
+ +
+
+
-
"{{name}}" has no unread items.
\ No newline at end of file diff --git a/src/main/webapp/templates/feeds.view.html b/src/main/webapp/templates/feeds.view.html new file mode 100644 index 00000000..cf614dbe --- /dev/null +++ b/src/main/webapp/templates/feeds.view.html @@ -0,0 +1,27 @@ +
+ +
+

{{name}}  »

+
+ +
+
+ + {{entry.feedName}} + + + + +
+
+

+ +

+
+
+
+
+
"{{name}}" has no unread items.
+
+
\ No newline at end of file