├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── be │ └── aca │ └── liferay │ └── angular │ └── portlet │ ├── AngularPortlet.java │ └── resource │ ├── ResourceCacheLoader.java │ ├── ResourceContext.java │ ├── ResourceMethodExecutor.java │ ├── ResourcePortlet.java │ └── annotation │ ├── CacheResource.java │ ├── Context.java │ ├── Param.java │ └── Resource.java ├── resources ├── Language.properties ├── Language_el.properties ├── Language_nl.properties └── logback.groovy └── webapp ├── WEB-INF ├── liferay-display.xml ├── liferay-hook.xml ├── liferay-plugin-package.properties ├── liferay-portlet.xml ├── portlet.xml ├── web.xml └── wro.groovy ├── css ├── font-awesome.css └── style.css ├── custom_jsps └── html │ └── common │ └── themes │ └── top_head.jsp ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.svg ├── fontawesome-webfont.ttf ├── fontawesome-webfont.woff └── fontawesome-webfont.woff2 ├── img └── icon.png ├── js ├── angular-translate-loader-url.js ├── angular-translate.js ├── angular-ui-router.js ├── angular.js ├── controller │ ├── AddController.js │ ├── DetailController.js │ ├── Init.js │ └── ListController.js ├── directive │ ├── Init.js │ └── LiferayDirective.js ├── jcs-auto-validate.js ├── main.js └── service │ ├── ErrorMessageResolver.js │ ├── Init.js │ ├── bookmarkFactory.js │ ├── releaseFactory.js │ └── urlFactory.js ├── jsp ├── edit.jsp └── view.jsp └── partials ├── add.html ├── detail.html ├── liferay.html └── list.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target 3 | *.iml 4 | .idea 5 | 6 | # Mobile Tools for Java (J2ME) 7 | .mtj.tmp/ 8 | 9 | # Package Files # 10 | *.jar 11 | *.war 12 | *.ear 13 | 14 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 15 | hs_err_pid* 16 | 17 | #eclipse stuff 18 | /.project 19 | /.classpath 20 | /.settings/ 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 planetsizebrain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-portlet 2 | =============== 3 | 4 | An example portlet that shows how AngularJS can be used in a portal environment. It shows how the following thing can be done: 5 | 6 | * Multiple instances of the same Angular portlet (requires bootstrapping Angular yourself) 7 | * How to get ngRoute to work when direct URL access isn't possible 8 | * Using Liferay JSON services in AngularJS 9 | * How to reference partial HTML files (create correct portlet resource URL) 10 | * Portlet event integration in AngularJS 11 | * i18n via angular-translate that uses the Liferay and portlet resource bundles 12 | * Providing your own JSON services using a portlet resource request (and some fancy custom code so it can be done using annotations, complete with caching) 13 | * How to avoid the one big Javascript file problem in a Maven project without using Grunt, etc... 14 | * A custom AngularJS directive (referencing a partial HTML as a template) 15 | 16 | As mentioned the portlet contains some additional code that isn't specifically needed for the AngularJS part, but that does some nice stuff that makes exposing JSON stuff from your portlet easier. 17 | 18 | For this there is a base portlet class that you can extend, ResourcePortlet (which itself extends from Liferay's MVCPortlet), that enables you to just annotate some simple methods in your portlet as being resource providers. 19 | 20 | The annotations that accomplish this are: 21 | 22 | * @Resource 23 | * @Param 24 | * @Context 25 | * @CacheResource 26 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | be.aca.liferay 4 | angular 5 | 1.1.0-SNAPSHOT 6 | war 7 | Liferay Angular Portlet 8 | Liferay Angular Portlet 9 | https://github.com/planetsizebrain/angular-portlet 10 | 11 | 12 | UTF-8 13 | 1.7 14 | 6.2.1 15 | 1.1.2 16 | 17 | 18 | 19 | angular-portlet 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-compiler-plugin 24 | 3.1 25 | 26 | ${jvm.version} 27 | ${jvm.version} 28 | UTF-8 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-war-plugin 34 | 2.6 35 | 36 | 40 | js/controller/*.js,js/service/*.js,js/directive/*.js 41 | 42 | 43 | 44 | 48 | ro.isdc.wro4j 49 | wro4j-maven-plugin 50 | 1.7.7 51 | 52 | 53 | compile 54 | 55 | run 56 | 57 | 58 | 59 | 60 | 61 | controllers,services,directives 62 | ${project.build.directory}/${project.build.finalName}/js 63 | ${basedir}/src/main/webapp/ 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-antrun-plugin 69 | 1.8 70 | 71 | 72 | compile 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | run 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | com.liferay.portal 94 | portal-service 95 | ${liferay.version} 96 | provided 97 | 98 | 99 | com.liferay.portal 100 | util-java 101 | ${liferay.version} 102 | provided 103 | 104 | 105 | com.liferay.portal 106 | util-taglib 107 | ${liferay.version} 108 | provided 109 | 110 | 111 | com.liferay.portal 112 | util-bridges 113 | ${liferay.version} 114 | provided 115 | 116 | 117 | 118 | 119 | javax.servlet 120 | servlet-api 121 | 2.5 122 | provided 123 | 124 | 125 | javax.portlet 126 | portlet-api 127 | 2.0 128 | provided 129 | 130 | 131 | javax.servlet.jsp 132 | jsp-api 133 | 2.0 134 | provided 135 | 136 | 137 | 138 | 139 | com.google.code.gson 140 | gson 141 | 2.2.4 142 | 143 | 144 | com.google.guava 145 | guava 146 | 18.0 147 | 148 | 149 | org.reflections 150 | reflections 151 | 0.9.9-RC1 152 | 153 | 154 | org.springframework 155 | spring-core 156 | 3.2.0.RELEASE 157 | 158 | 159 | 160 | 161 | org.slf4j 162 | slf4j-api 163 | 1.7.10 164 | 165 | 166 | ch.qos.logback 167 | logback-classic 168 | ${logback.version} 169 | 170 | 171 | ch.qos.logback 172 | logback-core 173 | ${logback.version} 174 | 175 | 176 | org.codehaus.groovy 177 | groovy 178 | 2.3.5 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/AngularPortlet.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet; 2 | 3 | import be.aca.liferay.angular.portlet.resource.*; 4 | import be.aca.liferay.angular.portlet.resource.annotation.CacheResource; 5 | import be.aca.liferay.angular.portlet.resource.annotation.Param; 6 | import be.aca.liferay.angular.portlet.resource.annotation.Resource; 7 | import com.google.common.base.Strings; 8 | import com.liferay.portal.kernel.exception.PortalException; 9 | import com.liferay.portal.kernel.exception.SystemException; 10 | import com.liferay.portal.kernel.servlet.SessionMessages; 11 | import com.liferay.portal.kernel.util.PortalClassLoaderUtil; 12 | import com.liferay.portal.kernel.util.StringPool; 13 | import com.liferay.portal.model.Release; 14 | import com.liferay.portal.service.ReleaseLocalServiceUtil; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | import javax.portlet.*; 19 | import java.lang.reflect.Field; 20 | import java.util.Locale; 21 | import java.util.Map; 22 | 23 | public class AngularPortlet extends ResourcePortlet { 24 | 25 | private static final Locale DEFAULT_LIFERAY_LOCALE = new Locale(StringPool.BLANK); 26 | 27 | private final static Logger logger = LoggerFactory.getLogger(AngularPortlet.class); 28 | 29 | @ProcessAction(name = "clearCache") 30 | public void clearCache(ActionRequest actionRequest, ActionResponse actionResponse) { 31 | resourceCache.invalidateAll(); 32 | SessionMessages.add(actionRequest, "cache-cleared"); 33 | } 34 | 35 | @Resource(id = "release") 36 | public Release getRelease(@Param String releaseId) throws SystemException, PortalException { 37 | long releaseIdValue = Long.parseLong(releaseId); 38 | return ReleaseLocalServiceUtil.getRelease(releaseIdValue); 39 | } 40 | 41 | @Resource(id = "language") 42 | @CacheResource(keyParam = "locale") 43 | public Map getLanguage(@Param String locale) throws Exception { 44 | logger.debug("Get language bundle for locale {}", locale); 45 | 46 | Locale localeValue = DEFAULT_LIFERAY_LOCALE; 47 | if (!Strings.isNullOrEmpty(locale)) { 48 | localeValue = Locale.forLanguageTag(locale); 49 | } 50 | 51 | // Some ugly code, using the Liferay portal classloader to get at the language map 52 | // that contains a cached resource bundle for the active locales 53 | ClassLoader portalClassLoader = PortalClassLoaderUtil.getClassLoader(); 54 | 55 | Class c = portalClassLoader.loadClass("com.liferay.portal.language.LanguageResources"); 56 | Field f = c.getDeclaredField("_languageMaps"); 57 | f.setAccessible(true); 58 | 59 | Map> bundles = (Map>) f.get(null); 60 | 61 | return bundles.get(localeValue); 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/ResourceCacheLoader.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource; 2 | 3 | import be.aca.liferay.angular.portlet.resource.annotation.Context; 4 | import be.aca.liferay.angular.portlet.resource.annotation.Param; 5 | import com.google.common.cache.CacheLoader; 6 | import com.google.common.collect.Lists; 7 | import com.google.gson.Gson; 8 | import com.liferay.portal.kernel.util.StringPool; 9 | import org.springframework.core.LocalVariableTableParameterNameDiscoverer; 10 | import org.springframework.core.ParameterNameDiscoverer; 11 | 12 | import javax.portlet.ResourceRequest; 13 | import java.lang.annotation.Annotation; 14 | import java.lang.reflect.Method; 15 | import java.util.List; 16 | 17 | public class ResourceCacheLoader extends CacheLoader { 18 | 19 | @Override 20 | public String load(ResourceContext context) throws Exception { 21 | Method method = context.getMethod(); 22 | Annotation[][] paramAnnotations = method.getParameterAnnotations(); 23 | // http://www.beyondjava.net/blog/reading-java-8-method-parameter-named-reflection/ 24 | ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); 25 | String[] names = parameterNameDiscoverer.getParameterNames(method); 26 | Class[] types = method.getParameterTypes(); 27 | 28 | // http://stackoverflow.com/questions/15139424/not-able-to-invoke-main-method-using-reflection-illegalargumentexception-argu 29 | List values = Lists.newArrayList(); 30 | for (int i = 0; i < paramAnnotations.length; i++) { 31 | // http://tutorials.jenkov.com/java-reflection/annotations.html#parameter 32 | String name = names[i]; 33 | Class type = types[i]; 34 | 35 | Annotation[] annotations = paramAnnotations[i]; 36 | 37 | if (annotations.length > 0) { 38 | for (Annotation annotation : annotations) { 39 | // http://stackoverflow.com/questions/3348363/checking-if-an-annotation-is-of-a-specific-type 40 | if (annotation instanceof Param) { 41 | String value = context.getRequest().getParameter(name); 42 | values.add(value); 43 | } 44 | if (annotation instanceof Context) { 45 | if (type.isAssignableFrom(ResourceRequest.class)) { 46 | values.add(context.getRequest()); 47 | } else { 48 | values.add(context.getResponse()); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | Object result = method.invoke(context.getPortlet(), values.toArray()); 56 | 57 | String json = StringPool.BLANK; 58 | if (result != null) { 59 | Gson gson = new Gson(); 60 | json = gson.toJson(result); 61 | } 62 | 63 | return json; 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/ResourceContext.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource; 2 | 3 | import com.google.common.base.Joiner; 4 | import com.google.common.base.Objects; 5 | import com.liferay.portal.kernel.util.StringPool; 6 | 7 | import javax.portlet.ResourceRequest; 8 | import javax.portlet.ResourceResponse; 9 | import java.lang.reflect.Method; 10 | 11 | public final class ResourceContext { 12 | 13 | private ResourcePortlet portlet; 14 | private ResourceRequest request; 15 | private ResourceResponse response; 16 | private Method method; 17 | private String resourceId; 18 | private String paramValue; 19 | 20 | public ResourceContext(ResourcePortlet portlet, ResourceRequest request, ResourceResponse response, Method method, String resourceId) { 21 | this.portlet = portlet; 22 | this.request = request; 23 | this.response = response; 24 | this.method = method; 25 | this.resourceId = resourceId; 26 | } 27 | 28 | public ResourcePortlet getPortlet() { 29 | return portlet; 30 | } 31 | 32 | public ResourceRequest getRequest() { 33 | return request; 34 | } 35 | 36 | public ResourceResponse getResponse() { 37 | return response; 38 | } 39 | 40 | public String getKey() { 41 | return Joiner.on(StringPool.DASH).join(resourceId, paramValue); 42 | } 43 | 44 | public Method getMethod() { 45 | return method; 46 | } 47 | 48 | public void setParamValue(String paramValue) { 49 | this.paramValue = paramValue; 50 | } 51 | 52 | @Override 53 | public boolean equals(Object obj) { 54 | if (this == obj) { 55 | return true; 56 | } 57 | if (obj == null || getClass() != obj.getClass()) { 58 | return false; 59 | } 60 | 61 | final ResourceContext other = (ResourceContext) obj; 62 | 63 | return Objects.equal(this.paramValue, other.paramValue) && 64 | Objects.equal(this.resourceId, other.resourceId); 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | return Objects.hashCode(resourceId, paramValue); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/ResourceMethodExecutor.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource; 2 | 3 | import be.aca.liferay.angular.portlet.resource.annotation.Context; 4 | import be.aca.liferay.angular.portlet.resource.annotation.Param; 5 | import com.google.common.cache.CacheLoader; 6 | import com.google.common.collect.Lists; 7 | import com.google.gson.Gson; 8 | import com.liferay.portal.kernel.util.StringPool; 9 | import org.springframework.core.LocalVariableTableParameterNameDiscoverer; 10 | import org.springframework.core.ParameterNameDiscoverer; 11 | 12 | import javax.portlet.ResourceRequest; 13 | import java.lang.annotation.Annotation; 14 | import java.lang.reflect.Method; 15 | import java.util.List; 16 | 17 | /** 18 | * Tries to determine the correct method to call on the given portlet that matches the 19 | * values in the given context and returns the result as a JSON string. 20 | */ 21 | public class ResourceMethodExecutor { 22 | 23 | public String process(ResourceContext context) throws Exception { 24 | Method method = context.getMethod(); 25 | Annotation[][] paramAnnotations = method.getParameterAnnotations(); 26 | // http://www.beyondjava.net/blog/reading-java-8-method-parameter-named-reflection/ 27 | ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); 28 | String[] names = parameterNameDiscoverer.getParameterNames(method); 29 | Class[] types = method.getParameterTypes(); 30 | 31 | // http://stackoverflow.com/questions/15139424/not-able-to-invoke-main-method-using-reflection-illegalargumentexception-argu 32 | List values = Lists.newArrayList(); 33 | for (int i = 0; i < paramAnnotations.length; i++) { 34 | // http://tutorials.jenkov.com/java-reflection/annotations.html#parameter 35 | String name = names[i]; 36 | Class type = types[i]; 37 | 38 | Annotation[] annotations = paramAnnotations[i]; 39 | 40 | if (annotations.length > 0) { 41 | for (Annotation annotation : annotations) { 42 | // http://stackoverflow.com/questions/3348363/checking-if-an-annotation-is-of-a-specific-type 43 | if (annotation instanceof Param) { 44 | String value = context.getRequest().getParameter(name); 45 | values.add(value); 46 | } 47 | if (annotation instanceof Context) { 48 | if (type.isAssignableFrom(ResourceRequest.class)) { 49 | values.add(context.getRequest()); 50 | } else { 51 | values.add(context.getResponse()); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | Object result = method.invoke(context.getPortlet(), values.toArray()); 59 | 60 | String json = StringPool.BLANK; 61 | if (result != null) { 62 | Gson gson = new Gson(); 63 | json = gson.toJson(result); 64 | } 65 | 66 | return json; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/ResourcePortlet.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource; 2 | 3 | import be.aca.liferay.angular.portlet.resource.annotation.CacheResource; 4 | import be.aca.liferay.angular.portlet.resource.annotation.Resource; 5 | import com.google.common.base.Optional; 6 | import com.google.common.cache.Cache; 7 | import com.google.common.cache.CacheBuilder; 8 | import com.google.common.cache.LoadingCache; 9 | import com.liferay.util.bridges.mvc.MVCPortlet; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import javax.portlet.PortletException; 14 | import javax.portlet.ResourceRequest; 15 | import javax.portlet.ResourceResponse; 16 | import java.io.IOException; 17 | import java.lang.annotation.Annotation; 18 | import java.lang.reflect.Method; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | public class ResourcePortlet extends MVCPortlet { 22 | 23 | private final static Logger logger = LoggerFactory.getLogger(ResourcePortlet.class); 24 | 25 | protected final Cache resourceCache = CacheBuilder.newBuilder() 26 | .maximumSize(64) 27 | .expireAfterWrite(10, TimeUnit.MINUTES) 28 | .build(); 29 | 30 | private ResourceMethodExecutor methodExecutor = new ResourceMethodExecutor(); 31 | 32 | @Override 33 | public void serveResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse) throws IOException, PortletException { 34 | String resourceId = resourceRequest.getResourceID(); 35 | 36 | Optional method = findMatchingResourceMethod(resourceId); 37 | if (method.isPresent()) { 38 | try { 39 | String json; 40 | ResourceContext context = new ResourceContext(this, resourceRequest, resourceResponse, method.get(), resourceId); 41 | 42 | CacheResource cacheResource = context.getMethod().getAnnotation(CacheResource.class); 43 | if (cacheResource != null) { 44 | String param = resourceRequest.getParameter(cacheResource.keyParam()); 45 | context.setParamValue(param); 46 | 47 | logger.debug("Get resource {} with ID {} from cache", resourceId, param); 48 | 49 | json = resourceCache.getIfPresent(context); 50 | if (json == null) { 51 | json = methodExecutor.process(context); 52 | resourceCache.put(context, json); 53 | } 54 | } else { 55 | logger.debug("Get resource {} without cache", resourceId); 56 | 57 | json = methodExecutor.process(context); 58 | } 59 | 60 | resourceResponse.getWriter().print(json); 61 | } catch (Exception e) { 62 | logger.error("Problem calling resource serving method for {}", resourceId, e); 63 | throw new PortletException(e); 64 | } 65 | } else { 66 | logger.error("No matching resource method found for ID: {}", resourceId); 67 | throw new PortletException("No matching resource method found for ID: " + resourceId); 68 | } 69 | } 70 | 71 | private Optional findMatchingResourceMethod(String resourceId) { 72 | for (Method method : this.getClass().getMethods()) { 73 | for (Annotation annotation : method.getAnnotations()) { 74 | if (annotation.annotationType() == Resource.class) { 75 | Resource resource = (Resource) annotation; 76 | String id = resource.id(); 77 | 78 | if (resourceId.equals(id) && method.getReturnType() != Void.TYPE) { 79 | return Optional.of(method); 80 | } 81 | } 82 | } 83 | } 84 | 85 | return Optional.absent(); 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/annotation/CacheResource.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.METHOD) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Inherited 8 | public @interface CacheResource { 9 | 10 | public String keyParam(); 11 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/annotation/Context.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.PARAMETER) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Inherited 8 | public @interface Context { 9 | 10 | // public String id(); 11 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/annotation/Param.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.PARAMETER) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Inherited 8 | public @interface Param { 9 | 10 | public String id() default ""; 11 | } -------------------------------------------------------------------------------- /src/main/java/be/aca/liferay/angular/portlet/resource/annotation/Resource.java: -------------------------------------------------------------------------------- 1 | package be.aca.liferay.angular.portlet.resource.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.METHOD) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Inherited 8 | public @interface Resource { 9 | 10 | public String id(); 11 | } -------------------------------------------------------------------------------- /src/main/resources/Language.properties: -------------------------------------------------------------------------------- 1 | action.add=Add bookmark 2 | action.cancel=Cancel 3 | action.delete=delete 4 | action.detail=detail 5 | action.submit=Submit 6 | auth.token=Auth token: 7 | add.new.bookmark=Add new bookmark 8 | bookmarks=Bookmarks 9 | company.id=Company ID 10 | detail.for.bookmark=Detail for bookmark 11 | label.description=Description 12 | label.name=Name 13 | label.url=URL 14 | liferay.version=Version: 15 | portlet.id=PID: 16 | table.actions=Actions 17 | table.id=ID 18 | table.name=Name 19 | 20 | # jcs-autoValidate messages 21 | validation.defaultMsg=Please add error message for {0} 22 | validation.email=Please enter a valid email address 23 | validation.minlength=Please enter at least {0} characters 24 | validation.maxlength=You have entered more than the maximum {0} characters 25 | validation.min=Please enter the minimum number of {0} 26 | validation.max=Please enter the maximum number of {0} 27 | validation.required=This field is required 28 | validation.date=Please enter a valid date 29 | validation.pattern=Please ensure the entered information adheres to this pattern {0} 30 | validation.number=Please enter a valid number 31 | validation.url=Please enter a valid URL in the format of http(s)://wwww.google.com -------------------------------------------------------------------------------- /src/main/resources/Language_el.properties: -------------------------------------------------------------------------------- 1 | action.add=Προσθήκη 2 | action.cancel=Άκυρο 3 | action.delete=Διαγραφή 4 | action.detail=Λεπτομέρειες 5 | action.submit=Υποβολή 6 | auth.token=Auth token: 7 | add.new.bookmark=Προσθήκη bookmark 8 | bookmarks=Bookmarks 9 | company.id=Company ID 10 | detail.for.bookmark=Λεπτομέρειες bookmark 11 | label.description=Περιγραφή 12 | label.name=Όνομα 13 | label.url=URL 14 | portlet.id=PID: 15 | liferay.version=Έκδοση: 16 | table.actions=Ενέργειες 17 | table.id=ID 18 | table.name=Όνομα 19 | 20 | # jcs-autoValidate messages 21 | validation.defaultMsg=Παρακαλώ ορίστε μήνυμα λάθους για το {0} 22 | validation.email=Παρακαλώ εισάγετε μία έγκυρη διεύθυνση 23 | validation.minlength=Εισάγετε το λιγότερο {0} χαρακτήρες 24 | validation.maxlength=Έχετε εισάγει περισσότερους από το μέγιστο των {0} χαρακτήρων 25 | validation.min=Παρακαλώ εισάγετε τον ελάχιστο αριθμό {0} 26 | validation.max=Παρακαλώ εισάγετε τον μέγιστο αριθμό {0} 27 | validation.required=Το πεδίο είναι υποχρεωτικό 28 | validation.date=Παρακαλώ εισάγετε μία έγκυρη ημερομηνία 29 | validation.pattern=Παρακαλώ βαβαιωθείτε ότι τα δεδομένα που εισάγατε συμφωνούν με το παρακάτω πρότυπο {0} 30 | validation.number=Παρακαλώ εισάγετε ένα έγκυρο αριθμό 31 | validation.url=Παρακαλώ εισάγετε ένα έγκυρο url στη μορφή: http(s)://wwww.google.com 32 | -------------------------------------------------------------------------------- /src/main/resources/Language_nl.properties: -------------------------------------------------------------------------------- 1 | action.add=Bookmark toevoegen 2 | action.cancel=Cancel 3 | action.delete=verwijderen 4 | action.detail=detail 5 | action.submit=Submit 6 | auth.token=Auth token: 7 | add.new.bookmark=Nieuwe bookmark toevoegen 8 | bookmarks=Bookmarks 9 | company.id=Company ID 10 | detail.for.bookmark=Detail van een bookmark 11 | label.description=Omschrijving 12 | label.name=Naam 13 | label.url=URL 14 | portlet.id=PID: 15 | liferay.version=Versie: 16 | table.actions=Acties 17 | table.id=ID 18 | table.name=Naam 19 | 20 | # jcs-autoValidate messages 21 | validation.defaultMsg=Nog geen foutboodschap beschikbaar voor {0} 22 | validation.email=Gelieve een geldig email adres in te geven 23 | validation.minlength=Gelieve minstens {0} karakters in te geven 24 | validation.maxlength=Gelieve maximaal {0} karakters in te geven 25 | validation.min=Gelieve een getal in te geven dat minimal {0} is 26 | validation.max=Gelieve een getal in te geven dat maximaal {0} is 27 | validation.required=Dit veld is verplicht 28 | validation.date=Gelieve een geldige datum in te geven 29 | validation.pattern=Gelieve ervoor te zorgen dat de ingevulde waarde voldoet aan het patroon {0} 30 | validation.number=Gelieve een geldig nummer in te geven 31 | validation.url=Gelieve een geldige URL in te geven van het formaat: http(s)://wwww.google.com 32 | -------------------------------------------------------------------------------- /src/main/resources/logback.groovy: -------------------------------------------------------------------------------- 1 | appender("STDOUT", ConsoleAppender) { 2 | encoder(PatternLayoutEncoder) { 3 | pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n" 4 | } 5 | } 6 | 7 | logger("be.aca.liferay.angular.portlet", DEBUG) 8 | 9 | root(INFO, ["STDOUT"]) 10 | 11 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-display.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-hook.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | Language.properties 12 | Language_en_US.properties 13 | Language_nl.properties 14 | Language_nl_BE.properties 15 | Language_el.properties 16 | Language_el_GR.properties 17 | /custom_jsps 18 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-plugin-package.properties: -------------------------------------------------------------------------------- 1 | name=angular 2 | module-group-id=liferay 3 | module-incremental-version=1 4 | tags= 5 | short-description= 6 | long-description= 7 | change-log= 8 | page-url=http://www.aca-it.be 9 | author=ACA IT-Solutions 10 | licenses=LGPL 11 | liferay-versions=6.2.0+ 12 | # https://www.liferay.com/web/denis-signoretto/blog/-/blogs/using-slf4j-and-liferay-logging-framework-in-custom-plugins 13 | deploy-excludes=\ 14 | **/WEB-INF/lib/log4j.jar,\ 15 | **/WEB-INF/lib/log4j-extras.jar,\ 16 | **/WEB-INF/lib/util-slf4j.jar,\ 17 | **/WEB-INF/classes/log4j.properties,\ 18 | **/WEB-INF/classes/logging.properties -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-portlet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | angular 7 | /img/icon.png 8 | true 9 | false 10 | /angular-portlet/css/font-awesome.css 11 | /angular-portlet/css/style.css 12 | 16 | /angular-portlet/js/angular.js 17 | /angular-portlet/js/angular-ui-router.js 18 | /angular-portlet/js/angular-translate.js 19 | /angular-portlet/js/angular-translate-loader-url.js 20 | /angular-portlet/js/jcs-auto-validate.js 21 | /js/main.js 22 | /js/controllers.js 23 | /js/services.js 24 | /js/directives.js 25 | angular-portlet 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/portlet.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | angular 9 | Angular Demo Portlet 10 | be.aca.liferay.angular.portlet.AngularPortlet 11 | 12 | view-template 13 | /jsp/view.jsp 14 | 15 | 16 | edit-template 17 | /jsp/edit.jsp 18 | 19 | 0 20 | 21 | text/html 22 | view 23 | edit 24 | 25 | en 26 | en_US 27 | nl 28 | nl_BE 29 | el 30 | el_GR 31 | Language 32 | 33 | Angular Demo Portlet 34 | Angular Demo Portlet 35 | angular 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/wro.groovy: -------------------------------------------------------------------------------- 1 | groups { 2 | controllers { 3 | js(minimize: false, "/js/controller/Init.js") 4 | js(minimize: false, "/js/controller/*Controller.js") 5 | } 6 | services { 7 | js(minimize: false, "/js/service/Init.js") 8 | js(minimize: false, "/js/service/*Factory.js") 9 | js(minimize: false, "/js/service/ErrorMessageResolver.js") 10 | } 11 | directives { 12 | js(minimize: false, "/js/directive/Init.js") 13 | js(minimize: false, "/js/directive/*Directive.js") 14 | } 15 | all { 16 | controllers() 17 | services() 18 | directives() 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/webapp/css/font-awesome.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | /* FONT PATH 6 | * -------------------------- */ 7 | @font-face { 8 | font-family: 'FontAwesome'; 9 | src: url('../fonts/fontawesome-webfont.eot?v=4.3.0'); 10 | src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | } 14 | .fa { 15 | display: inline-block; 16 | font: normal normal normal 14px/1 FontAwesome; 17 | font-size: inherit; 18 | text-rendering: auto; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | transform: translate(0, 0); 22 | } 23 | /* makes the font 33% larger relative to the icon container */ 24 | .fa-lg { 25 | font-size: 1.33333333em; 26 | line-height: 0.75em; 27 | vertical-align: -15%; 28 | } 29 | .fa-2x { 30 | font-size: 2em; 31 | } 32 | .fa-3x { 33 | font-size: 3em; 34 | } 35 | .fa-4x { 36 | font-size: 4em; 37 | } 38 | .fa-5x { 39 | font-size: 5em; 40 | } 41 | .fa-fw { 42 | width: 1.28571429em; 43 | text-align: center; 44 | } 45 | .fa-ul { 46 | padding-left: 0; 47 | margin-left: 2.14285714em; 48 | list-style-type: none; 49 | } 50 | .fa-ul > li { 51 | position: relative; 52 | } 53 | .fa-li { 54 | position: absolute; 55 | left: -2.14285714em; 56 | width: 2.14285714em; 57 | top: 0.14285714em; 58 | text-align: center; 59 | } 60 | .fa-li.fa-lg { 61 | left: -1.85714286em; 62 | } 63 | .fa-border { 64 | padding: .2em .25em .15em; 65 | border: solid 0.08em #eeeeee; 66 | border-radius: .1em; 67 | } 68 | .pull-right { 69 | float: right; 70 | } 71 | .pull-left { 72 | float: left; 73 | } 74 | .fa.pull-left { 75 | margin-right: .3em; 76 | } 77 | .fa.pull-right { 78 | margin-left: .3em; 79 | } 80 | .fa-spin { 81 | -webkit-animation: fa-spin 2s infinite linear; 82 | animation: fa-spin 2s infinite linear; 83 | } 84 | .fa-pulse { 85 | -webkit-animation: fa-spin 1s infinite steps(8); 86 | animation: fa-spin 1s infinite steps(8); 87 | } 88 | @-webkit-keyframes fa-spin { 89 | 0% { 90 | -webkit-transform: rotate(0deg); 91 | transform: rotate(0deg); 92 | } 93 | 100% { 94 | -webkit-transform: rotate(359deg); 95 | transform: rotate(359deg); 96 | } 97 | } 98 | @keyframes fa-spin { 99 | 0% { 100 | -webkit-transform: rotate(0deg); 101 | transform: rotate(0deg); 102 | } 103 | 100% { 104 | -webkit-transform: rotate(359deg); 105 | transform: rotate(359deg); 106 | } 107 | } 108 | .fa-rotate-90 { 109 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); 110 | -webkit-transform: rotate(90deg); 111 | -ms-transform: rotate(90deg); 112 | transform: rotate(90deg); 113 | } 114 | .fa-rotate-180 { 115 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); 116 | -webkit-transform: rotate(180deg); 117 | -ms-transform: rotate(180deg); 118 | transform: rotate(180deg); 119 | } 120 | .fa-rotate-270 { 121 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); 122 | -webkit-transform: rotate(270deg); 123 | -ms-transform: rotate(270deg); 124 | transform: rotate(270deg); 125 | } 126 | .fa-flip-horizontal { 127 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); 128 | -webkit-transform: scale(-1, 1); 129 | -ms-transform: scale(-1, 1); 130 | transform: scale(-1, 1); 131 | } 132 | .fa-flip-vertical { 133 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); 134 | -webkit-transform: scale(1, -1); 135 | -ms-transform: scale(1, -1); 136 | transform: scale(1, -1); 137 | } 138 | :root .fa-rotate-90, 139 | :root .fa-rotate-180, 140 | :root .fa-rotate-270, 141 | :root .fa-flip-horizontal, 142 | :root .fa-flip-vertical { 143 | filter: none; 144 | } 145 | .fa-stack { 146 | position: relative; 147 | display: inline-block; 148 | width: 2em; 149 | height: 2em; 150 | line-height: 2em; 151 | vertical-align: middle; 152 | } 153 | .fa-stack-1x, 154 | .fa-stack-2x { 155 | position: absolute; 156 | left: 0; 157 | width: 100%; 158 | text-align: center; 159 | } 160 | .fa-stack-1x { 161 | line-height: inherit; 162 | } 163 | .fa-stack-2x { 164 | font-size: 2em; 165 | } 166 | .fa-inverse { 167 | color: #ffffff; 168 | } 169 | /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen 170 | readers do not read off random characters that represent icons */ 171 | .fa-glass:before { 172 | content: "\f000"; 173 | } 174 | .fa-music:before { 175 | content: "\f001"; 176 | } 177 | .fa-search:before { 178 | content: "\f002"; 179 | } 180 | .fa-envelope-o:before { 181 | content: "\f003"; 182 | } 183 | .fa-heart:before { 184 | content: "\f004"; 185 | } 186 | .fa-star:before { 187 | content: "\f005"; 188 | } 189 | .fa-star-o:before { 190 | content: "\f006"; 191 | } 192 | .fa-user:before { 193 | content: "\f007"; 194 | } 195 | .fa-film:before { 196 | content: "\f008"; 197 | } 198 | .fa-th-large:before { 199 | content: "\f009"; 200 | } 201 | .fa-th:before { 202 | content: "\f00a"; 203 | } 204 | .fa-th-list:before { 205 | content: "\f00b"; 206 | } 207 | .fa-check:before { 208 | content: "\f00c"; 209 | } 210 | .fa-remove:before, 211 | .fa-close:before, 212 | .fa-times:before { 213 | content: "\f00d"; 214 | } 215 | .fa-search-plus:before { 216 | content: "\f00e"; 217 | } 218 | .fa-search-minus:before { 219 | content: "\f010"; 220 | } 221 | .fa-power-off:before { 222 | content: "\f011"; 223 | } 224 | .fa-signal:before { 225 | content: "\f012"; 226 | } 227 | .fa-gear:before, 228 | .fa-cog:before { 229 | content: "\f013"; 230 | } 231 | .fa-trash-o:before { 232 | content: "\f014"; 233 | } 234 | .fa-home:before { 235 | content: "\f015"; 236 | } 237 | .fa-file-o:before { 238 | content: "\f016"; 239 | } 240 | .fa-clock-o:before { 241 | content: "\f017"; 242 | } 243 | .fa-road:before { 244 | content: "\f018"; 245 | } 246 | .fa-download:before { 247 | content: "\f019"; 248 | } 249 | .fa-arrow-circle-o-down:before { 250 | content: "\f01a"; 251 | } 252 | .fa-arrow-circle-o-up:before { 253 | content: "\f01b"; 254 | } 255 | .fa-inbox:before { 256 | content: "\f01c"; 257 | } 258 | .fa-play-circle-o:before { 259 | content: "\f01d"; 260 | } 261 | .fa-rotate-right:before, 262 | .fa-repeat:before { 263 | content: "\f01e"; 264 | } 265 | .fa-refresh:before { 266 | content: "\f021"; 267 | } 268 | .fa-list-alt:before { 269 | content: "\f022"; 270 | } 271 | .fa-lock:before { 272 | content: "\f023"; 273 | } 274 | .fa-flag:before { 275 | content: "\f024"; 276 | } 277 | .fa-headphones:before { 278 | content: "\f025"; 279 | } 280 | .fa-volume-off:before { 281 | content: "\f026"; 282 | } 283 | .fa-volume-down:before { 284 | content: "\f027"; 285 | } 286 | .fa-volume-up:before { 287 | content: "\f028"; 288 | } 289 | .fa-qrcode:before { 290 | content: "\f029"; 291 | } 292 | .fa-barcode:before { 293 | content: "\f02a"; 294 | } 295 | .fa-tag:before { 296 | content: "\f02b"; 297 | } 298 | .fa-tags:before { 299 | content: "\f02c"; 300 | } 301 | .fa-book:before { 302 | content: "\f02d"; 303 | } 304 | .fa-bookmark:before { 305 | content: "\f02e"; 306 | } 307 | .fa-print:before { 308 | content: "\f02f"; 309 | } 310 | .fa-camera:before { 311 | content: "\f030"; 312 | } 313 | .fa-font:before { 314 | content: "\f031"; 315 | } 316 | .fa-bold:before { 317 | content: "\f032"; 318 | } 319 | .fa-italic:before { 320 | content: "\f033"; 321 | } 322 | .fa-text-height:before { 323 | content: "\f034"; 324 | } 325 | .fa-text-width:before { 326 | content: "\f035"; 327 | } 328 | .fa-align-left:before { 329 | content: "\f036"; 330 | } 331 | .fa-align-center:before { 332 | content: "\f037"; 333 | } 334 | .fa-align-right:before { 335 | content: "\f038"; 336 | } 337 | .fa-align-justify:before { 338 | content: "\f039"; 339 | } 340 | .fa-list:before { 341 | content: "\f03a"; 342 | } 343 | .fa-dedent:before, 344 | .fa-outdent:before { 345 | content: "\f03b"; 346 | } 347 | .fa-indent:before { 348 | content: "\f03c"; 349 | } 350 | .fa-video-camera:before { 351 | content: "\f03d"; 352 | } 353 | .fa-photo:before, 354 | .fa-image:before, 355 | .fa-picture-o:before { 356 | content: "\f03e"; 357 | } 358 | .fa-pencil:before { 359 | content: "\f040"; 360 | } 361 | .fa-map-marker:before { 362 | content: "\f041"; 363 | } 364 | .fa-adjust:before { 365 | content: "\f042"; 366 | } 367 | .fa-tint:before { 368 | content: "\f043"; 369 | } 370 | .fa-edit:before, 371 | .fa-pencil-square-o:before { 372 | content: "\f044"; 373 | } 374 | .fa-share-square-o:before { 375 | content: "\f045"; 376 | } 377 | .fa-check-square-o:before { 378 | content: "\f046"; 379 | } 380 | .fa-arrows:before { 381 | content: "\f047"; 382 | } 383 | .fa-step-backward:before { 384 | content: "\f048"; 385 | } 386 | .fa-fast-backward:before { 387 | content: "\f049"; 388 | } 389 | .fa-backward:before { 390 | content: "\f04a"; 391 | } 392 | .fa-play:before { 393 | content: "\f04b"; 394 | } 395 | .fa-pause:before { 396 | content: "\f04c"; 397 | } 398 | .fa-stop:before { 399 | content: "\f04d"; 400 | } 401 | .fa-forward:before { 402 | content: "\f04e"; 403 | } 404 | .fa-fast-forward:before { 405 | content: "\f050"; 406 | } 407 | .fa-step-forward:before { 408 | content: "\f051"; 409 | } 410 | .fa-eject:before { 411 | content: "\f052"; 412 | } 413 | .fa-chevron-left:before { 414 | content: "\f053"; 415 | } 416 | .fa-chevron-right:before { 417 | content: "\f054"; 418 | } 419 | .fa-plus-circle:before { 420 | content: "\f055"; 421 | } 422 | .fa-minus-circle:before { 423 | content: "\f056"; 424 | } 425 | .fa-times-circle:before { 426 | content: "\f057"; 427 | } 428 | .fa-check-circle:before { 429 | content: "\f058"; 430 | } 431 | .fa-question-circle:before { 432 | content: "\f059"; 433 | } 434 | .fa-info-circle:before { 435 | content: "\f05a"; 436 | } 437 | .fa-crosshairs:before { 438 | content: "\f05b"; 439 | } 440 | .fa-times-circle-o:before { 441 | content: "\f05c"; 442 | } 443 | .fa-check-circle-o:before { 444 | content: "\f05d"; 445 | } 446 | .fa-ban:before { 447 | content: "\f05e"; 448 | } 449 | .fa-arrow-left:before { 450 | content: "\f060"; 451 | } 452 | .fa-arrow-right:before { 453 | content: "\f061"; 454 | } 455 | .fa-arrow-up:before { 456 | content: "\f062"; 457 | } 458 | .fa-arrow-down:before { 459 | content: "\f063"; 460 | } 461 | .fa-mail-forward:before, 462 | .fa-share:before { 463 | content: "\f064"; 464 | } 465 | .fa-expand:before { 466 | content: "\f065"; 467 | } 468 | .fa-compress:before { 469 | content: "\f066"; 470 | } 471 | .fa-plus:before { 472 | content: "\f067"; 473 | } 474 | .fa-minus:before { 475 | content: "\f068"; 476 | } 477 | .fa-asterisk:before { 478 | content: "\f069"; 479 | } 480 | .fa-exclamation-circle:before { 481 | content: "\f06a"; 482 | } 483 | .fa-gift:before { 484 | content: "\f06b"; 485 | } 486 | .fa-leaf:before { 487 | content: "\f06c"; 488 | } 489 | .fa-fire:before { 490 | content: "\f06d"; 491 | } 492 | .fa-eye:before { 493 | content: "\f06e"; 494 | } 495 | .fa-eye-slash:before { 496 | content: "\f070"; 497 | } 498 | .fa-warning:before, 499 | .fa-exclamation-triangle:before { 500 | content: "\f071"; 501 | } 502 | .fa-plane:before { 503 | content: "\f072"; 504 | } 505 | .fa-calendar:before { 506 | content: "\f073"; 507 | } 508 | .fa-random:before { 509 | content: "\f074"; 510 | } 511 | .fa-comment:before { 512 | content: "\f075"; 513 | } 514 | .fa-magnet:before { 515 | content: "\f076"; 516 | } 517 | .fa-chevron-up:before { 518 | content: "\f077"; 519 | } 520 | .fa-chevron-down:before { 521 | content: "\f078"; 522 | } 523 | .fa-retweet:before { 524 | content: "\f079"; 525 | } 526 | .fa-shopping-cart:before { 527 | content: "\f07a"; 528 | } 529 | .fa-folder:before { 530 | content: "\f07b"; 531 | } 532 | .fa-folder-open:before { 533 | content: "\f07c"; 534 | } 535 | .fa-arrows-v:before { 536 | content: "\f07d"; 537 | } 538 | .fa-arrows-h:before { 539 | content: "\f07e"; 540 | } 541 | .fa-bar-chart-o:before, 542 | .fa-bar-chart:before { 543 | content: "\f080"; 544 | } 545 | .fa-twitter-square:before { 546 | content: "\f081"; 547 | } 548 | .fa-facebook-square:before { 549 | content: "\f082"; 550 | } 551 | .fa-camera-retro:before { 552 | content: "\f083"; 553 | } 554 | .fa-key:before { 555 | content: "\f084"; 556 | } 557 | .fa-gears:before, 558 | .fa-cogs:before { 559 | content: "\f085"; 560 | } 561 | .fa-comments:before { 562 | content: "\f086"; 563 | } 564 | .fa-thumbs-o-up:before { 565 | content: "\f087"; 566 | } 567 | .fa-thumbs-o-down:before { 568 | content: "\f088"; 569 | } 570 | .fa-star-half:before { 571 | content: "\f089"; 572 | } 573 | .fa-heart-o:before { 574 | content: "\f08a"; 575 | } 576 | .fa-sign-out:before { 577 | content: "\f08b"; 578 | } 579 | .fa-linkedin-square:before { 580 | content: "\f08c"; 581 | } 582 | .fa-thumb-tack:before { 583 | content: "\f08d"; 584 | } 585 | .fa-external-link:before { 586 | content: "\f08e"; 587 | } 588 | .fa-sign-in:before { 589 | content: "\f090"; 590 | } 591 | .fa-trophy:before { 592 | content: "\f091"; 593 | } 594 | .fa-github-square:before { 595 | content: "\f092"; 596 | } 597 | .fa-upload:before { 598 | content: "\f093"; 599 | } 600 | .fa-lemon-o:before { 601 | content: "\f094"; 602 | } 603 | .fa-phone:before { 604 | content: "\f095"; 605 | } 606 | .fa-square-o:before { 607 | content: "\f096"; 608 | } 609 | .fa-bookmark-o:before { 610 | content: "\f097"; 611 | } 612 | .fa-phone-square:before { 613 | content: "\f098"; 614 | } 615 | .fa-twitter:before { 616 | content: "\f099"; 617 | } 618 | .fa-facebook-f:before, 619 | .fa-facebook:before { 620 | content: "\f09a"; 621 | } 622 | .fa-github:before { 623 | content: "\f09b"; 624 | } 625 | .fa-unlock:before { 626 | content: "\f09c"; 627 | } 628 | .fa-credit-card:before { 629 | content: "\f09d"; 630 | } 631 | .fa-rss:before { 632 | content: "\f09e"; 633 | } 634 | .fa-hdd-o:before { 635 | content: "\f0a0"; 636 | } 637 | .fa-bullhorn:before { 638 | content: "\f0a1"; 639 | } 640 | .fa-bell:before { 641 | content: "\f0f3"; 642 | } 643 | .fa-certificate:before { 644 | content: "\f0a3"; 645 | } 646 | .fa-hand-o-right:before { 647 | content: "\f0a4"; 648 | } 649 | .fa-hand-o-left:before { 650 | content: "\f0a5"; 651 | } 652 | .fa-hand-o-up:before { 653 | content: "\f0a6"; 654 | } 655 | .fa-hand-o-down:before { 656 | content: "\f0a7"; 657 | } 658 | .fa-arrow-circle-left:before { 659 | content: "\f0a8"; 660 | } 661 | .fa-arrow-circle-right:before { 662 | content: "\f0a9"; 663 | } 664 | .fa-arrow-circle-up:before { 665 | content: "\f0aa"; 666 | } 667 | .fa-arrow-circle-down:before { 668 | content: "\f0ab"; 669 | } 670 | .fa-globe:before { 671 | content: "\f0ac"; 672 | } 673 | .fa-wrench:before { 674 | content: "\f0ad"; 675 | } 676 | .fa-tasks:before { 677 | content: "\f0ae"; 678 | } 679 | .fa-filter:before { 680 | content: "\f0b0"; 681 | } 682 | .fa-briefcase:before { 683 | content: "\f0b1"; 684 | } 685 | .fa-arrows-alt:before { 686 | content: "\f0b2"; 687 | } 688 | .fa-group:before, 689 | .fa-users:before { 690 | content: "\f0c0"; 691 | } 692 | .fa-chain:before, 693 | .fa-link:before { 694 | content: "\f0c1"; 695 | } 696 | .fa-cloud:before { 697 | content: "\f0c2"; 698 | } 699 | .fa-flask:before { 700 | content: "\f0c3"; 701 | } 702 | .fa-cut:before, 703 | .fa-scissors:before { 704 | content: "\f0c4"; 705 | } 706 | .fa-copy:before, 707 | .fa-files-o:before { 708 | content: "\f0c5"; 709 | } 710 | .fa-paperclip:before { 711 | content: "\f0c6"; 712 | } 713 | .fa-save:before, 714 | .fa-floppy-o:before { 715 | content: "\f0c7"; 716 | } 717 | .fa-square:before { 718 | content: "\f0c8"; 719 | } 720 | .fa-navicon:before, 721 | .fa-reorder:before, 722 | .fa-bars:before { 723 | content: "\f0c9"; 724 | } 725 | .fa-list-ul:before { 726 | content: "\f0ca"; 727 | } 728 | .fa-list-ol:before { 729 | content: "\f0cb"; 730 | } 731 | .fa-strikethrough:before { 732 | content: "\f0cc"; 733 | } 734 | .fa-underline:before { 735 | content: "\f0cd"; 736 | } 737 | .fa-table:before { 738 | content: "\f0ce"; 739 | } 740 | .fa-magic:before { 741 | content: "\f0d0"; 742 | } 743 | .fa-truck:before { 744 | content: "\f0d1"; 745 | } 746 | .fa-pinterest:before { 747 | content: "\f0d2"; 748 | } 749 | .fa-pinterest-square:before { 750 | content: "\f0d3"; 751 | } 752 | .fa-google-plus-square:before { 753 | content: "\f0d4"; 754 | } 755 | .fa-google-plus:before { 756 | content: "\f0d5"; 757 | } 758 | .fa-money:before { 759 | content: "\f0d6"; 760 | } 761 | .fa-caret-down:before { 762 | content: "\f0d7"; 763 | } 764 | .fa-caret-up:before { 765 | content: "\f0d8"; 766 | } 767 | .fa-caret-left:before { 768 | content: "\f0d9"; 769 | } 770 | .fa-caret-right:before { 771 | content: "\f0da"; 772 | } 773 | .fa-columns:before { 774 | content: "\f0db"; 775 | } 776 | .fa-unsorted:before, 777 | .fa-sort:before { 778 | content: "\f0dc"; 779 | } 780 | .fa-sort-down:before, 781 | .fa-sort-desc:before { 782 | content: "\f0dd"; 783 | } 784 | .fa-sort-up:before, 785 | .fa-sort-asc:before { 786 | content: "\f0de"; 787 | } 788 | .fa-envelope:before { 789 | content: "\f0e0"; 790 | } 791 | .fa-linkedin:before { 792 | content: "\f0e1"; 793 | } 794 | .fa-rotate-left:before, 795 | .fa-undo:before { 796 | content: "\f0e2"; 797 | } 798 | .fa-legal:before, 799 | .fa-gavel:before { 800 | content: "\f0e3"; 801 | } 802 | .fa-dashboard:before, 803 | .fa-tachometer:before { 804 | content: "\f0e4"; 805 | } 806 | .fa-comment-o:before { 807 | content: "\f0e5"; 808 | } 809 | .fa-comments-o:before { 810 | content: "\f0e6"; 811 | } 812 | .fa-flash:before, 813 | .fa-bolt:before { 814 | content: "\f0e7"; 815 | } 816 | .fa-sitemap:before { 817 | content: "\f0e8"; 818 | } 819 | .fa-umbrella:before { 820 | content: "\f0e9"; 821 | } 822 | .fa-paste:before, 823 | .fa-clipboard:before { 824 | content: "\f0ea"; 825 | } 826 | .fa-lightbulb-o:before { 827 | content: "\f0eb"; 828 | } 829 | .fa-exchange:before { 830 | content: "\f0ec"; 831 | } 832 | .fa-cloud-download:before { 833 | content: "\f0ed"; 834 | } 835 | .fa-cloud-upload:before { 836 | content: "\f0ee"; 837 | } 838 | .fa-user-md:before { 839 | content: "\f0f0"; 840 | } 841 | .fa-stethoscope:before { 842 | content: "\f0f1"; 843 | } 844 | .fa-suitcase:before { 845 | content: "\f0f2"; 846 | } 847 | .fa-bell-o:before { 848 | content: "\f0a2"; 849 | } 850 | .fa-coffee:before { 851 | content: "\f0f4"; 852 | } 853 | .fa-cutlery:before { 854 | content: "\f0f5"; 855 | } 856 | .fa-file-text-o:before { 857 | content: "\f0f6"; 858 | } 859 | .fa-building-o:before { 860 | content: "\f0f7"; 861 | } 862 | .fa-hospital-o:before { 863 | content: "\f0f8"; 864 | } 865 | .fa-ambulance:before { 866 | content: "\f0f9"; 867 | } 868 | .fa-medkit:before { 869 | content: "\f0fa"; 870 | } 871 | .fa-fighter-jet:before { 872 | content: "\f0fb"; 873 | } 874 | .fa-beer:before { 875 | content: "\f0fc"; 876 | } 877 | .fa-h-square:before { 878 | content: "\f0fd"; 879 | } 880 | .fa-plus-square:before { 881 | content: "\f0fe"; 882 | } 883 | .fa-angle-double-left:before { 884 | content: "\f100"; 885 | } 886 | .fa-angle-double-right:before { 887 | content: "\f101"; 888 | } 889 | .fa-angle-double-up:before { 890 | content: "\f102"; 891 | } 892 | .fa-angle-double-down:before { 893 | content: "\f103"; 894 | } 895 | .fa-angle-left:before { 896 | content: "\f104"; 897 | } 898 | .fa-angle-right:before { 899 | content: "\f105"; 900 | } 901 | .fa-angle-up:before { 902 | content: "\f106"; 903 | } 904 | .fa-angle-down:before { 905 | content: "\f107"; 906 | } 907 | .fa-desktop:before { 908 | content: "\f108"; 909 | } 910 | .fa-laptop:before { 911 | content: "\f109"; 912 | } 913 | .fa-tablet:before { 914 | content: "\f10a"; 915 | } 916 | .fa-mobile-phone:before, 917 | .fa-mobile:before { 918 | content: "\f10b"; 919 | } 920 | .fa-circle-o:before { 921 | content: "\f10c"; 922 | } 923 | .fa-quote-left:before { 924 | content: "\f10d"; 925 | } 926 | .fa-quote-right:before { 927 | content: "\f10e"; 928 | } 929 | .fa-spinner:before { 930 | content: "\f110"; 931 | } 932 | .fa-circle:before { 933 | content: "\f111"; 934 | } 935 | .fa-mail-reply:before, 936 | .fa-reply:before { 937 | content: "\f112"; 938 | } 939 | .fa-github-alt:before { 940 | content: "\f113"; 941 | } 942 | .fa-folder-o:before { 943 | content: "\f114"; 944 | } 945 | .fa-folder-open-o:before { 946 | content: "\f115"; 947 | } 948 | .fa-smile-o:before { 949 | content: "\f118"; 950 | } 951 | .fa-frown-o:before { 952 | content: "\f119"; 953 | } 954 | .fa-meh-o:before { 955 | content: "\f11a"; 956 | } 957 | .fa-gamepad:before { 958 | content: "\f11b"; 959 | } 960 | .fa-keyboard-o:before { 961 | content: "\f11c"; 962 | } 963 | .fa-flag-o:before { 964 | content: "\f11d"; 965 | } 966 | .fa-flag-checkered:before { 967 | content: "\f11e"; 968 | } 969 | .fa-terminal:before { 970 | content: "\f120"; 971 | } 972 | .fa-code:before { 973 | content: "\f121"; 974 | } 975 | .fa-mail-reply-all:before, 976 | .fa-reply-all:before { 977 | content: "\f122"; 978 | } 979 | .fa-star-half-empty:before, 980 | .fa-star-half-full:before, 981 | .fa-star-half-o:before { 982 | content: "\f123"; 983 | } 984 | .fa-location-arrow:before { 985 | content: "\f124"; 986 | } 987 | .fa-crop:before { 988 | content: "\f125"; 989 | } 990 | .fa-code-fork:before { 991 | content: "\f126"; 992 | } 993 | .fa-unlink:before, 994 | .fa-chain-broken:before { 995 | content: "\f127"; 996 | } 997 | .fa-question:before { 998 | content: "\f128"; 999 | } 1000 | .fa-info:before { 1001 | content: "\f129"; 1002 | } 1003 | .fa-exclamation:before { 1004 | content: "\f12a"; 1005 | } 1006 | .fa-superscript:before { 1007 | content: "\f12b"; 1008 | } 1009 | .fa-subscript:before { 1010 | content: "\f12c"; 1011 | } 1012 | .fa-eraser:before { 1013 | content: "\f12d"; 1014 | } 1015 | .fa-puzzle-piece:before { 1016 | content: "\f12e"; 1017 | } 1018 | .fa-microphone:before { 1019 | content: "\f130"; 1020 | } 1021 | .fa-microphone-slash:before { 1022 | content: "\f131"; 1023 | } 1024 | .fa-shield:before { 1025 | content: "\f132"; 1026 | } 1027 | .fa-calendar-o:before { 1028 | content: "\f133"; 1029 | } 1030 | .fa-fire-extinguisher:before { 1031 | content: "\f134"; 1032 | } 1033 | .fa-rocket:before { 1034 | content: "\f135"; 1035 | } 1036 | .fa-maxcdn:before { 1037 | content: "\f136"; 1038 | } 1039 | .fa-chevron-circle-left:before { 1040 | content: "\f137"; 1041 | } 1042 | .fa-chevron-circle-right:before { 1043 | content: "\f138"; 1044 | } 1045 | .fa-chevron-circle-up:before { 1046 | content: "\f139"; 1047 | } 1048 | .fa-chevron-circle-down:before { 1049 | content: "\f13a"; 1050 | } 1051 | .fa-html5:before { 1052 | content: "\f13b"; 1053 | } 1054 | .fa-css3:before { 1055 | content: "\f13c"; 1056 | } 1057 | .fa-anchor:before { 1058 | content: "\f13d"; 1059 | } 1060 | .fa-unlock-alt:before { 1061 | content: "\f13e"; 1062 | } 1063 | .fa-bullseye:before { 1064 | content: "\f140"; 1065 | } 1066 | .fa-ellipsis-h:before { 1067 | content: "\f141"; 1068 | } 1069 | .fa-ellipsis-v:before { 1070 | content: "\f142"; 1071 | } 1072 | .fa-rss-square:before { 1073 | content: "\f143"; 1074 | } 1075 | .fa-play-circle:before { 1076 | content: "\f144"; 1077 | } 1078 | .fa-ticket:before { 1079 | content: "\f145"; 1080 | } 1081 | .fa-minus-square:before { 1082 | content: "\f146"; 1083 | } 1084 | .fa-minus-square-o:before { 1085 | content: "\f147"; 1086 | } 1087 | .fa-level-up:before { 1088 | content: "\f148"; 1089 | } 1090 | .fa-level-down:before { 1091 | content: "\f149"; 1092 | } 1093 | .fa-check-square:before { 1094 | content: "\f14a"; 1095 | } 1096 | .fa-pencil-square:before { 1097 | content: "\f14b"; 1098 | } 1099 | .fa-external-link-square:before { 1100 | content: "\f14c"; 1101 | } 1102 | .fa-share-square:before { 1103 | content: "\f14d"; 1104 | } 1105 | .fa-compass:before { 1106 | content: "\f14e"; 1107 | } 1108 | .fa-toggle-down:before, 1109 | .fa-caret-square-o-down:before { 1110 | content: "\f150"; 1111 | } 1112 | .fa-toggle-up:before, 1113 | .fa-caret-square-o-up:before { 1114 | content: "\f151"; 1115 | } 1116 | .fa-toggle-right:before, 1117 | .fa-caret-square-o-right:before { 1118 | content: "\f152"; 1119 | } 1120 | .fa-euro:before, 1121 | .fa-eur:before { 1122 | content: "\f153"; 1123 | } 1124 | .fa-gbp:before { 1125 | content: "\f154"; 1126 | } 1127 | .fa-dollar:before, 1128 | .fa-usd:before { 1129 | content: "\f155"; 1130 | } 1131 | .fa-rupee:before, 1132 | .fa-inr:before { 1133 | content: "\f156"; 1134 | } 1135 | .fa-cny:before, 1136 | .fa-rmb:before, 1137 | .fa-yen:before, 1138 | .fa-jpy:before { 1139 | content: "\f157"; 1140 | } 1141 | .fa-ruble:before, 1142 | .fa-rouble:before, 1143 | .fa-rub:before { 1144 | content: "\f158"; 1145 | } 1146 | .fa-won:before, 1147 | .fa-krw:before { 1148 | content: "\f159"; 1149 | } 1150 | .fa-bitcoin:before, 1151 | .fa-btc:before { 1152 | content: "\f15a"; 1153 | } 1154 | .fa-file:before { 1155 | content: "\f15b"; 1156 | } 1157 | .fa-file-text:before { 1158 | content: "\f15c"; 1159 | } 1160 | .fa-sort-alpha-asc:before { 1161 | content: "\f15d"; 1162 | } 1163 | .fa-sort-alpha-desc:before { 1164 | content: "\f15e"; 1165 | } 1166 | .fa-sort-amount-asc:before { 1167 | content: "\f160"; 1168 | } 1169 | .fa-sort-amount-desc:before { 1170 | content: "\f161"; 1171 | } 1172 | .fa-sort-numeric-asc:before { 1173 | content: "\f162"; 1174 | } 1175 | .fa-sort-numeric-desc:before { 1176 | content: "\f163"; 1177 | } 1178 | .fa-thumbs-up:before { 1179 | content: "\f164"; 1180 | } 1181 | .fa-thumbs-down:before { 1182 | content: "\f165"; 1183 | } 1184 | .fa-youtube-square:before { 1185 | content: "\f166"; 1186 | } 1187 | .fa-youtube:before { 1188 | content: "\f167"; 1189 | } 1190 | .fa-xing:before { 1191 | content: "\f168"; 1192 | } 1193 | .fa-xing-square:before { 1194 | content: "\f169"; 1195 | } 1196 | .fa-youtube-play:before { 1197 | content: "\f16a"; 1198 | } 1199 | .fa-dropbox:before { 1200 | content: "\f16b"; 1201 | } 1202 | .fa-stack-overflow:before { 1203 | content: "\f16c"; 1204 | } 1205 | .fa-instagram:before { 1206 | content: "\f16d"; 1207 | } 1208 | .fa-flickr:before { 1209 | content: "\f16e"; 1210 | } 1211 | .fa-adn:before { 1212 | content: "\f170"; 1213 | } 1214 | .fa-bitbucket:before { 1215 | content: "\f171"; 1216 | } 1217 | .fa-bitbucket-square:before { 1218 | content: "\f172"; 1219 | } 1220 | .fa-tumblr:before { 1221 | content: "\f173"; 1222 | } 1223 | .fa-tumblr-square:before { 1224 | content: "\f174"; 1225 | } 1226 | .fa-long-arrow-down:before { 1227 | content: "\f175"; 1228 | } 1229 | .fa-long-arrow-up:before { 1230 | content: "\f176"; 1231 | } 1232 | .fa-long-arrow-left:before { 1233 | content: "\f177"; 1234 | } 1235 | .fa-long-arrow-right:before { 1236 | content: "\f178"; 1237 | } 1238 | .fa-apple:before { 1239 | content: "\f179"; 1240 | } 1241 | .fa-windows:before { 1242 | content: "\f17a"; 1243 | } 1244 | .fa-android:before { 1245 | content: "\f17b"; 1246 | } 1247 | .fa-linux:before { 1248 | content: "\f17c"; 1249 | } 1250 | .fa-dribbble:before { 1251 | content: "\f17d"; 1252 | } 1253 | .fa-skype:before { 1254 | content: "\f17e"; 1255 | } 1256 | .fa-foursquare:before { 1257 | content: "\f180"; 1258 | } 1259 | .fa-trello:before { 1260 | content: "\f181"; 1261 | } 1262 | .fa-female:before { 1263 | content: "\f182"; 1264 | } 1265 | .fa-male:before { 1266 | content: "\f183"; 1267 | } 1268 | .fa-gittip:before, 1269 | .fa-gratipay:before { 1270 | content: "\f184"; 1271 | } 1272 | .fa-sun-o:before { 1273 | content: "\f185"; 1274 | } 1275 | .fa-moon-o:before { 1276 | content: "\f186"; 1277 | } 1278 | .fa-archive:before { 1279 | content: "\f187"; 1280 | } 1281 | .fa-bug:before { 1282 | content: "\f188"; 1283 | } 1284 | .fa-vk:before { 1285 | content: "\f189"; 1286 | } 1287 | .fa-weibo:before { 1288 | content: "\f18a"; 1289 | } 1290 | .fa-renren:before { 1291 | content: "\f18b"; 1292 | } 1293 | .fa-pagelines:before { 1294 | content: "\f18c"; 1295 | } 1296 | .fa-stack-exchange:before { 1297 | content: "\f18d"; 1298 | } 1299 | .fa-arrow-circle-o-right:before { 1300 | content: "\f18e"; 1301 | } 1302 | .fa-arrow-circle-o-left:before { 1303 | content: "\f190"; 1304 | } 1305 | .fa-toggle-left:before, 1306 | .fa-caret-square-o-left:before { 1307 | content: "\f191"; 1308 | } 1309 | .fa-dot-circle-o:before { 1310 | content: "\f192"; 1311 | } 1312 | .fa-wheelchair:before { 1313 | content: "\f193"; 1314 | } 1315 | .fa-vimeo-square:before { 1316 | content: "\f194"; 1317 | } 1318 | .fa-turkish-lira:before, 1319 | .fa-try:before { 1320 | content: "\f195"; 1321 | } 1322 | .fa-plus-square-o:before { 1323 | content: "\f196"; 1324 | } 1325 | .fa-space-shuttle:before { 1326 | content: "\f197"; 1327 | } 1328 | .fa-slack:before { 1329 | content: "\f198"; 1330 | } 1331 | .fa-envelope-square:before { 1332 | content: "\f199"; 1333 | } 1334 | .fa-wordpress:before { 1335 | content: "\f19a"; 1336 | } 1337 | .fa-openid:before { 1338 | content: "\f19b"; 1339 | } 1340 | .fa-institution:before, 1341 | .fa-bank:before, 1342 | .fa-university:before { 1343 | content: "\f19c"; 1344 | } 1345 | .fa-mortar-board:before, 1346 | .fa-graduation-cap:before { 1347 | content: "\f19d"; 1348 | } 1349 | .fa-yahoo:before { 1350 | content: "\f19e"; 1351 | } 1352 | .fa-google:before { 1353 | content: "\f1a0"; 1354 | } 1355 | .fa-reddit:before { 1356 | content: "\f1a1"; 1357 | } 1358 | .fa-reddit-square:before { 1359 | content: "\f1a2"; 1360 | } 1361 | .fa-stumbleupon-circle:before { 1362 | content: "\f1a3"; 1363 | } 1364 | .fa-stumbleupon:before { 1365 | content: "\f1a4"; 1366 | } 1367 | .fa-delicious:before { 1368 | content: "\f1a5"; 1369 | } 1370 | .fa-digg:before { 1371 | content: "\f1a6"; 1372 | } 1373 | .fa-pied-piper:before { 1374 | content: "\f1a7"; 1375 | } 1376 | .fa-pied-piper-alt:before { 1377 | content: "\f1a8"; 1378 | } 1379 | .fa-drupal:before { 1380 | content: "\f1a9"; 1381 | } 1382 | .fa-joomla:before { 1383 | content: "\f1aa"; 1384 | } 1385 | .fa-language:before { 1386 | content: "\f1ab"; 1387 | } 1388 | .fa-fax:before { 1389 | content: "\f1ac"; 1390 | } 1391 | .fa-building:before { 1392 | content: "\f1ad"; 1393 | } 1394 | .fa-child:before { 1395 | content: "\f1ae"; 1396 | } 1397 | .fa-paw:before { 1398 | content: "\f1b0"; 1399 | } 1400 | .fa-spoon:before { 1401 | content: "\f1b1"; 1402 | } 1403 | .fa-cube:before { 1404 | content: "\f1b2"; 1405 | } 1406 | .fa-cubes:before { 1407 | content: "\f1b3"; 1408 | } 1409 | .fa-behance:before { 1410 | content: "\f1b4"; 1411 | } 1412 | .fa-behance-square:before { 1413 | content: "\f1b5"; 1414 | } 1415 | .fa-steam:before { 1416 | content: "\f1b6"; 1417 | } 1418 | .fa-steam-square:before { 1419 | content: "\f1b7"; 1420 | } 1421 | .fa-recycle:before { 1422 | content: "\f1b8"; 1423 | } 1424 | .fa-automobile:before, 1425 | .fa-car:before { 1426 | content: "\f1b9"; 1427 | } 1428 | .fa-cab:before, 1429 | .fa-taxi:before { 1430 | content: "\f1ba"; 1431 | } 1432 | .fa-tree:before { 1433 | content: "\f1bb"; 1434 | } 1435 | .fa-spotify:before { 1436 | content: "\f1bc"; 1437 | } 1438 | .fa-deviantart:before { 1439 | content: "\f1bd"; 1440 | } 1441 | .fa-soundcloud:before { 1442 | content: "\f1be"; 1443 | } 1444 | .fa-database:before { 1445 | content: "\f1c0"; 1446 | } 1447 | .fa-file-pdf-o:before { 1448 | content: "\f1c1"; 1449 | } 1450 | .fa-file-word-o:before { 1451 | content: "\f1c2"; 1452 | } 1453 | .fa-file-excel-o:before { 1454 | content: "\f1c3"; 1455 | } 1456 | .fa-file-powerpoint-o:before { 1457 | content: "\f1c4"; 1458 | } 1459 | .fa-file-photo-o:before, 1460 | .fa-file-picture-o:before, 1461 | .fa-file-image-o:before { 1462 | content: "\f1c5"; 1463 | } 1464 | .fa-file-zip-o:before, 1465 | .fa-file-archive-o:before { 1466 | content: "\f1c6"; 1467 | } 1468 | .fa-file-sound-o:before, 1469 | .fa-file-audio-o:before { 1470 | content: "\f1c7"; 1471 | } 1472 | .fa-file-movie-o:before, 1473 | .fa-file-video-o:before { 1474 | content: "\f1c8"; 1475 | } 1476 | .fa-file-code-o:before { 1477 | content: "\f1c9"; 1478 | } 1479 | .fa-vine:before { 1480 | content: "\f1ca"; 1481 | } 1482 | .fa-codepen:before { 1483 | content: "\f1cb"; 1484 | } 1485 | .fa-jsfiddle:before { 1486 | content: "\f1cc"; 1487 | } 1488 | .fa-life-bouy:before, 1489 | .fa-life-buoy:before, 1490 | .fa-life-saver:before, 1491 | .fa-support:before, 1492 | .fa-life-ring:before { 1493 | content: "\f1cd"; 1494 | } 1495 | .fa-circle-o-notch:before { 1496 | content: "\f1ce"; 1497 | } 1498 | .fa-ra:before, 1499 | .fa-rebel:before { 1500 | content: "\f1d0"; 1501 | } 1502 | .fa-ge:before, 1503 | .fa-empire:before { 1504 | content: "\f1d1"; 1505 | } 1506 | .fa-git-square:before { 1507 | content: "\f1d2"; 1508 | } 1509 | .fa-git:before { 1510 | content: "\f1d3"; 1511 | } 1512 | .fa-hacker-news:before { 1513 | content: "\f1d4"; 1514 | } 1515 | .fa-tencent-weibo:before { 1516 | content: "\f1d5"; 1517 | } 1518 | .fa-qq:before { 1519 | content: "\f1d6"; 1520 | } 1521 | .fa-wechat:before, 1522 | .fa-weixin:before { 1523 | content: "\f1d7"; 1524 | } 1525 | .fa-send:before, 1526 | .fa-paper-plane:before { 1527 | content: "\f1d8"; 1528 | } 1529 | .fa-send-o:before, 1530 | .fa-paper-plane-o:before { 1531 | content: "\f1d9"; 1532 | } 1533 | .fa-history:before { 1534 | content: "\f1da"; 1535 | } 1536 | .fa-genderless:before, 1537 | .fa-circle-thin:before { 1538 | content: "\f1db"; 1539 | } 1540 | .fa-header:before { 1541 | content: "\f1dc"; 1542 | } 1543 | .fa-paragraph:before { 1544 | content: "\f1dd"; 1545 | } 1546 | .fa-sliders:before { 1547 | content: "\f1de"; 1548 | } 1549 | .fa-share-alt:before { 1550 | content: "\f1e0"; 1551 | } 1552 | .fa-share-alt-square:before { 1553 | content: "\f1e1"; 1554 | } 1555 | .fa-bomb:before { 1556 | content: "\f1e2"; 1557 | } 1558 | .fa-soccer-ball-o:before, 1559 | .fa-futbol-o:before { 1560 | content: "\f1e3"; 1561 | } 1562 | .fa-tty:before { 1563 | content: "\f1e4"; 1564 | } 1565 | .fa-binoculars:before { 1566 | content: "\f1e5"; 1567 | } 1568 | .fa-plug:before { 1569 | content: "\f1e6"; 1570 | } 1571 | .fa-slideshare:before { 1572 | content: "\f1e7"; 1573 | } 1574 | .fa-twitch:before { 1575 | content: "\f1e8"; 1576 | } 1577 | .fa-yelp:before { 1578 | content: "\f1e9"; 1579 | } 1580 | .fa-newspaper-o:before { 1581 | content: "\f1ea"; 1582 | } 1583 | .fa-wifi:before { 1584 | content: "\f1eb"; 1585 | } 1586 | .fa-calculator:before { 1587 | content: "\f1ec"; 1588 | } 1589 | .fa-paypal:before { 1590 | content: "\f1ed"; 1591 | } 1592 | .fa-google-wallet:before { 1593 | content: "\f1ee"; 1594 | } 1595 | .fa-cc-visa:before { 1596 | content: "\f1f0"; 1597 | } 1598 | .fa-cc-mastercard:before { 1599 | content: "\f1f1"; 1600 | } 1601 | .fa-cc-discover:before { 1602 | content: "\f1f2"; 1603 | } 1604 | .fa-cc-amex:before { 1605 | content: "\f1f3"; 1606 | } 1607 | .fa-cc-paypal:before { 1608 | content: "\f1f4"; 1609 | } 1610 | .fa-cc-stripe:before { 1611 | content: "\f1f5"; 1612 | } 1613 | .fa-bell-slash:before { 1614 | content: "\f1f6"; 1615 | } 1616 | .fa-bell-slash-o:before { 1617 | content: "\f1f7"; 1618 | } 1619 | .fa-trash:before { 1620 | content: "\f1f8"; 1621 | } 1622 | .fa-copyright:before { 1623 | content: "\f1f9"; 1624 | } 1625 | .fa-at:before { 1626 | content: "\f1fa"; 1627 | } 1628 | .fa-eyedropper:before { 1629 | content: "\f1fb"; 1630 | } 1631 | .fa-paint-brush:before { 1632 | content: "\f1fc"; 1633 | } 1634 | .fa-birthday-cake:before { 1635 | content: "\f1fd"; 1636 | } 1637 | .fa-area-chart:before { 1638 | content: "\f1fe"; 1639 | } 1640 | .fa-pie-chart:before { 1641 | content: "\f200"; 1642 | } 1643 | .fa-line-chart:before { 1644 | content: "\f201"; 1645 | } 1646 | .fa-lastfm:before { 1647 | content: "\f202"; 1648 | } 1649 | .fa-lastfm-square:before { 1650 | content: "\f203"; 1651 | } 1652 | .fa-toggle-off:before { 1653 | content: "\f204"; 1654 | } 1655 | .fa-toggle-on:before { 1656 | content: "\f205"; 1657 | } 1658 | .fa-bicycle:before { 1659 | content: "\f206"; 1660 | } 1661 | .fa-bus:before { 1662 | content: "\f207"; 1663 | } 1664 | .fa-ioxhost:before { 1665 | content: "\f208"; 1666 | } 1667 | .fa-angellist:before { 1668 | content: "\f209"; 1669 | } 1670 | .fa-cc:before { 1671 | content: "\f20a"; 1672 | } 1673 | .fa-shekel:before, 1674 | .fa-sheqel:before, 1675 | .fa-ils:before { 1676 | content: "\f20b"; 1677 | } 1678 | .fa-meanpath:before { 1679 | content: "\f20c"; 1680 | } 1681 | .fa-buysellads:before { 1682 | content: "\f20d"; 1683 | } 1684 | .fa-connectdevelop:before { 1685 | content: "\f20e"; 1686 | } 1687 | .fa-dashcube:before { 1688 | content: "\f210"; 1689 | } 1690 | .fa-forumbee:before { 1691 | content: "\f211"; 1692 | } 1693 | .fa-leanpub:before { 1694 | content: "\f212"; 1695 | } 1696 | .fa-sellsy:before { 1697 | content: "\f213"; 1698 | } 1699 | .fa-shirtsinbulk:before { 1700 | content: "\f214"; 1701 | } 1702 | .fa-simplybuilt:before { 1703 | content: "\f215"; 1704 | } 1705 | .fa-skyatlas:before { 1706 | content: "\f216"; 1707 | } 1708 | .fa-cart-plus:before { 1709 | content: "\f217"; 1710 | } 1711 | .fa-cart-arrow-down:before { 1712 | content: "\f218"; 1713 | } 1714 | .fa-diamond:before { 1715 | content: "\f219"; 1716 | } 1717 | .fa-ship:before { 1718 | content: "\f21a"; 1719 | } 1720 | .fa-user-secret:before { 1721 | content: "\f21b"; 1722 | } 1723 | .fa-motorcycle:before { 1724 | content: "\f21c"; 1725 | } 1726 | .fa-street-view:before { 1727 | content: "\f21d"; 1728 | } 1729 | .fa-heartbeat:before { 1730 | content: "\f21e"; 1731 | } 1732 | .fa-venus:before { 1733 | content: "\f221"; 1734 | } 1735 | .fa-mars:before { 1736 | content: "\f222"; 1737 | } 1738 | .fa-mercury:before { 1739 | content: "\f223"; 1740 | } 1741 | .fa-transgender:before { 1742 | content: "\f224"; 1743 | } 1744 | .fa-transgender-alt:before { 1745 | content: "\f225"; 1746 | } 1747 | .fa-venus-double:before { 1748 | content: "\f226"; 1749 | } 1750 | .fa-mars-double:before { 1751 | content: "\f227"; 1752 | } 1753 | .fa-venus-mars:before { 1754 | content: "\f228"; 1755 | } 1756 | .fa-mars-stroke:before { 1757 | content: "\f229"; 1758 | } 1759 | .fa-mars-stroke-v:before { 1760 | content: "\f22a"; 1761 | } 1762 | .fa-mars-stroke-h:before { 1763 | content: "\f22b"; 1764 | } 1765 | .fa-neuter:before { 1766 | content: "\f22c"; 1767 | } 1768 | .fa-facebook-official:before { 1769 | content: "\f230"; 1770 | } 1771 | .fa-pinterest-p:before { 1772 | content: "\f231"; 1773 | } 1774 | .fa-whatsapp:before { 1775 | content: "\f232"; 1776 | } 1777 | .fa-server:before { 1778 | content: "\f233"; 1779 | } 1780 | .fa-user-plus:before { 1781 | content: "\f234"; 1782 | } 1783 | .fa-user-times:before { 1784 | content: "\f235"; 1785 | } 1786 | .fa-hotel:before, 1787 | .fa-bed:before { 1788 | content: "\f236"; 1789 | } 1790 | .fa-viacoin:before { 1791 | content: "\f237"; 1792 | } 1793 | .fa-train:before { 1794 | content: "\f238"; 1795 | } 1796 | .fa-subway:before { 1797 | content: "\f239"; 1798 | } 1799 | .fa-medium:before { 1800 | content: "\f23a"; 1801 | } 1802 | -------------------------------------------------------------------------------- /src/main/webapp/css/style.css: -------------------------------------------------------------------------------- 1 | .angular-portlet .fa { 2 | color: gray; 3 | /* Fix cursor issue: no hand shown */ 4 | cursor: pointer; 5 | } 6 | 7 | input.ng-invalid { 8 | border: 1px solid red !important; 9 | } 10 | input.ng-valid { 11 | border: 1px solid green !important; 12 | } -------------------------------------------------------------------------------- /src/main/webapp/custom_jsps/html/common/themes/top_head.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://liferay.com/tld/util" prefix="liferay-util" %> 2 | 3 | <%@ page import="com.liferay.portal.kernel.util.StringUtil" %> 4 | 5 | 6 | 7 | 8 | 9 | <% 10 | html = StringUtil.add( 11 | html, 12 | "", 13 | "\n"); 14 | %> 15 | 16 | <%= html %> -------------------------------------------------------------------------------- /src/main/webapp/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planetsizebrain/angular-portlet/7bd23200fa472cfaf2c54a8959a3009a9bab8c38/src/main/webapp/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/main/webapp/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planetsizebrain/angular-portlet/7bd23200fa472cfaf2c54a8959a3009a9bab8c38/src/main/webapp/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/webapp/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planetsizebrain/angular-portlet/7bd23200fa472cfaf2c54a8959a3009a9bab8c38/src/main/webapp/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/webapp/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planetsizebrain/angular-portlet/7bd23200fa472cfaf2c54a8959a3009a9bab8c38/src/main/webapp/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/webapp/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planetsizebrain/angular-portlet/7bd23200fa472cfaf2c54a8959a3009a9bab8c38/src/main/webapp/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/main/webapp/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/planetsizebrain/angular-portlet/7bd23200fa472cfaf2c54a8959a3009a9bab8c38/src/main/webapp/img/icon.png -------------------------------------------------------------------------------- /src/main/webapp/js/angular-translate-loader-url.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-translate - v2.5.2 - 2014-12-10 3 | * http://github.com/angular-translate/angular-translate 4 | * Copyright (c) 2014 ; Licensed MIT 5 | */ 6 | angular.module('pascalprecht.translate') 7 | /** 8 | * @ngdoc object 9 | * @name pascalprecht.translate.$translateUrlLoader 10 | * @requires $q 11 | * @requires $http 12 | * 13 | * @description 14 | * Creates a loading function for a typical dynamic url pattern: 15 | * "locale.php?lang=en_US", "locale.php?lang=de_DE", "locale.php?language=nl_NL" etc. 16 | * Prefixing the specified url, the current requested, language id will be applied 17 | * with "?{queryParameter}={key}". 18 | * Using this service, the response of these urls must be an object of 19 | * key-value pairs. 20 | * 21 | * @param {object} options Options object, which gets the url, key and 22 | * optional queryParameter ('lang' is used by default). 23 | */ 24 | .factory('$translateUrlLoader', ['$q', '$http', function ($q, $http) { 25 | 26 | return function (options) { 27 | 28 | if (!options || !options.url) { 29 | throw new Error('Couldn\'t use urlLoader since no url is given!'); 30 | } 31 | 32 | var deferred = $q.defer(), 33 | requestParams = {}; 34 | 35 | requestParams[options.queryParameter || 'lang'] = options.key; 36 | 37 | $http(angular.extend({ 38 | url: options.url, 39 | params: requestParams, 40 | method: 'GET' 41 | }, options.$http)).success(function (data) { 42 | deferred.resolve(data); 43 | }).error(function (data) { 44 | deferred.reject(options.key); 45 | }); 46 | 47 | return deferred.promise; 48 | }; 49 | }]); 50 | -------------------------------------------------------------------------------- /src/main/webapp/js/controller/AddController.js: -------------------------------------------------------------------------------- 1 | angular.module('app.controllers'). 2 | controller('AddCtrl', ['$scope', '$rootScope', 'bookmarkFactory', '$state', '$stateParams', 3 | function($scope, $rootScope, bookmarkFactory, $state, $stateParams) { 4 | 5 | console.log("Add new bookmark..."); 6 | 7 | $scope.model = { 8 | currentBookmark: {} 9 | }; 10 | 11 | $scope.store = function() { 12 | bookmarkFactory.addBookmark($scope.model.currentBookmark).then(function(result) { 13 | console.log("Added new bookmark: " + $scope.model.currentBookmark.name); 14 | 15 | Liferay.fire('reloadBookmarks', { portletId: $scope.portletId }); 16 | $state.go('list'); 17 | }); 18 | }; 19 | } 20 | ] 21 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/controller/DetailController.js: -------------------------------------------------------------------------------- 1 | angular.module('app.controllers'). 2 | controller('DetailCtrl', ['$scope', '$rootScope', 'bookmarkFactory', '$state', '$stateParams', 3 | function($scope, $rootScope, bookmarkFactory, $state, $stateParams) { 4 | 5 | console.log("Show detail for bookmark: " + $stateParams.bookmark.entryId); 6 | 7 | $scope.model = { 8 | currentBookmark: $stateParams.bookmark 9 | }; 10 | 11 | $scope.save = function() { 12 | bookmarkFactory.saveBookmark($scope.model.currentBookmark).then(function(result) { 13 | Liferay.fire('reloadBookmarks', { portletId: $scope.portletId }); 14 | $state.go('list'); 15 | }); 16 | } 17 | } 18 | ] 19 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/controller/Init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('app.controllers', []); -------------------------------------------------------------------------------- /src/main/webapp/js/controller/ListController.js: -------------------------------------------------------------------------------- 1 | // Define scope in this way to avoid 'Unknown provider' problem 2 | // https://groups.google.com/forum/#!msg/angular/_EMeX_Dci2U/xQuDwWadCrsJ 3 | angular.module('app.controllers'). 4 | controller("ListCtrl", ['$scope', '$rootScope', '$http', '$timeout', 'bookmarkFactory', '$stateParams', 5 | function($scope, $rootScope, $http, $timeout, bookmarkFactory, $stateParams) { 6 | 7 | $scope.model = {}; 8 | 9 | $scope.remove = function(bookmark) { 10 | bookmarkFactory.deleteBookmark(bookmark).then(function(result) { 11 | Liferay.fire('reloadBookmarks', { portletId: $scope.portletId }); 12 | $scope.load(); 13 | }); 14 | }; 15 | 16 | $scope.load = function() { 17 | $timeout(function() { 18 | bookmarkFactory.getBookmarks().then(function(bookmarks) { 19 | $scope.model.bookmarks = bookmarks; 20 | }); 21 | }); 22 | }; 23 | 24 | Liferay.on('reloadBookmarks', function(event) { 25 | console.log("Reload event", event.portletId, $scope.portletId); 26 | 27 | // Filter out event if we triggered it in this portlet instance 28 | if (event.portletId != $scope.portletId) { 29 | console.log("RELOAD!"); 30 | $scope.load(); 31 | } 32 | }); 33 | 34 | $scope.load(); 35 | } 36 | ] 37 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/directive/Init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module("app.directives", []); -------------------------------------------------------------------------------- /src/main/webapp/js/directive/LiferayDirective.js: -------------------------------------------------------------------------------- 1 | // A custom directive that used a partial HTML template to output some Liferay data 2 | angular.module('app.directives'). 3 | directive('liferay', ['url', 4 | function(url) { 5 | var directive = {}; 6 | 7 | directive.restrict = 'E'; 8 | directive.templateUrl = url.createRenderUrl('liferay'); 9 | 10 | return directive; 11 | } 12 | ] 13 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/jcs-auto-validate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * angular-auto-validate - v1.16.22 - 2015-02-19 3 | * https://github.com/jonsamwell/angular-auto-validate 4 | * Copyright (c) 2015 Jon Samwell (http://www.jonsamwell.com) 5 | */ 6 | (function (angular) { 7 | 'use strict'; 8 | 9 | angular.module('jcs-autoValidate', []); 10 | }(angular)); 11 | 12 | (function (angular) { 13 | 'use strict'; 14 | 15 | angular.module('jcs-autoValidate') 16 | .provider('validator', [ 17 | function () { 18 | var elementStateModifiers = {}, 19 | enableValidElementStyling = true, 20 | enableInvalidElementStyling = true, 21 | validationEnabled = true, 22 | 23 | toBoolean = function (value) { 24 | var v; 25 | if (value && value.length !== 0) { 26 | v = value.toLowerCase(); 27 | value = !(v === 'f' || v === '0' || v === 'false'); 28 | } else { 29 | value = false; 30 | } 31 | 32 | return value; 33 | }, 34 | 35 | getAttributeValue = function (el, attrName) { 36 | var val; 37 | 38 | if (el !== undefined) { 39 | val = el.attr(attrName) || el.attr('data-' + attrName); 40 | } 41 | 42 | return val; 43 | }, 44 | 45 | attributeExists = function (el, attrName) { 46 | var exists; 47 | 48 | if (el !== undefined) { 49 | exists = el.attr(attrName) !== undefined || el.attr('data-' + attrName) !== undefined; 50 | } 51 | 52 | return exists; 53 | }, 54 | 55 | getBooleanAttributeValue = function (el, attrName) { 56 | return toBoolean(getAttributeValue(el, attrName)); 57 | }, 58 | 59 | validElementStylingEnabled = function (el) { 60 | return enableValidElementStyling && !getBooleanAttributeValue(el, 'disable-valid-styling'); 61 | }, 62 | 63 | invalidElementStylingEnabled = function (el) { 64 | return enableInvalidElementStyling && !getBooleanAttributeValue(el, 'disable-invalid-styling'); 65 | }; 66 | 67 | /** 68 | * @ngdoc function 69 | * @name validator#enable 70 | * @methodOf validator 71 | * 72 | * @description 73 | * By default auto validate will validate all forms and elements with an ngModel directive on. By 74 | * setting enabled to false you will explicitly have to opt in to enable validation on forms and child 75 | * elements. 76 | * 77 | * Note: this can be overridden by add the 'auto-validate-enabled="true/false' attribute to a form. 78 | * 79 | * Example: 80 | * 81 | * app.config(function (validator) { 82 | * validator.enable(false); 83 | * }); 84 | * 85 | * 86 | * @param {Boolean} isEnabled true to enable, false to disable. 87 | */ 88 | this.enable = function (isEnabled) { 89 | validationEnabled = isEnabled; 90 | }; 91 | 92 | /** 93 | * @ngdoc function 94 | * @name validator#isEnabled 95 | * @methodOf validator 96 | * 97 | * @description 98 | * Returns true if the library is enabeld. 99 | * 100 | * @return {Boolean} true if enabled, otherwise false. 101 | */ 102 | this.isEnabled = function () { 103 | return validationEnabled; 104 | }; 105 | 106 | /** 107 | * @ngdoc function 108 | * @name validator#setDefaultElementModifier 109 | * @methodOf validator 110 | * 111 | * @description 112 | * Sets the default element modifier that will be used by the validator 113 | * to change an elements UI state. Please ensure the modifier has been registered 114 | * before setting it as default. 115 | * 116 | * Note: this can be changed by setting the 117 | * element modifier attribute on the input element 'data-element-modifier="myCustomModifier"' 118 | * 119 | * Example: 120 | * 121 | * app.config(function (validator) { 122 | * validator.setDefaultElementModifier('myCustomModifier'); 123 | * }); 124 | * 125 | * 126 | * @param {string} key The key name of the modifier. 127 | */ 128 | this.setDefaultElementModifier = function (key) { 129 | if (elementStateModifiers[key] === undefined) { 130 | throw new Error('Element modifier not registered: ' + key); 131 | } 132 | 133 | this.defaultElementModifier = key; 134 | }; 135 | 136 | /** 137 | * @ngdoc function 138 | * @name validator#registerDomModifier 139 | * @methodOf validator 140 | * 141 | * @description 142 | * Registers an object that adheres to the elementModifier interface and is 143 | * able to modifier an elements dom so that appears valid / invalid for a specific 144 | * scenario i.e. the Twitter Bootstrap css framework, Foundation CSS framework etc. 145 | * 146 | * Example: 147 | * 148 | * app.config(function (validator) { 149 | * validator.registerDomModifier('customDomModifier', { 150 | * makeValid: function (el) { 151 | * el.removeClass(el, 'invalid'); 152 | * el.addClass(el, 'valid'); 153 | * }, 154 | * makeInvalid: function (el, err, domManipulator) { 155 | * el.removeClass(el, 'valid'); 156 | * el.addClass(el, 'invalid'); 157 | * } 158 | * }); 159 | * }); 160 | * 161 | * 162 | * @param {string} key The key name of the modifier 163 | * @param {object} modifier An object which implements the elementModifier interface 164 | */ 165 | this.registerDomModifier = function (key, modifier) { 166 | elementStateModifiers[key] = modifier; 167 | }; 168 | 169 | /** 170 | * @ngdoc function 171 | * @name validator#setErrorMessageResolver 172 | * @methodOf validator 173 | * 174 | * @description 175 | * Registers an object that adheres to the elementModifier interface and is 176 | * able to modifier an elements dom so that appears valid / invalid for a specific 177 | * scenario i.e. the Twitter Bootstrap css framework, Foundation CSS framework etc. 178 | * 179 | * Example: 180 | * 181 | * app.config(function (validator) { 182 | * validator.setErrorMessageResolver(function (errorKey, el) { 183 | * var defer = $q.defer(); 184 | * // resolve the correct error from the given key and resolve the returned promise. 185 | * return defer.promise(); 186 | * }); 187 | * }); 188 | * 189 | * 190 | * @param {function} resolver A method that returns a promise with the resolved error message in. 191 | */ 192 | this.setErrorMessageResolver = function (resolver) { 193 | this.errorMessageResolver = resolver; 194 | }; 195 | 196 | /** 197 | * @ngdoc function 198 | * @name validator#getErrorMessage 199 | * @methodOf validator 200 | * 201 | * @description 202 | * Resolves the error message for the given error type. 203 | * 204 | * @param {String} errorKey The error type. 205 | * @param {Element} el The UI element that is the focus of the error. 206 | * It is provided as the error message may need information from the element i.e. ng-min (the min allowed value). 207 | */ 208 | this.getErrorMessage = function (errorKey, el) { 209 | var defer; 210 | if (this.errorMessageResolver === undefined) { 211 | throw new Error('Please set an error message resolver via the setErrorMessageResolver function before attempting to resolve an error message.'); 212 | } 213 | 214 | if (attributeExists(el, 'disable-validation-message')) { 215 | defer = angular.injector(['ng']).get('$q').defer(); 216 | defer.resolve(''); 217 | return defer.promise; 218 | } else { 219 | return this.errorMessageResolver(errorKey, el); 220 | } 221 | }; 222 | 223 | /** 224 | * @ngdoc function 225 | * @name validator#setValidElementStyling 226 | * @methodOf validator 227 | * 228 | * @description 229 | * Globally enables valid element visual styling. This is enabled by default. 230 | * 231 | * @param {Boolean} enabled True to enable style otherwise false. 232 | */ 233 | this.setValidElementStyling = function (enabled) { 234 | enableValidElementStyling = enabled; 235 | }; 236 | 237 | 238 | /** 239 | * @ngdoc function 240 | * @name validator#setInvalidElementStyling 241 | * @methodOf validator 242 | * 243 | * @description 244 | * Globally enables invalid element visual styling. This is enabled by default. 245 | * 246 | * @param {Boolean} enabled True to enable style otherwise false. 247 | */ 248 | this.setInvalidElementStyling = function (enabled) { 249 | enableInvalidElementStyling = enabled; 250 | }; 251 | 252 | this.getDomModifier = function (el) { 253 | var modifierKey = (el !== undefined ? el.attr('element-modifier') : this.defaultElementModifier) || 254 | (el !== undefined ? el.attr('data-element-modifier') : this.defaultElementModifier) || 255 | this.defaultElementModifier; 256 | 257 | if (modifierKey === undefined) { 258 | throw new Error('Please set a default dom modifier via the setDefaultElementModifier method on the validator class.'); 259 | } 260 | 261 | return elementStateModifiers[modifierKey]; 262 | }; 263 | 264 | this.makeValid = function (el) { 265 | if (validElementStylingEnabled(el)) { 266 | this.getDomModifier(el).makeValid(el); 267 | } else { 268 | this.makeDefault(el); 269 | } 270 | }; 271 | 272 | this.makeInvalid = function (el, errorMsg) { 273 | if (invalidElementStylingEnabled(el)) { 274 | this.getDomModifier(el).makeInvalid(el, errorMsg); 275 | } else { 276 | this.makeDefault(el); 277 | } 278 | }; 279 | 280 | this.makeDefault = function (el) { 281 | var dm = this.getDomModifier(el); 282 | if (dm.makeDefault) { 283 | dm.makeDefault(el); 284 | } 285 | }; 286 | 287 | this.$get = [ 288 | function () { 289 | return this; 290 | } 291 | ]; 292 | } 293 | ]); 294 | }(angular)); 295 | 296 | (function (angular) { 297 | 'use strict'; 298 | 299 | angular.module('jcs-autoValidate') 300 | .factory('bootstrap3ElementModifier', [ 301 | '$log', 302 | function ($log) { 303 | var reset = function (el) { 304 | angular.forEach(el.find('span'), function (spanEl) { 305 | spanEl = angular.element(spanEl); 306 | if (spanEl.hasClass('error-msg') || spanEl.hasClass('form-control-feedback') || spanEl.hasClass('control-feedback')) { 307 | spanEl.remove(); 308 | } 309 | }); 310 | 311 | el.removeClass('has-success has-error has-feedback'); 312 | }, 313 | findWithClassElementAsc = function (el, klass) { 314 | var retuenEl, 315 | parent = el; 316 | for (var i = 0; i <= 3; i += 1) { 317 | if (parent !== undefined && parent.hasClass(klass)) { 318 | retuenEl = parent; 319 | break; 320 | } else if (parent !== undefined) { 321 | parent = parent.parent(); 322 | } 323 | } 324 | 325 | return retuenEl; 326 | }, 327 | 328 | findWithClassElementDesc = function (el, klass) { 329 | var child; 330 | for (var i = 0; i < el.children.length; i += 1) { 331 | child = el.children[i]; 332 | if (child !== undefined && angular.element(child).hasClass(klass)) { 333 | break; 334 | } else if (child.children !== undefined) { 335 | child = findWithClassElementDesc(child, klass); 336 | if (child.length > 0) { 337 | break; 338 | } 339 | } 340 | } 341 | 342 | return angular.element(child); 343 | }, 344 | 345 | findFormGroupElement = function (el) { 346 | return findWithClassElementAsc(el, 'form-group'); 347 | }, 348 | 349 | findInputGroupElement = function (el) { 350 | return findWithClassElementDesc(el, 'input-group'); 351 | }, 352 | 353 | insertAfter = function (referenceNode, newNode) { 354 | referenceNode[0].parentNode.insertBefore(newNode[0], referenceNode[0].nextSibling); 355 | }, 356 | 357 | /** 358 | * @ngdoc property 359 | * @name bootstrap3ElementModifier#addValidationStateIcons 360 | * @propertyOf bootstrap3ElementModifier 361 | * @returns {bool} True if an state icon will be added to the element in the valid and invalid control 362 | * states. The default is false. 363 | */ 364 | addValidationStateIcons = false, 365 | 366 | /** 367 | * @ngdoc function 368 | * @name bootstrap3ElementModifier#enableValidationStateIcons 369 | * @methodOf bootstrap3ElementModifier 370 | * 371 | * @description 372 | * Makes an element appear invalid by apply an icon to the input element. 373 | * 374 | * @param {bool} enable - True to enable the icon otherwise false. 375 | */ 376 | enableValidationStateIcons = function (enable) { 377 | addValidationStateIcons = enable; 378 | }, 379 | 380 | /** 381 | * @ngdoc function 382 | * @name bootstrap3ElementModifier#makeValid 383 | * @methodOf bootstrap3ElementModifier 384 | * 385 | * @description 386 | * Makes an element appear valid by apply bootstrap 3 specific styles and child elements. If the service 387 | * property 'addValidationStateIcons' is true it will also append validation glyphicon to the element. 388 | * See: http://getbootstrap.com/css/#forms-control-validation 389 | * 390 | * @param {Element} el - The input control element that is the target of the validation. 391 | */ 392 | makeValid = function (el) { 393 | var frmGroupEl = findFormGroupElement(el), 394 | inputGroupEl; 395 | 396 | if (frmGroupEl) { 397 | reset(frmGroupEl); 398 | inputGroupEl = findInputGroupElement(frmGroupEl[0]); 399 | frmGroupEl.addClass('has-success ' + (inputGroupEl.length > 0 ? '' : 'has-feedback')); 400 | if (addValidationStateIcons) { 401 | var iconElText = ''; 402 | if (inputGroupEl.length > 0) { 403 | iconElText = iconElText.replace('form-', ''); 404 | iconElText = '' + iconElText + '' + errorMsg + ''), 429 | inputGroupEl; 430 | 431 | if (frmGroupEl) { 432 | reset(frmGroupEl); 433 | inputGroupEl = findInputGroupElement(frmGroupEl[0]); 434 | frmGroupEl.addClass('has-error ' + (inputGroupEl.length > 0 ? '' : 'has-feedback')); 435 | insertAfter(inputGroupEl.length > 0 ? inputGroupEl : el, helpTextEl); 436 | if (addValidationStateIcons) { 437 | var iconElText = ''; 438 | if (inputGroupEl.length > 0) { 439 | iconElText = iconElText.replace('form-', ''); 440 | iconElText = '' + iconElText + ' 0 ? parentColumn : el, el); 762 | }, 763 | 764 | /** 765 | * @ngdoc function 766 | * @name foundation5ElementModifier#makeInvalid 767 | * @methodOf foundation5ElementModifier 768 | * 769 | * @description 770 | * Makes an element appear invalid by apply Foundation 5 specific styles and child elements. 771 | * See: http://foundation.zurb.com/docs/components/forms.html 772 | * 773 | * @param {Element} el - The input control element that is the target of the validation. 774 | */ 775 | makeInvalid = function (el, errorMsg) { 776 | var parentColumn = findParentColumn(el), 777 | helpTextEl; 778 | reset(parentColumn || el, el); 779 | el.addClass('error'); 780 | if (parentColumn) { 781 | helpTextEl = angular.element('' + errorMsg + ''); 782 | parentColumn.append(helpTextEl); 783 | } 784 | }, 785 | 786 | /** 787 | * @ngdoc function 788 | * @name foundation5ElementModifier#makeDefault 789 | * @methodOf foundation5ElementModifier 790 | * 791 | * @description 792 | * Makes an element appear in its default visual state by apply foundation 5 specific styles and child elements. 793 | * 794 | * @param {Element} el - The input control element that is the target of the validation. 795 | */ 796 | makeDefault = function (el) { 797 | makeValid(el); 798 | }; 799 | 800 | return { 801 | makeValid: makeValid, 802 | makeInvalid: makeInvalid, 803 | makeDefault: makeDefault, 804 | key: 'foundation5' 805 | }; 806 | } 807 | ]); 808 | }(angular)); 809 | 810 | (function (angular) { 811 | 'use strict'; 812 | 813 | 814 | angular.module('jcs-autoValidate') 815 | .factory('jcs-elementUtils', [ 816 | function () { 817 | var isElementVisible = function (el) { 818 | return el[0].offsetWidth > 0 && el[0].offsetHeight > 0; 819 | }; 820 | 821 | return { 822 | isElementVisible: isElementVisible 823 | }; 824 | } 825 | ]); 826 | 827 | angular.module('jcs-autoValidate') 828 | .factory('validationManager', [ 829 | 'validator', 830 | 'jcs-elementUtils', 831 | function (validator, elementUtils) { 832 | var elementTypesToValidate = ['input', 'textarea', 'select', 'form'], 833 | 834 | elementIsVisible = function (el) { 835 | return elementUtils.isElementVisible(el); 836 | }, 837 | 838 | /** 839 | * Only validate if the element is present, it is visible 840 | * it is either a valid user input control (input, select, textare, form) or 841 | * it is a custom control register by the developer. 842 | * @param el 843 | * @returns {boolean} true to indicate it should be validated 844 | */ 845 | shouldValidateElement = function (el) { 846 | return el && 847 | el.length > 0 && 848 | elementIsVisible(el) && 849 | (elementTypesToValidate.indexOf(el[0].nodeName.toLowerCase()) > -1 || 850 | el[0].hasAttribute('register-custom-form-control')); 851 | }, 852 | 853 | /** 854 | * @ngdoc validateElement 855 | * @name validation#validateElement 856 | * @param {object} modelCtrl holds the information about the element e.g. $invalid, $valid 857 | * @param {Boolean} forceValidation if set to true forces the validation even if the element is pristine 858 | * @description 859 | * Validate the form element and make invalid/valid element model status. 860 | */ 861 | validateElement = function (modelCtrl, el, forceValidation) { 862 | var isValid = true, 863 | needsValidation = modelCtrl.$pristine === false || forceValidation, 864 | errorType, 865 | findErrorType = function ($errors) { 866 | var keepGoing = true, 867 | errorTypeToReturn; 868 | angular.forEach($errors, function (status, errortype) { 869 | if (keepGoing && status) { 870 | keepGoing = false; 871 | errorTypeToReturn = errortype; 872 | } 873 | }); 874 | 875 | return errorTypeToReturn; 876 | }; 877 | 878 | if ((forceValidation || (shouldValidateElement(el) && modelCtrl && needsValidation))) { 879 | isValid = !modelCtrl.$invalid; 880 | 881 | if (isValid) { 882 | validator.makeValid(el); 883 | } else { 884 | errorType = findErrorType(modelCtrl.$error); 885 | 886 | if (errorType === undefined) { 887 | // we have a weird situation some users are encountering where a custom control 888 | // is valid but the ngModel is report it isn't and thus no valid error type can be found 889 | isValid = true; 890 | } else { 891 | validator.getErrorMessage(errorType, el).then(function (errorMsg) { 892 | validator.makeInvalid(el, errorMsg); 893 | }); 894 | } 895 | } 896 | } 897 | 898 | return isValid; 899 | }, 900 | 901 | resetElement = function (element) { 902 | validator.makeDefault(element); 903 | }, 904 | 905 | resetForm = function (frmElement) { 906 | angular.forEach(frmElement[0], function (element) { 907 | var controller, 908 | ctrlElement = angular.element(element); 909 | controller = ctrlElement.controller('ngModel'); 910 | 911 | if (controller !== undefined) { 912 | if (ctrlElement[0].nodeName.toLowerCase() === 'form') { 913 | // we probably have a sub form 914 | resetForm(ctrlElement); 915 | } else { 916 | controller.$setPristine(); 917 | } 918 | } 919 | }); 920 | }, 921 | 922 | validateForm = function (frmElement) { 923 | var frmValid = true, 924 | frmCtrl = frmElement ? angular.element(frmElement).controller('form') : undefined, 925 | processElement = function (ctrlElement, force) { 926 | var controller, isValid; 927 | ctrlElement = angular.element(ctrlElement); 928 | controller = ctrlElement.controller('ngModel'); 929 | 930 | if (controller !== undefined && (force || shouldValidateElement(ctrlElement))) { 931 | if (ctrlElement[0].nodeName.toLowerCase() === 'form') { 932 | // we probably have a sub form 933 | validateForm(ctrlElement); 934 | } else { 935 | isValid = validateElement(controller, ctrlElement, true); 936 | frmValid = frmValid && isValid; 937 | } 938 | } 939 | }; 940 | 941 | if (frmElement === undefined || (frmCtrl !== undefined && frmCtrl.disableDynamicValidation)) { 942 | return frmElement !== undefined; 943 | } 944 | 945 | // IE8 holds the child controls collection in the all property 946 | // Firefox in the elements and chrome as a child iterator 947 | angular.forEach((frmElement[0].all || frmElement[0].elements) || frmElement[0], function (ctrlElement) { 948 | processElement(ctrlElement); 949 | }); 950 | 951 | // If you have a custom form control that should be validated i.e. 952 | // ... it will not be part of the forms 953 | // HTMLFormControlsCollection and thus won't be included in the above element iteration although 954 | // it will be on the Angular FormController (if it has a name attribute). So adding the directive 955 | // register-custom-form-control="" to the control root and autoValidate will include it in this 956 | // iteration. 957 | if (frmElement[0].customHTMLFormControlsCollection) { 958 | angular.forEach(frmElement[0].customHTMLFormControlsCollection, function (ctrlElement) { 959 | // need to force the validation as the element might not be a known form input type 960 | // so the normal validation process will ignore it. 961 | processElement(ctrlElement, true); 962 | }); 963 | } 964 | 965 | return frmValid; 966 | }, 967 | 968 | setElementValidationError = function (element, errorMsgKey, errorMsg) { 969 | if (errorMsgKey) { 970 | validator.getErrorMessage(errorMsgKey, element).then(function (msg) { 971 | validator.makeInvalid(element, msg); 972 | }); 973 | } else { 974 | validator.makeInvalid(element, errorMsg); 975 | } 976 | }; 977 | 978 | return { 979 | setElementValidationError: setElementValidationError, 980 | validateElement: validateElement, 981 | validateForm: validateForm, 982 | resetElement: resetElement, 983 | resetForm: resetForm 984 | }; 985 | } 986 | ]); 987 | }(angular)); 988 | 989 | (function (angular) { 990 | 'use strict'; 991 | 992 | angular.module('jcs-autoValidate').directive('form', [ 993 | 'validationManager', 994 | function (validationManager) { 995 | return { 996 | restrict: 'E', 997 | link: function (scope, el) { 998 | el.on('reset', function () { 999 | validationManager.resetForm(el); 1000 | }); 1001 | 1002 | scope.$on('$destroy', function () { 1003 | el.off('reset'); 1004 | }); 1005 | } 1006 | }; 1007 | } 1008 | ]); 1009 | }(angular)); 1010 | 1011 | (function (angular) { 1012 | 'use strict'; 1013 | 1014 | angular.module('jcs-autoValidate').directive('registerCustomFormControl', [ 1015 | 1016 | function () { 1017 | var findParentForm = function (el) { 1018 | var parent = el; 1019 | for (var i = 0; i <= 10; i += 1) { 1020 | if (parent !== undefined && parent.nodeName.toLowerCase() === 'form') { 1021 | break; 1022 | } else if (parent !== undefined) { 1023 | parent = angular.element(parent).parent()[0]; 1024 | } 1025 | } 1026 | 1027 | return parent; 1028 | }; 1029 | 1030 | return { 1031 | restrict: 'A', 1032 | link: function (scope, element) { 1033 | var frmEl = findParentForm(element.parent()[0]); 1034 | if (frmEl) { 1035 | frmEl.customHTMLFormControlsCollection = frmEl.customHTMLFormControlsCollection || []; 1036 | frmEl.customHTMLFormControlsCollection.push(element[0]); 1037 | } 1038 | } 1039 | }; 1040 | } 1041 | ]); 1042 | }(angular)); 1043 | 1044 | (function (angular) { 1045 | 'use strict'; 1046 | 1047 | angular.module('jcs-autoValidate').directive('form', [ 1048 | 'validator', 1049 | function (validator) { 1050 | return { 1051 | restrict: 'E', 1052 | require: 'form', 1053 | compile: function () { 1054 | return { 1055 | pre: function (scope, element, attrs, ctrl) { 1056 | ctrl.disableDynamicValidation = !validator.isEnabled(); 1057 | if (attrs.disableDynamicValidation !== undefined) { 1058 | ctrl.disableDynamicValidation = attrs.disableDynamicValidation === undefined || attrs.disableDynamicValidation === '' || attrs.disableDynamicValidation === 'true'; 1059 | } 1060 | } 1061 | }; 1062 | } 1063 | }; 1064 | } 1065 | ]); 1066 | }(angular)); 1067 | 1068 | (function (angular) { 1069 | 'use strict'; 1070 | 1071 | angular.module('jcs-autoValidate').config(['$provide', 1072 | function ($provide) { 1073 | $provide.decorator('ngSubmitDirective', [ 1074 | '$delegate', 1075 | '$parse', 1076 | 'validationManager', 1077 | function ($delegate, $parse, validationManager) { 1078 | $delegate[0].compile = function ($element, attr) { 1079 | var fn = $parse(attr.ngSubmit), 1080 | force = attr.ngSubmitForce === 'true'; 1081 | return function (scope, element) { 1082 | element.on('submit', function (event) { 1083 | scope.$apply(function () { 1084 | if (validationManager.validateForm(element) || force === true) { 1085 | fn(scope, { 1086 | $event: event 1087 | }); 1088 | } 1089 | }); 1090 | }); 1091 | }; 1092 | }; 1093 | 1094 | return $delegate; 1095 | } 1096 | ]); 1097 | } 1098 | ]); 1099 | }(angular)); 1100 | 1101 | (function (angular) { 1102 | 'use strict'; 1103 | 1104 | angular.module('jcs-autoValidate').config(['$provide', 1105 | function ($provide) { 1106 | $provide.decorator('ngModelDirective', [ 1107 | '$timeout', 1108 | '$delegate', 1109 | 'validationManager', 1110 | 'jcs-debounce', 1111 | function ($timeout, $delegate, validationManager, debounce) { 1112 | var directive = $delegate[0], 1113 | link = directive.link || directive.compile; 1114 | 1115 | directive.compile = function (el) { 1116 | return function (scope, element, attrs, ctrls) { 1117 | var ngModelCtrl = ctrls[0], 1118 | frmCtrl = ctrls[1], 1119 | supportsNgModelOptions = angular.version.major >= 1 && angular.version.minor >= 3, 1120 | ngModelOptions = attrs.ngModelOptions === undefined ? undefined : scope.$eval(attrs.ngModelOptions), 1121 | setValidity = ngModelCtrl.$setValidity, 1122 | setPristine = ngModelCtrl.$setPristine, 1123 | setValidationState = debounce.debounce(function () { 1124 | validationManager.validateElement(ngModelCtrl, element); 1125 | }, 100); 1126 | 1127 | // in the RC of 1.3 there is no directive.link only the directive.compile which 1128 | // needs to be invoked to get at the link functions. 1129 | if (supportsNgModelOptions && angular.isFunction(link)) { 1130 | link = link(el); 1131 | } 1132 | 1133 | if (link.pre) { 1134 | link.pre.apply(this, arguments); 1135 | ngModelOptions = ngModelCtrl.$options === undefined ? undefined : ngModelCtrl.$options; 1136 | } 1137 | 1138 | if (attrs.formnovalidate === undefined || (frmCtrl !== undefined && frmCtrl.disableDynamicValidation === false)) { 1139 | if (supportsNgModelOptions || ngModelOptions === undefined || ngModelOptions.updateOn === undefined || ngModelOptions.updateOn === '') { 1140 | ngModelCtrl.$setValidity = function (validationErrorKey, isValid) { 1141 | setValidity.call(ngModelCtrl, validationErrorKey, isValid); 1142 | setValidationState(); 1143 | }; 1144 | } else { 1145 | element.on(ngModelOptions.updateOn, function () { 1146 | setValidationState(); 1147 | }); 1148 | 1149 | scope.$on('$destroy', function () { 1150 | element.off(ngModelOptions.updateOn); 1151 | }); 1152 | } 1153 | 1154 | // We override this so we can reset the element state when it is called. 1155 | ngModelCtrl.$setPristine = function () { 1156 | setPristine.call(ngModelCtrl); 1157 | validationManager.resetElement(element); 1158 | }; 1159 | 1160 | ngModelCtrl.autoValidated = true; 1161 | } 1162 | 1163 | if (link.post) { 1164 | link.post.apply(this, arguments); 1165 | } else { 1166 | link.apply(this, arguments); 1167 | } 1168 | 1169 | ngModelCtrl.setExternalValidation = function (errorMsgKey, errorMessage, addToModelErrors) { 1170 | if (addToModelErrors) { 1171 | if (ngModelCtrl.$error) { 1172 | ngModelCtrl.$error[errorMsgKey] = false; 1173 | } else { 1174 | ngModelCtrl.$errors[errorMsgKey] = false; 1175 | } 1176 | } 1177 | 1178 | validationManager.setElementValidationError(element, errorMsgKey, errorMessage); 1179 | }; 1180 | 1181 | ngModelCtrl.removeExternalValidation = function (errorMsgKey, addToModelErrors) { 1182 | if (addToModelErrors) { 1183 | if (ngModelCtrl.$error) { 1184 | ngModelCtrl.$error[errorMsgKey] = true; 1185 | } else { 1186 | ngModelCtrl.$errors[errorMsgKey] = true; 1187 | } 1188 | } 1189 | 1190 | validationManager.resetElement(element); 1191 | }; 1192 | 1193 | if (frmCtrl) { 1194 | frmCtrl.setExternalValidation = function (modelProperty, errorMsgKey, errorMessageOverride, addToModelErrors) { 1195 | var success = false; 1196 | if (frmCtrl[modelProperty]) { 1197 | frmCtrl[modelProperty].setExternalValidation(errorMsgKey, errorMessageOverride, addToModelErrors); 1198 | success = true; 1199 | } 1200 | 1201 | return success; 1202 | }; 1203 | 1204 | frmCtrl.removeExternalValidation = function (modelProperty, errorMsgKey, errorMessageOverride, addToModelErrors) { 1205 | var success = false; 1206 | if (frmCtrl[modelProperty]) { 1207 | frmCtrl[modelProperty].removeExternalValidation(errorMsgKey, addToModelErrors); 1208 | success = true; 1209 | } 1210 | 1211 | return success; 1212 | }; 1213 | } 1214 | }; 1215 | }; 1216 | 1217 | return $delegate; 1218 | } 1219 | ]); 1220 | } 1221 | ]); 1222 | }(angular)); 1223 | 1224 | (function (angular) { 1225 | 'use strict'; 1226 | 1227 | angular.module('jcs-autoValidate') 1228 | .run([ 1229 | 'validator', 1230 | 'defaultErrorMessageResolver', 1231 | 'bootstrap3ElementModifier', 1232 | 'foundation5ElementModifier', 1233 | function (validator, defaultErrorMessageResolver, bootstrap3ElementModifier, foundation5ElementModifier) { 1234 | validator.setErrorMessageResolver(defaultErrorMessageResolver.resolve); 1235 | validator.registerDomModifier(bootstrap3ElementModifier.key, bootstrap3ElementModifier); 1236 | validator.registerDomModifier(foundation5ElementModifier.key, foundation5ElementModifier); 1237 | validator.setDefaultElementModifier(bootstrap3ElementModifier.key); 1238 | } 1239 | ]); 1240 | }(angular)); 1241 | -------------------------------------------------------------------------------- /src/main/webapp/js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function bootstrap(id, portletId) { 4 | 5 | var app = angular.module(id, ["ui.router", "app.factories", "app.controllers", "app.directives", "pascalprecht.translate", "jcs-autoValidate"]); 6 | 7 | app.run(['$rootScope', 'releaseFactory', 'url', 'validator', 'i18nErrorMessageResolver', 8 | function($rootScope, releaseFactory, url, validator, i18nErrorMessageResolver) { 9 | 10 | // Calculate the actual portlet ID and put that in the root scope for all to use. 11 | $rootScope.portletId = portletId.substr(1, portletId.length - 2); 12 | 13 | // Put some Liferay stuff on the root scope to be used by our custom directive. 14 | $rootScope.liferay = { 15 | token: Liferay.authToken, 16 | companyId: Liferay.ThemeDisplay.getCompanyId(), 17 | loggedIn: Liferay.ThemeDisplay.isSignedIn() 18 | }; 19 | 20 | releaseFactory.getRelease().then(function(release) { 21 | $rootScope.liferay.release = release; 22 | }); 23 | 24 | // We're using the $stateChangeStart event as a point to intervene in the navigation so 25 | // that we can correct the templateUrl values that are used, because the need to be able 26 | // to be resolved as a valid portlet resource URL. We use the existence of a dummy property 27 | // to see if the URL is already fixed or not. 28 | $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) { 29 | if (!toState.hasOwnProperty('fixedUrl')) { 30 | toState.templateUrl = url.createRenderUrl(toState.templateUrl); 31 | toState.fixedUrl = true; 32 | } 33 | }); 34 | 35 | validator.setErrorMessageResolver(i18nErrorMessageResolver.resolve); 36 | } 37 | ]); 38 | 39 | app.config(['$urlRouterProvider', '$stateProvider', '$locationProvider', '$translateProvider', 'urlProvider', 40 | function($urlRouterProvider, $stateProvider, $locationProvider, $translateProvider, urlProvider) { 41 | 42 | urlProvider.setPid(portletId); 43 | 44 | $translateProvider.useUrlLoader(urlProvider.$get().createResourceUrl('language', 'locale', Liferay.ThemeDisplay.getBCP47LanguageId())); 45 | $translateProvider.preferredLanguage(Liferay.ThemeDisplay.getBCP47LanguageId()); 46 | 47 | // No # when routing! 48 | $locationProvider.html5Mode(true); 49 | 50 | var currentPageUrl = Liferay.ThemeDisplay.getLayoutURL(); 51 | currentPageUrl = currentPageUrl.substr(currentPageUrl.indexOf('/', 10)); 52 | $urlRouterProvider.otherwise(currentPageUrl); 53 | 54 | $stateProvider 55 | .state("list", { 56 | url: currentPageUrl, 57 | templateUrl: 'list', 58 | controller: 'ListCtrl' 59 | }) 60 | .state("detail", { 61 | templateUrl: 'detail', 62 | params: { 63 | bookmark: {} 64 | }, 65 | controller: 'DetailCtrl' 66 | }) 67 | .state("add", { 68 | templateUrl: 'add', 69 | controller: 'AddCtrl' 70 | }); 71 | } 72 | ]); 73 | 74 | // Don't use 'ng-app', but bootstrap Angular ourselves, so we can control naming and 75 | // scoping when portlet is instanceable: 76 | // http://stackoverflow.com/questions/18571301/angularjs-multiple-ng-app-within-a-page 77 | // http://docs.angularjs.org/guide/bootstrap 78 | // 79 | // We use the element ID, something that is based on the portlet instance ID, as the 80 | // Angular module ID. 81 | angular.bootstrap(document.getElementById(id), [id]); 82 | } -------------------------------------------------------------------------------- /src/main/webapp/js/service/ErrorMessageResolver.js: -------------------------------------------------------------------------------- 1 | angular.module("app.factories"). 2 | 3 | // A custom error message resolver that provides custom error messages defined 4 | // in the Liferay/portlet language bundles. Uses a prefix key so they don't clash 5 | // with other Liferay keys and reuses the code from the library itself to 6 | // replace the {0} values. 7 | factory('i18nErrorMessageResolver', ['$q', '$translate', 8 | function($q, $translate) { 9 | 10 | var resolve = function(errorType, el) { 11 | var defer = $q.defer(); 12 | 13 | var prefix = "validation."; 14 | $translate(prefix + errorType).then(function(message) { 15 | if (el && el.attr) { 16 | try { 17 | var parameters = []; 18 | var parameter = el.attr('ng-' + errorType); 19 | if (parameter === undefined) { 20 | parameter = el.attr('data-ng-' + errorType) || el.attr(errorType); 21 | } 22 | 23 | parameters.push(parameter || ''); 24 | 25 | message = message.format(parameters); 26 | } catch (e) {} 27 | } 28 | 29 | defer.resolve(message); 30 | }); 31 | 32 | return defer.promise; 33 | }; 34 | 35 | return { 36 | resolve: resolve 37 | }; 38 | } 39 | ] 40 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/service/Init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module("app.factories", []); -------------------------------------------------------------------------------- /src/main/webapp/js/service/bookmarkFactory.js: -------------------------------------------------------------------------------- 1 | angular.module("app.factories"). 2 | factory('bookmarkFactory', function($q) { 3 | var getBookmarks = function() { 4 | var deferred = $q.defer(); 5 | 6 | Liferay.Service( 7 | '/bookmarksentry/get-group-entries', 8 | { 9 | groupId: Liferay.ThemeDisplay.getScopeGroupId(), 10 | start: -1, 11 | end: -1 12 | }, 13 | // Calling like this, with additional function param, calls it async 14 | // http://www.liferay.com/de/community/forums/-/message_boards/view_message/12303402 15 | // Using promise ($q) to make this work 16 | function(obj) { 17 | deferred.resolve(obj); 18 | } 19 | ); 20 | 21 | return deferred.promise; 22 | }; 23 | 24 | var saveBookmark = function(bookmark) { 25 | var deferred = $q.defer(); 26 | 27 | Liferay.Service( 28 | '/bookmarksentry/update-entry', 29 | { 30 | entryId: bookmark.entryId, 31 | groupId: bookmark.groupId, 32 | folderId: bookmark.folderId, 33 | name: bookmark.name, 34 | url: bookmark.url, 35 | description: bookmark.description, 36 | serviceContext: {} 37 | }, 38 | function(obj) { 39 | deferred.resolve(obj); 40 | } 41 | ); 42 | 43 | return deferred.promise; 44 | }; 45 | 46 | var addBookmark = function(bookmark) { 47 | var deferred = $q.defer(); 48 | 49 | Liferay.Service( 50 | '/bookmarksentry/add-entry', 51 | { 52 | groupId: Liferay.ThemeDisplay.getScopeGroupId(), 53 | folderId: 0, 54 | name: bookmark.name, 55 | url: bookmark.url, 56 | description: bookmark.description, 57 | serviceContext: {} 58 | }, 59 | function(obj) { 60 | deferred.resolve(obj); 61 | } 62 | ); 63 | 64 | return deferred.promise; 65 | }; 66 | 67 | var deleteBookmark = function(bookmark) { 68 | var deferred = $q.defer(); 69 | 70 | Liferay.Service( 71 | '/bookmarksentry/delete-entry', 72 | { 73 | entryId: bookmark.entryId 74 | }, 75 | function(obj) { 76 | deferred.resolve(obj); 77 | } 78 | ); 79 | 80 | return deferred.promise; 81 | }; 82 | 83 | return { 84 | getBookmarks: getBookmarks, 85 | saveBookmark: saveBookmark, 86 | addBookmark: addBookmark, 87 | deleteBookmark: deleteBookmark 88 | }; 89 | } 90 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/service/releaseFactory.js: -------------------------------------------------------------------------------- 1 | angular.module("app.factories"). 2 | factory('releaseFactory', ['$q', '$http', 'url', 3 | function($q, $http, url) { 4 | var getRelease = function() { 5 | var deferred = $q.defer(); 6 | var resource = url.createResourceUrl("release", "releaseId", "1"); 7 | 8 | $http.get(resource.toString()).success(function(data, status, headers, config) { 9 | // this callback will be called asynchronously when the response is available 10 | deferred.resolve(data); 11 | }); 12 | 13 | return deferred.promise; 14 | }; 15 | 16 | return { 17 | getRelease: getRelease 18 | }; 19 | }] 20 | ); -------------------------------------------------------------------------------- /src/main/webapp/js/service/urlFactory.js: -------------------------------------------------------------------------------- 1 | angular.module("app.factories"). 2 | provider('url', function() { 3 | // In the provider function, you cannot inject any service or factory. 4 | // This can only be done at the "$get" method. 5 | 6 | this.pid = ''; 7 | 8 | this.$get = function() { 9 | var pid = this.pid; 10 | return { 11 | createRenderUrl: function(page) { 12 | var resourceURL = Liferay.PortletURL.createRenderURL(); 13 | resourceURL.setPortletId(pid); 14 | resourceURL.setPortletMode('view'); 15 | resourceURL.setWindowState('exclusive'); 16 | resourceURL.setParameter('jspPage', '/partials/' + page + '.html'); 17 | 18 | return resourceURL.toString(); 19 | }, 20 | createResourceUrl: function(resourceId, paramName, paramValue) { 21 | // Need to set both resourceId and portletId for request to work 22 | // resourceId can be used to check and distinguish on server side 23 | var resourceURL = Liferay.PortletURL.createResourceURL(); 24 | resourceURL.setPortletId(pid); 25 | resourceURL.setResourceId(resourceId); 26 | resourceURL.setParameter(paramName, paramValue); 27 | 28 | return resourceURL.toString(); 29 | } 30 | } 31 | }; 32 | 33 | this.setPid = function(pid) { 34 | this.pid = pid.substr(1, pid.length - 2); 35 | }; 36 | } 37 | ); -------------------------------------------------------------------------------- /src/main/webapp/jsp/edit.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %> 2 | <%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %> 3 | <%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %> 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/webapp/jsp/view.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet" %> 2 | <%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %> 3 | <%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %> 4 | 5 | 6 | 7 | 8 | You need to be logged in to use this portlet 9 | 10 | 11 | 12 | 16 | 17 | 18 | // Pass namespace so Angular can be correctly namespaced 19 | // https://www.liferay.com/community/forums/-/message_boards/view_message/18488646 20 | bootstrap('main', ''); 21 | 22 | -------------------------------------------------------------------------------- /src/main/webapp/partials/add.html: -------------------------------------------------------------------------------- 1 | add.new.bookmark 2 | 3 | 4 | label.name 5 | 6 | 7 | 8 | label.description 9 | 10 | 11 | 12 | label.url 13 | 14 | 15 | action.submit 16 | action.cancel 17 | -------------------------------------------------------------------------------- /src/main/webapp/partials/detail.html: -------------------------------------------------------------------------------- 1 | detail.for.bookmark 2 | 3 | 4 | label.name 5 | 6 | 7 | 8 | label.description 9 | 10 | 11 | 12 | label.url 13 | 14 | 15 | action.submit 16 | action.cancel 17 | -------------------------------------------------------------------------------- /src/main/webapp/partials/liferay.html: -------------------------------------------------------------------------------- 1 | portlet.id {{portletId}} 2 | liferay.version {{liferay.release._buildNumber}} | auth.token {{liferay.token}} | company.id: {{liferay.companyId}} -------------------------------------------------------------------------------- /src/main/webapp/partials/list.html: -------------------------------------------------------------------------------- 1 | bookmarks 2 | 3 | 4 | 5 | 6 | table.id 7 | table.name 8 | table.actions 9 | 10 | 11 | 12 | 13 | {{bookmark.entryId}} 14 | {{bookmark.name}} 15 | | | 16 | 17 | 18 | 19 | 20 | action.add 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------
81 | * app.config(function (validator) { 82 | * validator.enable(false); 83 | * }); 84 | *
121 | * app.config(function (validator) { 122 | * validator.setDefaultElementModifier('myCustomModifier'); 123 | * }); 124 | *
148 | * app.config(function (validator) { 149 | * validator.registerDomModifier('customDomModifier', { 150 | * makeValid: function (el) { 151 | * el.removeClass(el, 'invalid'); 152 | * el.addClass(el, 'valid'); 153 | * }, 154 | * makeInvalid: function (el, err, domManipulator) { 155 | * el.removeClass(el, 'valid'); 156 | * el.addClass(el, 'invalid'); 157 | * } 158 | * }); 159 | * }); 160 | *
181 | * app.config(function (validator) { 182 | * validator.setErrorMessageResolver(function (errorKey, el) { 183 | * var defer = $q.defer(); 184 | * // resolve the correct error from the given key and resolve the returned promise. 185 | * return defer.promise(); 186 | * }); 187 | * }); 188 | *